@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dash0/sdk-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Dash0's Web SDK to collect telemetry from end-users' web browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/dash0.umd.cjs",
|
|
@@ -68,11 +68,9 @@
|
|
|
68
68
|
"url": "https://github.com/dash0hq/dash0-sdk-web/issues"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"web-vitals": "^5.0.3"
|
|
72
|
-
"ts-deepmerge": "^7.0.3"
|
|
71
|
+
"web-vitals": "^5.0.3"
|
|
73
72
|
},
|
|
74
73
|
"devDependencies": {
|
|
75
|
-
"dotenv-cli": "^8.0.0",
|
|
76
74
|
"@babel/core": "^7.26.10",
|
|
77
75
|
"@babel/preset-env": "^7.26.9",
|
|
78
76
|
"@release-it/conventional-changelog": "^10.0.1",
|
|
@@ -94,8 +92,8 @@
|
|
|
94
92
|
"@wdio/mocha-framework": "^9.12.5",
|
|
95
93
|
"@wdio/sauce-service": "^9.12.5",
|
|
96
94
|
"@wdio/spec-reporter": "^9.12.3",
|
|
97
|
-
"wdio-lambdatest-service": "^4.0.0",
|
|
98
95
|
"body-parser": "^2.2.0",
|
|
96
|
+
"dotenv-cli": "^8.0.0",
|
|
99
97
|
"eslint": "^9.24.0",
|
|
100
98
|
"express": "^5.1.0",
|
|
101
99
|
"google-closure-compiler": "^20240317.0.0",
|
|
@@ -112,6 +110,7 @@
|
|
|
112
110
|
"typescript": "^5.8.3",
|
|
113
111
|
"uuid": "^11.1.0",
|
|
114
112
|
"vitest": "^3.1.1",
|
|
113
|
+
"wdio-lambdatest-service": "^4.0.0",
|
|
115
114
|
"webdriverio": "^9.12.5"
|
|
116
115
|
},
|
|
117
116
|
"resolutions": {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Shared types and helpers for reading build-environment variables exposed
|
|
2
|
+
// to the browser bundle. Owned by this module because it is used from both
|
|
3
|
+
// init.ts (environment / deployment detection) and vcs.ts (VCS detection).
|
|
4
|
+
//
|
|
5
|
+
// IMPORTANT: each `process.env.NAME` MUST be a LITERAL accessor in the
|
|
6
|
+
// caller's source. Webpack DefinePlugin, Next.js, Gatsby, and equivalents
|
|
7
|
+
// substitute env vars at build time only when they see the literal form.
|
|
8
|
+
// Dynamic lookups (`process.env[name]`, iterating a string array) are NOT
|
|
9
|
+
// substituted — they resolve to `undefined` in the browser bundle.
|
|
10
|
+
//
|
|
11
|
+
// The type unions below are the single source of truth for which env var
|
|
12
|
+
// names the SDK reads. They are composed via template-literal types so
|
|
13
|
+
// every (prefix × suffix) combination is enumerated at the type level.
|
|
14
|
+
// Adding a framework prefix is a one-line edit to `FrameworkPrefix`; adding
|
|
15
|
+
// a suffix is a one-line edit to the appropriate suffix union.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Framework prefixes the SDK reads as literal `process.env.{PREFIX}{SUFFIX}`
|
|
19
|
+
* accessors. Vercel auto-prefixes its `VERCEL_*` system vars under each
|
|
20
|
+
* (per https://vercel.com/docs/environment-variables/framework-environment-variables).
|
|
21
|
+
* Users on platforms without auto-prefixing (Netlify, Cloudflare Pages,
|
|
22
|
+
* custom CI) can expose env vars under any of these prefixes to get the
|
|
23
|
+
* same detection.
|
|
24
|
+
*
|
|
25
|
+
* Caveat: a prefix being listed here means the SDK *will read* a literal
|
|
26
|
+
* `process.env.{PREFIX}_X` accessor — it does not guarantee the consumer's
|
|
27
|
+
* bundler will substitute that accessor. Webpack-based bundlers (Next.js,
|
|
28
|
+
* Gatsby, CRA) substitute `process.env.X` literals by default. Vite reads
|
|
29
|
+
* env vars via `import.meta.env.VITE_*` by default; for `process.env.VITE_*`
|
|
30
|
+
* to be substituted in a Vite build the consumer must add
|
|
31
|
+
* `define: { 'process.env.VITE_X': JSON.stringify(...) }` to their Vite
|
|
32
|
+
* config, or use a `process.env` polyfill plugin. When deploying to Vercel
|
|
33
|
+
* the `VITE_VERCEL_*` substitution happens inside the build environment, so
|
|
34
|
+
* Vite + Vercel works out of the box; Vite users on other platforms need to
|
|
35
|
+
* surface env vars via their own bundler config.
|
|
36
|
+
*
|
|
37
|
+
* Keep `FRAMEWORK_PREFIX_SAMPLES` in `init_test.ts` in sync when adding a
|
|
38
|
+
* prefix here.
|
|
39
|
+
*/
|
|
40
|
+
export type FrameworkPrefix =
|
|
41
|
+
| "NEXT_PUBLIC_"
|
|
42
|
+
| "NUXT_PUBLIC_"
|
|
43
|
+
| "NUXT_ENV_"
|
|
44
|
+
| "REACT_APP_"
|
|
45
|
+
| "GATSBY_"
|
|
46
|
+
| "VITE_"
|
|
47
|
+
| "PUBLIC_"
|
|
48
|
+
| "VUE_APP_"
|
|
49
|
+
| "REDWOOD_ENV_"
|
|
50
|
+
| "SANITY_STUDIO_";
|
|
51
|
+
|
|
52
|
+
/** Vercel system env vars consumed by detectEnvironment / detectDeploymentName / detectDeploymentId. */
|
|
53
|
+
type VercelDeploymentSuffix = "VERCEL_ENV" | "VERCEL_TARGET_ENV" | "VERCEL_BRANCH_URL";
|
|
54
|
+
|
|
55
|
+
/** Vercel git env vars consumed by VCS detection. */
|
|
56
|
+
type VercelGitSuffix =
|
|
57
|
+
| "VERCEL_GIT_PROVIDER"
|
|
58
|
+
| "VERCEL_GIT_REPO_OWNER"
|
|
59
|
+
| "VERCEL_GIT_REPO_SLUG"
|
|
60
|
+
| "VERCEL_GIT_COMMIT_REF"
|
|
61
|
+
| "VERCEL_GIT_COMMIT_SHA"
|
|
62
|
+
| "VERCEL_GIT_PULL_REQUEST_ID";
|
|
63
|
+
|
|
64
|
+
/** Netlify build env vars (Netlify does not auto-prefix; users surface these via their framework convention). */
|
|
65
|
+
type NetlifyGitSuffix = "REPOSITORY_URL" | "BRANCH" | "COMMIT_REF" | "REVIEW_ID";
|
|
66
|
+
|
|
67
|
+
/** Every env var name the SDK reads as a literal `process.env.X` accessor. */
|
|
68
|
+
type BrowserBuildEnvKey = `${FrameworkPrefix}${VercelDeploymentSuffix | VercelGitSuffix | NetlifyGitSuffix}`;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Module-locally typed shape of `process.env`. Consumers re-declare `process`
|
|
72
|
+
* with this type so dot-notation accessors are typed (no `@ts-expect-error`)
|
|
73
|
+
* and `noPropertyAccessFromIndexSignature` does not fire (each key is a
|
|
74
|
+
* known union member, not an index signature).
|
|
75
|
+
*/
|
|
76
|
+
export type BrowserBuildEnv = { readonly [K in BrowserBuildEnvKey]?: string };
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return the first truthy value, or undefined. Used to walk the list of
|
|
80
|
+
* framework-prefixed variants of an env var and pick whichever the user's
|
|
81
|
+
* bundler substituted at build time.
|
|
82
|
+
*
|
|
83
|
+
* The falsy check skips empty strings as well as undefined — bundlers that
|
|
84
|
+
* do not substitute a literal leave it as `undefined`, not `""`, so empty
|
|
85
|
+
* is treated as "not set". This is intentional and matches every known
|
|
86
|
+
* `VERCEL_GIT_*` / `REPOSITORY_URL` / etc. value shape (non-empty strings
|
|
87
|
+
* or absent; Vercel PR IDs are positive integer strings, never `"0"`).
|
|
88
|
+
*/
|
|
89
|
+
export function pickFirstString(...values: (string | undefined)[]): string | undefined {
|
|
90
|
+
for (const value of values) {
|
|
91
|
+
if (value) return value;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
package/src/api/init.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
fetch,
|
|
14
14
|
generateUniqueId,
|
|
15
|
+
isSessionSampledIn,
|
|
15
16
|
isSafeServiceName,
|
|
16
17
|
PAGE_LOAD_ID_BYTES,
|
|
17
18
|
warn,
|
|
@@ -23,17 +24,19 @@ import {
|
|
|
23
24
|
pick,
|
|
24
25
|
loc,
|
|
25
26
|
} from "../utils";
|
|
26
|
-
import { trackSessions } from "./session";
|
|
27
|
+
import { sessionId, trackSessions } from "./session";
|
|
27
28
|
import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
|
|
28
29
|
import { startErrorInstrumentation } from "../instrumentations/errors";
|
|
29
30
|
import { addAttribute } from "../utils/otel";
|
|
30
31
|
import { instrumentFetch } from "../instrumentations/http/fetch";
|
|
31
32
|
import { startNavigationInstrumentation } from "../instrumentations/navigation";
|
|
32
|
-
import { merge } from "ts-deepmerge";
|
|
33
33
|
import { initializeTabId } from "../utils/tab-id";
|
|
34
34
|
import { InitOptions, InstrumentationName } from "../types/options";
|
|
35
|
+
import { BrowserBuildEnv, pickFirstString } from "./browser-env";
|
|
35
36
|
import { applyVcsResourceAttributes } from "./vcs";
|
|
36
37
|
|
|
38
|
+
declare const process: { env?: BrowserBuildEnv } | undefined;
|
|
39
|
+
|
|
37
40
|
let hasBeenInitialised: boolean = false;
|
|
38
41
|
|
|
39
42
|
export function init(opts: InitOptions) {
|
|
@@ -94,6 +97,17 @@ export function init(opts: InitOptions) {
|
|
|
94
97
|
initializeTabId();
|
|
95
98
|
trackSessions(opts.sessionInactivityTimeoutMillis, opts.sessionTerminationTimeoutMillis);
|
|
96
99
|
|
|
100
|
+
if (opts.sessionSamplingRate != null) {
|
|
101
|
+
const rate = Math.max(0, Math.min(100, opts.sessionSamplingRate));
|
|
102
|
+
vars.isSessionSampled = sessionId != null ? isSessionSampledIn(sessionId, rate) : rate > 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!vars.isSessionSampled) {
|
|
106
|
+
debug("Session is not sampled. No telemetry will be transmitted for this session.");
|
|
107
|
+
hasBeenInitialised = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
if (isInstrumentationEnabled("@dash0/navigation", opts)) {
|
|
98
112
|
startNavigationInstrumentation();
|
|
99
113
|
}
|
|
@@ -158,18 +172,30 @@ function isClient() {
|
|
|
158
172
|
return win != null;
|
|
159
173
|
}
|
|
160
174
|
|
|
175
|
+
// Vercel auto-prefixes its system env vars under every framework preset
|
|
176
|
+
// (see https://vercel.com/docs/environment-variables/framework-environment-variables).
|
|
177
|
+
// The shared `FrameworkPrefix` union in `./browser-env` is the single source
|
|
178
|
+
// of truth for which prefixes the SDK recognises. To add a new prefix, edit
|
|
179
|
+
// it there; the literal accessors below pick it up automatically via the
|
|
180
|
+
// typed `process.env` declaration.
|
|
181
|
+
|
|
161
182
|
function detectEnvironment(opts: InitOptions): string | undefined {
|
|
162
|
-
// if there is a manually specified value we use that
|
|
163
183
|
if (opts.environment) {
|
|
164
184
|
return opts.environment;
|
|
165
185
|
}
|
|
166
|
-
|
|
167
|
-
// if process isn't defined access to it causes an exception, but we can't check for its present due to how
|
|
168
|
-
// plugins like webpack define work.
|
|
169
186
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
return pickFirstString(
|
|
188
|
+
process?.env?.NEXT_PUBLIC_VERCEL_ENV,
|
|
189
|
+
process?.env?.NUXT_PUBLIC_VERCEL_ENV,
|
|
190
|
+
process?.env?.NUXT_ENV_VERCEL_ENV,
|
|
191
|
+
process?.env?.REACT_APP_VERCEL_ENV,
|
|
192
|
+
process?.env?.GATSBY_VERCEL_ENV,
|
|
193
|
+
process?.env?.VITE_VERCEL_ENV,
|
|
194
|
+
process?.env?.PUBLIC_VERCEL_ENV,
|
|
195
|
+
process?.env?.VUE_APP_VERCEL_ENV,
|
|
196
|
+
process?.env?.REDWOOD_ENV_VERCEL_ENV,
|
|
197
|
+
process?.env?.SANITY_STUDIO_VERCEL_ENV
|
|
198
|
+
);
|
|
173
199
|
} catch (_ignored) {
|
|
174
200
|
return undefined;
|
|
175
201
|
}
|
|
@@ -179,13 +205,19 @@ function detectDeploymentName(opts: InitOptions): string | undefined {
|
|
|
179
205
|
if (opts.deploymentName) {
|
|
180
206
|
return opts.deploymentName;
|
|
181
207
|
}
|
|
182
|
-
|
|
183
|
-
// if process isn't defined access to it causes an exception, but we can't check for its present due to how
|
|
184
|
-
// plugins like webpack define work.
|
|
185
208
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
return pickFirstString(
|
|
210
|
+
process?.env?.NEXT_PUBLIC_VERCEL_TARGET_ENV,
|
|
211
|
+
process?.env?.NUXT_PUBLIC_VERCEL_TARGET_ENV,
|
|
212
|
+
process?.env?.NUXT_ENV_VERCEL_TARGET_ENV,
|
|
213
|
+
process?.env?.REACT_APP_VERCEL_TARGET_ENV,
|
|
214
|
+
process?.env?.GATSBY_VERCEL_TARGET_ENV,
|
|
215
|
+
process?.env?.VITE_VERCEL_TARGET_ENV,
|
|
216
|
+
process?.env?.PUBLIC_VERCEL_TARGET_ENV,
|
|
217
|
+
process?.env?.VUE_APP_VERCEL_TARGET_ENV,
|
|
218
|
+
process?.env?.REDWOOD_ENV_VERCEL_TARGET_ENV,
|
|
219
|
+
process?.env?.SANITY_STUDIO_VERCEL_TARGET_ENV
|
|
220
|
+
);
|
|
189
221
|
} catch (_ignored) {
|
|
190
222
|
return undefined;
|
|
191
223
|
}
|
|
@@ -195,13 +227,19 @@ function detectDeploymentId(opts: InitOptions): string | undefined {
|
|
|
195
227
|
if (opts.deploymentId) {
|
|
196
228
|
return opts.deploymentId;
|
|
197
229
|
}
|
|
198
|
-
|
|
199
|
-
// if process isn't defined access to it causes an exception, but we can't check for its present due to how
|
|
200
|
-
// plugins like webpack define work.
|
|
201
230
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
231
|
+
return pickFirstString(
|
|
232
|
+
process?.env?.NEXT_PUBLIC_VERCEL_BRANCH_URL,
|
|
233
|
+
process?.env?.NUXT_PUBLIC_VERCEL_BRANCH_URL,
|
|
234
|
+
process?.env?.NUXT_ENV_VERCEL_BRANCH_URL,
|
|
235
|
+
process?.env?.REACT_APP_VERCEL_BRANCH_URL,
|
|
236
|
+
process?.env?.GATSBY_VERCEL_BRANCH_URL,
|
|
237
|
+
process?.env?.VITE_VERCEL_BRANCH_URL,
|
|
238
|
+
process?.env?.PUBLIC_VERCEL_BRANCH_URL,
|
|
239
|
+
process?.env?.VUE_APP_VERCEL_BRANCH_URL,
|
|
240
|
+
process?.env?.REDWOOD_ENV_VERCEL_BRANCH_URL,
|
|
241
|
+
process?.env?.SANITY_STUDIO_VERCEL_BRANCH_URL
|
|
242
|
+
);
|
|
205
243
|
} catch (_ignored) {
|
|
206
244
|
return undefined;
|
|
207
245
|
}
|
|
@@ -246,3 +284,26 @@ function isInstrumentationEnabled(name: InstrumentationName, opts: InitOptions):
|
|
|
246
284
|
|
|
247
285
|
return instrumentations.includes(name);
|
|
248
286
|
}
|
|
287
|
+
|
|
288
|
+
function merge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
|
|
289
|
+
const result = { ...target };
|
|
290
|
+
for (const key of Object.keys(source) as Array<keyof T>) {
|
|
291
|
+
const srcVal = source[key];
|
|
292
|
+
const dstVal = target[key];
|
|
293
|
+
if (srcVal !== undefined) {
|
|
294
|
+
if (
|
|
295
|
+
srcVal !== null &&
|
|
296
|
+
typeof srcVal === "object" &&
|
|
297
|
+
!Array.isArray(srcVal) &&
|
|
298
|
+
typeof dstVal === "object" &&
|
|
299
|
+
dstVal !== null &&
|
|
300
|
+
!Array.isArray(dstVal)
|
|
301
|
+
) {
|
|
302
|
+
result[key] = { ...dstVal, ...srcVal } as T[keyof T];
|
|
303
|
+
} else {
|
|
304
|
+
result[key] = srcVal as T[keyof T];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
package/src/api/init_test.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
import { InitOptions, InstrumentationName } from "../types/options";
|
|
3
3
|
import { PropagatorConfig, Vars } from "../vars";
|
|
4
4
|
import {
|
|
5
|
+
DEPLOYMENT_ENVIRONMENT_NAME,
|
|
6
|
+
DEPLOYMENT_ID,
|
|
7
|
+
DEPLOYMENT_NAME,
|
|
5
8
|
SERVICE_NAME,
|
|
6
9
|
SERVICE_NAMESPACE,
|
|
7
10
|
VCS_CHANGE_ID,
|
|
@@ -440,7 +443,8 @@ describe("init", () => {
|
|
|
440
443
|
// detectVcsFromVercel is actually wired up.
|
|
441
444
|
const FRAMEWORK_PREFIX_SAMPLES: Array<[label: string, prefix: string]> = [
|
|
442
445
|
["Next.js", "NEXT_PUBLIC_"],
|
|
443
|
-
["Nuxt", "
|
|
446
|
+
["Nuxt 3", "NUXT_PUBLIC_"],
|
|
447
|
+
["Nuxt 2", "NUXT_ENV_"],
|
|
444
448
|
["Create React App", "REACT_APP_"],
|
|
445
449
|
["Gatsby", "GATSBY_"],
|
|
446
450
|
["Vite", "VITE_"],
|
|
@@ -684,4 +688,69 @@ describe("init", () => {
|
|
|
684
688
|
expect(stringAttr(VCS_REPOSITORY_NAME)).toBeUndefined();
|
|
685
689
|
});
|
|
686
690
|
});
|
|
691
|
+
|
|
692
|
+
describe("environment + deployment auto-detection", () => {
|
|
693
|
+
afterEach(() => {
|
|
694
|
+
vi.unstubAllEnvs();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const stringAttr = (key: string) => vars.resource.attributes.find((attr) => attr.key === key)?.value.stringValue;
|
|
698
|
+
|
|
699
|
+
// The 9 framework prefixes Vercel auto-prefixes its `VERCEL_*` system
|
|
700
|
+
// vars under (per https://vercel.com/docs/environment-variables/framework-environment-variables).
|
|
701
|
+
// Same matrix the VCS detection uses, sourced from the shared
|
|
702
|
+
// `FrameworkPrefix` union in `./browser-env`.
|
|
703
|
+
const FRAMEWORK_PREFIX_SAMPLES: Array<[label: string, prefix: string]> = [
|
|
704
|
+
["Next.js", "NEXT_PUBLIC_"],
|
|
705
|
+
["Nuxt 3", "NUXT_PUBLIC_"],
|
|
706
|
+
["Nuxt 2", "NUXT_ENV_"],
|
|
707
|
+
["Create React App", "REACT_APP_"],
|
|
708
|
+
["Gatsby", "GATSBY_"],
|
|
709
|
+
["Vite", "VITE_"],
|
|
710
|
+
["Astro / SvelteKit / Hydrogen", "PUBLIC_"],
|
|
711
|
+
["Vue CLI", "VUE_APP_"],
|
|
712
|
+
["RedwoodJS", "REDWOOD_ENV_"],
|
|
713
|
+
["Sanity Studio", "SANITY_STUDIO_"],
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
it.each(FRAMEWORK_PREFIX_SAMPLES)(
|
|
717
|
+
"derives environment + deployment resource attributes from %s framework prefix (%s)",
|
|
718
|
+
(_label, prefix) => {
|
|
719
|
+
vi.stubEnv(`${prefix}VERCEL_ENV`, "production");
|
|
720
|
+
vi.stubEnv(`${prefix}VERCEL_TARGET_ENV`, "production");
|
|
721
|
+
vi.stubEnv(`${prefix}VERCEL_BRANCH_URL`, "my-site-git-main.vercel.app");
|
|
722
|
+
|
|
723
|
+
init(baseOptions);
|
|
724
|
+
|
|
725
|
+
expect(stringAttr(DEPLOYMENT_ENVIRONMENT_NAME)).toBe("production");
|
|
726
|
+
expect(stringAttr(DEPLOYMENT_NAME)).toBe("production");
|
|
727
|
+
expect(stringAttr(DEPLOYMENT_ID)).toBe("my-site-git-main.vercel.app");
|
|
728
|
+
}
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
it("opts.environment / opts.deploymentName / opts.deploymentId override env-var detection", () => {
|
|
732
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_ENV", "from-env");
|
|
733
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_TARGET_ENV", "from-env");
|
|
734
|
+
vi.stubEnv("NEXT_PUBLIC_VERCEL_BRANCH_URL", "from-env");
|
|
735
|
+
|
|
736
|
+
init({
|
|
737
|
+
...baseOptions,
|
|
738
|
+
environment: "manual-environment",
|
|
739
|
+
deploymentName: "manual-deployment-name",
|
|
740
|
+
deploymentId: "manual-deployment-id",
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
expect(stringAttr(DEPLOYMENT_ENVIRONMENT_NAME)).toBe("manual-environment");
|
|
744
|
+
expect(stringAttr(DEPLOYMENT_NAME)).toBe("manual-deployment-name");
|
|
745
|
+
expect(stringAttr(DEPLOYMENT_ID)).toBe("manual-deployment-id");
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("emits no environment/deployment attributes when neither env vars nor opts are set", () => {
|
|
749
|
+
init(baseOptions);
|
|
750
|
+
|
|
751
|
+
expect(stringAttr(DEPLOYMENT_ENVIRONMENT_NAME)).toBeUndefined();
|
|
752
|
+
expect(stringAttr(DEPLOYMENT_NAME)).toBeUndefined();
|
|
753
|
+
expect(stringAttr(DEPLOYMENT_ID)).toBeUndefined();
|
|
754
|
+
});
|
|
755
|
+
});
|
|
687
756
|
});
|
package/src/api/vcs.ts
CHANGED
|
@@ -10,41 +10,7 @@ import {
|
|
|
10
10
|
} from "../semantic-conventions";
|
|
11
11
|
import { InitOptions, VcsAttributes } from "../types/options";
|
|
12
12
|
import { addAttribute } from "../utils/otel";
|
|
13
|
-
|
|
14
|
-
// Module-local typing for the `process` global. The SDK does not depend on
|
|
15
|
-
// @types/node, and bundler substitution requires literal `process.env.NAME`
|
|
16
|
-
// accessors (dot notation, not bracket lookup). We enumerate the exact set of
|
|
17
|
-
// env var names the readers below access via template-literal types — typos
|
|
18
|
-
// are caught at compile time, and `noPropertyAccessFromIndexSignature` does
|
|
19
|
-
// not fire because each key is a known union member, not an index signature.
|
|
20
|
-
//
|
|
21
|
-
// Adding a new framework prefix here automatically permits every combination
|
|
22
|
-
// with every known suffix. Adding a vendor suffix permits every combination
|
|
23
|
-
// with every known prefix.
|
|
24
|
-
type FrameworkPrefix =
|
|
25
|
-
| "NEXT_PUBLIC_"
|
|
26
|
-
| "NUXT_ENV_"
|
|
27
|
-
| "REACT_APP_"
|
|
28
|
-
| "GATSBY_"
|
|
29
|
-
| "VITE_"
|
|
30
|
-
| "PUBLIC_"
|
|
31
|
-
| "VUE_APP_"
|
|
32
|
-
| "REDWOOD_ENV_"
|
|
33
|
-
| "SANITY_STUDIO_";
|
|
34
|
-
|
|
35
|
-
type VercelGitSuffix =
|
|
36
|
-
| "VERCEL_GIT_PROVIDER"
|
|
37
|
-
| "VERCEL_GIT_REPO_OWNER"
|
|
38
|
-
| "VERCEL_GIT_REPO_SLUG"
|
|
39
|
-
| "VERCEL_GIT_COMMIT_REF"
|
|
40
|
-
| "VERCEL_GIT_COMMIT_SHA"
|
|
41
|
-
| "VERCEL_GIT_PULL_REQUEST_ID";
|
|
42
|
-
|
|
43
|
-
type NetlifyGitSuffix = "REPOSITORY_URL" | "BRANCH" | "COMMIT_REF" | "REVIEW_ID";
|
|
44
|
-
|
|
45
|
-
type BrowserBuildEnvKey = `${FrameworkPrefix}${VercelGitSuffix | NetlifyGitSuffix}`;
|
|
46
|
-
|
|
47
|
-
type BrowserBuildEnv = { readonly [K in BrowserBuildEnvKey]?: string };
|
|
13
|
+
import { BrowserBuildEnv, pickFirstString } from "./browser-env";
|
|
48
14
|
|
|
49
15
|
declare const process: { env?: BrowserBuildEnv } | undefined;
|
|
50
16
|
|
|
@@ -134,6 +100,7 @@ function detectVcsFromVercel(): VcsAttributes {
|
|
|
134
100
|
try {
|
|
135
101
|
provider = pickFirstString(
|
|
136
102
|
process?.env?.NEXT_PUBLIC_VERCEL_GIT_PROVIDER,
|
|
103
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_PROVIDER,
|
|
137
104
|
process?.env?.NUXT_ENV_VERCEL_GIT_PROVIDER,
|
|
138
105
|
process?.env?.REACT_APP_VERCEL_GIT_PROVIDER,
|
|
139
106
|
process?.env?.GATSBY_VERCEL_GIT_PROVIDER,
|
|
@@ -145,6 +112,7 @@ function detectVcsFromVercel(): VcsAttributes {
|
|
|
145
112
|
);
|
|
146
113
|
owner = pickFirstString(
|
|
147
114
|
process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER,
|
|
115
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_REPO_OWNER,
|
|
148
116
|
process?.env?.NUXT_ENV_VERCEL_GIT_REPO_OWNER,
|
|
149
117
|
process?.env?.REACT_APP_VERCEL_GIT_REPO_OWNER,
|
|
150
118
|
process?.env?.GATSBY_VERCEL_GIT_REPO_OWNER,
|
|
@@ -156,6 +124,7 @@ function detectVcsFromVercel(): VcsAttributes {
|
|
|
156
124
|
);
|
|
157
125
|
repo = pickFirstString(
|
|
158
126
|
process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG,
|
|
127
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_REPO_SLUG,
|
|
159
128
|
process?.env?.NUXT_ENV_VERCEL_GIT_REPO_SLUG,
|
|
160
129
|
process?.env?.REACT_APP_VERCEL_GIT_REPO_SLUG,
|
|
161
130
|
process?.env?.GATSBY_VERCEL_GIT_REPO_SLUG,
|
|
@@ -167,6 +136,7 @@ function detectVcsFromVercel(): VcsAttributes {
|
|
|
167
136
|
);
|
|
168
137
|
ref = pickFirstString(
|
|
169
138
|
process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF,
|
|
139
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_COMMIT_REF,
|
|
170
140
|
process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_REF,
|
|
171
141
|
process?.env?.REACT_APP_VERCEL_GIT_COMMIT_REF,
|
|
172
142
|
process?.env?.GATSBY_VERCEL_GIT_COMMIT_REF,
|
|
@@ -178,6 +148,7 @@ function detectVcsFromVercel(): VcsAttributes {
|
|
|
178
148
|
);
|
|
179
149
|
revision = pickFirstString(
|
|
180
150
|
process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
|
|
151
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
|
|
181
152
|
process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_SHA,
|
|
182
153
|
process?.env?.REACT_APP_VERCEL_GIT_COMMIT_SHA,
|
|
183
154
|
process?.env?.GATSBY_VERCEL_GIT_COMMIT_SHA,
|
|
@@ -189,6 +160,7 @@ function detectVcsFromVercel(): VcsAttributes {
|
|
|
189
160
|
);
|
|
190
161
|
changeId = pickFirstString(
|
|
191
162
|
process?.env?.NEXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID,
|
|
163
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID,
|
|
192
164
|
process?.env?.NUXT_ENV_VERCEL_GIT_PULL_REQUEST_ID,
|
|
193
165
|
process?.env?.REACT_APP_VERCEL_GIT_PULL_REQUEST_ID,
|
|
194
166
|
process?.env?.GATSBY_VERCEL_GIT_PULL_REQUEST_ID,
|
|
@@ -227,6 +199,7 @@ function detectVcsFromNetlify(): VcsAttributes {
|
|
|
227
199
|
try {
|
|
228
200
|
repositoryUrl = pickFirstString(
|
|
229
201
|
process?.env?.NEXT_PUBLIC_REPOSITORY_URL,
|
|
202
|
+
process?.env?.NUXT_PUBLIC_REPOSITORY_URL,
|
|
230
203
|
process?.env?.NUXT_ENV_REPOSITORY_URL,
|
|
231
204
|
process?.env?.REACT_APP_REPOSITORY_URL,
|
|
232
205
|
process?.env?.GATSBY_REPOSITORY_URL,
|
|
@@ -238,6 +211,7 @@ function detectVcsFromNetlify(): VcsAttributes {
|
|
|
238
211
|
);
|
|
239
212
|
branch = pickFirstString(
|
|
240
213
|
process?.env?.NEXT_PUBLIC_BRANCH,
|
|
214
|
+
process?.env?.NUXT_PUBLIC_BRANCH,
|
|
241
215
|
process?.env?.NUXT_ENV_BRANCH,
|
|
242
216
|
process?.env?.REACT_APP_BRANCH,
|
|
243
217
|
process?.env?.GATSBY_BRANCH,
|
|
@@ -249,6 +223,7 @@ function detectVcsFromNetlify(): VcsAttributes {
|
|
|
249
223
|
);
|
|
250
224
|
commit = pickFirstString(
|
|
251
225
|
process?.env?.NEXT_PUBLIC_COMMIT_REF,
|
|
226
|
+
process?.env?.NUXT_PUBLIC_COMMIT_REF,
|
|
252
227
|
process?.env?.NUXT_ENV_COMMIT_REF,
|
|
253
228
|
process?.env?.REACT_APP_COMMIT_REF,
|
|
254
229
|
process?.env?.GATSBY_COMMIT_REF,
|
|
@@ -260,6 +235,7 @@ function detectVcsFromNetlify(): VcsAttributes {
|
|
|
260
235
|
);
|
|
261
236
|
reviewId = pickFirstString(
|
|
262
237
|
process?.env?.NEXT_PUBLIC_REVIEW_ID,
|
|
238
|
+
process?.env?.NUXT_PUBLIC_REVIEW_ID,
|
|
263
239
|
process?.env?.NUXT_ENV_REVIEW_ID,
|
|
264
240
|
process?.env?.REACT_APP_REVIEW_ID,
|
|
265
241
|
process?.env?.GATSBY_REVIEW_ID,
|
|
@@ -285,13 +261,6 @@ function detectVcsFromNetlify(): VcsAttributes {
|
|
|
285
261
|
};
|
|
286
262
|
}
|
|
287
263
|
|
|
288
|
-
function pickFirstString(...values: (string | undefined)[]): string | undefined {
|
|
289
|
-
for (const value of values) {
|
|
290
|
-
if (value) return value;
|
|
291
|
-
}
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
264
|
function buildRepositoryUrlFromVercel(
|
|
296
265
|
provider: string | undefined,
|
|
297
266
|
owner: string | undefined,
|
package/src/transport/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ function isRateLimited() {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function sendLog(log: LogRecord): void {
|
|
24
|
+
if (!vars.isSessionSampled) return;
|
|
25
|
+
|
|
24
26
|
if (isRateLimited()) {
|
|
25
27
|
debug("Transport rate limit. Will not send item.", log);
|
|
26
28
|
return;
|
|
@@ -49,6 +51,7 @@ function sendLogs(logs: LogRecord[]): void {
|
|
|
49
51
|
|
|
50
52
|
export function sendSpan(span: Span | undefined): void {
|
|
51
53
|
if (!span) return;
|
|
54
|
+
if (!vars.isSessionSampled) return;
|
|
52
55
|
|
|
53
56
|
if (isRateLimited()) {
|
|
54
57
|
debug("Transport rate limit. Will not send item.", span);
|
package/src/types/options.ts
CHANGED
|
@@ -54,9 +54,9 @@ export type InitOptions = {
|
|
|
54
54
|
/**
|
|
55
55
|
* When `true`, disable auto-detection of VCS (version control) context
|
|
56
56
|
* from the build environment. By default the SDK reads VCS context from
|
|
57
|
-
* Vercel (
|
|
58
|
-
* (
|
|
59
|
-
*
|
|
57
|
+
* Vercel (`<FRAMEWORK_PREFIX>VERCEL_GIT_*`) and Netlify
|
|
58
|
+
* (`<FRAMEWORK_PREFIX>REPOSITORY_URL`, `<FRAMEWORK_PREFIX>BRANCH`,
|
|
59
|
+
* `<FRAMEWORK_PREFIX>COMMIT_REF`, `<FRAMEWORK_PREFIX>REVIEW_ID`) and applies the values
|
|
60
60
|
* as resource attributes following the OTel `vcs.*` semantic conventions:
|
|
61
61
|
*
|
|
62
62
|
* - vcs.provider.name
|
|
@@ -94,6 +94,16 @@ export type InitOptions = {
|
|
|
94
94
|
*/
|
|
95
95
|
enabledInstrumentations?: InstrumentationName[];
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The percentage of sessions for which telemetry data is recorded and transmitted.
|
|
99
|
+
* Must be a number between 0 and 100.
|
|
100
|
+
* - 0: No sessions are recorded/transferred.
|
|
101
|
+
* - 100: All sessions are recorded/transferred (default).
|
|
102
|
+
* - Any other value: That percentage of sessions are recorded/transferred.
|
|
103
|
+
* The sampling decision is deterministic per session ID.
|
|
104
|
+
*/
|
|
105
|
+
sessionSamplingRate?: number;
|
|
106
|
+
|
|
97
107
|
/**
|
|
98
108
|
* The session inactivity timeout. Session inactivity is the maximum
|
|
99
109
|
* allowed time to pass between two page loads before the session is considered
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { crc32 } from "./crc32";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Determines whether a session should be sampled based on the session ID
|
|
5
|
+
* and a configured sampling rate.
|
|
6
|
+
*
|
|
7
|
+
* @param sessionId The hex session ID string
|
|
8
|
+
* @param samplingRate A number between 0 and 100 (inclusive)
|
|
9
|
+
* @returns true if the session should be sampled (data collected), false otherwise
|
|
10
|
+
*/
|
|
11
|
+
export function isSessionSampledIn(sessionId: string, samplingRate: number): boolean {
|
|
12
|
+
if (samplingRate <= 0) return false;
|
|
13
|
+
if (samplingRate >= 100) return true;
|
|
14
|
+
return crc32(sessionId) % 100 < samplingRate;
|
|
15
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isSessionSampledIn } from "./session-sampling";
|
|
3
|
+
import { generateSessionId } from "./session-id";
|
|
4
|
+
|
|
5
|
+
describe("isSessionSampledIn", () => {
|
|
6
|
+
it("returns false when sampling rate is 0", () => {
|
|
7
|
+
expect(isSessionSampledIn("00abcdef01234567", 0)).toBe(false);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns true when sampling rate is 100", () => {
|
|
11
|
+
expect(isSessionSampledIn("00abcdef01234567", 100)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns deterministic results for the same session ID and rate", () => {
|
|
15
|
+
const sessionId = "00abcdef01234567";
|
|
16
|
+
const rate = 50;
|
|
17
|
+
const result1 = isSessionSampledIn(sessionId, rate);
|
|
18
|
+
const result2 = isSessionSampledIn(sessionId, rate);
|
|
19
|
+
expect(result1).toBe(result2);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("produces different results for different session IDs", () => {
|
|
23
|
+
const results = new Set<boolean>();
|
|
24
|
+
// Generate enough IDs to get both true and false with high probability
|
|
25
|
+
for (let i = 0; i < 100; i++) {
|
|
26
|
+
results.add(isSessionSampledIn(generateSessionId(), 50));
|
|
27
|
+
}
|
|
28
|
+
expect(results.size).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("produces roughly correct distribution over many session IDs", () => {
|
|
32
|
+
const rate = 30;
|
|
33
|
+
const total = 10000;
|
|
34
|
+
let sampledIn = 0;
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < total; i++) {
|
|
37
|
+
if (isSessionSampledIn(generateSessionId(), rate)) {
|
|
38
|
+
sampledIn++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const actualRate = sampledIn / total;
|
|
43
|
+
// Allow 5% tolerance
|
|
44
|
+
expect(actualRate).toBeGreaterThan(0.25);
|
|
45
|
+
expect(actualRate).toBeLessThan(0.35);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("handles edge case: rate just above 0", () => {
|
|
49
|
+
// With rate=1, about 1% of sessions should be sampled in
|
|
50
|
+
let sampledIn = 0;
|
|
51
|
+
const total = 10000;
|
|
52
|
+
for (let i = 0; i < total; i++) {
|
|
53
|
+
if (isSessionSampledIn(generateSessionId(), 1)) {
|
|
54
|
+
sampledIn++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const actualRate = sampledIn / total;
|
|
58
|
+
expect(actualRate).toBeGreaterThan(0.0);
|
|
59
|
+
expect(actualRate).toBeLessThan(0.05);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("handles edge case: rate just below 100", () => {
|
|
63
|
+
// With rate=99, about 99% of sessions should be sampled in
|
|
64
|
+
let sampledIn = 0;
|
|
65
|
+
const total = 10000;
|
|
66
|
+
for (let i = 0; i < total; i++) {
|
|
67
|
+
if (isSessionSampledIn(generateSessionId(), 99)) {
|
|
68
|
+
sampledIn++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const actualRate = sampledIn / total;
|
|
72
|
+
expect(actualRate).toBeGreaterThan(0.95);
|
|
73
|
+
expect(actualRate).toBeLessThan(1.0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns false for negative sampling rates", () => {
|
|
77
|
+
expect(isSessionSampledIn("00abcdef01234567", -10)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns true for sampling rates above 100", () => {
|
|
81
|
+
expect(isSessionSampledIn("00abcdef01234567", 150)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|