@visulima/error
Version:
Error with more than just a message, stacktrace parsing.
274 lines (272 loc) • 10.1 kB
JavaScript
const debugLog = (message, ...arguments_) => {
if (process.env.DEBUG && String(process.env.DEBUG) === "true") {
console.debug(`error:parse-stacktrace: ${message}`, ...arguments_);
}
};
const UNKNOWN_FUNCTION = "<unknown>";
const CHROMIUM_REGEX = /^.*?\s*at\s(?:(.+?\)(?:\s\[.+\])?|\(?.*?)\s?\((?:address\sat\s)?)?(?:async\s)?((?:<anonymous>|[-a-z]+:|.*bundle|\/)?.*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
const CHROMIUM_EVAL_REGEX = /\((\S+)\),\s(<[^>]+>)?:(\d+)?:(\d+)?\)?/;
const CHROMIUM_MAPPED = /(.*?):(\d+):(\d+)(\s<-\s(.+):(\d+):(\d+))?/;
const WINDOWS_EVAL_REGEX = /(eval)\sat\s(<anonymous>)\s\((.*)\)?:(\d+)?:(\d+)\),\s*(<anonymous>)?:(\d+)?:(\d+)/;
const NODE_REGEX = /^\s*in\s(?:([^\\/]+(?:\s\[as\s\S+\])?)\s\(?)?\(at?\s?(.*?):(\d+)(?::(\d+))?\)?\s*$/;
const NODE_NESTED_REGEX = /in\s(.*)\s\(at\s(.+)\)\sat/;
const REACT_ANDROID_NATIVE_REGEX = /^(?:.*@)?(.*):(\d+):(\d+)$/;
const GECKO_REGEX = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:[-a-z]+)?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. \/=]+)(?::(\d+))?(?::(\d+))?\s*$/i;
const GECKO_EVAL_REGEX = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i;
const FIREFOX_REGEX = /(\S[^\s[]*\[.*\]|.*?)@(.*):(\d+):(\d+)/;
const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/;
const extractSafariExtensionDetails = (methodName, url) => {
const isSafariExtension = methodName.includes("safari-extension");
const isSafariWebExtension = methodName.includes("safari-web-extension");
return isSafariExtension || isSafariWebExtension ? [
methodName.includes("@") ? methodName.split("@")[0] : UNKNOWN_FUNCTION,
isSafariExtension ? `safari-extension:${url}` : `safari-web-extension:${url}`
] : [methodName, url];
};
const parseMapped = (trace, maybeMapped) => {
const match = CHROMIUM_MAPPED.exec(maybeMapped);
if (match) {
trace.file = match[1];
trace.line = +match[2];
trace.column = +match[3];
}
};
const parseNode = (line) => {
const nestedNode = NODE_NESTED_REGEX.exec(line);
if (nestedNode) {
debugLog(`parse nested node error stack line: "${line}"`, `found: ${JSON.stringify(nestedNode)}`);
const split = nestedNode[2].split(":");
return {
column: split[2] ? +split[2] : void 0,
file: split[0],
line: split[1] ? +split[1] : void 0,
methodName: nestedNode[1] || UNKNOWN_FUNCTION,
raw: line,
type: void 0
};
}
const node = NODE_REGEX.exec(line);
if (node) {
debugLog(`parse node error stack line: "${line}"`, `found: ${JSON.stringify(node)}`);
const trace = {
column: node[4] ? +node[4] : void 0,
file: node[2] ? node[2].replace(/at\s/, "") : void 0,
line: node[3] ? +node[3] : void 0,
methodName: node[1] || UNKNOWN_FUNCTION,
raw: line,
type: line.startsWith("internal") ? "internal" : void 0
};
parseMapped(trace, `${node[2]}:${node[3]}:${node[4]}`);
return trace;
}
return void 0;
};
const parseChromium = (line) => {
const parts = CHROMIUM_REGEX.exec(line);
if (parts) {
debugLog(`parse chrome error stack line: "${line}"`, `found: ${JSON.stringify(parts)}`);
const isNative = parts[2]?.startsWith("native");
const isEval = parts[2]?.startsWith("eval") || parts[1]?.startsWith("eval");
let evalOrigin;
let windowsParts;
if (isEval) {
const subMatch = CHROMIUM_EVAL_REGEX.exec(line);
if (subMatch) {
const split = /^(\S+):(\d+):(\d+)$|^(\S+):(\d+)$/.exec(subMatch[1]);
if (split) {
parts[2] = split[4] ?? split[1];
parts[3] = split[5] ?? split[2];
parts[4] = split[3];
} else if (subMatch[2]) {
parts[2] = subMatch[1];
}
if (subMatch[2]) {
evalOrigin = {
column: subMatch[4] ? +subMatch[4] : void 0,
file: subMatch[2],
line: subMatch[3] ? +subMatch[3] : void 0,
methodName: "eval",
raw: line,
type: "eval"
};
}
} else {
const windowsSubMatch = WINDOWS_EVAL_REGEX.exec(line);
if (windowsSubMatch) {
windowsParts = {
column: windowsSubMatch[5] ? +windowsSubMatch[5] : void 0,
file: windowsSubMatch[3],
line: windowsSubMatch[4] ? +windowsSubMatch[4] : void 0
};
evalOrigin = {
column: windowsSubMatch[8] ? +windowsSubMatch[8] : void 0,
file: windowsSubMatch[2],
line: windowsSubMatch[7] ? +windowsSubMatch[7] : void 0,
methodName: "eval",
raw: windowsSubMatch[0],
type: "eval"
};
}
}
}
const [methodName, file] = extractSafariExtensionDetails(
// Normalize IE's 'Anonymous function'
parts[1] ? parts[1].replace(/^Anonymous function$/, "<anonymous>") : UNKNOWN_FUNCTION,
parts[2]
);
const trace = {
column: parts[4] ? +parts[4] : void 0,
evalOrigin,
file,
line: parts[3] ? +parts[3] : void 0,
methodName,
raw: line,
// eslint-disable-next-line sonarjs/no-nested-conditional
type: isEval ? "eval" : isNative ? "native" : void 0
};
if (windowsParts) {
trace.column = windowsParts.column;
trace.file = windowsParts.file;
trace.line = windowsParts.line;
} else {
parseMapped(trace, `${file}:${parts[3]}:${parts[4]}`);
}
return trace;
}
return void 0;
};
const parseGecko = (line, topFrameMeta) => {
const parts = GECKO_REGEX.exec(line);
if (parts) {
debugLog(`parse gecko error stack line: "${line}"`, `found: ${JSON.stringify(parts)}`);
const isEval = parts[3]?.includes(" > eval");
const subMatch = isEval && parts[3] && GECKO_EVAL_REGEX.exec(parts[3]);
let evalOrigin;
if (isEval && subMatch) {
parts[3] = subMatch[1];
evalOrigin = {
column: parts[5] ? +parts[5] : void 0,
file: parts[3],
line: parts[4] ? +parts[4] : void 0,
methodName: "eval",
raw: line,
type: "eval"
};
parts[4] = subMatch[2];
}
const [methodName, file] = extractSafariExtensionDetails(
// Normalize IE's 'Anonymous function'
parts[1] ? parts[1].replace(/^Anonymous function$/, "<anonymous>") : UNKNOWN_FUNCTION,
parts[3]
);
let column;
if ((topFrameMeta?.type === "safari" || !isEval && topFrameMeta?.type === "firefox") && topFrameMeta.column) {
column = topFrameMeta.column;
} else if (!isEval && parts[5]) {
column = +parts[5];
}
let lineNumber;
if ((topFrameMeta?.type === "safari" || !isEval && topFrameMeta?.type === "firefox") && topFrameMeta.line) {
lineNumber = topFrameMeta.line;
} else if (parts[4]) {
lineNumber = +parts[4];
}
return {
column,
evalOrigin,
file,
line: lineNumber,
methodName,
raw: line,
// eslint-disable-next-line sonarjs/no-nested-conditional
type: isEval ? "eval" : file.includes("[native code]") ? "native" : void 0
};
}
return void 0;
};
const parseFirefox = (line, topFrameMeta) => {
const parts = FIREFOX_REGEX.exec(line);
const isEval = parts ? parts[2].includes(" > eval") : false;
if (!isEval && parts) {
debugLog(`parse firefox error stack line: "${line}"`, `found: ${JSON.stringify(parts)}`);
return {
column: parts[4] ? +parts[4] : topFrameMeta?.column ?? void 0,
file: parts[2],
line: parts[3] ? +parts[3] : topFrameMeta?.line ?? void 0,
methodName: parts[1] || UNKNOWN_FUNCTION,
raw: line,
type: void 0
};
}
return void 0;
};
const parseReactAndroidNative = (line) => {
const parts = REACT_ANDROID_NATIVE_REGEX.exec(line);
if (parts) {
debugLog(`parse react android native error stack line: "${line}"`, `found: ${JSON.stringify(parts)}`);
return {
column: parts[3] ? +parts[3] : void 0,
file: parts[1],
line: parts[2] ? +parts[2] : void 0,
methodName: UNKNOWN_FUNCTION,
raw: line,
type: void 0
};
}
return void 0;
};
const parseStacktrace = (error, { filter, frameLimit = 50 } = {}) => {
let lines = (error.stacktrace ?? error.stack ?? "").split("\n").map((line) => {
const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, "$1") : line;
return cleanedLine.replace(/^\s+|\s+$/g, "");
}).filter((line) => !/\S*(?:Error: |AggregateError:)/.test(line) && line !== "eval code");
if (filter) {
lines = lines.filter((element) => filter(element));
}
lines = lines.slice(0, frameLimit);
return lines.reduce((stack, line, currentIndex) => {
if (!line) {
return stack;
}
if (line.length > 1024) {
return stack;
}
let parseResult;
if (/^\s*in\s.*/.test(line)) {
parseResult = parseNode(line);
} else if (/^.*?\s*at\s.*/.test(line)) {
parseResult = parseChromium(line);
} else if (/^.*?\s*@.*|\[native code\]/.test(line)) {
let topFrameMeta;
if (currentIndex === 0) {
if (error.columnNumber || error.lineNumber) {
topFrameMeta = {
// @ts-expect-error columnNumber and columnNumber property only exists on Firefox
column: error.columnNumber,
// @ts-expect-error columnNumber and lineNumber property only exists on Firefox
line: error.lineNumber,
type: "firefox"
};
} else if (error.line || error.column) {
topFrameMeta = {
// @ts-expect-error column property only exists on safari
column: error.column,
// @ts-expect-error line property only exists on safari
line: error.line,
type: "safari"
};
}
}
parseResult = parseFirefox(line, topFrameMeta) || parseGecko(line, topFrameMeta);
} else {
parseResult = parseReactAndroidNative(line);
}
if (parseResult) {
stack.push(parseResult);
} else {
debugLog(`parse error stack line: "${line}"`, "not parser found");
}
return stack;
}, []);
};
export { parseStacktrace as default };