@watchforge/browser 0.1.14 → 0.1.16
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 +245 -7
- package/src/express.d.ts +12 -0
- package/src/express.js +69 -0
- package/src/index.d.ts +11 -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/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,10 @@ 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;
|
|
28
|
+
let CAPTURE_NODE_MULTIPLE_RESOLVES = false;
|
|
25
29
|
|
|
26
30
|
// Detect environment
|
|
27
31
|
const isBrowser = typeof window !== "undefined";
|
|
@@ -34,6 +38,8 @@ let breadcrumbs = [];
|
|
|
34
38
|
let browserInstrumentationInstalled = false;
|
|
35
39
|
let nodeInstrumentationInstalled = false;
|
|
36
40
|
let replayInitPromise = null;
|
|
41
|
+
let capturingGlobalError = false;
|
|
42
|
+
let browserRegisterKey = null;
|
|
37
43
|
|
|
38
44
|
export function addBreadcrumb(breadcrumb) {
|
|
39
45
|
const entry = {
|
|
@@ -58,6 +64,59 @@ function isWatchForgeInternalMessage(message) {
|
|
|
58
64
|
return String(message || "").includes("WatchForge SDK");
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
function normalizeException(error, fallbackMessage = "Unknown error") {
|
|
68
|
+
if (error instanceof Error) return error;
|
|
69
|
+
|
|
70
|
+
const message =
|
|
71
|
+
typeof error === "string"
|
|
72
|
+
? error
|
|
73
|
+
: error?.message ||
|
|
74
|
+
error?.reason?.message ||
|
|
75
|
+
String(error || fallbackMessage);
|
|
76
|
+
const normalized = new Error(message || fallbackMessage);
|
|
77
|
+
|
|
78
|
+
if (error && typeof error === "object") {
|
|
79
|
+
if (typeof error.stack === "string") {
|
|
80
|
+
normalized.stack = error.stack;
|
|
81
|
+
}
|
|
82
|
+
if (typeof error.name === "string") {
|
|
83
|
+
normalized.name = error.name;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return normalized;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function captureGlobalException(error, context = {}) {
|
|
91
|
+
if (capturingGlobalError) return;
|
|
92
|
+
capturingGlobalError = true;
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
capturingGlobalError = false;
|
|
95
|
+
}, 0);
|
|
96
|
+
|
|
97
|
+
void captureException(normalizeException(error, context.message), context);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getElementDescriptor(element) {
|
|
101
|
+
if (!element || typeof element !== "object") return null;
|
|
102
|
+
const tag = element.tagName ? String(element.tagName).toLowerCase() : "unknown";
|
|
103
|
+
const src =
|
|
104
|
+
element.currentSrc ||
|
|
105
|
+
element.src ||
|
|
106
|
+
element.href ||
|
|
107
|
+
element.action ||
|
|
108
|
+
null;
|
|
109
|
+
return {
|
|
110
|
+
tag,
|
|
111
|
+
url: src ? String(src) : null,
|
|
112
|
+
id: element.id || null,
|
|
113
|
+
className:
|
|
114
|
+
typeof element.className === "string"
|
|
115
|
+
? element.className
|
|
116
|
+
: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
61
120
|
// Simple browser environment detectors (best-effort, not 100% accurate)
|
|
62
121
|
function getBrowserContext() {
|
|
63
122
|
if (!isBrowser) return null;
|
|
@@ -163,8 +222,7 @@ function setupBrowserInstrumentation() {
|
|
|
163
222
|
if (browserInstrumentationInstalled) return;
|
|
164
223
|
browserInstrumentationInstalled = true;
|
|
165
224
|
|
|
166
|
-
|
|
167
|
-
window.onerror = function (msg, url, line, col, error) {
|
|
225
|
+
const handleWindowError = function (msg, url, line, col, error) {
|
|
168
226
|
addBreadcrumb({
|
|
169
227
|
type: "error",
|
|
170
228
|
level: "error",
|
|
@@ -172,20 +230,94 @@ function setupBrowserInstrumentation() {
|
|
|
172
230
|
message: String(msg),
|
|
173
231
|
data: { url, line, col },
|
|
174
232
|
});
|
|
175
|
-
|
|
233
|
+
captureGlobalException(error || msg, {
|
|
234
|
+
tags: {
|
|
235
|
+
handled: false,
|
|
236
|
+
mechanism: "window.onerror",
|
|
237
|
+
},
|
|
238
|
+
extra: { url, line, col },
|
|
239
|
+
message: String(msg),
|
|
240
|
+
});
|
|
176
241
|
};
|
|
177
242
|
|
|
178
|
-
|
|
243
|
+
const handleUnhandledRejection = function (event) {
|
|
244
|
+
const reason = event?.reason;
|
|
179
245
|
addBreadcrumb({
|
|
180
246
|
type: "error",
|
|
181
247
|
level: "error",
|
|
182
248
|
category: "unhandledrejection",
|
|
183
|
-
message: String(
|
|
249
|
+
message: String(reason),
|
|
184
250
|
data: {},
|
|
185
251
|
});
|
|
186
|
-
|
|
252
|
+
captureGlobalException(reason, {
|
|
253
|
+
tags: {
|
|
254
|
+
handled: false,
|
|
255
|
+
mechanism: "unhandledrejection",
|
|
256
|
+
},
|
|
257
|
+
});
|
|
187
258
|
};
|
|
188
259
|
|
|
260
|
+
const previousOnError = window.onerror;
|
|
261
|
+
window.onerror = function (msg, url, line, col, error) {
|
|
262
|
+
handleWindowError(msg, url, line, col, error);
|
|
263
|
+
if (typeof previousOnError === "function") {
|
|
264
|
+
return previousOnError.call(window, msg, url, line, col, error);
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const previousUnhandledRejection = window.onunhandledrejection;
|
|
270
|
+
window.onunhandledrejection = function (event) {
|
|
271
|
+
handleUnhandledRejection(event);
|
|
272
|
+
if (typeof previousUnhandledRejection === "function") {
|
|
273
|
+
return previousUnhandledRejection.call(window, event);
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
window.addEventListener(
|
|
279
|
+
"error",
|
|
280
|
+
(event) => {
|
|
281
|
+
if (event?.error || event?.message) {
|
|
282
|
+
handleWindowError(
|
|
283
|
+
event.message,
|
|
284
|
+
event.filename,
|
|
285
|
+
event.lineno,
|
|
286
|
+
event.colno,
|
|
287
|
+
event.error
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const target = getElementDescriptor(event?.target);
|
|
293
|
+
if (!target) return;
|
|
294
|
+
|
|
295
|
+
addBreadcrumb({
|
|
296
|
+
type: "error",
|
|
297
|
+
level: "error",
|
|
298
|
+
category: "resource",
|
|
299
|
+
message: `Failed to load <${target.tag}> resource`,
|
|
300
|
+
data: target,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (!CAPTURE_RESOURCE_ERRORS) return;
|
|
304
|
+
|
|
305
|
+
captureGlobalException(
|
|
306
|
+
new Error(`Failed to load <${target.tag}> resource${target.url ? `: ${target.url}` : ""}`),
|
|
307
|
+
{
|
|
308
|
+
tags: {
|
|
309
|
+
handled: false,
|
|
310
|
+
mechanism: "resource.error",
|
|
311
|
+
resource_tag: target.tag,
|
|
312
|
+
},
|
|
313
|
+
extra: target,
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
true
|
|
318
|
+
);
|
|
319
|
+
window.addEventListener("unhandledrejection", handleUnhandledRejection);
|
|
320
|
+
|
|
189
321
|
// Console breadcrumbs
|
|
190
322
|
["log", "info", "warn", "error", "debug"].forEach((level) => {
|
|
191
323
|
if (!console[level]) return;
|
|
@@ -348,6 +480,20 @@ function setupBrowserInstrumentation() {
|
|
|
348
480
|
duration_ms: duration,
|
|
349
481
|
},
|
|
350
482
|
});
|
|
483
|
+
if (CAPTURE_FAILED_REQUESTS) {
|
|
484
|
+
void captureException(error, {
|
|
485
|
+
tags: {
|
|
486
|
+
handled: true,
|
|
487
|
+
mechanism: "fetch",
|
|
488
|
+
operation: "http.client",
|
|
489
|
+
},
|
|
490
|
+
extra: {
|
|
491
|
+
url,
|
|
492
|
+
method,
|
|
493
|
+
duration_ms: duration,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
}
|
|
351
497
|
throw error;
|
|
352
498
|
}
|
|
353
499
|
};
|
|
@@ -373,9 +519,10 @@ function setupBrowserInstrumentation() {
|
|
|
373
519
|
const start = Date.now();
|
|
374
520
|
xhr.addEventListener("loadend", () => {
|
|
375
521
|
const duration = Date.now() - start;
|
|
522
|
+
const level = xhr.status >= 500 || xhr.status === 0 ? "error" : "info";
|
|
376
523
|
addBreadcrumb({
|
|
377
524
|
type: "http",
|
|
378
|
-
level
|
|
525
|
+
level,
|
|
379
526
|
category: "http",
|
|
380
527
|
message: `${method} ${url}`,
|
|
381
528
|
data: {
|
|
@@ -386,6 +533,20 @@ function setupBrowserInstrumentation() {
|
|
|
386
533
|
},
|
|
387
534
|
});
|
|
388
535
|
});
|
|
536
|
+
xhr.addEventListener("error", () => {
|
|
537
|
+
if (!CAPTURE_FAILED_REQUESTS) return;
|
|
538
|
+
void captureException(new Error(`XMLHttpRequest failed: ${method || "GET"} ${url || ""}`), {
|
|
539
|
+
tags: {
|
|
540
|
+
handled: true,
|
|
541
|
+
mechanism: "xhr",
|
|
542
|
+
operation: "http.client",
|
|
543
|
+
},
|
|
544
|
+
extra: {
|
|
545
|
+
url,
|
|
546
|
+
method,
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
});
|
|
389
550
|
origSend.apply(xhr, sendArgs);
|
|
390
551
|
};
|
|
391
552
|
|
|
@@ -431,13 +592,43 @@ export function register({
|
|
|
431
592
|
ignoreClass = "rr-ignore",
|
|
432
593
|
maskTextClass = "rr-mask",
|
|
433
594
|
captureConsoleErrors = true,
|
|
595
|
+
captureResourceErrors = true,
|
|
596
|
+
captureFailedRequests = true,
|
|
597
|
+
captureNodeWarnings = false,
|
|
598
|
+
captureNodeMultipleResolves = false,
|
|
434
599
|
projectRoot = null,
|
|
435
600
|
}) {
|
|
601
|
+
const nextBrowserRegisterKey = isBrowser
|
|
602
|
+
? JSON.stringify({
|
|
603
|
+
dsn,
|
|
604
|
+
app_env,
|
|
605
|
+
release,
|
|
606
|
+
debug,
|
|
607
|
+
replaysSessionSampleRate,
|
|
608
|
+
replaysOnErrorSampleRate,
|
|
609
|
+
maskAllInputs,
|
|
610
|
+
blockClass,
|
|
611
|
+
ignoreClass,
|
|
612
|
+
maskTextClass,
|
|
613
|
+
captureConsoleErrors,
|
|
614
|
+
captureResourceErrors,
|
|
615
|
+
captureFailedRequests,
|
|
616
|
+
})
|
|
617
|
+
: null;
|
|
618
|
+
|
|
619
|
+
if (isBrowser && browserRegisterKey === nextBrowserRegisterKey) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
436
623
|
DSN = dsn;
|
|
437
624
|
APP_ENV = app_env;
|
|
438
625
|
RELEASE = release;
|
|
439
626
|
DEBUG = debug;
|
|
440
627
|
CAPTURE_CONSOLE_ERRORS = Boolean(captureConsoleErrors);
|
|
628
|
+
CAPTURE_RESOURCE_ERRORS = Boolean(captureResourceErrors);
|
|
629
|
+
CAPTURE_FAILED_REQUESTS = Boolean(captureFailedRequests);
|
|
630
|
+
CAPTURE_NODE_WARNINGS = Boolean(captureNodeWarnings);
|
|
631
|
+
CAPTURE_NODE_MULTIPLE_RESOLVES = Boolean(captureNodeMultipleResolves);
|
|
441
632
|
setProjectRoot(
|
|
442
633
|
projectRoot ||
|
|
443
634
|
(isNode ? process.env.WATCHFORGE_PROJECT_ROOT || process.env.INIT_CWD : null)
|
|
@@ -467,6 +658,7 @@ export function register({
|
|
|
467
658
|
|
|
468
659
|
// Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
|
|
469
660
|
if (isBrowser) {
|
|
661
|
+
browserRegisterKey = nextBrowserRegisterKey;
|
|
470
662
|
setupBrowserInstrumentation();
|
|
471
663
|
replayInitPromise = initReplay({
|
|
472
664
|
replaysSessionSampleRate,
|
|
@@ -491,6 +683,52 @@ export function register({
|
|
|
491
683
|
process.on("unhandledRejection", (reason) => {
|
|
492
684
|
void captureException(reason);
|
|
493
685
|
});
|
|
686
|
+
|
|
687
|
+
process.on("multipleResolves", (type, promise, reason) => {
|
|
688
|
+
addBreadcrumb({
|
|
689
|
+
type: "error",
|
|
690
|
+
level: "warning",
|
|
691
|
+
category: "process.multipleResolves",
|
|
692
|
+
message: `Node.js promise multipleResolves: ${type}`,
|
|
693
|
+
data: {
|
|
694
|
+
type,
|
|
695
|
+
reason: reason?.message || String(reason || ""),
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
if (!CAPTURE_NODE_MULTIPLE_RESOLVES) return;
|
|
700
|
+
void captureException(
|
|
701
|
+
normalizeException(reason, `Node.js promise multipleResolves: ${type}`),
|
|
702
|
+
{
|
|
703
|
+
tags: {
|
|
704
|
+
handled: true,
|
|
705
|
+
mechanism: "process.multipleResolves",
|
|
706
|
+
type,
|
|
707
|
+
},
|
|
708
|
+
}
|
|
709
|
+
);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
process.on("warning", (warning) => {
|
|
713
|
+
addBreadcrumb({
|
|
714
|
+
type: "warning",
|
|
715
|
+
level: "warning",
|
|
716
|
+
category: "process.warning",
|
|
717
|
+
message: warning?.message || String(warning),
|
|
718
|
+
data: {
|
|
719
|
+
name: warning?.name || null,
|
|
720
|
+
code: warning?.code || null,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
if (!CAPTURE_NODE_WARNINGS) return;
|
|
725
|
+
void captureException(normalizeException(warning, "Node.js process warning"), {
|
|
726
|
+
tags: {
|
|
727
|
+
handled: true,
|
|
728
|
+
mechanism: "process.warning",
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
});
|
|
494
732
|
}
|
|
495
733
|
}
|
|
496
734
|
|
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,10 @@ export interface WatchForgeRegisterOptions {
|
|
|
10
10
|
ignoreClass?: string;
|
|
11
11
|
maskTextClass?: string;
|
|
12
12
|
captureConsoleErrors?: boolean;
|
|
13
|
+
captureResourceErrors?: boolean;
|
|
14
|
+
captureFailedRequests?: boolean;
|
|
15
|
+
captureNodeWarnings?: boolean;
|
|
16
|
+
captureNodeMultipleResolves?: boolean;
|
|
13
17
|
projectRoot?: string | null;
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -41,3 +45,10 @@ export {
|
|
|
41
45
|
Transaction,
|
|
42
46
|
Span,
|
|
43
47
|
} from "./tracing.js";
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
expressMiddleware,
|
|
51
|
+
expressRequestMiddleware,
|
|
52
|
+
withWatchForgeExpressHandler,
|
|
53
|
+
wrapExpressHandler,
|
|
54
|
+
} 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;
|