import { uuidV4 } from '@tencent/merlin-core'; import type { ErrorIssue } from '../types'; import { extractSourceFromStackFrames, parseBrowserStack } from './stack-parsers'; /** 定位信息字段是否完整 * @desc 只要有文件名和行号就认为完整 */ function isIssuePositionComplete(issue: { file?: string; line?: number }) { return !!issue.file && issue.file !== 'undefined' && issue.line !== undefined; } // 搞这一串 instanceof 工具函数的原因:并非所有环境都会有 instanceof 右侧 constructor function instanceofDOMException(wt: any): wt is DOMException { try { return !!globalThis.DOMException && wt instanceof DOMException; } catch (_e) { return false; } } function instanceofError(wt: any): wt is Error { try { return !!Error && wt instanceof Error; } catch (_e) { return false; } } function instanceofEvent(wt: any): wt is Event { try { return !!Event && wt instanceof Event; } catch (_e) { return false; } } function instanceofPromiseRejectionEvent(wt: any): wt is PromiseRejectionEvent { try { return !!PromiseRejectionEvent && wt instanceof PromiseRejectionEvent; } catch (_e) { return false; } } function instanceofErrorEvent(wt: any): wt is ErrorEvent { try { return !!ErrorEvent && wt instanceof ErrorEvent; } catch (_e) { return false; } } function instanceofHTMLElement(wt: any): wt is HTMLElement { try { return !!HTMLElement && wt instanceof HTMLElement; } catch (_e) { return false; } } /** 仅从 Error 解出信息 */ export function buildIssueFromError(error: Error, name?: string): ErrorIssue { if (!instanceofError(error)) { console.error('[Merlin] unexpected Object captured', error); return { id: uuidV4(), name: 'UnknownObject', message: '[Merlin] unexpected Object captured', timestamp: Date.now(), }; } const stackFrames = error.stack ? parseBrowserStack(error.stack) : []; const { file, col, line } = extractSourceFromStackFrames(stackFrames); let defaultName = 'Error'; if (instanceofDOMException(error)) { defaultName = 'DOMException'; } return { id: uuidV4(), name: name || error.name || defaultName, message: error.message, timestamp: Date.now(), stack: error.stack, stackFrames, file, // 若 stack 没能解出 line 和 col,用 Error 可能自带的 line: line ?? (error as any).line, col: col ?? (error as any).column, }; } function buildIssueFromErrorEvent(errorEvent: ErrorEvent): ErrorIssue { const issue = buildIssueFromError(errorEvent.error); if ( isIssuePositionComplete({ file: errorEvent.filename, line: errorEvent.lineno, // col: errorEvent.colno, }) ) { // 若 errorEvent 有完整的定位信息,则优先用它的 issue.file = errorEvent.filename; issue.line = errorEvent.lineno; issue.col = errorEvent.colno; } else if (errorEvent.lineno !== undefined && errorEvent.colno !== undefined && !isIssuePositionComplete(issue)) { // 若 error 内层没有解出完整定位信息,则用 Event 的 line 和 col issue.line = errorEvent.lineno; issue.col = errorEvent.colno; } return issue; } function buildIssueFromPromiseRejectionEvent(promiseRejectionEvent: PromiseRejectionEvent): ErrorIssue { const reason = promiseRejectionEvent.reason || (promiseRejectionEvent as any).detail?.reason; if (reason) { if (instanceofError(reason)) { return buildIssueFromError(reason); } if (!instanceofPromiseRejectionEvent(reason)) { // 若 reason 还是一个 PromiseRejectionEvent,有可能是它自身,不继续解 return buildIssueFromEvent(reason); } } return { id: uuidV4(), name: 'UnknownPromiseRejection', message: `${promiseRejectionEvent.type}: ${reason || 'UnknownReason'}`, timestamp: Date.now(), }; } /** 由 Event 解出 ErrorIssue */ export function buildIssueFromEvent(event: Event): ErrorIssue { if (instanceofPromiseRejectionEvent(event)) { return buildIssueFromPromiseRejectionEvent(event); } if (instanceofErrorEvent(event)) { return buildIssueFromErrorEvent(event); } if (instanceofEvent(event)) { // 其他类型的事件,如冒泡起来的资源加载失败 // 先只处理疑似资源加载失败的情况,可能覆盖不全,后续迭代 if (event.target && instanceofHTMLElement(event.target)) { const el = event.target; let targetLabel = el.tagName; if (el.id) { targetLabel += `#${el.id}`; } el.classList.forEach((c) => (targetLabel += `.${c}`)); return { id: uuidV4(), name: 'Event', message: `${event.type}: ${targetLabel}`, timestamp: Date.now(), sysIdx1: el.getAttribute('src') || undefined, }; } // 未知 Event return { id: uuidV4(), name: 'UnknownEvent', message: `${event?.type} ${event?.constructor?.name || ''}`, timestamp: Date.now(), }; } // 非 Event return { id: uuidV4(), name: 'UnknownEvent', message: `${(event as any)?.type} ${(event as any)?.constructor?.name || ''}`, timestamp: Date.now(), }; } /** 转换在 onerror 情况下捕获的错误 */ export function buildIssueFromOnError( event: Event | string, source?: string, lineno?: number, colno?: number, error?: Error, ): ErrorIssue { if (!error) { if (typeof event === 'string') { return { id: uuidV4(), name: 'UnknownError', message: event, timestamp: Date.now(), file: source, line: lineno, col: colno, }; } return buildIssueFromEvent(event); } // 有 error,以 error 为准 const issue = buildIssueFromError(error); if ( isIssuePositionComplete({ file: source, line: lineno, // col: colno, }) ) { // 若 onerror 参数给了完整的定位信息,则用 onerror 的 issue.file = source; issue.line = lineno; issue.col = colno; } else if (lineno !== undefined && colno !== undefined && !isIssuePositionComplete(issue)) { // 若 error 内层没有解出完整定位信息,则用 onerror 的 line 和 col issue.line = lineno; issue.col = colno; } return issue; } /** 转换在 liteApp addEventListener('error') 捕获的错误 */ export function buildIssueFromLiteAppError( error: | Error | { name?: string; message?: string; stack?: string; url?: string; }, origin?: string, ): ErrorIssue { if (instanceofError(error)) { const issue = buildIssueFromError(error); return { ...issue, name: issue.name || origin || 'UnknownOrigin', }; } const stackFrames = error.stack ? parseBrowserStack(error.stack) : []; const { file, col, line } = extractSourceFromStackFrames(stackFrames); return { id: uuidV4(), name: error.name || origin || 'UnknownOrigin', message: error.message || origin || '', timestamp: Date.now(), stack: error.stack, stackFrames, file, // 若 stack 没能解出 line 和 col,用 Error 可能自带的 line: line ?? (error as any).line, col: col ?? (error as any).column, sysIdx1: error.url || undefined, }; }