@watchforge/browser 0.1.13 → 0.1.15
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/bin/watchforge.js +96 -0
- package/package.json +1 -1
- package/src/client.js +230 -7
- package/src/express.d.ts +12 -0
- package/src/express.js +69 -0
- package/src/index.d.ts +10 -0
- package/src/index.js +6 -1
- package/src/next-server.d.ts +15 -0
- package/src/next-server.js +60 -0
- package/src/next.d.ts +1 -0
- package/src/next.js +5 -2
- package/src/react.d.ts +8 -1
- package/src/react.js +18 -0
- package/src/stacktrace.js +98 -26
package/bin/watchforge.js
CHANGED
|
@@ -344,6 +344,65 @@ export default function GlobalError(${propsSignature}) {
|
|
|
344
344
|
log(`wrote ${path.relative(cwd, globalErrorPath)}`);
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
function createNextAppError(cwd, layoutPath) {
|
|
348
|
+
const appDir = path.dirname(layoutPath);
|
|
349
|
+
const usesTs = projectUsesTypeScript(cwd);
|
|
350
|
+
const errorPath = path.join(appDir, usesTs ? "error.tsx" : "error.jsx");
|
|
351
|
+
|
|
352
|
+
if (fileExists(errorPath)) {
|
|
353
|
+
const content = fs.readFileSync(errorPath, "utf8");
|
|
354
|
+
if (content.includes("@watchforge/browser") || content.includes("captureException")) {
|
|
355
|
+
log(`${path.relative(cwd, errorPath)} already contains WatchForge setup`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
log(`skipped ${path.relative(cwd, errorPath)} because it already exists`);
|
|
359
|
+
log("add captureException(error) there to report App Router segment render errors");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const propsSignature = usesTs
|
|
364
|
+
? `{\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}`
|
|
365
|
+
: `{ error, reset }`;
|
|
366
|
+
|
|
367
|
+
const content = `"use client";
|
|
368
|
+
|
|
369
|
+
import { useEffect } from "react";
|
|
370
|
+
import { captureException } from "@watchforge/browser";
|
|
371
|
+
|
|
372
|
+
export default function Error(${propsSignature}) {
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
void captureException(error, {
|
|
375
|
+
tags: {
|
|
376
|
+
framework: "nextjs",
|
|
377
|
+
runtime: "error-boundary",
|
|
378
|
+
},
|
|
379
|
+
extra: {
|
|
380
|
+
digest: error.digest,
|
|
381
|
+
},
|
|
382
|
+
contexts: {
|
|
383
|
+
nextjs: {
|
|
384
|
+
error_boundary: "app-error",
|
|
385
|
+
digest: error.digest || null,
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}, [error]);
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div>
|
|
393
|
+
<h2>Something went wrong.</h2>
|
|
394
|
+
<button type="button" onClick={reset}>
|
|
395
|
+
Try again
|
|
396
|
+
</button>
|
|
397
|
+
</div>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
`;
|
|
401
|
+
|
|
402
|
+
writeIfChanged(errorPath, content);
|
|
403
|
+
log(`wrote ${path.relative(cwd, errorPath)}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
347
406
|
function createNextInstrumentation(cwd, layoutPath) {
|
|
348
407
|
const appRoot = path.dirname(layoutPath).includes(`${path.sep}src${path.sep}app`)
|
|
349
408
|
? path.join(cwd, "src")
|
|
@@ -399,6 +458,41 @@ export const onRequestError = watchForgeOnRequestError;
|
|
|
399
458
|
log(`wrote ${path.relative(cwd, instrumentationPath)}`);
|
|
400
459
|
}
|
|
401
460
|
|
|
461
|
+
function createNextClientInstrumentation(cwd, layoutPath) {
|
|
462
|
+
const appRoot = path.dirname(layoutPath).includes(`${path.sep}src${path.sep}app`)
|
|
463
|
+
? path.join(cwd, "src")
|
|
464
|
+
: cwd;
|
|
465
|
+
const instrumentationClientPath = path.join(
|
|
466
|
+
appRoot,
|
|
467
|
+
projectUsesTypeScript(cwd) ? "instrumentation-client.ts" : "instrumentation-client.js"
|
|
468
|
+
);
|
|
469
|
+
const configImport = toImportPath(
|
|
470
|
+
path.dirname(instrumentationClientPath),
|
|
471
|
+
getConfigPath(cwd)
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (fileExists(instrumentationClientPath)) {
|
|
475
|
+
const content = fs.readFileSync(instrumentationClientPath, "utf8");
|
|
476
|
+
if (content.includes("@watchforge/browser") && content.includes("register(")) {
|
|
477
|
+
log(`${path.relative(cwd, instrumentationClientPath)} already contains WatchForge client setup`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
log(`skipped ${path.relative(cwd, instrumentationClientPath)} because it already exists`);
|
|
482
|
+
log("add register(watchforgeConfig) there to install browser error handlers before hydration");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const content = `import { register } from "@watchforge/browser";
|
|
487
|
+
import { watchforgeConfig } from "${configImport}";
|
|
488
|
+
|
|
489
|
+
register(watchforgeConfig);
|
|
490
|
+
`;
|
|
491
|
+
|
|
492
|
+
writeIfChanged(instrumentationClientPath, content);
|
|
493
|
+
log(`wrote ${path.relative(cwd, instrumentationClientPath)}`);
|
|
494
|
+
}
|
|
495
|
+
|
|
402
496
|
function patchNextConfig(cwd) {
|
|
403
497
|
const configPath = findNextConfig(cwd);
|
|
404
498
|
if (!configPath) {
|
|
@@ -595,8 +689,10 @@ function initNextjs(args) {
|
|
|
595
689
|
if (layoutPath) {
|
|
596
690
|
patchNextConfig(cwd);
|
|
597
691
|
patchAppRouter(cwd, layoutPath);
|
|
692
|
+
createNextAppError(cwd, layoutPath);
|
|
598
693
|
createNextGlobalError(cwd, layoutPath);
|
|
599
694
|
createNextInstrumentation(cwd, layoutPath);
|
|
695
|
+
createNextClientInstrumentation(cwd, layoutPath);
|
|
600
696
|
return;
|
|
601
697
|
}
|
|
602
698
|
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -22,6 +22,9 @@ let APP_ENV = "production";
|
|
|
22
22
|
let RELEASE = null;
|
|
23
23
|
let DEBUG = false;
|
|
24
24
|
let CAPTURE_CONSOLE_ERRORS = true;
|
|
25
|
+
let CAPTURE_RESOURCE_ERRORS = true;
|
|
26
|
+
let CAPTURE_FAILED_REQUESTS = true;
|
|
27
|
+
let CAPTURE_NODE_WARNINGS = false;
|
|
25
28
|
|
|
26
29
|
// Detect environment
|
|
27
30
|
const isBrowser = typeof window !== "undefined";
|
|
@@ -34,6 +37,8 @@ let breadcrumbs = [];
|
|
|
34
37
|
let browserInstrumentationInstalled = false;
|
|
35
38
|
let nodeInstrumentationInstalled = false;
|
|
36
39
|
let replayInitPromise = null;
|
|
40
|
+
let capturingGlobalError = false;
|
|
41
|
+
let browserRegisterKey = null;
|
|
37
42
|
|
|
38
43
|
export function addBreadcrumb(breadcrumb) {
|
|
39
44
|
const entry = {
|
|
@@ -58,6 +63,59 @@ function isWatchForgeInternalMessage(message) {
|
|
|
58
63
|
return String(message || "").includes("WatchForge SDK");
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
function normalizeException(error, fallbackMessage = "Unknown error") {
|
|
67
|
+
if (error instanceof Error) return error;
|
|
68
|
+
|
|
69
|
+
const message =
|
|
70
|
+
typeof error === "string"
|
|
71
|
+
? error
|
|
72
|
+
: error?.message ||
|
|
73
|
+
error?.reason?.message ||
|
|
74
|
+
String(error || fallbackMessage);
|
|
75
|
+
const normalized = new Error(message || fallbackMessage);
|
|
76
|
+
|
|
77
|
+
if (error && typeof error === "object") {
|
|
78
|
+
if (typeof error.stack === "string") {
|
|
79
|
+
normalized.stack = error.stack;
|
|
80
|
+
}
|
|
81
|
+
if (typeof error.name === "string") {
|
|
82
|
+
normalized.name = error.name;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return normalized;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function captureGlobalException(error, context = {}) {
|
|
90
|
+
if (capturingGlobalError) return;
|
|
91
|
+
capturingGlobalError = true;
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
capturingGlobalError = false;
|
|
94
|
+
}, 0);
|
|
95
|
+
|
|
96
|
+
void captureException(normalizeException(error, context.message), context);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getElementDescriptor(element) {
|
|
100
|
+
if (!element || typeof element !== "object") return null;
|
|
101
|
+
const tag = element.tagName ? String(element.tagName).toLowerCase() : "unknown";
|
|
102
|
+
const src =
|
|
103
|
+
element.currentSrc ||
|
|
104
|
+
element.src ||
|
|
105
|
+
element.href ||
|
|
106
|
+
element.action ||
|
|
107
|
+
null;
|
|
108
|
+
return {
|
|
109
|
+
tag,
|
|
110
|
+
url: src ? String(src) : null,
|
|
111
|
+
id: element.id || null,
|
|
112
|
+
className:
|
|
113
|
+
typeof element.className === "string"
|
|
114
|
+
? element.className
|
|
115
|
+
: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
61
119
|
// Simple browser environment detectors (best-effort, not 100% accurate)
|
|
62
120
|
function getBrowserContext() {
|
|
63
121
|
if (!isBrowser) return null;
|
|
@@ -163,8 +221,7 @@ function setupBrowserInstrumentation() {
|
|
|
163
221
|
if (browserInstrumentationInstalled) return;
|
|
164
222
|
browserInstrumentationInstalled = true;
|
|
165
223
|
|
|
166
|
-
|
|
167
|
-
window.onerror = function (msg, url, line, col, error) {
|
|
224
|
+
const handleWindowError = function (msg, url, line, col, error) {
|
|
168
225
|
addBreadcrumb({
|
|
169
226
|
type: "error",
|
|
170
227
|
level: "error",
|
|
@@ -172,20 +229,94 @@ function setupBrowserInstrumentation() {
|
|
|
172
229
|
message: String(msg),
|
|
173
230
|
data: { url, line, col },
|
|
174
231
|
});
|
|
175
|
-
|
|
232
|
+
captureGlobalException(error || msg, {
|
|
233
|
+
tags: {
|
|
234
|
+
handled: false,
|
|
235
|
+
mechanism: "window.onerror",
|
|
236
|
+
},
|
|
237
|
+
extra: { url, line, col },
|
|
238
|
+
message: String(msg),
|
|
239
|
+
});
|
|
176
240
|
};
|
|
177
241
|
|
|
178
|
-
|
|
242
|
+
const handleUnhandledRejection = function (event) {
|
|
243
|
+
const reason = event?.reason;
|
|
179
244
|
addBreadcrumb({
|
|
180
245
|
type: "error",
|
|
181
246
|
level: "error",
|
|
182
247
|
category: "unhandledrejection",
|
|
183
|
-
message: String(
|
|
248
|
+
message: String(reason),
|
|
184
249
|
data: {},
|
|
185
250
|
});
|
|
186
|
-
|
|
251
|
+
captureGlobalException(reason, {
|
|
252
|
+
tags: {
|
|
253
|
+
handled: false,
|
|
254
|
+
mechanism: "unhandledrejection",
|
|
255
|
+
},
|
|
256
|
+
});
|
|
187
257
|
};
|
|
188
258
|
|
|
259
|
+
const previousOnError = window.onerror;
|
|
260
|
+
window.onerror = function (msg, url, line, col, error) {
|
|
261
|
+
handleWindowError(msg, url, line, col, error);
|
|
262
|
+
if (typeof previousOnError === "function") {
|
|
263
|
+
return previousOnError.call(window, msg, url, line, col, error);
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const previousUnhandledRejection = window.onunhandledrejection;
|
|
269
|
+
window.onunhandledrejection = function (event) {
|
|
270
|
+
handleUnhandledRejection(event);
|
|
271
|
+
if (typeof previousUnhandledRejection === "function") {
|
|
272
|
+
return previousUnhandledRejection.call(window, event);
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
window.addEventListener(
|
|
278
|
+
"error",
|
|
279
|
+
(event) => {
|
|
280
|
+
if (event?.error || event?.message) {
|
|
281
|
+
handleWindowError(
|
|
282
|
+
event.message,
|
|
283
|
+
event.filename,
|
|
284
|
+
event.lineno,
|
|
285
|
+
event.colno,
|
|
286
|
+
event.error
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const target = getElementDescriptor(event?.target);
|
|
292
|
+
if (!target) return;
|
|
293
|
+
|
|
294
|
+
addBreadcrumb({
|
|
295
|
+
type: "error",
|
|
296
|
+
level: "error",
|
|
297
|
+
category: "resource",
|
|
298
|
+
message: `Failed to load <${target.tag}> resource`,
|
|
299
|
+
data: target,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (!CAPTURE_RESOURCE_ERRORS) return;
|
|
303
|
+
|
|
304
|
+
captureGlobalException(
|
|
305
|
+
new Error(`Failed to load <${target.tag}> resource${target.url ? `: ${target.url}` : ""}`),
|
|
306
|
+
{
|
|
307
|
+
tags: {
|
|
308
|
+
handled: false,
|
|
309
|
+
mechanism: "resource.error",
|
|
310
|
+
resource_tag: target.tag,
|
|
311
|
+
},
|
|
312
|
+
extra: target,
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
},
|
|
316
|
+
true
|
|
317
|
+
);
|
|
318
|
+
window.addEventListener("unhandledrejection", handleUnhandledRejection);
|
|
319
|
+
|
|
189
320
|
// Console breadcrumbs
|
|
190
321
|
["log", "info", "warn", "error", "debug"].forEach((level) => {
|
|
191
322
|
if (!console[level]) return;
|
|
@@ -348,6 +479,20 @@ function setupBrowserInstrumentation() {
|
|
|
348
479
|
duration_ms: duration,
|
|
349
480
|
},
|
|
350
481
|
});
|
|
482
|
+
if (CAPTURE_FAILED_REQUESTS) {
|
|
483
|
+
void captureException(error, {
|
|
484
|
+
tags: {
|
|
485
|
+
handled: true,
|
|
486
|
+
mechanism: "fetch",
|
|
487
|
+
operation: "http.client",
|
|
488
|
+
},
|
|
489
|
+
extra: {
|
|
490
|
+
url,
|
|
491
|
+
method,
|
|
492
|
+
duration_ms: duration,
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
}
|
|
351
496
|
throw error;
|
|
352
497
|
}
|
|
353
498
|
};
|
|
@@ -373,9 +518,10 @@ function setupBrowserInstrumentation() {
|
|
|
373
518
|
const start = Date.now();
|
|
374
519
|
xhr.addEventListener("loadend", () => {
|
|
375
520
|
const duration = Date.now() - start;
|
|
521
|
+
const level = xhr.status >= 500 || xhr.status === 0 ? "error" : "info";
|
|
376
522
|
addBreadcrumb({
|
|
377
523
|
type: "http",
|
|
378
|
-
level
|
|
524
|
+
level,
|
|
379
525
|
category: "http",
|
|
380
526
|
message: `${method} ${url}`,
|
|
381
527
|
data: {
|
|
@@ -386,6 +532,20 @@ function setupBrowserInstrumentation() {
|
|
|
386
532
|
},
|
|
387
533
|
});
|
|
388
534
|
});
|
|
535
|
+
xhr.addEventListener("error", () => {
|
|
536
|
+
if (!CAPTURE_FAILED_REQUESTS) return;
|
|
537
|
+
void captureException(new Error(`XMLHttpRequest failed: ${method || "GET"} ${url || ""}`), {
|
|
538
|
+
tags: {
|
|
539
|
+
handled: true,
|
|
540
|
+
mechanism: "xhr",
|
|
541
|
+
operation: "http.client",
|
|
542
|
+
},
|
|
543
|
+
extra: {
|
|
544
|
+
url,
|
|
545
|
+
method,
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
});
|
|
389
549
|
origSend.apply(xhr, sendArgs);
|
|
390
550
|
};
|
|
391
551
|
|
|
@@ -431,13 +591,41 @@ export function register({
|
|
|
431
591
|
ignoreClass = "rr-ignore",
|
|
432
592
|
maskTextClass = "rr-mask",
|
|
433
593
|
captureConsoleErrors = true,
|
|
594
|
+
captureResourceErrors = true,
|
|
595
|
+
captureFailedRequests = true,
|
|
596
|
+
captureNodeWarnings = false,
|
|
434
597
|
projectRoot = null,
|
|
435
598
|
}) {
|
|
599
|
+
const nextBrowserRegisterKey = isBrowser
|
|
600
|
+
? JSON.stringify({
|
|
601
|
+
dsn,
|
|
602
|
+
app_env,
|
|
603
|
+
release,
|
|
604
|
+
debug,
|
|
605
|
+
replaysSessionSampleRate,
|
|
606
|
+
replaysOnErrorSampleRate,
|
|
607
|
+
maskAllInputs,
|
|
608
|
+
blockClass,
|
|
609
|
+
ignoreClass,
|
|
610
|
+
maskTextClass,
|
|
611
|
+
captureConsoleErrors,
|
|
612
|
+
captureResourceErrors,
|
|
613
|
+
captureFailedRequests,
|
|
614
|
+
})
|
|
615
|
+
: null;
|
|
616
|
+
|
|
617
|
+
if (isBrowser && browserRegisterKey === nextBrowserRegisterKey) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
436
621
|
DSN = dsn;
|
|
437
622
|
APP_ENV = app_env;
|
|
438
623
|
RELEASE = release;
|
|
439
624
|
DEBUG = debug;
|
|
440
625
|
CAPTURE_CONSOLE_ERRORS = Boolean(captureConsoleErrors);
|
|
626
|
+
CAPTURE_RESOURCE_ERRORS = Boolean(captureResourceErrors);
|
|
627
|
+
CAPTURE_FAILED_REQUESTS = Boolean(captureFailedRequests);
|
|
628
|
+
CAPTURE_NODE_WARNINGS = Boolean(captureNodeWarnings);
|
|
441
629
|
setProjectRoot(
|
|
442
630
|
projectRoot ||
|
|
443
631
|
(isNode ? process.env.WATCHFORGE_PROJECT_ROOT || process.env.INIT_CWD : null)
|
|
@@ -467,6 +655,7 @@ export function register({
|
|
|
467
655
|
|
|
468
656
|
// Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
|
|
469
657
|
if (isBrowser) {
|
|
658
|
+
browserRegisterKey = nextBrowserRegisterKey;
|
|
470
659
|
setupBrowserInstrumentation();
|
|
471
660
|
replayInitPromise = initReplay({
|
|
472
661
|
replaysSessionSampleRate,
|
|
@@ -491,6 +680,40 @@ export function register({
|
|
|
491
680
|
process.on("unhandledRejection", (reason) => {
|
|
492
681
|
void captureException(reason);
|
|
493
682
|
});
|
|
683
|
+
|
|
684
|
+
process.on("multipleResolves", (type, promise, reason) => {
|
|
685
|
+
void captureException(
|
|
686
|
+
normalizeException(reason, `Node.js promise multipleResolves: ${type}`),
|
|
687
|
+
{
|
|
688
|
+
tags: {
|
|
689
|
+
handled: false,
|
|
690
|
+
mechanism: "process.multipleResolves",
|
|
691
|
+
type,
|
|
692
|
+
},
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
process.on("warning", (warning) => {
|
|
698
|
+
addBreadcrumb({
|
|
699
|
+
type: "warning",
|
|
700
|
+
level: "warning",
|
|
701
|
+
category: "process.warning",
|
|
702
|
+
message: warning?.message || String(warning),
|
|
703
|
+
data: {
|
|
704
|
+
name: warning?.name || null,
|
|
705
|
+
code: warning?.code || null,
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
if (!CAPTURE_NODE_WARNINGS) return;
|
|
710
|
+
void captureException(normalizeException(warning, "Node.js process warning"), {
|
|
711
|
+
tags: {
|
|
712
|
+
handled: true,
|
|
713
|
+
mechanism: "process.warning",
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
});
|
|
494
717
|
}
|
|
495
718
|
}
|
|
496
719
|
|
package/src/express.d.ts
CHANGED
|
@@ -10,3 +10,15 @@ export function expressMiddleware(): (
|
|
|
10
10
|
res: any,
|
|
11
11
|
next: (error?: unknown) => void
|
|
12
12
|
) => void;
|
|
13
|
+
|
|
14
|
+
export type ExpressHandler = (
|
|
15
|
+
req: any,
|
|
16
|
+
res: any,
|
|
17
|
+
next?: (error?: unknown) => void
|
|
18
|
+
) => unknown | Promise<unknown>;
|
|
19
|
+
|
|
20
|
+
export function withWatchForgeExpressHandler<T extends ExpressHandler>(
|
|
21
|
+
handler: T
|
|
22
|
+
): T;
|
|
23
|
+
|
|
24
|
+
export const wrapExpressHandler: typeof withWatchForgeExpressHandler;
|
package/src/express.js
CHANGED
|
@@ -87,6 +87,75 @@ export function expressMiddleware() {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
export function withWatchForgeExpressHandler(handler) {
|
|
91
|
+
return async function watchForgeExpressHandler(req, res, next) {
|
|
92
|
+
try {
|
|
93
|
+
return await handler(req, res, next);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
await captureException(error, {
|
|
96
|
+
user: getUserContext(req),
|
|
97
|
+
request: getExpressRequestContext(req),
|
|
98
|
+
tags: {
|
|
99
|
+
framework: "express",
|
|
100
|
+
route: req.route?.path || req.path,
|
|
101
|
+
runtime: "server",
|
|
102
|
+
},
|
|
103
|
+
extra: {
|
|
104
|
+
params: req.params || {},
|
|
105
|
+
},
|
|
106
|
+
contexts: {
|
|
107
|
+
express: {
|
|
108
|
+
route: req.route?.path || req.path,
|
|
109
|
+
base_url: req.baseUrl || null,
|
|
110
|
+
handler_wrapper: true,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
if (typeof next === "function") {
|
|
115
|
+
return next(error);
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const wrapExpressHandler = withWatchForgeExpressHandler;
|
|
123
|
+
|
|
124
|
+
function getRequestIp(req) {
|
|
125
|
+
return (
|
|
126
|
+
req.ip ||
|
|
127
|
+
req.connection?.remoteAddress ||
|
|
128
|
+
req.socket?.remoteAddress ||
|
|
129
|
+
(req.connection?.socket ? req.connection.socket.remoteAddress : null) ||
|
|
130
|
+
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
|
|
131
|
+
req.headers["x-real-ip"] ||
|
|
132
|
+
null
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getUserContext(req) {
|
|
137
|
+
const ip = getRequestIp(req);
|
|
138
|
+
return req.user
|
|
139
|
+
? {
|
|
140
|
+
id: String(req.user.id),
|
|
141
|
+
email: req.user.email || null,
|
|
142
|
+
username: req.user.username || null,
|
|
143
|
+
ip_address: ip,
|
|
144
|
+
}
|
|
145
|
+
: null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getExpressRequestContext(req) {
|
|
149
|
+
return {
|
|
150
|
+
url: `${req.protocol}://${req.get("host")}${req.originalUrl || req.url}`,
|
|
151
|
+
method: req.method,
|
|
152
|
+
headers: sanitizeHeaders(req.headers),
|
|
153
|
+
query: req.query || {},
|
|
154
|
+
body: req.body || {},
|
|
155
|
+
ip: getRequestIp(req),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
90
159
|
function sanitizeHeaders(headers) {
|
|
91
160
|
const sanitized = {};
|
|
92
161
|
for (const [key, value] of Object.entries(headers)) {
|
package/src/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export interface WatchForgeRegisterOptions {
|
|
|
10
10
|
ignoreClass?: string;
|
|
11
11
|
maskTextClass?: string;
|
|
12
12
|
captureConsoleErrors?: boolean;
|
|
13
|
+
captureResourceErrors?: boolean;
|
|
14
|
+
captureFailedRequests?: boolean;
|
|
15
|
+
captureNodeWarnings?: boolean;
|
|
13
16
|
projectRoot?: string | null;
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -41,3 +44,10 @@ export {
|
|
|
41
44
|
Transaction,
|
|
42
45
|
Span,
|
|
43
46
|
} from "./tracing.js";
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
expressMiddleware,
|
|
50
|
+
expressRequestMiddleware,
|
|
51
|
+
withWatchForgeExpressHandler,
|
|
52
|
+
wrapExpressHandler,
|
|
53
|
+
} from "./express.js";
|
package/src/index.js
CHANGED
|
@@ -13,4 +13,9 @@ export { init } from "./client.js";
|
|
|
13
13
|
export { startTransaction, getCurrentTransaction, finishTransaction, Transaction, Span } from "./tracing.js";
|
|
14
14
|
|
|
15
15
|
// Export framework integrations
|
|
16
|
-
export {
|
|
16
|
+
export {
|
|
17
|
+
expressMiddleware,
|
|
18
|
+
expressRequestMiddleware,
|
|
19
|
+
withWatchForgeExpressHandler,
|
|
20
|
+
wrapExpressHandler,
|
|
21
|
+
} from "./express.js";
|
package/src/next-server.d.ts
CHANGED
|
@@ -45,3 +45,18 @@ export type NextRouteHandler<TContext = unknown> = (
|
|
|
45
45
|
export function withWatchForgeRouteHandler<TContext = unknown>(
|
|
46
46
|
handler: NextRouteHandler<TContext>
|
|
47
47
|
): NextRouteHandler<TContext>;
|
|
48
|
+
|
|
49
|
+
export function withWatchForgeServerAction<TArgs extends unknown[], TResult>(
|
|
50
|
+
action: (...args: TArgs) => TResult | Promise<TResult>,
|
|
51
|
+
actionName?: string
|
|
52
|
+
): (...args: TArgs) => Promise<TResult>;
|
|
53
|
+
|
|
54
|
+
export type NextApiHandler = (req: any, res: any) => unknown | Promise<unknown>;
|
|
55
|
+
|
|
56
|
+
export function withWatchForgeApiHandler<T extends NextApiHandler>(
|
|
57
|
+
handler: T
|
|
58
|
+
): T;
|
|
59
|
+
|
|
60
|
+
export const wrapRouteHandler: typeof withWatchForgeRouteHandler;
|
|
61
|
+
export const wrapServerAction: typeof withWatchForgeServerAction;
|
|
62
|
+
export const wrapApiHandler: typeof withWatchForgeApiHandler;
|
package/src/next-server.js
CHANGED
|
@@ -116,3 +116,63 @@ export function withWatchForgeRouteHandler(handler) {
|
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
+
|
|
120
|
+
export function withWatchForgeServerAction(action, actionName = action?.name || "serverAction") {
|
|
121
|
+
return async function watchForgeServerAction(...args) {
|
|
122
|
+
try {
|
|
123
|
+
return await action(...args);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
await captureException(error, {
|
|
126
|
+
tags: {
|
|
127
|
+
framework: "nextjs",
|
|
128
|
+
runtime: "server",
|
|
129
|
+
mechanism: "server-action",
|
|
130
|
+
action: actionName,
|
|
131
|
+
},
|
|
132
|
+
contexts: {
|
|
133
|
+
nextjs: {
|
|
134
|
+
router: "app",
|
|
135
|
+
server_action: true,
|
|
136
|
+
action: actionName,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function withWatchForgeApiHandler(handler) {
|
|
146
|
+
return async function watchForgeApiHandler(req, res) {
|
|
147
|
+
try {
|
|
148
|
+
return await handler(req, res);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
await captureException(error, {
|
|
151
|
+
request: {
|
|
152
|
+
url: req?.url || "",
|
|
153
|
+
method: req?.method || "",
|
|
154
|
+
headers: sanitizeRequestHeaders(req?.headers),
|
|
155
|
+
query: req?.query || {},
|
|
156
|
+
body: req?.body,
|
|
157
|
+
},
|
|
158
|
+
tags: {
|
|
159
|
+
framework: "nextjs",
|
|
160
|
+
runtime: "server",
|
|
161
|
+
router: "pages",
|
|
162
|
+
route_type: "api",
|
|
163
|
+
},
|
|
164
|
+
contexts: {
|
|
165
|
+
nextjs: {
|
|
166
|
+
router: "pages",
|
|
167
|
+
api_route: true,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const wrapRouteHandler = withWatchForgeRouteHandler;
|
|
177
|
+
export const wrapServerAction = withWatchForgeServerAction;
|
|
178
|
+
export const wrapApiHandler = withWatchForgeApiHandler;
|
package/src/next.d.ts
CHANGED
package/src/next.js
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useEffect } from "react";
|
|
4
4
|
import { register } from "./client.js";
|
|
5
|
+
import { ErrorBoundary } from "./react.js";
|
|
5
6
|
|
|
6
|
-
export function WatchForgeProvider({ options, children = null }) {
|
|
7
|
+
export function WatchForgeProvider({ options, children = null, fallback = null }) {
|
|
7
8
|
useEffect(() => {
|
|
8
9
|
register(options);
|
|
9
10
|
}, [options]);
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
if (!children) return null;
|
|
13
|
+
|
|
14
|
+
return React.createElement(ErrorBoundary, { fallback }, children);
|
|
12
15
|
}
|
package/src/react.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Component, ReactNode } from "react";
|
|
1
|
+
import type { Component, ComponentType, ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
export interface ErrorBoundaryProps {
|
|
4
4
|
children?: ReactNode;
|
|
@@ -6,3 +6,10 @@ export interface ErrorBoundaryProps {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export class ErrorBoundary extends Component<ErrorBoundaryProps> {}
|
|
9
|
+
|
|
10
|
+
export function withWatchForgeErrorBoundary<P>(
|
|
11
|
+
component: ComponentType<P>,
|
|
12
|
+
options?: Pick<ErrorBoundaryProps, "fallback">
|
|
13
|
+
): ComponentType<P>;
|
|
14
|
+
|
|
15
|
+
export const withErrorBoundary: typeof withWatchForgeErrorBoundary;
|
package/src/react.js
CHANGED
|
@@ -50,3 +50,21 @@ export class ErrorBoundary extends React.Component {
|
|
|
50
50
|
return this.props.children;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
export function withWatchForgeErrorBoundary(Component, options = {}) {
|
|
55
|
+
const Wrapped = function WatchForgeErrorBoundaryWrapper(props) {
|
|
56
|
+
return React.createElement(
|
|
57
|
+
ErrorBoundary,
|
|
58
|
+
{ fallback: options.fallback },
|
|
59
|
+
React.createElement(Component, props)
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
Wrapped.displayName = `withWatchForgeErrorBoundary(${
|
|
64
|
+
Component.displayName || Component.name || "Component"
|
|
65
|
+
})`;
|
|
66
|
+
|
|
67
|
+
return Wrapped;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const withErrorBoundary = withWatchForgeErrorBoundary;
|
package/src/stacktrace.js
CHANGED
|
@@ -538,6 +538,37 @@ async function fetchBrowserSourceMap(absPath) {
|
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
+
function parseDataSourceMap(sourceMappingUrl) {
|
|
542
|
+
if (!sourceMappingUrl?.startsWith("data:")) return null;
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
const commaIndex = sourceMappingUrl.indexOf(",");
|
|
546
|
+
if (commaIndex === -1) return null;
|
|
547
|
+
|
|
548
|
+
const metadata = sourceMappingUrl.slice(0, commaIndex);
|
|
549
|
+
const payload = sourceMappingUrl.slice(commaIndex + 1);
|
|
550
|
+
const json = metadata.includes(";base64")
|
|
551
|
+
? decodeURIComponent(escape(atob(payload)))
|
|
552
|
+
: decodeURIComponent(payload);
|
|
553
|
+
return JSON.parse(json);
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function findInlineSourceMap(sourceText) {
|
|
560
|
+
if (!sourceText || typeof sourceText !== "string") return null;
|
|
561
|
+
|
|
562
|
+
const sourceMapPattern = /\/\/# sourceMappingURL=([^\s"'\\)]+)/g;
|
|
563
|
+
let match;
|
|
564
|
+
while ((match = sourceMapPattern.exec(sourceText))) {
|
|
565
|
+
const sourceMap = parseDataSourceMap(match[1]);
|
|
566
|
+
if (sourceMap) return sourceMap;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
541
572
|
async function findSourceMapForFrame(frame) {
|
|
542
573
|
const wanted = normalizeSourcePath(frame.abs_path || frame.module);
|
|
543
574
|
const scriptUrls = [];
|
|
@@ -582,16 +613,31 @@ async function findSourceMapForFrame(frame) {
|
|
|
582
613
|
return null;
|
|
583
614
|
}
|
|
584
615
|
|
|
585
|
-
async function
|
|
586
|
-
const resolved = await findSourceMapForFrame(frame);
|
|
587
|
-
if (!resolved) return false;
|
|
588
|
-
|
|
589
|
-
const { sourceMap } = resolved;
|
|
590
|
-
|
|
616
|
+
async function applySourceMapToFrame(frame, sourceMap) {
|
|
591
617
|
try {
|
|
592
618
|
const SourceMapConsumer = await getSourceMapConsumer();
|
|
593
619
|
if (!SourceMapConsumer) return false;
|
|
594
620
|
|
|
621
|
+
const sources = Array.isArray(sourceMap.sources) ? sourceMap.sources : [];
|
|
622
|
+
const contents = Array.isArray(sourceMap.sourcesContent)
|
|
623
|
+
? sourceMap.sourcesContent
|
|
624
|
+
: [];
|
|
625
|
+
const matchedSourceIndex = sources.findIndex((candidate) =>
|
|
626
|
+
sourceMatchesFrame(candidate, frame.abs_path || frame.module || frame.raw_abs_path)
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
if (matchedSourceIndex >= 0 && contents[matchedSourceIndex]) {
|
|
630
|
+
const source = sources[matchedSourceIndex];
|
|
631
|
+
const sourceLines = contents[matchedSourceIndex].split("\n");
|
|
632
|
+
const originalLine = frame.lineno;
|
|
633
|
+
if (originalLine && originalLine <= sourceLines.length) {
|
|
634
|
+
frame.abs_path = source;
|
|
635
|
+
frame.filename = normalizeSourcePath(source).split("/").pop() || frame.filename;
|
|
636
|
+
applySourceContext(frame, sourceLines, originalLine);
|
|
637
|
+
if (frame.context_line) return true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
595
641
|
const consumer = await new SourceMapConsumer(sourceMap);
|
|
596
642
|
let source = null;
|
|
597
643
|
let lineno = frame.lineno;
|
|
@@ -639,28 +685,49 @@ async function enrichFrameWithSourceMap(frame) {
|
|
|
639
685
|
}
|
|
640
686
|
}
|
|
641
687
|
|
|
642
|
-
function
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
Object.values(globalObject).find(
|
|
646
|
-
(value) =>
|
|
647
|
-
Array.isArray(value) &&
|
|
648
|
-
value.some((item) => Array.isArray(item) && item.length >= 2)
|
|
649
|
-
) || [];
|
|
688
|
+
async function enrichFrameWithSourceMap(frame) {
|
|
689
|
+
const resolved = await findSourceMapForFrame(frame);
|
|
690
|
+
if (!resolved) return false;
|
|
650
691
|
|
|
692
|
+
return applySourceMapToFrame(frame, resolved.sourceMap);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function getWebpackChunkGlobals(globalObject) {
|
|
696
|
+
return Object.entries(globalObject)
|
|
697
|
+
.filter(([, value]) => Array.isArray(value))
|
|
698
|
+
.sort(([nameA], [nameB]) => {
|
|
699
|
+
const score = (name) => (name.startsWith("webpackChunk") ? 0 : 1);
|
|
700
|
+
return score(nameA) - score(nameB);
|
|
701
|
+
})
|
|
702
|
+
.map(([, value]) => value);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function getWebpackFrameArtifacts(frame) {
|
|
706
|
+
const globalObject = typeof globalThis !== "undefined" ? globalThis : window;
|
|
651
707
|
const framePath = normalizeSourcePath(
|
|
652
708
|
frame.abs_path || frame.module || frame.raw_abs_path
|
|
653
709
|
);
|
|
654
|
-
if (!framePath
|
|
655
|
-
|
|
656
|
-
for (const
|
|
657
|
-
const
|
|
658
|
-
|
|
710
|
+
if (!framePath) return null;
|
|
711
|
+
|
|
712
|
+
for (const webpackChunks of getWebpackChunkGlobals(globalObject)) {
|
|
713
|
+
for (const chunk of webpackChunks) {
|
|
714
|
+
const modules = Array.isArray(chunk) ? chunk[1] : null;
|
|
715
|
+
if (!modules || typeof modules !== "object") continue;
|
|
716
|
+
|
|
717
|
+
for (const [moduleId, factory] of Object.entries(modules)) {
|
|
718
|
+
const factorySource = String(factory);
|
|
719
|
+
if (
|
|
720
|
+
!sourceMatchesFrame(moduleId, framePath) &&
|
|
721
|
+
!sourceMatchesFrame(factorySource.match(/sourceURL=([^\s"'\\)]+)/)?.[1], framePath)
|
|
722
|
+
) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
659
725
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
726
|
+
return {
|
|
727
|
+
lines: factorySource.includes("\n") ? factorySource.split("\n") : null,
|
|
728
|
+
sourceMap: findInlineSourceMap(factorySource),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
664
731
|
}
|
|
665
732
|
}
|
|
666
733
|
|
|
@@ -670,9 +737,14 @@ function getSourceContentFromWebpackFrame(frame) {
|
|
|
670
737
|
async function enrichFrameWithBrowserSource(frame) {
|
|
671
738
|
if (!frame.in_app || !frame.lineno) return;
|
|
672
739
|
|
|
673
|
-
const
|
|
674
|
-
if (
|
|
675
|
-
|
|
740
|
+
const webpackArtifacts = getWebpackFrameArtifacts(frame);
|
|
741
|
+
if (webpackArtifacts?.sourceMap) {
|
|
742
|
+
const applied = await applySourceMapToFrame(frame, webpackArtifacts.sourceMap);
|
|
743
|
+
if (applied) return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (webpackArtifacts?.lines) {
|
|
747
|
+
applySourceContext(frame, webpackArtifacts.lines, frame.lineno);
|
|
676
748
|
if (frame.context_line) return;
|
|
677
749
|
}
|
|
678
750
|
|