@devfellowship/observability 2.1.0 → 2.2.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 +16 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +258 -14
- package/dist/index.js.map +1 -1
- package/package.json +13 -8
package/README.md
CHANGED
|
@@ -36,7 +36,8 @@ With zero props, the provider reads configuration from Vite environment variable
|
|
|
36
36
|
| `VITE_APP_NAME` | Service name for traces/metrics |
|
|
37
37
|
| `VITE_APP_VERSION` | Service version |
|
|
38
38
|
| `VITE_OTEL_API_KEY` | API key sent as `x-api-key` header |
|
|
39
|
-
| `VITE_SENTRY_DSN` | Sentry DSN (enables error tracking
|
|
39
|
+
| `VITE_SENTRY_DSN` | Sentry DSN (enables error tracking. Session replay is OFF.) |
|
|
40
|
+
| `VITE_GIT_SHA` | Git SHA of the build. Becomes the Sentry `release`. Falls back to `VITE_COMMIT_SHA`, then `<app>@<version>`. |
|
|
40
41
|
| `VITE_OTEL_ENABLED` | Set to `"true"` to enable (auto-enabled in production) |
|
|
41
42
|
|
|
42
43
|
## Explicit configuration
|
|
@@ -120,11 +121,24 @@ import { ErrorBoundary } from "@devfellowship/observability";
|
|
|
120
121
|
- **OTel metrics** — `PeriodicExportingMetricReader` → OTLP/HTTP exporter
|
|
121
122
|
- **Fetch instrumentation** — auto-creates spans for all `fetch()` calls (collector URLs filtered)
|
|
122
123
|
- **Web Vitals** — LCP, CLS, INP, TTFB reported as OTel histogram metrics
|
|
123
|
-
- **Sentry** — error tracking,
|
|
124
|
+
- **Sentry** — error tracking and browser tracing, with scrubbing (see Privacy). No session replay.
|
|
124
125
|
- **Error boundary** — catches React component errors, reports to both OTel and Sentry
|
|
125
126
|
- **React Query integration** — subscribes to query cache, creates error spans on failures
|
|
126
127
|
- **Resource attributes** — browser, OS, device type, session ID, viewport, connection type, route
|
|
127
128
|
|
|
129
|
+
## Privacy (Sentry)
|
|
130
|
+
|
|
131
|
+
Since 2.2.0 the Sentry client is built to leak no personal data (plan `20261101-fleet-observability-error-loop`, T1):
|
|
132
|
+
|
|
133
|
+
- **No session replay.** No replay integration, and both replay sample rates are 0. Do not add it back without a new privacy decision.
|
|
134
|
+
- **Anonymous user.** The Sentry user is a random id per browser (`localStorage` key `dfl_obs_anon_id`). `setSentryUser(id, role)` keeps the signature but ignores `id`: no account id, email or name is sent.
|
|
135
|
+
- **Scrubbing.** `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb` remove emails, URL query strings and fragments, JWTs, bearer tokens, key=value secrets and IPv4 addresses from messages, request data, headers, breadcrumbs and span data. Cookies, `Authorization` and similar headers are dropped. See `src/lib/scrub.ts`.
|
|
136
|
+
- **Fail closed, never throw.** The three hooks use `scrubEventOrDrop` and `scrubBreadcrumbOrDrop`. A getter-only property (seen 2026-11-04) is read and written safely. When a value needs scrubbing and its property cannot take the new value, the event or crumb is dropped, a `console.warn` names the key (never the value), and nothing is sent half scrubbed. A hook that threw was reported by the SDK as a new error and lost the transaction.
|
|
137
|
+
- **IP address.** `sendDefaultPii` is `false`; with it the SDK sets `infer_ip: "never"`. There is **no** `scrubIPAddresses` SDK option in `@sentry/react` 10.x. "Prevent Storing of IP Addresses" is a Sentry **project** setting and must be on in the project (`dfl-react`). The SDK never sends an IP, and a test checks it.
|
|
138
|
+
- **Release.** Set `VITE_GIT_SHA` (or the `release` prop) at build time, so a release changes at each deploy.
|
|
139
|
+
|
|
140
|
+
Tests: `npm test` (unit, with fixtures). Real-browser proof of "no replay envelope": `PLAYWRIGHT_PATH=<path to playwright/index.mjs> npm run proof:browser`.
|
|
141
|
+
|
|
128
142
|
## Development
|
|
129
143
|
|
|
130
144
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ interface ObservabilityConfig {
|
|
|
6
6
|
serviceName?: string;
|
|
7
7
|
/** Service version. Falls back to VITE_APP_VERSION or "0.0.0" */
|
|
8
8
|
serviceVersion?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Sentry release id. Pass the git SHA of the build. Falls back to
|
|
11
|
+
* VITE_GIT_SHA, VITE_COMMIT_SHA, then `serviceName@serviceVersion`.
|
|
12
|
+
*/
|
|
13
|
+
release?: string;
|
|
9
14
|
/** OTel collector base URL. Default: "https://otel.devfellowship.com" */
|
|
10
15
|
collectorUrl?: string;
|
|
11
16
|
/** API key sent as x-api-key header. Falls back to VITE_OTEL_API_KEY */
|
|
@@ -26,7 +31,7 @@ interface ObservabilityConfig {
|
|
|
26
31
|
interface ObservabilityProviderProps extends ObservabilityConfig {
|
|
27
32
|
children: ReactNode;
|
|
28
33
|
}
|
|
29
|
-
declare function ObservabilityProvider({ serviceName, serviceVersion, collectorUrl, apiKey, sentryDsn, environment, enabled, enableWebVitals, enableQueryTracking, enableFetchTracing, children, }: ObservabilityProviderProps): react_jsx_runtime.JSX.Element;
|
|
34
|
+
declare function ObservabilityProvider({ serviceName, serviceVersion, release, collectorUrl, apiKey, sentryDsn, environment, enabled, enableWebVitals, enableQueryTracking, enableFetchTracing, children, }: ObservabilityProviderProps): react_jsx_runtime.JSX.Element;
|
|
30
35
|
|
|
31
36
|
interface ErrorBoundaryProps {
|
|
32
37
|
children: ReactNode;
|
|
@@ -72,7 +77,11 @@ declare function setUserId(id: string): void;
|
|
|
72
77
|
declare function setUserRole(role: string): void;
|
|
73
78
|
|
|
74
79
|
declare function captureError(error: Error, context?: Record<string, string>): void;
|
|
75
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Kept for API compatibility. The account id is NOT sent to Sentry: the user
|
|
82
|
+
* stays the anonymous browser id. Only the role is attached.
|
|
83
|
+
*/
|
|
84
|
+
declare function setSentryUser(_id: string, role?: string): void;
|
|
76
85
|
|
|
77
86
|
/**
|
|
78
87
|
* Captures Core Web Vitals (LCP, FID, CLS, INP, TTFB) and reports them
|
package/dist/index.js
CHANGED
|
@@ -215,28 +215,269 @@ function uninstrumentFetch() {
|
|
|
215
215
|
|
|
216
216
|
// src/lib/sentry-init.ts
|
|
217
217
|
import * as Sentry from "@sentry/react";
|
|
218
|
+
|
|
219
|
+
// src/lib/scrub.ts
|
|
220
|
+
var REDACTED = "[redacted]";
|
|
221
|
+
var MAX_DEPTH = 8;
|
|
222
|
+
var SENSITIVE_KEY = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|apikey|api[-_]?key|x-auth-token|x-csrf-token|x-xsrf-token|x-supabase-auth|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|passwd|pwd|jwt|session[-_]?id|email|e-mail|username|user[-_]?name|ip[-_]?address|remote_addr|x-forwarded-for|x-real-ip)$/i;
|
|
223
|
+
var SKIP_KEYS = /* @__PURE__ */ new Set([
|
|
224
|
+
"event_id",
|
|
225
|
+
"timestamp",
|
|
226
|
+
"start_timestamp",
|
|
227
|
+
"release",
|
|
228
|
+
"environment",
|
|
229
|
+
"platform",
|
|
230
|
+
"sdk",
|
|
231
|
+
"level",
|
|
232
|
+
"type",
|
|
233
|
+
"trace_id",
|
|
234
|
+
"span_id",
|
|
235
|
+
"parent_span_id",
|
|
236
|
+
"op"
|
|
237
|
+
]);
|
|
238
|
+
var EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}/g;
|
|
239
|
+
var JWT = /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]*/g;
|
|
240
|
+
var BEARER = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
241
|
+
var IPV4 = /\b(?:25[0-5]|2[0-4]\d|1?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3}\b/g;
|
|
242
|
+
var KEY_VALUE_SECRET = /\b(access_token|refresh_token|id_token|token|apikey|api_key|secret|password|authorization)(["']?\s*[:=]\s*["']?)[^\s"'&,;)}\]]+/gi;
|
|
243
|
+
var URL_WITH_QUERY = /((?:\bhttps?:\/\/|\bwss?:\/\/)[^\s"'<>?#]*|(?<![\w:/.])\/[\w\-./~%@+]*)[?#][^\s"'<>]*/g;
|
|
244
|
+
var OPAQUE_SEGMENT = /\/(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[A-Za-z0-9_-]{32,})(?=[/?#\s"']|$)/g;
|
|
245
|
+
function stripUrl(url) {
|
|
246
|
+
const cut = url.search(/[?#]/);
|
|
247
|
+
const base = cut === -1 ? url : url.slice(0, cut);
|
|
248
|
+
return base.replace(OPAQUE_SEGMENT, "/:id");
|
|
249
|
+
}
|
|
250
|
+
function scrubString(input) {
|
|
251
|
+
if (!input) return input;
|
|
252
|
+
let out = input;
|
|
253
|
+
out = out.replace(JWT, "[jwt]");
|
|
254
|
+
out = out.replace(BEARER, "$1 [token]");
|
|
255
|
+
out = out.replace(URL_WITH_QUERY, (_m, base) => stripUrl(base));
|
|
256
|
+
out = out.replace(KEY_VALUE_SECRET, `$1$2${REDACTED}`);
|
|
257
|
+
out = out.replace(EMAIL, "[email]");
|
|
258
|
+
out = out.replace(IPV4, "[ip]");
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
var ScrubIncompleteError = class extends Error {
|
|
262
|
+
constructor(keys) {
|
|
263
|
+
super(`scrub could not write: ${keys.join(", ")}`);
|
|
264
|
+
this.name = "ScrubIncompleteError";
|
|
265
|
+
this.keys = keys;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
function newState() {
|
|
269
|
+
return { seen: /* @__PURE__ */ new WeakSet(), unwritable: [] };
|
|
270
|
+
}
|
|
271
|
+
function readKey(obj, key) {
|
|
272
|
+
try {
|
|
273
|
+
return { ok: true, value: obj[key] };
|
|
274
|
+
} catch {
|
|
275
|
+
return { ok: false };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function writeKey(obj, key, value) {
|
|
279
|
+
try {
|
|
280
|
+
obj[key] = value;
|
|
281
|
+
if (Object.is(obj[key], value)) return true;
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const d = Object.getOwnPropertyDescriptor(obj, key);
|
|
286
|
+
if (d && d.configurable) {
|
|
287
|
+
Object.defineProperty(obj, key, {
|
|
288
|
+
value,
|
|
289
|
+
writable: true,
|
|
290
|
+
enumerable: d.enumerable,
|
|
291
|
+
configurable: true
|
|
292
|
+
});
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
function removeKey(obj, key) {
|
|
300
|
+
try {
|
|
301
|
+
return delete obj[key] || !(key in obj);
|
|
302
|
+
} catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function scrubValue(value, depth, state) {
|
|
307
|
+
if (typeof value === "string") return scrubString(value);
|
|
308
|
+
if (value === null || typeof value !== "object") return value;
|
|
309
|
+
if (depth >= MAX_DEPTH) return value;
|
|
310
|
+
if (state.seen.has(value)) return value;
|
|
311
|
+
state.seen.add(value);
|
|
312
|
+
if (Array.isArray(value)) {
|
|
313
|
+
for (let i = 0; i < value.length; i++) {
|
|
314
|
+
const next = scrubValue(value[i], depth + 1, state);
|
|
315
|
+
if (next !== value[i] && !writeKey(value, String(i), next)) {
|
|
316
|
+
state.unwritable.push(String(i));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return value;
|
|
320
|
+
}
|
|
321
|
+
const obj = value;
|
|
322
|
+
for (const key of Object.keys(obj)) {
|
|
323
|
+
if (SENSITIVE_KEY.test(key)) {
|
|
324
|
+
const cur2 = readKey(obj, key);
|
|
325
|
+
if (cur2.ok && cur2.value !== void 0 && cur2.value !== null && !removeKey(obj, key)) {
|
|
326
|
+
state.unwritable.push(key);
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
331
|
+
const cur = readKey(obj, key);
|
|
332
|
+
if (!cur.ok) continue;
|
|
333
|
+
const v = cur.value;
|
|
334
|
+
const next = typeof v === "string" && isUrlKey(key) ? scrubString(stripUrl(v)) : scrubValue(v, depth + 1, state);
|
|
335
|
+
if (next !== v && !writeKey(obj, key, next)) state.unwritable.push(key);
|
|
336
|
+
}
|
|
337
|
+
return obj;
|
|
338
|
+
}
|
|
339
|
+
function isUrlKey(key) {
|
|
340
|
+
return /^(url|from|to|abs_path|filename|referrer|referer|origin_url|http\.url|url\.full|http\.query|url\.query|http\.fragment)$/i.test(
|
|
341
|
+
key
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
function scrubEvent(event) {
|
|
345
|
+
const e = event;
|
|
346
|
+
if (e.user && typeof e.user === "object") {
|
|
347
|
+
const { id, role } = e.user;
|
|
348
|
+
e.user = {
|
|
349
|
+
...id !== void 0 ? { id } : {},
|
|
350
|
+
...typeof role === "string" ? { role } : {}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (e.request && typeof e.request === "object") {
|
|
354
|
+
const r = e.request;
|
|
355
|
+
if (typeof r.url === "string") r.url = scrubString(stripUrl(r.url));
|
|
356
|
+
delete r.cookies;
|
|
357
|
+
delete r.query_string;
|
|
358
|
+
if (r.env && typeof r.env === "object") delete r.env.REMOTE_ADDR;
|
|
359
|
+
if (r.headers && typeof r.headers === "object") {
|
|
360
|
+
for (const k of Object.keys(r.headers)) {
|
|
361
|
+
if (SENSITIVE_KEY.test(k)) {
|
|
362
|
+
delete r.headers[k];
|
|
363
|
+
} else if (/^(referer|referrer|origin)$/i.test(k) && typeof r.headers[k] === "string") {
|
|
364
|
+
r.headers[k] = scrubString(stripUrl(r.headers[k]));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (typeof e.transaction === "string") {
|
|
370
|
+
e.transaction = scrubString(stripUrl(e.transaction));
|
|
371
|
+
}
|
|
372
|
+
const state = newState();
|
|
373
|
+
scrubValue(e, 0, state);
|
|
374
|
+
if (e.user && "ip_address" in e.user) delete e.user.ip_address;
|
|
375
|
+
if (state.unwritable.length > 0) throw new ScrubIncompleteError(state.unwritable);
|
|
376
|
+
return event;
|
|
377
|
+
}
|
|
378
|
+
function scrubBreadcrumb(crumb) {
|
|
379
|
+
const c = crumb;
|
|
380
|
+
if (typeof c.message === "string") c.message = scrubString(c.message);
|
|
381
|
+
if (c.data && typeof c.data === "object") {
|
|
382
|
+
const state = newState();
|
|
383
|
+
scrubValue(c.data, 0, state);
|
|
384
|
+
if (state.unwritable.length > 0) throw new ScrubIncompleteError(state.unwritable);
|
|
385
|
+
for (const k of ["url", "from", "to"]) {
|
|
386
|
+
if (typeof c.data[k] === "string") c.data[k] = scrubString(stripUrl(c.data[k]));
|
|
387
|
+
}
|
|
388
|
+
delete c.data.query;
|
|
389
|
+
delete c.data.body;
|
|
390
|
+
}
|
|
391
|
+
return crumb;
|
|
392
|
+
}
|
|
393
|
+
var _warned = /* @__PURE__ */ new Set();
|
|
394
|
+
function warnOnce(what, err) {
|
|
395
|
+
const detail = err instanceof ScrubIncompleteError ? err.keys.join(",") : err?.name;
|
|
396
|
+
const id = `${what}:${detail}`;
|
|
397
|
+
if (_warned.has(id)) return;
|
|
398
|
+
_warned.add(id);
|
|
399
|
+
try {
|
|
400
|
+
console.warn(`[observability] ${what} dropped: scrub failed (${detail})`);
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function scrubEventOrDrop(event) {
|
|
405
|
+
try {
|
|
406
|
+
return scrubEvent(event);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
warnOnce("event", err);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function scrubBreadcrumbOrDrop(crumb) {
|
|
413
|
+
try {
|
|
414
|
+
return scrubBreadcrumb(crumb);
|
|
415
|
+
} catch (err) {
|
|
416
|
+
warnOnce("breadcrumb", err);
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/lib/sentry-init.ts
|
|
218
422
|
var _initialized = false;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
423
|
+
var ANON_ID_KEY = "dfl_obs_anon_id";
|
|
424
|
+
var _memoryAnonId;
|
|
425
|
+
function randomId() {
|
|
426
|
+
try {
|
|
427
|
+
const c = globalThis.crypto;
|
|
428
|
+
if (c && typeof c.randomUUID === "function") return c.randomUUID();
|
|
429
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
430
|
+
const b = c.getRandomValues(new Uint8Array(16));
|
|
431
|
+
return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
432
|
+
}
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
return Math.random().toString(16).slice(2) + Date.now().toString(16);
|
|
436
|
+
}
|
|
437
|
+
function getAnonymousId() {
|
|
438
|
+
try {
|
|
439
|
+
const stored = globalThis.localStorage?.getItem(ANON_ID_KEY);
|
|
440
|
+
if (stored) return stored;
|
|
441
|
+
const fresh = randomId();
|
|
442
|
+
globalThis.localStorage?.setItem(ANON_ID_KEY, fresh);
|
|
443
|
+
return fresh;
|
|
444
|
+
} catch {
|
|
445
|
+
return _memoryAnonId ?? (_memoryAnonId = randomId());
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function buildSentryOptions(config) {
|
|
449
|
+
return {
|
|
222
450
|
dsn: config.dsn,
|
|
223
451
|
environment: config.environment,
|
|
224
|
-
release: `${config.serviceName}@${config.serviceVersion}`,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
],
|
|
229
|
-
tracesSampleRate: 0.2,
|
|
452
|
+
release: config.release || `${config.serviceName}@${config.serviceVersion}`,
|
|
453
|
+
// Privacy: no session replay. No replay integration and both rates 0, so no
|
|
454
|
+
// replay envelope is ever sent. Do not add it back without a new decision.
|
|
455
|
+
integrations: [Sentry.browserTracingIntegration()],
|
|
230
456
|
replaysSessionSampleRate: 0,
|
|
231
|
-
replaysOnErrorSampleRate:
|
|
457
|
+
replaysOnErrorSampleRate: 0,
|
|
458
|
+
tracesSampleRate: 0.2,
|
|
459
|
+
// The SDK sends no default PII. The server-side IP scrub is a project
|
|
460
|
+
// setting (see README), not an SDK option.
|
|
461
|
+
sendDefaultPii: false,
|
|
232
462
|
beforeSend(event) {
|
|
233
463
|
event.tags = {
|
|
234
464
|
...event.tags,
|
|
235
465
|
"service.name": config.serviceName
|
|
236
466
|
};
|
|
237
|
-
return event;
|
|
467
|
+
return scrubEventOrDrop(event);
|
|
468
|
+
},
|
|
469
|
+
beforeSendTransaction(event) {
|
|
470
|
+
return scrubEventOrDrop(event);
|
|
471
|
+
},
|
|
472
|
+
beforeBreadcrumb(crumb) {
|
|
473
|
+
return scrubBreadcrumbOrDrop(crumb);
|
|
238
474
|
}
|
|
239
|
-
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function initSentry(config) {
|
|
478
|
+
if (_initialized) return;
|
|
479
|
+
Sentry.init(buildSentryOptions(config));
|
|
480
|
+
Sentry.setUser({ id: getAnonymousId() });
|
|
240
481
|
Sentry.setTag("service.name", config.serviceName);
|
|
241
482
|
if (config.sandboxSlug) {
|
|
242
483
|
Sentry.setTag("sandbox.slug", config.sandboxSlug);
|
|
@@ -257,9 +498,9 @@ function captureError(error, context) {
|
|
|
257
498
|
Sentry.captureException(error);
|
|
258
499
|
});
|
|
259
500
|
}
|
|
260
|
-
function setSentryUser(
|
|
501
|
+
function setSentryUser(_id, role) {
|
|
261
502
|
if (!_initialized) return;
|
|
262
|
-
Sentry.setUser({ id, ...role ? { role } : {} });
|
|
503
|
+
Sentry.setUser({ id: getAnonymousId(), ...role ? { role } : {} });
|
|
263
504
|
}
|
|
264
505
|
|
|
265
506
|
// src/hooks/useWebVitals.ts
|
|
@@ -432,6 +673,7 @@ function getViteEnv() {
|
|
|
432
673
|
function ObservabilityProvider({
|
|
433
674
|
serviceName,
|
|
434
675
|
serviceVersion,
|
|
676
|
+
release,
|
|
435
677
|
collectorUrl,
|
|
436
678
|
apiKey,
|
|
437
679
|
sentryDsn,
|
|
@@ -446,6 +688,7 @@ function ObservabilityProvider({
|
|
|
446
688
|
const env = getViteEnv();
|
|
447
689
|
const resolvedServiceName = serviceName ?? env.VITE_APP_NAME ?? "unknown";
|
|
448
690
|
const resolvedServiceVersion = serviceVersion ?? env.VITE_APP_VERSION ?? "0.0.0";
|
|
691
|
+
const resolvedRelease = release ?? env.VITE_GIT_SHA ?? env.VITE_COMMIT_SHA;
|
|
449
692
|
const resolvedCollectorUrl = collectorUrl ?? "https://otel.devfellowship.com";
|
|
450
693
|
const resolvedApiKey = apiKey ?? env.VITE_OTEL_API_KEY;
|
|
451
694
|
const resolvedSentryDsn = sentryDsn ?? env.VITE_SENTRY_DSN;
|
|
@@ -460,6 +703,7 @@ function ObservabilityProvider({
|
|
|
460
703
|
dsn: resolvedSentryDsn,
|
|
461
704
|
serviceName: resolvedServiceName,
|
|
462
705
|
serviceVersion: resolvedServiceVersion,
|
|
706
|
+
release: resolvedRelease || void 0,
|
|
463
707
|
environment: resolvedEnvironment,
|
|
464
708
|
sandboxSlug,
|
|
465
709
|
sandboxBranch
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ObservabilityProvider.tsx","../src/lib/otel-init.ts","../src/lib/resource-attributes.ts","../src/lib/fetch-instrumentation.ts","../src/lib/sentry-init.ts","../src/hooks/useWebVitals.ts","../src/hooks/useQueryErrorReporter.ts","../src/ErrorBoundary.tsx","../src/lib/track-event.ts"],"sourcesContent":["import React, { useEffect, useRef, ReactNode } from \"react\";\nimport { trace, SpanStatusCode } from \"@opentelemetry/api\";\nimport {\n initOtelBrowser,\n shutdownOtel,\n type OtelProviders,\n} from \"./lib/otel-init\";\nimport {\n instrumentFetch,\n uninstrumentFetch,\n} from \"./lib/fetch-instrumentation\";\nimport { getDynamicAttributes } from \"./lib/resource-attributes\";\nimport { initSentry, captureError } from \"./lib/sentry-init\";\nimport { useWebVitals } from \"./hooks/useWebVitals\";\nimport { useQueryErrorReporter } from \"./hooks/useQueryErrorReporter\";\nimport { ErrorBoundary } from \"./ErrorBoundary\";\n\nexport interface ObservabilityConfig {\n /** Service name reported to the collector. Falls back to VITE_APP_NAME env var. */\n serviceName?: string;\n /** Service version. Falls back to VITE_APP_VERSION or \"0.0.0\" */\n serviceVersion?: string;\n /** OTel collector base URL. Default: \"https://otel.devfellowship.com\" */\n collectorUrl?: string;\n /** API key sent as x-api-key header. Falls back to VITE_OTEL_API_KEY */\n apiKey?: string;\n /** Sentry DSN. Falls back to VITE_SENTRY_DSN */\n sentryDsn?: string;\n /** Environment label. Default: \"production\" */\n environment?: string;\n /** Master kill-switch. Falls back to VITE_OTEL_ENABLED or import.meta.env.PROD */\n enabled?: boolean;\n /** Capture Core Web Vitals. Default: true */\n enableWebVitals?: boolean;\n /** Report React Query errors as spans. Default: true */\n enableQueryTracking?: boolean;\n /** Auto-trace all fetch() calls. Default: true */\n enableFetchTracing?: boolean;\n}\n\ninterface ObservabilityProviderProps extends ObservabilityConfig {\n children: ReactNode;\n}\n\n// ---- Internal active-instrumentation wrapper ----\n\nfunction ActiveInstrumentation({\n enableWebVitals,\n enableQueryTracking,\n children,\n}: {\n enableWebVitals: boolean;\n enableQueryTracking: boolean;\n children: ReactNode;\n}) {\n if (enableWebVitals) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useWebVitals();\n }\n\n if (enableQueryTracking) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useQueryErrorReporter();\n }\n\n return <>{children}</>;\n}\n\n// ---- Main provider ----\n\n// Safe access to Vite env vars (no-op outside Vite)\nfunction getViteEnv(): Record<string, string | boolean | undefined> {\n try {\n return (import.meta as ImportMeta & { env: Record<string, string | boolean> }).env ?? {};\n } catch {\n return {};\n }\n}\n\nexport function ObservabilityProvider({\n serviceName,\n serviceVersion,\n collectorUrl,\n apiKey,\n sentryDsn,\n environment,\n enabled,\n enableWebVitals = true,\n enableQueryTracking = true,\n enableFetchTracing = true,\n children,\n}: ObservabilityProviderProps) {\n const providersRef = useRef<OtelProviders | null>(null);\n\n const env = getViteEnv();\n const resolvedServiceName = serviceName ?? (env.VITE_APP_NAME as string) ?? \"unknown\";\n const resolvedServiceVersion = serviceVersion ?? (env.VITE_APP_VERSION as string) ?? \"0.0.0\";\n const resolvedCollectorUrl = collectorUrl ?? \"https://otel.devfellowship.com\";\n const resolvedApiKey = apiKey ?? (env.VITE_OTEL_API_KEY as string);\n const resolvedSentryDsn = sentryDsn ?? (env.VITE_SENTRY_DSN as string);\n const sandboxSlug = env.VITE_SANDBOX_SLUG as string | undefined;\n const sandboxBranch = env.VITE_SANDBOX_BRANCH as string | undefined;\n const resolvedEnvironment = environment ?? (sandboxSlug ? \"sandbox\" : (env.PROD ? \"production\" : \"development\"));\n const isEnabled = enabled ?? (env.VITE_OTEL_ENABLED === \"true\" || env.PROD === true);\n\n useEffect(() => {\n if (!isEnabled) return;\n\n // Initialize Sentry (if DSN provided)\n if (resolvedSentryDsn) {\n initSentry({\n dsn: resolvedSentryDsn,\n serviceName: resolvedServiceName,\n serviceVersion: resolvedServiceVersion,\n environment: resolvedEnvironment,\n sandboxSlug,\n sandboxBranch,\n });\n }\n\n // Initialize OTel SDK\n const providers = initOtelBrowser({\n serviceName: resolvedServiceName,\n serviceVersion: resolvedServiceVersion,\n collectorUrl: resolvedCollectorUrl,\n apiKey: resolvedApiKey,\n sandboxSlug,\n sandboxBranch,\n });\n providersRef.current = providers;\n\n // Fetch instrumentation\n if (enableFetchTracing) {\n const tracer = trace.getTracer(\"@dfl/observability\");\n instrumentFetch(tracer);\n }\n\n // Global error handlers\n const tracer = trace.getTracer(\"@dfl/observability\");\n\n const handleWindowError = (event: ErrorEvent) => {\n tracer.startActiveSpan(\"window.onerror\", (span) => {\n const dynamicAttrs = getDynamicAttributes();\n for (const [key, value] of Object.entries(dynamicAttrs)) {\n span.setAttribute(key, value);\n }\n\n span.setAttribute(\"error.message\", event.message);\n span.setAttribute(\"error.source\", event.filename ?? \"\");\n span.setAttribute(\"error.lineno\", event.lineno ?? 0);\n span.setAttribute(\"error.colno\", event.colno ?? 0);\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: event.message,\n });\n\n if (event.error instanceof Error) {\n span.recordException(event.error);\n captureError(event.error, {\n source: event.filename ?? \"\",\n lineno: String(event.lineno ?? 0),\n });\n }\n\n span.end();\n });\n };\n\n const handleUnhandledRejection = (event: PromiseRejectionEvent) => {\n tracer.startActiveSpan(\"unhandled-rejection\", (span) => {\n const dynamicAttrs = getDynamicAttributes();\n for (const [key, value] of Object.entries(dynamicAttrs)) {\n span.setAttribute(key, value);\n }\n\n const reason = event.reason;\n const message =\n reason instanceof Error ? reason.message : String(reason);\n\n span.setAttribute(\"error.message\", message);\n span.setAttribute(\"error.type\", \"unhandled_promise_rejection\");\n\n span.setStatus({ code: SpanStatusCode.ERROR, message });\n\n if (reason instanceof Error) {\n span.recordException(reason);\n captureError(reason, { type: \"unhandled_promise_rejection\" });\n }\n\n span.end();\n });\n };\n\n window.addEventListener(\"error\", handleWindowError);\n window.addEventListener(\"unhandledrejection\", handleUnhandledRejection);\n\n return () => {\n window.removeEventListener(\"error\", handleWindowError);\n window.removeEventListener(\"unhandledrejection\", handleUnhandledRejection);\n\n if (enableFetchTracing) {\n uninstrumentFetch();\n }\n\n if (providersRef.current) {\n shutdownOtel(providersRef.current);\n providersRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isEnabled]);\n\n // No-op mode: just render children without any instrumentation\n if (!isEnabled) {\n return <>{children}</>;\n }\n\n return (\n <ErrorBoundary>\n <ActiveInstrumentation\n enableWebVitals={enableWebVitals}\n enableQueryTracking={enableQueryTracking}\n >\n {children}\n </ActiveInstrumentation>\n </ErrorBoundary>\n );\n}\n","import { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n WebTracerProvider,\n BatchSpanProcessor,\n} from \"@opentelemetry/sdk-trace-web\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { trace, metrics } from \"@opentelemetry/api\";\nimport { getStaticAttributes } from \"./resource-attributes\";\n\nexport interface OtelBrowserConfig {\n serviceName: string;\n serviceVersion: string;\n collectorUrl: string;\n apiKey?: string;\n sandboxSlug?: string;\n sandboxBranch?: string;\n}\n\nexport interface OtelProviders {\n tracerProvider: WebTracerProvider;\n meterProvider: MeterProvider;\n}\n\nexport function initOtelBrowser(config: OtelBrowserConfig): OtelProviders {\n const { serviceName, serviceVersion, collectorUrl, apiKey, sandboxSlug, sandboxBranch } = config;\n\n const headers: Record<string, string> = {};\n if (apiKey) {\n headers[\"x-api-key\"] = apiKey;\n }\n\n const staticAttributes = getStaticAttributes();\n\n const resource = resourceFromAttributes({\n \"service.name\": serviceName,\n \"service.version\": serviceVersion,\n ...staticAttributes,\n ...(sandboxSlug ? { \"sandbox.slug\": sandboxSlug } : {}),\n ...(sandboxBranch ? { \"sandbox.branch\": sandboxBranch } : {}),\n });\n\n // --- Traces ---\n const traceExporter = new OTLPTraceExporter({\n url: `${collectorUrl}/v1/traces`,\n headers,\n });\n\n const tracerProvider = new WebTracerProvider({\n resource,\n spanProcessors: [new BatchSpanProcessor(traceExporter)],\n });\n\n tracerProvider.register();\n\n // --- Metrics ---\n const metricExporter = new OTLPMetricExporter({\n url: `${collectorUrl}/v1/metrics`,\n headers,\n });\n\n const metricReader = new PeriodicExportingMetricReader({\n exporter: metricExporter,\n exportIntervalMillis: 60_000,\n });\n\n const meterProvider = new MeterProvider({\n resource,\n readers: [metricReader],\n });\n\n metrics.setGlobalMeterProvider(meterProvider);\n\n // Store the collector URL globally so the fetch instrumentation can filter it\n (globalThis as Record<string, unknown>).__otelCollectorUrl = collectorUrl;\n\n return { tracerProvider, meterProvider };\n}\n\nexport function shutdownOtel(providers: OtelProviders): void {\n const { tracerProvider, meterProvider } = providers;\n\n trace.disable();\n metrics.disable();\n\n tracerProvider.shutdown().catch((err) => {\n console.warn(\"[observability] tracer shutdown error:\", err);\n });\n\n meterProvider.shutdown().catch((err) => {\n console.warn(\"[observability] meter shutdown error:\", err);\n });\n}\n","/**\n * Auto-collected resource attributes for browser observability.\n *\n * Static attributes are gathered once at init time.\n * Dynamic attributes change during the session (e.g. route, viewport).\n * User attributes are set after authentication completes.\n */\n\n// ---- Module-level mutable state for post-auth enrichment ----\nlet _userId: string | undefined;\nlet _userRole: string | undefined;\n\nexport function setUserId(id: string): void {\n _userId = id;\n}\n\nexport function setUserRole(role: string): void {\n _userRole = role;\n}\n\n// ---- Session ID (stable per tab session) ----\nfunction getSessionId(): string {\n const key = \"__otel_session_id\";\n let id = sessionStorage.getItem(key);\n if (!id) {\n id = crypto.randomUUID();\n sessionStorage.setItem(key, id);\n }\n return id;\n}\n\n// ---- Browser detection (lightweight, no external lib) ----\ninterface BrowserInfo {\n name: string;\n version: string;\n}\n\nfunction detectBrowser(): BrowserInfo {\n const ua = navigator.userAgent;\n\n if (ua.includes(\"Firefox/\")) {\n const match = ua.match(/Firefox\\/([\\d.]+)/);\n return { name: \"Firefox\", version: match?.[1] ?? \"unknown\" };\n }\n if (ua.includes(\"Edg/\")) {\n const match = ua.match(/Edg\\/([\\d.]+)/);\n return { name: \"Edge\", version: match?.[1] ?? \"unknown\" };\n }\n if (ua.includes(\"Chrome/\") && !ua.includes(\"Edg/\")) {\n const match = ua.match(/Chrome\\/([\\d.]+)/);\n return { name: \"Chrome\", version: match?.[1] ?? \"unknown\" };\n }\n if (ua.includes(\"Safari/\") && !ua.includes(\"Chrome/\")) {\n const match = ua.match(/Version\\/([\\d.]+)/);\n return { name: \"Safari\", version: match?.[1] ?? \"unknown\" };\n }\n\n return { name: \"unknown\", version: \"unknown\" };\n}\n\n// ---- OS detection ----\nfunction detectOS(): string {\n const ua = navigator.userAgent;\n if (ua.includes(\"Win\")) return \"Windows\";\n if (ua.includes(\"Mac\")) return \"macOS\";\n if (ua.includes(\"Linux\")) return \"Linux\";\n if (ua.includes(\"Android\")) return \"Android\";\n if (/iPhone|iPad|iPod/.test(ua)) return \"iOS\";\n return \"unknown\";\n}\n\n// ---- Device type via viewport heuristic ----\nfunction detectDeviceType(): string {\n const width = window.innerWidth;\n if (width < 768) return \"mobile\";\n if (width < 1024) return \"tablet\";\n return \"desktop\";\n}\n\n// ---- Connection info ----\nfunction getEffectiveConnectionType(): string | undefined {\n const nav = navigator as Navigator & {\n connection?: { effectiveType?: string };\n };\n return nav.connection?.effectiveType;\n}\n\n// ---- Public API ----\n\n/**\n * Attributes that are stable for the lifetime of the page.\n * Collected once during OTel SDK initialization.\n */\nexport function getStaticAttributes(): Record<string, string> {\n const browser = detectBrowser();\n const attrs: Record<string, string> = {\n \"browser.name\": browser.name,\n \"browser.version\": browser.version,\n \"os.type\": detectOS(),\n \"device.type\": detectDeviceType(),\n \"session.id\": getSessionId(),\n };\n\n return attrs;\n}\n\n/**\n * Attributes that may change between spans (viewport, route, user, connection).\n * Called per-span or at strategic points.\n */\nexport function getDynamicAttributes(): Record<string, string | number> {\n const attrs: Record<string, string | number> = {\n \"viewport.width\": window.innerWidth,\n \"viewport.height\": window.innerHeight,\n };\n\n if (_userId) attrs[\"user.id\"] = _userId;\n if (_userRole) attrs[\"user.role\"] = _userRole;\n\n const connType = getEffectiveConnectionType();\n if (connType) attrs[\"connection.effectiveType\"] = connType;\n\n // page.route is typically set by the ObservabilityProvider via React Router\n const route = (globalThis as Record<string, unknown>).__otelCurrentRoute;\n if (typeof route === \"string\") attrs[\"page.route\"] = route;\n\n return attrs;\n}\n","import { Tracer, SpanKind, SpanStatusCode } from \"@opentelemetry/api\";\nimport { getDynamicAttributes } from \"./resource-attributes\";\n\ntype NativeFetch = typeof globalThis.fetch;\n\nlet _originalFetch: NativeFetch | null = null;\n\n/**\n * Monkey-patches `window.fetch` to automatically create OTel spans for\n * every outgoing HTTP request.\n *\n * Collector URLs are filtered out to prevent infinite export loops.\n */\nexport function instrumentFetch(tracer: Tracer): void {\n if (_originalFetch) {\n // Already instrumented — avoid double-wrapping.\n return;\n }\n\n _originalFetch = window.fetch.bind(window);\n\n window.fetch = async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n const url =\n input instanceof Request ? input.url : input instanceof URL ? input.href : input;\n const method = init?.method ?? (input instanceof Request ? input.method : \"GET\");\n\n // Filter out OTel collector traffic to avoid infinite loops.\n const collectorUrl = (globalThis as Record<string, unknown>).__otelCollectorUrl;\n if (typeof collectorUrl === \"string\" && url.startsWith(collectorUrl)) {\n return _originalFetch!(input, init);\n }\n\n const dynamicAttrs = getDynamicAttributes();\n\n return tracer.startActiveSpan(\n `HTTP ${method.toUpperCase()}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"http.method\": method.toUpperCase(),\n \"http.url\": url,\n ...dynamicAttrs,\n },\n },\n async (span) => {\n try {\n const response = await _originalFetch!(input, init);\n\n span.setAttribute(\"http.status_code\", response.status);\n\n const contentLength = response.headers.get(\"content-length\");\n if (contentLength) {\n span.setAttribute(\n \"http.response_content_length\",\n parseInt(contentLength, 10),\n );\n }\n\n if (response.status >= 400) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: `HTTP ${response.status}`,\n });\n }\n\n return response;\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"Network request failed\";\n span.setStatus({ code: SpanStatusCode.ERROR, message });\n span.recordException(error instanceof Error ? error : new Error(message));\n throw error;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n\n/**\n * Restores the original `window.fetch`, removing instrumentation.\n */\nexport function uninstrumentFetch(): void {\n if (_originalFetch) {\n window.fetch = _originalFetch;\n _originalFetch = null;\n }\n}\n","import * as Sentry from \"@sentry/react\";\n\nexport interface SentryConfig {\n dsn: string;\n serviceName: string;\n serviceVersion: string;\n environment: string;\n sandboxSlug?: string;\n sandboxBranch?: string;\n}\n\nlet _initialized = false;\n\nexport function initSentry(config: SentryConfig): void {\n if (_initialized) return;\n\n Sentry.init({\n dsn: config.dsn,\n environment: config.environment,\n release: `${config.serviceName}@${config.serviceVersion}`,\n integrations: [\n Sentry.browserTracingIntegration(),\n Sentry.replayIntegration({ maskAllText: false, blockAllMedia: false }),\n ],\n tracesSampleRate: 0.2,\n replaysSessionSampleRate: 0.0,\n replaysOnErrorSampleRate: 1.0,\n beforeSend(event) {\n event.tags = {\n ...event.tags,\n \"service.name\": config.serviceName,\n };\n return event;\n },\n });\n\n Sentry.setTag(\"service.name\", config.serviceName);\n if (config.sandboxSlug) {\n Sentry.setTag(\"sandbox.slug\", config.sandboxSlug);\n }\n if (config.sandboxBranch) {\n Sentry.setTag(\"sandbox.branch\", config.sandboxBranch);\n }\n _initialized = true;\n}\n\nexport function captureError(error: Error, context?: Record<string, string>): void {\n if (!_initialized) return;\n Sentry.withScope((scope) => {\n if (context) {\n for (const [key, value] of Object.entries(context)) {\n scope.setExtra(key, value);\n }\n }\n Sentry.captureException(error);\n });\n}\n\nexport function setSentryUser(id: string, role?: string): void {\n if (!_initialized) return;\n Sentry.setUser({ id, ...(role ? { role } : {}) });\n}\n","import { useEffect, useRef } from \"react\";\nimport { metrics } from \"@opentelemetry/api\";\nimport type { Metric } from \"web-vitals\";\n\n/**\n * Captures Core Web Vitals (LCP, FID, CLS, INP, TTFB) and reports them\n * as OTel histogram metrics via the global MeterProvider.\n *\n * Runs once on mount. The `web-vitals` callbacks fire at most once per\n * metric per page load, so no debouncing is needed.\n */\nexport function useWebVitals(): void {\n const initialized = useRef(false);\n\n useEffect(() => {\n if (initialized.current) return;\n initialized.current = true;\n\n const meter = metrics.getMeter(\"web-vitals\");\n\n const lcpHistogram = meter.createHistogram(\"web_vital.lcp\", {\n description: \"Largest Contentful Paint (ms)\",\n unit: \"ms\",\n });\n const clsHistogram = meter.createHistogram(\"web_vital.cls\", {\n description: \"Cumulative Layout Shift\",\n unit: \"\",\n });\n const inpHistogram = meter.createHistogram(\"web_vital.inp\", {\n description: \"Interaction to Next Paint (ms)\",\n unit: \"ms\",\n });\n const ttfbHistogram = meter.createHistogram(\"web_vital.ttfb\", {\n description: \"Time to First Byte (ms)\",\n unit: \"ms\",\n });\n\n const report = (\n histogram: ReturnType<typeof meter.createHistogram>,\n metric: Metric,\n ) => {\n histogram.record(metric.value, {\n \"metric.id\": metric.id,\n \"metric.rating\": metric.rating,\n });\n };\n\n // Dynamic import so tree-shaking can drop web-vitals entirely when unused.\n import(\"web-vitals\").then(({ onLCP, onCLS, onINP, onTTFB }) => {\n onLCP((m) => report(lcpHistogram, m));\n onCLS((m) => report(clsHistogram, m));\n onINP((m) => report(inpHistogram, m));\n onTTFB((m) => report(ttfbHistogram, m));\n });\n }, []);\n}\n","import { useEffect } from \"react\";\nimport { useQueryClient, type QueryClient } from \"@tanstack/react-query\";\nimport { trace, SpanStatusCode } from \"@opentelemetry/api\";\nimport { getDynamicAttributes } from \"../lib/resource-attributes\";\n\n/**\n * Subscribes to the React Query cache and creates OTel error spans\n * whenever a query transitions to the `error` state.\n *\n * Safe to use outside QueryClientProvider — gracefully no-ops.\n */\nexport function useQueryErrorReporter(): void {\n let queryClient: QueryClient | null = null;\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n queryClient = useQueryClient();\n } catch {\n // Not inside a QueryClientProvider — query error reporting disabled\n }\n\n useEffect(() => {\n if (!queryClient) return;\n const tracer = trace.getTracer(\"@dfl/observability\");\n\n const unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n if (event.type === \"updated\" && event.action.type === \"error\") {\n const query = event.query;\n const error = query.state.error;\n\n tracer.startActiveSpan(\"react-query-error\", (span) => {\n const dynamicAttrs = getDynamicAttributes();\n for (const [key, value] of Object.entries(dynamicAttrs)) {\n span.setAttribute(key, value);\n }\n\n span.setAttribute(\"query.key\", JSON.stringify(query.queryKey));\n span.setAttribute(\n \"error.message\",\n error instanceof Error ? error.message : String(error),\n );\n span.setAttribute(\n \"error.type\",\n error instanceof Error ? error.constructor.name : typeof error,\n );\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message:\n error instanceof Error ? error.message : \"React Query error\",\n });\n\n if (error instanceof Error) {\n span.recordException(error);\n }\n\n span.end();\n });\n }\n });\n\n return () => {\n unsubscribe();\n };\n }, [queryClient]);\n}\n","import React, { Component, ErrorInfo, ReactNode } from \"react\";\nimport { trace, SpanStatusCode } from \"@opentelemetry/api\";\nimport { captureError } from \"./lib/sentry-init\";\n\ninterface ErrorBoundaryProps {\n children: ReactNode;\n fallback?: ReactNode;\n}\n\ninterface ErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * React error boundary that reports uncaught component errors to OTel\n * as error spans before rendering a fallback UI.\n */\nexport class ErrorBoundary extends Component<\n ErrorBoundaryProps,\n ErrorBoundaryState\n> {\n constructor(props: ErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n const tracer = trace.getTracer(\"@dfl/observability\");\n\n tracer.startActiveSpan(\"react-error-boundary\", (span) => {\n span.setAttribute(\"error.message\", error.message);\n span.setAttribute(\"error.stack\", error.stack ?? \"\");\n span.setAttribute(\n \"error.component_stack\",\n errorInfo.componentStack ?? \"\",\n );\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error.message,\n });\n\n span.recordException(error);\n span.end();\n });\n\n captureError(error, {\n component_stack: errorInfo.componentStack ?? \"\",\n });\n }\n\n private handleReload = (): void => {\n window.location.reload();\n };\n\n render(): ReactNode {\n if (this.state.hasError) {\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n return (\n <div className=\"flex min-h-[200px] flex-col items-center justify-center gap-4 rounded-lg border border-red-200 bg-red-50 p-8 text-center dark:border-red-900 dark:bg-red-950\">\n <h2 className=\"text-lg font-semibold text-red-800 dark:text-red-200\">\n Something went wrong\n </h2>\n <p className=\"max-w-md text-sm text-red-600 dark:text-red-400\">\n An unexpected error occurred. Please try reloading the page.\n </p>\n <button\n onClick={this.handleReload}\n className=\"rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:bg-red-700 dark:hover:bg-red-600\"\n >\n Reload page\n </button>\n </div>\n );\n }\n\n return this.props.children;\n }\n}\n","import { trace, SpanStatusCode } from \"@opentelemetry/api\";\nimport { getDynamicAttributes } from \"./resource-attributes\";\n\n/**\n * Emit a custom observability event as an OTel span.\n *\n * Uses the global tracer registered by the ObservabilityProvider,\n * so it can be called from anywhere in the app without prop-drilling.\n *\n * @example\n * ```ts\n * trackEvent(\"button_click\", { \"button.id\": \"checkout\", \"page\": \"/cart\" });\n * ```\n */\nexport function trackEvent(\n name: string,\n attributes?: Record<string, string | number | boolean>,\n): void {\n const tracer = trace.getTracer(\"@dfl/observability\");\n\n tracer.startActiveSpan(name, (span) => {\n const dynamicAttrs = getDynamicAttributes();\n\n span.setAttribute(\"event.name\", name);\n\n // Attach dynamic resource attributes (viewport, user, route, etc.)\n for (const [key, value] of Object.entries(dynamicAttrs)) {\n span.setAttribute(key, value);\n }\n\n // Attach caller-provided attributes\n if (attributes) {\n for (const [key, value] of Object.entries(attributes)) {\n span.setAttribute(key, value);\n }\n }\n\n span.setStatus({ code: SpanStatusCode.OK });\n span.end();\n });\n}\n"],"mappings":"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","names":["useEffect","useRef","trace","SpanStatusCode","init","metrics","useEffect","trace","SpanStatusCode","useEffect","trace","SpanStatusCode","trace","SpanStatusCode","trace","SpanStatusCode","jsx","useRef","useEffect","tracer","trace","SpanStatusCode","trace","SpanStatusCode","trace","SpanStatusCode"]}
|
|
1
|
+
{"version":3,"sources":["../src/ObservabilityProvider.tsx","../src/lib/otel-init.ts","../src/lib/resource-attributes.ts","../src/lib/fetch-instrumentation.ts","../src/lib/sentry-init.ts","../src/lib/scrub.ts","../src/hooks/useWebVitals.ts","../src/hooks/useQueryErrorReporter.ts","../src/ErrorBoundary.tsx","../src/lib/track-event.ts"],"sourcesContent":["import React, { useEffect, useRef, ReactNode } from \"react\";\nimport { trace, SpanStatusCode } from \"@opentelemetry/api\";\nimport {\n initOtelBrowser,\n shutdownOtel,\n type OtelProviders,\n} from \"./lib/otel-init\";\nimport {\n instrumentFetch,\n uninstrumentFetch,\n} from \"./lib/fetch-instrumentation\";\nimport { getDynamicAttributes } from \"./lib/resource-attributes\";\nimport { initSentry, captureError } from \"./lib/sentry-init\";\nimport { useWebVitals } from \"./hooks/useWebVitals\";\nimport { useQueryErrorReporter } from \"./hooks/useQueryErrorReporter\";\nimport { ErrorBoundary } from \"./ErrorBoundary\";\n\nexport interface ObservabilityConfig {\n /** Service name reported to the collector. Falls back to VITE_APP_NAME env var. */\n serviceName?: string;\n /** Service version. Falls back to VITE_APP_VERSION or \"0.0.0\" */\n serviceVersion?: string;\n /**\n * Sentry release id. Pass the git SHA of the build. Falls back to\n * VITE_GIT_SHA, VITE_COMMIT_SHA, then `serviceName@serviceVersion`.\n */\n release?: string;\n /** OTel collector base URL. Default: \"https://otel.devfellowship.com\" */\n collectorUrl?: string;\n /** API key sent as x-api-key header. Falls back to VITE_OTEL_API_KEY */\n apiKey?: string;\n /** Sentry DSN. Falls back to VITE_SENTRY_DSN */\n sentryDsn?: string;\n /** Environment label. Default: \"production\" */\n environment?: string;\n /** Master kill-switch. Falls back to VITE_OTEL_ENABLED or import.meta.env.PROD */\n enabled?: boolean;\n /** Capture Core Web Vitals. Default: true */\n enableWebVitals?: boolean;\n /** Report React Query errors as spans. Default: true */\n enableQueryTracking?: boolean;\n /** Auto-trace all fetch() calls. Default: true */\n enableFetchTracing?: boolean;\n}\n\ninterface ObservabilityProviderProps extends ObservabilityConfig {\n children: ReactNode;\n}\n\n// ---- Internal active-instrumentation wrapper ----\n\nfunction ActiveInstrumentation({\n enableWebVitals,\n enableQueryTracking,\n children,\n}: {\n enableWebVitals: boolean;\n enableQueryTracking: boolean;\n children: ReactNode;\n}) {\n if (enableWebVitals) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useWebVitals();\n }\n\n if (enableQueryTracking) {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useQueryErrorReporter();\n }\n\n return <>{children}</>;\n}\n\n// ---- Main provider ----\n\n// Safe access to Vite env vars (no-op outside Vite)\nfunction getViteEnv(): Record<string, string | boolean | undefined> {\n try {\n return (import.meta as ImportMeta & { env: Record<string, string | boolean> }).env ?? {};\n } catch {\n return {};\n }\n}\n\nexport function ObservabilityProvider({\n serviceName,\n serviceVersion,\n release,\n collectorUrl,\n apiKey,\n sentryDsn,\n environment,\n enabled,\n enableWebVitals = true,\n enableQueryTracking = true,\n enableFetchTracing = true,\n children,\n}: ObservabilityProviderProps) {\n const providersRef = useRef<OtelProviders | null>(null);\n\n const env = getViteEnv();\n const resolvedServiceName = serviceName ?? (env.VITE_APP_NAME as string) ?? \"unknown\";\n const resolvedServiceVersion = serviceVersion ?? (env.VITE_APP_VERSION as string) ?? \"0.0.0\";\n const resolvedRelease =\n release ??\n (env.VITE_GIT_SHA as string | undefined) ??\n (env.VITE_COMMIT_SHA as string | undefined);\n const resolvedCollectorUrl = collectorUrl ?? \"https://otel.devfellowship.com\";\n const resolvedApiKey = apiKey ?? (env.VITE_OTEL_API_KEY as string);\n const resolvedSentryDsn = sentryDsn ?? (env.VITE_SENTRY_DSN as string);\n const sandboxSlug = env.VITE_SANDBOX_SLUG as string | undefined;\n const sandboxBranch = env.VITE_SANDBOX_BRANCH as string | undefined;\n const resolvedEnvironment = environment ?? (sandboxSlug ? \"sandbox\" : (env.PROD ? \"production\" : \"development\"));\n const isEnabled = enabled ?? (env.VITE_OTEL_ENABLED === \"true\" || env.PROD === true);\n\n useEffect(() => {\n if (!isEnabled) return;\n\n // Initialize Sentry (if DSN provided)\n if (resolvedSentryDsn) {\n initSentry({\n dsn: resolvedSentryDsn,\n serviceName: resolvedServiceName,\n serviceVersion: resolvedServiceVersion,\n release: resolvedRelease || undefined,\n environment: resolvedEnvironment,\n sandboxSlug,\n sandboxBranch,\n });\n }\n\n // Initialize OTel SDK\n const providers = initOtelBrowser({\n serviceName: resolvedServiceName,\n serviceVersion: resolvedServiceVersion,\n collectorUrl: resolvedCollectorUrl,\n apiKey: resolvedApiKey,\n sandboxSlug,\n sandboxBranch,\n });\n providersRef.current = providers;\n\n // Fetch instrumentation\n if (enableFetchTracing) {\n const tracer = trace.getTracer(\"@dfl/observability\");\n instrumentFetch(tracer);\n }\n\n // Global error handlers\n const tracer = trace.getTracer(\"@dfl/observability\");\n\n const handleWindowError = (event: ErrorEvent) => {\n tracer.startActiveSpan(\"window.onerror\", (span) => {\n const dynamicAttrs = getDynamicAttributes();\n for (const [key, value] of Object.entries(dynamicAttrs)) {\n span.setAttribute(key, value);\n }\n\n span.setAttribute(\"error.message\", event.message);\n span.setAttribute(\"error.source\", event.filename ?? \"\");\n span.setAttribute(\"error.lineno\", event.lineno ?? 0);\n span.setAttribute(\"error.colno\", event.colno ?? 0);\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: event.message,\n });\n\n if (event.error instanceof Error) {\n span.recordException(event.error);\n captureError(event.error, {\n source: event.filename ?? \"\",\n lineno: String(event.lineno ?? 0),\n });\n }\n\n span.end();\n });\n };\n\n const handleUnhandledRejection = (event: PromiseRejectionEvent) => {\n tracer.startActiveSpan(\"unhandled-rejection\", (span) => {\n const dynamicAttrs = getDynamicAttributes();\n for (const [key, value] of Object.entries(dynamicAttrs)) {\n span.setAttribute(key, value);\n }\n\n const reason = event.reason;\n const message =\n reason instanceof Error ? reason.message : String(reason);\n\n span.setAttribute(\"error.message\", message);\n span.setAttribute(\"error.type\", \"unhandled_promise_rejection\");\n\n span.setStatus({ code: SpanStatusCode.ERROR, message });\n\n if (reason instanceof Error) {\n span.recordException(reason);\n captureError(reason, { type: \"unhandled_promise_rejection\" });\n }\n\n span.end();\n });\n };\n\n window.addEventListener(\"error\", handleWindowError);\n window.addEventListener(\"unhandledrejection\", handleUnhandledRejection);\n\n return () => {\n window.removeEventListener(\"error\", handleWindowError);\n window.removeEventListener(\"unhandledrejection\", handleUnhandledRejection);\n\n if (enableFetchTracing) {\n uninstrumentFetch();\n }\n\n if (providersRef.current) {\n shutdownOtel(providersRef.current);\n providersRef.current = null;\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isEnabled]);\n\n // No-op mode: just render children without any instrumentation\n if (!isEnabled) {\n return <>{children}</>;\n }\n\n return (\n <ErrorBoundary>\n <ActiveInstrumentation\n enableWebVitals={enableWebVitals}\n enableQueryTracking={enableQueryTracking}\n >\n {children}\n </ActiveInstrumentation>\n </ErrorBoundary>\n );\n}\n","import { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n WebTracerProvider,\n BatchSpanProcessor,\n} from \"@opentelemetry/sdk-trace-web\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { trace, metrics } from \"@opentelemetry/api\";\nimport { getStaticAttributes } from \"./resource-attributes\";\n\nexport interface OtelBrowserConfig {\n serviceName: string;\n serviceVersion: string;\n collectorUrl: string;\n apiKey?: string;\n sandboxSlug?: string;\n sandboxBranch?: string;\n}\n\nexport interface OtelProviders {\n tracerProvider: WebTracerProvider;\n meterProvider: MeterProvider;\n}\n\nexport function initOtelBrowser(config: OtelBrowserConfig): OtelProviders {\n const { serviceName, serviceVersion, collectorUrl, apiKey, sandboxSlug, sandboxBranch } = config;\n\n const headers: Record<string, string> = {};\n if (apiKey) {\n headers[\"x-api-key\"] = apiKey;\n }\n\n const staticAttributes = getStaticAttributes();\n\n const resource = resourceFromAttributes({\n \"service.name\": serviceName,\n \"service.version\": serviceVersion,\n ...staticAttributes,\n ...(sandboxSlug ? { \"sandbox.slug\": sandboxSlug } : {}),\n ...(sandboxBranch ? { \"sandbox.branch\": sandboxBranch } : {}),\n });\n\n // --- Traces ---\n const traceExporter = new OTLPTraceExporter({\n url: `${collectorUrl}/v1/traces`,\n headers,\n });\n\n const tracerProvider = new WebTracerProvider({\n resource,\n spanProcessors: [new BatchSpanProcessor(traceExporter)],\n });\n\n tracerProvider.register();\n\n // --- Metrics ---\n const metricExporter = new OTLPMetricExporter({\n url: `${collectorUrl}/v1/metrics`,\n headers,\n });\n\n const metricReader = new PeriodicExportingMetricReader({\n exporter: metricExporter,\n exportIntervalMillis: 60_000,\n });\n\n const meterProvider = new MeterProvider({\n resource,\n readers: [metricReader],\n });\n\n metrics.setGlobalMeterProvider(meterProvider);\n\n // Store the collector URL globally so the fetch instrumentation can filter it\n (globalThis as Record<string, unknown>).__otelCollectorUrl = collectorUrl;\n\n return { tracerProvider, meterProvider };\n}\n\nexport function shutdownOtel(providers: OtelProviders): void {\n const { tracerProvider, meterProvider } = providers;\n\n trace.disable();\n metrics.disable();\n\n tracerProvider.shutdown().catch((err) => {\n console.warn(\"[observability] tracer shutdown error:\", err);\n });\n\n meterProvider.shutdown().catch((err) => {\n console.warn(\"[observability] meter shutdown error:\", err);\n });\n}\n","/**\n * Auto-collected resource attributes for browser observability.\n *\n * Static attributes are gathered once at init time.\n * Dynamic attributes change during the session (e.g. route, viewport).\n * User attributes are set after authentication completes.\n */\n\n// ---- Module-level mutable state for post-auth enrichment ----\nlet _userId: string | undefined;\nlet _userRole: string | undefined;\n\nexport function setUserId(id: string): void {\n _userId = id;\n}\n\nexport function setUserRole(role: string): void {\n _userRole = role;\n}\n\n// ---- Session ID (stable per tab session) ----\nfunction getSessionId(): string {\n const key = \"__otel_session_id\";\n let id = sessionStorage.getItem(key);\n if (!id) {\n id = crypto.randomUUID();\n sessionStorage.setItem(key, id);\n }\n return id;\n}\n\n// ---- Browser detection (lightweight, no external lib) ----\ninterface BrowserInfo {\n name: string;\n version: string;\n}\n\nfunction detectBrowser(): BrowserInfo {\n const ua = navigator.userAgent;\n\n if (ua.includes(\"Firefox/\")) {\n const match = ua.match(/Firefox\\/([\\d.]+)/);\n return { name: \"Firefox\", version: match?.[1] ?? \"unknown\" };\n }\n if (ua.includes(\"Edg/\")) {\n const match = ua.match(/Edg\\/([\\d.]+)/);\n return { name: \"Edge\", version: match?.[1] ?? \"unknown\" };\n }\n if (ua.includes(\"Chrome/\") && !ua.includes(\"Edg/\")) {\n const match = ua.match(/Chrome\\/([\\d.]+)/);\n return { name: \"Chrome\", version: match?.[1] ?? \"unknown\" };\n }\n if (ua.includes(\"Safari/\") && !ua.includes(\"Chrome/\")) {\n const match = ua.match(/Version\\/([\\d.]+)/);\n return { name: \"Safari\", version: match?.[1] ?? \"unknown\" };\n }\n\n return { name: \"unknown\", version: \"unknown\" };\n}\n\n// ---- OS detection ----\nfunction detectOS(): string {\n const ua = navigator.userAgent;\n if (ua.includes(\"Win\")) return \"Windows\";\n if (ua.includes(\"Mac\")) return \"macOS\";\n if (ua.includes(\"Linux\")) return \"Linux\";\n if (ua.includes(\"Android\")) return \"Android\";\n if (/iPhone|iPad|iPod/.test(ua)) return \"iOS\";\n return \"unknown\";\n}\n\n// ---- Device type via viewport heuristic ----\nfunction detectDeviceType(): string {\n const width = window.innerWidth;\n if (width < 768) return \"mobile\";\n if (width < 1024) return \"tablet\";\n return \"desktop\";\n}\n\n// ---- Connection info ----\nfunction getEffectiveConnectionType(): string | undefined {\n const nav = navigator as Navigator & {\n connection?: { effectiveType?: string };\n };\n return nav.connection?.effectiveType;\n}\n\n// ---- Public API ----\n\n/**\n * Attributes that are stable for the lifetime of the page.\n * Collected once during OTel SDK initialization.\n */\nexport function getStaticAttributes(): Record<string, string> {\n const browser = detectBrowser();\n const attrs: Record<string, string> = {\n \"browser.name\": browser.name,\n \"browser.version\": browser.version,\n \"os.type\": detectOS(),\n \"device.type\": detectDeviceType(),\n \"session.id\": getSessionId(),\n };\n\n return attrs;\n}\n\n/**\n * Attributes that may change between spans (viewport, route, user, connection).\n * Called per-span or at strategic points.\n */\nexport function getDynamicAttributes(): Record<string, string | number> {\n const attrs: Record<string, string | number> = {\n \"viewport.width\": window.innerWidth,\n \"viewport.height\": window.innerHeight,\n };\n\n if (_userId) attrs[\"user.id\"] = _userId;\n if (_userRole) attrs[\"user.role\"] = _userRole;\n\n const connType = getEffectiveConnectionType();\n if (connType) attrs[\"connection.effectiveType\"] = connType;\n\n // page.route is typically set by the ObservabilityProvider via React Router\n const route = (globalThis as Record<string, unknown>).__otelCurrentRoute;\n if (typeof route === \"string\") attrs[\"page.route\"] = route;\n\n return attrs;\n}\n","import { Tracer, SpanKind, SpanStatusCode } from \"@opentelemetry/api\";\nimport { getDynamicAttributes } from \"./resource-attributes\";\n\ntype NativeFetch = typeof globalThis.fetch;\n\nlet _originalFetch: NativeFetch | null = null;\n\n/**\n * Monkey-patches `window.fetch` to automatically create OTel spans for\n * every outgoing HTTP request.\n *\n * Collector URLs are filtered out to prevent infinite export loops.\n */\nexport function instrumentFetch(tracer: Tracer): void {\n if (_originalFetch) {\n // Already instrumented — avoid double-wrapping.\n return;\n }\n\n _originalFetch = window.fetch.bind(window);\n\n window.fetch = async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n const url =\n input instanceof Request ? input.url : input instanceof URL ? input.href : input;\n const method = init?.method ?? (input instanceof Request ? input.method : \"GET\");\n\n // Filter out OTel collector traffic to avoid infinite loops.\n const collectorUrl = (globalThis as Record<string, unknown>).__otelCollectorUrl;\n if (typeof collectorUrl === \"string\" && url.startsWith(collectorUrl)) {\n return _originalFetch!(input, init);\n }\n\n const dynamicAttrs = getDynamicAttributes();\n\n return tracer.startActiveSpan(\n `HTTP ${method.toUpperCase()}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"http.method\": method.toUpperCase(),\n \"http.url\": url,\n ...dynamicAttrs,\n },\n },\n async (span) => {\n try {\n const response = await _originalFetch!(input, init);\n\n span.setAttribute(\"http.status_code\", response.status);\n\n const contentLength = response.headers.get(\"content-length\");\n if (contentLength) {\n span.setAttribute(\n \"http.response_content_length\",\n parseInt(contentLength, 10),\n );\n }\n\n if (response.status >= 400) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: `HTTP ${response.status}`,\n });\n }\n\n return response;\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"Network request failed\";\n span.setStatus({ code: SpanStatusCode.ERROR, message });\n span.recordException(error instanceof Error ? error : new Error(message));\n throw error;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n\n/**\n * Restores the original `window.fetch`, removing instrumentation.\n */\nexport function uninstrumentFetch(): void {\n if (_originalFetch) {\n window.fetch = _originalFetch;\n _originalFetch = null;\n }\n}\n","import * as Sentry from \"@sentry/react\";\nimport { scrubBreadcrumbOrDrop, scrubEventOrDrop } from \"./scrub\";\n\nexport interface SentryConfig {\n dsn: string;\n serviceName: string;\n serviceVersion: string;\n /**\n * Release id. Pass the git SHA of the build. When it is missing, the release\n * falls back to `serviceName@serviceVersion`, which does not change per deploy.\n */\n release?: string;\n environment: string;\n sandboxSlug?: string;\n sandboxBranch?: string;\n}\n\nlet _initialized = false;\n\nconst ANON_ID_KEY = \"dfl_obs_anon_id\";\nlet _memoryAnonId: string | undefined;\n\nfunction randomId(): string {\n try {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === \"function\") return c.randomUUID();\n if (c && typeof c.getRandomValues === \"function\") {\n const b = c.getRandomValues(new Uint8Array(16));\n return Array.from(b, (x) => x.toString(16).padStart(2, \"0\")).join(\"\");\n }\n } catch {\n /* fall through */\n }\n return Math.random().toString(16).slice(2) + Date.now().toString(16);\n}\n\n/**\n * A random id per browser. It is not linked to any account, so it counts\n * users without naming one. Never an email, never a name.\n */\nexport function getAnonymousId(): string {\n try {\n const stored = globalThis.localStorage?.getItem(ANON_ID_KEY);\n if (stored) return stored;\n const fresh = randomId();\n globalThis.localStorage?.setItem(ANON_ID_KEY, fresh);\n return fresh;\n } catch {\n return (_memoryAnonId ??= randomId());\n }\n}\n\n/** Sentry options for the DFL fleet. Exported so tests can init with a mock transport. */\nexport function buildSentryOptions(config: SentryConfig): Sentry.BrowserOptions {\n return {\n dsn: config.dsn,\n environment: config.environment,\n release: config.release || `${config.serviceName}@${config.serviceVersion}`,\n // Privacy: no session replay. No replay integration and both rates 0, so no\n // replay envelope is ever sent. Do not add it back without a new decision.\n integrations: [Sentry.browserTracingIntegration()],\n replaysSessionSampleRate: 0,\n replaysOnErrorSampleRate: 0,\n tracesSampleRate: 0.2,\n // The SDK sends no default PII. The server-side IP scrub is a project\n // setting (see README), not an SDK option.\n sendDefaultPii: false,\n beforeSend(event) {\n event.tags = {\n ...event.tags,\n \"service.name\": config.serviceName,\n };\n return scrubEventOrDrop(event);\n },\n beforeSendTransaction(event) {\n return scrubEventOrDrop(event);\n },\n beforeBreadcrumb(crumb) {\n return scrubBreadcrumbOrDrop(crumb);\n },\n };\n}\n\nexport function initSentry(config: SentryConfig): void {\n if (_initialized) return;\n\n Sentry.init(buildSentryOptions(config));\n Sentry.setUser({ id: getAnonymousId() });\n\n Sentry.setTag(\"service.name\", config.serviceName);\n if (config.sandboxSlug) {\n Sentry.setTag(\"sandbox.slug\", config.sandboxSlug);\n }\n if (config.sandboxBranch) {\n Sentry.setTag(\"sandbox.branch\", config.sandboxBranch);\n }\n _initialized = true;\n}\n\nexport function captureError(error: Error, context?: Record<string, string>): void {\n if (!_initialized) return;\n Sentry.withScope((scope) => {\n if (context) {\n for (const [key, value] of Object.entries(context)) {\n scope.setExtra(key, value);\n }\n }\n Sentry.captureException(error);\n });\n}\n\n/**\n * Kept for API compatibility. The account id is NOT sent to Sentry: the user\n * stays the anonymous browser id. Only the role is attached.\n */\nexport function setSentryUser(_id: string, role?: string): void {\n if (!_initialized) return;\n Sentry.setUser({ id: getAnonymousId(), ...(role ? { role } : {}) });\n}\n","/**\n * Telemetry scrubbing for Sentry events, transactions and breadcrumbs.\n *\n * Goal (plan 20261101-fleet-observability-error-loop, T1): no email, no URL\n * query string or fragment, no token or JWT, and no IP address leaves the\n * browser. Error capture itself is not changed: a scrubbed event is still sent.\n *\n * Pure functions with no Sentry import, so they are easy to test.\n */\n\nconst REDACTED = \"[redacted]\";\nconst MAX_DEPTH = 8;\n\n// Keys whose VALUE is always removed, whatever it holds.\nconst SENSITIVE_KEY =\n /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|apikey|api[-_]?key|x-auth-token|x-csrf-token|x-xsrf-token|x-supabase-auth|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|passwd|pwd|jwt|session[-_]?id|email|e-mail|username|user[-_]?name|ip[-_]?address|remote_addr|x-forwarded-for|x-real-ip)$/i;\n\n// Keys that the Sentry SDK fills with its own metadata. We do not walk them.\nconst SKIP_KEYS = new Set([\n \"event_id\",\n \"timestamp\",\n \"start_timestamp\",\n \"release\",\n \"environment\",\n \"platform\",\n \"sdk\",\n \"level\",\n \"type\",\n \"trace_id\",\n \"span_id\",\n \"parent_span_id\",\n \"op\",\n]);\n\nconst EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}/g;\nconst JWT = /\\beyJ[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]*/g;\nconst BEARER = /\\b(Bearer|Basic)\\s+[A-Za-z0-9._~+/=-]{8,}/gi;\nconst IPV4 =\n /\\b(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)){3}\\b/g;\n// key=value pairs that carry a secret, in free text (outside a URL query).\nconst KEY_VALUE_SECRET =\n /\\b(access_token|refresh_token|id_token|token|apikey|api_key|secret|password|authorization)([\"']?\\s*[:=]\\s*[\"']?)[^\\s\"'&,;)}\\]]+/gi;\n// An absolute URL, or a path that starts with \"/\", followed by ? or #.\nconst URL_WITH_QUERY =\n /((?:\\bhttps?:\\/\\/|\\bwss?:\\/\\/)[^\\s\"'<>?#]*|(?<![\\w:/.])\\/[\\w\\-./~%@+]*)[?#][^\\s\"'<>]*/g;\n// An opaque path segment: 32+ chars of hex/base64url, or a UUID. Likely a\n// token, an invite code or an entity id. Removed inside URLs only.\nconst OPAQUE_SEGMENT =\n /\\/(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[A-Za-z0-9_-]{32,})(?=[/?#\\s\"']|$)/g;\n\n/** Remove the query string and the fragment from a URL or path. */\nexport function stripUrl(url: string): string {\n const cut = url.search(/[?#]/);\n const base = cut === -1 ? url : url.slice(0, cut);\n return base.replace(OPAQUE_SEGMENT, \"/:id\");\n}\n\n/** Scrub one free-text string: emails, JWTs, bearer tokens, IPs, URL queries. */\nexport function scrubString(input: string): string {\n if (!input) return input;\n let out = input;\n out = out.replace(JWT, \"[jwt]\");\n out = out.replace(BEARER, \"$1 [token]\");\n out = out.replace(URL_WITH_QUERY, (_m, base: string) => stripUrl(base));\n out = out.replace(KEY_VALUE_SECRET, `$1$2${REDACTED}`);\n out = out.replace(EMAIL, \"[email]\");\n out = out.replace(IPV4, \"[ip]\");\n return out;\n}\n\n/**\n * Thrown by scrubEvent when a value had to change but the property could not be\n * written (a getter-only accessor that is not configurable). The event is then\n * not safe to send. Only key NAMES are kept, never values.\n */\nexport class ScrubIncompleteError extends Error {\n readonly keys: string[];\n constructor(keys: string[]) {\n super(`scrub could not write: ${keys.join(\", \")}`);\n this.name = \"ScrubIncompleteError\";\n this.keys = keys;\n }\n}\n\ninterface ScrubState {\n seen: WeakSet<object>;\n /** Names of the keys whose scrubbed value could not be stored. */\n unwritable: string[];\n}\n\nfunction newState(): ScrubState {\n return { seen: new WeakSet(), unwritable: [] };\n}\n\n// A property can be a getter without a setter (seen in production on\n// 2026-11-04: \"Cannot set property $ of #<Object> which has only a getter\").\n// A plain assignment then throws in strict mode, and the transaction was lost.\nfunction readKey(obj: Record<string, unknown>, key: string): { ok: boolean; value?: unknown } {\n try {\n return { ok: true, value: obj[key] };\n } catch {\n return { ok: false }; // a getter that throws: Sentry cannot read it either\n }\n}\n\n/** Store a scrubbed value. Returns false when the property cannot take it. */\nfunction writeKey(obj: Record<string, unknown>, key: string, value: unknown): boolean {\n try {\n obj[key] = value;\n if (Object.is(obj[key], value)) return true;\n } catch {\n // fall through to the descriptor path\n }\n try {\n const d = Object.getOwnPropertyDescriptor(obj, key);\n if (d && d.configurable) {\n Object.defineProperty(obj, key, {\n value,\n writable: true,\n enumerable: d.enumerable,\n configurable: true,\n });\n return true;\n }\n } catch {\n // not writable at all\n }\n return false;\n}\n\nfunction removeKey(obj: Record<string, unknown>, key: string): boolean {\n try {\n return delete obj[key] || !(key in obj);\n } catch {\n return false;\n }\n}\n\nfunction scrubValue(value: unknown, depth: number, state: ScrubState): unknown {\n if (typeof value === \"string\") return scrubString(value);\n if (value === null || typeof value !== \"object\") return value;\n if (depth >= MAX_DEPTH) return value;\n if (state.seen.has(value as object)) return value;\n state.seen.add(value as object);\n\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const next = scrubValue(value[i], depth + 1, state);\n if (next !== value[i] && !writeKey(value as any, String(i), next)) {\n state.unwritable.push(String(i));\n }\n }\n return value;\n }\n\n const obj = value as Record<string, unknown>;\n for (const key of Object.keys(obj)) {\n if (SENSITIVE_KEY.test(key)) {\n const cur = readKey(obj, key);\n if (cur.ok && cur.value !== undefined && cur.value !== null && !removeKey(obj, key)) {\n state.unwritable.push(key);\n }\n continue;\n }\n if (SKIP_KEYS.has(key)) continue;\n const cur = readKey(obj, key);\n if (!cur.ok) continue;\n const v = cur.value;\n const next =\n typeof v === \"string\" && isUrlKey(key)\n ? scrubString(stripUrl(v))\n : scrubValue(v, depth + 1, state);\n // Write only a value that changed. A nested object comes back as the same\n // reference (it was edited in place), so a getter-only key that holds an\n // object needs no write at all.\n if (next !== v && !writeKey(obj, key, next)) state.unwritable.push(key);\n }\n return obj;\n}\n\nfunction isUrlKey(key: string): boolean {\n return /^(url|from|to|abs_path|filename|referrer|referer|origin_url|http\\.url|url\\.full|http\\.query|url\\.query|http\\.fragment)$/i.test(\n key,\n );\n}\n\n/**\n * Scrub a Sentry error event or transaction event, in place.\n * Returns the same object, so it can be returned from beforeSend.\n * Throws ScrubIncompleteError when a value that needed scrubbing could not be\n * written. The caller must then drop the event (fail closed, see sentry-init).\n */\nexport function scrubEvent<T extends Record<string, any>>(event: T): T {\n const e = event as Record<string, any>;\n\n // User: nothing but the anonymous id (set elsewhere) and a role.\n if (e.user && typeof e.user === \"object\") {\n const { id, role } = e.user as Record<string, unknown>;\n e.user = {\n ...(id !== undefined ? { id } : {}),\n ...(typeof role === \"string\" ? { role } : {}),\n };\n }\n\n // Request: URL without query or fragment. No cookies, no query string.\n if (e.request && typeof e.request === \"object\") {\n const r = e.request as Record<string, any>;\n if (typeof r.url === \"string\") r.url = scrubString(stripUrl(r.url));\n delete r.cookies;\n delete r.query_string;\n if (r.env && typeof r.env === \"object\") delete r.env.REMOTE_ADDR;\n if (r.headers && typeof r.headers === \"object\") {\n for (const k of Object.keys(r.headers)) {\n if (SENSITIVE_KEY.test(k)) {\n delete r.headers[k];\n } else if (/^(referer|referrer|origin)$/i.test(k) && typeof r.headers[k] === \"string\") {\n r.headers[k] = scrubString(stripUrl(r.headers[k]));\n }\n }\n }\n }\n\n if (typeof e.transaction === \"string\") {\n e.transaction = scrubString(stripUrl(e.transaction));\n }\n\n // Everything else: a recursive pass (message, exception values, breadcrumbs,\n // spans, extra, contexts, tags, request data).\n const state = newState();\n scrubValue(e, 0, state);\n\n // Sentry rebuilds user.ip_address from the connection when it is missing.\n // We never send one.\n if (e.user && \"ip_address\" in e.user) delete e.user.ip_address;\n\n if (state.unwritable.length > 0) throw new ScrubIncompleteError(state.unwritable);\n return event;\n}\n\n/** Scrub one breadcrumb, in place. Returns the same object. Throws like scrubEvent. */\nexport function scrubBreadcrumb<T extends Record<string, any>>(crumb: T): T {\n const c = crumb as Record<string, any>;\n if (typeof c.message === \"string\") c.message = scrubString(c.message);\n if (c.data && typeof c.data === \"object\") {\n const state = newState();\n scrubValue(c.data, 0, state);\n if (state.unwritable.length > 0) throw new ScrubIncompleteError(state.unwritable);\n for (const k of [\"url\", \"from\", \"to\"]) {\n if (typeof c.data[k] === \"string\") c.data[k] = scrubString(stripUrl(c.data[k]));\n }\n delete c.data.query;\n delete c.data.body;\n }\n return crumb;\n}\n\nconst _warned = new Set<string>();\n\nfunction warnOnce(what: string, err: unknown): void {\n // Key names only. A name is a code identifier, never user data.\n const detail = err instanceof ScrubIncompleteError ? err.keys.join(\",\") : (err as Error)?.name;\n const id = `${what}:${detail}`;\n if (_warned.has(id)) return;\n _warned.add(id);\n try {\n console.warn(`[observability] ${what} dropped: scrub failed (${detail})`);\n } catch {\n /* no console */\n }\n}\n\n/**\n * Scrub for beforeSend and beforeSendTransaction. Fail closed: when the scrub\n * cannot finish, the event is dropped (null) and never sent half scrubbed. An\n * error thrown here would be caught by the SDK and reported as a new error\n * (the 2026-11-04 incident), so this function never throws.\n */\nexport function scrubEventOrDrop<T extends Record<string, any>>(event: T): T | null {\n try {\n return scrubEvent(event);\n } catch (err) {\n warnOnce(\"event\", err);\n return null;\n }\n}\n\n/** Scrub for beforeBreadcrumb. Same rule: drop the crumb, never throw. */\nexport function scrubBreadcrumbOrDrop<T extends Record<string, any>>(crumb: T): T | null {\n try {\n return scrubBreadcrumb(crumb);\n } catch (err) {\n warnOnce(\"breadcrumb\", err);\n return null;\n }\n}\n","import { useEffect, useRef } from \"react\";\nimport { metrics } from \"@opentelemetry/api\";\nimport type { Metric } from \"web-vitals\";\n\n/**\n * Captures Core Web Vitals (LCP, FID, CLS, INP, TTFB) and reports them\n * as OTel histogram metrics via the global MeterProvider.\n *\n * Runs once on mount. The `web-vitals` callbacks fire at most once per\n * metric per page load, so no debouncing is needed.\n */\nexport function useWebVitals(): void {\n const initialized = useRef(false);\n\n useEffect(() => {\n if (initialized.current) return;\n initialized.current = true;\n\n const meter = metrics.getMeter(\"web-vitals\");\n\n const lcpHistogram = meter.createHistogram(\"web_vital.lcp\", {\n description: \"Largest Contentful Paint (ms)\",\n unit: \"ms\",\n });\n const clsHistogram = meter.createHistogram(\"web_vital.cls\", {\n description: \"Cumulative Layout Shift\",\n unit: \"\",\n });\n const inpHistogram = meter.createHistogram(\"web_vital.inp\", {\n description: \"Interaction to Next Paint (ms)\",\n unit: \"ms\",\n });\n const ttfbHistogram = meter.createHistogram(\"web_vital.ttfb\", {\n description: \"Time to First Byte (ms)\",\n unit: \"ms\",\n });\n\n const report = (\n histogram: ReturnType<typeof meter.createHistogram>,\n metric: Metric,\n ) => {\n histogram.record(metric.value, {\n \"metric.id\": metric.id,\n \"metric.rating\": metric.rating,\n });\n };\n\n // Dynamic import so tree-shaking can drop web-vitals entirely when unused.\n import(\"web-vitals\").then(({ onLCP, onCLS, onINP, onTTFB }) => {\n onLCP((m) => report(lcpHistogram, m));\n onCLS((m) => report(clsHistogram, m));\n onINP((m) => report(inpHistogram, m));\n onTTFB((m) => report(ttfbHistogram, m));\n });\n }, []);\n}\n","import { useEffect } from \"react\";\nimport { useQueryClient, type QueryClient } from \"@tanstack/react-query\";\nimport { trace, SpanStatusCode } from \"@opentelemetry/api\";\nimport { getDynamicAttributes } from \"../lib/resource-attributes\";\n\n/**\n * Subscribes to the React Query cache and creates OTel error spans\n * whenever a query transitions to the `error` state.\n *\n * Safe to use outside QueryClientProvider — gracefully no-ops.\n */\nexport function useQueryErrorReporter(): void {\n let queryClient: QueryClient | null = null;\n try {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n queryClient = useQueryClient();\n } catch {\n // Not inside a QueryClientProvider — query error reporting disabled\n }\n\n useEffect(() => {\n if (!queryClient) return;\n const tracer = trace.getTracer(\"@dfl/observability\");\n\n const unsubscribe = queryClient.getQueryCache().subscribe((event) => {\n if (event.type === \"updated\" && event.action.type === \"error\") {\n const query = event.query;\n const error = query.state.error;\n\n tracer.startActiveSpan(\"react-query-error\", (span) => {\n const dynamicAttrs = getDynamicAttributes();\n for (const [key, value] of Object.entries(dynamicAttrs)) {\n span.setAttribute(key, value);\n }\n\n span.setAttribute(\"query.key\", JSON.stringify(query.queryKey));\n span.setAttribute(\n \"error.message\",\n error instanceof Error ? error.message : String(error),\n );\n span.setAttribute(\n \"error.type\",\n error instanceof Error ? error.constructor.name : typeof error,\n );\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message:\n error instanceof Error ? error.message : \"React Query error\",\n });\n\n if (error instanceof Error) {\n span.recordException(error);\n }\n\n span.end();\n });\n }\n });\n\n return () => {\n unsubscribe();\n };\n }, [queryClient]);\n}\n","import React, { Component, ErrorInfo, ReactNode } from \"react\";\nimport { trace, SpanStatusCode } from \"@opentelemetry/api\";\nimport { captureError } from \"./lib/sentry-init\";\n\ninterface ErrorBoundaryProps {\n children: ReactNode;\n fallback?: ReactNode;\n}\n\ninterface ErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * React error boundary that reports uncaught component errors to OTel\n * as error spans before rendering a fallback UI.\n */\nexport class ErrorBoundary extends Component<\n ErrorBoundaryProps,\n ErrorBoundaryState\n> {\n constructor(props: ErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): ErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo): void {\n const tracer = trace.getTracer(\"@dfl/observability\");\n\n tracer.startActiveSpan(\"react-error-boundary\", (span) => {\n span.setAttribute(\"error.message\", error.message);\n span.setAttribute(\"error.stack\", error.stack ?? \"\");\n span.setAttribute(\n \"error.component_stack\",\n errorInfo.componentStack ?? \"\",\n );\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error.message,\n });\n\n span.recordException(error);\n span.end();\n });\n\n captureError(error, {\n component_stack: errorInfo.componentStack ?? \"\",\n });\n }\n\n private handleReload = (): void => {\n window.location.reload();\n };\n\n render(): ReactNode {\n if (this.state.hasError) {\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n return (\n <div className=\"flex min-h-[200px] flex-col items-center justify-center gap-4 rounded-lg border border-red-200 bg-red-50 p-8 text-center dark:border-red-900 dark:bg-red-950\">\n <h2 className=\"text-lg font-semibold text-red-800 dark:text-red-200\">\n Something went wrong\n </h2>\n <p className=\"max-w-md text-sm text-red-600 dark:text-red-400\">\n An unexpected error occurred. Please try reloading the page.\n </p>\n <button\n onClick={this.handleReload}\n className=\"rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:bg-red-700 dark:hover:bg-red-600\"\n >\n Reload page\n </button>\n </div>\n );\n }\n\n return this.props.children;\n }\n}\n","import { trace, SpanStatusCode } from \"@opentelemetry/api\";\nimport { getDynamicAttributes } from \"./resource-attributes\";\n\n/**\n * Emit a custom observability event as an OTel span.\n *\n * Uses the global tracer registered by the ObservabilityProvider,\n * so it can be called from anywhere in the app without prop-drilling.\n *\n * @example\n * ```ts\n * trackEvent(\"button_click\", { \"button.id\": \"checkout\", \"page\": \"/cart\" });\n * ```\n */\nexport function trackEvent(\n name: string,\n attributes?: Record<string, string | number | boolean>,\n): void {\n const tracer = trace.getTracer(\"@dfl/observability\");\n\n tracer.startActiveSpan(name, (span) => {\n const dynamicAttrs = getDynamicAttributes();\n\n span.setAttribute(\"event.name\", name);\n\n // Attach dynamic resource attributes (viewport, user, route, etc.)\n for (const [key, value] of Object.entries(dynamicAttrs)) {\n span.setAttribute(key, value);\n }\n\n // Attach caller-provided attributes\n if (attributes) {\n for (const [key, value] of Object.entries(attributes)) {\n span.setAttribute(key, value);\n }\n }\n\n span.setStatus({ code: SpanStatusCode.OK });\n span.end();\n });\n}\n"],"mappings":"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","names":["useEffect","useRef","trace","SpanStatusCode","init","cur","metrics","useEffect","trace","SpanStatusCode","useEffect","trace","SpanStatusCode","trace","SpanStatusCode","trace","SpanStatusCode","jsx","useRef","useEffect","tracer","trace","SpanStatusCode","trace","SpanStatusCode","trace","SpanStatusCode"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devfellowship/observability",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Browser observability SDK — OpenTelemetry + Sentry for React apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,15 +18,18 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsup",
|
|
21
|
-
"prepublishOnly": "npm run build"
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"proof:browser": "node tests/browser/run.mjs"
|
|
22
25
|
},
|
|
23
26
|
"dependencies": {
|
|
24
27
|
"@opentelemetry/api": "^1.9.0",
|
|
25
|
-
"@opentelemetry/exporter-metrics-otlp-http": "^0.
|
|
26
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
27
|
-
"@opentelemetry/resources": "^
|
|
28
|
-
"@opentelemetry/sdk-metrics": "^
|
|
29
|
-
"@opentelemetry/sdk-trace-web": "^
|
|
28
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.219.0",
|
|
29
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
|
|
30
|
+
"@opentelemetry/resources": "^2.8.0",
|
|
31
|
+
"@opentelemetry/sdk-metrics": "^2.8.0",
|
|
32
|
+
"@opentelemetry/sdk-trace-web": "^2.8.0",
|
|
30
33
|
"@opentelemetry/semantic-conventions": "^1.30.0",
|
|
31
34
|
"@sentry/react": "^10.47.0",
|
|
32
35
|
"web-vitals": "^4.2.0"
|
|
@@ -43,8 +46,10 @@
|
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@tanstack/react-query": "^5.96.1",
|
|
45
48
|
"@types/react": "^18.3.28",
|
|
49
|
+
"jsdom": "^26.1.0",
|
|
46
50
|
"tsup": "^8.5.1",
|
|
47
|
-
"typescript": "^5.9.3"
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"vitest": "^3.2.7"
|
|
48
53
|
},
|
|
49
54
|
"publishConfig": {
|
|
50
55
|
"access": "public"
|