@watchforge/browser 0.1.11 → 0.1.13
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 +19 -14
- package/package.json +1 -1
- package/src/client.js +22 -2
- package/src/contexts.js +1 -1
- package/src/index.d.ts +1 -0
- package/src/replay.js +22 -1
- package/src/stacktrace.js +227 -9
- package/src/tracing.js +1 -1
package/bin/watchforge.js
CHANGED
|
@@ -214,6 +214,7 @@ function createConfig(cwd, args) {
|
|
|
214
214
|
replaysOnErrorSampleRate: ${Number(args.replaysOnError)},
|
|
215
215
|
replaysSessionSampleRate: ${Number(args.replaysSession)},
|
|
216
216
|
maskAllInputs: true,
|
|
217
|
+
projectRoot: process.env.INIT_CWD || process.env.PWD || "",
|
|
217
218
|
};
|
|
218
219
|
`;
|
|
219
220
|
writeIfChanged(configPath, content);
|
|
@@ -308,21 +309,25 @@ import { watchforgeConfig } from "${configImport}";
|
|
|
308
309
|
export default function GlobalError(${propsSignature}) {
|
|
309
310
|
useEffect(() => {
|
|
310
311
|
register(watchforgeConfig);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
extra: {
|
|
317
|
-
digest: error.digest,
|
|
318
|
-
},
|
|
319
|
-
contexts: {
|
|
320
|
-
nextjs: {
|
|
321
|
-
error_boundary: "global-error",
|
|
322
|
-
digest: error.digest || null,
|
|
312
|
+
const timer = window.setTimeout(() => {
|
|
313
|
+
void captureException(error, {
|
|
314
|
+
tags: {
|
|
315
|
+
framework: "nextjs",
|
|
316
|
+
runtime: "error-boundary",
|
|
323
317
|
},
|
|
324
|
-
|
|
325
|
-
|
|
318
|
+
extra: {
|
|
319
|
+
digest: error.digest,
|
|
320
|
+
},
|
|
321
|
+
contexts: {
|
|
322
|
+
nextjs: {
|
|
323
|
+
error_boundary: "global-error",
|
|
324
|
+
digest: error.digest || null,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}, 100);
|
|
329
|
+
|
|
330
|
+
return () => window.clearTimeout(timer);
|
|
326
331
|
}, [error]);
|
|
327
332
|
|
|
328
333
|
return (
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { sendEvent, parseDsn } from "./transport.js";
|
|
|
2
2
|
import {
|
|
3
3
|
buildStacktraceFromError,
|
|
4
4
|
enrichStacktraceAsync,
|
|
5
|
+
setProjectRoot,
|
|
5
6
|
} from "./stacktrace.js";
|
|
6
7
|
import {
|
|
7
8
|
getSdkMetadata,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
flushReplayForEvent,
|
|
14
15
|
getReplayContext,
|
|
15
16
|
initReplay,
|
|
17
|
+
waitForReplayReady,
|
|
16
18
|
} from "./replay.js";
|
|
17
19
|
|
|
18
20
|
let DSN = null;
|
|
@@ -31,6 +33,7 @@ const MAX_BREADCRUMBS = 100;
|
|
|
31
33
|
let breadcrumbs = [];
|
|
32
34
|
let browserInstrumentationInstalled = false;
|
|
33
35
|
let nodeInstrumentationInstalled = false;
|
|
36
|
+
let replayInitPromise = null;
|
|
34
37
|
|
|
35
38
|
export function addBreadcrumb(breadcrumb) {
|
|
36
39
|
const entry = {
|
|
@@ -428,12 +431,17 @@ export function register({
|
|
|
428
431
|
ignoreClass = "rr-ignore",
|
|
429
432
|
maskTextClass = "rr-mask",
|
|
430
433
|
captureConsoleErrors = true,
|
|
434
|
+
projectRoot = null,
|
|
431
435
|
}) {
|
|
432
436
|
DSN = dsn;
|
|
433
437
|
APP_ENV = app_env;
|
|
434
438
|
RELEASE = release;
|
|
435
439
|
DEBUG = debug;
|
|
436
440
|
CAPTURE_CONSOLE_ERRORS = Boolean(captureConsoleErrors);
|
|
441
|
+
setProjectRoot(
|
|
442
|
+
projectRoot ||
|
|
443
|
+
(isNode ? process.env.WATCHFORGE_PROJECT_ROOT || process.env.INIT_CWD : null)
|
|
444
|
+
);
|
|
437
445
|
|
|
438
446
|
// Initialize tracing
|
|
439
447
|
initTracing(dsn, app_env, debug);
|
|
@@ -460,7 +468,7 @@ export function register({
|
|
|
460
468
|
// Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
|
|
461
469
|
if (isBrowser) {
|
|
462
470
|
setupBrowserInstrumentation();
|
|
463
|
-
initReplay({
|
|
471
|
+
replayInitPromise = initReplay({
|
|
464
472
|
replaysSessionSampleRate,
|
|
465
473
|
replaysOnErrorSampleRate,
|
|
466
474
|
maskAllInputs,
|
|
@@ -497,7 +505,10 @@ export async function captureException(error, context = {}) {
|
|
|
497
505
|
|
|
498
506
|
let stacktrace = buildStacktraceFromError(error);
|
|
499
507
|
if (stacktrace) {
|
|
500
|
-
stacktrace = await enrichStacktraceAsync(
|
|
508
|
+
stacktrace = await enrichStacktraceAsync(
|
|
509
|
+
stacktrace,
|
|
510
|
+
error?.message || String(error || "")
|
|
511
|
+
);
|
|
501
512
|
}
|
|
502
513
|
|
|
503
514
|
const event = {
|
|
@@ -511,6 +522,15 @@ export async function captureException(error, context = {}) {
|
|
|
511
522
|
sdk: getSdkMetadata(),
|
|
512
523
|
};
|
|
513
524
|
|
|
525
|
+
if (isBrowser && replayInitPromise) {
|
|
526
|
+
try {
|
|
527
|
+
await replayInitPromise;
|
|
528
|
+
await waitForReplayReady();
|
|
529
|
+
} catch {
|
|
530
|
+
// Replay is best-effort; never block error delivery.
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
514
534
|
const replay = flushReplayForEvent(DSN, event.event_id);
|
|
515
535
|
if (replay) {
|
|
516
536
|
event.replay_id = replay.replay_id;
|
package/src/contexts.js
CHANGED
|
@@ -7,7 +7,7 @@ const isNode =
|
|
|
7
7
|
typeof process !== "undefined" && process.versions && process.versions.node;
|
|
8
8
|
|
|
9
9
|
export const SDK_NAME = "@watchforge/browser";
|
|
10
|
-
export const SDK_VERSION = "0.1.
|
|
10
|
+
export const SDK_VERSION = "0.1.12";
|
|
11
11
|
|
|
12
12
|
export function getSdkMetadata() {
|
|
13
13
|
return {
|
package/src/index.d.ts
CHANGED
package/src/replay.js
CHANGED
|
@@ -120,6 +120,27 @@ export async function initReplay(config = {}) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
export function waitForReplayReady(timeoutMs = 300) {
|
|
124
|
+
if (!isBrowser || !replayId || !sessionId) return Promise.resolve(false);
|
|
125
|
+
if (events.length > 0) return Promise.resolve(true);
|
|
126
|
+
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const started = Date.now();
|
|
129
|
+
const check = () => {
|
|
130
|
+
if (events.length > 0) {
|
|
131
|
+
resolve(true);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (Date.now() - started >= timeoutMs) {
|
|
135
|
+
resolve(false);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
setTimeout(check, 25);
|
|
139
|
+
};
|
|
140
|
+
check();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
export function getReplayContext() {
|
|
124
145
|
if (!isBrowser || !replayId || !sessionId) return null;
|
|
125
146
|
return {
|
|
@@ -151,7 +172,7 @@ export function flushReplayForEvent(dsn, eventId) {
|
|
|
151
172
|
events,
|
|
152
173
|
sdk: {
|
|
153
174
|
name: "@watchforge/browser",
|
|
154
|
-
version: "0.1.
|
|
175
|
+
version: "0.1.12",
|
|
155
176
|
},
|
|
156
177
|
};
|
|
157
178
|
|
package/src/stacktrace.js
CHANGED
|
@@ -60,6 +60,7 @@ export function buildStacktraceFromError(error) {
|
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const errorMessage = error?.message || "";
|
|
63
64
|
const stackLines = error.stack.split("\n");
|
|
64
65
|
const frames = [];
|
|
65
66
|
|
|
@@ -88,6 +89,7 @@ export function buildStacktraceFromError(error) {
|
|
|
88
89
|
post_context: [],
|
|
89
90
|
vars: {},
|
|
90
91
|
in_app: inApp,
|
|
92
|
+
error_message: errorMessage,
|
|
91
93
|
});
|
|
92
94
|
}
|
|
93
95
|
|
|
@@ -114,6 +116,45 @@ function applySourceContext(frame, sourceLines, lineno) {
|
|
|
114
116
|
.map((l) => l.replace(/\r$/, ""));
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
function getUndefinedIdentifier(message) {
|
|
120
|
+
if (!message || typeof message !== "string") return null;
|
|
121
|
+
const match = message.match(/^([A-Za-z_$][\w$]*) is not defined$/);
|
|
122
|
+
return match?.[1] || null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function inferLineFromErrorMessage(frame, sourceLines) {
|
|
126
|
+
const identifier = getUndefinedIdentifier(frame.error_message);
|
|
127
|
+
if (!identifier || !sourceLines?.length) return null;
|
|
128
|
+
|
|
129
|
+
const tokenPattern = new RegExp(`\\b${identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
130
|
+
const functionName = frame.function && frame.function !== "<anonymous>"
|
|
131
|
+
? String(frame.function)
|
|
132
|
+
: null;
|
|
133
|
+
|
|
134
|
+
const candidates = [];
|
|
135
|
+
for (let i = 0; i < sourceLines.length; i++) {
|
|
136
|
+
if (!tokenPattern.test(sourceLines[i])) continue;
|
|
137
|
+
candidates.push(i);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!candidates.length) return null;
|
|
141
|
+
|
|
142
|
+
if (functionName) {
|
|
143
|
+
const functionPattern = new RegExp(`\\b${functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
144
|
+
let nearestFunctionLine = -1;
|
|
145
|
+
for (let i = 0; i < sourceLines.length; i++) {
|
|
146
|
+
if (functionPattern.test(sourceLines[i])) {
|
|
147
|
+
nearestFunctionLine = i;
|
|
148
|
+
}
|
|
149
|
+
if (candidates.includes(i) && nearestFunctionLine !== -1) {
|
|
150
|
+
return i + 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return candidates[0] + 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
117
158
|
function normalizeSourcePath(source) {
|
|
118
159
|
if (!source) return "";
|
|
119
160
|
|
|
@@ -167,6 +208,114 @@ function getNodeModules() {
|
|
|
167
208
|
return nodeModulesPromise;
|
|
168
209
|
}
|
|
169
210
|
|
|
211
|
+
let cachedProjectRoots = null;
|
|
212
|
+
let configuredProjectRoot = null;
|
|
213
|
+
|
|
214
|
+
export function setProjectRoot(projectRoot) {
|
|
215
|
+
configuredProjectRoot = projectRoot || null;
|
|
216
|
+
cachedProjectRoots = null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function packageJsonHasNext(packageJson) {
|
|
220
|
+
return Boolean(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function findProjectRoots() {
|
|
224
|
+
if (cachedProjectRoots) return cachedProjectRoots;
|
|
225
|
+
|
|
226
|
+
const mods = await getNodeModules();
|
|
227
|
+
if (!mods) {
|
|
228
|
+
cachedProjectRoots = [process.cwd()];
|
|
229
|
+
return cachedProjectRoots;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { fs, path } = mods;
|
|
233
|
+
const roots = new Set();
|
|
234
|
+
const seeds = [
|
|
235
|
+
configuredProjectRoot,
|
|
236
|
+
process.env.WATCHFORGE_PROJECT_ROOT,
|
|
237
|
+
process.env.INIT_CWD,
|
|
238
|
+
process.env.PWD,
|
|
239
|
+
process.cwd(),
|
|
240
|
+
].filter(Boolean);
|
|
241
|
+
|
|
242
|
+
for (const seed of seeds) {
|
|
243
|
+
roots.add(seed);
|
|
244
|
+
|
|
245
|
+
let dir = seed;
|
|
246
|
+
for (let depth = 0; depth < 10; depth++) {
|
|
247
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
248
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
249
|
+
try {
|
|
250
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
251
|
+
if (packageJsonHasNext(packageJson)) {
|
|
252
|
+
roots.add(dir);
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
// ignore invalid package.json
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const parent = path.dirname(dir);
|
|
260
|
+
if (parent === dir) break;
|
|
261
|
+
dir = parent;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
cachedProjectRoots = [...roots];
|
|
266
|
+
return cachedProjectRoots;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function readFileLines(fs, filePath) {
|
|
270
|
+
const stat = fs.statSync(filePath);
|
|
271
|
+
if (stat.size > MAX_SOURCE_FILE_BYTES) return null;
|
|
272
|
+
return fs.readFileSync(filePath, "utf8").split("\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function findFileUnderRoot(fs, path, root, normalizedPath, basename, maxDepth = 5) {
|
|
276
|
+
const suffix = normalizedPath.replace(/^\/+/, "");
|
|
277
|
+
const directCandidate = path.join(root, suffix);
|
|
278
|
+
if (fs.existsSync(directCandidate)) {
|
|
279
|
+
return directCandidate;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
283
|
+
let scannedFiles = 0;
|
|
284
|
+
|
|
285
|
+
while (stack.length > 0 && scannedFiles < 500) {
|
|
286
|
+
const { dir, depth } = stack.pop();
|
|
287
|
+
if (depth > maxDepth) continue;
|
|
288
|
+
|
|
289
|
+
let entries = [];
|
|
290
|
+
try {
|
|
291
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
292
|
+
} catch {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const entry of entries) {
|
|
297
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === ".git") {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const entryPath = path.join(dir, entry.name);
|
|
302
|
+
if (entry.isFile()) {
|
|
303
|
+
scannedFiles += 1;
|
|
304
|
+
if (entry.name === basename) {
|
|
305
|
+
const normalizedEntry = entryPath.replace(/\\/g, "/");
|
|
306
|
+
if (normalizedEntry.endsWith(suffix)) {
|
|
307
|
+
return entryPath;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else if (entry.isDirectory()) {
|
|
311
|
+
stack.push({ dir: entryPath, depth: depth + 1 });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
170
319
|
async function readNodeSourceLines(absPath) {
|
|
171
320
|
if (!isNode || !absPath) return null;
|
|
172
321
|
|
|
@@ -185,26 +334,87 @@ async function readNodeSourceLines(absPath) {
|
|
|
185
334
|
filePath = fileURLToPath(filePath);
|
|
186
335
|
}
|
|
187
336
|
|
|
188
|
-
|
|
189
|
-
|
|
337
|
+
const normalizedPath = normalizeSourcePath(filePath);
|
|
338
|
+
const roots = await findProjectRoots();
|
|
339
|
+
const candidates = new Set();
|
|
340
|
+
|
|
341
|
+
if (path.isAbsolute(filePath)) {
|
|
342
|
+
candidates.add(filePath);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (const root of roots) {
|
|
346
|
+
candidates.add(path.resolve(root, filePath));
|
|
347
|
+
candidates.add(path.resolve(root, normalizedPath));
|
|
348
|
+
|
|
349
|
+
const srcMatch = normalizedPath.match(/(?:^|\/)?(src\/.+)$/);
|
|
350
|
+
if (srcMatch) {
|
|
351
|
+
candidates.add(path.resolve(root, srcMatch[1]));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const candidate of candidates) {
|
|
356
|
+
if (!candidate || !fs.existsSync(candidate)) continue;
|
|
357
|
+
const lines = readFileLines(fs, candidate);
|
|
358
|
+
if (lines) return lines;
|
|
190
359
|
}
|
|
191
360
|
|
|
192
|
-
|
|
361
|
+
const basename = path.basename(normalizedPath);
|
|
362
|
+
for (const root of roots) {
|
|
363
|
+
const packageJsonPath = path.join(root, "package.json");
|
|
364
|
+
if (!fs.existsSync(packageJsonPath)) continue;
|
|
365
|
+
|
|
366
|
+
let isNextProject = root === process.env.INIT_CWD || root === process.env.PWD;
|
|
367
|
+
if (!isNextProject) {
|
|
368
|
+
try {
|
|
369
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
370
|
+
isNextProject = packageJsonHasNext(packageJson);
|
|
371
|
+
} catch {
|
|
372
|
+
isNextProject = false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
193
375
|
|
|
194
|
-
|
|
195
|
-
|
|
376
|
+
if (!isNextProject) continue;
|
|
377
|
+
|
|
378
|
+
const discovered = findFileUnderRoot(fs, path, root, normalizedPath, basename);
|
|
379
|
+
if (discovered && fs.existsSync(discovered)) {
|
|
380
|
+
const lines = readFileLines(fs, discovered);
|
|
381
|
+
if (lines) return lines;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
196
384
|
|
|
197
|
-
|
|
198
|
-
return content.split("\n");
|
|
385
|
+
return null;
|
|
199
386
|
} catch {
|
|
200
387
|
return null;
|
|
201
388
|
}
|
|
202
389
|
}
|
|
203
390
|
|
|
391
|
+
function resolveFrameLine(frame, lines) {
|
|
392
|
+
let lineno = frame.lineno;
|
|
393
|
+
if (!lines?.length || !lineno) return lineno;
|
|
394
|
+
|
|
395
|
+
if (lineno > lines.length) {
|
|
396
|
+
const inferredLine = inferLineFromErrorMessage(frame, lines);
|
|
397
|
+
if (inferredLine) {
|
|
398
|
+
lineno = inferredLine;
|
|
399
|
+
frame.lineno = inferredLine;
|
|
400
|
+
const identifier = getUndefinedIdentifier(frame.error_message);
|
|
401
|
+
if (identifier) {
|
|
402
|
+
const col = lines[inferredLine - 1].indexOf(identifier);
|
|
403
|
+
if (col >= 0) frame.colno = col + 1;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return lineno;
|
|
409
|
+
}
|
|
410
|
+
|
|
204
411
|
async function enrichFrameWithNodeSource(frame) {
|
|
205
412
|
if (!frame.in_app || !frame.lineno) return;
|
|
206
413
|
const lines = await readNodeSourceLines(frame.raw_abs_path || frame.abs_path);
|
|
207
|
-
if (lines)
|
|
414
|
+
if (!lines) return;
|
|
415
|
+
|
|
416
|
+
const lineno = resolveFrameLine(frame, lines);
|
|
417
|
+
applySourceContext(frame, lines, lineno);
|
|
208
418
|
}
|
|
209
419
|
|
|
210
420
|
function getBrowserSourceFetchCandidates(absPath, rawAbsPath) {
|
|
@@ -475,13 +685,21 @@ async function enrichFrameWithBrowserSource(frame) {
|
|
|
475
685
|
await enrichFrameWithSourceMap(frame);
|
|
476
686
|
}
|
|
477
687
|
|
|
478
|
-
export async function enrichStacktraceAsync(stacktrace) {
|
|
688
|
+
export async function enrichStacktraceAsync(stacktrace, errorMessage = null) {
|
|
479
689
|
if (!stacktrace?.frames?.length) return stacktrace;
|
|
480
690
|
|
|
481
691
|
const inAppFrames = stacktrace.frames.filter((f) => f.in_app);
|
|
482
692
|
// Enrich the error frame first (last in-app frame), then nearby frames.
|
|
483
693
|
const targets = [...inAppFrames.slice(-5)].reverse();
|
|
484
694
|
|
|
695
|
+
if (errorMessage) {
|
|
696
|
+
for (const frame of targets) {
|
|
697
|
+
if (!frame.error_message) {
|
|
698
|
+
frame.error_message = errorMessage;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
485
703
|
if (isNode) {
|
|
486
704
|
await Promise.all(targets.map((frame) => enrichFrameWithNodeSource(frame)));
|
|
487
705
|
} else if (isBrowser) {
|
package/src/tracing.js
CHANGED