@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
package/src/api/vcs.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { vars } from "../vars";
|
|
2
|
+
import {
|
|
3
|
+
VCS_CHANGE_ID,
|
|
4
|
+
VCS_OWNER_NAME,
|
|
5
|
+
VCS_PROVIDER_NAME,
|
|
6
|
+
VCS_REF_HEAD_NAME,
|
|
7
|
+
VCS_REF_HEAD_REVISION,
|
|
8
|
+
VCS_REPOSITORY_NAME,
|
|
9
|
+
VCS_REPOSITORY_URL_FULL,
|
|
10
|
+
} from "../semantic-conventions";
|
|
11
|
+
import { InitOptions, VcsAttributes } from "../types/options";
|
|
12
|
+
import { addAttribute } from "../utils/otel";
|
|
13
|
+
import { BrowserBuildEnv, pickFirstString } from "./browser-env";
|
|
14
|
+
|
|
15
|
+
declare const process: { env?: BrowserBuildEnv } | undefined;
|
|
16
|
+
|
|
17
|
+
// Single source of truth mapping each VcsAttributes field to its OTel
|
|
18
|
+
// resource attribute name. Typed as `Record<keyof VcsAttributes, string>` so
|
|
19
|
+
// adding a field to VcsAttributes without adding an entry here is a compile
|
|
20
|
+
// error — no silent "I forgot to apply that field" bugs.
|
|
21
|
+
const VCS_FIELD_TO_ATTRIBUTE: Record<keyof VcsAttributes, string> = {
|
|
22
|
+
providerName: VCS_PROVIDER_NAME,
|
|
23
|
+
ownerName: VCS_OWNER_NAME,
|
|
24
|
+
repositoryName: VCS_REPOSITORY_NAME,
|
|
25
|
+
repositoryUrlFull: VCS_REPOSITORY_URL_FULL,
|
|
26
|
+
refHeadName: VCS_REF_HEAD_NAME,
|
|
27
|
+
refHeadRevision: VCS_REF_HEAD_REVISION,
|
|
28
|
+
changeId: VCS_CHANGE_ID,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Derive VCS (version control) context from the build environment and apply
|
|
33
|
+
* the resulting `vcs.*` resource attributes to `vars.resource.attributes`.
|
|
34
|
+
*
|
|
35
|
+
* Detection precedence per attribute: `opts.vcs` → Vercel env vars → Netlify
|
|
36
|
+
* env vars. Manual overrides via `opts.vcs` always win, even when
|
|
37
|
+
* `opts.disableVcsDetection` is set.
|
|
38
|
+
*/
|
|
39
|
+
export function applyVcsResourceAttributes(opts: InitOptions) {
|
|
40
|
+
const vcs = detectVcs(opts);
|
|
41
|
+
for (const field of Object.keys(VCS_FIELD_TO_ATTRIBUTE) as (keyof VcsAttributes)[]) {
|
|
42
|
+
const value = vcs[field];
|
|
43
|
+
if (value) {
|
|
44
|
+
addAttribute(vars.resource.attributes, VCS_FIELD_TO_ATTRIBUTE[field], value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function detectVcs(opts: InitOptions): VcsAttributes {
|
|
50
|
+
// opts.vcs manual overrides always win, even when auto-detection is disabled.
|
|
51
|
+
// This lets callers on non-Vercel/Netlify platforms supply context explicitly
|
|
52
|
+
// while still opting out of any env-var reading.
|
|
53
|
+
const override = opts.vcs ?? {};
|
|
54
|
+
|
|
55
|
+
if (opts.disableVcsDetection) {
|
|
56
|
+
return override;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const vercel = detectVcsFromVercel();
|
|
60
|
+
const netlify = detectVcsFromNetlify();
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
providerName: override.providerName ?? vercel.providerName ?? netlify.providerName,
|
|
64
|
+
ownerName: override.ownerName ?? vercel.ownerName ?? netlify.ownerName,
|
|
65
|
+
repositoryName: override.repositoryName ?? vercel.repositoryName ?? netlify.repositoryName,
|
|
66
|
+
repositoryUrlFull: override.repositoryUrlFull ?? vercel.repositoryUrlFull ?? netlify.repositoryUrlFull,
|
|
67
|
+
refHeadName: override.refHeadName ?? vercel.refHeadName ?? netlify.refHeadName,
|
|
68
|
+
refHeadRevision: override.refHeadRevision ?? vercel.refHeadRevision ?? netlify.refHeadRevision,
|
|
69
|
+
changeId: override.changeId ?? vercel.changeId ?? netlify.changeId,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Frameworks expose env vars to the browser bundle using a per-framework
|
|
74
|
+
// prefix (Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`, etc.). For each known
|
|
75
|
+
// vendor suffix (e.g. Vercel's `VERCEL_GIT_PROVIDER`, Netlify's
|
|
76
|
+
// `REPOSITORY_URL`) we enumerate the prefixed variants below.
|
|
77
|
+
//
|
|
78
|
+
// IMPORTANT: each `process.env.NAME` MUST be a LITERAL accessor. Webpack
|
|
79
|
+
// DefinePlugin, Next.js, Gatsby, and equivalents substitute env vars at
|
|
80
|
+
// build time only when they see the literal form. Dynamic lookups like
|
|
81
|
+
// `process.env[name]` or iterating a string array are NOT substituted —
|
|
82
|
+
// they would resolve to `undefined` in the browser bundle.
|
|
83
|
+
//
|
|
84
|
+
// The 9 prefixes below are the framework presets Vercel auto-prefixes
|
|
85
|
+
// `VERCEL_*` system vars under (https://vercel.com/docs/environment-variables/framework-environment-variables).
|
|
86
|
+
// Users on platforms without auto-prefixing (Netlify, Cloudflare Pages, custom
|
|
87
|
+
// CI) can manually expose any env var under any of these prefixes and get
|
|
88
|
+
// the same auto-detection.
|
|
89
|
+
//
|
|
90
|
+
// Adding a new framework prefix = one new literal accessor per attribute in
|
|
91
|
+
// each `detectVcsFrom*` function below.
|
|
92
|
+
|
|
93
|
+
function detectVcsFromVercel(): VcsAttributes {
|
|
94
|
+
let provider: string | undefined;
|
|
95
|
+
let owner: string | undefined;
|
|
96
|
+
let repo: string | undefined;
|
|
97
|
+
let ref: string | undefined;
|
|
98
|
+
let revision: string | undefined;
|
|
99
|
+
let changeId: string | undefined;
|
|
100
|
+
try {
|
|
101
|
+
provider = pickFirstString(
|
|
102
|
+
process?.env?.NEXT_PUBLIC_VERCEL_GIT_PROVIDER,
|
|
103
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_PROVIDER,
|
|
104
|
+
process?.env?.NUXT_ENV_VERCEL_GIT_PROVIDER,
|
|
105
|
+
process?.env?.REACT_APP_VERCEL_GIT_PROVIDER,
|
|
106
|
+
process?.env?.GATSBY_VERCEL_GIT_PROVIDER,
|
|
107
|
+
process?.env?.VITE_VERCEL_GIT_PROVIDER,
|
|
108
|
+
process?.env?.PUBLIC_VERCEL_GIT_PROVIDER,
|
|
109
|
+
process?.env?.VUE_APP_VERCEL_GIT_PROVIDER,
|
|
110
|
+
process?.env?.REDWOOD_ENV_VERCEL_GIT_PROVIDER,
|
|
111
|
+
process?.env?.SANITY_STUDIO_VERCEL_GIT_PROVIDER
|
|
112
|
+
);
|
|
113
|
+
owner = pickFirstString(
|
|
114
|
+
process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER,
|
|
115
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_REPO_OWNER,
|
|
116
|
+
process?.env?.NUXT_ENV_VERCEL_GIT_REPO_OWNER,
|
|
117
|
+
process?.env?.REACT_APP_VERCEL_GIT_REPO_OWNER,
|
|
118
|
+
process?.env?.GATSBY_VERCEL_GIT_REPO_OWNER,
|
|
119
|
+
process?.env?.VITE_VERCEL_GIT_REPO_OWNER,
|
|
120
|
+
process?.env?.PUBLIC_VERCEL_GIT_REPO_OWNER,
|
|
121
|
+
process?.env?.VUE_APP_VERCEL_GIT_REPO_OWNER,
|
|
122
|
+
process?.env?.REDWOOD_ENV_VERCEL_GIT_REPO_OWNER,
|
|
123
|
+
process?.env?.SANITY_STUDIO_VERCEL_GIT_REPO_OWNER
|
|
124
|
+
);
|
|
125
|
+
repo = pickFirstString(
|
|
126
|
+
process?.env?.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG,
|
|
127
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_REPO_SLUG,
|
|
128
|
+
process?.env?.NUXT_ENV_VERCEL_GIT_REPO_SLUG,
|
|
129
|
+
process?.env?.REACT_APP_VERCEL_GIT_REPO_SLUG,
|
|
130
|
+
process?.env?.GATSBY_VERCEL_GIT_REPO_SLUG,
|
|
131
|
+
process?.env?.VITE_VERCEL_GIT_REPO_SLUG,
|
|
132
|
+
process?.env?.PUBLIC_VERCEL_GIT_REPO_SLUG,
|
|
133
|
+
process?.env?.VUE_APP_VERCEL_GIT_REPO_SLUG,
|
|
134
|
+
process?.env?.REDWOOD_ENV_VERCEL_GIT_REPO_SLUG,
|
|
135
|
+
process?.env?.SANITY_STUDIO_VERCEL_GIT_REPO_SLUG
|
|
136
|
+
);
|
|
137
|
+
ref = pickFirstString(
|
|
138
|
+
process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF,
|
|
139
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_COMMIT_REF,
|
|
140
|
+
process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_REF,
|
|
141
|
+
process?.env?.REACT_APP_VERCEL_GIT_COMMIT_REF,
|
|
142
|
+
process?.env?.GATSBY_VERCEL_GIT_COMMIT_REF,
|
|
143
|
+
process?.env?.VITE_VERCEL_GIT_COMMIT_REF,
|
|
144
|
+
process?.env?.PUBLIC_VERCEL_GIT_COMMIT_REF,
|
|
145
|
+
process?.env?.VUE_APP_VERCEL_GIT_COMMIT_REF,
|
|
146
|
+
process?.env?.REDWOOD_ENV_VERCEL_GIT_COMMIT_REF,
|
|
147
|
+
process?.env?.SANITY_STUDIO_VERCEL_GIT_COMMIT_REF
|
|
148
|
+
);
|
|
149
|
+
revision = pickFirstString(
|
|
150
|
+
process?.env?.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
|
|
151
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
|
|
152
|
+
process?.env?.NUXT_ENV_VERCEL_GIT_COMMIT_SHA,
|
|
153
|
+
process?.env?.REACT_APP_VERCEL_GIT_COMMIT_SHA,
|
|
154
|
+
process?.env?.GATSBY_VERCEL_GIT_COMMIT_SHA,
|
|
155
|
+
process?.env?.VITE_VERCEL_GIT_COMMIT_SHA,
|
|
156
|
+
process?.env?.PUBLIC_VERCEL_GIT_COMMIT_SHA,
|
|
157
|
+
process?.env?.VUE_APP_VERCEL_GIT_COMMIT_SHA,
|
|
158
|
+
process?.env?.REDWOOD_ENV_VERCEL_GIT_COMMIT_SHA,
|
|
159
|
+
process?.env?.SANITY_STUDIO_VERCEL_GIT_COMMIT_SHA
|
|
160
|
+
);
|
|
161
|
+
changeId = pickFirstString(
|
|
162
|
+
process?.env?.NEXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID,
|
|
163
|
+
process?.env?.NUXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID,
|
|
164
|
+
process?.env?.NUXT_ENV_VERCEL_GIT_PULL_REQUEST_ID,
|
|
165
|
+
process?.env?.REACT_APP_VERCEL_GIT_PULL_REQUEST_ID,
|
|
166
|
+
process?.env?.GATSBY_VERCEL_GIT_PULL_REQUEST_ID,
|
|
167
|
+
process?.env?.VITE_VERCEL_GIT_PULL_REQUEST_ID,
|
|
168
|
+
process?.env?.PUBLIC_VERCEL_GIT_PULL_REQUEST_ID,
|
|
169
|
+
process?.env?.VUE_APP_VERCEL_GIT_PULL_REQUEST_ID,
|
|
170
|
+
process?.env?.REDWOOD_ENV_VERCEL_GIT_PULL_REQUEST_ID,
|
|
171
|
+
process?.env?.SANITY_STUDIO_VERCEL_GIT_PULL_REQUEST_ID
|
|
172
|
+
);
|
|
173
|
+
} catch (_ignored) {
|
|
174
|
+
// process is not defined (or a bundler shimmed it strangely) — skip.
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
providerName: provider,
|
|
179
|
+
ownerName: owner,
|
|
180
|
+
repositoryName: repo,
|
|
181
|
+
repositoryUrlFull: buildRepositoryUrlFromVercel(provider, owner, repo),
|
|
182
|
+
refHeadName: ref,
|
|
183
|
+
refHeadRevision: revision,
|
|
184
|
+
changeId,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function detectVcsFromNetlify(): VcsAttributes {
|
|
189
|
+
// Netlify exposes read-only build env vars (REPOSITORY_URL, BRANCH,
|
|
190
|
+
// COMMIT_REF, REVIEW_ID) but does not auto-prefix them for browser
|
|
191
|
+
// exposure. Users must surface them via their framework's prefix
|
|
192
|
+
// convention themselves (e.g. `NEXT_PUBLIC_REPOSITORY_URL = $REPOSITORY_URL`
|
|
193
|
+
// for Next.js, `VITE_REPOSITORY_URL = $REPOSITORY_URL` for Vite, etc.).
|
|
194
|
+
// We enumerate the same 9 framework prefixes as the Vercel path above.
|
|
195
|
+
let repositoryUrl: string | undefined;
|
|
196
|
+
let branch: string | undefined;
|
|
197
|
+
let commit: string | undefined;
|
|
198
|
+
let reviewId: string | undefined;
|
|
199
|
+
try {
|
|
200
|
+
repositoryUrl = pickFirstString(
|
|
201
|
+
process?.env?.NEXT_PUBLIC_REPOSITORY_URL,
|
|
202
|
+
process?.env?.NUXT_PUBLIC_REPOSITORY_URL,
|
|
203
|
+
process?.env?.NUXT_ENV_REPOSITORY_URL,
|
|
204
|
+
process?.env?.REACT_APP_REPOSITORY_URL,
|
|
205
|
+
process?.env?.GATSBY_REPOSITORY_URL,
|
|
206
|
+
process?.env?.VITE_REPOSITORY_URL,
|
|
207
|
+
process?.env?.PUBLIC_REPOSITORY_URL,
|
|
208
|
+
process?.env?.VUE_APP_REPOSITORY_URL,
|
|
209
|
+
process?.env?.REDWOOD_ENV_REPOSITORY_URL,
|
|
210
|
+
process?.env?.SANITY_STUDIO_REPOSITORY_URL
|
|
211
|
+
);
|
|
212
|
+
branch = pickFirstString(
|
|
213
|
+
process?.env?.NEXT_PUBLIC_BRANCH,
|
|
214
|
+
process?.env?.NUXT_PUBLIC_BRANCH,
|
|
215
|
+
process?.env?.NUXT_ENV_BRANCH,
|
|
216
|
+
process?.env?.REACT_APP_BRANCH,
|
|
217
|
+
process?.env?.GATSBY_BRANCH,
|
|
218
|
+
process?.env?.VITE_BRANCH,
|
|
219
|
+
process?.env?.PUBLIC_BRANCH,
|
|
220
|
+
process?.env?.VUE_APP_BRANCH,
|
|
221
|
+
process?.env?.REDWOOD_ENV_BRANCH,
|
|
222
|
+
process?.env?.SANITY_STUDIO_BRANCH
|
|
223
|
+
);
|
|
224
|
+
commit = pickFirstString(
|
|
225
|
+
process?.env?.NEXT_PUBLIC_COMMIT_REF,
|
|
226
|
+
process?.env?.NUXT_PUBLIC_COMMIT_REF,
|
|
227
|
+
process?.env?.NUXT_ENV_COMMIT_REF,
|
|
228
|
+
process?.env?.REACT_APP_COMMIT_REF,
|
|
229
|
+
process?.env?.GATSBY_COMMIT_REF,
|
|
230
|
+
process?.env?.VITE_COMMIT_REF,
|
|
231
|
+
process?.env?.PUBLIC_COMMIT_REF,
|
|
232
|
+
process?.env?.VUE_APP_COMMIT_REF,
|
|
233
|
+
process?.env?.REDWOOD_ENV_COMMIT_REF,
|
|
234
|
+
process?.env?.SANITY_STUDIO_COMMIT_REF
|
|
235
|
+
);
|
|
236
|
+
reviewId = pickFirstString(
|
|
237
|
+
process?.env?.NEXT_PUBLIC_REVIEW_ID,
|
|
238
|
+
process?.env?.NUXT_PUBLIC_REVIEW_ID,
|
|
239
|
+
process?.env?.NUXT_ENV_REVIEW_ID,
|
|
240
|
+
process?.env?.REACT_APP_REVIEW_ID,
|
|
241
|
+
process?.env?.GATSBY_REVIEW_ID,
|
|
242
|
+
process?.env?.VITE_REVIEW_ID,
|
|
243
|
+
process?.env?.PUBLIC_REVIEW_ID,
|
|
244
|
+
process?.env?.VUE_APP_REVIEW_ID,
|
|
245
|
+
process?.env?.REDWOOD_ENV_REVIEW_ID,
|
|
246
|
+
process?.env?.SANITY_STUDIO_REVIEW_ID
|
|
247
|
+
);
|
|
248
|
+
} catch (_ignored) {
|
|
249
|
+
// process is not defined — skip.
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const parsed = parseRepositoryUrl(repositoryUrl);
|
|
253
|
+
return {
|
|
254
|
+
providerName: parsed?.providerName,
|
|
255
|
+
ownerName: parsed?.ownerName,
|
|
256
|
+
repositoryName: parsed?.repositoryName,
|
|
257
|
+
repositoryUrlFull: repositoryUrl,
|
|
258
|
+
refHeadName: branch,
|
|
259
|
+
refHeadRevision: commit,
|
|
260
|
+
changeId: reviewId,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildRepositoryUrlFromVercel(
|
|
265
|
+
provider: string | undefined,
|
|
266
|
+
owner: string | undefined,
|
|
267
|
+
repo: string | undefined
|
|
268
|
+
): string | undefined {
|
|
269
|
+
if (!provider || !owner || !repo) return undefined;
|
|
270
|
+
const host = vcsHostForProvider(provider);
|
|
271
|
+
if (!host) return undefined;
|
|
272
|
+
return `https://${host}/${owner}/${repo}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Single source of truth for provider↔host mapping. Both `vcsHostForProvider`
|
|
276
|
+
// (Vercel path: provider name is known, derive the canonical host) and
|
|
277
|
+
// `vcsProviderForHost` (Netlify path: only the URL is known, derive the
|
|
278
|
+
// provider) consult this map so adding a provider is one edit. Each entry is
|
|
279
|
+
// a list of accepted hostnames; the FIRST entry is the canonical host used
|
|
280
|
+
// when building URLs from a provider name. Subsequent entries are exact
|
|
281
|
+
// aliases — we do not match arbitrary subdomains.
|
|
282
|
+
const VCS_PROVIDER_HOSTS: Record<string, readonly string[]> = {
|
|
283
|
+
github: ["github.com", "gist.github.com"],
|
|
284
|
+
gitlab: ["gitlab.com"],
|
|
285
|
+
bitbucket: ["bitbucket.org"],
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
function vcsHostForProvider(provider: string): string | undefined {
|
|
289
|
+
return VCS_PROVIDER_HOSTS[provider]?.[0];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function parseRepositoryUrl(
|
|
293
|
+
url: string | undefined
|
|
294
|
+
): { providerName: string; ownerName: string; repositoryName: string } | undefined {
|
|
295
|
+
if (!url) return undefined;
|
|
296
|
+
|
|
297
|
+
let host: string;
|
|
298
|
+
let pathname: string;
|
|
299
|
+
try {
|
|
300
|
+
const parsed = new URL(url);
|
|
301
|
+
host = parsed.host;
|
|
302
|
+
pathname = parsed.pathname;
|
|
303
|
+
} catch (_ignored) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const provider = vcsProviderForHost(host);
|
|
308
|
+
if (!provider) return undefined;
|
|
309
|
+
|
|
310
|
+
const trimmed = pathname.replace(/^\/+/, "").replace(/\.git$/, "");
|
|
311
|
+
const segments = trimmed.split("/");
|
|
312
|
+
if (segments.length < 2) return undefined;
|
|
313
|
+
|
|
314
|
+
const ownerName = segments[0];
|
|
315
|
+
// Bitbucket and GitLab can have nested groups; the repository slug is the
|
|
316
|
+
// last path segment. GitHub paths are always two segments.
|
|
317
|
+
const repositoryName = segments[segments.length - 1];
|
|
318
|
+
if (!ownerName || !repositoryName) return undefined;
|
|
319
|
+
|
|
320
|
+
return { providerName: provider, ownerName, repositoryName };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function vcsProviderForHost(host: string): string | undefined {
|
|
324
|
+
for (const [provider, hosts] of Object.entries(VCS_PROVIDER_HOSTS)) {
|
|
325
|
+
if (hosts.includes(host)) return provider;
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
@@ -6,6 +6,15 @@ 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
8
|
|
|
9
|
+
// VCS Resource Attribute Keys
|
|
10
|
+
export const VCS_PROVIDER_NAME = "vcs.provider.name";
|
|
11
|
+
export const VCS_OWNER_NAME = "vcs.owner.name";
|
|
12
|
+
export const VCS_REPOSITORY_NAME = "vcs.repository.name";
|
|
13
|
+
export const VCS_REPOSITORY_URL_FULL = "vcs.repository.url.full";
|
|
14
|
+
export const VCS_REF_HEAD_NAME = "vcs.ref.head.name";
|
|
15
|
+
export const VCS_REF_HEAD_REVISION = "vcs.ref.head.revision";
|
|
16
|
+
export const VCS_CHANGE_ID = "vcs.change.id";
|
|
17
|
+
|
|
9
18
|
// Misc Signal Attribute Keys
|
|
10
19
|
export const EVENT_NAME = "event.name";
|
|
11
20
|
export const WEB_EVENT_TITLE = "dash0.web.event.title";
|
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
|
@@ -4,6 +4,29 @@ import { Endpoint, Vars, PropagatorConfig } from "../vars";
|
|
|
4
4
|
|
|
5
5
|
export type InstrumentationName = "@dash0/navigation" | "@dash0/web-vitals" | "@dash0/error" | "@dash0/fetch";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* VCS (version control) context describing the build the SDK is running
|
|
9
|
+
* inside. Used both as the public manual-override shape on `InitOptions.vcs`
|
|
10
|
+
* and internally as the merged result of auto-detection. Each field maps to
|
|
11
|
+
* a standard OpenTelemetry `vcs.*` resource attribute.
|
|
12
|
+
*/
|
|
13
|
+
export type VcsAttributes = {
|
|
14
|
+
/** vcs.provider.name — e.g. "github", "gitlab", "bitbucket". */
|
|
15
|
+
providerName?: string;
|
|
16
|
+
/** vcs.owner.name — repository owner / organization. */
|
|
17
|
+
ownerName?: string;
|
|
18
|
+
/** vcs.repository.name — short repository name (no owner prefix). */
|
|
19
|
+
repositoryName?: string;
|
|
20
|
+
/** vcs.repository.url.full — canonical repository URL. */
|
|
21
|
+
repositoryUrlFull?: string;
|
|
22
|
+
/** vcs.ref.head.name — branch or tag name the build was made from. */
|
|
23
|
+
refHeadName?: string;
|
|
24
|
+
/** vcs.ref.head.revision — commit SHA the build was made from. */
|
|
25
|
+
refHeadRevision?: string;
|
|
26
|
+
/** vcs.change.id — pull/merge request identifier (preview deploys). */
|
|
27
|
+
changeId?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
7
30
|
export type InitOptions = {
|
|
8
31
|
serviceName: string;
|
|
9
32
|
serviceNamespace?: string;
|
|
@@ -28,6 +51,39 @@ export type InitOptions = {
|
|
|
28
51
|
*/
|
|
29
52
|
rejectSuspiciousServiceName?: boolean;
|
|
30
53
|
|
|
54
|
+
/**
|
|
55
|
+
* When `true`, disable auto-detection of VCS (version control) context
|
|
56
|
+
* from the build environment. By default the SDK reads VCS context from
|
|
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
|
+
* as resource attributes following the OTel `vcs.*` semantic conventions:
|
|
61
|
+
*
|
|
62
|
+
* - vcs.provider.name
|
|
63
|
+
* - vcs.owner.name
|
|
64
|
+
* - vcs.repository.name
|
|
65
|
+
* - vcs.repository.url.full
|
|
66
|
+
* - vcs.ref.head.name
|
|
67
|
+
* - vcs.ref.head.revision
|
|
68
|
+
* - vcs.change.id
|
|
69
|
+
*
|
|
70
|
+
* Pairing telemetry with the git commit + branch the build came from lets
|
|
71
|
+
* Dash0 Agent answer questions like "which PR introduced this error?".
|
|
72
|
+
*
|
|
73
|
+
* Note: any fields supplied via `vcs` are still applied even when this flag
|
|
74
|
+
* is `true` — manual overrides always win. Set this flag when you want to
|
|
75
|
+
* prevent env-var reads entirely but still supply context explicitly.
|
|
76
|
+
*/
|
|
77
|
+
disableVcsDetection?: boolean;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Manually specify VCS (version control) context. Each provided field
|
|
81
|
+
* overrides the value the SDK would otherwise auto-detect from the build
|
|
82
|
+
* environment for that attribute. Use this for non-Vercel/Netlify
|
|
83
|
+
* deployments, or when the auto-detected values are wrong.
|
|
84
|
+
*/
|
|
85
|
+
vcs?: VcsAttributes;
|
|
86
|
+
|
|
31
87
|
/**
|
|
32
88
|
* OTLP endpoints to which the generated telemetry should be sent to.
|
|
33
89
|
*/
|
|
@@ -38,6 +94,16 @@ export type InitOptions = {
|
|
|
38
94
|
*/
|
|
39
95
|
enabledInstrumentations?: InstrumentationName[];
|
|
40
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
|
+
|
|
41
107
|
/**
|
|
42
108
|
* The session inactivity timeout. Session inactivity is the maximum
|
|
43
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
|
+
});
|
package/src/vars.ts
CHANGED
|
@@ -171,6 +171,12 @@ export type Vars = {
|
|
|
171
171
|
* experimental - in rare cases causes Chrome to crash to use at your own risk.
|
|
172
172
|
*/
|
|
173
173
|
enableTransportCompression: boolean;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Whether the current session is sampled in (true) or out (false).
|
|
177
|
+
* Determined at init time based on sessionSamplingRate and the session ID.
|
|
178
|
+
*/
|
|
179
|
+
isSessionSampled: boolean;
|
|
174
180
|
};
|
|
175
181
|
|
|
176
182
|
export const vars: Vars = {
|
|
@@ -198,4 +204,5 @@ export const vars: Vars = {
|
|
|
198
204
|
includeParts: [],
|
|
199
205
|
},
|
|
200
206
|
enableTransportCompression: false,
|
|
207
|
+
isSessionSampled: true,
|
|
201
208
|
};
|