@vutolabs/analytics
Version:
Vuto Analytics - instant, Stripe-native analytics for SaaS apps built with Next.js or React.
1 lines • 18.2 kB
Source Map (JSON)
{"version":3,"sources":["../../src/nextjs/index.tsx","../../src/react/index.tsx","../../package.json","../../src/queue.ts","../../src/utils.ts","../../src/generic.ts","../../src/nextjs/utils.ts"],"sourcesContent":["\"use client\";\nimport React, { Suspense, type ReactNode } from \"react\";\nimport { Analytics as AnalyticsScript } from \"../react\";\nimport type { AnalyticsProps } from \"../types\";\nimport { useRoute } from \"./utils\";\n\nfunction AnalyticsComponent(props: AnalyticsProps): ReactNode {\n const { route, path } = useRoute();\n return <AnalyticsScript path={path} route={route} {...props} framework=\"next\" />;\n}\n\n/**\n * Adds the `<Analytics />` component to the page body and starts tracking page views.\n *\n * @param [props.mode] - The mode to use for the analytics script. Defaults to `auto`.\n * - `auto` - Automatically detect the environment. Uses `production` if the environment cannot be determined.\n * - `production` - Always use the production script. (Sends events to the server)\n * - `development` - Always use the development script. (Logs events to the console)\n *\n * @example\n * ```tsx\n * import { Analytics } from '@vutolabs/analytics/next';\n *\n * export default function RootLayout({ children }: { children: React.ReactNode }) {\n * return (\n * <html lang=\"en\">\n * <head>\n * <title>Next.js</title>\n * </head>\n * <body>\n * {children}\n * <Analytics />\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport function Analytics(props: AnalyticsProps): null {\n // Because of incompatible types between ReactNode in React 19 and React 18 we return null (which is also what we render)\n return (\n <Suspense fallback={null}>\n <AnalyticsComponent {...props} />\n </Suspense>\n ) as never;\n}\n\nexport type { AnalyticsProps };\n","\"use client\";\nimport { useEffect } from \"react\";\nimport { inject, track } from \"../generic\";\nimport type { AnalyticsProps } from \"../types\";\n\n/**\n * Injects the Analytics script into the page head and starts tracking page views.\n * @param [props] - Analytics options.\n * @param [props.mode] - The mode to use for the analytics script. Defaults to `auto`.\n * - `auto` - Automatically detect the environment. Uses `production` if the environment cannot be determined.\n * - `production` - Always use the production script. (Sends events to the server)\n * - `development` - Always use the development script. (Logs events to the console)\n */\nfunction Analytics(\n props: AnalyticsProps & {\n framework?: string;\n route?: string | null;\n path?: string | null;\n }\n): null {\n const config: AnalyticsProps & {\n framework: string;\n } = {\n framework: props.framework || \"react\",\n mode: props.mode,\n projectId: props.projectId,\n };\n\n useEffect(() => {\n inject(config);\n }, []);\n\n return null;\n}\n\nexport { track, Analytics };\nexport type { AnalyticsProps };\n","{\n \"name\": \"@vutolabs/analytics\",\n \"version\": \"0.0.0-alpha.4\",\n \"description\": \"Vuto Analytics - instant, Stripe-native analytics for SaaS apps built with Next.js or React.\",\n \"keywords\": [\n \"analytics\",\n \"saas\",\n \"stripe\",\n \"nextjs\",\n \"react\",\n \"tracking\",\n \"mrr\",\n \"conversion\"\n ],\n \"license\": \"MPL-2.0\",\n \"author\": \"Kelvin Dupont\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/vutolabs/analytics\"\n },\n \"homepage\": \"https://vuto.dev\",\n \"bugs\": {\n \"url\": \"https://github.com/vutolabs/analytics/issues\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"files\": [\n \"dist\"\n ],\n \"exports\": {\n \"./package.json\": \"./package.json\",\n \".\": {\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\"\n },\n \"./next\": {\n \"import\": \"./dist/nextjs/index.mjs\",\n \"require\": \"./dist/nextjs/index.js\",\n \"types\": \"./dist/nextjs/index.d.ts\"\n },\n \"./react\": {\n \"import\": \"./dist/react/index.mjs\",\n \"require\": \"./dist/react/index.js\",\n \"types\": \"./dist/react/index.d.ts\"\n }\n },\n \"typesVersions\": {\n \"*\": {\n \"*\": [\n \"dist/index.d.ts\"\n ],\n \"next\": [\n \"dist/nextjs/index.d.ts\"\n ],\n \"react\": [\n \"dist/react/index.d.ts\"\n ]\n }\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"type-check\": \"tsc --noEmit\"\n },\n \"devDependencies\": {\n \"@swc/core\": \"^1.9.2\",\n \"@types/node\": \"^22.9.0\",\n \"@types/react\": \"^18.3.20\",\n \"@types/react-dom\": \"^19.1.2\",\n \"next\": \"^15.3.0\",\n \"tsup\": \"8.3.5\",\n \"typescript\": \"^5.1.6\"\n },\n \"peerDependencies\": {\n \"next\": \">=13\",\n \"react\": \"^18 || ^19\"\n },\n \"peerDependenciesMeta\": {\n \"next\": {\n \"optional\": false\n },\n \"react\": {\n \"optional\": false\n }\n }\n}\n","export const initQueue = (): void => {\n // initialize va until script is loaded\n if (window.va) return;\n\n window.va = function a(...params): void {\n (window.vaq = window.vaq || []).push(params);\n };\n};\n","import type { AllowedPropertyValues, AnalyticsProps, Mode } from \"./types\";\n\nexport function isBrowser(): boolean {\n return typeof window !== \"undefined\";\n}\n\nfunction detectEnvironment(): \"development\" | \"production\" {\n try {\n const env = process.env.NODE_ENV;\n if (env === \"development\" || env === \"test\") {\n return \"development\";\n }\n } catch (e) {\n // do nothing, this is okay\n }\n return \"production\";\n}\n\nexport function setMode(mode: Mode = \"auto\"): void {\n if (mode === \"auto\") {\n window.vam = detectEnvironment();\n return;\n }\n\n window.vam = mode;\n}\n\nexport function getMode(): Mode {\n const mode = isBrowser() ? window.vam : detectEnvironment();\n return mode || \"production\";\n}\n\nexport function isProduction(): boolean {\n return getMode() === \"production\";\n}\n\nexport function isDevelopment(): boolean {\n return getMode() === \"development\";\n}\n\nfunction removeKey(key: string, { [key]: _, ...rest }): Record<string, unknown> {\n return rest;\n}\n\nexport function parseProperties(\n properties: Record<string, unknown> | undefined,\n options: {\n strip?: boolean;\n }\n): Error | Record<string, AllowedPropertyValues> | undefined {\n if (!properties) return undefined;\n let props = properties;\n const errorProperties: string[] = [];\n for (const [key, value] of Object.entries(properties)) {\n if (typeof value === \"object\" && value !== null) {\n if (options.strip) {\n props = removeKey(key, props);\n } else {\n errorProperties.push(key);\n }\n }\n }\n\n if (errorProperties.length > 0 && !options.strip) {\n throw Error(\n `The following properties are not valid: ${errorProperties.join(\n \", \"\n )}. Only strings, numbers, booleans, and null are allowed.`\n );\n }\n return props as Record<string, AllowedPropertyValues>;\n}\n\nexport function computeRoute(\n pathname: string | null,\n pathParams: Record<string, string | string[]> | null\n): string | null {\n if (!pathname || !pathParams) {\n return pathname;\n }\n\n let result = pathname;\n try {\n const entries = Object.entries(pathParams);\n // simple keys must be handled first\n for (const [key, value] of entries) {\n if (!Array.isArray(value)) {\n const matcher = turnValueToRegExp(value);\n if (matcher.test(result)) {\n result = result.replace(matcher, `/[${key}]`);\n }\n }\n }\n // array values next\n for (const [key, value] of entries) {\n if (Array.isArray(value)) {\n const matcher = turnValueToRegExp(value.join(\"/\"));\n if (matcher.test(result)) {\n result = result.replace(matcher, `/[...${key}]`);\n }\n }\n }\n return result;\n } catch (e) {\n return pathname;\n }\n}\n\nfunction turnValueToRegExp(value: string): RegExp {\n return new RegExp(`/${escapeRegExp(value)}(?=[/?#]|$)`);\n}\n\nfunction escapeRegExp(string: string): string {\n return string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nexport function getScriptSrc(props: AnalyticsProps & { basePath?: string }): string {\n if (isDevelopment()) {\n // return \"https://vuto-backend-production.up.railway.app/analytics/script.dev.js\";\n return \"http://localhost:3000/analytics/script.js\";\n }\n\n return \"https://vuto-backend-production.up.railway.app/analytics/script.js\";\n}\n","import { name as packageName, version } from \"../package.json\";\nimport { initQueue } from \"./queue\";\nimport type { AllowedPropertyValues, AnalyticsProps, FlagsDataInput } from \"./types\";\nimport {\n isBrowser,\n parseProperties,\n setMode,\n isDevelopment,\n isProduction,\n computeRoute,\n getScriptSrc,\n} from \"./utils\";\n\n/**\n * Injects the Vuto Web Analytics script into the page head and starts tracking page views. Read more in our [documentation](https://vercel.com/docs/concepts/analytics/package).\n * @param [props] - Analytics options.\n * @param [props.mode] - The mode to use for the analytics script. Defaults to `auto`.\n * - `auto` - Automatically detect the environment. Uses `production` if the environment cannot be determined.\n * - `production` - Always use the production script. (Sends events to the server)\n * - `development` - Always use the development script. (Logs events to the console)\n */\nfunction inject(\n props: AnalyticsProps & {\n framework?: string;\n basePath?: string;\n }\n): void {\n if (!isBrowser()) return;\n\n setMode(props.mode);\n\n initQueue();\n\n const src = getScriptSrc(props);\n\n if (document.head.querySelector(`script[src*=\"${src}\"]`)) return;\n\n const script = document.createElement(\"script\");\n script.src = src;\n script.defer = true;\n script.dataset.sdkn = packageName + (props.framework ? `/${props.framework}` : \"\");\n script.dataset.sdkv = version;\n\n // Définir l'endpoint de collecte des données avec l'URL complète\n const scriptOrigin = new URL(src).origin;\n script.dataset.endpoint = `${scriptOrigin}/api/analytics`;\n script.dataset.projectId = props.projectId;\n\n script.onerror = (): void => {\n const errorMessage = isDevelopment()\n ? \"Please check if any ad blockers are enabled and try again.\"\n : \"Please check if the project is enabled in the Vuto Web Analytics dashboard.\";\n\n // eslint-disable-next-line no-console -- Logging to console is intentional\n console.log(`[Vuto Web Analytics] Failed to load script from ${src}. ${errorMessage}`);\n };\n\n document.head.appendChild(script);\n}\n\n/**\n * Tracks a custom event. Please refer to the [documentation](https://vercel.com/docs/concepts/analytics/custom-events) for more information on custom events.\n * @param name - The name of the event.\n * * Examples: `Purchase`, `Click Button`, or `Play Video`.\n * @param [properties] - Additional properties of the event. Nested objects are not supported. Allowed values are `string`, `number`, `boolean`, and `null`.\n */\nfunction track(\n name: string,\n properties?: Record<string, AllowedPropertyValues>,\n options?: {\n flags?: FlagsDataInput;\n }\n): void {\n if (!isBrowser()) {\n const msg =\n \"[Vuto Web Analytics] Please import `track` from `@vutolabs/analytics/server` when using this function in a server environment\";\n\n if (isProduction()) {\n // eslint-disable-next-line no-console -- Show warning in production\n console.warn(msg);\n } else {\n throw new Error(msg);\n }\n\n return;\n }\n\n if (!properties) {\n (window.va as ((event: string, properties?: unknown) => void) | undefined)?.(\"event\", {\n name,\n options,\n });\n return;\n }\n\n try {\n const props = parseProperties(properties, {\n strip: isProduction(),\n });\n\n (window.va as ((event: string, properties?: unknown) => void) | undefined)?.(\"event\", {\n name,\n data: props,\n options,\n });\n } catch (err) {\n if (err instanceof Error && isDevelopment()) {\n // eslint-disable-next-line no-console -- Logging to console is intentional\n console.error(err);\n }\n }\n}\n\n/**\n * Identifies a user with their email address. This links the current visitor ID to the provided email.\n * @param email - The email address of the user to identify.\n */\nfunction identify(email: string): void {\n if (!isBrowser()) {\n const msg =\n \"[Vuto Web Analytics] Please import `identify` from `@vutolabs/analytics/server` when using this function in a server environment\";\n\n if (isProduction()) {\n // eslint-disable-next-line no-console -- Show warning in production\n console.warn(msg);\n } else {\n throw new Error(msg);\n }\n\n return;\n }\n\n if (!email || typeof email !== \"string\") {\n if (isDevelopment()) {\n // eslint-disable-next-line no-console -- Logging to console is intentional\n console.error(\n \"[Vuto Web Analytics] Invalid email provided to identify(). Email must be a non-empty string.\"\n );\n }\n return;\n }\n\n // Get the visitor ID from localStorage\n const vid = localStorage.getItem(\"analytics_vid\");\n\n // Using type assertion to ensure compatibility with the va function\n (window.va as ((event: string, properties?: unknown) => void) | undefined)?.(\"identify\", {\n email,\n vid,\n });\n}\n\nexport { inject, track, identify, computeRoute };\nexport type { AnalyticsProps };\n\n// eslint-disable-next-line import/no-default-export -- Default export is intentional\nexport default {\n inject,\n track,\n identify,\n computeRoute,\n};\n","\"use client\";\n/* eslint-disable @typescript-eslint/no-unnecessary-condition -- can be empty in pages router */\nimport { useParams, usePathname, useSearchParams } from \"next/navigation\";\nimport { computeRoute } from \"../utils\";\n\nexport const useRoute = (): {\n route: string | null;\n path: string;\n} => {\n const params = useParams();\n const searchParams = useSearchParams();\n const path = usePathname();\n\n // Until we have route parameters, we don't compute the route\n if (!params) {\n return { route: null, path };\n }\n // in Next.js@13, useParams() could return an empty object for pages router, and we default to searchParams.\n const finalParams = Object.keys(params).length\n ? (params as Record<string, string | string[]>)\n : Object.fromEntries(searchParams.entries());\n return { route: computeRoute(path, finalParams), path };\n};\n\nexport function getBasePath(): string | undefined {\n // !! important !!\n // do not access env variables using process.env[varname]\n // some bundles won't replace the value at build time.\n // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- we can't use optionnal here, it'll break if process does not exist.\n if (typeof process === \"undefined\" || typeof process.env === \"undefined\") {\n return undefined;\n }\n return process.env.NEXT_PUBLIC_VERCEL_OBSERVABILITY_BASEPATH;\n}\n"],"mappings":"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","names":["Analytics"]}