├── .nvmrc ├── packages ├── web │ ├── .npmignore │ ├── tsconfig.json │ ├── src │ │ ├── queue.ts │ │ ├── vue │ │ │ ├── index.ts │ │ │ ├── utils.ts │ │ │ ├── utils.test.ts │ │ │ └── create-component.ts │ │ ├── nuxt │ │ │ └── index.ts │ │ ├── sveltekit │ │ │ ├── utils.ts │ │ │ ├── utils.test.ts │ │ │ └── index.ts │ │ ├── astro │ │ │ ├── component.ts │ │ │ └── index.astro │ │ ├── react │ │ │ ├── utils.ts │ │ │ ├── utils.test.ts │ │ │ ├── index.tsx │ │ │ └── index.test.tsx │ │ ├── remix │ │ │ ├── index.tsx │ │ │ ├── utils.ts │ │ │ └── utils.test.ts │ │ ├── nextjs │ │ │ ├── index.tsx │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ │ ├── types.ts │ │ ├── generic.test.ts │ │ ├── utils.ts │ │ ├── generic.ts │ │ ├── server │ │ │ └── index.ts │ │ └── utils.test.ts │ ├── jest.config.js │ ├── test.setup.ts │ ├── vitest.config.mts │ ├── tsup.config.js │ ├── README.md │ ├── package.json │ └── LICENSE └── eslint-config │ ├── package.json │ └── index.js ├── README.md ├── apps ├── nuxt │ ├── public │ │ ├── robots.txt │ │ └── favicon.ico │ ├── server │ │ └── tsconfig.json │ ├── pages │ │ ├── index.vue │ │ └── blog │ │ │ └── [category] │ │ │ └── [slug].vue │ ├── tsconfig.json │ ├── nuxt.config.ts │ ├── assets │ │ ├── logo.svg │ │ ├── main.css │ │ └── base.css │ ├── .gitignore │ ├── app.vue │ ├── package.json │ ├── README.md │ └── layouts │ │ └── default.vue ├── sveltekit │ ├── .npmrc │ ├── static │ │ └── favicon.png │ ├── src │ │ ├── routes │ │ │ ├── +page.svelte │ │ │ ├── [slug] │ │ │ │ └── +page.svelte │ │ │ └── +layout.ts │ │ └── app.html │ ├── vite.config.js │ ├── svelte.config.js │ ├── .gitignore │ ├── package.json │ ├── jsconfig.json │ └── README.md ├── nextjs-15 │ ├── .eslintrc.json │ ├── src │ │ └── app │ │ │ ├── favicon.ico │ │ │ ├── blog │ │ │ ├── page.tsx │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ │ ├── fonts │ │ │ ├── GeistVF.woff │ │ │ └── GeistMonoVF.woff │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── page.module.css │ ├── next.config.mjs │ ├── .gitignore │ ├── package.json │ ├── tsconfig.json │ └── README.md ├── nextjs │ ├── jest-setup.ts │ ├── app │ │ ├── blog │ │ │ ├── page.tsx │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── rsc │ │ │ └── page.tsx │ │ ├── api │ │ │ ├── edge │ │ │ │ ├── route.ts │ │ │ │ └── route.test.ts │ │ │ ├── serverless │ │ │ │ ├── route.ts │ │ │ │ └── route.test.ts │ │ │ └── edge-no-context │ │ │ │ ├── route.ts │ │ │ │ └── route.test.ts │ │ ├── layout.tsx │ │ └── server-actions │ │ │ └── page.tsx │ ├── pages │ │ ├── blog-pages │ │ │ └── [slug].tsx │ │ ├── before-send │ │ │ ├── second.tsx │ │ │ └── first.tsx │ │ ├── navigation │ │ │ ├── second.tsx │ │ │ └── first.tsx │ │ ├── api │ │ │ ├── test.ts │ │ │ └── test.test.ts │ │ ├── index.tsx │ │ └── _app.tsx │ ├── jest.config.ts │ ├── .gitignore │ ├── middleware.ts │ ├── tsconfig.json │ ├── e2e │ │ ├── utils.ts │ │ ├── development │ │ │ ├── pageview.spec.ts │ │ │ └── beforeSend.spec.ts │ │ └── production │ │ │ ├── beforeSend.spec.ts │ │ │ └── pageview.spec.ts │ ├── package.json │ └── playwright.config.ts ├── astro │ ├── src │ │ ├── env.d.ts │ │ ├── content │ │ │ └── blog │ │ │ │ ├── henry.md │ │ │ │ └── bruno.md │ │ ├── pages │ │ │ ├── index.astro │ │ │ └── blog │ │ │ │ └── [...slug].astro │ │ └── layouts │ │ │ └── Base.astro │ ├── astro.config.mjs │ ├── tsconfig.json │ ├── .gitignore │ ├── package.json │ ├── public │ │ └── favicon.svg │ └── README.md ├── remix │ ├── .gitignore │ ├── public │ │ └── favicon.ico │ ├── vite.config.ts │ ├── app │ │ ├── routes │ │ │ ├── health.tsx │ │ │ ├── blog.$slug.tsx │ │ │ └── _index.tsx │ │ └── root.tsx │ ├── README.md │ ├── tsconfig.json │ └── package.json └── vue │ ├── public │ └── favicon.ico │ ├── src │ ├── main.js │ ├── assets │ │ ├── logo.svg │ │ ├── main.css │ │ └── base.css │ ├── components │ │ ├── icons │ │ │ ├── IconSupport.vue │ │ │ ├── IconTooling.vue │ │ │ ├── IconCommunity.vue │ │ │ ├── IconDocumentation.vue │ │ │ └── IconEcosystem.vue │ │ └── HelloWorld.vue │ └── App.vue │ ├── jsconfig.json │ ├── vite.config.js │ ├── index.html │ ├── package.json │ ├── .gitignore │ └── README.md ├── .gitignore ├── .github ├── banner.png ├── workflows │ ├── release.yml │ └── quality.yml └── composite-actions │ └── install │ └── action.yml ├── pnpm-workspace.yaml ├── .husky └── pre-commit ├── lint-staged.config.js ├── .prettierignore ├── tsconfig.json ├── package.json └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.21.0 -------------------------------------------------------------------------------- /packages/web/.npmignore: -------------------------------------------------------------------------------- 1 | src -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/web/README.md -------------------------------------------------------------------------------- /apps/nuxt/public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/sveltekit/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /apps/nextjs-15/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next" 3 | } 4 | -------------------------------------------------------------------------------- /apps/nextjs/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /apps/astro/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .vscode 5 | .vercel 6 | -------------------------------------------------------------------------------- /apps/remix/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/analytics/HEAD/.github/banner.png -------------------------------------------------------------------------------- /apps/nuxt/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'examples/*' 4 | - 'apps/*' 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*': 'prettier --write --ignore-unknown', 3 | }; 4 | -------------------------------------------------------------------------------- /apps/nuxt/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/analytics/HEAD/apps/nuxt/public/favicon.ico -------------------------------------------------------------------------------- /apps/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/analytics/HEAD/apps/vue/public/favicon.ico -------------------------------------------------------------------------------- /apps/remix/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/analytics/HEAD/apps/remix/public/favicon.ico -------------------------------------------------------------------------------- /apps/sveltekit/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/analytics/HEAD/apps/sveltekit/static/favicon.png -------------------------------------------------------------------------------- /apps/astro/src/content/blog/henry.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About Henry 3 | slug: henry 4 | --- 5 | 6 | Henry is a gentleman 7 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/analytics/HEAD/apps/nextjs-15/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/astro/src/content/blog/bruno.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About Bruno 3 | slug: bruno 4 | --- 5 | 6 | We don't talk about Bruno 7 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Blog() { 2 | return
Welcome to the blog
; 3 | } 4 | -------------------------------------------------------------------------------- /apps/nextjs/app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Blog() { 2 | return
Welcome to the app-router blog
; 3 | } 4 | -------------------------------------------------------------------------------- /apps/nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /apps/nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /apps/nextjs-15/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/analytics/HEAD/apps/nextjs-15/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /apps/sveltekit/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 |

Welcome to SvelteKit

2 |

Visit this page to know more...

3 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/analytics/HEAD/apps/nextjs-15/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | dist/ 3 | .astro/ 4 | .vercel/ 5 | .nuxt/ 6 | .output/ 7 | .cache/ 8 | build/ 9 | .svelte-kit/ 10 | pnpm-lock.yaml -------------------------------------------------------------------------------- /apps/astro/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | 3 | // https://astro.build/config 4 | export default defineConfig({}); 5 | -------------------------------------------------------------------------------- /apps/nextjs/pages/blog-pages/[slug].tsx: -------------------------------------------------------------------------------- 1 | export default function BlogPage() { 2 | return ( 3 |
4 |

Blog Page

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /apps/vue/src/main.js: -------------------------------------------------------------------------------- 1 | import './assets/main.css'; 2 | 3 | import { createApp } from 'vue'; 4 | import App from './App.vue'; 5 | 6 | createApp(App).mount('#app'); 7 | -------------------------------------------------------------------------------- /apps/vue/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | }, 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext" 5 | }, 6 | "include": ["src", "./test.setup.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /apps/nextjs/pages/before-send/second.tsx: -------------------------------------------------------------------------------- 1 | function Page() { 2 | return ( 3 |
4 |

Second Page

5 |
6 | ); 7 | } 8 | 9 | export default Page; 10 | -------------------------------------------------------------------------------- /apps/nextjs/pages/navigation/second.tsx: -------------------------------------------------------------------------------- 1 | function Page() { 2 | return ( 3 |
4 |

Second Page

5 |
6 | ); 7 | } 8 | 9 | export default Page; 10 | -------------------------------------------------------------------------------- /apps/remix/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { vitePlugin as remix } from '@remix-run/dev'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [remix()], 6 | }); 7 | -------------------------------------------------------------------------------- /apps/astro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base", 3 | "compilerOptions": { 4 | "strictNullChecks": true, 5 | "plugins": [{ "name": "@astrojs/ts-plugin" }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/nuxt/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: '2024-04-03', 4 | devtools: { enabled: true }, 5 | }); 6 | -------------------------------------------------------------------------------- /apps/sveltekit/vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | }); 7 | -------------------------------------------------------------------------------- /apps/sveltekit/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-vercel'; 2 | 3 | /** @type {import('@sveltejs/kit').Config} */ 4 | const config = { 5 | kit: { adapter: adapter() }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vercel/style-guide/typescript", 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "jsx": "react", 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/web/src/queue.ts: -------------------------------------------------------------------------------- 1 | export const initQueue = (): void => { 2 | // initialize va until script is loaded 3 | if (window.va) return; 4 | 5 | window.va = function a(...params): void { 6 | (window.vaq = window.vaq || []).push(params); 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /apps/nextjs/pages/navigation/first.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | function Page() { 4 | return ( 5 |
6 |

First Page

7 | Next 8 |
9 | ); 10 | } 11 | 12 | export default Page; 13 | -------------------------------------------------------------------------------- /apps/nextjs/pages/before-send/first.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | function Page() { 4 | return ( 5 |
6 |

First Page

7 | Next 8 |
9 | ); 10 | } 11 | 12 | export default Page; 13 | -------------------------------------------------------------------------------- /apps/nextjs/app/rsc/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | import { track } from '@vercel/analytics/server'; 3 | 4 | export default async function RSC() { 5 | cookies(); 6 | track('Viewed Experiment'); 7 | 8 | return
I did track a server action on render
; 9 | } 10 | -------------------------------------------------------------------------------- /apps/nuxt/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/vue/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | transform: { 4 | '^.+\\.(t|j)sx?$': '@swc/jest', 5 | }, 6 | coverageReporters: ['text', 'html'], 7 | setupFilesAfterEnv: ['/jest.setup.ts'], 8 | reporters: ['default', 'github-actions'], 9 | }; 10 | -------------------------------------------------------------------------------- /packages/web/test.setup.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach } from 'vitest'; 2 | import '@testing-library/jest-dom/vitest'; 3 | 4 | beforeEach(() => { 5 | // reset dom before each test 6 | const html = document.getElementsByTagName('html')[0]; 7 | if (html) { 8 | html.innerHTML = ''; 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /apps/nextjs/app/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function BlogPage({ params }: { params: { slug: string } }) { 4 | return ( 5 |
6 |

{params.slug}

7 | 8 | Back to blog 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/nuxt/pages/blog/[category]/[slug].vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /apps/sveltekit/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /apps/nextjs/app/api/edge/route.ts: -------------------------------------------------------------------------------- 1 | import { track } from '@vercel/analytics/server'; 2 | 3 | export const runtime = 'edge'; 4 | 5 | export const GET = async function handler() { 6 | await track('Edge Event', { 7 | data: 'edge', 8 | router: 'app', 9 | }); 10 | 11 | return new Response('OK'); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /apps/nextjs/app/api/serverless/route.ts: -------------------------------------------------------------------------------- 1 | import { track } from '@vercel/analytics/server'; 2 | 3 | export const GET = async function handler() { 4 | await track('Serverless Event', { 5 | data: 'serverless', 6 | router: 'app', 7 | }); 8 | return new Response('OK'); 9 | }; 10 | 11 | export const dynamic = 'force-dynamic'; 12 | -------------------------------------------------------------------------------- /apps/sveltekit/src/routes/[slug]/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |

We don't talk about {$page.params.slug}

7 | 8 | track('go-back', { slug: $page.params.slug })} 9 | >Go back 11 | -------------------------------------------------------------------------------- /packages/web/vitest.config.mts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | // eslint-disable-next-line import/no-default-export -- vitest needs a default export. 5 | export default defineConfig({ 6 | test: { 7 | environment: 'jsdom', 8 | setupFiles: './test.setup.ts', 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default async function BlogPage({ 4 | params, 5 | }: { 6 | params: Promise<{ slug: string }>; 7 | }) { 8 | const { slug } = await params; 9 | return ( 10 |
11 |

{slug}

12 | 13 | Back to blog 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/sveltekit/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment'; 2 | import { 3 | injectAnalytics, 4 | type BeforeSendEvent, 5 | } from '@vercel/analytics/sveltekit'; 6 | 7 | injectAnalytics({ 8 | mode: dev ? 'development' : 'production', 9 | beforeSend(event: BeforeSendEvent) { 10 | console.log('beforeSend', event); 11 | return event; 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /apps/vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | 3 | import { defineConfig } from 'vite'; 4 | import vue from '@vitejs/plugin-vue'; 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | '@': fileURLToPath(new URL('./src', import.meta.url)), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/web/src/vue/index.ts: -------------------------------------------------------------------------------- 1 | import type { AnalyticsProps, BeforeSend, BeforeSendEvent } from '../types'; 2 | import { createComponent } from './create-component'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- vue's defineComponent return type is any 5 | export const Analytics = createComponent(); 6 | export type { AnalyticsProps, BeforeSend, BeforeSendEvent }; 7 | -------------------------------------------------------------------------------- /apps/astro/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /apps/vue/src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /packages/web/src/nuxt/index.ts: -------------------------------------------------------------------------------- 1 | import type { AnalyticsProps, BeforeSend, BeforeSendEvent } from '../types'; 2 | import { createComponent } from '../vue/create-component'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- vue's defineComponent return type is any 5 | export const Analytics = createComponent('nuxt'); 6 | export type { AnalyticsProps, BeforeSend, BeforeSendEvent }; 7 | -------------------------------------------------------------------------------- /apps/remix/app/routes/health.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunctionArgs } from '@remix-run/node'; 2 | import { track } from '@vercel/analytics/server'; 3 | 4 | export async function loader({ request }: LoaderFunctionArgs) { 5 | const host = 6 | request.headers.get('X-Forwarded-Host') ?? request.headers.get('host'); 7 | 8 | await track('Health', { host }); 9 | 10 | return new Response('OK'); 11 | } 12 | -------------------------------------------------------------------------------- /apps/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/nuxt/app.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /packages/web/src/vue/utils.ts: -------------------------------------------------------------------------------- 1 | export function getBasePath(): string | undefined { 2 | // !! important !! 3 | // do not access env variables using import.meta.env[varname] 4 | // some bundles won't replace the value at build time. 5 | try { 6 | return import.meta.env.VITE_VERCEL_OBSERVABILITY_BASEPATH as 7 | | string 8 | | undefined; 9 | } catch { 10 | // do nothing 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/web/src/sveltekit/utils.ts: -------------------------------------------------------------------------------- 1 | export function getBasePath(): string | undefined { 2 | // !! important !! 3 | // do not access env variables using import.meta.env[varname] 4 | // some bundles won't replace the value at build time. 5 | try { 6 | return import.meta.env.VITE_VERCEL_OBSERVABILITY_BASEPATH as 7 | | string 8 | | undefined; 9 | } catch { 10 | // do nothing 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/blog/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 | My first blog post  7 | Feature just got released 8 |
9 |
{children}
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/nextjs/app/blog/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 | My first blog post {' '} 7 | Feature just got released 8 |
9 |
{children}
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/sveltekit/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "vite build", 8 | "dev": "vite", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@vercel/analytics": "workspace:*", 13 | "vue": "latest" 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue": "latest", 17 | "vite": "latest" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/astro/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | import Layout from '../layouts/Base.astro'; 4 | 5 | const blogEntries = await getCollection('blog'); 6 | --- 7 | 8 |

Welcome to Astro

9 | 16 |
17 | -------------------------------------------------------------------------------- /apps/nextjs/app/api/edge-no-context/route.ts: -------------------------------------------------------------------------------- 1 | import { track } from '@vercel/analytics/server'; 2 | 3 | export const runtime = 'edge'; 4 | 5 | async function handler(request: Request) { 6 | await track( 7 | 'Edge Event', 8 | { 9 | data: 'edge', 10 | router: 'app', 11 | manual: true, 12 | }, 13 | { 14 | request, 15 | } 16 | ); 17 | 18 | return new Response('OK'); 19 | } 20 | 21 | export const GET = handler; 22 | -------------------------------------------------------------------------------- /apps/nextjs/pages/api/test.ts: -------------------------------------------------------------------------------- 1 | import { track } from '@vercel/analytics/server'; 2 | import { NextFetchEvent, NextRequest } from 'next/server'; 3 | 4 | export const config = { 5 | runtime: 'edge', 6 | }; 7 | 8 | async function handler(request: NextRequest, event: NextFetchEvent) { 9 | track('Pages Api Route', { 10 | runtime: 'edge', 11 | router: 'pages', 12 | }); 13 | 14 | return new Response('OK'); 15 | } 16 | 17 | export default handler; 18 | -------------------------------------------------------------------------------- /apps/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "postinstall": "nuxt prepare", 10 | "preview": "nuxt preview" 11 | }, 12 | "dependencies": { 13 | "@vercel/analytics": "workspace:*", 14 | "nuxt": "^4.2.1", 15 | "vue": "latest", 16 | "vue-router": "latest" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/nextjs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from '@vercel/analytics/next'; 2 | 3 | export const metadata = { 4 | title: 'Next.js', 5 | description: 'Generated by Next.js', 6 | }; 7 | 8 | export default function RootLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return ( 14 | 15 | 16 | 17 | {children} 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/nextjs/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | import nextJest from 'next/jest.js'; 3 | 4 | const createJestConfig = nextJest({ 5 | dir: './', 6 | }); 7 | 8 | const config: Config = { 9 | coverageProvider: 'v8', 10 | testRegex: '\\/.+\\.test\\.tsx?$', 11 | setupFilesAfterEnv: ['/jest-setup.ts'], 12 | transform: { 13 | '^.+\\.(t|j)sx?$': '@swc/jest', 14 | }, 15 | }; 16 | 17 | export default createJestConfig(config); 18 | -------------------------------------------------------------------------------- /apps/vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /apps/remix/README.md: -------------------------------------------------------------------------------- 1 | # Remix Vercel Web Analytics Test 2 | 3 | ## Setup 4 | 5 | This application was created with the following commands: 6 | 7 | - `cd apps` 8 | - `pnpx create-remix@latest remix` (answers: no git, no dependencies installation) 9 | - `cd remix` 10 | - TODO 11 | - edit package.json to add `"@vercel/analytics": "workspace:*"` 12 | - `pnpm i` 13 | 14 | ## Usage 15 | 16 | Start it with `pnpm -F remix dev` and browse to [http://localhost:5173](http://localhost:5173) 17 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vercel/eslint-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "dependencies": { 7 | "@next/eslint-plugin-next": "13.4.7" 8 | }, 9 | "devDependencies": { 10 | "@vercel/style-guide": "5.0.1", 11 | "eslint": "8.43.0", 12 | "prettier": "2.8.8", 13 | "typescript": "5.1.3" 14 | }, 15 | "peerDependencies": { 16 | "eslint": "8.26.0", 17 | "typescript": "4.8.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/nextjs/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function Page() { 4 | return ( 5 | <> 6 |
Testing web analytics
7 |
    8 |
  • 9 | Pages directory A 10 |
  • 11 |
  • 12 | Pages directory B 13 |
  • 14 |
  • 15 | App directory 16 |
  • 17 |
18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/web/src/astro/component.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error typescript doesn't handle ./index.astro properly, but it's needed to generate types 2 | // eslint-disable-next-line import/no-default-export, no-useless-rename -- Exporting everything doesn't yield the desired outcome 3 | export { default as default } from './index.astro'; 4 | export type { 5 | AnalyticsProps, 6 | BeforeSend, 7 | BeforeSendEvent, 8 | // @ts-expect-error this filed is copied to dist, so it's ok to reference the generated index.d.ts 9 | } from '../index.d.ts'; 10 | -------------------------------------------------------------------------------- /apps/nextjs/app/server-actions/page.tsx: -------------------------------------------------------------------------------- 1 | import { track } from '@vercel/analytics/server'; 2 | 3 | export default function FeedbackPage() { 4 | async function submitFeedback(data: FormData) { 5 | 'use server'; 6 | 7 | await track('Feedback', { 8 | message: data.get('feedback') as string, 9 | }); 10 | } 11 | 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/astro/src/pages/blog/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | import Layout from '../../layouts/Base.astro'; 4 | 5 | export async function getStaticPaths() { 6 | const blogEntries = await getCollection('blog'); 7 | return blogEntries.map(entry => ({ 8 | params: { slug: entry.slug }, props: { entry }, 9 | })); 10 | } 11 | 12 | const { entry } = Astro.props; 13 | const { Content } = await entry.render(); 14 | --- 15 | 16 | 17 |

back

18 |
-------------------------------------------------------------------------------- /packages/web/src/react/utils.ts: -------------------------------------------------------------------------------- 1 | export function getBasePath(): string | undefined { 2 | // !! important !! 3 | // do not access env variables using process.env[varname] 4 | // some bundles won't replace the value at build time. 5 | // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- we can't use optionnal here, it'll break if process does not exist. 6 | if (typeof process === 'undefined' || typeof process.env === 'undefined') { 7 | return undefined; 8 | } 9 | return process.env.REACT_APP_VERCEL_OBSERVABILITY_BASEPATH; 10 | } 11 | -------------------------------------------------------------------------------- /apps/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/remix/app/routes/blog.$slug.tsx: -------------------------------------------------------------------------------- 1 | import { json, type LoaderFunctionArgs } from '@remix-run/node'; 2 | import { Link, useLoaderData } from '@remix-run/react'; 3 | 4 | export const loader = async ({ params }: LoaderFunctionArgs) => { 5 | return json({ slug: params.slug }); 6 | }; 7 | 8 | export default function BlogPage() { 9 | const { slug } = useLoaderData(); 10 | return ( 11 |
12 |

Blog

13 |

We don't talk about {slug}

14 |
15 | Back 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/eslint-config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | require.resolve('@vercel/style-guide/eslint/browser'), 5 | require.resolve('@vercel/style-guide/eslint/react'), 6 | require.resolve('@vercel/style-guide/eslint/next'), 7 | require.resolve('@vercel/style-guide/eslint/typescript'), 8 | require.resolve('@vercel/style-guide/eslint/jest'), 9 | ], 10 | env: { 11 | jest: true, 12 | }, 13 | parserOptions: { 14 | project: './tsconfig.json', 15 | }, 16 | ignorePatterns: ['*.config.js', 'dist/'], 17 | }; 18 | -------------------------------------------------------------------------------- /apps/nextjs-15/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "analytics", 3 | "repository": { 4 | "type": "git", 5 | "url": "git+https://github.com/vercel/analytics.git" 6 | }, 7 | "license": "MPL-2.0", 8 | "scripts": { 9 | "prepare": "husky install" 10 | }, 11 | "prettier": "@vercel/style-guide/prettier", 12 | "dependencies": { 13 | "lint-staged": "^13.2.2" 14 | }, 15 | "devDependencies": { 16 | "@vercel/style-guide": "^5.0.1", 17 | "husky": "^8.0.3", 18 | "prettier": "2.8.8", 19 | "typescript": "^5.1.6" 20 | }, 21 | "packageManager": "pnpm@8.6.5" 22 | } 23 | -------------------------------------------------------------------------------- /apps/nextjs/app/api/edge/route.test.ts: -------------------------------------------------------------------------------- 1 | import { GET } from './route'; 2 | 3 | describe('app API edge route', () => { 4 | const log = jest.spyOn(console, 'log').mockImplementation(() => void 0); 5 | 6 | beforeEach(() => jest.clearAllMocks()); 7 | 8 | it('tracks event', async () => { 9 | const response = await GET(); 10 | expect(response.status).toBe(200); 11 | expect(log).toHaveBeenCalledTimes(1); 12 | expect(log).toHaveBeenCalledWith( 13 | '[Vercel Web Analytics] Track "Edge Event" with data {"data":"edge","router":"app"}' 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/web/src/remix/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Analytics as AnalyticsScript } from '../react'; 3 | import type { AnalyticsProps, BeforeSend, BeforeSendEvent } from '../types'; 4 | import { getBasePath, useRoute } from './utils'; 5 | 6 | export function Analytics(props: Omit): JSX.Element { 7 | return ( 8 | 14 | ); 15 | } 16 | export type { AnalyticsProps, BeforeSend, BeforeSendEvent }; 17 | -------------------------------------------------------------------------------- /apps/astro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "astro": "astro", 8 | "build": "astro build", 9 | "dev": "astro dev", 10 | "preview": "astro preview", 11 | "start": "astro dev" 12 | }, 13 | "dependencies": { 14 | "@astrojs/ts-plugin": "^1.10.4", 15 | "@vercel/analytics": "workspace:*", 16 | "astro": "5.16.4" 17 | }, 18 | "devDependencies": { 19 | "@astrojs/check": "^0.5.4", 20 | "@vercel/analytics": "workspace:*", 21 | "typescript": "^5.3.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/nextjs/app/api/serverless/route.test.ts: -------------------------------------------------------------------------------- 1 | import { GET } from './route'; 2 | 3 | describe('app API serverless route', () => { 4 | const log = jest.spyOn(console, 'log').mockImplementation(() => void 0); 5 | 6 | beforeEach(() => jest.clearAllMocks()); 7 | 8 | it('tracks event', async () => { 9 | const response = await GET(); 10 | expect(response.status).toBe(200); 11 | expect(log).toHaveBeenCalledTimes(1); 12 | expect(log).toHaveBeenCalledWith( 13 | '[Vercel Web Analytics] Track "Serverless Event" with data {"data":"serverless","router":"app"}' 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /apps/nextjs/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import type { NextFetchEvent, NextRequest } from 'next/server'; 3 | import { track } from '@vercel/analytics/server'; 4 | 5 | export async function middleware(request: NextRequest, event: NextFetchEvent) { 6 | event.waitUntil( 7 | track('Redirect', { 8 | path: request.nextUrl.pathname, 9 | type: 'waitUntil', 10 | }) 11 | ); 12 | return NextResponse.redirect(new URL('/server-actions', request.url)); 13 | } 14 | 15 | // See "Matching Paths" below to learn more 16 | export const config = { 17 | matcher: '/middleware/:path*', 18 | }; 19 | -------------------------------------------------------------------------------- /apps/nextjs/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from '@vercel/analytics/next'; 2 | import type { AppProps } from 'next/app'; 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return ( 6 | <> 7 | { 9 | const url = new URL(event.url); 10 | if (url.searchParams.has('secret')) { 11 | url.searchParams.set('secret', 'REDACTED'); 12 | } 13 | return { 14 | ...event, 15 | url: url.toString(), 16 | }; 17 | }} 18 | /> 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/nextjs/app/api/edge-no-context/route.test.ts: -------------------------------------------------------------------------------- 1 | import { GET } from './route'; 2 | 3 | describe('app API edge-no-context route', () => { 4 | const log = jest.spyOn(console, 'log').mockImplementation(() => void 0); 5 | 6 | beforeEach(() => jest.clearAllMocks()); 7 | 8 | it('tracks event', async () => { 9 | const response = await GET(new Request(new URL('/', 'http://localhost'))); 10 | expect(response.status).toBe(200); 11 | expect(log).toHaveBeenCalledTimes(1); 12 | expect(log).toHaveBeenCalledWith( 13 | '[Vercel Web Analytics] Track "Edge Event" with data {"data":"edge","router":"app","manual":true}' 14 | ); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /apps/nuxt/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | h1 { 19 | font-weight: 500; 20 | font-size: 2.6rem; 21 | position: relative; 22 | top: -10px; 23 | } 24 | 25 | h3 { 26 | font-size: 1.2rem; 27 | } 28 | 29 | .greetings h1, 30 | .greetings h3 { 31 | text-align: center; 32 | } 33 | 34 | @media (hover: hover) { 35 | a:hover { 36 | background-color: hsla(160, 100%, 37%, 0.2); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/vue/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | font-weight: normal; 8 | } 9 | 10 | a, 11 | .green { 12 | text-decoration: none; 13 | color: hsla(160, 100%, 37%, 1); 14 | transition: 0.4s; 15 | padding: 3px; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/nextjs-15/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-15", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev --turbopack", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@vercel/analytics": "workspace:^", 13 | "next": "15.1.9", 14 | "react": "19.2.1", 15 | "react-dom": "19.2.1" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20", 19 | "@types/react": "npm:types-react@19.0.0-rc.1", 20 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", 21 | "eslint-config-next": "15.0.1", 22 | "typescript": "^5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/astro/src/layouts/Base.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Analytics from '@vercel/analytics/astro'; 3 | 4 | const { title } = Astro.props; 5 | --- 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {title} 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /apps/sveltekit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "vite build", 8 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 9 | "dev": "vite dev", 10 | "prepare": "svelte-kit sync", 11 | "preview": "vite preview" 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/adapter-vercel": "latest", 15 | "@sveltejs/kit": "latest", 16 | "@sveltejs/vite-plugin-svelte": "latest", 17 | "@vercel/analytics": "workspace:*", 18 | "svelte": "latest", 19 | "svelte-check": "latest", 20 | "typescript": "latest", 21 | "vite": "latest" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/sveltekit/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /apps/nextjs-15/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | }, 23 | "target": "ES2017" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/vue/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 barebone demo application for Vercel Web Analytics 2 | 3 | ## Setup 4 | 5 | This application was created with the following commands: 6 | 7 | - `cd apps` 8 | - `pnpm create vue@latest vue` (answer no to all questions) 9 | - `cd vue` 10 | - manually edit package.json to add `"@vercel/analytics": "workspace:*"` dependency 11 | - `pnpm i` 12 | 13 | Then we imported and used `` component in `src/App.vue` file: 14 | 15 | ```vue 16 | 19 | 20 | 23 | ``` 24 | 25 | ## Usage 26 | 27 | Start it with `pnpm -F vue dev` and browse to [http://localhost:5173](http://localhost:5173) 28 | -------------------------------------------------------------------------------- /packages/web/src/remix/utils.ts: -------------------------------------------------------------------------------- 1 | import { useLocation, useParams } from '@remix-run/react'; 2 | import { computeRoute } from '../utils'; 3 | 4 | export const useRoute = (): { route: string | null; path: string } => { 5 | const params = useParams(); 6 | const { pathname: path } = useLocation(); 7 | return { route: computeRoute(path, params as never), path }; 8 | }; 9 | 10 | export function getBasePath(): string | undefined { 11 | // !! important !! 12 | // do not access env variables using import.meta.env[varname] 13 | // some bundles won't replace the value at build time. 14 | try { 15 | return import.meta.env.VITE_VERCEL_OBSERVABILITY_BASEPATH as 16 | | string 17 | | undefined; 18 | } catch { 19 | // do nothing 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/remix/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Links, 3 | Meta, 4 | Outlet, 5 | Scripts, 6 | ScrollRestoration, 7 | } from '@remix-run/react'; 8 | import { Analytics } from '@vercel/analytics/remix'; 9 | 10 | export function Layout({ children }: { children: React.ReactNode }) { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {children} 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | export default function App() { 30 | return ; 31 | } 32 | -------------------------------------------------------------------------------- /apps/astro/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /apps/sveltekit/README.md: -------------------------------------------------------------------------------- 1 | # Sveltekit Demo application for Vercel Web Analytics 2 | 3 | ## Setup 4 | 5 | This application was created with the following commands: 6 | 7 | - `cd apps` 8 | - `pnpx sv create sveltekit` (answers: SvelteKit minimal, no Typescript, no additional install, pnpm) 9 | - `cd sveltekit` 10 | - add `src/+layout.js` to include `import { injectAnalytics } from '@vercel/analytics/sveltekit'; injectAnalytics();` 11 | - edit package.json to add `"@vercel/analytics": "workspace:*"` dependency and change `@sveltejs/adapter-auto` into `@sveltejs/adapter-vercel` 12 | - eddi `svelte.config.js` to change `@sveltejs/adapter-auto` into `@sveltejs/adapter-vercel` 13 | - `pnpm i` 14 | 15 | ## Usage 16 | 17 | Start it with `pnpm -F sveltekit dev` and browse to [http://localhost:5173](http://localhost:5173) 18 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #ffffff; 3 | --foreground: #171717; 4 | } 5 | 6 | @media (prefers-color-scheme: dark) { 7 | :root { 8 | --background: #0a0a0a; 9 | --foreground: #ededed; 10 | } 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | } 18 | 19 | body { 20 | color: var(--foreground); 21 | background: var(--background); 22 | font-family: Arial, Helvetica, sans-serif; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | * { 28 | box-sizing: border-box; 29 | padding: 0; 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: inherit; 35 | text-decoration: none; 36 | } 37 | 38 | @media (prefers-color-scheme: dark) { 39 | html { 40 | color-scheme: dark; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/nextjs/pages/api/test.test.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | import GET from './test'; 3 | 4 | describe('pages API edge route', () => { 5 | const log = jest.spyOn(console, 'log').mockImplementation(() => void 0); 6 | 7 | beforeEach(() => jest.clearAllMocks()); 8 | 9 | it('tracks event', async () => { 10 | const request = new NextRequest(new URL('/', 'http://localhost')); 11 | // @ts-expect-error -- we should pass a NextFetchEvent instead an empty object, but it's not used. 12 | const response = await GET(request, {}); 13 | expect(response.status).toBe(200); 14 | expect(log).toHaveBeenCalledTimes(1); 15 | expect(log).toHaveBeenCalledWith( 16 | '[Vercel Web Analytics] Track "Pages Api Route" with data {"runtime":"edge","router":"pages"}' 17 | ); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /apps/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "strictNullChecks": true 23 | }, 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx", 28 | ".next/types/**/*.ts", 29 | "./jest-setup.ts" 30 | ], 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /apps/remix/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import { Form, json, Link, useActionData } from '@remix-run/react'; 2 | import { track } from '@vercel/analytics/server'; 3 | 4 | export const action = async () => { 5 | await track('Server Action', { some: 'data' }); 6 | return json({ success: true }); 7 | }; 8 | 9 | export default function Home() { 10 | const data = useActionData(); 11 | return ( 12 |
13 |

Vercel Web Analytics Demo

14 | {data?.success ? ( 15 |

Success!

16 | ) : ( 17 |
18 | 19 |
20 | )} 21 | About Henri 22 |
23 | About Bruno 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/web/src/vue/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, it, expect } from 'vitest'; 2 | import { getBasePath } from './utils'; 3 | 4 | describe('getBasePath()', () => { 5 | const processSave = { ...process }; 6 | const envSave = { ...process.env }; 7 | 8 | afterEach(() => { 9 | global.process = { ...processSave }; 10 | process.env = { ...envSave }; 11 | }); 12 | 13 | it('returns basepath set for Vue', () => { 14 | const basepath = `/_vercel-${Math.random()}/insights`; 15 | import.meta.env.VITE_VERCEL_OBSERVABILITY_BASEPATH = basepath; 16 | expect(getBasePath()).toBe(basepath); 17 | }); 18 | 19 | it('returns null without import.meta', () => { 20 | // @ts-expect-error -- yes, we want to completely drop import.meta.env for this test!! 21 | import.meta.env = undefined; 22 | expect(getBasePath()).toBeUndefined(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/web/src/remix/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, it, expect } from 'vitest'; 2 | import { getBasePath } from './utils'; 3 | 4 | describe('getBasePath()', () => { 5 | const processSave = { ...process }; 6 | const envSave = { ...process.env }; 7 | 8 | afterEach(() => { 9 | global.process = { ...processSave }; 10 | process.env = { ...envSave }; 11 | }); 12 | 13 | it('returns basepath set for Remix', () => { 14 | const basepath = `/_vercel-${Math.random()}/insights`; 15 | import.meta.env.VITE_VERCEL_OBSERVABILITY_BASEPATH = basepath; 16 | expect(getBasePath()).toBe(basepath); 17 | }); 18 | 19 | it('returns null without import.meta', () => { 20 | // @ts-expect-error -- yes, we want to completely drop import.meta.env for this test!! 21 | import.meta.env = undefined; 22 | expect(getBasePath()).toBeUndefined(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/remix/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*.ts", 4 | "**/*.tsx", 5 | "**/.server/**/*.ts", 6 | "**/.server/**/*.tsx", 7 | "**/.client/**/*.ts", 8 | "**/.client/**/*.tsx" 9 | ], 10 | "compilerOptions": { 11 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 12 | "types": ["@remix-run/node", "vite/client"], 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "jsx": "react-jsx", 16 | "module": "ESNext", 17 | "moduleResolution": "Bundler", 18 | "resolveJsonModule": true, 19 | "target": "ES2022", 20 | "strict": true, 21 | "allowJs": true, 22 | "skipLibCheck": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "~/*": ["./app/*"] 27 | }, 28 | 29 | // Vite takes care of building everything, not tsc. 30 | "noEmit": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/web/src/sveltekit/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, it, expect } from 'vitest'; 2 | import { getBasePath } from './utils'; 3 | 4 | describe('getBasePath()', () => { 5 | const processSave = { ...process }; 6 | const envSave = { ...process.env }; 7 | 8 | afterEach(() => { 9 | global.process = { ...processSave }; 10 | process.env = { ...envSave }; 11 | }); 12 | 13 | it('returns basepath set for Sveltekit', () => { 14 | const basepath = `/_vercel-${Math.random()}/insights`; 15 | import.meta.env.VITE_VERCEL_OBSERVABILITY_BASEPATH = basepath; 16 | expect(getBasePath()).toBe(basepath); 17 | }); 18 | 19 | it('returns null without import.meta', () => { 20 | // @ts-expect-error -- yes, we want to completely drop import.meta.env for this test!! 21 | import.meta.env = undefined; 22 | expect(getBasePath()).toBeUndefined(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/nuxt/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Demo application for Vercel Speed-insights 2 | 3 | ## Setup 4 | 5 | This application was created with the following commands: 6 | 7 | - `cd apps` 8 | - `pnpx nuxi@latest init nuxt` (answers: npm, no git) 9 | - `cd nuxt` 10 | - `rm -rf node_modules .nuxt` 11 | - manually edit package.json to add `"@vercel/analytics": "workspace:*"` dependency 12 | - `pnpm i` 13 | 14 | Then we moved some code from vue's official template (styles, HelloWorld SFC) and added a few dynamic route to illustrate the use. 15 | We also imported and used `` component in `layouts/default.vue` file: 16 | 17 | ```vue 18 | 21 | 22 | 25 | ``` 26 | 27 | ## Usage 28 | 29 | Start it with `pnpm -F nuxt dev` and browse to [http://localhost:3000](http://localhost:3000) 30 | -------------------------------------------------------------------------------- /apps/vue/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 45 | -------------------------------------------------------------------------------- /apps/nextjs/e2e/utils.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test'; 2 | 3 | export async function useMockForProductionScript(props: { 4 | page: Page; 5 | onPageView: (page: string, payload: Object) => void; 6 | debug?: boolean; 7 | }) { 8 | await props.page.route('**/_vercel/insights/script.js', async (route, _) => { 9 | return route.fulfill({ 10 | status: 301, 11 | headers: { 12 | location: props.debug 13 | ? 'https://cdn.vercel-insights.com/v1/script.debug.js' 14 | : 'https://cdn.vercel-insights.com/v1/script.js', 15 | }, 16 | }); 17 | }); 18 | 19 | await props.page.route('**/_vercel/insights/view', async (route, request) => { 20 | const headers = request.headers(); 21 | 22 | props.onPageView(headers.referer, request.postDataJSON()); 23 | 24 | return route.fulfill({ 25 | status: 200, 26 | contentType: 'text/plain', 27 | body: 'OK', 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import localFont from 'next/font/local'; 3 | import { Analytics } from '@vercel/analytics/next'; 4 | import './globals.css'; 5 | 6 | const geistSans = localFont({ 7 | src: './fonts/GeistVF.woff', 8 | variable: '--font-geist-sans', 9 | weight: '100 900', 10 | }); 11 | const geistMono = localFont({ 12 | src: './fonts/GeistMonoVF.woff', 13 | variable: '--font-geist-mono', 14 | weight: '100 900', 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: 'Create Next App', 19 | description: 'Generated by create next app', 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode; 26 | }>) { 27 | return ( 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: checkout 10 | uses: actions/checkout@v3 11 | - name: Install 12 | uses: ./.github/composite-actions/install 13 | - name: lint 14 | run: pnpm -r lint 15 | - name: type-check 16 | run: pnpm -r type-check 17 | - name: prettier 18 | run: pnpm prettier -c . 19 | - name: build 20 | run: pnpm -r build 21 | - run: pnpm --filter @vercel/analytics publish --tag beta --no-git-checks 22 | if: github.event.release.prerelease == true 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }} 25 | - run: pnpm --filter @vercel/analytics publish --no-git-checks 26 | if: github.event.release.prerelease == false 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }} 29 | -------------------------------------------------------------------------------- /apps/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "private": true, 4 | "scripts": { 5 | "build": "next build", 6 | "dev": "next dev", 7 | "start": "next start", 8 | "test": "jest", 9 | "test:e2e:development": "NEXT_PUBLIC_ANALYTICS_MODE=development pnpm --filter nextjs... build && playwright test development", 10 | "test:e2e:production": "NEXT_PUBLIC_ANALYTICS_MODE=production pnpm --filter nextjs... build && playwright test production" 11 | }, 12 | "dependencies": { 13 | "@vercel/analytics": "workspace:*", 14 | "next": "14.1.0", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1" 17 | }, 18 | "devDependencies": { 19 | "@playwright/test": "1.35.1", 20 | "@swc/jest": "^0.2.26", 21 | "@testing-library/jest-dom": "^5.16.5", 22 | "@testing-library/react": "^14.0.0", 23 | "@types/jest": "^29.5.2", 24 | "jest": "^29.7.0", 25 | "jest-environment-jsdom": "^29.7.0", 26 | "ts-node": "^10.9.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/remix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "scripts": { 7 | "build": "remix vite:build", 8 | "dev": "remix vite:dev", 9 | "start": "remix-serve ./build/server/index.js", 10 | "typecheck": "tsc" 11 | }, 12 | "dependencies": { 13 | "@remix-run/node": "latest", 14 | "@remix-run/react": "latest", 15 | "@remix-run/serve": "latest", 16 | "@vercel/analytics": "workspace:*", 17 | "isbot": "^4.1.0", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0" 20 | }, 21 | "devDependencies": { 22 | "@remix-run/dev": "latest", 23 | "@types/react": "^18.2.20", 24 | "@types/react-dom": "^18.2.7", 25 | "autoprefixer": "^10.4.19", 26 | "postcss": "^8.4.38", 27 | "tailwindcss": "^3.4.4", 28 | "typescript": "^5.1.6", 29 | "vite": "^5.1.0", 30 | "vite-tsconfig-paths": "^4.2.1" 31 | }, 32 | "engines": { 33 | "node": ">=20.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/web/src/sveltekit/index.ts: -------------------------------------------------------------------------------- 1 | import { inject, pageview, track } from '../generic'; 2 | import type { AnalyticsProps, BeforeSend, BeforeSendEvent } from '../types'; 3 | import { getBasePath } from './utils'; 4 | import { page } from '$app/stores'; 5 | import { browser } from '$app/environment'; 6 | import type {} from '@sveltejs/kit'; 7 | 8 | function injectAnalytics(props: Omit = {}): void { 9 | if (browser) { 10 | inject({ 11 | ...props, 12 | basePath: getBasePath(), 13 | disableAutoTrack: true, 14 | framework: 'sveltekit', 15 | }); 16 | 17 | page.subscribe(({ route, url }) => { 18 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- route could be undefined in layout.js file 19 | if (route?.id) { 20 | pageview({ route: route.id, path: url.pathname }); 21 | } 22 | }); 23 | } 24 | } 25 | 26 | export { injectAnalytics, track }; 27 | export type { AnalyticsProps, BeforeSend, BeforeSendEvent }; 28 | -------------------------------------------------------------------------------- /apps/vue/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /packages/web/src/nextjs/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { Suspense, type ReactNode } from 'react'; 3 | import { Analytics as AnalyticsScript } from '../react'; 4 | import type { AnalyticsProps, BeforeSend, BeforeSendEvent } from '../types'; 5 | import { getBasePath, useRoute } from './utils'; 6 | 7 | type Props = Omit; 8 | 9 | function AnalyticsComponent(props: Props): ReactNode { 10 | const { route, path } = useRoute(); 11 | return ( 12 | 19 | ); 20 | } 21 | 22 | export function Analytics(props: Props): null { 23 | // Because of incompatible types between ReactNode in React 19 and React 18 we return null (which is also what we render) 24 | return ( 25 | 26 | 27 | 28 | ) as never; 29 | } 30 | 31 | export type { AnalyticsProps, BeforeSend, BeforeSendEvent }; 32 | -------------------------------------------------------------------------------- /apps/astro/README.md: -------------------------------------------------------------------------------- 1 | # Astro Demo application for Vercel Speed-insights 2 | 3 | ## Setup 4 | 5 | This application was created with the following commands: 6 | 7 | - `cd apps` 8 | - `pnpm create astro@latest astro` (answers: empty, no to all) 9 | - `cd astro` 10 | - manually edit package.json to add `"@vercel/analytics": "workspace:*"` dependency 11 | - `pnpm i` 12 | 13 | Then we've added: 14 | 15 | 1. a simple collection of Markdown blog posts in `src/contents/blog` folder 16 | 2. a blog post page in `src/pages/blog/[...slug].astro` 17 | 3. an index page in `src/pages/index.astro` which list all available blog posts 18 | 4. a layout in `src/components/Base.astro`, used in both page, which includes our Analytics.astro component: 19 | 20 | ```astro 21 | --- 22 | import Analytics from '@vercel/analytics/astro'; 23 | --- 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | ## Usage 36 | 37 | Start it with `pnpm -F nuxt dev` and browse to [http://localhost:4321](http://localhost:4321) 38 | -------------------------------------------------------------------------------- /.github/composite-actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Install' 2 | description: 'Sets up Node.js and runs install' 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Set up Node.js 7 | uses: actions/setup-node@v4 8 | with: 9 | node-version-file: '.nvmrc' 10 | registry-url: 'https://registry.npmjs.org' 11 | 12 | - name: Install pnpm with corepack 13 | shell: bash 14 | run: corepack enable && pnpm --version 15 | 16 | - name: Get pnpm store directory 17 | id: pnpm-cache 18 | shell: bash 19 | run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 20 | 21 | - name: Restore pnpm global cache 22 | uses: actions/cache@v3 23 | with: 24 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 25 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} 26 | restore-keys: | 27 | ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} 28 | ${{ runner.os }}-pnpm-store- 29 | 30 | - name: Install dependencies 31 | shell: bash 32 | run: pnpm install 33 | -------------------------------------------------------------------------------- /packages/web/src/react/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, it, expect } from 'vitest'; 2 | import { getBasePath } from './utils'; 3 | 4 | describe('getBasePath()', () => { 5 | const processSave = { ...process }; 6 | const envSave = { ...process.env }; 7 | 8 | afterEach(() => { 9 | global.process = { ...processSave }; 10 | process.env = { ...envSave }; 11 | }); 12 | 13 | it('returns null without process', () => { 14 | // @ts-expect-error -- yes, we want to completely drop process for this test!! 15 | global.process = undefined; 16 | expect(getBasePath()).toBeUndefined(); 17 | }); 18 | 19 | it('returns null without process.env', () => { 20 | // @ts-expect-error -- yes, we want to completely drop process.env for this test!! 21 | process.env = undefined; 22 | expect(getBasePath()).toBeUndefined(); 23 | }); 24 | 25 | it('returns basepath set for CRA', () => { 26 | const basepath = `/_vercel-${Math.random()}/insights`; 27 | process.env.REACT_APP_VERCEL_OBSERVABILITY_BASEPATH = basepath; 28 | expect(getBasePath()).toBe(basepath); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/web/src/nextjs/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, describe, it, expect } from 'vitest'; 2 | import { getBasePath } from './utils'; 3 | 4 | describe('getBasePath()', () => { 5 | const processSave = { ...process }; 6 | const envSave = { ...process.env }; 7 | 8 | afterEach(() => { 9 | global.process = { ...processSave }; 10 | process.env = { ...envSave }; 11 | }); 12 | 13 | it('returns null without process', () => { 14 | // @ts-expect-error -- yes, we want to completely drop process for this test!! 15 | global.process = undefined; 16 | expect(getBasePath()).toBeUndefined(); 17 | }); 18 | 19 | it('returns null without process.env', () => { 20 | // @ts-expect-error -- yes, we want to completely drop process.env for this test!! 21 | process.env = undefined; 22 | expect(getBasePath()).toBeUndefined(); 23 | }); 24 | 25 | it('returns basepath set for Nextjs', () => { 26 | const basepath = `/_vercel-${Math.random()}/insights`; 27 | process.env.NEXT_PUBLIC_VERCEL_OBSERVABILITY_BASEPATH = basepath; 28 | expect(getBasePath()).toBe(basepath); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /apps/vue/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /apps/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | 28 | 56 | -------------------------------------------------------------------------------- /packages/web/src/types.ts: -------------------------------------------------------------------------------- 1 | interface PageViewEvent { 2 | type: 'pageview'; 3 | url: string; 4 | } 5 | interface CustomEvent { 6 | type: 'event'; 7 | url: string; 8 | } 9 | 10 | export type BeforeSendEvent = PageViewEvent | CustomEvent; 11 | 12 | export type Mode = 'auto' | 'development' | 'production'; 13 | export type AllowedPropertyValues = 14 | | string 15 | | number 16 | | boolean 17 | | null 18 | | undefined; 19 | 20 | export type BeforeSend = (event: BeforeSendEvent) => BeforeSendEvent | null; 21 | 22 | export interface AnalyticsProps { 23 | beforeSend?: BeforeSend; 24 | debug?: boolean; 25 | mode?: Mode; 26 | 27 | scriptSrc?: string; 28 | endpoint?: string; 29 | 30 | dsn?: string; 31 | } 32 | 33 | declare global { 34 | interface Window { 35 | // Base interface 36 | va?: ( 37 | event: 'beforeSend' | 'event' | 'pageview', 38 | properties?: unknown 39 | ) => void; 40 | // Queue for actions, before the library is loaded 41 | vaq?: [string, unknown?][]; 42 | vai?: boolean; 43 | vam?: Mode; 44 | /** used by Astro component only */ 45 | webAnalyticsBeforeSend?: BeforeSend; 46 | } 47 | } 48 | 49 | export type PlainFlags = Record; 50 | export type FlagsDataInput = (string | PlainFlags)[] | PlainFlags; 51 | -------------------------------------------------------------------------------- /apps/vue/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /apps/nextjs/e2e/development/pageview.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { useMockForProductionScript } from '../utils'; 3 | 4 | test.describe('pageview', () => { 5 | test('should track page views when navigating between pages', async ({ 6 | page, 7 | }) => { 8 | const messages: string[] = []; 9 | await useMockForProductionScript({ 10 | page, 11 | onPageView: () => {}, 12 | debug: true, 13 | }); 14 | 15 | page.on('console', (msg) => { 16 | const message = msg.text(); 17 | 18 | if ( 19 | message.includes('[Vercel Web Analytics]') || 20 | message.includes('[Vercel Analytics]') 21 | ) { 22 | messages.push(message); 23 | } 24 | }); 25 | 26 | await page.goto('/navigation/first'); 27 | await page.waitForTimeout(800); 28 | 29 | await page.click('text=Next'); 30 | 31 | await expect(page).toHaveURL('/navigation/second'); 32 | await expect(page.locator('h1')).toContainText('Second Page'); 33 | 34 | await page.waitForTimeout(200); 35 | 36 | expect( 37 | messages.find((m) => 38 | m.includes('[pageview] http://localhost:3000/navigation/first') 39 | ) 40 | ).toBeDefined(); 41 | expect( 42 | messages.find((m) => 43 | m.includes('[pageview] http://localhost:3000/navigation/second') 44 | ) 45 | ).toBeDefined(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /apps/nuxt/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 67 | -------------------------------------------------------------------------------- /packages/web/src/nextjs/utils.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | /* eslint-disable @typescript-eslint/no-unnecessary-condition -- can be empty in pages router */ 3 | import { useParams, usePathname, useSearchParams } from 'next/navigation.js'; 4 | import { computeRoute } from '../utils'; 5 | 6 | export const useRoute = (): { 7 | route: string | null; 8 | path: string; 9 | } => { 10 | const params = useParams(); 11 | const searchParams = useSearchParams(); 12 | const path = usePathname(); 13 | 14 | // Until we have route parameters, we don't compute the route 15 | if (!params) { 16 | return { route: null, path }; 17 | } 18 | // in Next.js@13, useParams() could return an empty object for pages router, and we default to searchParams. 19 | const finalParams = Object.keys(params).length 20 | ? params 21 | : Object.fromEntries(searchParams.entries()); 22 | return { route: computeRoute(path, finalParams), path }; 23 | }; 24 | 25 | export function getBasePath(): string | undefined { 26 | // !! important !! 27 | // do not access env variables using process.env[varname] 28 | // some bundles won't replace the value at build time. 29 | // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- we can't use optionnal here, it'll break if process does not exist. 30 | if (typeof process === 'undefined' || typeof process.env === 'undefined') { 31 | return undefined; 32 | } 33 | return process.env.NEXT_PUBLIC_VERCEL_OBSERVABILITY_BASEPATH; 34 | } 35 | -------------------------------------------------------------------------------- /apps/nextjs/e2e/development/beforeSend.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { useMockForProductionScript } from '../utils'; 3 | 4 | test.describe('beforeSend', () => { 5 | test('should replace the value of the secret query parameter', async ({ 6 | page, 7 | }) => { 8 | const messages: string[] = []; 9 | await useMockForProductionScript({ 10 | page, 11 | onPageView: () => {}, 12 | debug: true, 13 | }); 14 | 15 | page.on('console', (msg) => { 16 | const message = msg.text(); 17 | 18 | if ( 19 | message.includes('[Vercel Web Analytics]') || 20 | message.includes('[Vercel Analytics]') 21 | ) { 22 | messages.push(message); 23 | } 24 | }); 25 | 26 | await page.goto('/before-send/first'); 27 | await page.waitForLoadState('networkidle'); 28 | 29 | await page.click('text=Next'); 30 | 31 | await expect(page).toHaveURL('/before-send/second?secret=vercel'); 32 | await expect(page.locator('h1')).toContainText('Second Page'); 33 | 34 | await page.waitForLoadState('networkidle'); 35 | 36 | expect( 37 | messages.find((m) => 38 | m.includes('[pageview] http://localhost:3000/before-send/first') 39 | ) 40 | ).toBeDefined(); 41 | expect( 42 | messages.find((m) => 43 | m.includes( 44 | '[pageview] http://localhost:3000/before-send/second?secret=REDACTED' 45 | ) 46 | ) 47 | ).toBeDefined(); 48 | 49 | expect(messages.find((m) => m.includes('secret=vercel'))).toBeUndefined(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /apps/nextjs-15/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /packages/web/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | const cfg = { 4 | splitting: false, 5 | sourcemap: true, 6 | clean: true, 7 | treeshake: false, 8 | dts: true, 9 | format: ['esm', 'cjs'], 10 | }; 11 | 12 | export default defineConfig([ 13 | { 14 | ...cfg, 15 | entry: { 16 | index: 'src/generic.ts', 17 | }, 18 | outDir: 'dist', 19 | }, 20 | { 21 | ...cfg, 22 | entry: { 23 | index: 'src/nextjs/index.tsx', 24 | }, 25 | external: ['react', 'next'], 26 | outDir: 'dist/next', 27 | }, 28 | { 29 | ...cfg, 30 | entry: { 31 | index: 'src/nuxt/index.ts', 32 | }, 33 | external: ['vue', 'vue-router'], 34 | outDir: 'dist/nuxt', 35 | }, 36 | { 37 | ...cfg, 38 | entry: { 39 | index: 'src/react/index.tsx', 40 | }, 41 | external: ['react'], 42 | outDir: 'dist/react', 43 | }, 44 | { 45 | ...cfg, 46 | entry: { 47 | index: 'src/remix/index.tsx', 48 | }, 49 | external: ['react', '@remix-run/react', 'react-router'], 50 | outDir: 'dist/remix', 51 | }, 52 | { 53 | ...cfg, 54 | entry: { 55 | index: 'src/server/index.ts', 56 | }, 57 | outDir: 'dist/server', 58 | }, 59 | { 60 | ...cfg, 61 | entry: { 62 | index: 'src/sveltekit/index.ts', 63 | }, 64 | external: ['svelte', '@sveltejs/kit', '$app'], 65 | outDir: 'dist/sveltekit', 66 | }, 67 | { 68 | ...cfg, 69 | entry: { 70 | index: 'src/vue/index.ts', 71 | }, 72 | external: ['vue', 'vue-router'], 73 | outDir: 'dist/vue', 74 | }, 75 | ]); 76 | -------------------------------------------------------------------------------- /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 11 | 12 | jobs: 13 | typescript: 14 | name: typescript 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 10 17 | steps: 18 | - name: checkout 19 | uses: actions/checkout@v3 20 | - name: Install 21 | uses: ./.github/composite-actions/install 22 | - name: lint 23 | run: pnpm -r lint 24 | - name: type-check 25 | run: pnpm -r type-check 26 | - name: prettier 27 | run: pnpm prettier -c . 28 | - name: build 29 | run: pnpm -r build 30 | - name: test 31 | run: pnpm -r test 32 | 33 | playwright: 34 | name: playwright 35 | runs-on: ubuntu-latest 36 | timeout-minutes: 10 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | test: 41 | - 'test:e2e:production' 42 | - 'test:e2e:development' 43 | steps: 44 | - name: checkout 45 | uses: actions/checkout@v3 46 | - name: Install 47 | uses: ./.github/composite-actions/install 48 | - name: get playwright version 49 | id: pw 50 | run: echo "version=$(cat apps/nextjs/package.json | jq -r '.devDependencies."@playwright/test"')" >> $GITHUB_OUTPUT 51 | - name: install playwright 52 | run: pnpx playwright@${{ steps.pw.outputs.version }} install 53 | - name: test 54 | run: pnpm -r ${{ matrix.test }} 55 | -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- 1 | ![Analytics](https://github.com/vercel/analytics/blob/main/.github/banner.png) 2 | 3 |
Vercel Web Analytics
4 |
Privacy-friendly, real-time traffic insights
5 |
6 |
7 | Website 8 | · 9 | Documentation 10 | · 11 | Twitter 12 |
13 | 14 | ## Overview 15 | 16 | `@vercel/analytics` allows you to track page views and custom events in your Next.js app or any other website that is deployed to Vercel. 17 | 18 | All page views are automatically tracked in your app. 19 | 20 | This package does **not** track data in development mode. 21 | 22 | ## Quickstart 23 | 24 | 1. Enable Vercel Web Analytics for a project in the [Vercel Dashboard](https://vercel.com/dashboard). 25 | 2. Add the `@vercel/analytics` package to your project 26 | 3. Inject the Analytics script to your app 27 | 28 | - If you are using **Next.js** or **React**, you can use the `` component to inject the script into your app. 29 | - To add the tracking script for other frameworks, use the `inject` function. 30 | - If you want to use Vercel Web Analytics on a static site without npm, follow the instructions in the [documentation](https://vercel.com/docs/analytics/quickstart). 31 | 32 | 4. Deploy your app to Vercel and see data flowing in. 33 | 34 | ## Documentation 35 | 36 | Find more details about this package in our [documentation](https://vercel.com/docs/analytics/quickstart). 37 | -------------------------------------------------------------------------------- /packages/web/src/vue/create-component.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, watch } from 'vue'; 2 | // for barebone vue project, vite will issue a warning since 'vue-router' import can't be resolved, 3 | import { useRoute } from 'vue-router'; 4 | import { inject, pageview, type AnalyticsProps } from '../generic'; 5 | import { computeRoute } from '../utils'; 6 | import { getBasePath } from './utils'; 7 | 8 | export function createComponent( 9 | framework = 'vue' 10 | ): ReturnType { 11 | return defineComponent({ 12 | props: ['dsn', 'beforeSend', 'debug', 'scriptSrc', 'endpoint', 'mode'], 13 | setup(props: Omit) { 14 | // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a React component. 15 | const route = useRoute(); 16 | inject({ 17 | ...props, 18 | basePath: getBasePath(), 19 | // keep auto-tracking unless we have route support (Nuxt or vue-router). 20 | disableAutoTrack: Boolean(route), 21 | framework, 22 | }); 23 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- route is undefined for barebone vue project. 24 | if (route && typeof window !== 'undefined') { 25 | const changeRoute = (): void => { 26 | pageview({ 27 | route: computeRoute(route.path, route.params), 28 | path: route.path, 29 | }); 30 | }; 31 | changeRoute(); 32 | watch(route, changeRoute); 33 | } 34 | }, 35 | // Vue component must have a render function, or a template. 36 | render() { 37 | return null; 38 | }, 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /apps/nextjs/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightTestConfig, devices } from '@playwright/test'; 2 | import path from 'path'; 3 | 4 | // Use process.env.PORT by default and fallback to port 3000 5 | const PORT = process.env.PORT || 3000; 6 | 7 | // Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port 8 | const baseURL = `http://localhost:${PORT}`; 9 | 10 | // Reference: https://playwright.dev/docs/test-configuration 11 | const config: PlaywrightTestConfig = { 12 | // Timeout per test 13 | timeout: 30 * 1000, 14 | // Test directory 15 | testDir: path.join(__dirname, 'e2e'), 16 | // Artifacts folder where screenshots, videos, and traces are stored. 17 | outputDir: 'test-results/', 18 | 19 | // Run your local dev server before starting the tests: 20 | // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests 21 | webServer: { 22 | command: 'pnpm start', 23 | url: baseURL, 24 | timeout: 120 * 1000, 25 | reuseExistingServer: !process.env.CI, 26 | }, 27 | 28 | use: { 29 | // Use baseURL so to make navigations relative. 30 | // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url 31 | baseURL, 32 | 33 | // Retry a test if its failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc. 34 | // More information: https://playwright.dev/docs/trace-viewer 35 | trace: 'retry-with-trace', 36 | }, 37 | 38 | projects: [ 39 | { 40 | name: 'Desktop Chrome', 41 | use: { 42 | ...devices['Desktop Chrome'], 43 | }, 44 | }, 45 | ], 46 | }; 47 | export default config; 48 | -------------------------------------------------------------------------------- /apps/nextjs/e2e/production/beforeSend.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { useMockForProductionScript } from '../utils'; 3 | 4 | test.describe('beforeSend', () => { 5 | test('should replace the value of the secret query parameter', async ({ 6 | page, 7 | }) => { 8 | const payloads: { page: string; payload: Object }[] = []; 9 | 10 | await useMockForProductionScript({ 11 | page, 12 | onPageView: (page, payload) => { 13 | payloads.push({ page, payload }); 14 | }, 15 | }); 16 | 17 | await page.goto('/before-send/first'); 18 | await page.waitForLoadState('networkidle'); 19 | 20 | await page.click('text=Next'); 21 | 22 | await expect(page).toHaveURL('/before-send/second?secret=vercel'); 23 | await expect(page.locator('h1')).toContainText('Second Page'); 24 | 25 | await page.waitForLoadState('networkidle'); 26 | 27 | expect(payloads).toMatchObject([ 28 | { 29 | page: 'http://localhost:3000/before-send/first', 30 | payload: { 31 | o: 'http://localhost:3000/before-send/first', 32 | sv: expect.any(String), 33 | sdkn: '@vercel/analytics/next', 34 | sdkv: expect.any(String), 35 | ts: expect.any(Number), 36 | r: '', 37 | }, 38 | }, 39 | { 40 | page: 'http://localhost:3000/before-send/second?secret=vercel', 41 | payload: { 42 | o: 'http://localhost:3000/before-send/second?secret=REDACTED', 43 | ts: expect.any(Number), 44 | sv: expect.any(String), 45 | sdkn: '@vercel/analytics/next', 46 | sdkv: expect.any(String), 47 | }, 48 | }, 49 | ]); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/web/src/astro/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Since this file will not be bundled by Tsup, it is referencing bundled files relative to dist/astro/ 3 | import type { AnalyticsProps } from '../index.d.ts'; 4 | type Props = Omit; 5 | 6 | const propsStr = JSON.stringify(Astro.props); 7 | const paramsStr = JSON.stringify(Astro.params); 8 | --- 9 | 10 | 14 | 15 | 55 | -------------------------------------------------------------------------------- /apps/vue/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /apps/nuxt/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | color: var(--color-text); 64 | background: var(--color-background); 65 | transition: color 0.5s, background-color 0.5s; 66 | line-height: 1.6; 67 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 68 | Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 69 | sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /apps/vue/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | color: var(--color-text); 64 | background: var(--color-background); 65 | transition: color 0.5s, background-color 0.5s; 66 | line-height: 1.6; 67 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 68 | Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 69 | sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /packages/web/src/generic.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, it, expect, vi } from 'vitest'; 2 | import { inject, track } from './generic'; 3 | import type { AllowedPropertyValues, Mode } from './types'; 4 | 5 | describe.each([ 6 | { 7 | mode: 'development', 8 | file: 'https://va.vercel-scripts.com/v1/script.debug.js', 9 | }, 10 | { 11 | mode: 'production', 12 | file: 'http://localhost:3000/_vercel/insights/script.js', 13 | }, 14 | ] as { mode: Mode; file: string }[])('in $mode mode', ({ mode, file }) => { 15 | describe('inject', () => { 16 | it('adds the script tag correctly', () => { 17 | inject({ mode }); 18 | 19 | const scripts = document.getElementsByTagName('script'); 20 | expect(scripts).toHaveLength(1); 21 | 22 | const script = document.head.querySelector('script'); 23 | 24 | if (!script) { 25 | throw new Error('Could not find script tag'); 26 | } 27 | 28 | expect(script.src).toEqual(file); 29 | expect(script).toHaveAttribute('defer'); 30 | }); 31 | }); 32 | 33 | describe('track custom events', () => { 34 | beforeEach(() => { 35 | // reset the internal queue before every test 36 | window.vaq = []; 37 | inject({ mode }); 38 | }); 39 | 40 | describe('queue custom events', () => { 41 | it('tracks event with name only', () => { 42 | const name = 'my event'; 43 | track(name); 44 | expect(window.vaq?.[0]).toEqual(['event', { name }]); 45 | }); 46 | 47 | it('allows custom data to be tracked', () => { 48 | const name = 'custom event'; 49 | const data = { string: 'string', number: 1 }; 50 | track(name, data); 51 | expect(window.vaq?.[0]).toEqual(['event', { name, data }]); 52 | }); 53 | 54 | it('should strip data for nested objects', () => { 55 | vi.spyOn(global.console, 'error').mockImplementation(() => void 0); 56 | 57 | const name = 'custom event'; 58 | const data = { string: 'string', number: 1 }; 59 | track(name, { 60 | ...data, 61 | nested: { object: '' } as unknown as AllowedPropertyValues, 62 | }); 63 | 64 | if (mode === 'development') { 65 | // eslint-disable-next-line no-console -- only in development 66 | expect(console.error).toHaveBeenCalledTimes(1); 67 | } else { 68 | expect(window.vaq?.[0]).toEqual(['event', { name, data }]); 69 | } 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/web/src/react/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useEffect } from 'react'; 3 | import { inject, track, pageview } from '../generic'; 4 | import type { AnalyticsProps, BeforeSend, BeforeSendEvent } from '../types'; 5 | import { getBasePath } from './utils'; 6 | 7 | /** 8 | * Injects the Vercel Web Analytics script into the page head and starts tracking page views. Read more in our [documentation](https://vercel.com/docs/concepts/analytics/package). 9 | * @param [props] - Analytics options. 10 | * @param [props.mode] - The mode to use for the analytics script. Defaults to `auto`. 11 | * - `auto` - Automatically detect the environment. Uses `production` if the environment cannot be determined. 12 | * - `production` - Always use the production script. (Sends events to the server) 13 | * - `development` - Always use the development script. (Logs events to the console) 14 | * @param [props.debug] - Whether to enable debug logging in development. Defaults to `true`. 15 | * @param [props.beforeSend] - A middleware function to modify events before they are sent. Should return the event object or `null` to cancel the event. 16 | * @example 17 | * ```js 18 | * import { Analytics } from '@vercel/analytics/react'; 19 | * 20 | * export default function App() { 21 | * return ( 22 | *
23 | * 24 | *

My App

25 | *
26 | * ); 27 | * } 28 | * ``` 29 | */ 30 | function Analytics( 31 | props: AnalyticsProps & { 32 | framework?: string; 33 | route?: string | null; 34 | path?: string | null; 35 | basePath?: string; 36 | } 37 | ): null { 38 | useEffect(() => { 39 | if (props.beforeSend) { 40 | window.va?.('beforeSend', props.beforeSend); 41 | } 42 | }, [props.beforeSend]); 43 | 44 | // biome-ignore lint/correctness/useExhaustiveDependencies: only run once 45 | useEffect(() => { 46 | inject({ 47 | framework: props.framework || 'react', 48 | basePath: props.basePath ?? getBasePath(), 49 | ...(props.route !== undefined && { disableAutoTrack: true }), 50 | ...props, 51 | }); 52 | // eslint-disable-next-line react-hooks/exhaustive-deps -- only run once 53 | }, []); 54 | 55 | useEffect(() => { 56 | // explicitely track page view, since we disabled auto tracking 57 | if (props.route && props.path) { 58 | pageview({ route: props.route, path: props.path }); 59 | } 60 | }, [props.route, props.path]); 61 | 62 | return null; 63 | } 64 | 65 | export { track, Analytics }; 66 | export type { AnalyticsProps, BeforeSend, BeforeSendEvent }; 67 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import styles from './page.module.css'; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 | Next.js logo 16 |
    17 |
  1. 18 | Get started by editing src/app/page.tsx. 19 |
  2. 20 |
  3. Save and see your changes instantly.
  4. 21 |
22 | 23 | 48 |
49 | 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /apps/nextjs-15/src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | --gray-rgb: 0, 0, 0; 3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08); 4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05); 5 | 6 | --button-primary-hover: #383838; 7 | --button-secondary-hover: #f2f2f2; 8 | 9 | display: grid; 10 | grid-template-rows: 20px 1fr 20px; 11 | align-items: center; 12 | justify-items: center; 13 | min-height: 100svh; 14 | padding: 80px; 15 | gap: 64px; 16 | font-family: var(--font-geist-sans); 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | .page { 21 | --gray-rgb: 255, 255, 255; 22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145); 23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06); 24 | 25 | --button-primary-hover: #ccc; 26 | --button-secondary-hover: #1a1a1a; 27 | } 28 | } 29 | 30 | .main { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 32px; 34 | grid-row-start: 2; 35 | } 36 | 37 | .main ol { 38 | font-family: var(--font-geist-mono); 39 | padding-left: 0; 40 | margin: 0; 41 | font-size: 14px; 42 | line-height: 24px; 43 | letter-spacing: -0.01em; 44 | list-style-position: inside; 45 | } 46 | 47 | .main li:not(:last-of-type) { 48 | margin-bottom: 8px; 49 | } 50 | 51 | .main code { 52 | font-family: inherit; 53 | background: var(--gray-alpha-100); 54 | padding: 2px 4px; 55 | border-radius: 4px; 56 | font-weight: 600; 57 | } 58 | 59 | .ctas { 60 | display: flex; 61 | gap: 16px; 62 | } 63 | 64 | .ctas a { 65 | appearance: none; 66 | border-radius: 128px; 67 | height: 48px; 68 | padding: 0 20px; 69 | border: none; 70 | border: 1px solid transparent; 71 | transition: background 0.2s, color 0.2s, border-color 0.2s; 72 | cursor: pointer; 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | font-size: 16px; 77 | line-height: 20px; 78 | font-weight: 500; 79 | } 80 | 81 | a.primary { 82 | background: var(--foreground); 83 | color: var(--background); 84 | gap: 8px; 85 | } 86 | 87 | a.secondary { 88 | border-color: var(--gray-alpha-200); 89 | min-width: 180px; 90 | } 91 | 92 | .footer { 93 | grid-row-start: 3; 94 | display: flex; 95 | gap: 24px; 96 | } 97 | 98 | .footer a { 99 | display: flex; 100 | align-items: center; 101 | gap: 8px; 102 | } 103 | 104 | .footer img { 105 | flex-shrink: 0; 106 | } 107 | 108 | /* Enable hover only on non-touch devices */ 109 | @media (hover: hover) and (pointer: fine) { 110 | a.primary:hover { 111 | background: var(--button-primary-hover); 112 | border-color: transparent; 113 | } 114 | 115 | a.secondary:hover { 116 | background: var(--button-secondary-hover); 117 | border-color: transparent; 118 | } 119 | 120 | .footer a:hover { 121 | text-decoration: underline; 122 | text-underline-offset: 4px; 123 | } 124 | } 125 | 126 | @media (max-width: 600px) { 127 | .page { 128 | padding: 32px; 129 | padding-bottom: 80px; 130 | } 131 | 132 | .main { 133 | align-items: center; 134 | } 135 | 136 | .main ol { 137 | text-align: center; 138 | } 139 | 140 | .ctas { 141 | flex-direction: column; 142 | } 143 | 144 | .ctas a { 145 | font-size: 14px; 146 | height: 40px; 147 | padding: 0 16px; 148 | } 149 | 150 | a.secondary { 151 | min-width: auto; 152 | } 153 | 154 | .footer { 155 | flex-wrap: wrap; 156 | align-items: center; 157 | justify-content: center; 158 | } 159 | } 160 | 161 | @media (prefers-color-scheme: dark) { 162 | .logo { 163 | filter: invert(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /packages/web/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { AllowedPropertyValues, AnalyticsProps, Mode } from './types'; 2 | 3 | export function isBrowser(): boolean { 4 | return typeof window !== 'undefined'; 5 | } 6 | 7 | function detectEnvironment(): 'development' | 'production' { 8 | try { 9 | const env = process.env.NODE_ENV; 10 | if (env === 'development' || env === 'test') { 11 | return 'development'; 12 | } 13 | } catch (e) { 14 | // do nothing, this is okay 15 | } 16 | return 'production'; 17 | } 18 | 19 | export function setMode(mode: Mode = 'auto'): void { 20 | if (mode === 'auto') { 21 | window.vam = detectEnvironment(); 22 | return; 23 | } 24 | 25 | window.vam = mode; 26 | } 27 | 28 | export function getMode(): Mode { 29 | const mode = isBrowser() ? window.vam : detectEnvironment(); 30 | return mode || 'production'; 31 | } 32 | 33 | export function isProduction(): boolean { 34 | return getMode() === 'production'; 35 | } 36 | 37 | export function isDevelopment(): boolean { 38 | return getMode() === 'development'; 39 | } 40 | 41 | function removeKey( 42 | key: string, 43 | { [key]: _, ...rest } 44 | ): Record { 45 | return rest; 46 | } 47 | 48 | export function parseProperties( 49 | properties: Record | undefined, 50 | options: { 51 | strip?: boolean; 52 | } 53 | ): Error | Record | undefined { 54 | if (!properties) return undefined; 55 | let props = properties; 56 | const errorProperties: string[] = []; 57 | for (const [key, value] of Object.entries(properties)) { 58 | if (typeof value === 'object' && value !== null) { 59 | if (options.strip) { 60 | props = removeKey(key, props); 61 | } else { 62 | errorProperties.push(key); 63 | } 64 | } 65 | } 66 | 67 | if (errorProperties.length > 0 && !options.strip) { 68 | throw Error( 69 | `The following properties are not valid: ${errorProperties.join( 70 | ', ' 71 | )}. Only strings, numbers, booleans, and null are allowed.` 72 | ); 73 | } 74 | return props as Record; 75 | } 76 | 77 | export function computeRoute( 78 | pathname: string | null, 79 | pathParams: Record | null 80 | ): string | null { 81 | if (!pathname || !pathParams) { 82 | return pathname; 83 | } 84 | 85 | let result = pathname; 86 | try { 87 | const entries = Object.entries(pathParams); 88 | // simple keys must be handled first 89 | for (const [key, value] of entries) { 90 | if (!Array.isArray(value)) { 91 | const matcher = turnValueToRegExp(value); 92 | if (matcher.test(result)) { 93 | result = result.replace(matcher, `/[${key}]`); 94 | } 95 | } 96 | } 97 | // array values next 98 | for (const [key, value] of entries) { 99 | if (Array.isArray(value)) { 100 | const matcher = turnValueToRegExp(value.join('/')); 101 | if (matcher.test(result)) { 102 | result = result.replace(matcher, `/[...${key}]`); 103 | } 104 | } 105 | } 106 | return result; 107 | } catch (e) { 108 | return pathname; 109 | } 110 | } 111 | 112 | function turnValueToRegExp(value: string): RegExp { 113 | return new RegExp(`/${escapeRegExp(value)}(?=[/?#]|$)`); 114 | } 115 | 116 | function escapeRegExp(string: string): string { 117 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 118 | } 119 | 120 | export function getScriptSrc( 121 | props: AnalyticsProps & { basePath?: string } 122 | ): string { 123 | if (props.scriptSrc) { 124 | return props.scriptSrc; 125 | } 126 | if (isDevelopment()) { 127 | return 'https://va.vercel-scripts.com/v1/script.debug.js'; 128 | } 129 | if (props.basePath) { 130 | return `${props.basePath}/insights/script.js`; 131 | } 132 | return '/_vercel/insights/script.js'; 133 | } 134 | -------------------------------------------------------------------------------- /packages/web/src/react/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { afterEach, beforeEach, describe, it, expect } from 'vitest'; 3 | import { cleanup, render } from '@testing-library/react'; 4 | import type { AllowedPropertyValues, AnalyticsProps, Mode } from '../types'; 5 | import { Analytics, track } from './index'; 6 | 7 | describe('', () => { 8 | afterEach(() => { 9 | cleanup(); 10 | }); 11 | 12 | beforeEach(() => { 13 | window.va = undefined; 14 | // reset the internal queue before every test 15 | window.vaq = []; 16 | }); 17 | 18 | describe.each([ 19 | { 20 | mode: 'development', 21 | file: 'https://va.vercel-scripts.com/v1/script.debug.js', 22 | }, 23 | { 24 | mode: 'production', 25 | file: 'http://localhost:3000/_vercel/insights/script.js', 26 | }, 27 | ] as { mode: Mode; file: string }[])('in $mode mode', ({ mode, file }) => { 28 | it('adds the script tag correctly', () => { 29 | render(); 30 | 31 | const scripts = document.getElementsByTagName('script'); 32 | expect(scripts).toHaveLength(1); 33 | 34 | const script = document.head.querySelector('script'); 35 | expect(script).toBeDefined(); 36 | expect(script?.src).toEqual(file); 37 | expect(script).toHaveAttribute('defer'); 38 | }); 39 | 40 | it('sets and changes beforeSend', () => { 41 | const beforeSend: Required['beforeSend'] = (event) => 42 | event; 43 | const beforeSend2: Required['beforeSend'] = (event) => 44 | event; 45 | const { rerender } = render( 46 | 47 | ); 48 | 49 | expect(window.vaq?.[0]).toEqual(['beforeSend', beforeSend]); 50 | expect(window.vaq).toHaveLength(1); 51 | window.vaq?.splice(0, 1); 52 | 53 | rerender(); 54 | expect(window.vaq).toHaveLength(0); 55 | 56 | rerender(); 57 | expect(window.vaq?.[0]).toEqual(['beforeSend', beforeSend2]); 58 | expect(window.vaq).toHaveLength(1); 59 | }); 60 | 61 | it('does not change beforeSend when undefined', () => { 62 | const beforeSend: Required['beforeSend'] = (event) => 63 | event; 64 | const { rerender } = render(); 65 | 66 | expect(window.vaq?.[0]).toEqual(['beforeSend', beforeSend]); 67 | expect(window.vaq).toHaveLength(1); 68 | window.vaq?.splice(0, 1); 69 | 70 | rerender(); 71 | expect(window.vaq).toHaveLength(0); 72 | }); 73 | }); 74 | 75 | describe('track custom events', () => { 76 | describe('queue custom events', () => { 77 | it('tracks event with name only', () => { 78 | render(); 79 | track('my event'); 80 | 81 | expect(window.vaq?.[0]).toEqual([ 82 | 'event', 83 | { 84 | name: 'my event', 85 | }, 86 | ]); 87 | }); 88 | 89 | it('allows custom data to be tracked', () => { 90 | render(); 91 | const name = 'custom event'; 92 | const data = { string: 'string', number: 1 }; 93 | track(name, data); 94 | 95 | expect(window.vaq?.[0]).toEqual(['event', { name, data }]); 96 | }); 97 | 98 | it('strips data for nested objects', () => { 99 | render(); 100 | const name = 'custom event'; 101 | const data = { string: 'string', number: 1 }; 102 | track(name, { 103 | ...data, 104 | nested: { object: '' } as unknown as AllowedPropertyValues, 105 | }); 106 | 107 | expect(window.vaq?.[0]).toEqual(['event', { name, data }]); 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vercel/analytics", 3 | "version": "1.6.1", 4 | "description": "Gain real-time traffic insights with Vercel Web Analytics", 5 | "keywords": [ 6 | "analytics", 7 | "vercel" 8 | ], 9 | "repository": { 10 | "url": "github:vercel/analytics", 11 | "directory": "packages/web" 12 | }, 13 | "license": "MPL-2.0", 14 | "exports": { 15 | "./package.json": "./package.json", 16 | ".": { 17 | "browser": "./dist/index.mjs", 18 | "import": "./dist/index.mjs", 19 | "require": "./dist/index.js" 20 | }, 21 | "./astro": { 22 | "import": "./dist/astro/component.ts" 23 | }, 24 | "./next": { 25 | "browser": "./dist/next/index.mjs", 26 | "import": "./dist/next/index.mjs", 27 | "require": "./dist/next/index.js" 28 | }, 29 | "./nuxt": { 30 | "browser": "./dist/nuxt/index.mjs", 31 | "import": "./dist/nuxt/index.mjs", 32 | "require": "./dist/nuxt/index.js" 33 | }, 34 | "./react": { 35 | "browser": "./dist/react/index.mjs", 36 | "import": "./dist/react/index.mjs", 37 | "require": "./dist/react/index.js" 38 | }, 39 | "./remix": { 40 | "browser": "./dist/remix/index.mjs", 41 | "import": "./dist/remix/index.mjs", 42 | "require": "./dist/remix/index.js" 43 | }, 44 | "./server": { 45 | "node": "./dist/server/index.mjs", 46 | "edge-light": "./dist/server/index.mjs", 47 | "import": "./dist/server/index.mjs", 48 | "require": "./dist/server/index.js", 49 | "default": "./dist/server/index.js" 50 | }, 51 | "./sveltekit": { 52 | "svelte": "./dist/sveltekit/index.mjs", 53 | "types": "./dist/sveltekit/index.d.ts" 54 | }, 55 | "./vue": { 56 | "browser": "./dist/vue/index.mjs", 57 | "import": "./dist/vue/index.mjs", 58 | "require": "./dist/vue/index.js" 59 | } 60 | }, 61 | "main": "./dist/index.mjs", 62 | "types": "./dist/index.d.ts", 63 | "typesVersions": { 64 | "*": { 65 | "*": [ 66 | "dist/index.d.ts" 67 | ], 68 | "next": [ 69 | "dist/next/index.d.ts" 70 | ], 71 | "nuxt": [ 72 | "dist/nuxt/index.d.ts" 73 | ], 74 | "react": [ 75 | "dist/react/index.d.ts" 76 | ], 77 | "remix": [ 78 | "dist/remix/index.d.ts" 79 | ], 80 | "server": [ 81 | "dist/server/index.d.ts" 82 | ], 83 | "sveltekit": [ 84 | "dist/sveltekit/index.d.ts" 85 | ], 86 | "vue": [ 87 | "dist/vue/index.d.ts" 88 | ] 89 | } 90 | }, 91 | "scripts": { 92 | "build": "tsup && pnpm copy-astro", 93 | "copy-astro": "cp -R src/astro dist/", 94 | "dev": "pnpm copy-astro && tsup --watch", 95 | "lint": "eslint .", 96 | "lint-fix": "eslint . --fix", 97 | "test": "vitest", 98 | "type-check": "tsc --noEmit" 99 | }, 100 | "eslintConfig": { 101 | "extends": [ 102 | "@vercel/eslint-config" 103 | ], 104 | "rules": { 105 | "tsdoc/syntax": "off" 106 | }, 107 | "ignorePatterns": [ 108 | "jest.setup.ts" 109 | ] 110 | }, 111 | "devDependencies": { 112 | "@swc/core": "^1.9.2", 113 | "@testing-library/jest-dom": "^6.6.3", 114 | "@testing-library/react": "^16.0.1", 115 | "@types/node": "^22.9.0", 116 | "@types/react": "^18.3.12", 117 | "@vercel/eslint-config": "workspace:0.0.0", 118 | "server-only": "^0.0.1", 119 | "svelte": "^5.1.10", 120 | "tsup": "8.3.5", 121 | "vitest": "^2.1.5", 122 | "vue": "^3.5.12", 123 | "vue-router": "^4.4.5" 124 | }, 125 | "peerDependencies": { 126 | "@remix-run/react": "^2", 127 | "@sveltejs/kit": "^1 || ^2", 128 | "next": ">= 13", 129 | "react": "^18 || ^19 || ^19.0.0-rc", 130 | "svelte": ">= 4", 131 | "vue": "^3", 132 | "vue-router": "^4" 133 | }, 134 | "peerDependenciesMeta": { 135 | "@remix-run/react": { 136 | "optional": true 137 | }, 138 | "@sveltejs/kit": { 139 | "optional": true 140 | }, 141 | "next": { 142 | "optional": true 143 | }, 144 | "react": { 145 | "optional": true 146 | }, 147 | "svelte": { 148 | "optional": true 149 | }, 150 | "vue": { 151 | "optional": true 152 | }, 153 | "vue-router": { 154 | "optional": true 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /packages/web/src/generic.ts: -------------------------------------------------------------------------------- 1 | import { name as packageName, version } from '../package.json'; 2 | import { initQueue } from './queue'; 3 | import type { 4 | AllowedPropertyValues, 5 | AnalyticsProps, 6 | FlagsDataInput, 7 | BeforeSend, 8 | BeforeSendEvent, 9 | } from './types'; 10 | import { 11 | isBrowser, 12 | parseProperties, 13 | setMode, 14 | isDevelopment, 15 | isProduction, 16 | computeRoute, 17 | getScriptSrc, 18 | } from './utils'; 19 | 20 | /** 21 | * Injects the Vercel Web Analytics script into the page head and starts tracking page views. Read more in our [documentation](https://vercel.com/docs/concepts/analytics/package). 22 | * @param [props] - Analytics options. 23 | * @param [props.mode] - The mode to use for the analytics script. Defaults to `auto`. 24 | * - `auto` - Automatically detect the environment. Uses `production` if the environment cannot be determined. 25 | * - `production` - Always use the production script. (Sends events to the server) 26 | * - `development` - Always use the development script. (Logs events to the console) 27 | * @param [props.debug] - Whether to enable debug logging in development. Defaults to `true`. 28 | * @param [props.beforeSend] - A middleware function to modify events before they are sent. Should return the event object or `null` to cancel the event. 29 | * @param [props.dsn] - The DSN of the project to send events to. Only required when self-hosting. 30 | * @param [props.disableAutoTrack] - Whether the injected script should track page views from pushState events. Disable if route is updated after pushState, a manually call page pageview(). 31 | */ 32 | function inject( 33 | props: AnalyticsProps & { 34 | framework?: string; 35 | disableAutoTrack?: boolean; 36 | basePath?: string; 37 | } = { 38 | debug: true, 39 | } 40 | ): void { 41 | if (!isBrowser()) return; 42 | 43 | setMode(props.mode); 44 | 45 | initQueue(); 46 | 47 | if (props.beforeSend) { 48 | window.va?.('beforeSend', props.beforeSend); 49 | } 50 | 51 | const src = getScriptSrc(props); 52 | 53 | if (document.head.querySelector(`script[src*="${src}"]`)) return; 54 | 55 | const script = document.createElement('script'); 56 | script.src = src; 57 | script.defer = true; 58 | script.dataset.sdkn = 59 | packageName + (props.framework ? `/${props.framework}` : ''); 60 | script.dataset.sdkv = version; 61 | 62 | if (props.disableAutoTrack) { 63 | script.dataset.disableAutoTrack = '1'; 64 | } 65 | if (props.endpoint) { 66 | script.dataset.endpoint = props.endpoint; 67 | } else if (props.basePath) { 68 | script.dataset.endpoint = `${props.basePath}/insights`; 69 | } 70 | if (props.dsn) { 71 | script.dataset.dsn = props.dsn; 72 | } 73 | 74 | script.onerror = (): void => { 75 | const errorMessage = isDevelopment() 76 | ? 'Please check if any ad blockers are enabled and try again.' 77 | : 'Be sure to enable Web Analytics for your project and deploy again. See https://vercel.com/docs/analytics/quickstart for more information.'; 78 | 79 | // eslint-disable-next-line no-console -- Logging to console is intentional 80 | console.log( 81 | `[Vercel Web Analytics] Failed to load script from ${src}. ${errorMessage}` 82 | ); 83 | }; 84 | 85 | if (isDevelopment() && props.debug === false) { 86 | script.dataset.debug = 'false'; 87 | } 88 | 89 | document.head.appendChild(script); 90 | } 91 | 92 | /** 93 | * Tracks a custom event. Please refer to the [documentation](https://vercel.com/docs/concepts/analytics/custom-events) for more information on custom events. 94 | * @param name - The name of the event. 95 | * * Examples: `Purchase`, `Click Button`, or `Play Video`. 96 | * @param [properties] - Additional properties of the event. Nested objects are not supported. Allowed values are `string`, `number`, `boolean`, and `null`. 97 | */ 98 | function track( 99 | name: string, 100 | properties?: Record, 101 | options?: { 102 | flags?: FlagsDataInput; 103 | } 104 | ): void { 105 | if (!isBrowser()) { 106 | const msg = 107 | '[Vercel Web Analytics] Please import `track` from `@vercel/analytics/server` when using this function in a server environment'; 108 | 109 | if (isProduction()) { 110 | // eslint-disable-next-line no-console -- Show warning in production 111 | console.warn(msg); 112 | } else { 113 | throw new Error(msg); 114 | } 115 | 116 | return; 117 | } 118 | 119 | if (!properties) { 120 | window.va?.('event', { name, options }); 121 | return; 122 | } 123 | 124 | try { 125 | const props = parseProperties(properties, { 126 | strip: isProduction(), 127 | }); 128 | 129 | window.va?.('event', { 130 | name, 131 | data: props, 132 | options, 133 | }); 134 | } catch (err) { 135 | if (err instanceof Error && isDevelopment()) { 136 | // eslint-disable-next-line no-console -- Logging to console is intentional 137 | console.error(err); 138 | } 139 | } 140 | } 141 | 142 | function pageview({ 143 | route, 144 | path, 145 | }: { 146 | route?: string | null; 147 | path?: string; 148 | }): void { 149 | window.va?.('pageview', { route, path }); 150 | } 151 | 152 | export { inject, track, pageview, computeRoute }; 153 | export type { AnalyticsProps, BeforeSend, BeforeSendEvent }; 154 | 155 | // eslint-disable-next-line import/no-default-export -- Default export is intentional 156 | export default { 157 | inject, 158 | track, 159 | computeRoute, 160 | }; 161 | -------------------------------------------------------------------------------- /apps/nextjs/e2e/production/pageview.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { useMockForProductionScript } from '../utils'; 3 | 4 | test.describe('pageview', () => { 5 | test('should track page views when navigating between pages', async ({ 6 | page, 7 | }) => { 8 | const payloads: { page: string; payload: Object }[] = []; 9 | 10 | await useMockForProductionScript({ 11 | page, 12 | onPageView: (page, payload) => { 13 | payloads.push({ page, payload }); 14 | }, 15 | }); 16 | 17 | await page.goto('/navigation/first'); 18 | await page.waitForLoadState('networkidle'); 19 | 20 | await page.click('text=Next'); 21 | 22 | await expect(page).toHaveURL('/navigation/second'); 23 | await expect(page.locator('h1')).toContainText('Second Page'); 24 | 25 | await page.waitForLoadState('networkidle'); 26 | 27 | expect(payloads).toEqual([ 28 | { 29 | page: 'http://localhost:3000/navigation/first', 30 | payload: { 31 | o: 'http://localhost:3000/navigation/first', 32 | ts: expect.any(Number), 33 | r: '', 34 | sv: expect.any(String), 35 | sdkn: '@vercel/analytics/next', 36 | sdkv: expect.any(String), 37 | dp: '/navigation/first', 38 | }, 39 | }, 40 | { 41 | page: 'http://localhost:3000/navigation/second', 42 | payload: { 43 | o: 'http://localhost:3000/navigation/second', 44 | ts: expect.any(Number), 45 | sv: expect.any(String), 46 | sdkn: '@vercel/analytics/next', 47 | sdkv: expect.any(String), 48 | dp: '/navigation/second', 49 | }, 50 | }, 51 | ]); 52 | }); 53 | 54 | test('should properly send dynamic route', async ({ page }) => { 55 | const payloads: { page: string; payload: Object }[] = []; 56 | 57 | await useMockForProductionScript({ 58 | page, 59 | onPageView: (page, payload) => { 60 | payloads.push({ page, payload }); 61 | }, 62 | }); 63 | 64 | await page.goto('/blog'); 65 | await page.waitForLoadState('networkidle'); 66 | 67 | await page.click('text=My first blog post'); 68 | 69 | await expect(page).toHaveURL('/blog/my-first-blogpost'); 70 | await expect(page.locator('h2')).toContainText('my-first-blogpost'); 71 | 72 | await page.waitForLoadState('networkidle'); 73 | 74 | await page.click('text=Back to blog'); 75 | 76 | await page.waitForLoadState('networkidle'); 77 | await expect(page).toHaveURL('/blog'); 78 | 79 | await page.click('text=Feature just got released'); 80 | 81 | await expect(page.locator('h2')).toContainText('new-feature-release'); 82 | 83 | expect(payloads).toEqual([ 84 | { 85 | page: 'http://localhost:3000/blog', 86 | payload: { 87 | dp: '/blog', 88 | o: 'http://localhost:3000/blog', 89 | r: '', 90 | sdkn: '@vercel/analytics/next', 91 | sdkv: expect.any(String), 92 | sv: expect.any(String), 93 | ts: expect.any(Number), 94 | }, 95 | }, 96 | { 97 | page: 'http://localhost:3000/blog/my-first-blogpost', 98 | payload: { 99 | dp: '/blog/[slug]', 100 | o: 'http://localhost:3000/blog/my-first-blogpost', 101 | sdkn: '@vercel/analytics/next', 102 | sdkv: expect.any(String), 103 | sv: expect.any(String), 104 | ts: expect.any(Number), 105 | }, 106 | }, 107 | { 108 | page: 'http://localhost:3000/blog', 109 | payload: { 110 | dp: '/blog', 111 | o: 'http://localhost:3000/blog', 112 | sdkn: '@vercel/analytics/next', 113 | sdkv: expect.any(String), 114 | sv: expect.any(String), 115 | ts: expect.any(Number), 116 | }, 117 | }, 118 | { 119 | page: 'http://localhost:3000/blog/new-feature-release', 120 | payload: { 121 | dp: '/blog/[slug]', 122 | o: 'http://localhost:3000/blog/new-feature-release', 123 | sdkn: '@vercel/analytics/next', 124 | sdkv: expect.any(String), 125 | sv: expect.any(String), 126 | ts: expect.any(Number), 127 | }, 128 | }, 129 | ]); 130 | }); 131 | 132 | test('should send pageviews when route doesnt change but path does', async ({ 133 | page, 134 | }) => { 135 | const payloads: { page: string; payload: Object }[] = []; 136 | 137 | await useMockForProductionScript({ 138 | page, 139 | onPageView: (page, payload) => { 140 | payloads.push({ page, payload }); 141 | }, 142 | }); 143 | 144 | await page.goto('/blog/my-first-blogpost'); 145 | await page.waitForLoadState('networkidle'); 146 | 147 | await page.click('text=Feature just got released'); 148 | 149 | await expect(page.locator('h2')).toContainText('new-feature-release'); 150 | 151 | expect(payloads).toEqual([ 152 | { 153 | page: 'http://localhost:3000/blog/my-first-blogpost', 154 | payload: { 155 | dp: '/blog/[slug]', 156 | o: 'http://localhost:3000/blog/my-first-blogpost', 157 | sdkn: '@vercel/analytics/next', 158 | sdkv: expect.any(String), 159 | sv: expect.any(String), 160 | ts: expect.any(Number), 161 | r: '', 162 | }, 163 | }, 164 | { 165 | page: 'http://localhost:3000/blog/new-feature-release', 166 | payload: { 167 | dp: '/blog/[slug]', 168 | o: 'http://localhost:3000/blog/new-feature-release', 169 | sdkn: '@vercel/analytics/next', 170 | sdkv: expect.any(String), 171 | sv: expect.any(String), 172 | ts: expect.any(Number), 173 | }, 174 | }, 175 | ]); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /packages/web/src/server/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console -- Allow logging on the server */ 2 | import type { 3 | AllowedPropertyValues, 4 | FlagsDataInput, 5 | PlainFlags, 6 | } from '../types'; 7 | import { isProduction, parseProperties } from '../utils'; 8 | 9 | type HeadersObject = Record; 10 | type AllowedHeaders = Headers | HeadersObject; 11 | 12 | function isHeaders(headers?: AllowedHeaders): headers is Headers { 13 | if (!headers) return false; 14 | return typeof (headers as HeadersObject).entries === 'function'; 15 | } 16 | 17 | interface Options { 18 | flags?: FlagsDataInput; 19 | headers?: AllowedHeaders; 20 | request?: { headers: AllowedHeaders }; 21 | } 22 | 23 | interface RequestContext { 24 | get: () => { 25 | headers: Record; 26 | url: string; 27 | waitUntil?: (promise: Promise) => void; 28 | flags?: { 29 | getValues: () => PlainFlags; 30 | reportValue: (key: string, value: unknown) => void; 31 | }; 32 | }; 33 | } 34 | 35 | const symbol = Symbol.for('@vercel/request-context'); 36 | const logPrefix = '[Vercel Web Analytics]'; 37 | 38 | export async function track( 39 | eventName: string, 40 | properties?: Record, 41 | options?: Options 42 | ): Promise { 43 | const ENDPOINT = 44 | process.env.VERCEL_WEB_ANALYTICS_ENDPOINT || process.env.VERCEL_URL; 45 | const DISABLE_LOGS = Boolean(process.env.VERCEL_WEB_ANALYTICS_DISABLE_LOGS); 46 | const BYPASS_SECRET = process.env.VERCEL_AUTOMATION_BYPASS_SECRET; 47 | 48 | if (typeof window !== 'undefined') { 49 | if (!isProduction()) { 50 | throw new Error( 51 | `${logPrefix} It seems like you imported the \`track\` function from \`@vercel/web-analytics/server\` in a browser environment. This function is only meant to be used in a server environment.` 52 | ); 53 | } 54 | 55 | return; 56 | } 57 | 58 | const props = parseProperties(properties, { 59 | strip: isProduction(), 60 | }); 61 | 62 | if (!ENDPOINT) { 63 | if (isProduction()) { 64 | console.log( 65 | `${logPrefix} Can't find VERCEL_URL in environment variables.` 66 | ); 67 | } else if (!DISABLE_LOGS) { 68 | console.log( 69 | `${logPrefix} Track "${eventName}" ${ 70 | props ? `with data ${JSON.stringify(props)}` : '' 71 | }` 72 | ); 73 | } 74 | return; 75 | } 76 | try { 77 | const requestContext = ( 78 | (globalThis as never)[symbol] as RequestContext | undefined 79 | )?.get(); 80 | 81 | let headers: AllowedHeaders | undefined; 82 | 83 | if (options && 'headers' in options) { 84 | headers = options.headers; 85 | } else if (options?.request) { 86 | headers = options.request.headers; 87 | } else if (requestContext?.headers) { 88 | // not explicitly passed in context, so take it from async storage 89 | headers = requestContext.headers; 90 | } 91 | 92 | let tmp: HeadersObject = {}; 93 | if (headers && isHeaders(headers)) { 94 | headers.forEach((value, key) => { 95 | tmp[key] = value; 96 | }); 97 | } else if (headers) { 98 | tmp = headers; 99 | } 100 | 101 | const origin = 102 | requestContext?.url || (tmp.referer as string) || `https://${ENDPOINT}`; 103 | 104 | const url = new URL(origin); 105 | 106 | const body = { 107 | o: origin, 108 | ts: new Date().getTime(), 109 | r: '', 110 | en: eventName, 111 | ed: props, 112 | f: safeGetFlags(options?.flags, requestContext), 113 | }; 114 | 115 | const hasHeaders = Boolean(headers); 116 | 117 | if (!hasHeaders) { 118 | throw new Error( 119 | 'No session context found. Pass `request` or `headers` to the `track` function.' 120 | ); 121 | } 122 | 123 | const promise = fetch(`${url.origin}/_vercel/insights/event`, { 124 | headers: { 125 | 'content-type': 'application/json', 126 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- The throwing is temporary until we add support for non Vercel hosted environments 127 | ...(hasHeaders 128 | ? { 129 | 'user-agent': tmp['user-agent'] as string, 130 | 'x-vercel-ip': tmp['x-forwarded-for'] as string, 131 | 'x-va-server': '1', 132 | cookie: tmp.cookie as string, 133 | } 134 | : { 135 | 'x-va-server': '2', 136 | }), 137 | ...(BYPASS_SECRET 138 | ? { 'x-vercel-protection-bypass': BYPASS_SECRET } 139 | : {}), 140 | }, 141 | body: JSON.stringify(body), 142 | method: 'POST', 143 | }) 144 | // We want to always consume the body; some cloud providers track fetch concurrency 145 | // and may not release the connection until the body is consumed. 146 | .then((response) => response.text()) 147 | .catch((err: unknown) => { 148 | if (err instanceof Error && 'response' in err) { 149 | console.error(err.response); 150 | } else { 151 | console.error(err); 152 | } 153 | }); 154 | 155 | if (requestContext?.waitUntil) { 156 | requestContext.waitUntil(promise); 157 | } else { 158 | await promise; 159 | } 160 | 161 | return void 0; 162 | } catch (err) { 163 | console.error(err); 164 | } 165 | } 166 | 167 | function safeGetFlags( 168 | flags: Options['flags'], 169 | requestContext?: ReturnType 170 | ): 171 | | { 172 | p: PlainFlags; 173 | } 174 | | undefined { 175 | try { 176 | if (!requestContext || !flags) return; 177 | // In the case plain flags are passed, just return them 178 | if (!Array.isArray(flags)) { 179 | return { p: flags }; 180 | } 181 | 182 | const plainFlags: Record = {}; 183 | // returns all available plain flags 184 | const resolvedPlainFlags = requestContext.flags?.getValues() ?? {}; 185 | 186 | for (const flag of flags) { 187 | if (typeof flag === 'string') { 188 | // only picks the desired flags 189 | plainFlags[flag] = resolvedPlainFlags[flag]; 190 | } else { 191 | // merge user-provided values with resolved values 192 | Object.assign(plainFlags, flag); 193 | } 194 | } 195 | 196 | return { p: plainFlags }; 197 | } catch { 198 | /* empty */ 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /packages/web/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeAll, describe, it, expect } from 'vitest'; 2 | import { 3 | computeRoute, 4 | getMode, 5 | getScriptSrc, 6 | parseProperties, 7 | setMode, 8 | } from './utils'; 9 | 10 | describe('utils', () => { 11 | describe('parse properties', () => { 12 | describe('strip', () => { 13 | it('should allow all properties', () => { 14 | const properties = { 15 | number: 10, 16 | string: 'some-string', 17 | boolean: true, 18 | nullable: null, 19 | }; 20 | 21 | const parsed = parseProperties(properties, { strip: true }); 22 | 23 | expect(properties).toEqual(parsed); 24 | }); 25 | 26 | it('should dismiss array and object', () => { 27 | const properties = { 28 | string: 'some-string', 29 | array: [], 30 | object: {}, 31 | }; 32 | 33 | const parsed = parseProperties(properties, { strip: true }); 34 | 35 | expect({ 36 | string: 'some-string', 37 | }).toEqual(parsed); 38 | }); 39 | }); 40 | 41 | describe('throw error', () => { 42 | it('should allow all properties', () => { 43 | const properties = { 44 | number: 10, 45 | string: 'some-string', 46 | boolean: true, 47 | nullable: null, 48 | }; 49 | 50 | const parsed = parseProperties(properties, { 51 | strip: false, 52 | }); 53 | 54 | expect(properties).toEqual(parsed); 55 | }); 56 | 57 | it('should throw an error for arrayProp and objectProp', () => { 58 | const properties = { 59 | string: 'some-string', 60 | arrayProp: [], 61 | objectProp: {}, 62 | }; 63 | 64 | expect(() => { 65 | parseProperties(properties, { strip: false }); 66 | }).toThrow(/arrayProp, objectProp/); 67 | }); 68 | }); 69 | }); 70 | 71 | describe('setMode', () => { 72 | describe('in production mode', () => { 73 | beforeAll(() => { 74 | process.env.NODE_ENV = 'production'; 75 | }); 76 | 77 | it('should set mode automatically if undefined', () => { 78 | setMode(); 79 | expect(getMode()).toBe('production'); 80 | }); 81 | 82 | it('should overwrite when set manually', () => { 83 | setMode('development'); 84 | expect(getMode()).toBe('development'); 85 | }); 86 | 87 | it('should set correctly when set to auto', () => { 88 | setMode('auto'); 89 | expect(getMode()).toBe('production'); 90 | }); 91 | }); 92 | 93 | describe('in development mode', () => { 94 | beforeAll(() => { 95 | process.env.NODE_ENV = 'development'; 96 | }); 97 | 98 | it('should set mode automatically if undefined', () => { 99 | setMode(); 100 | expect(getMode()).toBe('development'); 101 | }); 102 | 103 | it('should overwrite when set manually', () => { 104 | setMode('production'); 105 | expect(getMode()).toBe('production'); 106 | }); 107 | }); 108 | }); 109 | 110 | describe('computeRoute()', () => { 111 | it('returns unchanged pathname if no pathParams provided', () => { 112 | expect(computeRoute('/vercel/next-site/analytics', null)).toBe( 113 | '/vercel/next-site/analytics' 114 | ); 115 | }); 116 | 117 | it('returns null for null pathname', () => { 118 | expect(computeRoute(null, {})).toBe(null); 119 | }); 120 | 121 | it('replaces segments', () => { 122 | const input = '/vercel/next-site/analytics'; 123 | const params = { 124 | teamSlug: 'vercel', 125 | project: 'next-site', 126 | }; 127 | const expected = '/[teamSlug]/[project]/analytics'; 128 | expect(computeRoute(input, params)).toBe(expected); 129 | }); 130 | 131 | it('replaces segments even one param is not used', () => { 132 | const input = '/vercel/next-site/analytics'; 133 | const params = { 134 | lang: 'en', 135 | teamSlug: 'vercel', 136 | project: 'next-site', 137 | }; 138 | const expected = '/[teamSlug]/[project]/analytics'; 139 | expect(computeRoute(input, params)).toBe(expected); 140 | }); 141 | 142 | it('must not replace partial segments', () => { 143 | const input = '/next-site/vercel-site'; 144 | const params = { 145 | teamSlug: 'vercel', 146 | }; 147 | const expected = '/next-site/vercel-site'; // remains unchanged because "vercel" is a partial match 148 | expect(computeRoute(input, params)).toBe(expected); 149 | }); 150 | 151 | it('handles array segments', () => { 152 | const input = '/en/us/next-site'; 153 | const params = { 154 | langs: ['en', 'us'], 155 | }; 156 | const expected = '/[...langs]/next-site'; 157 | expect(computeRoute(input, params)).toBe(expected); 158 | }); 159 | 160 | it('handles array segments and individual segments', () => { 161 | const input = '/en/us/next-site'; 162 | const params = { 163 | langs: ['en', 'us'], 164 | team: 'next-site', 165 | }; 166 | const expected = '/[...langs]/[team]'; 167 | expect(computeRoute(input, params)).toBe(expected); 168 | }); 169 | 170 | it('handles special characters in url', () => { 171 | const input = '/123/test(test'; 172 | const params = { 173 | teamSlug: '123', 174 | project: 'test(test', 175 | }; 176 | 177 | const expected = '/[teamSlug]/[project]'; 178 | expect(computeRoute(input, params)).toBe(expected); 179 | }); 180 | 181 | it('handles special more characters', () => { 182 | const input = '/123/tes\\t(test/3.*'; 183 | const params = { 184 | teamSlug: '123', 185 | }; 186 | 187 | const expected = '/[teamSlug]/tes\\t(test/3.*'; 188 | expect(computeRoute(input, params)).toBe(expected); 189 | }); 190 | 191 | it('parallel routes where params matched both individually and within arrays', () => { 192 | const params = { 193 | catchAll: ['m', 'john', 'p', 'shirt'], 194 | merchantId: 'john', 195 | productSlug: 'shirt', 196 | }; 197 | expect(computeRoute('/m/john/p/shirt', params)).toBe( 198 | '/m/[merchantId]/p/[productSlug]' 199 | ); 200 | }); 201 | 202 | describe('edge case handling (same values for multiple params)', () => { 203 | it('replaces based on the priority of the pathParams keys', () => { 204 | const input = '/test/test'; 205 | const params = { 206 | teamSlug: 'test', 207 | project: 'test', 208 | }; 209 | const expected = '/[teamSlug]/[project]'; // 'teamSlug' takes priority over 'project' based on their order in the params object 210 | expect(computeRoute(input, params)).toBe(expected); 211 | }); 212 | 213 | it('handles reversed priority', () => { 214 | const input = '/test/test'; 215 | const params = { 216 | project: 'test', 217 | teamSlug: 'test', 218 | }; 219 | const expected = '/[project]/[teamSlug]'; // 'project' takes priority over 'teamSlug' here due to the reversed order in the params object 220 | expect(computeRoute(input, params)).toBe(expected); 221 | }); 222 | }); 223 | }); 224 | 225 | describe('getScriptSrc()', () => { 226 | const envSave = { ...process.env }; 227 | 228 | afterEach(() => { 229 | window.vam = undefined; 230 | process.env = { ...envSave }; 231 | }); 232 | 233 | it('returns debug script in development', () => { 234 | window.vam = 'development'; 235 | expect(getScriptSrc({})).toBe( 236 | 'https://va.vercel-scripts.com/v1/script.debug.js' 237 | ); 238 | }); 239 | 240 | it('returns the specified prop in development', () => { 241 | const scriptSrc = `https://example.com/${Math.random()}/script.js`; 242 | window.vam = 'development'; 243 | expect(getScriptSrc({ scriptSrc })).toBe(scriptSrc); 244 | }); 245 | 246 | it('returns generic route in production', () => { 247 | expect(getScriptSrc({})).toBe('/_vercel/insights/script.js'); 248 | }); 249 | 250 | it('returns base path in production', () => { 251 | const basePath = `/_vercel-${Math.random()}`; 252 | expect(getScriptSrc({ basePath })).toBe(`${basePath}/insights/script.js`); 253 | }); 254 | 255 | it('returns the specified prop in production', () => { 256 | const scriptSrc = `https://example.com/${Math.random()}/script.js`; 257 | expect(getScriptSrc({ scriptSrc })).toBe(scriptSrc); 258 | }); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /packages/web/LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. --------------------------------------------------------------------------------