@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@watchforge/browser",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "main": "./src/index.js",
5
5
  "description": "WatchForge JavaScript SDK for Node.js, Express.js, and React",
6
6
  "license": "MIT",
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
- // Parse a JavaScript Error.stack string into structured frames compatible with backend
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
- // Backend expects oldest frame first, newest last; JS stacks are newest first,
402
- // so we reverse to be consistent with Python-style traces.
403
- frames.reverse();
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 { frames };
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
- const stacktrace = buildStacktraceFromError(error);
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
- // In browser, auto-populate basic request + URL tags if not provided
467
+
500
468
  if (isBrowser) {
501
- const url = typeof window !== "undefined" ? window.location.href : undefined;
502
- const referrer =
503
- typeof document !== "undefined" ? document.referrer || undefined : undefined;
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 || (event.tags && event.tags.url),
516
- referrer: referrer || (event.tags && event.tags.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 url = typeof window !== "undefined" ? window.location.href : undefined;
578
- const referrer =
579
- typeof document !== "undefined" ? document.referrer || undefined : undefined;
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 || (event.tags && event.tags.url),
592
- referrer: referrer || (event.tags && event.tags.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
 
@@ -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
- // Capture exception with rich API-level context
49
- captureException(err, {
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 { register, captureException, captureMessage } from "./client.js";
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: 'react',
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
+ }