@vutolabs/analytics
Version:
Vuto Analytics - instant, Stripe-native analytics for SaaS apps built with Next.js or React.
1 lines • 14.9 kB
Source Map (JSON)
{"version":3,"sources":["../../src/react/index.tsx","../../package.json","../../src/queue.ts","../../src/utils.ts","../../src/generic.ts"],"sourcesContent":["\"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"],"mappings":"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","names":["name"]}