@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,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,7 +1,7 @@
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";
@@ -9,6 +9,7 @@ import { instrumentFetch } from "../instrumentations/http/fetch";
9
9
  import { startNavigationInstrumentation } from "../instrumentations/navigation";
10
10
  import { merge } from "ts-deepmerge";
11
11
  import { initializeTabId } from "../utils/tab-id";
12
+ import { pickFirstString } from "./browser-env";
12
13
  import { applyVcsResourceAttributes } from "./vcs";
13
14
  let hasBeenInitialised = false;
14
15
  export function init(opts) {
@@ -56,6 +57,15 @@ export function init(opts) {
56
57
  initializeSignalAttributes(opts);
57
58
  initializeTabId();
58
59
  trackSessions(opts.sessionInactivityTimeoutMillis, opts.sessionTerminationTimeoutMillis);
60
+ if (opts.sessionSamplingRate != null) {
61
+ const rate = Math.max(0, Math.min(100, opts.sessionSamplingRate));
62
+ vars.isSessionSampled = sessionId != null ? isSessionSampledIn(sessionId, rate) : rate > 0;
63
+ }
64
+ if (!vars.isSessionSampled) {
65
+ debug("Session is not sampled. No telemetry will be transmitted for this session.");
66
+ hasBeenInitialised = true;
67
+ return;
68
+ }
59
69
  if (isInstrumentationEnabled("@dash0/navigation", opts)) {
60
70
  startNavigationInstrumentation();
61
71
  }
@@ -107,17 +117,18 @@ function isSupported() {
107
117
  function isClient() {
108
118
  return win != null;
109
119
  }
120
+ // Vercel auto-prefixes its system env vars under every framework preset
121
+ // (see https://vercel.com/docs/environment-variables/framework-environment-variables).
122
+ // The shared `FrameworkPrefix` union in `./browser-env` is the single source
123
+ // of truth for which prefixes the SDK recognises. To add a new prefix, edit
124
+ // it there; the literal accessors below pick it up automatically via the
125
+ // typed `process.env` declaration.
110
126
  function detectEnvironment(opts) {
111
- // if there is a manually specified value we use that
112
127
  if (opts.environment) {
113
128
  return opts.environment;
114
129
  }
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
130
  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;
131
+ 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
132
  }
122
133
  catch (_ignored) {
123
134
  return undefined;
@@ -127,12 +138,8 @@ function detectDeploymentName(opts) {
127
138
  if (opts.deploymentName) {
128
139
  return opts.deploymentName;
129
140
  }
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
141
  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;
142
+ 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
143
  }
137
144
  catch (_ignored) {
138
145
  return undefined;
@@ -142,12 +149,8 @@ function detectDeploymentId(opts) {
142
149
  if (opts.deploymentId) {
143
150
  return opts.deploymentId;
144
151
  }
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
152
  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;
153
+ 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
154
  }
152
155
  catch (_ignored) {
153
156
  return undefined;
@@ -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
  };