@dash0/sdk-web 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dash0/sdk-web",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "Dash0's Web SDK to collect telemetry from end-users' web browsers",
5
5
  "type": "module",
6
6
  "main": "dist/dash0.umd.cjs",
@@ -68,11 +68,9 @@
68
68
  "url": "https://github.com/dash0hq/dash0-sdk-web/issues"
69
69
  },
70
70
  "dependencies": {
71
- "web-vitals": "^5.0.3",
72
- "ts-deepmerge": "^7.0.3"
71
+ "web-vitals": "^5.0.3"
73
72
  },
74
73
  "devDependencies": {
75
- "dotenv-cli": "^8.0.0",
76
74
  "@babel/core": "^7.26.10",
77
75
  "@babel/preset-env": "^7.26.9",
78
76
  "@release-it/conventional-changelog": "^10.0.1",
@@ -94,8 +92,8 @@
94
92
  "@wdio/mocha-framework": "^9.12.5",
95
93
  "@wdio/sauce-service": "^9.12.5",
96
94
  "@wdio/spec-reporter": "^9.12.3",
97
- "wdio-lambdatest-service": "^4.0.0",
98
95
  "body-parser": "^2.2.0",
96
+ "dotenv-cli": "^8.0.0",
99
97
  "eslint": "^9.24.0",
100
98
  "express": "^5.1.0",
101
99
  "google-closure-compiler": "^20240317.0.0",
@@ -112,6 +110,7 @@
112
110
  "typescript": "^5.8.3",
113
111
  "uuid": "^11.1.0",
114
112
  "vitest": "^3.1.1",
113
+ "wdio-lambdatest-service": "^4.0.0",
115
114
  "webdriverio": "^9.12.5"
116
115
  },
117
116
  "resolutions": {
@@ -0,0 +1,94 @@
1
+ // Shared types and helpers for reading build-environment variables exposed
2
+ // to the browser bundle. Owned by this module because it is used from both
3
+ // init.ts (environment / deployment detection) and vcs.ts (VCS detection).
4
+ //
5
+ // IMPORTANT: each `process.env.NAME` MUST be a LITERAL accessor in the
6
+ // caller's source. Webpack DefinePlugin, Next.js, Gatsby, and equivalents
7
+ // substitute env vars at build time only when they see the literal form.
8
+ // Dynamic lookups (`process.env[name]`, iterating a string array) are NOT
9
+ // substituted — they resolve to `undefined` in the browser bundle.
10
+ //
11
+ // The type unions below are the single source of truth for which env var
12
+ // names the SDK reads. They are composed via template-literal types so
13
+ // every (prefix × suffix) combination is enumerated at the type level.
14
+ // Adding a framework prefix is a one-line edit to `FrameworkPrefix`; adding
15
+ // a suffix is a one-line edit to the appropriate suffix union.
16
+
17
+ /**
18
+ * Framework prefixes the SDK reads as literal `process.env.{PREFIX}{SUFFIX}`
19
+ * accessors. Vercel auto-prefixes its `VERCEL_*` system vars under each
20
+ * (per https://vercel.com/docs/environment-variables/framework-environment-variables).
21
+ * Users on platforms without auto-prefixing (Netlify, Cloudflare Pages,
22
+ * custom CI) can expose env vars under any of these prefixes to get the
23
+ * same detection.
24
+ *
25
+ * Caveat: a prefix being listed here means the SDK *will read* a literal
26
+ * `process.env.{PREFIX}_X` accessor — it does not guarantee the consumer's
27
+ * bundler will substitute that accessor. Webpack-based bundlers (Next.js,
28
+ * Gatsby, CRA) substitute `process.env.X` literals by default. Vite reads
29
+ * env vars via `import.meta.env.VITE_*` by default; for `process.env.VITE_*`
30
+ * to be substituted in a Vite build the consumer must add
31
+ * `define: { 'process.env.VITE_X': JSON.stringify(...) }` to their Vite
32
+ * config, or use a `process.env` polyfill plugin. When deploying to Vercel
33
+ * the `VITE_VERCEL_*` substitution happens inside the build environment, so
34
+ * Vite + Vercel works out of the box; Vite users on other platforms need to
35
+ * surface env vars via their own bundler config.
36
+ *
37
+ * Keep `FRAMEWORK_PREFIX_SAMPLES` in `init_test.ts` in sync when adding a
38
+ * prefix here.
39
+ */
40
+ export type FrameworkPrefix =
41
+ | "NEXT_PUBLIC_"
42
+ | "NUXT_PUBLIC_"
43
+ | "NUXT_ENV_"
44
+ | "REACT_APP_"
45
+ | "GATSBY_"
46
+ | "VITE_"
47
+ | "PUBLIC_"
48
+ | "VUE_APP_"
49
+ | "REDWOOD_ENV_"
50
+ | "SANITY_STUDIO_";
51
+
52
+ /** Vercel system env vars consumed by detectEnvironment / detectDeploymentName / detectDeploymentId. */
53
+ type VercelDeploymentSuffix = "VERCEL_ENV" | "VERCEL_TARGET_ENV" | "VERCEL_BRANCH_URL";
54
+
55
+ /** Vercel git env vars consumed by VCS detection. */
56
+ type VercelGitSuffix =
57
+ | "VERCEL_GIT_PROVIDER"
58
+ | "VERCEL_GIT_REPO_OWNER"
59
+ | "VERCEL_GIT_REPO_SLUG"
60
+ | "VERCEL_GIT_COMMIT_REF"
61
+ | "VERCEL_GIT_COMMIT_SHA"
62
+ | "VERCEL_GIT_PULL_REQUEST_ID";
63
+
64
+ /** Netlify build env vars (Netlify does not auto-prefix; users surface these via their framework convention). */
65
+ type NetlifyGitSuffix = "REPOSITORY_URL" | "BRANCH" | "COMMIT_REF" | "REVIEW_ID";
66
+
67
+ /** Every env var name the SDK reads as a literal `process.env.X` accessor. */
68
+ type BrowserBuildEnvKey = `${FrameworkPrefix}${VercelDeploymentSuffix | VercelGitSuffix | NetlifyGitSuffix}`;
69
+
70
+ /**
71
+ * Module-locally typed shape of `process.env`. Consumers re-declare `process`
72
+ * with this type so dot-notation accessors are typed (no `@ts-expect-error`)
73
+ * and `noPropertyAccessFromIndexSignature` does not fire (each key is a
74
+ * known union member, not an index signature).
75
+ */
76
+ export type BrowserBuildEnv = { readonly [K in BrowserBuildEnvKey]?: string };
77
+
78
+ /**
79
+ * Return the first truthy value, or undefined. Used to walk the list of
80
+ * framework-prefixed variants of an env var and pick whichever the user's
81
+ * bundler substituted at build time.
82
+ *
83
+ * The falsy check skips empty strings as well as undefined — bundlers that
84
+ * do not substitute a literal leave it as `undefined`, not `""`, so empty
85
+ * is treated as "not set". This is intentional and matches every known
86
+ * `VERCEL_GIT_*` / `REPOSITORY_URL` / etc. value shape (non-empty strings
87
+ * or absent; Vercel PR IDs are positive integer strings, never `"0"`).
88
+ */
89
+ export function pickFirstString(...values: (string | undefined)[]): string | undefined {
90
+ for (const value of values) {
91
+ if (value) return value;
92
+ }
93
+ return undefined;
94
+ }
package/src/api/init.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import {
13
13
  fetch,
14
14
  generateUniqueId,
15
+ isSessionSampledIn,
15
16
  isSafeServiceName,
16
17
  PAGE_LOAD_ID_BYTES,
17
18
  warn,
@@ -23,17 +24,19 @@ import {
23
24
  pick,
24
25
  loc,
25
26
  } from "../utils";
26
- import { trackSessions } from "./session";
27
+ import { sessionId, trackSessions } from "./session";
27
28
  import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
28
29
  import { startErrorInstrumentation } from "../instrumentations/errors";
29
30
  import { addAttribute } from "../utils/otel";
30
31
  import { instrumentFetch } from "../instrumentations/http/fetch";
31
32
  import { startNavigationInstrumentation } from "../instrumentations/navigation";
32
- import { merge } from "ts-deepmerge";
33
33
  import { initializeTabId } from "../utils/tab-id";
34
34
  import { InitOptions, InstrumentationName } from "../types/options";
35
+ import { BrowserBuildEnv, pickFirstString } from "./browser-env";
35
36
  import { applyVcsResourceAttributes } from "./vcs";
36
37
 
38
+ declare const process: { env?: BrowserBuildEnv } | undefined;
39
+
37
40
  let hasBeenInitialised: boolean = false;
38
41
 
39
42
  export function init(opts: InitOptions) {
@@ -94,6 +97,17 @@ export function init(opts: InitOptions) {
94
97
  initializeTabId();
95
98
  trackSessions(opts.sessionInactivityTimeoutMillis, opts.sessionTerminationTimeoutMillis);
96
99
 
100
+ if (opts.sessionSamplingRate != null) {
101
+ const rate = Math.max(0, Math.min(100, opts.sessionSamplingRate));
102
+ vars.isSessionSampled = sessionId != null ? isSessionSampledIn(sessionId, rate) : rate > 0;
103
+ }
104
+
105
+ if (!vars.isSessionSampled) {
106
+ debug("Session is not sampled. No telemetry will be transmitted for this session.");
107
+ hasBeenInitialised = true;
108
+ return;
109
+ }
110
+
97
111
  if (isInstrumentationEnabled("@dash0/navigation", opts)) {
98
112
  startNavigationInstrumentation();
99
113
  }
@@ -158,18 +172,30 @@ function isClient() {
158
172
  return win != null;
159
173
  }
160
174
 
175
+ // Vercel auto-prefixes its system env vars under every framework preset
176
+ // (see https://vercel.com/docs/environment-variables/framework-environment-variables).
177
+ // The shared `FrameworkPrefix` union in `./browser-env` is the single source
178
+ // of truth for which prefixes the SDK recognises. To add a new prefix, edit
179
+ // it there; the literal accessors below pick it up automatically via the
180
+ // typed `process.env` declaration.
181
+
161
182
  function detectEnvironment(opts: InitOptions): string | undefined {
162
- // if there is a manually specified value we use that
163
183
  if (opts.environment) {
164
184
  return opts.environment;
165
185
  }
166
-
167
- // if process isn't defined access to it causes an exception, but we can't check for its present due to how
168
- // plugins like webpack define work.
169
186
  try {
170
- // vercel
171
- // @ts-expect-error -- we need to access like this to allow webpack in the nextjs build to replace this
172
- return process?.env?.NEXT_PUBLIC_VERCEL_ENV;
187
+ return pickFirstString(
188
+ process?.env?.NEXT_PUBLIC_VERCEL_ENV,
189
+ process?.env?.NUXT_PUBLIC_VERCEL_ENV,
190
+ process?.env?.NUXT_ENV_VERCEL_ENV,
191
+ process?.env?.REACT_APP_VERCEL_ENV,
192
+ process?.env?.GATSBY_VERCEL_ENV,
193
+ process?.env?.VITE_VERCEL_ENV,
194
+ process?.env?.PUBLIC_VERCEL_ENV,
195
+ process?.env?.VUE_APP_VERCEL_ENV,
196
+ process?.env?.REDWOOD_ENV_VERCEL_ENV,
197
+ process?.env?.SANITY_STUDIO_VERCEL_ENV
198
+ );
173
199
  } catch (_ignored) {
174
200
  return undefined;
175
201
  }
@@ -179,13 +205,19 @@ function detectDeploymentName(opts: InitOptions): string | undefined {
179
205
  if (opts.deploymentName) {
180
206
  return opts.deploymentName;
181
207
  }
182
-
183
- // if process isn't defined access to it causes an exception, but we can't check for its present due to how
184
- // plugins like webpack define work.
185
208
  try {
186
- // vercel
187
- // @ts-expect-error -- we need to access like this to allow webpack in the nextjs build to replace this
188
- return process?.env?.NEXT_PUBLIC_VERCEL_TARGET_ENV;
209
+ return pickFirstString(
210
+ process?.env?.NEXT_PUBLIC_VERCEL_TARGET_ENV,
211
+ process?.env?.NUXT_PUBLIC_VERCEL_TARGET_ENV,
212
+ process?.env?.NUXT_ENV_VERCEL_TARGET_ENV,
213
+ process?.env?.REACT_APP_VERCEL_TARGET_ENV,
214
+ process?.env?.GATSBY_VERCEL_TARGET_ENV,
215
+ process?.env?.VITE_VERCEL_TARGET_ENV,
216
+ process?.env?.PUBLIC_VERCEL_TARGET_ENV,
217
+ process?.env?.VUE_APP_VERCEL_TARGET_ENV,
218
+ process?.env?.REDWOOD_ENV_VERCEL_TARGET_ENV,
219
+ process?.env?.SANITY_STUDIO_VERCEL_TARGET_ENV
220
+ );
189
221
  } catch (_ignored) {
190
222
  return undefined;
191
223
  }
@@ -195,13 +227,19 @@ function detectDeploymentId(opts: InitOptions): string | undefined {
195
227
  if (opts.deploymentId) {
196
228
  return opts.deploymentId;
197
229
  }
198
-
199
- // if process isn't defined access to it causes an exception, but we can't check for its present due to how
200
- // plugins like webpack define work.
201
230
  try {
202
- // vercel
203
- // @ts-expect-error -- we need to access like this to allow webpack in the nextjs build to replace this
204
- return process?.env?.NEXT_PUBLIC_VERCEL_BRANCH_URL;
231
+ return pickFirstString(
232
+ process?.env?.NEXT_PUBLIC_VERCEL_BRANCH_URL,
233
+ process?.env?.NUXT_PUBLIC_VERCEL_BRANCH_URL,
234
+ process?.env?.NUXT_ENV_VERCEL_BRANCH_URL,
235
+ process?.env?.REACT_APP_VERCEL_BRANCH_URL,
236
+ process?.env?.GATSBY_VERCEL_BRANCH_URL,
237
+ process?.env?.VITE_VERCEL_BRANCH_URL,
238
+ process?.env?.PUBLIC_VERCEL_BRANCH_URL,
239
+ process?.env?.VUE_APP_VERCEL_BRANCH_URL,
240
+ process?.env?.REDWOOD_ENV_VERCEL_BRANCH_URL,
241
+ process?.env?.SANITY_STUDIO_VERCEL_BRANCH_URL
242
+ );
205
243
  } catch (_ignored) {
206
244
  return undefined;
207
245
  }
@@ -246,3 +284,26 @@ function isInstrumentationEnabled(name: InstrumentationName, opts: InitOptions):
246
284
 
247
285
  return instrumentations.includes(name);
248
286
  }
287
+
288
+ function merge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
289
+ const result = { ...target };
290
+ for (const key of Object.keys(source) as Array<keyof T>) {
291
+ const srcVal = source[key];
292
+ const dstVal = target[key];
293
+ if (srcVal !== undefined) {
294
+ if (
295
+ srcVal !== null &&
296
+ typeof srcVal === "object" &&
297
+ !Array.isArray(srcVal) &&
298
+ typeof dstVal === "object" &&
299
+ dstVal !== null &&
300
+ !Array.isArray(dstVal)
301
+ ) {
302
+ result[key] = { ...dstVal, ...srcVal } as T[keyof T];
303
+ } else {
304
+ result[key] = srcVal as T[keyof T];
305
+ }
306
+ }
307
+ }
308
+ return result;
309
+ }
@@ -2,6 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { InitOptions, InstrumentationName } from "../types/options";
3
3
  import { PropagatorConfig, Vars } from "../vars";
4
4
  import {
5
+ DEPLOYMENT_ENVIRONMENT_NAME,
6
+ DEPLOYMENT_ID,
7
+ DEPLOYMENT_NAME,
5
8
  SERVICE_NAME,
6
9
  SERVICE_NAMESPACE,
7
10
  VCS_CHANGE_ID,
@@ -440,7 +443,8 @@ describe("init", () => {
440
443
  // detectVcsFromVercel is actually wired up.
441
444
  const FRAMEWORK_PREFIX_SAMPLES: Array<[label: string, prefix: string]> = [
442
445
  ["Next.js", "NEXT_PUBLIC_"],
443
- ["Nuxt", "NUXT_ENV_"],
446
+ ["Nuxt 3", "NUXT_PUBLIC_"],
447
+ ["Nuxt 2", "NUXT_ENV_"],
444
448
  ["Create React App", "REACT_APP_"],
445
449
  ["Gatsby", "GATSBY_"],
446
450
  ["Vite", "VITE_"],
@@ -684,4 +688,69 @@ describe("init", () => {
684
688
  expect(stringAttr(VCS_REPOSITORY_NAME)).toBeUndefined();
685
689
  });
686
690
  });
691
+
692
+ describe("environment + deployment auto-detection", () => {
693
+ afterEach(() => {
694
+ vi.unstubAllEnvs();
695
+ });
696
+
697
+ const stringAttr = (key: string) => vars.resource.attributes.find((attr) => attr.key === key)?.value.stringValue;
698
+
699
+ // The 9 framework prefixes Vercel auto-prefixes its `VERCEL_*` system
700
+ // vars under (per https://vercel.com/docs/environment-variables/framework-environment-variables).
701
+ // Same matrix the VCS detection uses, sourced from the shared
702
+ // `FrameworkPrefix` union in `./browser-env`.
703
+ const FRAMEWORK_PREFIX_SAMPLES: Array<[label: string, prefix: string]> = [
704
+ ["Next.js", "NEXT_PUBLIC_"],
705
+ ["Nuxt 3", "NUXT_PUBLIC_"],
706
+ ["Nuxt 2", "NUXT_ENV_"],
707
+ ["Create React App", "REACT_APP_"],
708
+ ["Gatsby", "GATSBY_"],
709
+ ["Vite", "VITE_"],
710
+ ["Astro / SvelteKit / Hydrogen", "PUBLIC_"],
711
+ ["Vue CLI", "VUE_APP_"],
712
+ ["RedwoodJS", "REDWOOD_ENV_"],
713
+ ["Sanity Studio", "SANITY_STUDIO_"],
714
+ ];
715
+
716
+ it.each(FRAMEWORK_PREFIX_SAMPLES)(
717
+ "derives environment + deployment resource attributes from %s framework prefix (%s)",
718
+ (_label, prefix) => {
719
+ vi.stubEnv(`${prefix}VERCEL_ENV`, "production");
720
+ vi.stubEnv(`${prefix}VERCEL_TARGET_ENV`, "production");
721
+ vi.stubEnv(`${prefix}VERCEL_BRANCH_URL`, "my-site-git-main.vercel.app");
722
+
723
+ init(baseOptions);
724
+
725
+ expect(stringAttr(DEPLOYMENT_ENVIRONMENT_NAME)).toBe("production");
726
+ expect(stringAttr(DEPLOYMENT_NAME)).toBe("production");
727
+ expect(stringAttr(DEPLOYMENT_ID)).toBe("my-site-git-main.vercel.app");
728
+ }
729
+ );
730
+
731
+ it("opts.environment / opts.deploymentName / opts.deploymentId override env-var detection", () => {
732
+ vi.stubEnv("NEXT_PUBLIC_VERCEL_ENV", "from-env");
733
+ vi.stubEnv("NEXT_PUBLIC_VERCEL_TARGET_ENV", "from-env");
734
+ vi.stubEnv("NEXT_PUBLIC_VERCEL_BRANCH_URL", "from-env");
735
+
736
+ init({
737
+ ...baseOptions,
738
+ environment: "manual-environment",
739
+ deploymentName: "manual-deployment-name",
740
+ deploymentId: "manual-deployment-id",
741
+ });
742
+
743
+ expect(stringAttr(DEPLOYMENT_ENVIRONMENT_NAME)).toBe("manual-environment");
744
+ expect(stringAttr(DEPLOYMENT_NAME)).toBe("manual-deployment-name");
745
+ expect(stringAttr(DEPLOYMENT_ID)).toBe("manual-deployment-id");
746
+ });
747
+
748
+ it("emits no environment/deployment attributes when neither env vars nor opts are set", () => {
749
+ init(baseOptions);
750
+
751
+ expect(stringAttr(DEPLOYMENT_ENVIRONMENT_NAME)).toBeUndefined();
752
+ expect(stringAttr(DEPLOYMENT_NAME)).toBeUndefined();
753
+ expect(stringAttr(DEPLOYMENT_ID)).toBeUndefined();
754
+ });
755
+ });
687
756
  });
package/src/api/vcs.ts CHANGED
@@ -10,41 +10,7 @@ import {
10
10
  } from "../semantic-conventions";
11
11
  import { InitOptions, VcsAttributes } from "../types/options";
12
12
  import { addAttribute } from "../utils/otel";
13
-
14
- // Module-local typing for the `process` global. The SDK does not depend on
15
- // @types/node, and bundler substitution requires literal `process.env.NAME`
16
- // accessors (dot notation, not bracket lookup). We enumerate the exact set of
17
- // env var names the readers below access via template-literal types — typos
18
- // are caught at compile time, and `noPropertyAccessFromIndexSignature` does
19
- // not fire because each key is a known union member, not an index signature.
20
- //
21
- // Adding a new framework prefix here automatically permits every combination
22
- // with every known suffix. Adding a vendor suffix permits every combination
23
- // with every known prefix.
24
- type FrameworkPrefix =
25
- | "NEXT_PUBLIC_"
26
- | "NUXT_ENV_"
27
- | "REACT_APP_"
28
- | "GATSBY_"
29
- | "VITE_"
30
- | "PUBLIC_"
31
- | "VUE_APP_"
32
- | "REDWOOD_ENV_"
33
- | "SANITY_STUDIO_";
34
-
35
- type VercelGitSuffix =
36
- | "VERCEL_GIT_PROVIDER"
37
- | "VERCEL_GIT_REPO_OWNER"
38
- | "VERCEL_GIT_REPO_SLUG"
39
- | "VERCEL_GIT_COMMIT_REF"
40
- | "VERCEL_GIT_COMMIT_SHA"
41
- | "VERCEL_GIT_PULL_REQUEST_ID";
42
-
43
- type NetlifyGitSuffix = "REPOSITORY_URL" | "BRANCH" | "COMMIT_REF" | "REVIEW_ID";
44
-
45
- type BrowserBuildEnvKey = `${FrameworkPrefix}${VercelGitSuffix | NetlifyGitSuffix}`;
46
-
47
- type BrowserBuildEnv = { readonly [K in BrowserBuildEnvKey]?: string };
13
+ import { BrowserBuildEnv, pickFirstString } from "./browser-env";
48
14
 
49
15
  declare const process: { env?: BrowserBuildEnv } | undefined;
50
16
 
@@ -134,6 +100,7 @@ function detectVcsFromVercel(): VcsAttributes {
134
100
  try {
135
101
  provider = pickFirstString(
136
102
  process?.env?.NEXT_PUBLIC_VERCEL_GIT_PROVIDER,
103
+ process?.env?.NUXT_PUBLIC_VERCEL_GIT_PROVIDER,
137
104
  process?.env?.NUXT_ENV_VERCEL_GIT_PROVIDER,
138
105
  process?.env?.REACT_APP_VERCEL_GIT_PROVIDER,
139
106
  process?.env?.GATSBY_VERCEL_GIT_PROVIDER,
@@ -145,6 +112,7 @@ function detectVcsFromVercel(): VcsAttributes {
145
112
  );
146
113
  owner = pickFirstString(
147
114
  process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER,
115
+ process?.env?.NUXT_PUBLIC_VERCEL_GIT_REPO_OWNER,
148
116
  process?.env?.NUXT_ENV_VERCEL_GIT_REPO_OWNER,
149
117
  process?.env?.REACT_APP_VERCEL_GIT_REPO_OWNER,
150
118
  process?.env?.GATSBY_VERCEL_GIT_REPO_OWNER,
@@ -156,6 +124,7 @@ function detectVcsFromVercel(): VcsAttributes {
156
124
  );
157
125
  repo = pickFirstString(
158
126
  process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG,
127
+ process?.env?.NUXT_PUBLIC_VERCEL_GIT_REPO_SLUG,
159
128
  process?.env?.NUXT_ENV_VERCEL_GIT_REPO_SLUG,
160
129
  process?.env?.REACT_APP_VERCEL_GIT_REPO_SLUG,
161
130
  process?.env?.GATSBY_VERCEL_GIT_REPO_SLUG,
@@ -167,6 +136,7 @@ function detectVcsFromVercel(): VcsAttributes {
167
136
  );
168
137
  ref = pickFirstString(
169
138
  process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF,
139
+ process?.env?.NUXT_PUBLIC_VERCEL_GIT_COMMIT_REF,
170
140
  process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_REF,
171
141
  process?.env?.REACT_APP_VERCEL_GIT_COMMIT_REF,
172
142
  process?.env?.GATSBY_VERCEL_GIT_COMMIT_REF,
@@ -178,6 +148,7 @@ function detectVcsFromVercel(): VcsAttributes {
178
148
  );
179
149
  revision = pickFirstString(
180
150
  process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
151
+ process?.env?.NUXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
181
152
  process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_SHA,
182
153
  process?.env?.REACT_APP_VERCEL_GIT_COMMIT_SHA,
183
154
  process?.env?.GATSBY_VERCEL_GIT_COMMIT_SHA,
@@ -189,6 +160,7 @@ function detectVcsFromVercel(): VcsAttributes {
189
160
  );
190
161
  changeId = pickFirstString(
191
162
  process?.env?.NEXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID,
163
+ process?.env?.NUXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID,
192
164
  process?.env?.NUXT_ENV_VERCEL_GIT_PULL_REQUEST_ID,
193
165
  process?.env?.REACT_APP_VERCEL_GIT_PULL_REQUEST_ID,
194
166
  process?.env?.GATSBY_VERCEL_GIT_PULL_REQUEST_ID,
@@ -227,6 +199,7 @@ function detectVcsFromNetlify(): VcsAttributes {
227
199
  try {
228
200
  repositoryUrl = pickFirstString(
229
201
  process?.env?.NEXT_PUBLIC_REPOSITORY_URL,
202
+ process?.env?.NUXT_PUBLIC_REPOSITORY_URL,
230
203
  process?.env?.NUXT_ENV_REPOSITORY_URL,
231
204
  process?.env?.REACT_APP_REPOSITORY_URL,
232
205
  process?.env?.GATSBY_REPOSITORY_URL,
@@ -238,6 +211,7 @@ function detectVcsFromNetlify(): VcsAttributes {
238
211
  );
239
212
  branch = pickFirstString(
240
213
  process?.env?.NEXT_PUBLIC_BRANCH,
214
+ process?.env?.NUXT_PUBLIC_BRANCH,
241
215
  process?.env?.NUXT_ENV_BRANCH,
242
216
  process?.env?.REACT_APP_BRANCH,
243
217
  process?.env?.GATSBY_BRANCH,
@@ -249,6 +223,7 @@ function detectVcsFromNetlify(): VcsAttributes {
249
223
  );
250
224
  commit = pickFirstString(
251
225
  process?.env?.NEXT_PUBLIC_COMMIT_REF,
226
+ process?.env?.NUXT_PUBLIC_COMMIT_REF,
252
227
  process?.env?.NUXT_ENV_COMMIT_REF,
253
228
  process?.env?.REACT_APP_COMMIT_REF,
254
229
  process?.env?.GATSBY_COMMIT_REF,
@@ -260,6 +235,7 @@ function detectVcsFromNetlify(): VcsAttributes {
260
235
  );
261
236
  reviewId = pickFirstString(
262
237
  process?.env?.NEXT_PUBLIC_REVIEW_ID,
238
+ process?.env?.NUXT_PUBLIC_REVIEW_ID,
263
239
  process?.env?.NUXT_ENV_REVIEW_ID,
264
240
  process?.env?.REACT_APP_REVIEW_ID,
265
241
  process?.env?.GATSBY_REVIEW_ID,
@@ -285,13 +261,6 @@ function detectVcsFromNetlify(): VcsAttributes {
285
261
  };
286
262
  }
287
263
 
288
- function pickFirstString(...values: (string | undefined)[]): string | undefined {
289
- for (const value of values) {
290
- if (value) return value;
291
- }
292
- return undefined;
293
- }
294
-
295
264
  function buildRepositoryUrlFromVercel(
296
265
  provider: string | undefined,
297
266
  owner: string | undefined,
@@ -21,6 +21,8 @@ function isRateLimited() {
21
21
  }
22
22
 
23
23
  export function sendLog(log: LogRecord): void {
24
+ if (!vars.isSessionSampled) return;
25
+
24
26
  if (isRateLimited()) {
25
27
  debug("Transport rate limit. Will not send item.", log);
26
28
  return;
@@ -49,6 +51,7 @@ function sendLogs(logs: LogRecord[]): void {
49
51
 
50
52
  export function sendSpan(span: Span | undefined): void {
51
53
  if (!span) return;
54
+ if (!vars.isSessionSampled) return;
52
55
 
53
56
  if (isRateLimited()) {
54
57
  debug("Transport rate limit. Will not send item.", span);
@@ -54,9 +54,9 @@ export type InitOptions = {
54
54
  /**
55
55
  * When `true`, disable auto-detection of VCS (version control) context
56
56
  * from the build environment. By default the SDK reads VCS context from
57
- * Vercel (`NEXT_PUBLIC_VERCEL_GIT_*`) and Netlify
58
- * (`NEXT_PUBLIC_REPOSITORY_URL`, `NEXT_PUBLIC_BRANCH`,
59
- * `NEXT_PUBLIC_COMMIT_REF`, `NEXT_PUBLIC_REVIEW_ID`) and applies the values
57
+ * Vercel (`<FRAMEWORK_PREFIX>VERCEL_GIT_*`) and Netlify
58
+ * (`<FRAMEWORK_PREFIX>REPOSITORY_URL`, `<FRAMEWORK_PREFIX>BRANCH`,
59
+ * `<FRAMEWORK_PREFIX>COMMIT_REF`, `<FRAMEWORK_PREFIX>REVIEW_ID`) and applies the values
60
60
  * as resource attributes following the OTel `vcs.*` semantic conventions:
61
61
  *
62
62
  * - vcs.provider.name
@@ -94,6 +94,16 @@ export type InitOptions = {
94
94
  */
95
95
  enabledInstrumentations?: InstrumentationName[];
96
96
 
97
+ /**
98
+ * The percentage of sessions for which telemetry data is recorded and transmitted.
99
+ * Must be a number between 0 and 100.
100
+ * - 0: No sessions are recorded/transferred.
101
+ * - 100: All sessions are recorded/transferred (default).
102
+ * - Any other value: That percentage of sessions are recorded/transferred.
103
+ * The sampling decision is deterministic per session ID.
104
+ */
105
+ sessionSamplingRate?: number;
106
+
97
107
  /**
98
108
  * The session inactivity timeout. Session inactivity is the maximum
99
109
  * allowed time to pass between two page loads before the session is considered
@@ -16,3 +16,4 @@ export * from "./url";
16
16
  export * from "./pick";
17
17
  export * from "./sanitize";
18
18
  export * from "./wrap";
19
+ export * from "./session-sampling";
@@ -0,0 +1,15 @@
1
+ import { crc32 } from "./crc32";
2
+
3
+ /**
4
+ * Determines whether a session should be sampled based on the session ID
5
+ * and a configured sampling rate.
6
+ *
7
+ * @param sessionId The hex session ID string
8
+ * @param samplingRate A number between 0 and 100 (inclusive)
9
+ * @returns true if the session should be sampled (data collected), false otherwise
10
+ */
11
+ export function isSessionSampledIn(sessionId: string, samplingRate: number): boolean {
12
+ if (samplingRate <= 0) return false;
13
+ if (samplingRate >= 100) return true;
14
+ return crc32(sessionId) % 100 < samplingRate;
15
+ }
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isSessionSampledIn } from "./session-sampling";
3
+ import { generateSessionId } from "./session-id";
4
+
5
+ describe("isSessionSampledIn", () => {
6
+ it("returns false when sampling rate is 0", () => {
7
+ expect(isSessionSampledIn("00abcdef01234567", 0)).toBe(false);
8
+ });
9
+
10
+ it("returns true when sampling rate is 100", () => {
11
+ expect(isSessionSampledIn("00abcdef01234567", 100)).toBe(true);
12
+ });
13
+
14
+ it("returns deterministic results for the same session ID and rate", () => {
15
+ const sessionId = "00abcdef01234567";
16
+ const rate = 50;
17
+ const result1 = isSessionSampledIn(sessionId, rate);
18
+ const result2 = isSessionSampledIn(sessionId, rate);
19
+ expect(result1).toBe(result2);
20
+ });
21
+
22
+ it("produces different results for different session IDs", () => {
23
+ const results = new Set<boolean>();
24
+ // Generate enough IDs to get both true and false with high probability
25
+ for (let i = 0; i < 100; i++) {
26
+ results.add(isSessionSampledIn(generateSessionId(), 50));
27
+ }
28
+ expect(results.size).toBe(2);
29
+ });
30
+
31
+ it("produces roughly correct distribution over many session IDs", () => {
32
+ const rate = 30;
33
+ const total = 10000;
34
+ let sampledIn = 0;
35
+
36
+ for (let i = 0; i < total; i++) {
37
+ if (isSessionSampledIn(generateSessionId(), rate)) {
38
+ sampledIn++;
39
+ }
40
+ }
41
+
42
+ const actualRate = sampledIn / total;
43
+ // Allow 5% tolerance
44
+ expect(actualRate).toBeGreaterThan(0.25);
45
+ expect(actualRate).toBeLessThan(0.35);
46
+ });
47
+
48
+ it("handles edge case: rate just above 0", () => {
49
+ // With rate=1, about 1% of sessions should be sampled in
50
+ let sampledIn = 0;
51
+ const total = 10000;
52
+ for (let i = 0; i < total; i++) {
53
+ if (isSessionSampledIn(generateSessionId(), 1)) {
54
+ sampledIn++;
55
+ }
56
+ }
57
+ const actualRate = sampledIn / total;
58
+ expect(actualRate).toBeGreaterThan(0.0);
59
+ expect(actualRate).toBeLessThan(0.05);
60
+ });
61
+
62
+ it("handles edge case: rate just below 100", () => {
63
+ // With rate=99, about 99% of sessions should be sampled in
64
+ let sampledIn = 0;
65
+ const total = 10000;
66
+ for (let i = 0; i < total; i++) {
67
+ if (isSessionSampledIn(generateSessionId(), 99)) {
68
+ sampledIn++;
69
+ }
70
+ }
71
+ const actualRate = sampledIn / total;
72
+ expect(actualRate).toBeGreaterThan(0.95);
73
+ expect(actualRate).toBeLessThan(1.0);
74
+ });
75
+
76
+ it("returns false for negative sampling rates", () => {
77
+ expect(isSessionSampledIn("00abcdef01234567", -10)).toBe(false);
78
+ });
79
+
80
+ it("returns true for sampling rates above 100", () => {
81
+ expect(isSessionSampledIn("00abcdef01234567", 150)).toBe(true);
82
+ });
83
+ });