十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

前端监控 SDK 异常捕获与无损采样上报 前端监控 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: Recordstring, 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 Mapstring, 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。解决此问题的双重防线服务端响应头CDN 必须配置Access-Control-Allow-Origin: *。HTML 标签属性在注入或打包引用脚本时显式添加crossoriginanonymous属性。框架顶层 ErrorBoundary / try-catch 补位在模块内部的主入口手动包裹提取最底层的原生 Error 对象规避外层window.onerror的跨域清洗。通过端侧结构化面包屑沉淀、精细化指纹聚合限流与安全通道上报前端监控才能在纷繁复杂的异常风暴中稳立定盘星精准指引每一次线上故障的秒级定位。
返回列表