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.
--------------------------------------------------------------------------------