@dash0/sdk-web 0.21.0 → 0.22.0

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.
@@ -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,7 +24,7 @@ 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";
@@ -32,8 +33,11 @@ import { startNavigationInstrumentation } from "../instrumentations/navigation";
32
33
  import { merge } from "ts-deepmerge";
33
34
  import { initializeTabId } from "../utils/tab-id";
34
35
  import { InitOptions, InstrumentationName } from "../types/options";
36
+ import { BrowserBuildEnv, pickFirstString } from "./browser-env";
35
37
  import { applyVcsResourceAttributes } from "./vcs";
36
38
 
39
+ declare const process: { env?: BrowserBuildEnv } | undefined;
40
+
37
41
  let hasBeenInitialised: boolean = false;
38
42
 
39
43
  export function init(opts: InitOptions) {
@@ -94,6 +98,17 @@ export function init(opts: InitOptions) {
94
98
  initializeTabId();
95
99
  trackSessions(opts.sessionInactivityTimeoutMillis, opts.sessionTerminationTimeoutMillis);
96
100
 
101
+ if (opts.sessionSamplingRate != null) {
102
+ const rate = Math.max(0, Math.min(100, opts.sessionSamplingRate));
103
+ vars.isSessionSampled = sessionId != null ? isSessionSampledIn(sessionId, rate) : rate > 0;
104
+ }
105
+
106
+ if (!vars.isSessionSampled) {
107
+ debug("Session is not sampled. No telemetry will be transmitted for this session.");
108
+ hasBeenInitialised = true;
109
+ return;
110
+ }
111
+
97
112
  if (isInstrumentationEnabled("@dash0/navigation", opts)) {
98
113
  startNavigationInstrumentation();
99
114
  }
@@ -158,18 +173,30 @@ function isClient() {
158
173
  return win != null;
159
174
  }
160
175
 
176
+ // Vercel auto-prefixes its system env vars under every framework preset
177
+ // (see https://vercel.com/docs/environment-variables/framework-environment-variables).
178
+ // The shared `FrameworkPrefix` union in `./browser-env` is the single source
179
+ // of truth for which prefixes the SDK recognises. To add a new prefix, edit
180
+ // it there; the literal accessors below pick it up automatically via the
181
+ // typed `process.env` declaration.
182
+
161
183
  function detectEnvironment(opts: InitOptions): string | undefined {
162
- // if there is a manually specified value we use that
163
184
  if (opts.environment) {
164
185
  return opts.environment;
165
186
  }
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
187
  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;
188
+ return pickFirstString(
189
+ process?.env?.NEXT_PUBLIC_VERCEL_ENV,
190
+ process?.env?.NUXT_PUBLIC_VERCEL_ENV,
191
+ process?.env?.NUXT_ENV_VERCEL_ENV,
192
+ process?.env?.REACT_APP_VERCEL_ENV,
193
+ process?.env?.GATSBY_VERCEL_ENV,
194
+ process?.env?.VITE_VERCEL_ENV,
195
+ process?.env?.PUBLIC_VERCEL_ENV,
196
+ process?.env?.VUE_APP_VERCEL_ENV,
197
+ process?.env?.REDWOOD_ENV_VERCEL_ENV,
198
+ process?.env?.SANITY_STUDIO_VERCEL_ENV
199
+ );
173
200
  } catch (_ignored) {
174
201
  return undefined;
175
202
  }
@@ -179,13 +206,19 @@ function detectDeploymentName(opts: InitOptions): string | undefined {
179
206
  if (opts.deploymentName) {
180
207
  return opts.deploymentName;
181
208
  }
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
209
  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;
210
+ return pickFirstString(
211
+ process?.env?.NEXT_PUBLIC_VERCEL_TARGET_ENV,
212
+ process?.env?.NUXT_PUBLIC_VERCEL_TARGET_ENV,
213
+ process?.env?.NUXT_ENV_VERCEL_TARGET_ENV,
214
+ process?.env?.REACT_APP_VERCEL_TARGET_ENV,
215
+ process?.env?.GATSBY_VERCEL_TARGET_ENV,
216
+ process?.env?.VITE_VERCEL_TARGET_ENV,
217
+ process?.env?.PUBLIC_VERCEL_TARGET_ENV,
218
+ process?.env?.VUE_APP_VERCEL_TARGET_ENV,
219
+ process?.env?.REDWOOD_ENV_VERCEL_TARGET_ENV,
220
+ process?.env?.SANITY_STUDIO_VERCEL_TARGET_ENV
221
+ );
189
222
  } catch (_ignored) {
190
223
  return undefined;
191
224
  }
@@ -195,13 +228,19 @@ function detectDeploymentId(opts: InitOptions): string | undefined {
195
228
  if (opts.deploymentId) {
196
229
  return opts.deploymentId;
197
230
  }
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
231
  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;
232
+ return pickFirstString(
233
+ process?.env?.NEXT_PUBLIC_VERCEL_BRANCH_URL,
234
+ process?.env?.NUXT_PUBLIC_VERCEL_BRANCH_URL,
235
+ process?.env?.NUXT_ENV_VERCEL_BRANCH_URL,
236
+ process?.env?.REACT_APP_VERCEL_BRANCH_URL,
237
+ process?.env?.GATSBY_VERCEL_BRANCH_URL,
238
+ process?.env?.VITE_VERCEL_BRANCH_URL,
239
+ process?.env?.PUBLIC_VERCEL_BRANCH_URL,
240
+ process?.env?.VUE_APP_VERCEL_BRANCH_URL,
241
+ process?.env?.REDWOOD_ENV_VERCEL_BRANCH_URL,
242
+ process?.env?.SANITY_STUDIO_VERCEL_BRANCH_URL
243
+ );
205
244
  } catch (_ignored) {
206
245
  return undefined;
207
246
  }
@@ -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
+ });
package/src/vars.ts CHANGED
@@ -171,6 +171,12 @@ export type Vars = {
171
171
  * experimental - in rare cases causes Chrome to crash to use at your own risk.
172
172
  */
173
173
  enableTransportCompression: boolean;
174
+
175
+ /**
176
+ * Whether the current session is sampled in (true) or out (false).
177
+ * Determined at init time based on sessionSamplingRate and the session ID.
178
+ */
179
+ isSessionSampled: boolean;
174
180
  };
175
181
 
176
182
  export const vars: Vars = {
@@ -198,4 +204,5 @@ export const vars: Vars = {
198
204
  includeParts: [],
199
205
  },
200
206
  enableTransportCompression: false,
207
+ isSessionSampled: true,
201
208
  };