@watchforge/browser 0.1.5 → 0.1.6
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/package.json +1 -1
- package/src/stacktrace.js +121 -36
package/package.json
CHANGED
package/src/stacktrace.js
CHANGED
|
@@ -68,16 +68,21 @@ export function buildStacktraceFromError(error) {
|
|
|
68
68
|
if (!parsed) continue;
|
|
69
69
|
|
|
70
70
|
const { function: functionName, file, lineno, colno } = parsed;
|
|
71
|
-
const
|
|
72
|
-
const
|
|
71
|
+
const normalizedPath = normalizeSourcePath(file) || file;
|
|
72
|
+
const filename =
|
|
73
|
+
normalizedPath.split("/").pop()?.split("?")[0] ||
|
|
74
|
+
file.split("/").pop()?.split("?")[0] ||
|
|
75
|
+
file;
|
|
76
|
+
const inApp = !isLibraryPath(file) && !isLibraryPath(normalizedPath);
|
|
73
77
|
|
|
74
78
|
frames.push({
|
|
75
79
|
filename,
|
|
76
|
-
abs_path:
|
|
80
|
+
abs_path: normalizedPath,
|
|
81
|
+
raw_abs_path: file,
|
|
77
82
|
lineno,
|
|
78
83
|
colno,
|
|
79
84
|
function: functionName,
|
|
80
|
-
module:
|
|
85
|
+
module: normalizedPath,
|
|
81
86
|
context_line: null,
|
|
82
87
|
pre_context: [],
|
|
83
88
|
post_context: [],
|
|
@@ -198,44 +203,73 @@ async function readNodeSourceLines(absPath) {
|
|
|
198
203
|
|
|
199
204
|
async function enrichFrameWithNodeSource(frame) {
|
|
200
205
|
if (!frame.in_app || !frame.lineno) return;
|
|
201
|
-
const lines = await readNodeSourceLines(frame.abs_path);
|
|
206
|
+
const lines = await readNodeSourceLines(frame.raw_abs_path || frame.abs_path);
|
|
202
207
|
if (lines) applySourceContext(frame, lines, frame.lineno);
|
|
203
208
|
}
|
|
204
209
|
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
function getBrowserSourceFetchCandidates(absPath, rawAbsPath) {
|
|
211
|
+
const candidates = new Set();
|
|
212
|
+
const paths = [absPath, rawAbsPath].filter(Boolean);
|
|
207
213
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
214
|
+
for (const path of paths) {
|
|
215
|
+
const normalized = normalizeSourcePath(path);
|
|
216
|
+
if (!normalized) continue;
|
|
217
|
+
|
|
218
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
219
|
+
candidates.add(path);
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
}
|
|
222
|
+
if (normalized.startsWith("/")) {
|
|
223
|
+
candidates.add(`${window.location.origin}${normalized}`);
|
|
224
|
+
} else {
|
|
225
|
+
candidates.add(`${window.location.origin}/${normalized}`);
|
|
226
|
+
candidates.add(new URL(normalized, window.location.href).href);
|
|
220
227
|
}
|
|
221
228
|
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
if (path.startsWith("webpack-internal://")) {
|
|
230
|
+
const stripped = path
|
|
231
|
+
.replace(/^webpack-internal:\/\/\/?/, "/")
|
|
232
|
+
.replace(/^webpack-internal:\/\//, "/");
|
|
233
|
+
candidates.add(`${window.location.origin}${stripped}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
224
236
|
|
|
225
|
-
|
|
226
|
-
|
|
237
|
+
return [...candidates];
|
|
238
|
+
}
|
|
227
239
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
240
|
+
async function fetchBrowserSourceLines(absPath, rawAbsPath) {
|
|
241
|
+
if (!isBrowser || !absPath) return null;
|
|
232
242
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
243
|
+
const candidates = getBrowserSourceFetchCandidates(absPath, rawAbsPath);
|
|
244
|
+
|
|
245
|
+
for (const candidate of candidates) {
|
|
246
|
+
try {
|
|
247
|
+
const target = new URL(candidate, window.location.href);
|
|
248
|
+
if (target.origin !== window.location.origin) continue;
|
|
249
|
+
|
|
250
|
+
const response = await fetch(target.href, { credentials: "same-origin" });
|
|
251
|
+
if (!response.ok) continue;
|
|
252
|
+
|
|
253
|
+
const contentType = response.headers.get("content-type") || "";
|
|
254
|
+
if (
|
|
255
|
+
!contentType.includes("javascript") &&
|
|
256
|
+
!contentType.includes("text") &&
|
|
257
|
+
!contentType.includes("typescript") &&
|
|
258
|
+
!contentType.includes("json")
|
|
259
|
+
) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const text = await response.text();
|
|
264
|
+
if (text.length > MAX_SOURCE_FILE_BYTES) continue;
|
|
265
|
+
if (!text.includes("\n") && !text.includes(";") && text.length > 500) continue;
|
|
266
|
+
return text.split("\n");
|
|
267
|
+
} catch {
|
|
268
|
+
// try next candidate
|
|
269
|
+
}
|
|
238
270
|
}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
239
273
|
}
|
|
240
274
|
|
|
241
275
|
async function getSourceMapConsumer() {
|
|
@@ -294,9 +328,55 @@ async function fetchBrowserSourceMap(absPath) {
|
|
|
294
328
|
}
|
|
295
329
|
}
|
|
296
330
|
|
|
331
|
+
async function findSourceMapForFrame(frame) {
|
|
332
|
+
const wanted = normalizeSourcePath(frame.abs_path || frame.module);
|
|
333
|
+
const scriptUrls = [];
|
|
334
|
+
|
|
335
|
+
if (frame.raw_abs_path?.startsWith("http://") || frame.raw_abs_path?.startsWith("https://")) {
|
|
336
|
+
scriptUrls.push(frame.raw_abs_path);
|
|
337
|
+
}
|
|
338
|
+
if (frame.abs_path?.startsWith("http://") || frame.abs_path?.startsWith("https://")) {
|
|
339
|
+
scriptUrls.push(frame.abs_path);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (isBrowser) {
|
|
343
|
+
for (const script of document.querySelectorAll("script[src]")) {
|
|
344
|
+
const src = script.getAttribute("src");
|
|
345
|
+
if (!src) continue;
|
|
346
|
+
try {
|
|
347
|
+
const url = new URL(src, window.location.href);
|
|
348
|
+
if (url.origin === window.location.origin) {
|
|
349
|
+
scriptUrls.push(url.href);
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
// ignore invalid script src
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const seen = new Set();
|
|
358
|
+
for (const scriptUrl of scriptUrls) {
|
|
359
|
+
if (!scriptUrl || seen.has(scriptUrl)) continue;
|
|
360
|
+
seen.add(scriptUrl);
|
|
361
|
+
|
|
362
|
+
const sourceMap = await fetchBrowserSourceMap(scriptUrl);
|
|
363
|
+
if (!sourceMap) continue;
|
|
364
|
+
|
|
365
|
+
const sources = Array.isArray(sourceMap.sources) ? sourceMap.sources : [];
|
|
366
|
+
const hasMatch = sources.some((source) => sourceMatchesFrame(source, wanted));
|
|
367
|
+
if (hasMatch || !wanted) {
|
|
368
|
+
return { sourceMap, scriptUrl };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
297
375
|
async function enrichFrameWithSourceMap(frame) {
|
|
298
|
-
const
|
|
299
|
-
if (!
|
|
376
|
+
const resolved = await findSourceMapForFrame(frame);
|
|
377
|
+
if (!resolved) return false;
|
|
378
|
+
|
|
379
|
+
const { sourceMap } = resolved;
|
|
300
380
|
|
|
301
381
|
try {
|
|
302
382
|
const SourceMapConsumer = await getSourceMapConsumer();
|
|
@@ -358,7 +438,9 @@ function getSourceContentFromWebpackFrame(frame) {
|
|
|
358
438
|
value.some((item) => Array.isArray(item) && item.length >= 2)
|
|
359
439
|
) || [];
|
|
360
440
|
|
|
361
|
-
const framePath = normalizeSourcePath(
|
|
441
|
+
const framePath = normalizeSourcePath(
|
|
442
|
+
frame.abs_path || frame.module || frame.raw_abs_path
|
|
443
|
+
);
|
|
362
444
|
if (!framePath || !Array.isArray(webpackChunks)) return null;
|
|
363
445
|
|
|
364
446
|
for (const chunk of webpackChunks) {
|
|
@@ -384,7 +466,7 @@ async function enrichFrameWithBrowserSource(frame) {
|
|
|
384
466
|
if (frame.context_line) return;
|
|
385
467
|
}
|
|
386
468
|
|
|
387
|
-
const lines = await fetchBrowserSourceLines(frame.abs_path);
|
|
469
|
+
const lines = await fetchBrowserSourceLines(frame.abs_path, frame.raw_abs_path);
|
|
388
470
|
if (lines) {
|
|
389
471
|
applySourceContext(frame, lines, frame.lineno);
|
|
390
472
|
if (frame.context_line) return;
|
|
@@ -397,12 +479,15 @@ export async function enrichStacktraceAsync(stacktrace) {
|
|
|
397
479
|
if (!stacktrace?.frames?.length) return stacktrace;
|
|
398
480
|
|
|
399
481
|
const inAppFrames = stacktrace.frames.filter((f) => f.in_app);
|
|
400
|
-
|
|
482
|
+
// Enrich the error frame first (last in-app frame), then nearby frames.
|
|
483
|
+
const targets = [...inAppFrames.slice(-5)].reverse();
|
|
401
484
|
|
|
402
485
|
if (isNode) {
|
|
403
486
|
await Promise.all(targets.map((frame) => enrichFrameWithNodeSource(frame)));
|
|
404
487
|
} else if (isBrowser) {
|
|
405
|
-
|
|
488
|
+
for (const frame of targets) {
|
|
489
|
+
await enrichFrameWithBrowserSource(frame);
|
|
490
|
+
}
|
|
406
491
|
}
|
|
407
492
|
|
|
408
493
|
return stacktrace;
|