@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/README.md +12 -0
- package/dist/dash0.iife.js +1 -1
- package/dist/dash0.iife.js.map +1 -1
- package/dist/dash0.js +1 -1
- package/dist/dash0.js.map +1 -1
- package/dist/dash0.umd.cjs +1 -1
- package/dist/dash0.umd.cjs.map +1 -1
- package/dist/modules/api/browser-env.js +33 -0
- package/dist/modules/api/init.js +42 -19
- package/dist/modules/api/init_test.js +54 -2
- package/dist/modules/api/vcs.js +11 -17
- package/dist/modules/transport/index.js +4 -0
- package/dist/modules/utils/index.js +1 -0
- package/dist/modules/utils/session-sampling.js +16 -0
- package/dist/modules/utils/session-sampling_test.js +72 -0
- package/dist/modules/vars.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/api/browser-env.d.ts +54 -0
- package/dist/types/types/options.d.ts +12 -3
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/session-sampling.d.ts +9 -0
- package/dist/types/utils/session-sampling_test.d.ts +1 -0
- package/dist/types/vars.d.ts +5 -0
- package/package.json +4 -5
- package/src/api/browser-env.ts +94 -0
- package/src/api/init.ts +82 -21
- package/src/api/init_test.ts +70 -1
- package/src/api/vcs.ts +11 -42
- package/src/transport/index.ts +3 -0
- package/src/types/options.ts +13 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/session-sampling.ts +15 -0
- package/src/utils/session-sampling_test.ts +83 -0
- package/src/vars.ts +7 -0
|
@@ -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
|
+
}
|
package/dist/modules/api/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
});
|
package/dist/modules/api/vcs.js
CHANGED
|
@@ -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;
|
|
@@ -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
|
+
});
|