@watchforge/browser 0.1.1 → 0.1.3
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/package.json +1 -1
- package/src/client.js +67 -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/stacktrace.js +231 -0
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
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";
|
|
2
12
|
|
|
3
13
|
let DSN = null;
|
|
4
14
|
let APP_ENV = "production";
|
|
@@ -14,7 +24,7 @@ const isNode =
|
|
|
14
24
|
const MAX_BREADCRUMBS = 100;
|
|
15
25
|
let breadcrumbs = [];
|
|
16
26
|
|
|
17
|
-
function addBreadcrumb(breadcrumb) {
|
|
27
|
+
export function addBreadcrumb(breadcrumb) {
|
|
18
28
|
const entry = {
|
|
19
29
|
level: "info",
|
|
20
30
|
category: "log",
|
|
@@ -145,7 +155,7 @@ function setupBrowserInstrumentation() {
|
|
|
145
155
|
message: String(msg),
|
|
146
156
|
data: { url, line, col },
|
|
147
157
|
});
|
|
148
|
-
captureException(error || msg);
|
|
158
|
+
void captureException(error || msg);
|
|
149
159
|
};
|
|
150
160
|
|
|
151
161
|
window.onunhandledrejection = function (event) {
|
|
@@ -156,7 +166,7 @@ function setupBrowserInstrumentation() {
|
|
|
156
166
|
message: String(event.reason),
|
|
157
167
|
data: {},
|
|
158
168
|
});
|
|
159
|
-
captureException(event.reason);
|
|
169
|
+
void captureException(event.reason);
|
|
160
170
|
};
|
|
161
171
|
|
|
162
172
|
// Console breadcrumbs
|
|
@@ -340,73 +350,29 @@ function setupBrowserInstrumentation() {
|
|
|
340
350
|
}
|
|
341
351
|
}
|
|
342
352
|
|
|
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
|
-
}
|
|
353
|
+
import { initTracing } from "./tracing.js";
|
|
400
354
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
355
|
+
async function buildEventContexts(context = {}) {
|
|
356
|
+
const runtime = getRuntimeContext();
|
|
357
|
+
const browser = getBrowserContext();
|
|
358
|
+
const os = getOsContext();
|
|
359
|
+
const device = getDeviceContext();
|
|
360
|
+
const page = getPageContext();
|
|
361
|
+
const performanceCtx = getPerformanceContext();
|
|
362
|
+
const server = await getNodeServerContext();
|
|
404
363
|
|
|
405
|
-
return {
|
|
364
|
+
return {
|
|
365
|
+
...(context.contexts || {}),
|
|
366
|
+
...(runtime ? { runtime } : {}),
|
|
367
|
+
...(browser ? { browser } : {}),
|
|
368
|
+
...(os ? { os } : {}),
|
|
369
|
+
...(device ? { device } : {}),
|
|
370
|
+
...(page ? { page } : {}),
|
|
371
|
+
...(performanceCtx ? { performance: performanceCtx } : {}),
|
|
372
|
+
...(server ? { server } : {}),
|
|
373
|
+
};
|
|
406
374
|
}
|
|
407
375
|
|
|
408
|
-
import { initTracing } from "./tracing.js";
|
|
409
|
-
|
|
410
376
|
export function register({
|
|
411
377
|
dsn,
|
|
412
378
|
app_env = "production",
|
|
@@ -448,11 +414,11 @@ export function register({
|
|
|
448
414
|
// Node.js: Set up process error handlers
|
|
449
415
|
if (isNode) {
|
|
450
416
|
process.on("uncaughtException", (error) => {
|
|
451
|
-
captureException(error);
|
|
417
|
+
void captureException(error);
|
|
452
418
|
});
|
|
453
|
-
|
|
419
|
+
|
|
454
420
|
process.on("unhandledRejection", (reason) => {
|
|
455
|
-
captureException(reason);
|
|
421
|
+
void captureException(reason);
|
|
456
422
|
});
|
|
457
423
|
}
|
|
458
424
|
}
|
|
@@ -463,10 +429,13 @@ export function init(options) {
|
|
|
463
429
|
return register(options);
|
|
464
430
|
}
|
|
465
431
|
|
|
466
|
-
export function captureException(error, context = {}) {
|
|
432
|
+
export async function captureException(error, context = {}) {
|
|
467
433
|
if (!DSN) return;
|
|
468
434
|
|
|
469
|
-
|
|
435
|
+
let stacktrace = buildStacktraceFromError(error);
|
|
436
|
+
if (stacktrace) {
|
|
437
|
+
stacktrace = await enrichStacktraceAsync(stacktrace);
|
|
438
|
+
}
|
|
470
439
|
|
|
471
440
|
const event = {
|
|
472
441
|
event_id: generateEventId(),
|
|
@@ -476,14 +445,13 @@ export function captureException(error, context = {}) {
|
|
|
476
445
|
stacktrace: stacktrace || error?.stack,
|
|
477
446
|
timestamp: new Date().toISOString(),
|
|
478
447
|
platform: isBrowser ? "javascript" : "node",
|
|
448
|
+
sdk: getSdkMetadata(),
|
|
479
449
|
};
|
|
480
|
-
|
|
481
|
-
// Add release if set
|
|
450
|
+
|
|
482
451
|
if (RELEASE) {
|
|
483
452
|
event.release = RELEASE;
|
|
484
453
|
}
|
|
485
|
-
|
|
486
|
-
// Add context
|
|
454
|
+
|
|
487
455
|
if (context.user) {
|
|
488
456
|
event.user = context.user;
|
|
489
457
|
}
|
|
@@ -496,11 +464,11 @@ export function captureException(error, context = {}) {
|
|
|
496
464
|
if (context.extra) {
|
|
497
465
|
event.extra = context.extra;
|
|
498
466
|
}
|
|
499
|
-
|
|
467
|
+
|
|
500
468
|
if (isBrowser) {
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
|
|
469
|
+
const page = getPageContext();
|
|
470
|
+
const url = page?.url;
|
|
471
|
+
const referrer = page?.referrer;
|
|
504
472
|
if (!event.request && url) {
|
|
505
473
|
event.request = {
|
|
506
474
|
url,
|
|
@@ -512,38 +480,27 @@ export function captureException(error, context = {}) {
|
|
|
512
480
|
}
|
|
513
481
|
event.tags = {
|
|
514
482
|
...(event.tags || {}),
|
|
515
|
-
url: url ||
|
|
516
|
-
referrer: referrer ||
|
|
483
|
+
url: url || event.tags?.url,
|
|
484
|
+
referrer: referrer || event.tags?.referrer,
|
|
485
|
+
page_title: page?.title || event.tags?.page_title,
|
|
517
486
|
};
|
|
518
487
|
}
|
|
519
|
-
// Attach runtime + environment contexts (Sentry-like)
|
|
520
|
-
const runtime = getRuntimeContext();
|
|
521
|
-
const browser = getBrowserContext();
|
|
522
|
-
const os = getOsContext();
|
|
523
|
-
const device = getDeviceContext();
|
|
524
488
|
|
|
525
|
-
event.contexts =
|
|
526
|
-
...(context.contexts || {}),
|
|
527
|
-
...(runtime ? { runtime } : {}),
|
|
528
|
-
...(browser ? { browser } : {}),
|
|
529
|
-
...(os ? { os } : {}),
|
|
530
|
-
...(device ? { device } : {}),
|
|
531
|
-
};
|
|
489
|
+
event.contexts = await buildEventContexts(context);
|
|
532
490
|
|
|
533
|
-
// Attach breadcrumbs (copied snapshot so later mutations don't affect this event)
|
|
534
491
|
const bcs = getBreadcrumbsSnapshot();
|
|
535
492
|
if (bcs.length > 0) {
|
|
536
493
|
event.breadcrumbs = bcs;
|
|
537
494
|
}
|
|
538
|
-
|
|
495
|
+
|
|
539
496
|
if (DEBUG) {
|
|
540
497
|
console.log("WatchForge SDK - Capturing exception:", event);
|
|
541
498
|
}
|
|
542
|
-
|
|
499
|
+
|
|
543
500
|
sendEvent(DSN, event);
|
|
544
501
|
}
|
|
545
502
|
|
|
546
|
-
export function captureMessage(message, level = "info", context = {}) {
|
|
503
|
+
export async function captureMessage(message, level = "info", context = {}) {
|
|
547
504
|
if (!DSN) return;
|
|
548
505
|
|
|
549
506
|
const event = {
|
|
@@ -553,14 +510,13 @@ export function captureMessage(message, level = "info", context = {}) {
|
|
|
553
510
|
message,
|
|
554
511
|
timestamp: new Date().toISOString(),
|
|
555
512
|
platform: isBrowser ? "javascript" : "node",
|
|
513
|
+
sdk: getSdkMetadata(),
|
|
556
514
|
};
|
|
557
|
-
|
|
558
|
-
// Add release if set
|
|
515
|
+
|
|
559
516
|
if (RELEASE) {
|
|
560
517
|
event.release = RELEASE;
|
|
561
518
|
}
|
|
562
|
-
|
|
563
|
-
// Add context
|
|
519
|
+
|
|
564
520
|
if (context.user) {
|
|
565
521
|
event.user = context.user;
|
|
566
522
|
}
|
|
@@ -573,10 +529,11 @@ export function captureMessage(message, level = "info", context = {}) {
|
|
|
573
529
|
if (context.extra) {
|
|
574
530
|
event.extra = context.extra;
|
|
575
531
|
}
|
|
532
|
+
|
|
576
533
|
if (isBrowser) {
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
|
|
534
|
+
const page = getPageContext();
|
|
535
|
+
const url = page?.url;
|
|
536
|
+
const referrer = page?.referrer;
|
|
580
537
|
if (!event.request && url) {
|
|
581
538
|
event.request = {
|
|
582
539
|
url,
|
|
@@ -588,33 +545,23 @@ export function captureMessage(message, level = "info", context = {}) {
|
|
|
588
545
|
}
|
|
589
546
|
event.tags = {
|
|
590
547
|
...(event.tags || {}),
|
|
591
|
-
url: url ||
|
|
592
|
-
referrer: referrer ||
|
|
548
|
+
url: url || event.tags?.url,
|
|
549
|
+
referrer: referrer || event.tags?.referrer,
|
|
550
|
+
page_title: page?.title || event.tags?.page_title,
|
|
593
551
|
};
|
|
594
552
|
}
|
|
595
|
-
// Attach runtime + environment contexts
|
|
596
|
-
const runtime = getRuntimeContext();
|
|
597
|
-
const browser = getBrowserContext();
|
|
598
|
-
const os = getOsContext();
|
|
599
|
-
const device = getDeviceContext();
|
|
600
553
|
|
|
601
|
-
event.contexts =
|
|
602
|
-
...(context.contexts || {}),
|
|
603
|
-
...(runtime ? { runtime } : {}),
|
|
604
|
-
...(browser ? { browser } : {}),
|
|
605
|
-
...(os ? { os } : {}),
|
|
606
|
-
...(device ? { device } : {}),
|
|
607
|
-
};
|
|
554
|
+
event.contexts = await buildEventContexts(context);
|
|
608
555
|
|
|
609
556
|
const bcs = getBreadcrumbsSnapshot();
|
|
610
557
|
if (bcs.length > 0) {
|
|
611
558
|
event.breadcrumbs = bcs;
|
|
612
559
|
}
|
|
613
|
-
|
|
560
|
+
|
|
614
561
|
if (DEBUG) {
|
|
615
562
|
console.log("WatchForge SDK - Capturing message:", event);
|
|
616
563
|
}
|
|
617
|
-
|
|
564
|
+
|
|
618
565
|
sendEvent(DSN, event);
|
|
619
566
|
}
|
|
620
567
|
|
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
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack trace parsing and source context enrichment (Python SDK parity).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const CONTEXT_LINES = 5;
|
|
6
|
+
const MAX_SOURCE_FILE_BYTES = 512 * 1024;
|
|
7
|
+
|
|
8
|
+
const isBrowser = typeof window !== "undefined";
|
|
9
|
+
const isNode =
|
|
10
|
+
typeof process !== "undefined" && process.versions && process.versions.node;
|
|
11
|
+
|
|
12
|
+
const LIBRARY_PATH_PATTERNS = [
|
|
13
|
+
"/node_modules/",
|
|
14
|
+
"node_modules\\",
|
|
15
|
+
"/webpack/",
|
|
16
|
+
"webpack-internal://",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function isLibraryPath(filePath) {
|
|
20
|
+
if (!filePath) return false;
|
|
21
|
+
return LIBRARY_PATH_PATTERNS.some((p) => filePath.includes(p));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseStackLine(line) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed.startsWith("at ")) return null;
|
|
27
|
+
|
|
28
|
+
const withFn = trimmed.match(/^at\s+(.*?)\s+\((.*?):(\d+):(\d+)\)$/);
|
|
29
|
+
if (withFn) {
|
|
30
|
+
return {
|
|
31
|
+
function: withFn[1] || "<anonymous>",
|
|
32
|
+
file: withFn[2],
|
|
33
|
+
lineno: parseInt(withFn[3], 10),
|
|
34
|
+
colno: parseInt(withFn[4], 10),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const withoutFn = trimmed.match(/^at\s+(.*?):(\d+):(\d+)$/);
|
|
39
|
+
if (withoutFn) {
|
|
40
|
+
return {
|
|
41
|
+
function: "<anonymous>",
|
|
42
|
+
file: withoutFn[1],
|
|
43
|
+
lineno: parseInt(withoutFn[2], 10),
|
|
44
|
+
colno: parseInt(withoutFn[3], 10),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const evalMatch = trimmed.match(/^at\s+(.*?)\s+\((.*?)\)$/);
|
|
49
|
+
if (evalMatch) {
|
|
50
|
+
const inner = parseStackLine(`at ${evalMatch[2]}`);
|
|
51
|
+
if (inner) {
|
|
52
|
+
return { ...inner, function: evalMatch[1] || inner.function };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildStacktraceFromError(error) {
|
|
60
|
+
if (!error || !error.stack || typeof error.stack !== "string") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const stackLines = error.stack.split("\n");
|
|
65
|
+
const frames = [];
|
|
66
|
+
|
|
67
|
+
for (let i = 1; i < stackLines.length; i++) {
|
|
68
|
+
const parsed = parseStackLine(stackLines[i]);
|
|
69
|
+
if (!parsed) continue;
|
|
70
|
+
|
|
71
|
+
const { function: functionName, file, lineno, colno } = parsed;
|
|
72
|
+
const filename = file.split("/").pop()?.split("?")[0] || file;
|
|
73
|
+
const inApp = !isLibraryPath(file);
|
|
74
|
+
|
|
75
|
+
frames.push({
|
|
76
|
+
filename,
|
|
77
|
+
abs_path: file,
|
|
78
|
+
lineno,
|
|
79
|
+
colno,
|
|
80
|
+
function: functionName,
|
|
81
|
+
module: null,
|
|
82
|
+
context_line: null,
|
|
83
|
+
pre_context: [],
|
|
84
|
+
post_context: [],
|
|
85
|
+
vars: {},
|
|
86
|
+
in_app: inApp,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!frames.length) return null;
|
|
91
|
+
|
|
92
|
+
frames.reverse();
|
|
93
|
+
return { frames };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function applySourceContext(frame, sourceLines, lineno) {
|
|
97
|
+
if (!sourceLines?.length || !lineno || lineno < 1) return;
|
|
98
|
+
|
|
99
|
+
const idx = lineno - 1;
|
|
100
|
+
if (idx >= sourceLines.length) return;
|
|
101
|
+
|
|
102
|
+
frame.context_line = sourceLines[idx].replace(/\r$/, "");
|
|
103
|
+
const preStart = Math.max(0, idx - CONTEXT_LINES);
|
|
104
|
+
frame.pre_context = sourceLines
|
|
105
|
+
.slice(preStart, idx)
|
|
106
|
+
.map((l) => l.replace(/\r$/, ""));
|
|
107
|
+
const postEnd = Math.min(sourceLines.length, idx + CONTEXT_LINES + 1);
|
|
108
|
+
frame.post_context = sourceLines
|
|
109
|
+
.slice(idx + 1, postEnd)
|
|
110
|
+
.map((l) => l.replace(/\r$/, ""));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let nodeModulesPromise = null;
|
|
114
|
+
|
|
115
|
+
function getNodeModules() {
|
|
116
|
+
if (!isNode) return Promise.resolve(null);
|
|
117
|
+
if (!nodeModulesPromise) {
|
|
118
|
+
nodeModulesPromise = (async () => {
|
|
119
|
+
try {
|
|
120
|
+
const { createRequire } = await import("module");
|
|
121
|
+
const { fileURLToPath } = await import("url");
|
|
122
|
+
const req = createRequire(fileURLToPath(import.meta.url));
|
|
123
|
+
return {
|
|
124
|
+
fs: req("fs"),
|
|
125
|
+
path: req("path"),
|
|
126
|
+
fileURLToPath,
|
|
127
|
+
};
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
}
|
|
133
|
+
return nodeModulesPromise;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function readNodeSourceLines(absPath) {
|
|
137
|
+
if (!isNode || !absPath) return null;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
if (absPath.startsWith("http://") || absPath.startsWith("https://")) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const mods = await getNodeModules();
|
|
145
|
+
if (!mods) return null;
|
|
146
|
+
|
|
147
|
+
const { fs, path, fileURLToPath } = mods;
|
|
148
|
+
let filePath = absPath;
|
|
149
|
+
|
|
150
|
+
if (filePath.startsWith("file://")) {
|
|
151
|
+
filePath = fileURLToPath(filePath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!path.isAbsolute(filePath)) {
|
|
155
|
+
filePath = path.resolve(process.cwd(), filePath);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(filePath)) return null;
|
|
159
|
+
|
|
160
|
+
const stat = fs.statSync(filePath);
|
|
161
|
+
if (stat.size > MAX_SOURCE_FILE_BYTES) return null;
|
|
162
|
+
|
|
163
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
164
|
+
return content.split("\n");
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function enrichFrameWithNodeSource(frame) {
|
|
171
|
+
if (!frame.in_app || !frame.lineno) return;
|
|
172
|
+
const lines = await readNodeSourceLines(frame.abs_path);
|
|
173
|
+
if (lines) applySourceContext(frame, lines, frame.lineno);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function fetchBrowserSourceLines(absPath) {
|
|
177
|
+
if (!isBrowser || !absPath) return null;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
let url = absPath;
|
|
181
|
+
if (url.startsWith("webpack-internal://")) {
|
|
182
|
+
url = url.replace("webpack-internal:///", "/").replace("webpack-internal://", "/");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
186
|
+
if (url.startsWith("/")) {
|
|
187
|
+
url = `${window.location.origin}${url}`;
|
|
188
|
+
} else {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const target = new URL(url, window.location.href);
|
|
194
|
+
if (target.origin !== window.location.origin) return null;
|
|
195
|
+
|
|
196
|
+
const response = await fetch(target.href, { credentials: "same-origin" });
|
|
197
|
+
if (!response.ok) return null;
|
|
198
|
+
|
|
199
|
+
const contentType = response.headers.get("content-type") || "";
|
|
200
|
+
if (!contentType.includes("javascript") && !contentType.includes("text")) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const text = await response.text();
|
|
205
|
+
if (text.length > MAX_SOURCE_FILE_BYTES) return null;
|
|
206
|
+
return text.split("\n");
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function enrichFrameWithBrowserSource(frame) {
|
|
213
|
+
if (!frame.in_app || !frame.lineno) return;
|
|
214
|
+
const lines = await fetchBrowserSourceLines(frame.abs_path);
|
|
215
|
+
if (lines) applySourceContext(frame, lines, frame.lineno);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function enrichStacktraceAsync(stacktrace) {
|
|
219
|
+
if (!stacktrace?.frames?.length) return stacktrace;
|
|
220
|
+
|
|
221
|
+
const inAppFrames = stacktrace.frames.filter((f) => f.in_app);
|
|
222
|
+
const targets = inAppFrames.slice(-5);
|
|
223
|
+
|
|
224
|
+
if (isNode) {
|
|
225
|
+
await Promise.all(targets.map((frame) => enrichFrameWithNodeSource(frame)));
|
|
226
|
+
} else if (isBrowser) {
|
|
227
|
+
await Promise.all(targets.map((frame) => enrichFrameWithBrowserSource(frame)));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return stacktrace;
|
|
231
|
+
}
|