前端监控 SDK 异常捕获与无损采样上报
2026/9/12 4:05:36 网站建设 项目流程

前端监控 SDK 异常捕获与无损采样上报

当一个拥有日活数千万的前端应用在生产环境发生局部崩溃时,如果监控 SDK 只是机械地“见错就报”,后台日志流会在几秒钟内被数百万条一模一样的错误日志冲垮;而如果采用粗暴的随机丢弃(如 10% 随机采样),又极有可能漏掉偶发且致命的白屏崩溃。

一个工业级的前端监控 SDK,不仅要具备全方位捕获 JS 运行时异常、Promise 敲空、静态资源 404 与跨域脚本错误的能力,更要通过指纹聚合(Error Fingerprinting)行为面包屑(Breadcrumbs)自适应令牌桶采样,实现高价值异常的“无损捕捉与精简上报”。

监控捕获的全链路防御网

前端异常捕获必须覆盖四个主要攻击面:

┌───────────────────────────────┐ │ 全景异常监听与拦截层 │ └───────────────┬───────────────┘ │ ┌────────────────┬──────────────┴───┬─────────────────┬─────────────────┐ ▼ ▼ ▼ ▼ ▼ window.onerror unhandledrejection 资源加载(Capture) 网络拦截(Fetch/XHR) 白屏检测(DOM深度) (JS Runtime) (Promise 异步) (Script/Img 404) (5xx / Timeout) (Elements Check)

SDK 核心监听与聚合器实现

export interface ErrorEventPayload { fingerprint: string; // 错误唯一特征指纹 message: string; stack?: string; category: 'js' | 'promise' | 'resource' | 'network'; timestamp: number; breadcrumbs: BreadcrumbItem[]; meta: Record<string, any>; } export interface BreadcrumbItem { type: 'click' | 'route' | 'console' | 'network'; timestamp: number; detail: any; } export class TrackerSDK { private reportUrl: string; private breadcrumbs: BreadcrumbItem[] = []; private readonly maxBreadcrumbs = 20; private reportedFingerprints = new Map<string, number>(); // 指纹限流防抖 constructor(reportUrl: string) { this.reportUrl = reportUrl; this.initListeners(); this.hijackNetwork(); this.hijackUserInteractions(); } private initListeners() { // 1. 捕获 JS 运行时错误与静态资源加载失败(需开启 capture 阶段) window.addEventListener('error', (event) => { const target = event.target as HTMLElement; // 区分资源加载错误还是 JS 执行错误 if (target && (target.tagName === 'SCRIPT' || target.tagName === 'LINK' || target.tagName === 'IMG')) { this.captureResourceError(target); } else { this.captureJsError(event.error || event.message, event.filename, event.lineno, event.colno); } }, true); // 2. 捕获未处理的 Promise 拒绝 window.addEventListener('unhandledrejection', (event) => { let reason = event.reason; let message = 'Unhandled Rejection'; let stack = ''; if (reason instanceof Error) { message = reason.message; stack = reason.stack || ''; } else if (typeof reason === 'string') { message = reason; } this.emitReport({ fingerprint: this.generateFingerprint('promise', message, stack), message, stack, category: 'promise', timestamp: Date.now(), breadcrumbs: [...this.breadcrumbs], meta: {} }); }); } private captureJsError(error: Error | string, filename?: string, line?: number, col?: number) { const message = error instanceof Error ? error.message : String(error); const stack = error instanceof Error ? error.stack : `at ${filename}:${line}:${col}`; this.emitReport({ fingerprint: this.generateFingerprint('js', message, stack || ''), message, stack, category: 'js', timestamp: Date.now(), breadcrumbs: [...this.breadcrumbs], meta: { filename, line, col } }); } private captureResourceError(element: HTMLElement) { const url = (element as any).src || (element as any).href; const tagName = element.tagName.toLowerCase(); this.emitReport({ fingerprint: this.generateFingerprint('resource', `${tagName}_${url}`, ''), message: `Resource load failed: <${tagName}> ${url}`, category: 'resource', timestamp: Date.now(), breadcrumbs: [...this.breadcrumbs], meta: { tagName, url } }); } private addBreadcrumb(item: BreadcrumbItem) { this.breadcrumbs.push(item); if (this.breadcrumbs.length > this.maxBreadcrumbs) { this.breadcrumbs.shift(); } } private hijackUserInteractions() { window.addEventListener('click', (e) => { const target = e.target as HTMLElement; this.addBreadcrumb({ type: 'click', timestamp: Date.now(), detail: { tagName: target.tagName, className: target.className, id: target.id, innerText: target.innerText?.slice(0, 30) } }); }, { capture: true, passive: true }); } private hijackNetwork() { const originalFetch = window.fetch; window.fetch = async (...args) => { const url = typeof args[0] === 'string' ? args[0] : (args[0] as Request).url; try { const response = await originalFetch(...args); if (!response.ok) { this.addBreadcrumb({ type: 'network', timestamp: Date.now(), detail: { url, status: response.status } }); } return response; } catch (err: any) { this.addBreadcrumb({ type: 'network', timestamp: Date.now(), detail: { url, error: err.message } }); throw err; } }; } /** * 基于错误信息与调用栈首行生成唯一哈希指纹 */ private generateFingerprint(category: string, message: string, stack: string): string { const cleanStack = stack.split('\n').slice(0, 3).join(''); const raw = `${category}_${message}_${cleanStack}`; let hash = 0; for (let i = 0; i < raw.length; i++) { hash = (hash << 5) - hash + raw.charCodeAt(i); hash |= 0; } return `fp_${Math.abs(hash).toString(16)}`; } /** * 指纹自适应限流:同类错误 10 秒内只报一次,并带上频次计数 */ private emitReport(payload: ErrorEventPayload) { const now = Date.now(); const lastReportTime = this.reportedFingerprints.get(payload.fingerprint); if (lastReportTime && now - lastReportTime < 10000) { // 触发频次抑制 return; } this.reportedFingerprints.set(payload.fingerprint, now); this.send(payload); } private send(data: ErrorEventPayload) { const body = JSON.stringify(data); // 优先使用 sendBeacon 避免页面卸载时丢失请求 if (navigator.sendBeacon) { const blob = new Blob([body], { type: 'application/json' }); navigator.sendBeacon(this.reportUrl, blob); } else { fetch(this.reportUrl, { method: 'POST', body, headers: { 'Content-Type': 'application/json' }, keepalive: true }).catch(() => {}); } } }

跨域脚本报错Script error.破局

当静态资源部署在独立 CDN 域名下时,如果 JS 报错,浏览器出于同源安全策略会将错误详情遮蔽为通篇一律的Script error. at line 0

解决此问题的双重防线:

  1. 服务端响应头:CDN 必须配置Access-Control-Allow-Origin: *
  2. HTML 标签属性:在注入或打包引用脚本时,显式添加crossorigin="anonymous"属性。
  3. 框架顶层 ErrorBoundary / try-catch 补位:在模块内部的主入口手动包裹,提取最底层的原生 Error 对象,规避外层window.onerror的跨域清洗。

通过端侧结构化面包屑沉淀、精细化指纹聚合限流与安全通道上报,前端监控才能在纷繁复杂的异常风暴中稳立定盘星,精准指引每一次线上故障的秒级定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询