Skip to content

Commit e5d13a6

Browse files
szuenddevtools-frontend-scoped@luci-project-accounts.iam.gserviceaccount.com
authored andcommitted
[stack_trace]: Implement Error.stack parsing for V8 strings
This CL re-implements Error.stack parsing. Compared to the existing parser, we produce a fully structured frame instead of a prefix, link info and suffix. The advantage is twofold: * We disentangle the Error.stack format a bit more from the UI. * We can source map the "eval origin" now as well. Follow-up changes will wire-up the parsing to the trie and the StackTraceModel. R=pfaffe@chromium.org Bug: 485142682 Change-Id: Iff98b82a27ae3ddd1256d5c8e5a50aecae350b2a Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7754102 Reviewed-by: Philip Pfaffe <pfaffe@chromium.org> Commit-Queue: Simon Zünd <szuend@chromium.org>
1 parent 1647a51 commit e5d13a6

File tree

5 files changed

+341
-0
lines changed

5 files changed

+341
-0
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,7 @@ grd_files_unbundled_sources = [
12341234
"front_end/models/source_map_scopes/FunctionCodeResolver.js",
12351235
"front_end/models/source_map_scopes/NamesResolver.js",
12361236
"front_end/models/source_map_scopes/ScopeChainModel.js",
1237+
"front_end/models/stack_trace/DetailedErrorStackParser.js",
12371238
"front_end/models/stack_trace/ErrorStackParser.js",
12381239
"front_end/models/stack_trace/StackTrace.js",
12391240
"front_end/models/stack_trace/StackTraceImpl.js",

front_end/models/stack_trace/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ devtools_entrypoint("bundle") {
3030

3131
devtools_foundation_module("stack_trace_impl") {
3232
sources = [
33+
"DetailedErrorStackParser.ts",
3334
"StackTraceImpl.ts",
3435
"StackTraceModel.ts",
3536
"Trie.ts",
@@ -62,6 +63,7 @@ devtools_foundation_module("unittests") {
6263
testonly = true
6364

6465
sources = [
66+
"DetailedErrorStackParser.test.ts",
6567
"ErrorStackParser.test.ts",
6668
"StackTrace.test.ts",
6769
"StackTraceImpl.test.ts",
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2026 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// eslint-disable-next-line @devtools/es-modules-import
6+
import * as StackTraceImpl from './stack_trace_impl.js';
7+
8+
describe('DetailedErrorStackParser', () => {
9+
describe('parseRawFramesFromErrorStack', () => {
10+
it('parses standard V8 stack frames', () => {
11+
const stack = `Error: foo
12+
at functionName (http://www.example.org/script.js:10:5)
13+
at Class.methodName (http://www.example.org/script.js:20:1)
14+
at new Constructor (http://www.example.org/script.js:30:1)
15+
at async asyncFunction (http://www.example.org/script.js:40:1)
16+
at http://www.example.org/script.js:50:1`;
17+
const frames = StackTraceImpl.DetailedErrorStackParser.parseRawFramesFromErrorStack(stack);
18+
19+
assert.lengthOf(frames, 5);
20+
assert.deepEqual(frames[0], {
21+
url: 'http://www.example.org/script.js',
22+
functionName: 'functionName',
23+
lineNumber: 9,
24+
columnNumber: 4,
25+
parsedFrameInfo: {
26+
isAsync: false,
27+
isConstructor: false,
28+
isEval: false,
29+
isWasm: false,
30+
wasmModuleName: undefined,
31+
wasmFunctionIndex: undefined,
32+
typeName: undefined,
33+
methodName: undefined,
34+
promiseIndex: undefined,
35+
evalOrigin: undefined,
36+
},
37+
});
38+
assert.deepEqual(frames[1], {
39+
url: 'http://www.example.org/script.js',
40+
functionName: 'Class.methodName',
41+
lineNumber: 19,
42+
columnNumber: 0,
43+
parsedFrameInfo: {
44+
isAsync: false,
45+
isConstructor: false,
46+
isEval: false,
47+
isWasm: false,
48+
wasmModuleName: undefined,
49+
wasmFunctionIndex: undefined,
50+
typeName: 'Class',
51+
methodName: 'methodName',
52+
promiseIndex: undefined,
53+
evalOrigin: undefined,
54+
},
55+
});
56+
assert.deepEqual(frames[2], {
57+
url: 'http://www.example.org/script.js',
58+
functionName: 'Constructor',
59+
lineNumber: 29,
60+
columnNumber: 0,
61+
parsedFrameInfo: {
62+
isAsync: false,
63+
isConstructor: true,
64+
isEval: false,
65+
isWasm: false,
66+
wasmModuleName: undefined,
67+
wasmFunctionIndex: undefined,
68+
typeName: undefined,
69+
methodName: undefined,
70+
promiseIndex: undefined,
71+
evalOrigin: undefined,
72+
},
73+
});
74+
assert.deepEqual(frames[3], {
75+
url: 'http://www.example.org/script.js',
76+
functionName: 'asyncFunction',
77+
lineNumber: 39,
78+
columnNumber: 0,
79+
parsedFrameInfo: {
80+
isAsync: true,
81+
isConstructor: false,
82+
isEval: false,
83+
isWasm: false,
84+
wasmModuleName: undefined,
85+
wasmFunctionIndex: undefined,
86+
typeName: undefined,
87+
methodName: undefined,
88+
promiseIndex: undefined,
89+
evalOrigin: undefined,
90+
},
91+
});
92+
assert.deepEqual(frames[4], {
93+
url: 'http://www.example.org/script.js',
94+
functionName: '',
95+
lineNumber: 49,
96+
columnNumber: 0,
97+
parsedFrameInfo: {
98+
isAsync: false,
99+
isConstructor: false,
100+
isEval: false,
101+
isWasm: false,
102+
wasmModuleName: undefined,
103+
wasmFunctionIndex: undefined,
104+
typeName: undefined,
105+
methodName: undefined,
106+
promiseIndex: undefined,
107+
evalOrigin: undefined,
108+
},
109+
});
110+
});
111+
112+
it('parses eval frames', () => {
113+
const stack = `Error: foo
114+
at eval (eval at <anonymous> (http://www.example.org/script.js:10:5), <anonymous>:1:1)`;
115+
const frames = StackTraceImpl.DetailedErrorStackParser.parseRawFramesFromErrorStack(stack);
116+
117+
assert.lengthOf(frames, 1);
118+
assert.isTrue(frames[0].parsedFrameInfo?.isEval);
119+
assert.strictEqual(frames[0].url, '<anonymous>');
120+
assert.strictEqual(frames[0].lineNumber, 0);
121+
assert.strictEqual(frames[0].columnNumber, 0);
122+
123+
assert.exists(frames[0].parsedFrameInfo?.evalOrigin);
124+
assert.strictEqual(frames[0].parsedFrameInfo?.evalOrigin?.url, 'http://www.example.org/script.js');
125+
assert.strictEqual(frames[0].parsedFrameInfo?.evalOrigin?.lineNumber, 9);
126+
assert.strictEqual(frames[0].parsedFrameInfo?.evalOrigin?.columnNumber, 4);
127+
assert.strictEqual(frames[0].parsedFrameInfo?.evalOrigin?.functionName, '<anonymous>');
128+
});
129+
130+
it('parses deeply nested eval frames with actual function names', () => {
131+
const stack = `Error: foo
132+
at innerEval (eval at outerEval (eval at topEval (http://www.example.org/script.js:10:5)), <anonymous>:1:1)`;
133+
const frames = StackTraceImpl.DetailedErrorStackParser.parseRawFramesFromErrorStack(stack);
134+
135+
assert.lengthOf(frames, 1);
136+
assert.isTrue(frames[0].parsedFrameInfo?.isEval);
137+
assert.strictEqual(frames[0].url, '<anonymous>');
138+
assert.strictEqual(frames[0].lineNumber, 0);
139+
assert.strictEqual(frames[0].columnNumber, 0);
140+
141+
// Level 1: outerEval
142+
const outerEvalOrigin = frames[0].parsedFrameInfo?.evalOrigin;
143+
assert.exists(outerEvalOrigin);
144+
assert.isTrue(outerEvalOrigin?.parsedFrameInfo?.isEval);
145+
assert.strictEqual(outerEvalOrigin?.functionName, 'outerEval');
146+
assert.strictEqual(outerEvalOrigin?.url, ''); // no <anonymous> suffix
147+
assert.strictEqual(outerEvalOrigin?.lineNumber, -1);
148+
assert.strictEqual(outerEvalOrigin?.columnNumber, -1);
149+
150+
// Level 2: topEval
151+
const topEvalOrigin = outerEvalOrigin?.parsedFrameInfo?.evalOrigin;
152+
assert.exists(topEvalOrigin);
153+
assert.isFalse(topEvalOrigin?.parsedFrameInfo?.isEval);
154+
assert.strictEqual(topEvalOrigin?.functionName, 'topEval');
155+
assert.strictEqual(topEvalOrigin?.url, 'http://www.example.org/script.js');
156+
assert.strictEqual(topEvalOrigin?.lineNumber, 9);
157+
assert.strictEqual(topEvalOrigin?.columnNumber, 4);
158+
});
159+
160+
it('parses aliased method calls', () => {
161+
const stack = `Error: foo
162+
at Type.method [as alias] (http://www.example.org/script.js:10:5)`;
163+
const frames = StackTraceImpl.DetailedErrorStackParser.parseRawFramesFromErrorStack(stack);
164+
165+
assert.lengthOf(frames, 1);
166+
assert.strictEqual(frames[0].parsedFrameInfo?.typeName, 'Type');
167+
assert.strictEqual(frames[0].parsedFrameInfo?.methodName, 'alias');
168+
});
169+
170+
it('parses wasm frames', () => {
171+
const stack = `Error: foo
172+
at wasmModule.wasmFunc (http://www.example.org/script.js:wasm-function[123]:0xabc)`;
173+
const frames = StackTraceImpl.DetailedErrorStackParser.parseRawFramesFromErrorStack(stack);
174+
175+
assert.lengthOf(frames, 1);
176+
assert.isTrue(frames[0].parsedFrameInfo?.isWasm);
177+
assert.strictEqual(frames[0].url, 'http://www.example.org/script.js');
178+
assert.strictEqual(frames[0].parsedFrameInfo?.wasmModuleName, 'wasmModule');
179+
assert.strictEqual(frames[0].parsedFrameInfo?.wasmFunctionIndex, 123);
180+
assert.strictEqual(frames[0].columnNumber, 0xabc);
181+
});
182+
183+
it('parses promise.all index', () => {
184+
const stack = `Error: foo
185+
at Promise.all (index 2)`;
186+
const frames = StackTraceImpl.DetailedErrorStackParser.parseRawFramesFromErrorStack(stack);
187+
188+
assert.lengthOf(frames, 1);
189+
assert.strictEqual(frames[0].parsedFrameInfo?.promiseIndex, 2);
190+
assert.strictEqual(frames[0].url, '');
191+
assert.strictEqual(frames[0].functionName, 'Promise.all');
192+
});
193+
});
194+
});
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2026 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as Common from '../../core/common/common.js';
6+
import type * as Platform from '../../core/platform/platform.js';
7+
8+
import type {RawFrame} from './Trie.js';
9+
10+
/**
11+
* Takes a V8 Error#stack string and extracts structured information.
12+
*/
13+
export function parseRawFramesFromErrorStack(stack: string): RawFrame[] {
14+
const lines = stack.split('\n');
15+
const rawFrames: RawFrame[] = [];
16+
for (const line of lines) {
17+
const match = /^\s*at\s+(.*)/.exec(line);
18+
if (!match) {
19+
continue;
20+
}
21+
22+
let lineContent = match[1];
23+
let isAsync = false;
24+
if (lineContent.startsWith('async ')) {
25+
isAsync = true;
26+
lineContent = lineContent.substring(6);
27+
}
28+
29+
let isConstructor = false;
30+
if (lineContent.startsWith('new ')) {
31+
isConstructor = true;
32+
lineContent = lineContent.substring(4);
33+
}
34+
35+
let functionName = '';
36+
let url = '';
37+
let lineNumber = -1;
38+
let columnNumber = -1;
39+
let typeName: string|undefined;
40+
let methodName: string|undefined;
41+
let isEval = false;
42+
let isWasm = false;
43+
let wasmModuleName: string|undefined;
44+
let wasmFunctionIndex: number|undefined;
45+
let promiseIndex: number|undefined;
46+
let evalOrigin: RawFrame|undefined;
47+
48+
const openParenIndex = lineContent.indexOf(' (');
49+
if (lineContent.endsWith(')') && openParenIndex !== -1) {
50+
functionName = lineContent.substring(0, openParenIndex).trim();
51+
let location = lineContent.substring(openParenIndex + 2, lineContent.length - 1);
52+
53+
if (location.startsWith('eval at ')) {
54+
isEval = true;
55+
const commaIndex = location.lastIndexOf(', ');
56+
let evalOriginStr = location;
57+
if (commaIndex !== -1) {
58+
evalOriginStr = location.substring(0, commaIndex);
59+
location = location.substring(commaIndex + 2);
60+
} else {
61+
location = '';
62+
}
63+
64+
if (evalOriginStr.startsWith('eval at ')) {
65+
evalOriginStr = evalOriginStr.substring(8);
66+
}
67+
const innerOpenParen = evalOriginStr.indexOf(' (');
68+
let evalFunctionName = evalOriginStr;
69+
let evalLocation = '';
70+
if (innerOpenParen !== -1) {
71+
evalFunctionName = evalOriginStr.substring(0, innerOpenParen).trim();
72+
evalLocation = evalOriginStr.substring(innerOpenParen + 2, evalOriginStr.length - 1);
73+
evalOrigin = parseRawFramesFromErrorStack(` at ${evalFunctionName} (${evalLocation})`)[0];
74+
} else {
75+
evalOrigin = parseRawFramesFromErrorStack(` at ${evalFunctionName}`)[0];
76+
}
77+
}
78+
79+
if (location.startsWith('index ')) {
80+
promiseIndex = parseInt(location.substring(6), 10);
81+
url = '';
82+
} else if (location.includes(':wasm-function[')) {
83+
isWasm = true;
84+
const wasmMatch = /^(.*):wasm-function\[(\d+)\]:(0x[0-9a-fA-F]+)$/.exec(location);
85+
if (wasmMatch) {
86+
url = wasmMatch[1];
87+
wasmFunctionIndex = parseInt(wasmMatch[2], 10);
88+
columnNumber = parseInt(wasmMatch[3], 16);
89+
}
90+
} else {
91+
const splitResult = Common.ParsedURL.ParsedURL.splitLineAndColumn(location);
92+
url = splitResult.url;
93+
lineNumber = splitResult.lineNumber ?? -1;
94+
columnNumber = splitResult.columnNumber ?? -1;
95+
}
96+
} else {
97+
const splitResult = Common.ParsedURL.ParsedURL.splitLineAndColumn(lineContent);
98+
url = splitResult.url;
99+
lineNumber = splitResult.lineNumber ?? -1;
100+
columnNumber = splitResult.columnNumber ?? -1;
101+
}
102+
103+
// Handle "typeName.methodName [as alias]"
104+
if (functionName) {
105+
const aliasMatch = /(.*)\s+\[as\s+(.*)\]/.exec(functionName);
106+
if (aliasMatch) {
107+
methodName = aliasMatch[2];
108+
functionName = aliasMatch[1];
109+
}
110+
111+
const dotIndex = functionName.indexOf('.');
112+
if (dotIndex !== -1) {
113+
typeName = functionName.substring(0, dotIndex);
114+
methodName = methodName ?? functionName.substring(dotIndex + 1);
115+
}
116+
117+
if (isWasm && typeName) {
118+
wasmModuleName = typeName;
119+
}
120+
}
121+
122+
rawFrames.push({
123+
url: url as Platform.DevToolsPath.UrlString,
124+
functionName,
125+
lineNumber,
126+
columnNumber,
127+
parsedFrameInfo: {
128+
isAsync,
129+
isConstructor,
130+
isEval,
131+
evalOrigin,
132+
isWasm,
133+
wasmModuleName,
134+
wasmFunctionIndex,
135+
typeName,
136+
methodName,
137+
promiseIndex,
138+
},
139+
});
140+
}
141+
return rawFrames;
142+
}

front_end/models/stack_trace/stack_trace_impl.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import * as DetailedErrorStackParser from './DetailedErrorStackParser.js';
56
import * as StackTraceImpl from './StackTraceImpl.js';
67
import * as StackTraceModel from './StackTraceModel.js';
78
import * as Trie from './Trie.js';
89

910
export {
11+
DetailedErrorStackParser,
1012
StackTraceImpl,
1113
StackTraceModel,
1214
Trie,

0 commit comments

Comments
 (0)