@watchforge/browser 0.1.15 → 0.1.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@watchforge/browser",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "main": "./src/index.js",
5
5
  "types": "./src/index.d.ts",
6
6
  "description": "WatchForge JavaScript SDK for browser JavaScript, Next.js, React, Node.js, and Express.js",
package/src/client.js CHANGED
@@ -25,6 +25,7 @@ let CAPTURE_CONSOLE_ERRORS = true;
25
25
  let CAPTURE_RESOURCE_ERRORS = true;
26
26
  let CAPTURE_FAILED_REQUESTS = true;
27
27
  let CAPTURE_NODE_WARNINGS = false;
28
+ let CAPTURE_NODE_MULTIPLE_RESOLVES = false;
28
29
 
29
30
  // Detect environment
30
31
  const isBrowser = typeof window !== "undefined";
@@ -594,6 +595,7 @@ export function register({
594
595
  captureResourceErrors = true,
595
596
  captureFailedRequests = true,
596
597
  captureNodeWarnings = false,
598
+ captureNodeMultipleResolves = false,
597
599
  projectRoot = null,
598
600
  }) {
599
601
  const nextBrowserRegisterKey = isBrowser
@@ -626,6 +628,7 @@ export function register({
626
628
  CAPTURE_RESOURCE_ERRORS = Boolean(captureResourceErrors);
627
629
  CAPTURE_FAILED_REQUESTS = Boolean(captureFailedRequests);
628
630
  CAPTURE_NODE_WARNINGS = Boolean(captureNodeWarnings);
631
+ CAPTURE_NODE_MULTIPLE_RESOLVES = Boolean(captureNodeMultipleResolves);
629
632
  setProjectRoot(
630
633
  projectRoot ||
631
634
  (isNode ? process.env.WATCHFORGE_PROJECT_ROOT || process.env.INIT_CWD : null)
@@ -682,11 +685,23 @@ export function register({
682
685
  });
683
686
 
684
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;
685
700
  void captureException(
686
701
  normalizeException(reason, `Node.js promise multipleResolves: ${type}`),
687
702
  {
688
703
  tags: {
689
- handled: false,
704
+ handled: true,
690
705
  mechanism: "process.multipleResolves",
691
706
  type,
692
707
  },
package/src/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface WatchForgeRegisterOptions {
13
13
  captureResourceErrors?: boolean;
14
14
  captureFailedRequests?: boolean;
15
15
  captureNodeWarnings?: boolean;
16
+ captureNodeMultipleResolves?: boolean;
16
17
  projectRoot?: string | null;
17
18
  }
18
19
 
package/src/stacktrace.js CHANGED
@@ -122,6 +122,219 @@ function getUndefinedIdentifier(message) {
122
122
  return match?.[1] || null;
123
123
  }
124
124
 
125
+ function getReadPropertyName(message) {
126
+ if (!message || typeof message !== "string") return null;
127
+ const match = message.match(/Cannot read properties of (?:null|undefined) \(reading ['"]([^'"]+)['"]\)/);
128
+ return match?.[1] || null;
129
+ }
130
+
131
+ function isJsonParseError(message) {
132
+ if (!message || typeof message !== "string") return false;
133
+ return (
134
+ /in JSON at position/i.test(message) ||
135
+ /Unexpected token .* in JSON/i.test(message) ||
136
+ (/SyntaxError/i.test(message) && /JSON/i.test(message))
137
+ );
138
+ }
139
+
140
+ function isUriError(message) {
141
+ if (!message || typeof message !== "string") return false;
142
+ return /URI(?:Error| malformed)/i.test(message) || /decodeURIComponent/i.test(message);
143
+ }
144
+
145
+ function isInvalidArrayLengthError(message) {
146
+ if (!message || typeof message !== "string") return false;
147
+ return /Invalid array length/i.test(message);
148
+ }
149
+
150
+ function lineHasJsonParse(line) {
151
+ return Boolean(line && /JSON\.parse\s*\(/.test(line));
152
+ }
153
+
154
+ function lineHasDecodeUriComponent(line) {
155
+ return Boolean(line && /decodeURIComponent\s*\(/.test(line));
156
+ }
157
+
158
+ function lineHasNewArray(line) {
159
+ return Boolean(line && /new Array\s*\(/.test(line));
160
+ }
161
+
162
+ function lineHasInvalidArrayLiteral(line) {
163
+ return Boolean(line && /new Array\s*\(\s*-/.test(line));
164
+ }
165
+
166
+ function isInsideSetTimeout(sourceLines, lineIndex, scopeStart = 0) {
167
+ for (let i = lineIndex; i >= Math.max(scopeStart, lineIndex - 12); i--) {
168
+ if (/setTimeout\s*\(/.test(sourceLines[i])) return true;
169
+ }
170
+ return false;
171
+ }
172
+
173
+ function lineLooksLikeJsxRender(line) {
174
+ if (!line || typeof line !== "string") return false;
175
+ const trimmed = line.trim();
176
+ return (
177
+ trimmed.startsWith("return (") ||
178
+ trimmed.startsWith("return <") ||
179
+ trimmed.startsWith("<") ||
180
+ /^<\//.test(trimmed)
181
+ );
182
+ }
183
+
184
+ function getFunctionScopeStart(sourceLines, functionName) {
185
+ if (!sourceLines?.length || !functionName) return -1;
186
+
187
+ const normalized = String(functionName).replace(/\.useEffect$/, "");
188
+ const patterns = [];
189
+
190
+ if (functionName.includes("useEffect")) {
191
+ patterns.push(/useEffect\s*\(/);
192
+ }
193
+ if (normalized && normalized !== "<anonymous>") {
194
+ const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
195
+ patterns.push(new RegExp(`\\b${escaped}\\s*=\\s*(?:async\\s*)?\\(`));
196
+ patterns.push(new RegExp(`function\\s+${escaped}\\s*\\(`));
197
+ }
198
+
199
+ for (const pattern of patterns) {
200
+ for (let i = 0; i < sourceLines.length; i++) {
201
+ if (pattern.test(sourceLines[i])) return i;
202
+ }
203
+ }
204
+
205
+ return -1;
206
+ }
207
+
208
+ function getUseEffectBodyRange(sourceLines, scopeStart) {
209
+ if (scopeStart < 0 || !sourceLines?.length) return null;
210
+
211
+ for (let i = scopeStart + 1; i < sourceLines.length; i++) {
212
+ if (/^\s*\}\s*,\s*\[/.test(sourceLines[i])) {
213
+ return { start: scopeStart, end: i };
214
+ }
215
+ }
216
+
217
+ return null;
218
+ }
219
+
220
+ function isInsideUseEffectBody(sourceLines, lineIndex, scopeStart) {
221
+ const range = getUseEffectBodyRange(sourceLines, scopeStart);
222
+ if (!range || lineIndex < 0) return scopeStart >= 0 ? lineIndex >= scopeStart : true;
223
+ return lineIndex >= range.start && lineIndex <= range.end;
224
+ }
225
+
226
+ function filterCandidatesToScope(sourceLines, candidates, scopeStart, functionName) {
227
+ if (!candidates.length) return candidates;
228
+ if (!functionName?.includes("useEffect") || scopeStart < 0) return candidates;
229
+
230
+ const inBody = candidates.filter((idx) =>
231
+ isInsideUseEffectBody(sourceLines, idx, scopeStart)
232
+ );
233
+ return inBody.length ? inBody : candidates;
234
+ }
235
+
236
+ function pickCandidateNearScope(sourceLines, candidates, scopeStart, functionName) {
237
+ if (!candidates.length) return null;
238
+
239
+ const scoped = filterCandidatesToScope(sourceLines, candidates, scopeStart, functionName);
240
+ if (scopeStart < 0) return scoped[0] + 1;
241
+
242
+ const afterStart = scoped.filter((idx) => idx >= scopeStart);
243
+ const pool = afterStart.length ? afterStart : scoped;
244
+ return pool[0] + 1;
245
+ }
246
+
247
+ function pickCandidatePreferringSetTimeout(sourceLines, candidates, scopeStart, functionName) {
248
+ if (!candidates.length) return null;
249
+
250
+ const scoped = filterCandidatesToScope(sourceLines, candidates, scopeStart, functionName);
251
+ const pool =
252
+ scopeStart >= 0
253
+ ? scoped.filter((idx) => idx >= scopeStart).length
254
+ ? scoped.filter((idx) => idx >= scopeStart)
255
+ : scoped
256
+ : scoped;
257
+
258
+ const inSetTimeout = pool.filter((idx) =>
259
+ isInsideSetTimeout(sourceLines, idx, scopeStart)
260
+ );
261
+ if (inSetTimeout.length) return inSetTimeout[0] + 1;
262
+
263
+ return pool[0] + 1;
264
+ }
265
+
266
+ function inferJsonParseLine(frame, sourceLines) {
267
+ if (!isJsonParseError(frame.error_message) || !sourceLines?.length) return null;
268
+
269
+ const candidates = [];
270
+ for (let i = 0; i < sourceLines.length; i++) {
271
+ if (lineHasJsonParse(sourceLines[i])) candidates.push(i);
272
+ }
273
+
274
+ const scopeStart = getFunctionScopeStart(sourceLines, frame.function);
275
+ return pickCandidatePreferringSetTimeout(
276
+ sourceLines,
277
+ candidates,
278
+ scopeStart,
279
+ frame.function
280
+ );
281
+ }
282
+
283
+ function inferUriErrorLine(frame, sourceLines) {
284
+ if (!isUriError(frame.error_message) || !sourceLines?.length) return null;
285
+
286
+ const candidates = [];
287
+ for (let i = 0; i < sourceLines.length; i++) {
288
+ if (lineHasDecodeUriComponent(sourceLines[i])) candidates.push(i);
289
+ }
290
+
291
+ const scopeStart = getFunctionScopeStart(sourceLines, frame.function);
292
+ return pickCandidatePreferringSetTimeout(
293
+ sourceLines,
294
+ candidates,
295
+ scopeStart,
296
+ frame.function
297
+ );
298
+ }
299
+
300
+ function inferInvalidArrayLengthLine(frame, sourceLines) {
301
+ if (!isInvalidArrayLengthError(frame.error_message) || !sourceLines?.length) return null;
302
+
303
+ const scopeStart = getFunctionScopeStart(sourceLines, frame.function);
304
+ const candidates = [];
305
+ for (let i = 0; i < sourceLines.length; i++) {
306
+ if (lineHasNewArray(sourceLines[i])) candidates.push(i);
307
+ }
308
+ if (!candidates.length) return null;
309
+
310
+ const scoped = scopeStart >= 0 ? candidates.filter((idx) => idx >= scopeStart) : candidates;
311
+ const pool = scoped.length ? scoped : candidates;
312
+
313
+ const negativeLiteral = pool.filter((idx) =>
314
+ lineHasInvalidArrayLiteral(sourceLines[idx])
315
+ );
316
+ if (negativeLiteral.length) return negativeLiteral[0] + 1;
317
+
318
+ const inSetTimeout = pool.filter((idx) =>
319
+ isInsideSetTimeout(sourceLines, idx, scopeStart)
320
+ );
321
+ if (inSetTimeout.length) return inSetTimeout[0] + 1;
322
+
323
+ return pool[0] + 1;
324
+ }
325
+
326
+ function lineHasToken(line, token) {
327
+ if (!line || !token) return false;
328
+ const tokenPattern = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
329
+ return tokenPattern.test(line);
330
+ }
331
+
332
+ function lineHasPropertyAccess(line, property) {
333
+ if (!line || !property) return false;
334
+ const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
335
+ return new RegExp(`(?:\\.${escaped}\\b|\\[['"]${escaped}['"]\\]|\\["${escaped}"\\])`).test(line);
336
+ }
337
+
125
338
  function inferLineFromErrorMessage(frame, sourceLines) {
126
339
  const identifier = getUndefinedIdentifier(frame.error_message);
127
340
  if (!identifier || !sourceLines?.length) return null;
@@ -155,6 +368,100 @@ function inferLineFromErrorMessage(frame, sourceLines) {
155
368
  return candidates[0] + 1;
156
369
  }
157
370
 
371
+ function inferPropertyLineFromErrorMessage(frame, sourceLines) {
372
+ const property = getReadPropertyName(frame.error_message);
373
+ if (!property || !sourceLines?.length) return null;
374
+
375
+ for (let i = 0; i < sourceLines.length; i++) {
376
+ if (lineHasPropertyAccess(sourceLines[i], property)) {
377
+ return i + 1;
378
+ }
379
+ }
380
+
381
+ return null;
382
+ }
383
+
384
+ function setFrameLineFromSource(frame, sourceLines, lineno, tokenForColumn) {
385
+ if (!lineno || !sourceLines[lineno - 1]) return lineno;
386
+
387
+ frame.lineno = lineno;
388
+ if (tokenForColumn) {
389
+ const col = sourceLines[lineno - 1].indexOf(tokenForColumn);
390
+ if (col >= 0) frame.colno = col + 1;
391
+ }
392
+ return lineno;
393
+ }
394
+
395
+ function adjustLineFromErrorMessage(frame, sourceLines, lineno) {
396
+ if (!sourceLines?.length) return lineno;
397
+
398
+ const currentLine = lineno ? sourceLines[lineno - 1] : null;
399
+ const scopeStart = getFunctionScopeStart(sourceLines, frame.function);
400
+ const mappedOutsideUseEffect =
401
+ frame.function?.includes("useEffect") &&
402
+ lineno &&
403
+ !isInsideUseEffectBody(sourceLines, lineno - 1, scopeStart);
404
+ const mappedToRenderLine =
405
+ lineLooksLikeJsxRender(currentLine) &&
406
+ (frame.function?.includes("useEffect") || frame.function?.includes("setTimeout"));
407
+ const shouldReinfer = mappedToRenderLine || mappedOutsideUseEffect;
408
+
409
+ const identifier = getUndefinedIdentifier(frame.error_message);
410
+ if (identifier && (shouldReinfer || !lineHasToken(currentLine, identifier))) {
411
+ const inferredLine = inferLineFromErrorMessage(frame, sourceLines);
412
+ if (inferredLine) {
413
+ return setFrameLineFromSource(frame, sourceLines, inferredLine, identifier);
414
+ }
415
+ }
416
+
417
+ const property = getReadPropertyName(frame.error_message);
418
+ if (property && (shouldReinfer || !lineHasPropertyAccess(currentLine, property))) {
419
+ const inferredLine = inferPropertyLineFromErrorMessage(frame, sourceLines);
420
+ if (inferredLine) {
421
+ return setFrameLineFromSource(frame, sourceLines, inferredLine, `.${property}`);
422
+ }
423
+ }
424
+
425
+ if (isJsonParseError(frame.error_message) && (shouldReinfer || !lineHasJsonParse(currentLine))) {
426
+ const inferredLine = inferJsonParseLine(frame, sourceLines);
427
+ if (inferredLine) {
428
+ return setFrameLineFromSource(frame, sourceLines, inferredLine, "JSON.parse");
429
+ }
430
+ }
431
+
432
+ if (isUriError(frame.error_message) && (shouldReinfer || !lineHasDecodeUriComponent(currentLine))) {
433
+ const inferredLine = inferUriErrorLine(frame, sourceLines);
434
+ if (inferredLine) {
435
+ return setFrameLineFromSource(frame, sourceLines, inferredLine, "decodeURIComponent");
436
+ }
437
+ }
438
+
439
+ if (isInvalidArrayLengthError(frame.error_message)) {
440
+ const inferredLine = inferInvalidArrayLengthLine(frame, sourceLines);
441
+ if (inferredLine) {
442
+ const inferredSourceLine = sourceLines[inferredLine - 1];
443
+ const shouldReplace =
444
+ shouldReinfer ||
445
+ !lineHasNewArray(currentLine) ||
446
+ (lineHasInvalidArrayLiteral(inferredSourceLine) &&
447
+ !lineHasInvalidArrayLiteral(currentLine)) ||
448
+ (frame.function?.includes("useEffect") &&
449
+ isInsideSetTimeout(sourceLines, inferredLine - 1) &&
450
+ !isInsideSetTimeout(
451
+ sourceLines,
452
+ lineno ? lineno - 1 : -1,
453
+ getFunctionScopeStart(sourceLines, frame.function)
454
+ ));
455
+
456
+ if (shouldReplace) {
457
+ return setFrameLineFromSource(frame, sourceLines, inferredLine, "new Array");
458
+ }
459
+ }
460
+ }
461
+
462
+ return lineno;
463
+ }
464
+
158
465
  function normalizeSourcePath(source) {
159
466
  if (!source) return "";
160
467
 
@@ -392,6 +699,8 @@ function resolveFrameLine(frame, lines) {
392
699
  let lineno = frame.lineno;
393
700
  if (!lines?.length || !lineno) return lineno;
394
701
 
702
+ lineno = adjustLineFromErrorMessage(frame, lines, lineno);
703
+
395
704
  if (lineno > lines.length) {
396
705
  const inferredLine = inferLineFromErrorMessage(frame, lines);
397
706
  if (inferredLine) {
@@ -569,6 +878,53 @@ function findInlineSourceMap(sourceText) {
569
878
  return null;
570
879
  }
571
880
 
881
+ function isOriginalSourceFrame(frame) {
882
+ const normalized = normalizeSourcePath(frame.abs_path || frame.module || frame.raw_abs_path);
883
+ if (!normalized) return false;
884
+
885
+ if (/^_next\//.test(normalized) || /\/_next\//.test(normalized)) return false;
886
+ if (/\.(?:m?js|css)(?:\.map)?$/.test(normalized) && !/\.(?:tsx?|jsx?)$/.test(normalized)) {
887
+ return false;
888
+ }
889
+
890
+ return (
891
+ /(?:^|\/)src\/.+\.(?:tsx?|jsx?)$/.test(normalized) ||
892
+ /\.(?:tsx?|jsx?)$/.test(normalized)
893
+ );
894
+ }
895
+
896
+ function getSourceMapContentForFrame(frame, sourceMap) {
897
+ if (!sourceMap || !Array.isArray(sourceMap.sources)) return null;
898
+
899
+ const sourceIndex = sourceMap.sources.findIndex((source) =>
900
+ sourceMatchesFrame(source, frame.abs_path || frame.module || frame.raw_abs_path)
901
+ );
902
+ if (sourceIndex < 0) return null;
903
+
904
+ const content = Array.isArray(sourceMap.sourcesContent)
905
+ ? sourceMap.sourcesContent[sourceIndex]
906
+ : null;
907
+ if (!content) return null;
908
+
909
+ return {
910
+ source: sourceMap.sources[sourceIndex],
911
+ lines: content.split("\n"),
912
+ };
913
+ }
914
+
915
+ function applyOriginalSourceFrameContext(frame, sourceMap) {
916
+ if (!isOriginalSourceFrame(frame) || !frame.lineno) return false;
917
+
918
+ const sourceContent = getSourceMapContentForFrame(frame, sourceMap);
919
+ if (!sourceContent?.lines?.length) return false;
920
+ if (frame.lineno > sourceContent.lines.length) return false;
921
+
922
+ frame.abs_path = sourceContent.source;
923
+ frame.filename = normalizeSourcePath(sourceContent.source).split("/").pop() || frame.filename;
924
+ applySourceContext(frame, sourceContent.lines, frame.lineno);
925
+ return Boolean(frame.context_line);
926
+ }
927
+
572
928
  async function findSourceMapForFrame(frame) {
573
929
  const wanted = normalizeSourcePath(frame.abs_path || frame.module);
574
930
  const scriptUrls = [];
@@ -615,29 +971,13 @@ async function findSourceMapForFrame(frame) {
615
971
 
616
972
  async function applySourceMapToFrame(frame, sourceMap) {
617
973
  try {
974
+ if (applyOriginalSourceFrameContext(frame, sourceMap)) {
975
+ return true;
976
+ }
977
+
618
978
  const SourceMapConsumer = await getSourceMapConsumer();
619
979
  if (!SourceMapConsumer) return false;
620
980
 
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
-
641
981
  const consumer = await new SourceMapConsumer(sourceMap);
642
982
  let source = null;
643
983
  let lineno = frame.lineno;
@@ -655,7 +995,7 @@ async function applySourceMapToFrame(frame, sourceMap) {
655
995
  }
656
996
  }
657
997
 
658
- if (!source) {
998
+ if (!source && sourceMap.sources?.length) {
659
999
  source = sourceMap.sources?.find((candidate) =>
660
1000
  sourceMatchesFrame(candidate, frame.abs_path)
661
1001
  );
@@ -674,11 +1014,15 @@ async function applySourceMapToFrame(frame, sourceMap) {
674
1014
 
675
1015
  if (!content) return false;
676
1016
 
1017
+ const sourceLines = content.split("\n");
1018
+ lineno = adjustLineFromErrorMessage(frame, sourceLines, lineno);
1019
+ colno = frame.colno ?? colno;
1020
+
677
1021
  frame.abs_path = source;
678
1022
  frame.filename = normalizeSourcePath(source).split("/").pop() || frame.filename;
679
1023
  frame.lineno = lineno;
680
1024
  frame.colno = colno;
681
- applySourceContext(frame, content.split("\n"), lineno);
1025
+ applySourceContext(frame, sourceLines, lineno);
682
1026
  return Boolean(frame.context_line);
683
1027
  } catch {
684
1028
  return false;
@@ -737,20 +1081,30 @@ function getWebpackFrameArtifacts(frame) {
737
1081
  async function enrichFrameWithBrowserSource(frame) {
738
1082
  if (!frame.in_app || !frame.lineno) return;
739
1083
 
1084
+ if (isOriginalSourceFrame(frame)) {
1085
+ const lines = await fetchBrowserSourceLines(frame.abs_path, frame.raw_abs_path);
1086
+ if (lines) {
1087
+ applySourceContext(frame, lines, frame.lineno);
1088
+ if (frame.context_line) return;
1089
+ }
1090
+ }
1091
+
740
1092
  const webpackArtifacts = getWebpackFrameArtifacts(frame);
741
1093
  if (webpackArtifacts?.sourceMap) {
742
1094
  const applied = await applySourceMapToFrame(frame, webpackArtifacts.sourceMap);
743
1095
  if (applied) return;
744
1096
  }
745
1097
 
746
- if (webpackArtifacts?.lines) {
747
- applySourceContext(frame, webpackArtifacts.lines, frame.lineno);
1098
+ if (webpackArtifacts?.lines && !webpackArtifacts?.sourceMap) {
1099
+ const lineno = resolveFrameLine(frame, webpackArtifacts.lines);
1100
+ applySourceContext(frame, webpackArtifacts.lines, lineno);
748
1101
  if (frame.context_line) return;
749
1102
  }
750
1103
 
751
1104
  const lines = await fetchBrowserSourceLines(frame.abs_path, frame.raw_abs_path);
752
1105
  if (lines) {
753
- applySourceContext(frame, lines, frame.lineno);
1106
+ const lineno = resolveFrameLine(frame, lines);
1107
+ applySourceContext(frame, lines, lineno);
754
1108
  if (frame.context_line) return;
755
1109
  }
756
1110