@watchforge/browser 0.1.1 → 0.1.4
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/CONFIGURATION_GUIDE.md +134 -1
- package/README.md +19 -0
- package/bin/watchforge.js +258 -0
- package/package.json +10 -2
- package/src/client.js +97 -120
- package/src/contexts.js +110 -0
- package/src/express.js +25 -2
- package/src/index.js +7 -2
- package/src/react.js +8 -2
- package/src/replay.js +128 -0
- package/src/stacktrace.js +233 -0
- package/src/transport.js +45 -3
package/src/client.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { sendEvent, parseDsn } from "./transport.js";
|
|
2
|
+
import {
|
|
3
|
+
buildStacktraceFromError,
|
|
4
|
+
enrichStacktraceAsync,
|
|
5
|
+
} from "./stacktrace.js";
|
|
6
|
+
import {
|
|
7
|
+
getSdkMetadata,
|
|
8
|
+
getPageContext,
|
|
9
|
+
getPerformanceContext,
|
|
10
|
+
getNodeServerContext,
|
|
11
|
+
} from "./contexts.js";
|
|
12
|
+
import {
|
|
13
|
+
flushReplayForEvent,
|
|
14
|
+
getReplayContext,
|
|
15
|
+
initReplay,
|
|
16
|
+
} from "./replay.js";
|
|
2
17
|
|
|
3
18
|
let DSN = null;
|
|
4
19
|
let APP_ENV = "production";
|
|
@@ -14,7 +29,7 @@ const isNode =
|
|
|
14
29
|
const MAX_BREADCRUMBS = 100;
|
|
15
30
|
let breadcrumbs = [];
|
|
16
31
|
|
|
17
|
-
function addBreadcrumb(breadcrumb) {
|
|
32
|
+
export function addBreadcrumb(breadcrumb) {
|
|
18
33
|
const entry = {
|
|
19
34
|
level: "info",
|
|
20
35
|
category: "log",
|
|
@@ -145,7 +160,7 @@ function setupBrowserInstrumentation() {
|
|
|
145
160
|
message: String(msg),
|
|
146
161
|
data: { url, line, col },
|
|
147
162
|
});
|
|
148
|
-
captureException(error || msg);
|
|
163
|
+
void captureException(error || msg);
|
|
149
164
|
};
|
|
150
165
|
|
|
151
166
|
window.onunhandledrejection = function (event) {
|
|
@@ -156,7 +171,7 @@ function setupBrowserInstrumentation() {
|
|
|
156
171
|
message: String(event.reason),
|
|
157
172
|
data: {},
|
|
158
173
|
});
|
|
159
|
-
captureException(event.reason);
|
|
174
|
+
void captureException(event.reason);
|
|
160
175
|
};
|
|
161
176
|
|
|
162
177
|
// Console breadcrumbs
|
|
@@ -340,78 +355,40 @@ function setupBrowserInstrumentation() {
|
|
|
340
355
|
}
|
|
341
356
|
}
|
|
342
357
|
|
|
343
|
-
|
|
344
|
-
function buildStacktraceFromError(error) {
|
|
345
|
-
if (!error || !error.stack || typeof error.stack !== "string") {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const stackLines = error.stack.split("\n");
|
|
350
|
-
const frames = [];
|
|
351
|
-
|
|
352
|
-
for (let i = 1; i < stackLines.length; i++) {
|
|
353
|
-
const line = stackLines[i].trim();
|
|
354
|
-
// Examples:
|
|
355
|
-
// at funcName (http://localhost:5173/src/App.tsx:10:15)
|
|
356
|
-
// at http://localhost:5173/assets/index-abc123.js:1:1234
|
|
357
|
-
const match =
|
|
358
|
-
line.match(/^at\s+(.*?)\s+\((.*?):(\d+):(\d+)\)$/) ||
|
|
359
|
-
line.match(/^at\s+(.*?):(\d+):(\d+)$/);
|
|
360
|
-
|
|
361
|
-
if (!match) continue;
|
|
362
|
-
|
|
363
|
-
let functionName;
|
|
364
|
-
let file;
|
|
365
|
-
let lineNo;
|
|
366
|
-
let colNo;
|
|
367
|
-
|
|
368
|
-
if (match.length === 5) {
|
|
369
|
-
functionName = match[1] || "<anonymous>";
|
|
370
|
-
file = match[2];
|
|
371
|
-
lineNo = parseInt(match[3], 10);
|
|
372
|
-
colNo = parseInt(match[4], 10);
|
|
373
|
-
} else {
|
|
374
|
-
functionName = "<anonymous>";
|
|
375
|
-
file = match[1];
|
|
376
|
-
lineNo = parseInt(match[2], 10);
|
|
377
|
-
colNo = parseInt(match[3], 10);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const filename = file.split("/").pop() || file;
|
|
381
|
-
|
|
382
|
-
frames.push({
|
|
383
|
-
filename,
|
|
384
|
-
abs_path: file,
|
|
385
|
-
lineno: lineNo,
|
|
386
|
-
colno: colNo,
|
|
387
|
-
function: functionName,
|
|
388
|
-
module: null,
|
|
389
|
-
context_line: null,
|
|
390
|
-
pre_context: [],
|
|
391
|
-
post_context: [],
|
|
392
|
-
vars: {},
|
|
393
|
-
in_app: true,
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (!frames.length) {
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
358
|
+
import { initTracing } from "./tracing.js";
|
|
400
359
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
360
|
+
async function buildEventContexts(context = {}) {
|
|
361
|
+
const runtime = getRuntimeContext();
|
|
362
|
+
const browser = getBrowserContext();
|
|
363
|
+
const os = getOsContext();
|
|
364
|
+
const device = getDeviceContext();
|
|
365
|
+
const page = getPageContext();
|
|
366
|
+
const performanceCtx = getPerformanceContext();
|
|
367
|
+
const server = await getNodeServerContext();
|
|
404
368
|
|
|
405
|
-
return {
|
|
369
|
+
return {
|
|
370
|
+
...(context.contexts || {}),
|
|
371
|
+
...(runtime ? { runtime } : {}),
|
|
372
|
+
...(browser ? { browser } : {}),
|
|
373
|
+
...(os ? { os } : {}),
|
|
374
|
+
...(device ? { device } : {}),
|
|
375
|
+
...(page ? { page } : {}),
|
|
376
|
+
...(performanceCtx ? { performance: performanceCtx } : {}),
|
|
377
|
+
...(server ? { server } : {}),
|
|
378
|
+
};
|
|
406
379
|
}
|
|
407
380
|
|
|
408
|
-
import { initTracing } from "./tracing.js";
|
|
409
|
-
|
|
410
381
|
export function register({
|
|
411
382
|
dsn,
|
|
412
383
|
app_env = "production",
|
|
413
384
|
release = null,
|
|
414
385
|
debug = false,
|
|
386
|
+
replaysSessionSampleRate = 0,
|
|
387
|
+
replaysOnErrorSampleRate = 0,
|
|
388
|
+
maskAllInputs = true,
|
|
389
|
+
blockClass = "rr-block",
|
|
390
|
+
ignoreClass = "rr-ignore",
|
|
391
|
+
maskTextClass = "rr-mask",
|
|
415
392
|
}) {
|
|
416
393
|
DSN = dsn;
|
|
417
394
|
APP_ENV = app_env;
|
|
@@ -443,16 +420,25 @@ export function register({
|
|
|
443
420
|
// Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
|
|
444
421
|
if (isBrowser) {
|
|
445
422
|
setupBrowserInstrumentation();
|
|
423
|
+
initReplay({
|
|
424
|
+
replaysSessionSampleRate,
|
|
425
|
+
replaysOnErrorSampleRate,
|
|
426
|
+
maskAllInputs,
|
|
427
|
+
blockClass,
|
|
428
|
+
ignoreClass,
|
|
429
|
+
maskTextClass,
|
|
430
|
+
debug,
|
|
431
|
+
});
|
|
446
432
|
}
|
|
447
433
|
|
|
448
434
|
// Node.js: Set up process error handlers
|
|
449
435
|
if (isNode) {
|
|
450
436
|
process.on("uncaughtException", (error) => {
|
|
451
|
-
captureException(error);
|
|
437
|
+
void captureException(error);
|
|
452
438
|
});
|
|
453
|
-
|
|
439
|
+
|
|
454
440
|
process.on("unhandledRejection", (reason) => {
|
|
455
|
-
captureException(reason);
|
|
441
|
+
void captureException(reason);
|
|
456
442
|
});
|
|
457
443
|
}
|
|
458
444
|
}
|
|
@@ -463,10 +449,13 @@ export function init(options) {
|
|
|
463
449
|
return register(options);
|
|
464
450
|
}
|
|
465
451
|
|
|
466
|
-
export function captureException(error, context = {}) {
|
|
452
|
+
export async function captureException(error, context = {}) {
|
|
467
453
|
if (!DSN) return;
|
|
468
454
|
|
|
469
|
-
|
|
455
|
+
let stacktrace = buildStacktraceFromError(error);
|
|
456
|
+
if (stacktrace) {
|
|
457
|
+
stacktrace = await enrichStacktraceAsync(stacktrace);
|
|
458
|
+
}
|
|
470
459
|
|
|
471
460
|
const event = {
|
|
472
461
|
event_id: generateEventId(),
|
|
@@ -476,14 +465,19 @@ export function captureException(error, context = {}) {
|
|
|
476
465
|
stacktrace: stacktrace || error?.stack,
|
|
477
466
|
timestamp: new Date().toISOString(),
|
|
478
467
|
platform: isBrowser ? "javascript" : "node",
|
|
468
|
+
sdk: getSdkMetadata(),
|
|
479
469
|
};
|
|
480
|
-
|
|
481
|
-
|
|
470
|
+
|
|
471
|
+
const replay = flushReplayForEvent(DSN, event.event_id);
|
|
472
|
+
if (replay) {
|
|
473
|
+
event.replay_id = replay.replay_id;
|
|
474
|
+
event.session_id = replay.session_id;
|
|
475
|
+
}
|
|
476
|
+
|
|
482
477
|
if (RELEASE) {
|
|
483
478
|
event.release = RELEASE;
|
|
484
479
|
}
|
|
485
|
-
|
|
486
|
-
// Add context
|
|
480
|
+
|
|
487
481
|
if (context.user) {
|
|
488
482
|
event.user = context.user;
|
|
489
483
|
}
|
|
@@ -496,11 +490,11 @@ export function captureException(error, context = {}) {
|
|
|
496
490
|
if (context.extra) {
|
|
497
491
|
event.extra = context.extra;
|
|
498
492
|
}
|
|
499
|
-
|
|
493
|
+
|
|
500
494
|
if (isBrowser) {
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
|
|
495
|
+
const page = getPageContext();
|
|
496
|
+
const url = page?.url;
|
|
497
|
+
const referrer = page?.referrer;
|
|
504
498
|
if (!event.request && url) {
|
|
505
499
|
event.request = {
|
|
506
500
|
url,
|
|
@@ -512,38 +506,31 @@ export function captureException(error, context = {}) {
|
|
|
512
506
|
}
|
|
513
507
|
event.tags = {
|
|
514
508
|
...(event.tags || {}),
|
|
515
|
-
url: url ||
|
|
516
|
-
referrer: referrer ||
|
|
509
|
+
url: url || event.tags?.url,
|
|
510
|
+
referrer: referrer || event.tags?.referrer,
|
|
511
|
+
page_title: page?.title || event.tags?.page_title,
|
|
517
512
|
};
|
|
518
513
|
}
|
|
519
|
-
// Attach runtime + environment contexts (Sentry-like)
|
|
520
|
-
const runtime = getRuntimeContext();
|
|
521
|
-
const browser = getBrowserContext();
|
|
522
|
-
const os = getOsContext();
|
|
523
|
-
const device = getDeviceContext();
|
|
524
514
|
|
|
525
|
-
event.contexts =
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
...(device ? { device } : {}),
|
|
531
|
-
};
|
|
515
|
+
event.contexts = await buildEventContexts(context);
|
|
516
|
+
const replayContext = getReplayContext();
|
|
517
|
+
if (replayContext) {
|
|
518
|
+
event.contexts.replay = replayContext;
|
|
519
|
+
}
|
|
532
520
|
|
|
533
|
-
// Attach breadcrumbs (copied snapshot so later mutations don't affect this event)
|
|
534
521
|
const bcs = getBreadcrumbsSnapshot();
|
|
535
522
|
if (bcs.length > 0) {
|
|
536
523
|
event.breadcrumbs = bcs;
|
|
537
524
|
}
|
|
538
|
-
|
|
525
|
+
|
|
539
526
|
if (DEBUG) {
|
|
540
527
|
console.log("WatchForge SDK - Capturing exception:", event);
|
|
541
528
|
}
|
|
542
|
-
|
|
529
|
+
|
|
543
530
|
sendEvent(DSN, event);
|
|
544
531
|
}
|
|
545
532
|
|
|
546
|
-
export function captureMessage(message, level = "info", context = {}) {
|
|
533
|
+
export async function captureMessage(message, level = "info", context = {}) {
|
|
547
534
|
if (!DSN) return;
|
|
548
535
|
|
|
549
536
|
const event = {
|
|
@@ -553,14 +540,13 @@ export function captureMessage(message, level = "info", context = {}) {
|
|
|
553
540
|
message,
|
|
554
541
|
timestamp: new Date().toISOString(),
|
|
555
542
|
platform: isBrowser ? "javascript" : "node",
|
|
543
|
+
sdk: getSdkMetadata(),
|
|
556
544
|
};
|
|
557
|
-
|
|
558
|
-
// Add release if set
|
|
545
|
+
|
|
559
546
|
if (RELEASE) {
|
|
560
547
|
event.release = RELEASE;
|
|
561
548
|
}
|
|
562
|
-
|
|
563
|
-
// Add context
|
|
549
|
+
|
|
564
550
|
if (context.user) {
|
|
565
551
|
event.user = context.user;
|
|
566
552
|
}
|
|
@@ -573,10 +559,11 @@ export function captureMessage(message, level = "info", context = {}) {
|
|
|
573
559
|
if (context.extra) {
|
|
574
560
|
event.extra = context.extra;
|
|
575
561
|
}
|
|
562
|
+
|
|
576
563
|
if (isBrowser) {
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
|
|
564
|
+
const page = getPageContext();
|
|
565
|
+
const url = page?.url;
|
|
566
|
+
const referrer = page?.referrer;
|
|
580
567
|
if (!event.request && url) {
|
|
581
568
|
event.request = {
|
|
582
569
|
url,
|
|
@@ -588,33 +575,23 @@ export function captureMessage(message, level = "info", context = {}) {
|
|
|
588
575
|
}
|
|
589
576
|
event.tags = {
|
|
590
577
|
...(event.tags || {}),
|
|
591
|
-
url: url ||
|
|
592
|
-
referrer: referrer ||
|
|
578
|
+
url: url || event.tags?.url,
|
|
579
|
+
referrer: referrer || event.tags?.referrer,
|
|
580
|
+
page_title: page?.title || event.tags?.page_title,
|
|
593
581
|
};
|
|
594
582
|
}
|
|
595
|
-
// Attach runtime + environment contexts
|
|
596
|
-
const runtime = getRuntimeContext();
|
|
597
|
-
const browser = getBrowserContext();
|
|
598
|
-
const os = getOsContext();
|
|
599
|
-
const device = getDeviceContext();
|
|
600
583
|
|
|
601
|
-
event.contexts =
|
|
602
|
-
...(context.contexts || {}),
|
|
603
|
-
...(runtime ? { runtime } : {}),
|
|
604
|
-
...(browser ? { browser } : {}),
|
|
605
|
-
...(os ? { os } : {}),
|
|
606
|
-
...(device ? { device } : {}),
|
|
607
|
-
};
|
|
584
|
+
event.contexts = await buildEventContexts(context);
|
|
608
585
|
|
|
609
586
|
const bcs = getBreadcrumbsSnapshot();
|
|
610
587
|
if (bcs.length > 0) {
|
|
611
588
|
event.breadcrumbs = bcs;
|
|
612
589
|
}
|
|
613
|
-
|
|
590
|
+
|
|
614
591
|
if (DEBUG) {
|
|
615
592
|
console.log("WatchForge SDK - Capturing message:", event);
|
|
616
593
|
}
|
|
617
|
-
|
|
594
|
+
|
|
618
595
|
sendEvent(DSN, event);
|
|
619
596
|
}
|
|
620
597
|
|
package/src/contexts.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event context builders (browser, Node, performance).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const isBrowser = typeof window !== "undefined";
|
|
6
|
+
const isNode =
|
|
7
|
+
typeof process !== "undefined" && process.versions && process.versions.node;
|
|
8
|
+
|
|
9
|
+
export const SDK_NAME = "@watchforge/browser";
|
|
10
|
+
export const SDK_VERSION = "0.1.1";
|
|
11
|
+
|
|
12
|
+
export function getSdkMetadata() {
|
|
13
|
+
return {
|
|
14
|
+
name: SDK_NAME,
|
|
15
|
+
version: SDK_VERSION,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getPageContext() {
|
|
20
|
+
if (!isBrowser) return null;
|
|
21
|
+
|
|
22
|
+
const loc = window.location;
|
|
23
|
+
return {
|
|
24
|
+
url: loc.href,
|
|
25
|
+
path: loc.pathname,
|
|
26
|
+
search: loc.search,
|
|
27
|
+
hash: loc.hash,
|
|
28
|
+
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
29
|
+
title: typeof document !== "undefined" ? document.title || null : null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getPerformanceContext() {
|
|
34
|
+
if (!isBrowser || typeof performance === "undefined") return null;
|
|
35
|
+
|
|
36
|
+
const ctx = {};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
40
|
+
if (nav) {
|
|
41
|
+
ctx.navigation = {
|
|
42
|
+
type: nav.type,
|
|
43
|
+
dom_content_loaded_ms: Math.round(nav.domContentLoadedEventEnd),
|
|
44
|
+
load_event_ms: Math.round(nav.loadEventEnd),
|
|
45
|
+
dom_interactive_ms: Math.round(nav.domInteractive),
|
|
46
|
+
response_end_ms: Math.round(nav.responseEnd),
|
|
47
|
+
transfer_size: nav.transferSize ?? null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// ignore
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const resources = performance
|
|
56
|
+
.getEntriesByType("resource")
|
|
57
|
+
.slice(-20)
|
|
58
|
+
.map((r) => ({
|
|
59
|
+
name: r.name?.slice(0, 200),
|
|
60
|
+
initiator_type: r.initiatorType,
|
|
61
|
+
duration_ms: Math.round(r.duration),
|
|
62
|
+
transfer_size: r.transferSize ?? null,
|
|
63
|
+
}));
|
|
64
|
+
if (resources.length) {
|
|
65
|
+
ctx.resources = resources;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Object.keys(ctx).length ? ctx : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let nodeOsPromise = null;
|
|
75
|
+
|
|
76
|
+
async function getNodeOs() {
|
|
77
|
+
if (!isNode) return null;
|
|
78
|
+
if (!nodeOsPromise) {
|
|
79
|
+
nodeOsPromise = import("os").catch(() => null);
|
|
80
|
+
}
|
|
81
|
+
return nodeOsPromise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function getNodeServerContext() {
|
|
85
|
+
if (!isNode) return null;
|
|
86
|
+
|
|
87
|
+
const os = await getNodeOs();
|
|
88
|
+
if (!os) return null;
|
|
89
|
+
|
|
90
|
+
const mem = process.memoryUsage();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: os.hostname(),
|
|
94
|
+
os: {
|
|
95
|
+
name: os.platform(),
|
|
96
|
+
version: os.release(),
|
|
97
|
+
arch: os.arch(),
|
|
98
|
+
},
|
|
99
|
+
runtime: {
|
|
100
|
+
name: "node",
|
|
101
|
+
version: process.version,
|
|
102
|
+
},
|
|
103
|
+
process: {
|
|
104
|
+
pid: process.pid,
|
|
105
|
+
uptime_seconds: Math.round(process.uptime()),
|
|
106
|
+
memory_rss_mb: Math.round(mem.rss / 1024 / 1024),
|
|
107
|
+
memory_heap_used_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/src/express.js
CHANGED
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
|
|
14
14
|
import { captureException } from "./client.js";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Optional first middleware — records request start time for duration_ms on errors.
|
|
18
|
+
*/
|
|
19
|
+
export function expressRequestMiddleware() {
|
|
20
|
+
return function (req, _res, next) {
|
|
21
|
+
req._watchforgeStart = Date.now();
|
|
22
|
+
next();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export function expressMiddleware() {
|
|
17
27
|
return function (err, req, res, next) {
|
|
18
28
|
// Extract IP address
|
|
@@ -45,17 +55,30 @@ export function expressMiddleware() {
|
|
|
45
55
|
ip: ip,
|
|
46
56
|
};
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
const durationMs =
|
|
59
|
+
typeof req._watchforgeStart === "number"
|
|
60
|
+
? Date.now() - req._watchforgeStart
|
|
61
|
+
: null;
|
|
62
|
+
|
|
63
|
+
void captureException(err, {
|
|
50
64
|
user,
|
|
51
65
|
request,
|
|
52
66
|
tags: {
|
|
53
67
|
framework: "express",
|
|
54
68
|
route: req.route?.path || req.path,
|
|
55
69
|
status_code: res && res.statusCode,
|
|
70
|
+
...(durationMs != null ? { duration_ms: durationMs } : {}),
|
|
56
71
|
},
|
|
57
72
|
extra: {
|
|
58
73
|
params: req.params || {},
|
|
74
|
+
...(durationMs != null ? { duration_ms: durationMs } : {}),
|
|
75
|
+
},
|
|
76
|
+
contexts: {
|
|
77
|
+
express: {
|
|
78
|
+
route: req.route?.path || req.path,
|
|
79
|
+
base_url: req.baseUrl || null,
|
|
80
|
+
duration_ms: durationMs,
|
|
81
|
+
},
|
|
59
82
|
},
|
|
60
83
|
});
|
|
61
84
|
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Export main functions
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
register,
|
|
4
|
+
captureException,
|
|
5
|
+
captureMessage,
|
|
6
|
+
addBreadcrumb,
|
|
7
|
+
} from "./client.js";
|
|
3
8
|
|
|
4
9
|
// Export deprecated init for backward compatibility
|
|
5
10
|
export { init } from "./client.js";
|
|
@@ -8,5 +13,5 @@ export { init } from "./client.js";
|
|
|
8
13
|
export { startTransaction, getCurrentTransaction, finishTransaction, Transaction, Span } from "./tracing.js";
|
|
9
14
|
|
|
10
15
|
// Export framework integrations
|
|
11
|
-
export { expressMiddleware } from "./express.js";
|
|
16
|
+
export { expressMiddleware, expressRequestMiddleware } from "./express.js";
|
|
12
17
|
export { ErrorBoundary } from "./react.js";
|
package/src/react.js
CHANGED
|
@@ -26,12 +26,18 @@ export class ErrorBoundary extends React.Component {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
componentDidCatch(error, errorInfo) {
|
|
29
|
-
captureException(error, {
|
|
29
|
+
void captureException(error, {
|
|
30
30
|
extra: {
|
|
31
31
|
componentStack: errorInfo.componentStack,
|
|
32
32
|
},
|
|
33
33
|
tags: {
|
|
34
|
-
framework:
|
|
34
|
+
framework: "react",
|
|
35
|
+
},
|
|
36
|
+
contexts: {
|
|
37
|
+
react: {
|
|
38
|
+
framework: "react",
|
|
39
|
+
component_stack: errorInfo.componentStack || null,
|
|
40
|
+
},
|
|
35
41
|
},
|
|
36
42
|
});
|
|
37
43
|
}
|
package/src/replay.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { sendReplay } from "./transport.js";
|
|
2
|
+
|
|
3
|
+
const isBrowser = typeof window !== "undefined";
|
|
4
|
+
const MAX_BUFFER_MS = 60 * 1000;
|
|
5
|
+
const MAX_EVENTS = 500;
|
|
6
|
+
|
|
7
|
+
let options = {
|
|
8
|
+
replaysSessionSampleRate: 0,
|
|
9
|
+
replaysOnErrorSampleRate: 0,
|
|
10
|
+
maskAllInputs: true,
|
|
11
|
+
blockClass: "rr-block",
|
|
12
|
+
ignoreClass: "rr-ignore",
|
|
13
|
+
maskTextClass: "rr-mask",
|
|
14
|
+
};
|
|
15
|
+
let stopRecording = null;
|
|
16
|
+
let events = [];
|
|
17
|
+
let replayId = null;
|
|
18
|
+
let sessionId = null;
|
|
19
|
+
let startedAt = null;
|
|
20
|
+
let sessionSampled = false;
|
|
21
|
+
|
|
22
|
+
function uuid() {
|
|
23
|
+
if (isBrowser && crypto?.randomUUID) return crypto.randomUUID();
|
|
24
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
25
|
+
const r = (Math.random() * 16) | 0;
|
|
26
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
27
|
+
return v.toString(16);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shouldSample(rate) {
|
|
32
|
+
const n = Number(rate || 0);
|
|
33
|
+
if (n <= 0) return false;
|
|
34
|
+
if (n >= 1) return true;
|
|
35
|
+
return Math.random() < n;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function trimBuffer(now = Date.now()) {
|
|
39
|
+
events = events.filter((event) => now - event.timestamp <= MAX_BUFFER_MS);
|
|
40
|
+
if (events.length > MAX_EVENTS) {
|
|
41
|
+
events = events.slice(-MAX_EVENTS);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function initReplay(config = {}) {
|
|
46
|
+
if (!isBrowser) return;
|
|
47
|
+
|
|
48
|
+
options = {
|
|
49
|
+
...options,
|
|
50
|
+
...config,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
sessionSampled = shouldSample(options.replaysSessionSampleRate);
|
|
54
|
+
const shouldRecord = sessionSampled || Number(options.replaysOnErrorSampleRate || 0) > 0;
|
|
55
|
+
|
|
56
|
+
if (!shouldRecord || stopRecording) return;
|
|
57
|
+
|
|
58
|
+
replayId = replayId || uuid();
|
|
59
|
+
sessionId = sessionId || uuid();
|
|
60
|
+
startedAt = new Date().toISOString();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const { record } = await import("rrweb");
|
|
64
|
+
stopRecording = record({
|
|
65
|
+
emit(event) {
|
|
66
|
+
events.push(event);
|
|
67
|
+
trimBuffer(event.timestamp || Date.now());
|
|
68
|
+
},
|
|
69
|
+
maskAllInputs: options.maskAllInputs,
|
|
70
|
+
blockClass: options.blockClass,
|
|
71
|
+
ignoreClass: options.ignoreClass,
|
|
72
|
+
maskTextClass: options.maskTextClass,
|
|
73
|
+
maskInputOptions: {
|
|
74
|
+
password: true,
|
|
75
|
+
email: true,
|
|
76
|
+
tel: true,
|
|
77
|
+
text: Boolean(options.maskAllInputs),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (options.debug) {
|
|
82
|
+
console.warn("WatchForge SDK: failed to start replay recording", error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getReplayContext() {
|
|
88
|
+
if (!isBrowser || !replayId || !sessionId) return null;
|
|
89
|
+
return {
|
|
90
|
+
replay_id: replayId,
|
|
91
|
+
session_id: sessionId,
|
|
92
|
+
sampled: Boolean(stopRecording),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function flushReplayForEvent(dsn, eventId) {
|
|
97
|
+
if (!isBrowser || !dsn || !replayId || !sessionId || events.length === 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!sessionSampled && !shouldSample(options.replaysOnErrorSampleRate)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
trimBuffer();
|
|
106
|
+
|
|
107
|
+
const payload = {
|
|
108
|
+
replay_id: replayId,
|
|
109
|
+
session_id: sessionId,
|
|
110
|
+
event_id: eventId,
|
|
111
|
+
started_at: startedAt,
|
|
112
|
+
finished_at: new Date().toISOString(),
|
|
113
|
+
url: window.location.href,
|
|
114
|
+
user_agent: navigator.userAgent,
|
|
115
|
+
events,
|
|
116
|
+
sdk: {
|
|
117
|
+
name: "@watchforge/browser",
|
|
118
|
+
version: "0.1.3",
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
sendReplay(dsn, payload);
|
|
123
|
+
return {
|
|
124
|
+
replay_id: replayId,
|
|
125
|
+
session_id: sessionId,
|
|
126
|
+
event_count: events.length,
|
|
127
|
+
};
|
|
128
|
+
}
|