@dash0/sdk-web 0.20.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.
- 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 +23 -18
- package/dist/modules/api/init_test.js +272 -1
- package/dist/modules/api/vcs.js +190 -0
- package/dist/modules/semantic-conventions.js +8 -0
- 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/api/vcs.d.ts +10 -0
- package/dist/types/semantic-conventions.d.ts +7 -0
- package/dist/types/types/options.d.ts +62 -0
- 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 +1 -1
- package/src/api/browser-env.ts +94 -0
- package/src/api/init.ts +62 -20
- package/src/api/init_test.ts +357 -1
- package/src/api/vcs.ts +328 -0
- package/src/semantic-conventions.ts +9 -0
- package/src/transport/index.ts +3 -0
- package/src/types/options.ts +66 -0
- 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,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,8 @@ 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";
|
|
13
|
+
import { applyVcsResourceAttributes } from "./vcs";
|
|
12
14
|
let hasBeenInitialised = false;
|
|
13
15
|
export function init(opts) {
|
|
14
16
|
if (hasBeenInitialised) {
|
|
@@ -55,6 +57,15 @@ export function init(opts) {
|
|
|
55
57
|
initializeSignalAttributes(opts);
|
|
56
58
|
initializeTabId();
|
|
57
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
|
+
}
|
|
58
69
|
if (isInstrumentationEnabled("@dash0/navigation", opts)) {
|
|
59
70
|
startNavigationInstrumentation();
|
|
60
71
|
}
|
|
@@ -89,6 +100,7 @@ function initializeResourceAttributes(opts) {
|
|
|
89
100
|
if (deploymentId) {
|
|
90
101
|
addAttribute(vars.resource.attributes, DEPLOYMENT_ID, deploymentId);
|
|
91
102
|
}
|
|
103
|
+
applyVcsResourceAttributes(opts);
|
|
92
104
|
}
|
|
93
105
|
function initializeSignalAttributes(opts) {
|
|
94
106
|
addAttribute(vars.signalAttributes, PAGE_LOAD_ID, generateUniqueId(PAGE_LOAD_ID_BYTES));
|
|
@@ -105,17 +117,18 @@ function isSupported() {
|
|
|
105
117
|
function isClient() {
|
|
106
118
|
return win != null;
|
|
107
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.
|
|
108
126
|
function detectEnvironment(opts) {
|
|
109
|
-
// if there is a manually specified value we use that
|
|
110
127
|
if (opts.environment) {
|
|
111
128
|
return opts.environment;
|
|
112
129
|
}
|
|
113
|
-
// if process isn't defined access to it causes an exception, but we can't check for its present due to how
|
|
114
|
-
// plugins like webpack define work.
|
|
115
130
|
try {
|
|
116
|
-
|
|
117
|
-
// @ts-expect-error -- we need to access like this to allow webpack in the nextjs build to replace this
|
|
118
|
-
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);
|
|
119
132
|
}
|
|
120
133
|
catch (_ignored) {
|
|
121
134
|
return undefined;
|
|
@@ -125,12 +138,8 @@ function detectDeploymentName(opts) {
|
|
|
125
138
|
if (opts.deploymentName) {
|
|
126
139
|
return opts.deploymentName;
|
|
127
140
|
}
|
|
128
|
-
// if process isn't defined access to it causes an exception, but we can't check for its present due to how
|
|
129
|
-
// plugins like webpack define work.
|
|
130
141
|
try {
|
|
131
|
-
|
|
132
|
-
// @ts-expect-error -- we need to access like this to allow webpack in the nextjs build to replace this
|
|
133
|
-
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);
|
|
134
143
|
}
|
|
135
144
|
catch (_ignored) {
|
|
136
145
|
return undefined;
|
|
@@ -140,12 +149,8 @@ function detectDeploymentId(opts) {
|
|
|
140
149
|
if (opts.deploymentId) {
|
|
141
150
|
return opts.deploymentId;
|
|
142
151
|
}
|
|
143
|
-
// if process isn't defined access to it causes an exception, but we can't check for its present due to how
|
|
144
|
-
// plugins like webpack define work.
|
|
145
152
|
try {
|
|
146
|
-
|
|
147
|
-
// @ts-expect-error -- we need to access like this to allow webpack in the nextjs build to replace this
|
|
148
|
-
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);
|
|
149
154
|
}
|
|
150
155
|
catch (_ignored) {
|
|
151
156
|
return undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { SERVICE_NAME, SERVICE_NAMESPACE } 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(),
|
|
@@ -315,4 +315,275 @@ describe("init", () => {
|
|
|
315
315
|
expect(serviceNameAttr?.value.stringValue).toBe("unknown");
|
|
316
316
|
});
|
|
317
317
|
});
|
|
318
|
+
describe("vcs auto-detection", () => {
|
|
319
|
+
afterEach(() => {
|
|
320
|
+
vi.unstubAllEnvs();
|
|
321
|
+
});
|
|
322
|
+
const stringAttr = (key) => vars.resource.attributes.find((attr) => attr.key === key)?.value.stringValue;
|
|
323
|
+
it("derives full vcs resource attributes from Vercel env vars", () => {
|
|
324
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_PROVIDER", "github");
|
|
325
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER", "dash0hq");
|
|
326
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG", "dash0");
|
|
327
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF", "main");
|
|
328
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA", "abc123");
|
|
329
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID", "42");
|
|
330
|
+
init(baseOptions);
|
|
331
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBe("github");
|
|
332
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("dash0hq");
|
|
333
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBe("dash0");
|
|
334
|
+
expect(stringAttr(VCS_REPOSITORY_URL_FULL)).toBe("https://github.com/dash0hq/dash0");
|
|
335
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("main");
|
|
336
|
+
expect(stringAttr(VCS_REF_HEAD_REVISION)).toBe("abc123");
|
|
337
|
+
expect(stringAttr(VCS_CHANGE_ID)).toBe("42");
|
|
338
|
+
});
|
|
339
|
+
// The 9 framework prefixes Vercel auto-prefixes its VERCEL_GIT_* system
|
|
340
|
+
// vars under (per https://vercel.com/docs/environment-variables/framework-environment-variables).
|
|
341
|
+
// A passing test for each confirms the literal-accessor enumeration in
|
|
342
|
+
// detectVcsFromVercel is actually wired up.
|
|
343
|
+
const FRAMEWORK_PREFIX_SAMPLES = [
|
|
344
|
+
["Next.js", "NEXT_PUBLIC_"],
|
|
345
|
+
["Nuxt 3", "NUXT_PUBLIC_"],
|
|
346
|
+
["Nuxt 2", "NUXT_ENV_"],
|
|
347
|
+
["Create React App", "REACT_APP_"],
|
|
348
|
+
["Gatsby", "GATSBY_"],
|
|
349
|
+
["Vite", "VITE_"],
|
|
350
|
+
["Astro / SvelteKit / Hydrogen", "PUBLIC_"],
|
|
351
|
+
["Vue CLI", "VUE_APP_"],
|
|
352
|
+
["RedwoodJS", "REDWOOD_ENV_"],
|
|
353
|
+
["Sanity Studio", "SANITY_STUDIO_"],
|
|
354
|
+
];
|
|
355
|
+
it.each(FRAMEWORK_PREFIX_SAMPLES)("derives Vercel vcs attributes from %s framework prefix (%s)", (_label, prefix) => {
|
|
356
|
+
vi.stubEnv(`${prefix}VERCEL_GIT_PROVIDER`, "github");
|
|
357
|
+
vi.stubEnv(`${prefix}VERCEL_GIT_REPO_OWNER`, "dash0hq");
|
|
358
|
+
vi.stubEnv(`${prefix}VERCEL_GIT_REPO_SLUG`, "dash0");
|
|
359
|
+
vi.stubEnv(`${prefix}VERCEL_GIT_COMMIT_REF`, "main");
|
|
360
|
+
vi.stubEnv(`${prefix}VERCEL_GIT_COMMIT_SHA`, "abc123");
|
|
361
|
+
vi.stubEnv(`${prefix}VERCEL_GIT_PULL_REQUEST_ID`, "42");
|
|
362
|
+
init(baseOptions);
|
|
363
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBe("github");
|
|
364
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("dash0hq");
|
|
365
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBe("dash0");
|
|
366
|
+
expect(stringAttr(VCS_REPOSITORY_URL_FULL)).toBe("https://github.com/dash0hq/dash0");
|
|
367
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("main");
|
|
368
|
+
expect(stringAttr(VCS_REF_HEAD_REVISION)).toBe("abc123");
|
|
369
|
+
expect(stringAttr(VCS_CHANGE_ID)).toBe("42");
|
|
370
|
+
});
|
|
371
|
+
it.each(FRAMEWORK_PREFIX_SAMPLES)("derives Netlify vcs attributes from %s framework prefix (%s)", (_label, prefix) => {
|
|
372
|
+
vi.stubEnv(`${prefix}REPOSITORY_URL`, "https://github.com/dash0hq/dash0");
|
|
373
|
+
vi.stubEnv(`${prefix}BRANCH`, "feature/x");
|
|
374
|
+
vi.stubEnv(`${prefix}COMMIT_REF`, "def456");
|
|
375
|
+
vi.stubEnv(`${prefix}REVIEW_ID`, "99");
|
|
376
|
+
init(baseOptions);
|
|
377
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBe("github");
|
|
378
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("dash0hq");
|
|
379
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBe("dash0");
|
|
380
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("feature/x");
|
|
381
|
+
expect(stringAttr(VCS_REF_HEAD_REVISION)).toBe("def456");
|
|
382
|
+
expect(stringAttr(VCS_CHANGE_ID)).toBe("99");
|
|
383
|
+
});
|
|
384
|
+
it("derives vcs resource attributes from Netlify env vars (parsed REPOSITORY_URL)", () => {
|
|
385
|
+
vi.stubEnv("NEXT_PUBLIC_REPOSITORY_URL", "https://github.com/dash0hq/dash0.git");
|
|
386
|
+
vi.stubEnv("NEXT_PUBLIC_BRANCH", "feature/x");
|
|
387
|
+
vi.stubEnv("NEXT_PUBLIC_COMMIT_REF", "def456");
|
|
388
|
+
vi.stubEnv("NEXT_PUBLIC_REVIEW_ID", "99");
|
|
389
|
+
init(baseOptions);
|
|
390
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBe("github");
|
|
391
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("dash0hq");
|
|
392
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBe("dash0");
|
|
393
|
+
expect(stringAttr(VCS_REPOSITORY_URL_FULL)).toBe("https://github.com/dash0hq/dash0.git");
|
|
394
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("feature/x");
|
|
395
|
+
expect(stringAttr(VCS_REF_HEAD_REVISION)).toBe("def456");
|
|
396
|
+
expect(stringAttr(VCS_CHANGE_ID)).toBe("99");
|
|
397
|
+
});
|
|
398
|
+
it("prefers Vercel values over Netlify values when both are present", () => {
|
|
399
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_PROVIDER", "github");
|
|
400
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER", "vercel-owner");
|
|
401
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG", "vercel-repo");
|
|
402
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF", "vercel-branch");
|
|
403
|
+
vi.stubEnv("NEXT_PUBLIC_REPOSITORY_URL", "https://github.com/netlify-owner/netlify-repo");
|
|
404
|
+
vi.stubEnv("NEXT_PUBLIC_BRANCH", "netlify-branch");
|
|
405
|
+
init(baseOptions);
|
|
406
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("vercel-owner");
|
|
407
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBe("vercel-repo");
|
|
408
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("vercel-branch");
|
|
409
|
+
});
|
|
410
|
+
it("opts.vcs overrides individual auto-detected fields", () => {
|
|
411
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_PROVIDER", "github");
|
|
412
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER", "dash0hq");
|
|
413
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG", "dash0");
|
|
414
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF", "main");
|
|
415
|
+
init({
|
|
416
|
+
...baseOptions,
|
|
417
|
+
vcs: {
|
|
418
|
+
refHeadName: "release/2026.06",
|
|
419
|
+
changeId: "1234",
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("dash0hq");
|
|
423
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("release/2026.06");
|
|
424
|
+
expect(stringAttr(VCS_CHANGE_ID)).toBe("1234");
|
|
425
|
+
});
|
|
426
|
+
it("disableVcsDetection: true emits no vcs.* resource attributes even when env vars are set", () => {
|
|
427
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_PROVIDER", "github");
|
|
428
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER", "dash0hq");
|
|
429
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA", "abc123");
|
|
430
|
+
init({ ...baseOptions, disableVcsDetection: true });
|
|
431
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBeUndefined();
|
|
432
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBeUndefined();
|
|
433
|
+
expect(stringAttr(VCS_REF_HEAD_REVISION)).toBeUndefined();
|
|
434
|
+
});
|
|
435
|
+
it("disableVcsDetection: true still applies opts.vcs manual overrides", () => {
|
|
436
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_PROVIDER", "github");
|
|
437
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER", "dash0hq");
|
|
438
|
+
init({
|
|
439
|
+
...baseOptions,
|
|
440
|
+
disableVcsDetection: true,
|
|
441
|
+
vcs: { providerName: "gitlab", refHeadName: "release/1.0" },
|
|
442
|
+
});
|
|
443
|
+
// env-var values are suppressed
|
|
444
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBeUndefined();
|
|
445
|
+
// manual overrides still win
|
|
446
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBe("gitlab");
|
|
447
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("release/1.0");
|
|
448
|
+
});
|
|
449
|
+
it("applies opts.vcs fields as the sole source when no env vars are set", () => {
|
|
450
|
+
init({
|
|
451
|
+
...baseOptions,
|
|
452
|
+
vcs: {
|
|
453
|
+
providerName: "github",
|
|
454
|
+
ownerName: "my-org",
|
|
455
|
+
repositoryName: "my-repo",
|
|
456
|
+
repositoryUrlFull: "https://github.com/my-org/my-repo",
|
|
457
|
+
refHeadName: "main",
|
|
458
|
+
refHeadRevision: "deadbeef",
|
|
459
|
+
changeId: "7",
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBe("github");
|
|
463
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("my-org");
|
|
464
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBe("my-repo");
|
|
465
|
+
expect(stringAttr(VCS_REPOSITORY_URL_FULL)).toBe("https://github.com/my-org/my-repo");
|
|
466
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("main");
|
|
467
|
+
expect(stringAttr(VCS_REF_HEAD_REVISION)).toBe("deadbeef");
|
|
468
|
+
expect(stringAttr(VCS_CHANGE_ID)).toBe("7");
|
|
469
|
+
});
|
|
470
|
+
it("emits nothing when no env vars and no opts.vcs are provided", () => {
|
|
471
|
+
init(baseOptions);
|
|
472
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBeUndefined();
|
|
473
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBeUndefined();
|
|
474
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBeUndefined();
|
|
475
|
+
expect(stringAttr(VCS_REPOSITORY_URL_FULL)).toBeUndefined();
|
|
476
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBeUndefined();
|
|
477
|
+
expect(stringAttr(VCS_REF_HEAD_REVISION)).toBeUndefined();
|
|
478
|
+
expect(stringAttr(VCS_CHANGE_ID)).toBeUndefined();
|
|
479
|
+
});
|
|
480
|
+
it("emits only the partial attributes that are available", () => {
|
|
481
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA", "abc123");
|
|
482
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF", "main");
|
|
483
|
+
init(baseOptions);
|
|
484
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("main");
|
|
485
|
+
expect(stringAttr(VCS_REF_HEAD_REVISION)).toBe("abc123");
|
|
486
|
+
expect(stringAttr(VCS_REPOSITORY_URL_FULL)).toBeUndefined();
|
|
487
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBeUndefined();
|
|
488
|
+
});
|
|
489
|
+
it("does not derive repository URL when provider is unknown", () => {
|
|
490
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_PROVIDER", "someproprietary");
|
|
491
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER", "owner");
|
|
492
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG", "repo");
|
|
493
|
+
init(baseOptions);
|
|
494
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBe("someproprietary");
|
|
495
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("owner");
|
|
496
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBe("repo");
|
|
497
|
+
expect(stringAttr(VCS_REPOSITORY_URL_FULL)).toBeUndefined();
|
|
498
|
+
});
|
|
499
|
+
it("accepts gist.github.com as a github host alias", () => {
|
|
500
|
+
vi.stubEnv("NEXT_PUBLIC_REPOSITORY_URL", "https://gist.github.com/owner/abc123");
|
|
501
|
+
init(baseOptions);
|
|
502
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBe("github");
|
|
503
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("owner");
|
|
504
|
+
});
|
|
505
|
+
it("rejects arbitrary github.com subdomains", () => {
|
|
506
|
+
vi.stubEnv("NEXT_PUBLIC_REPOSITORY_URL", "https://evil.github.com/attacker/repo");
|
|
507
|
+
init(baseOptions);
|
|
508
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBeUndefined();
|
|
509
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBeUndefined();
|
|
510
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBeUndefined();
|
|
511
|
+
});
|
|
512
|
+
it("ignores a Netlify REPOSITORY_URL on an unknown host", () => {
|
|
513
|
+
vi.stubEnv("NEXT_PUBLIC_REPOSITORY_URL", "https://internal-git.example.com/team/app");
|
|
514
|
+
vi.stubEnv("NEXT_PUBLIC_BRANCH", "main");
|
|
515
|
+
init(baseOptions);
|
|
516
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBeUndefined();
|
|
517
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBeUndefined();
|
|
518
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBeUndefined();
|
|
519
|
+
expect(stringAttr(VCS_REPOSITORY_URL_FULL)).toBe("https://internal-git.example.com/team/app");
|
|
520
|
+
expect(stringAttr(VCS_REF_HEAD_NAME)).toBe("main");
|
|
521
|
+
});
|
|
522
|
+
it("parses GitLab nested-group repository URLs to the last path segment", () => {
|
|
523
|
+
vi.stubEnv("NEXT_PUBLIC_REPOSITORY_URL", "https://gitlab.com/group/subgroup/app.git");
|
|
524
|
+
init(baseOptions);
|
|
525
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBe("gitlab");
|
|
526
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBe("group");
|
|
527
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBe("app");
|
|
528
|
+
});
|
|
529
|
+
it("ignores a malformed REPOSITORY_URL", () => {
|
|
530
|
+
vi.stubEnv("NEXT_PUBLIC_REPOSITORY_URL", "not-a-url");
|
|
531
|
+
init(baseOptions);
|
|
532
|
+
expect(stringAttr(VCS_REPOSITORY_URL_FULL)).toBe("not-a-url");
|
|
533
|
+
expect(stringAttr(VCS_PROVIDER_NAME)).toBeUndefined();
|
|
534
|
+
expect(stringAttr(VCS_OWNER_NAME)).toBeUndefined();
|
|
535
|
+
expect(stringAttr(VCS_REPOSITORY_NAME)).toBeUndefined();
|
|
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
|
+
});
|
|
318
589
|
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { vars } from "../vars";
|
|
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
|
+
import { addAttribute } from "../utils/otel";
|
|
4
|
+
import { pickFirstString } from "./browser-env";
|
|
5
|
+
// Single source of truth mapping each VcsAttributes field to its OTel
|
|
6
|
+
// resource attribute name. Typed as `Record<keyof VcsAttributes, string>` so
|
|
7
|
+
// adding a field to VcsAttributes without adding an entry here is a compile
|
|
8
|
+
// error — no silent "I forgot to apply that field" bugs.
|
|
9
|
+
const VCS_FIELD_TO_ATTRIBUTE = {
|
|
10
|
+
providerName: VCS_PROVIDER_NAME,
|
|
11
|
+
ownerName: VCS_OWNER_NAME,
|
|
12
|
+
repositoryName: VCS_REPOSITORY_NAME,
|
|
13
|
+
repositoryUrlFull: VCS_REPOSITORY_URL_FULL,
|
|
14
|
+
refHeadName: VCS_REF_HEAD_NAME,
|
|
15
|
+
refHeadRevision: VCS_REF_HEAD_REVISION,
|
|
16
|
+
changeId: VCS_CHANGE_ID,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Derive VCS (version control) context from the build environment and apply
|
|
20
|
+
* the resulting `vcs.*` resource attributes to `vars.resource.attributes`.
|
|
21
|
+
*
|
|
22
|
+
* Detection precedence per attribute: `opts.vcs` → Vercel env vars → Netlify
|
|
23
|
+
* env vars. Manual overrides via `opts.vcs` always win, even when
|
|
24
|
+
* `opts.disableVcsDetection` is set.
|
|
25
|
+
*/
|
|
26
|
+
export function applyVcsResourceAttributes(opts) {
|
|
27
|
+
const vcs = detectVcs(opts);
|
|
28
|
+
for (const field of Object.keys(VCS_FIELD_TO_ATTRIBUTE)) {
|
|
29
|
+
const value = vcs[field];
|
|
30
|
+
if (value) {
|
|
31
|
+
addAttribute(vars.resource.attributes, VCS_FIELD_TO_ATTRIBUTE[field], value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function detectVcs(opts) {
|
|
36
|
+
// opts.vcs manual overrides always win, even when auto-detection is disabled.
|
|
37
|
+
// This lets callers on non-Vercel/Netlify platforms supply context explicitly
|
|
38
|
+
// while still opting out of any env-var reading.
|
|
39
|
+
const override = opts.vcs ?? {};
|
|
40
|
+
if (opts.disableVcsDetection) {
|
|
41
|
+
return override;
|
|
42
|
+
}
|
|
43
|
+
const vercel = detectVcsFromVercel();
|
|
44
|
+
const netlify = detectVcsFromNetlify();
|
|
45
|
+
return {
|
|
46
|
+
providerName: override.providerName ?? vercel.providerName ?? netlify.providerName,
|
|
47
|
+
ownerName: override.ownerName ?? vercel.ownerName ?? netlify.ownerName,
|
|
48
|
+
repositoryName: override.repositoryName ?? vercel.repositoryName ?? netlify.repositoryName,
|
|
49
|
+
repositoryUrlFull: override.repositoryUrlFull ?? vercel.repositoryUrlFull ?? netlify.repositoryUrlFull,
|
|
50
|
+
refHeadName: override.refHeadName ?? vercel.refHeadName ?? netlify.refHeadName,
|
|
51
|
+
refHeadRevision: override.refHeadRevision ?? vercel.refHeadRevision ?? netlify.refHeadRevision,
|
|
52
|
+
changeId: override.changeId ?? vercel.changeId ?? netlify.changeId,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Frameworks expose env vars to the browser bundle using a per-framework
|
|
56
|
+
// prefix (Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`, etc.). For each known
|
|
57
|
+
// vendor suffix (e.g. Vercel's `VERCEL_GIT_PROVIDER`, Netlify's
|
|
58
|
+
// `REPOSITORY_URL`) we enumerate the prefixed variants below.
|
|
59
|
+
//
|
|
60
|
+
// IMPORTANT: each `process.env.NAME` MUST be a LITERAL accessor. Webpack
|
|
61
|
+
// DefinePlugin, Next.js, Gatsby, and equivalents substitute env vars at
|
|
62
|
+
// build time only when they see the literal form. Dynamic lookups like
|
|
63
|
+
// `process.env[name]` or iterating a string array are NOT substituted —
|
|
64
|
+
// they would resolve to `undefined` in the browser bundle.
|
|
65
|
+
//
|
|
66
|
+
// The 9 prefixes below are the framework presets Vercel auto-prefixes
|
|
67
|
+
// `VERCEL_*` system vars under (https://vercel.com/docs/environment-variables/framework-environment-variables).
|
|
68
|
+
// Users on platforms without auto-prefixing (Netlify, Cloudflare Pages, custom
|
|
69
|
+
// CI) can manually expose any env var under any of these prefixes and get
|
|
70
|
+
// the same auto-detection.
|
|
71
|
+
//
|
|
72
|
+
// Adding a new framework prefix = one new literal accessor per attribute in
|
|
73
|
+
// each `detectVcsFrom*` function below.
|
|
74
|
+
function detectVcsFromVercel() {
|
|
75
|
+
let provider;
|
|
76
|
+
let owner;
|
|
77
|
+
let repo;
|
|
78
|
+
let ref;
|
|
79
|
+
let revision;
|
|
80
|
+
let changeId;
|
|
81
|
+
try {
|
|
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);
|
|
88
|
+
}
|
|
89
|
+
catch (_ignored) {
|
|
90
|
+
// process is not defined (or a bundler shimmed it strangely) — skip.
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
providerName: provider,
|
|
94
|
+
ownerName: owner,
|
|
95
|
+
repositoryName: repo,
|
|
96
|
+
repositoryUrlFull: buildRepositoryUrlFromVercel(provider, owner, repo),
|
|
97
|
+
refHeadName: ref,
|
|
98
|
+
refHeadRevision: revision,
|
|
99
|
+
changeId,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function detectVcsFromNetlify() {
|
|
103
|
+
// Netlify exposes read-only build env vars (REPOSITORY_URL, BRANCH,
|
|
104
|
+
// COMMIT_REF, REVIEW_ID) but does not auto-prefix them for browser
|
|
105
|
+
// exposure. Users must surface them via their framework's prefix
|
|
106
|
+
// convention themselves (e.g. `NEXT_PUBLIC_REPOSITORY_URL = $REPOSITORY_URL`
|
|
107
|
+
// for Next.js, `VITE_REPOSITORY_URL = $REPOSITORY_URL` for Vite, etc.).
|
|
108
|
+
// We enumerate the same 9 framework prefixes as the Vercel path above.
|
|
109
|
+
let repositoryUrl;
|
|
110
|
+
let branch;
|
|
111
|
+
let commit;
|
|
112
|
+
let reviewId;
|
|
113
|
+
try {
|
|
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);
|
|
118
|
+
}
|
|
119
|
+
catch (_ignored) {
|
|
120
|
+
// process is not defined — skip.
|
|
121
|
+
}
|
|
122
|
+
const parsed = parseRepositoryUrl(repositoryUrl);
|
|
123
|
+
return {
|
|
124
|
+
providerName: parsed?.providerName,
|
|
125
|
+
ownerName: parsed?.ownerName,
|
|
126
|
+
repositoryName: parsed?.repositoryName,
|
|
127
|
+
repositoryUrlFull: repositoryUrl,
|
|
128
|
+
refHeadName: branch,
|
|
129
|
+
refHeadRevision: commit,
|
|
130
|
+
changeId: reviewId,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function buildRepositoryUrlFromVercel(provider, owner, repo) {
|
|
134
|
+
if (!provider || !owner || !repo)
|
|
135
|
+
return undefined;
|
|
136
|
+
const host = vcsHostForProvider(provider);
|
|
137
|
+
if (!host)
|
|
138
|
+
return undefined;
|
|
139
|
+
return `https://${host}/${owner}/${repo}`;
|
|
140
|
+
}
|
|
141
|
+
// Single source of truth for provider↔host mapping. Both `vcsHostForProvider`
|
|
142
|
+
// (Vercel path: provider name is known, derive the canonical host) and
|
|
143
|
+
// `vcsProviderForHost` (Netlify path: only the URL is known, derive the
|
|
144
|
+
// provider) consult this map so adding a provider is one edit. Each entry is
|
|
145
|
+
// a list of accepted hostnames; the FIRST entry is the canonical host used
|
|
146
|
+
// when building URLs from a provider name. Subsequent entries are exact
|
|
147
|
+
// aliases — we do not match arbitrary subdomains.
|
|
148
|
+
const VCS_PROVIDER_HOSTS = {
|
|
149
|
+
github: ["github.com", "gist.github.com"],
|
|
150
|
+
gitlab: ["gitlab.com"],
|
|
151
|
+
bitbucket: ["bitbucket.org"],
|
|
152
|
+
};
|
|
153
|
+
function vcsHostForProvider(provider) {
|
|
154
|
+
return VCS_PROVIDER_HOSTS[provider]?.[0];
|
|
155
|
+
}
|
|
156
|
+
function parseRepositoryUrl(url) {
|
|
157
|
+
if (!url)
|
|
158
|
+
return undefined;
|
|
159
|
+
let host;
|
|
160
|
+
let pathname;
|
|
161
|
+
try {
|
|
162
|
+
const parsed = new URL(url);
|
|
163
|
+
host = parsed.host;
|
|
164
|
+
pathname = parsed.pathname;
|
|
165
|
+
}
|
|
166
|
+
catch (_ignored) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const provider = vcsProviderForHost(host);
|
|
170
|
+
if (!provider)
|
|
171
|
+
return undefined;
|
|
172
|
+
const trimmed = pathname.replace(/^\/+/, "").replace(/\.git$/, "");
|
|
173
|
+
const segments = trimmed.split("/");
|
|
174
|
+
if (segments.length < 2)
|
|
175
|
+
return undefined;
|
|
176
|
+
const ownerName = segments[0];
|
|
177
|
+
// Bitbucket and GitLab can have nested groups; the repository slug is the
|
|
178
|
+
// last path segment. GitHub paths are always two segments.
|
|
179
|
+
const repositoryName = segments[segments.length - 1];
|
|
180
|
+
if (!ownerName || !repositoryName)
|
|
181
|
+
return undefined;
|
|
182
|
+
return { providerName: provider, ownerName, repositoryName };
|
|
183
|
+
}
|
|
184
|
+
function vcsProviderForHost(host) {
|
|
185
|
+
for (const [provider, hosts] of Object.entries(VCS_PROVIDER_HOSTS)) {
|
|
186
|
+
if (hosts.includes(host))
|
|
187
|
+
return provider;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
@@ -5,6 +5,14 @@ export const SERVICE_VERSION = "service.version";
|
|
|
5
5
|
export const DEPLOYMENT_ENVIRONMENT_NAME = "deployment.environment.name";
|
|
6
6
|
export const DEPLOYMENT_NAME = "deployment.name";
|
|
7
7
|
export const DEPLOYMENT_ID = "deployment.id";
|
|
8
|
+
// VCS Resource Attribute Keys
|
|
9
|
+
export const VCS_PROVIDER_NAME = "vcs.provider.name";
|
|
10
|
+
export const VCS_OWNER_NAME = "vcs.owner.name";
|
|
11
|
+
export const VCS_REPOSITORY_NAME = "vcs.repository.name";
|
|
12
|
+
export const VCS_REPOSITORY_URL_FULL = "vcs.repository.url.full";
|
|
13
|
+
export const VCS_REF_HEAD_NAME = "vcs.ref.head.name";
|
|
14
|
+
export const VCS_REF_HEAD_REVISION = "vcs.ref.head.revision";
|
|
15
|
+
export const VCS_CHANGE_ID = "vcs.change.id";
|
|
8
16
|
// Misc Signal Attribute Keys
|
|
9
17
|
export const EVENT_NAME = "event.name";
|
|
10
18
|
export const WEB_EVENT_TITLE = "dash0.web.event.title";
|
|
@@ -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;
|