UNPKG

@visulima/error

Version:

Error with more than just a message, stacktrace parsing.

274 lines (272 loc) 10.1 kB
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 };