// REF: https://github.com/getsentry/sentry-javascript/blob/38278210118576d041117e11e2bd6507677625f6/packages/browser/src/stack-parsers.ts import type { StackFrame } from '../types'; function createFrame(filename: string, func: string, lineno?: number, colno?: number): StackFrame { return { filename, func, lineno, colno, }; } type StackParser = (stack: string, skipFirst?: number) => StackFrame[]; type StackLineParser = (line: string) => StackFrame | undefined; const UNKNOWN_FUNCTION = '?'; const CHROME_REGEX = /^\s*at (?:(.*?) ?\((?:address at )?)?((?:file|https?|blob|chrome-extension|address|native|eval|webpack||[-a-z]+:|.*bundle|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; const CHROME_EVAL_REGEX = /\((\S*)(?::(\d+))(?::(\d+))\)/; const chrome: StackLineParser = (line) => { const parts = CHROME_REGEX.exec(line); if (parts) { const isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line if (isEval) { const subMatch = CHROME_EVAL_REGEX.exec(parts[2]); if (subMatch) { [parts[2], parts[3], parts[4]] = [subMatch[1], subMatch[2], subMatch[3]]; } } const [func, filename] = extractSafariExtensionDetails(parts[1] || UNKNOWN_FUNCTION, parts[2]); return createFrame(filename, func, parts[3] ? +parts[3] : undefined, parts[4] ? +parts[4] : undefined); } return; }; const GECKO_REGEX = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension|capacitor).*?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i; const GECKO_EVAL_REGEX = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; const gecko: StackLineParser = (line) => { const parts = GECKO_REGEX.exec(line); if (parts) { const isEval = parts[3] && parts[3].indexOf(' > eval') > -1; if (isEval) { const subMatch = GECKO_EVAL_REGEX.exec(parts[3]); if (subMatch) { // throw out eval line/column and use top-most line number parts[1] = parts[1] || 'eval'; [parts[3], parts[4]] = [subMatch[1], subMatch[2]]; parts[5] = ''; // no column when eval } } let filename = parts[3]; let func = parts[1] || UNKNOWN_FUNCTION; [func, filename] = extractSafariExtensionDetails(func, filename); return createFrame(filename, func, parts[4] ? +parts[4] : undefined, parts[5] ? +parts[5] : undefined); } return; }; const WINJS_REGEX = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i; const winjs: StackLineParser = (line) => { const parts = WINJS_REGEX.exec(line); return parts ? createFrame(parts[2], parts[1] || UNKNOWN_FUNCTION, +parts[3], parts[4] ? +parts[4] : undefined) : undefined; }; const OPERA10_REGEX = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i; const opera10: StackLineParser = (line) => { const parts = OPERA10_REGEX.exec(line); return parts ? createFrame(parts[2], parts[3] || UNKNOWN_FUNCTION, +parts[1]) : undefined; }; const OPERA11_REGEX = / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^)]+))\(.*\))? in (.*):\s*$/i; const opera11: StackLineParser = (line) => { const parts = OPERA11_REGEX.exec(line); return parts ? createFrame(parts[5], parts[3] || parts[4] || UNKNOWN_FUNCTION, +parts[1], +parts[2]) : undefined; }; const extractSafariExtensionDetails = (func: string, filename: string): [string, string] => { const isSafariExtension = func.indexOf('safari-extension') !== -1; const isSafariWebExtension = func.indexOf('safari-web-extension') !== -1; return isSafariExtension || isSafariWebExtension ? [ func.indexOf('@') !== -1 ? func.split('@')[0] : UNKNOWN_FUNCTION, isSafariExtension ? `safari-extension:${filename}` : `safari-web-extension:${filename}`, ] : [func, filename]; }; const BROWSER_STACK_PARSERS = [opera10, opera11, chrome, winjs, gecko]; function createStackParser(...parsers: StackLineParser[]): StackParser { return ( stack: string, skipFirst = 0, /** 最多保留多少 frame,取 0 时保留全部 */ maxDepth = 0, ): StackFrame[] => { const frames: StackFrame[] = []; for (const line of stack.split('\n').slice(skipFirst)) { for (const parser of parsers) { const frame = parser(line); if (frame) { frames.push(frame); if (maxDepth > 0 && frames.length >= maxDepth) { return frames; } break; } } } return frames; }; } const DEFAULT_FUNCTION_NAME = ''; export const parseBrowserStack = createStackParser(...BROWSER_STACK_PARSERS); /** 从 function 对象解出函数名 */ export function getFunctionName(fn: unknown): string { try { if (!fn || typeof fn !== 'function') { return DEFAULT_FUNCTION_NAME; } return fn.name || DEFAULT_FUNCTION_NAME; } catch (e) { return DEFAULT_FUNCTION_NAME; } } /** 从 stackFrames 中解出 sourcefile 信息 */ export function extractSourceFromStackFrames(stackFrames: StackFrame[]) { for (const frame of stackFrames) { if (frame.filename && frame.lineno !== undefined && frame.colno !== undefined) { return { file: frame.filename, line: frame.lineno, col: frame.colno, }; } } return {}; }