@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.
@@ -0,0 +1,33 @@
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
+ * Return the first truthy value, or undefined. Used to walk the list of
18
+ * framework-prefixed variants of an env var and pick whichever the user's
19
+ * bundler substituted at build time.
20
+ *
21
+ * The falsy check skips empty strings as well as undefined — bundlers that
22
+ * do not substitute a literal leave it as `undefined`, not `""`, so empty
23
+ * is treated as "not set". This is intentional and matches every known
24
+ * `VERCEL_GIT_*` / `REPOSITORY_URL` / etc. value shape (non-empty strings
25
+ * or absent; Vercel PR IDs are positive integer strings, never `"0"`).
26
+ */
27
+ export function pickFirstString(...values) {
28
+ for (const value of values) {
29
+ if (value)
30
+ return value;
31
+ }
32
+ return undefined;
33
+ }
@@ -1,14 +1,14 @@
1
1
  import { vars } from "../vars";
2
2
  import { DEPLOYMENT_ENVIRONMENT_NAME, DEPLOYMENT_ID, DEPLOYMENT_NAME, PAGE_LOAD_ID, SERVICE_NAME, SERVICE_NAMESPACE, SERVICE_VERSION, USER_AGENT, } from "../semantic-conventions";
3
- import { fetch, generateUniqueId, isSafeServiceName, PAGE_LOAD_ID_BYTES, warn, debug, perf, nav, win, NO_VALUE_FALLBACK, pick, loc, } from "../utils";
4
- import { trackSessions } from "./session";
3
+ import { fetch, generateUniqueId, isSessionSampledIn, isSafeServiceName, PAGE_LOAD_ID_BYTES, warn, debug, perf, nav, win, NO_VALUE_FALLBACK, pick, loc, } from "../utils";
4
+ import { sessionId, trackSessions } from "./session";
5
5
  import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
6
6
  import { startErrorInstrumentation } from "../instrumentations/errors";
7
7
  import { addAttribute } from "../utils/otel";
8
8
  import { instrumentFetch } from "../instrumentations/http/fetch";
9
9
  import { startNavigationInstrumentation } from "../instrumentations/navigation";
10
- import { merge } from "ts-deepmerge";
11
10
  import { initializeTabId } from "../utils/tab-id";
11
+ import { pickFirstString } from "./browser-env";
12
12
  import { applyVcsResourceAttributes } from "./vcs";
13
13
  let hasBeenInitialised = false;
14
14
  export function init(opts) {
@@ -56,6 +56,15 @@ export function init(opts) {
56
56
  initializeSignalAttributes(opts);
57
57
  initializeTabId();
58
58
  trackSessions(opts.sessionInactivityTimeoutMillis, opts.sessionTerminationTimeoutMillis);
59
+ if (opts.sessionSamplingRate != null) {
60
+ const rate = Math.max(0, Math.min(100, opts.sessionSamplingRate));
61
+ vars.isSessionSampled = sessionId != null ? isSessionSampledIn(sessionId, rate) : rate > 0;
62
+ }
63
+ if (!vars.isSessionSampled) {
64
+ debug("Session is not sampled. No telemetry will be transmitted for this session.");
65
+ hasBeenInitialised = true;
66
+ return;
67
+ }
59
68
  if (isInstrumentationEnabled("@dash0/navigation", opts)) {
60
69
  startNavigationInstrumentation();
61
70
  }
@@ -107,17 +116,18 @@ function isSupported() {
107
116
  function isClient() {
108
117
  return win != null;
109
118
  }
119
+ // Vercel auto-prefixes its system env vars under every framework preset
120
+ // (see https://vercel.com/docs/environment-variables/framework-environment-variables).
121
+ // The shared `FrameworkPrefix` union in `./browser-env` is the single source
122
+ // of truth for which prefixes the SDK recognises. To add a new prefix, edit
123
+ // it there; the literal accessors below pick it up automatically via the
124
+ // typed `process.env` declaration.
110
125
  function detectEnvironment(opts) {
111
- // if there is a manually specified value we use that
112
126
  if (opts.environment) {
113
127
  return opts.environment;
114
128
  }
115
- // if process isn't defined access to it causes an exception, but we can't check for its present due to how
116
- // plugins like webpack define work.
117
129
  try {
118
- // vercel
119
- // @ts-expect-error -- we need to access like this to allow webpack in the nextjs build to replace this
120
- return process?.env?.NEXT_PUBLIC_VERCEL_ENV;
130
+ return pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_ENV, process?.env?.NUXT_PUBLIC_VERCEL_ENV, process?.env?.NUXT_ENV_VERCEL_ENV, process?.env?.REACT_APP_VERCEL_ENV, process?.env?.GATSBY_VERCEL_ENV, process?.env?.VITE_VERCEL_ENV, process?.env?.PUBLIC_VERCEL_ENV, process?.env?.VUE_APP_VERCEL_ENV, process?.env?.REDWOOD_ENV_VERCEL_ENV, process?.env?.SANITY_STUDIO_VERCEL_ENV);
121
131
  }
122
132
  catch (_ignored) {
123
133
  return undefined;
@@ -127,12 +137,8 @@ function detectDeploymentName(opts) {
127
137
  if (opts.deploymentName) {
128
138
  return opts.deploymentName;
129
139
  }
130
- // if process isn't defined access to it causes an exception, but we can't check for its present due to how
131
- // plugins like webpack define work.
132
140
  try {
133
- // vercel
134
- // @ts-expect-error -- we need to access like this to allow webpack in the nextjs build to replace this
135
- return process?.env?.NEXT_PUBLIC_VERCEL_TARGET_ENV;
141
+ return pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_TARGET_ENV, process?.env?.NUXT_PUBLIC_VERCEL_TARGET_ENV, process?.env?.NUXT_ENV_VERCEL_TARGET_ENV, process?.env?.REACT_APP_VERCEL_TARGET_ENV, process?.env?.GATSBY_VERCEL_TARGET_ENV, process?.env?.VITE_VERCEL_TARGET_ENV, process?.env?.PUBLIC_VERCEL_TARGET_ENV, process?.env?.VUE_APP_VERCEL_TARGET_ENV, process?.env?.REDWOOD_ENV_VERCEL_TARGET_ENV, process?.env?.SANITY_STUDIO_VERCEL_TARGET_ENV);
136
142
  }
137
143
  catch (_ignored) {
138
144
  return undefined;
@@ -142,12 +148,8 @@ function detectDeploymentId(opts) {
142
148
  if (opts.deploymentId) {
143
149
  return opts.deploymentId;
144
150
  }
145
- // if process isn't defined access to it causes an exception, but we can't check for its present due to how
146
- // plugins like webpack define work.
147
151
  try {
148
- // vercel
149
- // @ts-expect-error -- we need to access like this to allow webpack in the nextjs build to replace this
150
- return process?.env?.NEXT_PUBLIC_VERCEL_BRANCH_URL;
152
+ return pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_BRANCH_URL, process?.env?.NUXT_PUBLIC_VERCEL_BRANCH_URL, process?.env?.NUXT_ENV_VERCEL_BRANCH_URL, process?.env?.REACT_APP_VERCEL_BRANCH_URL, process?.env?.GATSBY_VERCEL_BRANCH_URL, process?.env?.VITE_VERCEL_BRANCH_URL, process?.env?.PUBLIC_VERCEL_BRANCH_URL, process?.env?.VUE_APP_VERCEL_BRANCH_URL, process?.env?.REDWOOD_ENV_VERCEL_BRANCH_URL, process?.env?.SANITY_STUDIO_VERCEL_BRANCH_URL);
151
153
  }
152
154
  catch (_ignored) {
153
155
  return undefined;
@@ -188,3 +190,24 @@ function isInstrumentationEnabled(name, opts) {
188
190
  return true;
189
191
  return instrumentations.includes(name);
190
192
  }
193
+ function merge(target, source) {
194
+ const result = { ...target };
195
+ for (const key of Object.keys(source)) {
196
+ const srcVal = source[key];
197
+ const dstVal = target[key];
198
+ if (srcVal !== undefined) {
199
+ if (srcVal !== null &&
200
+ typeof srcVal === "object" &&
201
+ !Array.isArray(srcVal) &&
202
+ typeof dstVal === "object" &&
203
+ dstVal !== null &&
204
+ !Array.isArray(dstVal)) {
205
+ result[key] = { ...dstVal, ...srcVal };
206
+ }
207
+ else {
208
+ result[key] = srcVal;
209
+ }
210
+ }
211
+ }
212
+ return result;
213
+ }
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { SERVICE_NAME, SERVICE_NAMESPACE, VCS_CHANGE_ID, VCS_OWNER_NAME, VCS_PROVIDER_NAME, VCS_REF_HEAD_NAME, VCS_REF_HEAD_REVISION, VCS_REPOSITORY_NAME, VCS_REPOSITORY_URL_FULL, } from "../semantic-conventions";
2
+ import { DEPLOYMENT_ENVIRONMENT_NAME, DEPLOYMENT_ID, DEPLOYMENT_NAME, SERVICE_NAME, SERVICE_NAMESPACE, VCS_CHANGE_ID, VCS_OWNER_NAME, VCS_PROVIDER_NAME, VCS_REF_HEAD_NAME, VCS_REF_HEAD_REVISION, VCS_REPOSITORY_NAME, VCS_REPOSITORY_URL_FULL, } from "../semantic-conventions";
3
3
  // Mock all the instrumentation modules
4
4
  vi.mock("../instrumentations/web-vitals", () => ({
5
5
  startWebVitalsInstrumentation: vi.fn(),
@@ -342,7 +342,8 @@ describe("init", () => {
342
342
  // detectVcsFromVercel is actually wired up.
343
343
  const FRAMEWORK_PREFIX_SAMPLES = [
344
344
  ["Next.js", "NEXT_PUBLIC_"],
345
- ["Nuxt", "NUXT_ENV_"],
345
+ ["Nuxt 3", "NUXT_PUBLIC_"],
346
+ ["Nuxt 2", "NUXT_ENV_"],
346
347
  ["Create React App", "REACT_APP_"],
347
348
  ["Gatsby", "GATSBY_"],
348
349
  ["Vite", "VITE_"],
@@ -534,4 +535,55 @@ describe("init", () => {
534
535
  expect(stringAttr(VCS_REPOSITORY_NAME)).toBeUndefined();
535
536
  });
536
537
  });
538
+ describe("environment + deployment auto-detection", () => {
539
+ afterEach(() => {
540
+ vi.unstubAllEnvs();
541
+ });
542
+ const stringAttr = (key) => vars.resource.attributes.find((attr) => attr.key === key)?.value.stringValue;
543
+ // The 9 framework prefixes Vercel auto-prefixes its `VERCEL_*` system
544
+ // vars under (per https://vercel.com/docs/environment-variables/framework-environment-variables).
545
+ // Same matrix the VCS detection uses, sourced from the shared
546
+ // `FrameworkPrefix` union in `./browser-env`.
547
+ const FRAMEWORK_PREFIX_SAMPLES = [
548
+ ["Next.js", "NEXT_PUBLIC_"],
549
+ ["Nuxt 3", "NUXT_PUBLIC_"],
550
+ ["Nuxt 2", "NUXT_ENV_"],
551
+ ["Create React App", "REACT_APP_"],
552
+ ["Gatsby", "GATSBY_"],
553
+ ["Vite", "VITE_"],
554
+ ["Astro / SvelteKit / Hydrogen", "PUBLIC_"],
555
+ ["Vue CLI", "VUE_APP_"],
556
+ ["RedwoodJS", "REDWOOD_ENV_"],
557
+ ["Sanity Studio", "SANITY_STUDIO_"],
558
+ ];
559
+ it.each(FRAMEWORK_PREFIX_SAMPLES)("derives environment + deployment resource attributes from %s framework prefix (%s)", (_label, prefix) => {
560
+ vi.stubEnv(`${prefix}VERCEL_ENV`, "production");
561
+ vi.stubEnv(`${prefix}VERCEL_TARGET_ENV`, "production");
562
+ vi.stubEnv(`${prefix}VERCEL_BRANCH_URL`, "my-site-git-main.vercel.app");
563
+ init(baseOptions);
564
+ expect(stringAttr(DEPLOYMENT_ENVIRONMENT_NAME)).toBe("production");
565
+ expect(stringAttr(DEPLOYMENT_NAME)).toBe("production");
566
+ expect(stringAttr(DEPLOYMENT_ID)).toBe("my-site-git-main.vercel.app");
567
+ });
568
+ it("opts.environment / opts.deploymentName / opts.deploymentId override env-var detection", () => {
569
+ vi.stubEnv("NEXT_PUBLIC_VERCEL_ENV", "from-env");
570
+ vi.stubEnv("NEXT_PUBLIC_VERCEL_TARGET_ENV", "from-env");
571
+ vi.stubEnv("NEXT_PUBLIC_VERCEL_BRANCH_URL", "from-env");
572
+ init({
573
+ ...baseOptions,
574
+ environment: "manual-environment",
575
+ deploymentName: "manual-deployment-name",
576
+ deploymentId: "manual-deployment-id",
577
+ });
578
+ expect(stringAttr(DEPLOYMENT_ENVIRONMENT_NAME)).toBe("manual-environment");
579
+ expect(stringAttr(DEPLOYMENT_NAME)).toBe("manual-deployment-name");
580
+ expect(stringAttr(DEPLOYMENT_ID)).toBe("manual-deployment-id");
581
+ });
582
+ it("emits no environment/deployment attributes when neither env vars nor opts are set", () => {
583
+ init(baseOptions);
584
+ expect(stringAttr(DEPLOYMENT_ENVIRONMENT_NAME)).toBeUndefined();
585
+ expect(stringAttr(DEPLOYMENT_NAME)).toBeUndefined();
586
+ expect(stringAttr(DEPLOYMENT_ID)).toBeUndefined();
587
+ });
588
+ });
537
589
  });
@@ -1,6 +1,7 @@
1
1
  import { vars } from "../vars";
2
2
  import { VCS_CHANGE_ID, VCS_OWNER_NAME, VCS_PROVIDER_NAME, VCS_REF_HEAD_NAME, VCS_REF_HEAD_REVISION, VCS_REPOSITORY_NAME, VCS_REPOSITORY_URL_FULL, } from "../semantic-conventions";
3
3
  import { addAttribute } from "../utils/otel";
4
+ import { pickFirstString } from "./browser-env";
4
5
  // Single source of truth mapping each VcsAttributes field to its OTel
5
6
  // resource attribute name. Typed as `Record<keyof VcsAttributes, string>` so
6
7
  // adding a field to VcsAttributes without adding an entry here is a compile
@@ -78,12 +79,12 @@ function detectVcsFromVercel() {
78
79
  let revision;
79
80
  let changeId;
80
81
  try {
81
- provider = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_PROVIDER, process?.env?.NUXT_ENV_VERCEL_GIT_PROVIDER, process?.env?.REACT_APP_VERCEL_GIT_PROVIDER, process?.env?.GATSBY_VERCEL_GIT_PROVIDER, process?.env?.VITE_VERCEL_GIT_PROVIDER, process?.env?.PUBLIC_VERCEL_GIT_PROVIDER, process?.env?.VUE_APP_VERCEL_GIT_PROVIDER, process?.env?.REDWOOD_ENV_VERCEL_GIT_PROVIDER, process?.env?.SANITY_STUDIO_VERCEL_GIT_PROVIDER);
82
- owner = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER, process?.env?.NUXT_ENV_VERCEL_GIT_REPO_OWNER, process?.env?.REACT_APP_VERCEL_GIT_REPO_OWNER, process?.env?.GATSBY_VERCEL_GIT_REPO_OWNER, process?.env?.VITE_VERCEL_GIT_REPO_OWNER, process?.env?.PUBLIC_VERCEL_GIT_REPO_OWNER, process?.env?.VUE_APP_VERCEL_GIT_REPO_OWNER, process?.env?.REDWOOD_ENV_VERCEL_GIT_REPO_OWNER, process?.env?.SANITY_STUDIO_VERCEL_GIT_REPO_OWNER);
83
- repo = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG, process?.env?.NUXT_ENV_VERCEL_GIT_REPO_SLUG, process?.env?.REACT_APP_VERCEL_GIT_REPO_SLUG, process?.env?.GATSBY_VERCEL_GIT_REPO_SLUG, process?.env?.VITE_VERCEL_GIT_REPO_SLUG, process?.env?.PUBLIC_VERCEL_GIT_REPO_SLUG, process?.env?.VUE_APP_VERCEL_GIT_REPO_SLUG, process?.env?.REDWOOD_ENV_VERCEL_GIT_REPO_SLUG, process?.env?.SANITY_STUDIO_VERCEL_GIT_REPO_SLUG);
84
- ref = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF, process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_REF, process?.env?.REACT_APP_VERCEL_GIT_COMMIT_REF, process?.env?.GATSBY_VERCEL_GIT_COMMIT_REF, process?.env?.VITE_VERCEL_GIT_COMMIT_REF, process?.env?.PUBLIC_VERCEL_GIT_COMMIT_REF, process?.env?.VUE_APP_VERCEL_GIT_COMMIT_REF, process?.env?.REDWOOD_ENV_VERCEL_GIT_COMMIT_REF, process?.env?.SANITY_STUDIO_VERCEL_GIT_COMMIT_REF);
85
- revision = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_SHA, process?.env?.REACT_APP_VERCEL_GIT_COMMIT_SHA, process?.env?.GATSBY_VERCEL_GIT_COMMIT_SHA, process?.env?.VITE_VERCEL_GIT_COMMIT_SHA, process?.env?.PUBLIC_VERCEL_GIT_COMMIT_SHA, process?.env?.VUE_APP_VERCEL_GIT_COMMIT_SHA, process?.env?.REDWOOD_ENV_VERCEL_GIT_COMMIT_SHA, process?.env?.SANITY_STUDIO_VERCEL_GIT_COMMIT_SHA);
86
- changeId = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.NUXT_ENV_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.REACT_APP_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.GATSBY_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.VITE_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.PUBLIC_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.VUE_APP_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.REDWOOD_ENV_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.SANITY_STUDIO_VERCEL_GIT_PULL_REQUEST_ID);
82
+ provider = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_PROVIDER, process?.env?.NUXT_PUBLIC_VERCEL_GIT_PROVIDER, process?.env?.NUXT_ENV_VERCEL_GIT_PROVIDER, process?.env?.REACT_APP_VERCEL_GIT_PROVIDER, process?.env?.GATSBY_VERCEL_GIT_PROVIDER, process?.env?.VITE_VERCEL_GIT_PROVIDER, process?.env?.PUBLIC_VERCEL_GIT_PROVIDER, process?.env?.VUE_APP_VERCEL_GIT_PROVIDER, process?.env?.REDWOOD_ENV_VERCEL_GIT_PROVIDER, process?.env?.SANITY_STUDIO_VERCEL_GIT_PROVIDER);
83
+ owner = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER, process?.env?.NUXT_PUBLIC_VERCEL_GIT_REPO_OWNER, process?.env?.NUXT_ENV_VERCEL_GIT_REPO_OWNER, process?.env?.REACT_APP_VERCEL_GIT_REPO_OWNER, process?.env?.GATSBY_VERCEL_GIT_REPO_OWNER, process?.env?.VITE_VERCEL_GIT_REPO_OWNER, process?.env?.PUBLIC_VERCEL_GIT_REPO_OWNER, process?.env?.VUE_APP_VERCEL_GIT_REPO_OWNER, process?.env?.REDWOOD_ENV_VERCEL_GIT_REPO_OWNER, process?.env?.SANITY_STUDIO_VERCEL_GIT_REPO_OWNER);
84
+ repo = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG, process?.env?.NUXT_PUBLIC_VERCEL_GIT_REPO_SLUG, process?.env?.NUXT_ENV_VERCEL_GIT_REPO_SLUG, process?.env?.REACT_APP_VERCEL_GIT_REPO_SLUG, process?.env?.GATSBY_VERCEL_GIT_REPO_SLUG, process?.env?.VITE_VERCEL_GIT_REPO_SLUG, process?.env?.PUBLIC_VERCEL_GIT_REPO_SLUG, process?.env?.VUE_APP_VERCEL_GIT_REPO_SLUG, process?.env?.REDWOOD_ENV_VERCEL_GIT_REPO_SLUG, process?.env?.SANITY_STUDIO_VERCEL_GIT_REPO_SLUG);
85
+ ref = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF, process?.env?.NUXT_PUBLIC_VERCEL_GIT_COMMIT_REF, process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_REF, process?.env?.REACT_APP_VERCEL_GIT_COMMIT_REF, process?.env?.GATSBY_VERCEL_GIT_COMMIT_REF, process?.env?.VITE_VERCEL_GIT_COMMIT_REF, process?.env?.PUBLIC_VERCEL_GIT_COMMIT_REF, process?.env?.VUE_APP_VERCEL_GIT_COMMIT_REF, process?.env?.REDWOOD_ENV_VERCEL_GIT_COMMIT_REF, process?.env?.SANITY_STUDIO_VERCEL_GIT_COMMIT_REF);
86
+ revision = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, process?.env?.NUXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_SHA, process?.env?.REACT_APP_VERCEL_GIT_COMMIT_SHA, process?.env?.GATSBY_VERCEL_GIT_COMMIT_SHA, process?.env?.VITE_VERCEL_GIT_COMMIT_SHA, process?.env?.PUBLIC_VERCEL_GIT_COMMIT_SHA, process?.env?.VUE_APP_VERCEL_GIT_COMMIT_SHA, process?.env?.REDWOOD_ENV_VERCEL_GIT_COMMIT_SHA, process?.env?.SANITY_STUDIO_VERCEL_GIT_COMMIT_SHA);
87
+ changeId = pickFirstString(process?.env?.NEXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.NUXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.NUXT_ENV_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.REACT_APP_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.GATSBY_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.VITE_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.PUBLIC_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.VUE_APP_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.REDWOOD_ENV_VERCEL_GIT_PULL_REQUEST_ID, process?.env?.SANITY_STUDIO_VERCEL_GIT_PULL_REQUEST_ID);
87
88
  }
88
89
  catch (_ignored) {
89
90
  // process is not defined (or a bundler shimmed it strangely) — skip.
@@ -110,10 +111,10 @@ function detectVcsFromNetlify() {
110
111
  let commit;
111
112
  let reviewId;
112
113
  try {
113
- repositoryUrl = pickFirstString(process?.env?.NEXT_PUBLIC_REPOSITORY_URL, process?.env?.NUXT_ENV_REPOSITORY_URL, process?.env?.REACT_APP_REPOSITORY_URL, process?.env?.GATSBY_REPOSITORY_URL, process?.env?.VITE_REPOSITORY_URL, process?.env?.PUBLIC_REPOSITORY_URL, process?.env?.VUE_APP_REPOSITORY_URL, process?.env?.REDWOOD_ENV_REPOSITORY_URL, process?.env?.SANITY_STUDIO_REPOSITORY_URL);
114
- branch = pickFirstString(process?.env?.NEXT_PUBLIC_BRANCH, process?.env?.NUXT_ENV_BRANCH, process?.env?.REACT_APP_BRANCH, process?.env?.GATSBY_BRANCH, process?.env?.VITE_BRANCH, process?.env?.PUBLIC_BRANCH, process?.env?.VUE_APP_BRANCH, process?.env?.REDWOOD_ENV_BRANCH, process?.env?.SANITY_STUDIO_BRANCH);
115
- commit = pickFirstString(process?.env?.NEXT_PUBLIC_COMMIT_REF, process?.env?.NUXT_ENV_COMMIT_REF, process?.env?.REACT_APP_COMMIT_REF, process?.env?.GATSBY_COMMIT_REF, process?.env?.VITE_COMMIT_REF, process?.env?.PUBLIC_COMMIT_REF, process?.env?.VUE_APP_COMMIT_REF, process?.env?.REDWOOD_ENV_COMMIT_REF, process?.env?.SANITY_STUDIO_COMMIT_REF);
116
- reviewId = pickFirstString(process?.env?.NEXT_PUBLIC_REVIEW_ID, process?.env?.NUXT_ENV_REVIEW_ID, process?.env?.REACT_APP_REVIEW_ID, process?.env?.GATSBY_REVIEW_ID, process?.env?.VITE_REVIEW_ID, process?.env?.PUBLIC_REVIEW_ID, process?.env?.VUE_APP_REVIEW_ID, process?.env?.REDWOOD_ENV_REVIEW_ID, process?.env?.SANITY_STUDIO_REVIEW_ID);
114
+ repositoryUrl = pickFirstString(process?.env?.NEXT_PUBLIC_REPOSITORY_URL, process?.env?.NUXT_PUBLIC_REPOSITORY_URL, process?.env?.NUXT_ENV_REPOSITORY_URL, process?.env?.REACT_APP_REPOSITORY_URL, process?.env?.GATSBY_REPOSITORY_URL, process?.env?.VITE_REPOSITORY_URL, process?.env?.PUBLIC_REPOSITORY_URL, process?.env?.VUE_APP_REPOSITORY_URL, process?.env?.REDWOOD_ENV_REPOSITORY_URL, process?.env?.SANITY_STUDIO_REPOSITORY_URL);
115
+ branch = pickFirstString(process?.env?.NEXT_PUBLIC_BRANCH, process?.env?.NUXT_PUBLIC_BRANCH, process?.env?.NUXT_ENV_BRANCH, process?.env?.REACT_APP_BRANCH, process?.env?.GATSBY_BRANCH, process?.env?.VITE_BRANCH, process?.env?.PUBLIC_BRANCH, process?.env?.VUE_APP_BRANCH, process?.env?.REDWOOD_ENV_BRANCH, process?.env?.SANITY_STUDIO_BRANCH);
116
+ commit = pickFirstString(process?.env?.NEXT_PUBLIC_COMMIT_REF, process?.env?.NUXT_PUBLIC_COMMIT_REF, process?.env?.NUXT_ENV_COMMIT_REF, process?.env?.REACT_APP_COMMIT_REF, process?.env?.GATSBY_COMMIT_REF, process?.env?.VITE_COMMIT_REF, process?.env?.PUBLIC_COMMIT_REF, process?.env?.VUE_APP_COMMIT_REF, process?.env?.REDWOOD_ENV_COMMIT_REF, process?.env?.SANITY_STUDIO_COMMIT_REF);
117
+ reviewId = pickFirstString(process?.env?.NEXT_PUBLIC_REVIEW_ID, process?.env?.NUXT_PUBLIC_REVIEW_ID, process?.env?.NUXT_ENV_REVIEW_ID, process?.env?.REACT_APP_REVIEW_ID, process?.env?.GATSBY_REVIEW_ID, process?.env?.VITE_REVIEW_ID, process?.env?.PUBLIC_REVIEW_ID, process?.env?.VUE_APP_REVIEW_ID, process?.env?.REDWOOD_ENV_REVIEW_ID, process?.env?.SANITY_STUDIO_REVIEW_ID);
117
118
  }
118
119
  catch (_ignored) {
119
120
  // process is not defined — skip.
@@ -129,13 +130,6 @@ function detectVcsFromNetlify() {
129
130
  changeId: reviewId,
130
131
  };
131
132
  }
132
- function pickFirstString(...values) {
133
- for (const value of values) {
134
- if (value)
135
- return value;
136
- }
137
- return undefined;
138
- }
139
133
  function buildRepositoryUrlFromVercel(provider, owner, repo) {
140
134
  if (!provider || !owner || !repo)
141
135
  return undefined;
@@ -15,6 +15,8 @@ function isRateLimited() {
15
15
  return rateLimiter();
16
16
  }
17
17
  export function sendLog(log) {
18
+ if (!vars.isSessionSampled)
19
+ return;
18
20
  if (isRateLimited()) {
19
21
  debug("Transport rate limit. Will not send item.", log);
20
22
  return;
@@ -41,6 +43,8 @@ function sendLogs(logs) {
41
43
  export function sendSpan(span) {
42
44
  if (!span)
43
45
  return;
46
+ if (!vars.isSessionSampled)
47
+ return;
44
48
  if (isRateLimited()) {
45
49
  debug("Transport rate limit. Will not send item.", span);
46
50
  return;
@@ -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,16 @@
1
+ import { crc32 } from "./crc32";
2
+ /**
3
+ * Determines whether a session should be sampled based on the session ID
4
+ * and a configured sampling rate.
5
+ *
6
+ * @param sessionId The hex session ID string
7
+ * @param samplingRate A number between 0 and 100 (inclusive)
8
+ * @returns true if the session should be sampled (data collected), false otherwise
9
+ */
10
+ export function isSessionSampledIn(sessionId, samplingRate) {
11
+ if (samplingRate <= 0)
12
+ return false;
13
+ if (samplingRate >= 100)
14
+ return true;
15
+ return crc32(sessionId) % 100 < samplingRate;
16
+ }
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isSessionSampledIn } from "./session-sampling";
3
+ import { generateSessionId } from "./session-id";
4
+ describe("isSessionSampledIn", () => {
5
+ it("returns false when sampling rate is 0", () => {
6
+ expect(isSessionSampledIn("00abcdef01234567", 0)).toBe(false);
7
+ });
8
+ it("returns true when sampling rate is 100", () => {
9
+ expect(isSessionSampledIn("00abcdef01234567", 100)).toBe(true);
10
+ });
11
+ it("returns deterministic results for the same session ID and rate", () => {
12
+ const sessionId = "00abcdef01234567";
13
+ const rate = 50;
14
+ const result1 = isSessionSampledIn(sessionId, rate);
15
+ const result2 = isSessionSampledIn(sessionId, rate);
16
+ expect(result1).toBe(result2);
17
+ });
18
+ it("produces different results for different session IDs", () => {
19
+ const results = new Set();
20
+ // Generate enough IDs to get both true and false with high probability
21
+ for (let i = 0; i < 100; i++) {
22
+ results.add(isSessionSampledIn(generateSessionId(), 50));
23
+ }
24
+ expect(results.size).toBe(2);
25
+ });
26
+ it("produces roughly correct distribution over many session IDs", () => {
27
+ const rate = 30;
28
+ const total = 10000;
29
+ let sampledIn = 0;
30
+ for (let i = 0; i < total; i++) {
31
+ if (isSessionSampledIn(generateSessionId(), rate)) {
32
+ sampledIn++;
33
+ }
34
+ }
35
+ const actualRate = sampledIn / total;
36
+ // Allow 5% tolerance
37
+ expect(actualRate).toBeGreaterThan(0.25);
38
+ expect(actualRate).toBeLessThan(0.35);
39
+ });
40
+ it("handles edge case: rate just above 0", () => {
41
+ // With rate=1, about 1% of sessions should be sampled in
42
+ let sampledIn = 0;
43
+ const total = 10000;
44
+ for (let i = 0; i < total; i++) {
45
+ if (isSessionSampledIn(generateSessionId(), 1)) {
46
+ sampledIn++;
47
+ }
48
+ }
49
+ const actualRate = sampledIn / total;
50
+ expect(actualRate).toBeGreaterThan(0.0);
51
+ expect(actualRate).toBeLessThan(0.05);
52
+ });
53
+ it("handles edge case: rate just below 100", () => {
54
+ // With rate=99, about 99% of sessions should be sampled in
55
+ let sampledIn = 0;
56
+ const total = 10000;
57
+ for (let i = 0; i < total; i++) {
58
+ if (isSessionSampledIn(generateSessionId(), 99)) {
59
+ sampledIn++;
60
+ }
61
+ }
62
+ const actualRate = sampledIn / total;
63
+ expect(actualRate).toBeGreaterThan(0.95);
64
+ expect(actualRate).toBeLessThan(1.0);
65
+ });
66
+ it("returns false for negative sampling rates", () => {
67
+ expect(isSessionSampledIn("00abcdef01234567", -10)).toBe(false);
68
+ });
69
+ it("returns true for sampling rates above 100", () => {
70
+ expect(isSessionSampledIn("00abcdef01234567", 150)).toBe(true);
71
+ });
72
+ });
@@ -24,4 +24,5 @@ export const vars = {
24
24
  includeParts: [],
25
25
  },
26
26
  enableTransportCompression: false,
27
+ isSessionSampled: true,
27
28
  };