功能说明
该示例解析 V8 引擎生成的错误调用栈,把每行 at 记录转换成结构化对象,包含函数名、文件路径、行号、列号以及是否为异步帧或原生帧。结构化后的帧便于做错误聚合、源码映射前的预处理、日志字段提取与崩溃报告展示。
关键代码
parseStackTrace 按行拆分,只处理以 at 开头的行,去掉前缀后交给 parseFrame。parseFrame 先识别 native,再剥离可选的 async 前缀。带括号的帧用懒匹配提取函数名与位置,裸位置行只提取文件与行列,其余形式保留函数名但位置为空。normalizeName 把空字符串与 eval 归一为 null。行列号通过 Number 转换。
使用注意
该解析器针对 V8,包括 Node 与 Chromium;Firefox 与 Safari 的栈格式不同,需要单独规则。异步帧只标记 async 前缀,不代表完整的异步因果链。eval 与匿名函数可能没有可读名称,压缩代码还需要 source map 还原。文件路径可能包含冒号,因此正则使用懒匹配并从右侧捕获行列号。解析结果不应直接展示给终端用户。
验证方式
解析包含 fetchUser、async loadProfile、裸文件位置与 native 的栈,断言函数名、文件、行号、列号、async 与 native 标记全部正确。测试没有 at 行的字符串返回空数组,native 帧的位置字段为 null。再传入非字符串断言 TypeError,并验证 eval 帧名为 null。
代码示例
export interface StackFrame {
readonly functionName: string | null;
readonly file: string | null;
readonly line: number | null;
readonly column: number | null;
readonly native: boolean;
readonly async: boolean;
}
export function parseStackTrace(stack: string): StackFrame[] {
if (typeof stack !== "string") throw new TypeError("stack must be a string");
const frames: StackFrame[] = [];
for (const rawLine of stack.split("\n")) {
const line = rawLine.trim();
if (!line.startsWith("at ")) continue;
const frame = parseFrame(line.slice(3));
if (frame !== null) frames.push(frame);
}
return frames;
}
function parseFrame(value: string): StackFrame | null {
if (value === "native") {
return { functionName: null, file: null, line: null, column: null, native: true, async: false };
}
let text = value;
let isAsync = false;
if (text.startsWith("async ")) {
isAsync = true;
text = text.slice(6);
}
const locationMatch = /^(.*?) \((.+):(\d+):(\d+)\)$/.exec(text);
if (locationMatch !== null) {
return {
functionName: normalizeName(locationMatch[1] ?? ""),
file: locationMatch[2] ?? null,
line: Number(locationMatch[3]),
column: Number(locationMatch[4]),
native: false,
async: isAsync,
};
}
const bareMatch = /^(.+):(\d+):(\d+)$/.exec(text);
if (bareMatch !== null) {
return {
functionName: null,
file: bareMatch[1] ?? null,
line: Number(bareMatch[2]),
column: Number(bareMatch[3]),
native: false,
async: isAsync,
};
}
return {
functionName: normalizeName(text),
file: null,
line: null,
column: null,
native: false,
async: isAsync,
};
}
function normalizeName(value: string): string | null {
const name = value.trim();
if (name === "" || name === "eval") return null;
return name;
}
const stack = [
"Error: boom",
" at fetchUser (/app/src/users.js:42:13)",
" at async loadProfile (/app/src/profile.js:8:20)",
" at /app/src/index.js:3:1",
" at native",
].join("\n");
console.log(parseStackTrace(stack));
console.log(parseStackTrace("Error: empty"));