@watchforge/browser 0.1.0 → 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/README.md CHANGED
@@ -1,142 +1,62 @@
1
- # WatchForge JavaScript SDK
1
+ # WatchForge JavaScript SDK (`@watchforge/browser`)
2
2
 
3
- Official JavaScript SDK for WatchForge error tracking and monitoring.
3
+ Browser and Node SDK for WatchForge. **One call to `register()`** turns on automatic error reporting—no manual `try/catch` required for typical crashes.
4
4
 
5
- ## Installation
5
+ ## What this package does *not* require
6
6
 
7
- ### Option 1: Copy SDK files directly
7
+ - **No Material UI (MUI)** and no other UI library. If `npm install` fails with errors about `@mui/material` or `@mui/lab`, that conflict comes from **your app’s existing dependencies**, not from WatchForge. Fix or align MUI versions in your project, or install with:
8
8
 
9
- Copy the `src/` folder into your project and import:
9
+ ```bash
10
+ npm install @watchforge/browser --legacy-peer-deps
11
+ ```
10
12
 
11
- ```javascript
12
- import { register, captureException, captureMessage } from './path/to/sdk/index.js';
13
- ```
13
+ - **React is optional.** It is only needed if you use the React `ErrorBoundary` export from `@watchforge/browser/react`. Plain JavaScript, Vue, Angular, etc. can use the main entry without installing React.
14
14
 
15
- ### Option 2: Install via npm (when published)
15
+ ## Installation
16
16
 
17
17
  ```bash
18
18
  npm install @watchforge/browser
19
19
  ```
20
20
 
21
- ## Quick Start
22
-
23
- ```javascript
24
- import { register, captureException, captureMessage, flush } from '@watchforge/browser';
25
-
26
- // Initialize the SDK
27
- register({
28
- endpoint: "https://PUBLIC_KEY@localhost:8001/PROJECT_ID",
29
- app_env: "development",
30
- debug: true // Set to false in production
31
- });
32
-
33
- // Capture an exception
34
- try {
35
- // your code
36
- } catch (error) {
37
- captureException(error);
38
- }
39
-
40
- // Capture a message
41
- captureMessage("Something happened", "info");
42
-
43
- // Manually flush events (optional - auto-flushes after 5s or 10 events)
44
- flush();
45
- ```
46
-
47
- ## API Reference
21
+ ## Quick start (all most apps need)
48
22
 
49
- ### `register(options)`
23
+ Call **`register()` once** as early as possible (e.g. app entry / main bundle), with your **DSN** from the WatchForge project settings.
50
24
 
51
- Initialize the WatchForge SDK.
52
-
53
- **Parameters:**
54
- - `endpoint` (string, required): Your WatchForge endpoint URL
55
- - `app_env` (string, optional): Application environment (default: "production")
56
- - `debug` (boolean, optional): Enable debug logging (default: false)
57
-
58
- **Example:**
59
25
  ```javascript
26
+ import { register } from '@watchforge/browser';
27
+
60
28
  register({
61
- endpoint: "https://abc123@watchforge.io/project-uuid",
62
- app_env: "production",
63
- debug: false
29
+ dsn: 'https://PUBLIC_KEY@your-host/PROJECT_ID',
30
+ app_env: 'production', // e.g. development | staging | production
31
+ debug: false, // true while integrating locally
64
32
  });
65
33
  ```
66
34
 
67
- ### `captureException(error)`
68
-
69
- Capture an exception/error.
70
-
71
- **Parameters:**
72
- - `error` (Error): The error object to capture
73
-
74
- **Example:**
75
- ```javascript
76
- try {
77
- riskyOperation();
78
- } catch (error) {
79
- captureException(error);
80
- }
81
- ```
82
-
83
- ### `captureMessage(message, level)`
84
-
85
- Capture a custom message.
86
-
87
- **Parameters:**
88
- - `message` (string): The message to capture
89
- - `level` (string, optional): Message level - "info", "warning", or "error" (default: "info")
90
-
91
- **Example:**
92
- ```javascript
93
- captureMessage("User logged in", "info");
94
- captureMessage("Rate limit approaching", "warning");
95
- captureMessage("Critical system failure", "error");
96
- ```
97
-
98
- ### `flush()`
99
-
100
- Manually flush all buffered events to the backend immediately.
35
+ That’s enough for **automatic** reporting in the browser and Node:
101
36
 
102
- **Example:**
103
- ```javascript
104
- captureMessage("Important event");
105
- flush(); // Send immediately
106
- ```
37
+ | Where | What gets reported automatically |
38
+ |--------|----------------------------------|
39
+ | **Browser** | Uncaught errors (`window.onerror`), unhandled promise rejections, plus breadcrumbs (console, clicks, navigation) to give context around failures |
40
+ | **Node** | `uncaughtException` and `unhandledRejection` |
107
41
 
108
- ## Automatic Error Capture
42
+ You do **not** need to call `captureException` in normal flows—those global handlers already send errors to WatchForge.
109
43
 
110
- The SDK automatically captures:
111
- - **Unhandled errors** via `window.onerror`
112
- - **Unhandled promise rejections** via `window.onunhandledrejection`
44
+ ## How events reach WatchForge
113
45
 
114
- No additional setup required!
46
+ After `register()`, the SDK buffers events and sends them to your ingestion endpoint derived from the DSN. Failures are grouped and shown in the WatchForge UI like other SDKs.
115
47
 
116
- ## Event Buffering
48
+ ## Optional: copy files instead of npm
117
49
 
118
- Events are automatically buffered and sent:
119
- - When 10 events accumulate, OR
120
- - After 5 seconds of inactivity
50
+ You can copy the `src/` folder into your repo and import from your path (same `register` API).
121
51
 
122
- Use `flush()` to send immediately.
52
+ ## Advanced (optional APIs)
123
53
 
124
- ## Backward Compatibility
54
+ For manual reporting (handled errors, custom messages, immediate flush), see **`CONFIGURATION_GUIDE.md`** in this repository. The main package also exports `captureException`, `captureMessage`, and `flush` if you need them—**they are not required** for automatic error capture.
125
55
 
126
- The old `init()` function is still available but deprecated:
127
-
128
- ```javascript
129
- // Old (deprecated)
130
- init({ dsn: "...", environment: "production" });
131
-
132
- // New (recommended)
133
- register({ endpoint: "...", app_env: "production" });
134
- ```
56
+ ### Deprecated
135
57
 
136
- ## Browser Support
58
+ `init()` is deprecated; use `register()` with a `dsn` field.
137
59
 
138
- - Chrome/Edge (latest)
139
- - Firefox (latest)
140
- - Safari (latest)
60
+ ## Browser support
141
61
 
142
- Requires ES6 modules support.
62
+ Modern browsers with ES modules. For **React** error boundaries, import from `@watchforge/browser/react`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@watchforge/browser",
3
- "version": "0.1.0",
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",
@@ -32,6 +32,11 @@
32
32
  "peerDependencies": {
33
33
  "react": ">=16.8.0"
34
34
  },
35
+ "peerDependenciesMeta": {
36
+ "react": {
37
+ "optional": true
38
+ }
39
+ },
35
40
  "browser": {
36
41
  "http": false,
37
42
  "https": false,
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
+ }