@watchforge/browser 0.1.3 → 0.1.5
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/CONFIGURATION_GUIDE.md +271 -10
- package/README.md +157 -1
- package/bin/watchforge.js +284 -0
- package/package.json +38 -5
- package/src/client.js +30 -0
- package/src/express.d.ts +12 -0
- package/src/index.d.ts +41 -0
- package/src/index.js +1 -2
- package/src/next-server.d.ts +26 -0
- package/src/next-server.js +58 -0
- package/src/next.d.ts +11 -0
- package/src/next.js +12 -0
- package/src/react.d.ts +8 -0
- package/src/replay.js +128 -0
- package/src/stacktrace.js +185 -7
- package/src/tracing.d.ts +39 -0
- package/src/transport.js +45 -3
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
WatchForge setup wizard
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
npx @watchforge/browser -i nextjs --dsn <dsn> [options]
|
|
13
|
+
npx @watchforge/browser init nextjs --dsn <dsn> [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-i, --integration <name> Framework integration. Currently: nextjs
|
|
17
|
+
--dsn <dsn> WatchForge DSN
|
|
18
|
+
--app-env <env> App environment (default: production)
|
|
19
|
+
--debug Enable SDK debug logging
|
|
20
|
+
--replays-on-error <rate> Upload replay when errors happen (default: 0)
|
|
21
|
+
--replays-session <rate> Continuously sample sessions (default: 0)
|
|
22
|
+
--skip-install Do not install @watchforge/browser
|
|
23
|
+
-h, --help Show help
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
npx @watchforge/browser -i nextjs --dsn "https://PUBLIC_KEY@dev.watchforges.com/PROJECT_ID" --app-env development --replays-on-error 1
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const args = {
|
|
31
|
+
integration: null,
|
|
32
|
+
dsn: null,
|
|
33
|
+
appEnv: "production",
|
|
34
|
+
debug: false,
|
|
35
|
+
replaysOnError: "0",
|
|
36
|
+
replaysSession: "0",
|
|
37
|
+
skipInstall: false,
|
|
38
|
+
help: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const arg = argv[i];
|
|
43
|
+
const next = argv[i + 1];
|
|
44
|
+
|
|
45
|
+
if (arg === "-h" || arg === "--help") args.help = true;
|
|
46
|
+
else if (arg === "-i" || arg === "--integration") {
|
|
47
|
+
args.integration = next;
|
|
48
|
+
i++;
|
|
49
|
+
} else if (arg === "init" || arg === "setup") {
|
|
50
|
+
args.integration = argv[i + 1] || args.integration;
|
|
51
|
+
i++;
|
|
52
|
+
} else if (arg === "--dsn") {
|
|
53
|
+
args.dsn = next;
|
|
54
|
+
i++;
|
|
55
|
+
} else if (arg === "--app-env" || arg === "--environment") {
|
|
56
|
+
args.appEnv = next || args.appEnv;
|
|
57
|
+
i++;
|
|
58
|
+
} else if (arg === "--debug") {
|
|
59
|
+
args.debug = true;
|
|
60
|
+
} else if (arg === "--replays-on-error") {
|
|
61
|
+
args.replaysOnError = next || "0";
|
|
62
|
+
i++;
|
|
63
|
+
} else if (arg === "--replays-session") {
|
|
64
|
+
args.replaysSession = next || "0";
|
|
65
|
+
i++;
|
|
66
|
+
} else if (arg === "--skip-install") {
|
|
67
|
+
args.skipInstall = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function log(message) {
|
|
75
|
+
console.log(`WatchForge: ${message}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fail(message) {
|
|
79
|
+
console.error(`WatchForge setup failed: ${message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fileExists(filePath) {
|
|
84
|
+
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function commandExists(command) {
|
|
88
|
+
try {
|
|
89
|
+
execSync(`command -v ${command}`, { stdio: "ignore" });
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findNextLayout(cwd) {
|
|
97
|
+
const candidates = [
|
|
98
|
+
path.join(cwd, "src", "app", "layout.tsx"),
|
|
99
|
+
path.join(cwd, "src", "app", "layout.jsx"),
|
|
100
|
+
path.join(cwd, "app", "layout.tsx"),
|
|
101
|
+
path.join(cwd, "app", "layout.jsx"),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const appRoot of [path.join(cwd, "src", "app"), path.join(cwd, "app")]) {
|
|
105
|
+
if (!fs.existsSync(appRoot) || !fs.statSync(appRoot).isDirectory()) continue;
|
|
106
|
+
const entries = fs.readdirSync(appRoot, { withFileTypes: true }).sort((a, b) => {
|
|
107
|
+
const score = (name) => {
|
|
108
|
+
if (name.includes("frontend") || name.includes("site")) return -1;
|
|
109
|
+
if (name.includes("payload") || name.includes("admin")) return 1;
|
|
110
|
+
return 0;
|
|
111
|
+
};
|
|
112
|
+
return score(a.name) - score(b.name) || a.name.localeCompare(b.name);
|
|
113
|
+
});
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (!entry.isDirectory()) continue;
|
|
116
|
+
candidates.push(path.join(appRoot, entry.name, "layout.tsx"));
|
|
117
|
+
candidates.push(path.join(appRoot, entry.name, "layout.jsx"));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return candidates.find(fileExists) || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function findPagesApp(cwd) {
|
|
125
|
+
const candidates = [
|
|
126
|
+
path.join(cwd, "src", "pages", "_app.tsx"),
|
|
127
|
+
path.join(cwd, "src", "pages", "_app.jsx"),
|
|
128
|
+
path.join(cwd, "pages", "_app.tsx"),
|
|
129
|
+
path.join(cwd, "pages", "_app.jsx"),
|
|
130
|
+
];
|
|
131
|
+
return candidates.find(fileExists) || null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeIfChanged(filePath, content) {
|
|
135
|
+
if (fileExists(filePath) && fs.readFileSync(filePath, "utf8") === content) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
139
|
+
fs.writeFileSync(filePath, content);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createConfig(cwd, args) {
|
|
144
|
+
const configPath = path.join(cwd, "watchforge.config.ts");
|
|
145
|
+
const content = `export const watchforgeConfig = {
|
|
146
|
+
dsn: ${JSON.stringify(args.dsn)},
|
|
147
|
+
app_env: ${JSON.stringify(args.appEnv)},
|
|
148
|
+
debug: ${args.debug ? "true" : "false"},
|
|
149
|
+
replaysOnErrorSampleRate: ${Number(args.replaysOnError)},
|
|
150
|
+
replaysSessionSampleRate: ${Number(args.replaysSession)},
|
|
151
|
+
maskAllInputs: true,
|
|
152
|
+
};
|
|
153
|
+
`;
|
|
154
|
+
writeIfChanged(configPath, content);
|
|
155
|
+
log(`wrote ${path.relative(cwd, configPath)}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function installPackage(cwd, skipInstall) {
|
|
159
|
+
if (skipInstall) {
|
|
160
|
+
log("skipped npm install");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const hasPnpm = fileExists(path.join(cwd, "pnpm-lock.yaml"));
|
|
165
|
+
const hasYarn = fileExists(path.join(cwd, "yarn.lock"));
|
|
166
|
+
const hasBun = fileExists(path.join(cwd, "bun.lockb"));
|
|
167
|
+
|
|
168
|
+
const command = hasPnpm && commandExists("pnpm")
|
|
169
|
+
? "pnpm add @watchforge/browser"
|
|
170
|
+
: hasYarn && commandExists("yarn")
|
|
171
|
+
? "yarn add @watchforge/browser"
|
|
172
|
+
: hasBun && commandExists("bun")
|
|
173
|
+
? "bun add @watchforge/browser"
|
|
174
|
+
: "npm install @watchforge/browser";
|
|
175
|
+
|
|
176
|
+
log(`installing package: ${command}`);
|
|
177
|
+
execSync(command, { cwd, stdio: "inherit" });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function patchAppRouter(cwd, layoutPath) {
|
|
181
|
+
const appDir = path.dirname(layoutPath);
|
|
182
|
+
const configPath = path.join(cwd, "watchforge.config.ts");
|
|
183
|
+
let configImport = path.relative(appDir, configPath).replace(/\\/g, "/");
|
|
184
|
+
configImport = configImport.replace(/\.ts$/, "");
|
|
185
|
+
if (!configImport.startsWith(".")) {
|
|
186
|
+
configImport = `./${configImport}`;
|
|
187
|
+
}
|
|
188
|
+
const initPath = path.join(appDir, "watchforge-init.tsx");
|
|
189
|
+
|
|
190
|
+
const initContent = `"use client";
|
|
191
|
+
|
|
192
|
+
import { WatchForgeProvider } from "@watchforge/browser/next";
|
|
193
|
+
import { watchforgeConfig } from "${configImport}";
|
|
194
|
+
|
|
195
|
+
export default function WatchForgeInit() {
|
|
196
|
+
return <WatchForgeProvider options={watchforgeConfig} />;
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
writeIfChanged(initPath, initContent);
|
|
201
|
+
log(`wrote ${path.relative(cwd, initPath)}`);
|
|
202
|
+
|
|
203
|
+
let layout = fs.readFileSync(layoutPath, "utf8");
|
|
204
|
+
if (!layout.includes("WatchForgeInit")) {
|
|
205
|
+
layout = `import WatchForgeInit from "./watchforge-init";\n${layout}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!layout.includes("<WatchForgeInit />")) {
|
|
209
|
+
layout = layout.replace(/<body([^>]*)>/, "<body$1>\n <WatchForgeInit />\n ");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fs.writeFileSync(layoutPath, layout);
|
|
213
|
+
log(`patched ${path.relative(cwd, layoutPath)}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function patchPagesRouter(cwd, appPath) {
|
|
217
|
+
const pagesDir = path.dirname(appPath);
|
|
218
|
+
const isSrcPages = pagesDir.endsWith(path.join("src", "pages"));
|
|
219
|
+
const configImport = isSrcPages ? "../../watchforge.config" : "../watchforge.config";
|
|
220
|
+
const content = fs.readFileSync(appPath, "utf8");
|
|
221
|
+
|
|
222
|
+
if (content.includes("register(watchforgeConfig)")) {
|
|
223
|
+
log(`${path.relative(cwd, appPath)} already contains WatchForge setup`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const patched = `import { useEffect } from "react";
|
|
228
|
+
import { register } from "@watchforge/browser";
|
|
229
|
+
import { watchforgeConfig } from "${configImport}";
|
|
230
|
+
|
|
231
|
+
${content.replace(
|
|
232
|
+
/function\s+App\s*\(([^)]*)\)\s*{/,
|
|
233
|
+
"function App($1) {\n useEffect(() => {\n register(watchforgeConfig);\n }, []);"
|
|
234
|
+
)}`;
|
|
235
|
+
|
|
236
|
+
fs.writeFileSync(appPath, patched);
|
|
237
|
+
log(`patched ${path.relative(cwd, appPath)}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function initNextjs(args) {
|
|
241
|
+
const cwd = process.cwd();
|
|
242
|
+
if (!fileExists(path.join(cwd, "package.json"))) {
|
|
243
|
+
fail("run this command from the root of your Next.js project");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
createConfig(cwd, args);
|
|
247
|
+
installPackage(cwd, args.skipInstall);
|
|
248
|
+
|
|
249
|
+
const layoutPath = findNextLayout(cwd);
|
|
250
|
+
if (layoutPath) {
|
|
251
|
+
patchAppRouter(cwd, layoutPath);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const appPath = findPagesApp(cwd);
|
|
256
|
+
if (appPath) {
|
|
257
|
+
patchPagesRouter(cwd, appPath);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fail("could not find app/layout.tsx or pages/_app.tsx");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const args = parseArgs(process.argv.slice(2));
|
|
265
|
+
|
|
266
|
+
if (args.help) {
|
|
267
|
+
console.log(HELP);
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!args.integration) {
|
|
272
|
+
fail("missing integration. Use: -i nextjs");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (args.integration !== "nextjs") {
|
|
276
|
+
fail(`unsupported integration "${args.integration}". Currently supported: nextjs`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!args.dsn) {
|
|
280
|
+
fail("missing --dsn");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
initNextjs(args);
|
|
284
|
+
log("setup complete");
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@watchforge/browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
|
-
"
|
|
5
|
+
"types": "./src/index.d.ts",
|
|
6
|
+
"description": "WatchForge JavaScript SDK for browser JavaScript, Next.js, React, Node.js, and Express.js",
|
|
6
7
|
"license": "MIT",
|
|
7
8
|
"keywords": [
|
|
8
9
|
"watchforge",
|
|
@@ -15,24 +16,55 @@
|
|
|
15
16
|
],
|
|
16
17
|
"exports": {
|
|
17
18
|
".": {
|
|
19
|
+
"types": "./src/index.d.ts",
|
|
18
20
|
"browser": "./src/index.js",
|
|
19
21
|
"node": "./src/index.js",
|
|
20
22
|
"default": "./src/index.js"
|
|
21
23
|
},
|
|
22
|
-
"./
|
|
23
|
-
|
|
24
|
+
"./node": {
|
|
25
|
+
"types": "./src/index.d.ts",
|
|
26
|
+
"default": "./src/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./next": {
|
|
29
|
+
"types": "./src/next.d.ts",
|
|
30
|
+
"default": "./src/next.js"
|
|
31
|
+
},
|
|
32
|
+
"./next/server": {
|
|
33
|
+
"types": "./src/next-server.d.ts",
|
|
34
|
+
"default": "./src/next-server.js"
|
|
35
|
+
},
|
|
36
|
+
"./express": {
|
|
37
|
+
"types": "./src/express.d.ts",
|
|
38
|
+
"default": "./src/express.js"
|
|
39
|
+
},
|
|
40
|
+
"./react": {
|
|
41
|
+
"types": "./src/react.d.ts",
|
|
42
|
+
"default": "./src/react.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"bin": {
|
|
46
|
+
"watchforge": "bin/watchforge.js"
|
|
24
47
|
},
|
|
25
48
|
"files": [
|
|
49
|
+
"bin/**/*.js",
|
|
26
50
|
"src/**/*.js",
|
|
51
|
+
"src/**/*.d.ts",
|
|
27
52
|
"README.md",
|
|
28
53
|
"CONFIGURATION_GUIDE.md",
|
|
29
54
|
"LICENSE"
|
|
30
55
|
],
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
31
59
|
"type": "module",
|
|
32
60
|
"peerDependencies": {
|
|
61
|
+
"express": ">=4.0.0",
|
|
33
62
|
"react": ">=16.8.0"
|
|
34
63
|
},
|
|
35
64
|
"peerDependenciesMeta": {
|
|
65
|
+
"express": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
36
68
|
"react": {
|
|
37
69
|
"optional": true
|
|
38
70
|
}
|
|
@@ -43,7 +75,8 @@
|
|
|
43
75
|
"url": false
|
|
44
76
|
},
|
|
45
77
|
"dependencies": {
|
|
46
|
-
"
|
|
78
|
+
"rrweb": "^2.0.1",
|
|
79
|
+
"source-map-js": "^1.2.1"
|
|
47
80
|
},
|
|
48
81
|
"devDependencies": {
|
|
49
82
|
"rollup": "^4.60.0"
|
package/src/client.js
CHANGED
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
getPerformanceContext,
|
|
10
10
|
getNodeServerContext,
|
|
11
11
|
} from "./contexts.js";
|
|
12
|
+
import {
|
|
13
|
+
flushReplayForEvent,
|
|
14
|
+
getReplayContext,
|
|
15
|
+
initReplay,
|
|
16
|
+
} from "./replay.js";
|
|
12
17
|
|
|
13
18
|
let DSN = null;
|
|
14
19
|
let APP_ENV = "production";
|
|
@@ -378,6 +383,12 @@ export function register({
|
|
|
378
383
|
app_env = "production",
|
|
379
384
|
release = null,
|
|
380
385
|
debug = false,
|
|
386
|
+
replaysSessionSampleRate = 0,
|
|
387
|
+
replaysOnErrorSampleRate = 0,
|
|
388
|
+
maskAllInputs = true,
|
|
389
|
+
blockClass = "rr-block",
|
|
390
|
+
ignoreClass = "rr-ignore",
|
|
391
|
+
maskTextClass = "rr-mask",
|
|
381
392
|
}) {
|
|
382
393
|
DSN = dsn;
|
|
383
394
|
APP_ENV = app_env;
|
|
@@ -409,6 +420,15 @@ export function register({
|
|
|
409
420
|
// Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
|
|
410
421
|
if (isBrowser) {
|
|
411
422
|
setupBrowserInstrumentation();
|
|
423
|
+
initReplay({
|
|
424
|
+
replaysSessionSampleRate,
|
|
425
|
+
replaysOnErrorSampleRate,
|
|
426
|
+
maskAllInputs,
|
|
427
|
+
blockClass,
|
|
428
|
+
ignoreClass,
|
|
429
|
+
maskTextClass,
|
|
430
|
+
debug,
|
|
431
|
+
});
|
|
412
432
|
}
|
|
413
433
|
|
|
414
434
|
// Node.js: Set up process error handlers
|
|
@@ -448,6 +468,12 @@ export async function captureException(error, context = {}) {
|
|
|
448
468
|
sdk: getSdkMetadata(),
|
|
449
469
|
};
|
|
450
470
|
|
|
471
|
+
const replay = flushReplayForEvent(DSN, event.event_id);
|
|
472
|
+
if (replay) {
|
|
473
|
+
event.replay_id = replay.replay_id;
|
|
474
|
+
event.session_id = replay.session_id;
|
|
475
|
+
}
|
|
476
|
+
|
|
451
477
|
if (RELEASE) {
|
|
452
478
|
event.release = RELEASE;
|
|
453
479
|
}
|
|
@@ -487,6 +513,10 @@ export async function captureException(error, context = {}) {
|
|
|
487
513
|
}
|
|
488
514
|
|
|
489
515
|
event.contexts = await buildEventContexts(context);
|
|
516
|
+
const replayContext = getReplayContext();
|
|
517
|
+
if (replayContext) {
|
|
518
|
+
event.contexts.replay = replayContext;
|
|
519
|
+
}
|
|
490
520
|
|
|
491
521
|
const bcs = getBreadcrumbsSnapshot();
|
|
492
522
|
if (bcs.length > 0) {
|
package/src/express.d.ts
ADDED
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface WatchForgeRegisterOptions {
|
|
2
|
+
dsn: string;
|
|
3
|
+
app_env?: string;
|
|
4
|
+
release?: string | null;
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
replaysSessionSampleRate?: number;
|
|
7
|
+
replaysOnErrorSampleRate?: number;
|
|
8
|
+
maskAllInputs?: boolean;
|
|
9
|
+
blockClass?: string;
|
|
10
|
+
ignoreClass?: string;
|
|
11
|
+
maskTextClass?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WatchForgeCaptureContext {
|
|
15
|
+
user?: Record<string, unknown>;
|
|
16
|
+
request?: Record<string, unknown>;
|
|
17
|
+
tags?: Record<string, unknown>;
|
|
18
|
+
extra?: Record<string, unknown>;
|
|
19
|
+
contexts?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function register(options: WatchForgeRegisterOptions): void;
|
|
23
|
+
export function init(options: WatchForgeRegisterOptions): void;
|
|
24
|
+
export function captureException(
|
|
25
|
+
error: unknown,
|
|
26
|
+
context?: WatchForgeCaptureContext
|
|
27
|
+
): Promise<void>;
|
|
28
|
+
export function captureMessage(
|
|
29
|
+
message: string,
|
|
30
|
+
level?: string,
|
|
31
|
+
context?: WatchForgeCaptureContext
|
|
32
|
+
): Promise<void>;
|
|
33
|
+
export function addBreadcrumb(breadcrumb: Record<string, unknown>): void;
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
startTransaction,
|
|
37
|
+
getCurrentTransaction,
|
|
38
|
+
finishTransaction,
|
|
39
|
+
Transaction,
|
|
40
|
+
Span,
|
|
41
|
+
} from "./tracing.js";
|
package/src/index.js
CHANGED
|
@@ -13,5 +13,4 @@ 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 { expressMiddleware, expressRequestMiddleware } from "./express.js";
|
|
17
|
-
export { ErrorBoundary } from "./react.js";
|
|
16
|
+
export { expressMiddleware, expressRequestMiddleware } from "./express.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WatchForgeCaptureContext,
|
|
3
|
+
WatchForgeRegisterOptions,
|
|
4
|
+
} from "./index.js";
|
|
5
|
+
|
|
6
|
+
export function register(options: WatchForgeRegisterOptions): void;
|
|
7
|
+
|
|
8
|
+
export function captureException(
|
|
9
|
+
error: unknown,
|
|
10
|
+
context?: WatchForgeCaptureContext
|
|
11
|
+
): Promise<void>;
|
|
12
|
+
|
|
13
|
+
export function captureMessage(
|
|
14
|
+
message: string,
|
|
15
|
+
level?: string,
|
|
16
|
+
context?: WatchForgeCaptureContext
|
|
17
|
+
): Promise<void>;
|
|
18
|
+
|
|
19
|
+
export type NextRouteHandler<TContext = unknown> = (
|
|
20
|
+
request: Request,
|
|
21
|
+
context: TContext
|
|
22
|
+
) => Response | Promise<Response>;
|
|
23
|
+
|
|
24
|
+
export function withWatchForgeRouteHandler<TContext = unknown>(
|
|
25
|
+
handler: NextRouteHandler<TContext>
|
|
26
|
+
): NextRouteHandler<TContext>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureException,
|
|
3
|
+
captureMessage,
|
|
4
|
+
register,
|
|
5
|
+
} from "./client.js";
|
|
6
|
+
|
|
7
|
+
export { captureException, captureMessage, register };
|
|
8
|
+
|
|
9
|
+
function sanitizeHeaders(headers) {
|
|
10
|
+
if (!headers || typeof headers.entries !== "function") return {};
|
|
11
|
+
|
|
12
|
+
const sensitive = new Set([
|
|
13
|
+
"authorization",
|
|
14
|
+
"cookie",
|
|
15
|
+
"set-cookie",
|
|
16
|
+
"x-api-key",
|
|
17
|
+
"x-csrftoken",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
return Object.fromEntries(
|
|
21
|
+
Array.from(headers.entries()).filter(
|
|
22
|
+
([key]) => !sensitive.has(key.toLowerCase())
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getRequestContext(request) {
|
|
28
|
+
if (!request) return null;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
url: request.url || "",
|
|
32
|
+
method: request.method || "",
|
|
33
|
+
headers: sanitizeHeaders(request.headers),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function withWatchForgeRouteHandler(handler) {
|
|
38
|
+
return async function watchForgeRouteHandler(request, context) {
|
|
39
|
+
try {
|
|
40
|
+
return await handler(request, context);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
await captureException(error, {
|
|
43
|
+
request: getRequestContext(request),
|
|
44
|
+
tags: {
|
|
45
|
+
framework: "nextjs",
|
|
46
|
+
runtime: "server",
|
|
47
|
+
},
|
|
48
|
+
contexts: {
|
|
49
|
+
nextjs: {
|
|
50
|
+
router: "app",
|
|
51
|
+
route_handler: true,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/next.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { WatchForgeRegisterOptions } from "./index.js";
|
|
3
|
+
|
|
4
|
+
export interface WatchForgeProviderProps {
|
|
5
|
+
options: WatchForgeRegisterOptions;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function WatchForgeProvider(
|
|
10
|
+
props: WatchForgeProviderProps
|
|
11
|
+
): ReactElement | null;
|
package/src/next.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect } from "react";
|
|
4
|
+
import { register } from "./client.js";
|
|
5
|
+
|
|
6
|
+
export function WatchForgeProvider({ options, children = null }) {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
register(options);
|
|
9
|
+
}, [options]);
|
|
10
|
+
|
|
11
|
+
return React.createElement(React.Fragment, null, children);
|
|
12
|
+
}
|