Skip to content

Commit dfec07e

Browse files
szuenddevtools-frontend-scoped@luci-project-accounts.iam.gserviceaccount.com
authored andcommitted
Reconstruct structured stack traces from Error.stack strings
This CL adds a new 'createFromErrorStackLikeString' to StackTraceModel that wires up the new Error.stack parser to the stack trace machinery. We also wire up translating of eval origin positions. R=pfaffe@chromium.org Bug: 485142682 Change-Id: Ia656b9e53f1b93afb546ef2b4f9fad0d7c1d4f81 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7754111 Reviewed-by: Philip Pfaffe <pfaffe@chromium.org> Commit-Queue: Simon Zünd <szuend@chromium.org>
1 parent e56df2a commit dfec07e

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

front_end/models/stack_trace/StackTraceModel.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,4 +501,100 @@ describe('StackTraceModel', () => {
501501
assert.strictEqual(stackTrace.syncFragment.frames[2].rawName, 'foo');
502502
});
503503
});
504+
505+
describe('createFromErrorStackLikeString', () => {
506+
it('correctly handles a stack trace with sync and async fragments', async () => {
507+
const {model} = setup();
508+
509+
const stackTrace = await model.createFromErrorStackLikeString(
510+
`Error: foo
511+
at foo (foo.js:1:10)
512+
at bar (foo.js:2:20)`,
513+
identityTranslateFn, {
514+
exceptionId: 1,
515+
text: 'Uncaught Error: foo',
516+
lineNumber: 0,
517+
columnNumber: 0,
518+
stackTrace: {
519+
callFrames: [
520+
{
521+
functionName: 'foo',
522+
url: 'foo.js',
523+
scriptId: 'id1' as Protocol.Runtime.ScriptId,
524+
lineNumber: 0,
525+
columnNumber: 9,
526+
},
527+
{
528+
functionName: 'bar',
529+
url: 'foo.js',
530+
scriptId: 'id1' as Protocol.Runtime.ScriptId,
531+
lineNumber: 1,
532+
columnNumber: 19,
533+
},
534+
],
535+
parent: {
536+
description: 'setTimeout',
537+
callFrames: [
538+
{
539+
functionName: 'barFnX',
540+
url: 'bar.js',
541+
scriptId: 'id2' as Protocol.Runtime.ScriptId,
542+
lineNumber: 0,
543+
columnNumber: 9,
544+
},
545+
],
546+
},
547+
},
548+
});
549+
550+
assert.strictEqual(stringifyStackTrace(stackTrace), [
551+
'at foo (foo.js:0:9)',
552+
'at bar (foo.js:1:19)',
553+
'--- setTimeout -------------------------',
554+
'at barFnX (bar.js:0:9)',
555+
].join('\n'));
556+
});
557+
558+
it('correctly translates evalOrigin frames', async () => {
559+
const {model} = setup();
560+
561+
const translateFn: StackTraceImpl.StackTraceModel.TranslateRawFrames = (frames, _target) => {
562+
// Expand the evalOrigin into 2 frames to simulate inlining.
563+
return Promise.resolve(frames.map(f => {
564+
if (f.functionName === 'outerEval') { // the evalOrigin frame
565+
return [
566+
{url: 'inlined.js', name: 'inlinedFn', line: 5, column: 5},
567+
{url: f.url, name: f.functionName, line: f.lineNumber, column: f.columnNumber},
568+
];
569+
}
570+
return [{
571+
url: f.url,
572+
name: f.functionName,
573+
line: f.lineNumber,
574+
column: f.columnNumber,
575+
}];
576+
}));
577+
};
578+
579+
const stackTrace = await model.createFromErrorStackLikeString(
580+
`Error: foo
581+
at eval (eval at outerEval (foo.js:10:5), <anonymous>:1:1)`,
582+
translateFn);
583+
584+
const frames = stackTrace.syncFragment.frames as StackTrace.StackTrace.ParsedErrorStackFrame[];
585+
assert.lengthOf(frames, 1);
586+
assert.strictEqual(frames[0].url, '<anonymous>');
587+
assert.strictEqual(frames[0].line, 0);
588+
589+
assert.exists(frames[0].evalOrigin);
590+
// The evalOrigin is represented as a single ParsedErrorStackFrame that points to the top-most inlined frame
591+
assert.strictEqual(frames[0].evalOrigin?.url, 'inlined.js');
592+
assert.strictEqual(frames[0].evalOrigin?.name, 'inlinedFn');
593+
assert.strictEqual(frames[0].evalOrigin?.line, 5);
594+
595+
// NOTE: Because evalOrigin only surfaces a single ParsedErrorStackFrame,
596+
// the remaining inlined frames ('outerEval' at 'foo.js:10:5') are technically dropped in the public API!
597+
// This is a known limitation of having evalOrigin as a single frame rather than an array.
598+
});
599+
});
504600
});

front_end/models/stack_trace/StackTraceModel.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as Common from '../../core/common/common.js';
66
import * as SDK from '../../core/sdk/sdk.js';
77
import type * as Protocol from '../../generated/protocol.js';
88

9+
import {augmentRawFramesWithScriptIds, parseRawFramesFromErrorStack} from './DetailedErrorStackParser.js';
910
// eslint-disable-next-line @devtools/es-modules-import
1011
import * as StackTrace from './stack_trace.js';
1112
import {
@@ -14,6 +15,7 @@ import {
1415
DebuggableFragmentImpl,
1516
FragmentImpl,
1617
FrameImpl,
18+
ParsedErrorStackFragmentImpl,
1719
StackTraceImpl
1820
} from './StackTraceImpl.js';
1921
import {type FrameNode, type RawFrame, Trie} from './Trie.js';
@@ -54,6 +56,23 @@ export class StackTraceModel extends SDK.SDKModel.SDKModel<unknown> {
5456
return new StackTraceImpl(syncFragment, asyncFragments);
5557
}
5658

59+
async createFromErrorStackLikeString(
60+
stack: string, rawFramesToUIFrames: TranslateRawFrames,
61+
exceptionDetails?: Protocol.Runtime.ExceptionDetails): Promise<StackTrace.StackTrace.ParsedErrorStackTrace> {
62+
const rawFrames = parseRawFramesFromErrorStack(stack);
63+
if (exceptionDetails?.stackTrace) {
64+
augmentRawFramesWithScriptIds(rawFrames, exceptionDetails.stackTrace);
65+
}
66+
67+
const [syncFragment, asyncFragments] = await Promise.all([
68+
this.#createFragment(rawFrames, rawFramesToUIFrames),
69+
exceptionDetails?.stackTrace ? this.#createAsyncFragments(exceptionDetails.stackTrace, rawFramesToUIFrames) :
70+
Promise.resolve([]),
71+
]);
72+
73+
return new StackTraceImpl(new ParsedErrorStackFragmentImpl(syncFragment), asyncFragments);
74+
}
75+
5776
async createFromDebuggerPaused(
5877
pausedDetails: SDK.DebuggerModel.DebuggerPausedDetails,
5978
rawFramesToUIFrames: TranslateRawFrames): Promise<StackTrace.StackTrace.DebuggableStackTrace> {
@@ -162,12 +181,32 @@ export class StackTraceModel extends SDK.SDKModel.SDKModel<unknown> {
162181
const uiFrames = await rawFramesToUIFrames(rawFrames, this.target());
163182
console.assert(rawFrames.length === uiFrames.length, 'Broken rawFramesToUIFrames implementation');
164183

184+
const evalOriginPromises: Array<ReturnType<TranslateRawFrames>> = [];
185+
for (const node of fragment.node.getCallStack()) {
186+
if (node.parsedFrameInfo?.evalOrigin) {
187+
// Evaluate each eval origin individually, as they are not a contiguous stack trace.
188+
evalOriginPromises.push(rawFramesToUIFrames([node.parsedFrameInfo.evalOrigin], this.target()));
189+
}
190+
}
191+
192+
const evalUiFrames = await Promise.all(evalOriginPromises);
193+
165194
let i = 0;
195+
let evalI = 0;
166196
for (const node of fragment.node.getCallStack()) {
167197
node.frames = uiFrames[i++].map(
168198
frame => new FrameImpl(
169199
frame.url, frame.uiSourceCode, frame.name, frame.line, frame.column, frame.missingDebugInfo,
170200
node.rawFrame.functionName));
201+
202+
if (node.parsedFrameInfo?.evalOrigin) {
203+
const evalOriginRawFrame = node.parsedFrameInfo.evalOrigin;
204+
// evalUiFrames[evalI] is Array<Array<Frame>>, and since we passed a 1-element array, we take [0]
205+
node.evalOriginFrames = evalUiFrames[evalI++][0].map(
206+
frame => new FrameImpl(
207+
frame.url, frame.uiSourceCode, frame.name, frame.line, frame.column, frame.missingDebugInfo,
208+
evalOriginRawFrame.functionName));
209+
}
171210
}
172211
}
173212

0 commit comments

Comments
 (0)