前阵子有个线上项目出了个诡异的问题:某个页面的按钮偶尔没反应,本地联调和测试环境都复现不出来,用户反馈也是断断续续。打开生产环境的控制台,只见一片空白——没有报错、没有警告,仿佛一切正常。这种“静默失败”最折磨人。也就是那阵子,我决定动手写一个专门用于前端运行时错误捕获和分析的小工具,代号叫 rea(Runtime Error Analyzer)。它要解决的事情很朴素:把线上环境里看不到的报错,自动抓到、整理清楚、在需要的时候告诉我。下面这篇记录里有完整的设计思路,也有接入真实项目后的坑和教训,做前端监控、做工具链的同学可以参考。
1. 为什么我会动手写一个叫“rea”的小工具
1.1 一个让我反复折腾的线上故障
那次出问题的模块本身不复杂:一个列表页,用户点击某一行的“详情”按钮,弹层展示详情数据。问题在于,这个弹层在部分用户那里能打开,在另一部分用户那里点了毫无反应。因为没有报错信息,我根本不知道是点击事件没绑定上,还是接口返回的数据结构不对,还是弹层组件渲染时抛了异常。
几轮排查下来,唯一的收获是“在某一台测试机上偶尔能复现”,但一开 DevTools 就恢复正常,典型的“调试时它就好,关掉就出问题”。这种问题靠本地日志根本没法追。我意识到,生产环境的应用绝对不能依赖“用户帮你打开控制台”这种排查方式,必须在代码层面自动把错误信息留下来。
这个痛点很普遍:业务代码一上线,就是各种压缩混淆、异步加载、第三方脚本、不同浏览器内核的组合,报错信息失真严重。与其继续被这个问题反复折磨,不如自己做一个轻量级的错误捕获工具,把线上错误“挖”出来。
1.2 为什么没直接套用现成的监控平台
你可能想问:行业里成熟的监控平台那么多,直接用不就好了?我当时也认真评估过,最后放弃了,理由有几个很现实:
- 配置链路太长。要注册账号、建应用、配域名白名单、加 SDK,小项目根本不想搞这么重的仪式感。
- 数据出域问题。有些项目的业务数据比较敏感,错误堆栈里可能夹带 URL 参数、接口字段,团队对数据上报到第三方平台有顾虑。
- 免费额度对高频项目不友好。错误监控按量计费,一个稍微活跃点的前端应用,错误上报量很容易把免费额度跑穿。
- 定制空间有限。现成平台通常只给你“上报—展示”的功能,想按自己的思路做错误聚类、按业务线过滤、自定义面包屑,都很麻烦。
自研不一定能做得比大平台更广,但可以做得更贴自己的业务。rea 的定位从一开始就很明确:轻量、自托管、能细化定制,只做“错误采集—清洗—上报—查询”这条链路。
1.3 rea 的功能需求清单
动手之前,我把需求写成了一张清单,后面所有开发和迭代都以它为准:
- 捕获全局未处理异常(window error);
- 捕获未处理的 Promise 拒绝(unhandledrejection);
- 捕获资源加载失败(脚本、图片、样式等);
- 正常提取错误堆栈,并对堆栈做清洗降噪;
- 记录用户的关键操作路径(点击、路由跳转、接口请求),用于还原出错现场;
- 批量上报并支持失败重试;
- 对业务代码的性能影响降到最低;
- 支持 sourcemap 映射,把压缩后的堆栈还原成源码位置。
有了这份清单,我才好确定架构边界,不至于一上来就设计得过度复杂。
2. rea 的架构设计与模块划分
2.1 整体设计思路:把错误当数据流处理
rea 的核心并不复杂,简单说就是“全局布点、统一收集、集中处理”。整个工具在运行时是一个单例对象,页面初始化时注册各种全局监听器,然后把捕获到的错误统一塞进一条处理管道里。
我把这条管道分成四层,各管各的:
- 捕获层:负责监听各种来源的错误事件,把浏览器原始的 error 事件、rejection 事件、资源加载失败事件转换成统一的内部数据结构;
- 解析层:对原始错误信息做清洗、格式化,过滤掉框架内部调用帧,提取关键堆栈帧,必要时做 sourcemap 还原;
- 存储层:维护一个内存队列,积攒一定量错误后统一上报;同时用本地存储暂存失败的上报请求;
- 上报层:把错误数据通过接口发送到自建日志服务,支持批量、节流、去重。
你可以把这四层理解成一条流水线:捕获层是摄像头,先把现场录下来;解析层是剪辑师,把没用的画面剪掉;存储层是临时仓库,货攒够了再发车;上报层是快递员,负责把包裹安稳送到目的地。这样分层的好处是每一层都可以独立替换或扩展,比如上报层想从走接口改成走日志网关,只需要改一个模块。
2.2 错误记录的数据结构设计
数据模型是错误分析工具的地基。如果一开始字段设计得不够用,后面加字段会连带改一堆模块。我在 rea 里定义了一条错误记录的核心结构:
interface REAErrorRecord { id: string; // 唯一标识,用于去重 time: number; // 错误发生时间戳 level: 'error' | 'warn' | 'info'; type: 'script' | 'resource' | 'unhandledrejection' | 'custom'; message: string; // 错误信息 stack: string; // 清洗后的堆栈 filename: string; // 出错文件 lineno: number; colno: number; breadcrumbs: REABreadcrumb[]; // 用户操作轨迹 device: { userAgent: string; language: string; screen: string; }; url: string; // 出错页面地址 custom?: Record<string, unknown>; // 业务自定义字段 }这份结构里有两个地方容易被忽略。一是breadcrumbs和device必须和错误本身一起上报,因为同一个错误在不同设备、不同操作路径下的根因可能完全不一样;二是custom字段一定要放开,因为业务方经常需要额外带上“当前用户身份”“订单号”这类上下文信息。没有这个扩展位,工具用起来会很别扭。
2.3 模块间通信:轻量事件管道
rea 内部没有引入复杂的状态管理,模块之间通过一个极简的发布订阅通道通信。捕获层产生一条新错误时,调用rea.push(record);解析层监听push事件做清洗;上报层监听解析结果做批量发送。这个通道只做一件事——把数据从源头送到下游,没有多余动作。
// 简化版事件通道 const listeners = new Map(); function on(eventName, handler) { if (!listeners.has(eventName)) listeners.set(eventName, []); listeners.get(eventName).push(handler); } function emit(eventName, data) { const handlers = listeners.get(eventName) || []; handlers.forEach((handler) => { try { handler(data); } catch (e) { // 处理函数自身出错时不能影响主流程 } }); }这里有个很重要的设计取舍:监听器内部必须各自捕获异常。因为错误分析工具本身的代码如果抛错,绝不能再抛回业务环境里去。否则就是“本来想治病的药把病人吃坏了”。这也是这类工具的基本自觉。
3. 关键实现细节:从捕获到上报的完整链路
3.1 全局错误事件监听:onerror 与 addEventListener 的取舍
捕获全局脚本异常,最常见的方式有两种:window.onerror和window.addEventListener('error', handler, true)。很多人直接选一个用,但两者有本质区别。
window.onerror能拿到message、source、lineno、colno、error对象,信息比较完整,但它是一个覆盖式的属性赋值,如果项目里其他代码也设置了window.onerror,就会被顶掉。比如有些第三方 SDK 或老代码喜欢劫持onerror,很容易互相覆盖。
addEventListener('error', handler, true)注册的是标准的 DOM 事件监听,可以并存多个处理器,不会被覆盖。而且第三个参数用true开启捕获阶段,能比冒泡阶段更早接住错误,也更容易区分错误来源。但直接从 event 对象上解析信息会比较绕,不如onerror直观。
rea 的实际做法是:统一使用addEventListener('error', handler, true),在 handler 内部从事件对象上提取信息。这样做的好处是接管了全局注册权,不会和业务代码、第三方库冲突;代价是自己多写几行解析逻辑。核心代码如下:
window.addEventListener('error', (event) => { // 区分资源加载失败和脚本运行错误 const target = event.target; if (target && (target.src || target.href)) { // 资源加载失败,例如 <script src="..."> rea.push({ type: 'resource', level: 'error', time: Date.now(), message: `${target.tagName.toLowerCase()} 加载失败: ${target.src || target.href}`, stack: '', filename: target.src || target.href, lineno: 0, colno: 0 }); return; } // 脚本运行错误 rea.push({ type: 'script', level: 'error', time: Date.now(), message: event.message, stack: event.error ? event.error.stack : '', filename: event.filename || '', lineno: event.lineno || 0, colno: event.colno || 0 }); }, true);判断资源加载失败靠的是event.target是否存在src或href。因为资源加载出错时,event.message往往为空,但 target 会指向具体的 DOM 元素,能拿到实际的加载地址。这个细节特别适合排查“某个 JS 文件没加载出来导致功能白屏”的问题。
3.2 Promise 拒绝的捕获:异步错误的兜底网
window.onerror和error事件有一个共同短板:捕获不到未处理的 Promise 拒绝。比如一句await someAsyncFunc()内部抛异常,但外层没有 try/catch,在大多数浏览器里只会触发unhandledrejection事件,不会触发全局 error。类似问题在现代前端项目里占比越来越高,尤其是接口请求相关的异常。
rea 单独监听unhandledrejection事件:
window.addEventListener('unhandledrejection', (event) => { const reason = event.reason; let message = ''; let stack = ''; if (reason instanceof Error) { message = reason.message; stack = reason.stack ? cleanStack(reason.stack) : ''; } else { // 有些 promise 拒绝抛出的不是 Error 对象,而是普通对象或字符串 message = typeof reason === 'string' ? reason : JSON.stringify(reason); } rea.push({ type: 'unhandledrejection', level: 'error', time: Date.now(), message, stack, filename: stack ? extractFileNameFromStack(stack) : '', lineno: 0, colno: 0 }); });这里我踩过一个坑:很多拒绝原因并不是Error实例,而是一个普通对象,比如Promise.reject({ code: 403, msg: 'forbidden' })。如果直接读reason.message,会得到 undefined;如果直接JSON.stringify(reason),又可能遇到循环引用导致序列化失败。所以要先判断类型,再决定怎么取值,非 Error 对象尽量用安全序列化处理,能拿多少信息拿多少。
另外,捕获 unhandledrejection 是有时间窗口的。浏览器会在 Promise 被标记为无法处理时才触发这个事件,如果代码里某个 promise 在后续才被 catch,则不会触发。实际项目中我遇到过“报错记录里出现了一条 unhandledrejection,但业务代码明明有 catch”的情况,就是因为 catch 调用晚于事件派发。遇到这类误报,需要在业务侧规范写法,尽量在 promise 链中同步 catch。
3.3 堆栈清洗与格式化:让报错不再是一大坨噪音
浏览器原生的error.stack是一长串文本,里面混合着业务代码、框架内部代码、第三方库代码,直接看非常痛苦。特别是在压缩后的生产包里,堆栈还可能是完全没意义的混淆名称。rea 的解析层要做三件事:
第一,按行解析堆栈帧。每一行对应一个调用帧,提取出文件名、行号、列号、函数名。以 Chrome 为例,典型的调用帧长这样:
at handleClick (app.js:123:45) at HTMLButtonElement.dispatch (vendor.js:45:12)我用正则把这几种常见格式统一解析成结构化对象,再拼成可读字符串。
第二,过滤框架与库的内部帧。框架内部的调度代码、事件绑定代码、渲染代码对定位业务错误帮助不大,反而干扰视线。rea 维护了一个“噪音关键词”列表,凡是调用栈里出现这些关键词的帧,一概过滤掉。这个列表可以按项目动态配置。
const defaultNoisePatterns = [ /node_modules\//, /webpack\/runtime/, /react-dom/, /vue\.runtime/, /zone\.js/, /core-js/ ]; function cleanStack(stack, extraPatterns = []) { const patterns = [...defaultNoisePatterns, ...extraPatterns]; return stack .split('\n') .map((line) => line.trim()) .filter((line) => line && !patterns.some((p) => p.test(line))) .slice(0, 10) // 最多保留 10 帧,避免无用的长堆栈 .join('\n'); }第三,控制堆栈长度。有些嵌套很深的报错堆栈能有几十上百行,全部上报既费流量也不好阅读。截取前 10 个有效帧,足够定位绝大多数问题,还能大幅降低上报数据体量。
3.4 面包屑记录:还原用户出错前的操作路径
光有一条错误堆栈,往往只能告诉你“代码在哪一行炸了”,但无法告诉你“用户是怎么走到这一步的”。rea 里加入了一个简单的面包屑记录机制,类似路口监控:用户点击、路由切换、接口请求发生时,都往循环队列里记一条小记录。出错时,把最近 30 条操作轨迹和错误记录一起上报。
const breadcrumbs = []; function addBreadcrumb(category, message, data) { breadcrumbs.push({ time: Date.now(), category, // 'click' | 'route' | 'request' | 'custom' message, data: data ? safeStringify(data) : '' }); if (breadcrumbs.length > 30) breadcrumbs.shift(); // 只保留最近 30 条 } document.addEventListener('click', (e) => { const el = e.target; addBreadcrumb('click', `${el.tagName}${el.id ? '#' + el.id : ''}${el.className ? '.' + el.className : ''}`.slice(0, 80)); }, true);注意两个细节:一是面包屑队列必须固定容量,否则站点访问时间长了内存越占越多;二是记录内容要做截断,不能把超长的选择器或接口响应都塞进一条面包屑里。
面包屑在实际排障中价值极大。曾经有一条线上错误的堆栈完全指向一个工具函数,单看堆栈没人知道为什么触发。后来翻面包屑,发现用户先在一个输入框键入内容,然后快速连点了两次提交按钮,第二个请求在第一个请求返回前就发出去,收到了一个非预期状态码,引发后续渲染报错。这条路径,光靠堆栈根本看不出来。
3.5 上报策略:批量、防抖、重试
错误上报如果每发生一条就发一个请求,既浪费连接也容易被打到日志接口限流。rea 的上报层做了三层控制:
批量发送。错误先进入内存队列,当队列长度达到阈值(比如 10 条),或距离上一次上报超过一定时间(比如 5 秒),统一发送一次。这样既保证实时性,又显著减少请求次数。
防抖与节流。如果某个瞬间连续触发几十条同类错误,比如某个公共模块加载失败导致连锁报错,rea 会按错误指纹去重合并。指纹用“错误类型 + 文件名 + 行号 + 消息前 50 字符”生成一个哈希,同一指纹在短时间窗口内只上报第一条。
失败重试。上报请求可能因为网络波动或服务器短暂异常而失败。rea 会把发送失败的批次序列化后写入localStorage,下次页面加载或下一次定时器触发时重新尝试。
下面是最核心的上报逻辑:
const queue = []; let timer = null; const MAX_BATCH = 10; const FLUSH_INTERVAL = 5000; function flush() { if (queue.length === 0) return; const events = queue.splice(0, queue.length); const payload = JSON.stringify({ events, appId: REA_APP_ID }); if (navigator.sendBeacon) { // sendBeacon 在页面卸载时也能可靠发送 navigator.sendBeacon(REA_REPORT_URL, payload); } else { fetch(REA_REPORT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true }).catch(() => { // 失败暂存,等下次重试 try { const saved = JSON.parse(localStorage.getItem(REA_SAVE_KEY) || '[]'); saved.push(...events); localStorage.setItem(REA_SAVE_KEY, JSON.stringify(saved)); } catch (e) { /* 本地存储写入失败就丢弃,避免无限循环 */ } }); } }这里值得说一句:在页面关闭、跳转这种场景下,fetch可能还没发出去连接就被中断了,但sendBeacon是专门为“页面即将销毁时上报”设计的,所以优先用sendBeacon。rea 在移动端 H5 里接入时,这一个选择直接决定了很多关键错误会不会在用户离开页面时丢失。
4. 接入真实项目后我踩过的坑
4.1 “Script error.” 空报错:被浏览器屏蔽的跨域异常
rea 上线跑了一周后,我发现上报里有相当一部分错误信息只有孤零零的一行Script error.,没有堆栈、没有行列号,根本没法用。查了半天,这是浏览器对跨域脚本的安全限制:当脚本通过<script src="//other-domain.com/lib.js">加载时,如果目标服务器没有返回正确的 CORS 头,浏览器会把运行时的具体报错信息隐藏起来,只留一句毫无意义的 “Script error.”。
定位这类问题有两个层面。第一层是让脚本本身支持跨域捕获:在<script>标签上添加crossorigin="anonymous"属性,同时要求服务器在响应头里带上Access-Control-Allow-Origin: *或对应域名。这样浏览器才肯把具体的错误信息传给全局事件。
第二层是兜底处理:对于实在无法加头的第三方脚本,rea 会把Script error.单独标记,不进入常规错误聚合,避免噪音干扰。这类错误虽然拿不到具体堆栈,但它至少提示了“有一个外部脚本在某个页面上炸了”,有线索总比没线索好。
4.2 重复上报:一组错误出现两条记录
调试时我又发现一个怪现象:同一条错误经常出现两条记录,一条来自error事件,一条来自unhandledrejection。后来弄明白了,某些浏览器把异步函数中的异常既派发为 rejection,又在某些场景下触发了全局 error;也有的是因为我在window.addEventListener('error')里对这种错误做了处理,但框架内部自己也注册了一套错误处理器,两边同时捕获,又都往 rea 里塞。
解决思路是加一个“已处理指纹”的集合。rea 在 push 一条错误前,先计算唯一指纹,如果短时间内该指纹已经存在,就只更新计数不上报重复内容。
const recentKeys = new Set(); function makeFingerprint(record) { return `${record.type}:${record.filename}:${record.lineno}:${record.colno}:${record.message.slice(0, 50)}`; } function push(record) { const key = makeFingerprint(record); if (recentKeys.has(key)) { return; // 重复错误,直接忽略 } recentKeys.add(key); setTimeout(() => recentKeys.delete(key), 60000); // 60 秒内去重 emit('record', record); }这个 60 秒去重窗口足够覆盖绝大多数重复捕获场景。加了这层之后,错误列表瞬间清爽了很多。
4.3 压缩混淆后的堆栈还原:sourcemap 的正确用法
生产环境跑的是压缩混淆过的代码,报错堆栈常见长这样:
at t (app.9f8a2b.js:1:12345) at n (app.9f8a2b.js:1:23456)这种堆栈只能证明“代码在 app.js 里炸了”,但完全定位不到源码。要解决这个问题,必须做 sourcemap 还原。
首先,在构建阶段生成 sourcemap 文件。以常见的前端构建工具为例,配置里开启 sourcemap 输出,得到app.js.map这样的文件。然后,rea 的服务端在接收到带行列号的错误堆栈后,用 sourcemap 解析库把压缩位置映射回源码位置。
// 伪代码:源码位置还原逻辑 const { SourceMapConsumer } = require('source-map'); async function resolveStackFrame(stackFrame, mapFiles) { const consumer = await new SourceMapConsumer(mapFiles[stackFrame.file]); const pos = consumer.originalPositionFor({ line: stackFrame.line, column: stackFrame.column }); return { source: pos.source, // 原始文件名 line: pos.line, // 原始行号 column: pos.column, // 原始列号 name: pos.name // 原始函数名 }; }用这个方案,我成功定位过好几个之前完全看不懂的报错,最后发现都是压缩后代码逻辑顺序导致的兼容性问题。
这里必须提醒一个安全红线:sourcemap 文件绝不能发布到公网静态目录。因为 sourcemap 会暴露完整源码,等于把项目的业务逻辑全部公开。rea 的方案是:sourcemap 只在服务端留存,且只在错误解析链路中使用,静态服务器上做一个“禁止访问”规则。还有另一个折中方案:不上传整个 map 文件,只把构建产物中“源码路径、变量名”的映射关系以脱敏形式存入内部服务。这一步千万不能省。
4.4 误报与噪音:业务主动抛错也算 error
接入一段时间后,我们收到一批“看起来不太对”的上报:错误信息是一串业务提示,比如“订单状态异常”“无权限访问”“数据为空”,它们不是代码 bug,而是业务逻辑主动抛出的异常或进行某种流程控制时触发的提示。这些报错如果原样上报,会把错误列表搅浑,让人分不清哪些是真故障。
rea 为此设计了忽略规则和自定义分级机制。对于已知的业务抛出场景,运营方可以把特定的错误信息模板加入忽略列表;也可以把某些业务异常标记为 warn 级别而不是 error 级别,不进入核心告警。在 rea 的配置面板上,我用“错误指纹模板 + 匹配规则”的简单方式管理,比如按错误信息前缀匹配、按文件名匹配、按自定义标签匹配。
实际使用下来,这里的配置需要和业务团队充分沟通。有些业务主动抛错确实需要关注,比如“下单失败”就不只是流程控制,可能是接口异常的前置信号。所以 rea 的级别标记不是简单地屏蔽,而是分类处理。
4.5 性能开销:错误工具不能变成新的性能瓶颈
错误分析工具自己跑在业务页面上,如果设计得不谨慎,会让页面变得更卡,这就是本末倒置。rea 在性能上有几条铁律:
- 捕获和推送分离。监听器只负责把原始事件转成结构化数据、塞进队列,绝不直接在事件回调里做耗时操作,比如
JSON.stringify大对象、同步写入 localStorage、请求发送。 - 堆栈清洗延迟化。清洗和 sourcemap 解析这类计算量大的操作,尽量放到
requestIdleCallback里去执行,浏览器空闲了再处理。 - 上报批量与节流。前面说过,攒够 10 条或者 5 秒一次,避免高频请求。
- 错误对象安全序列化。有些错误对象里附加了巨大对象字段,直接序列化会卡主线程。rea 用自定义的
safeStringify,只保留可枚举的、浅层的、长度受限的字段。
有一次我实测,在低端安卓机上反复触发 50 条错误,页面帧率几乎没有波动,说明这种“延迟化 + 批量”的设计对性能影响足够小。自研工具最怕的就是“监控代码比业务代码还慢”,这一点必须作为核心指标来盯。
5. 日常接入 rea 后的真实效果与后续扩展
5.1 上线几周后我看到了这些变化
rea 接入到真实项目后的效果,比我想象中明显。最直接的改变是:排查问题从“打开日志盲猜”变成“拿着错误路径还原现场”。有两条记录让我印象很深。
一条是只在某品牌低端安卓机上出现的报错,堆栈指向某个数组方法在当前系统 WebView 中不受支持。这种问题如果靠用户反馈,几乎不可能定位,因为用户也不知道怎么描述。另一条是页面跳转后老页面定时器没有清理,在特定操作顺序下会调用一个已卸载组件的状态更新,最终抛错。这个 bug 在面包屑里清晰显示为“用户从商品列表页快速切换到详情页再切回,5 秒后报错”,定位过程不到十分钟。
这些案例证明了一点:错误上报工具真正有用的地方,不在于告诉你“哪一行代码炸了”,而在于把“用户路径 + 环境信息 + 错误堆栈”三者拼成完整现场。单纯一个堆栈,很多时候什么也证明不了。
5.2 后续扩展方向:聚类、告警、与环境结合
rea 当前版本满足了我的核心需求,但要说完全够用还早。我整理了几个明确想加的扩展方向,按优先级排:
第一是错误聚类。现在错误列表还是按时间排的,如果某个报错每天出现几百次,需要人工分辨它的规律。下一步准备按堆栈指纹做自动聚类,把同类错误合并成一条“问题”,并统计影响用户数和影响页面数。
第二是告警通知。rea 已经接入了简单的告警逻辑:某个“问题”在 5 分钟内的上报量超过阈值,就通过内部协作机器人的 webhook 推一条消息。这一步做好之后,线上故障就不需要等用户反馈了。
第三是与性能数据结合。错误经常不是孤立发生的,它背后可能是接口超时、资源加载慢、内存占用过高等。把错误记录和页面性能数据放在同一条时间线上看,能更容易发现“先有性能劣化、后有报错”的因果链。
5.3 给想自研错误分析工具的人几条实在建议
如果你也想动手写一个类似的内部工具,这几条经验应该能帮你少走弯路:
- 先跑通最小闭环。不要上来就设计监控平台、告警体系、可视化大盘。先把“捕获—清洗—上报—查询”这条链路跑起来,哪怕上报后只在日志平台里查,也算闭环。功能是长出来的,不是设计出来的。
- 配置项要给默认值。项目成员接入时,没人会愿意逐个配置参数。rea 的所有内部参数都有合理默认值,比如面包屑容量 30 条、上报间隔 5 秒、堆栈截取 10 帧。想改的再单独覆盖。
- 敏感字段必须脱敏。错误堆栈和面包屑里很可能夹带用户手机号、订单号、token。rea 在序列化时对明显敏感字段做正则替换,避免敏感数据落入日志系统。
- 不要试图捕获一切。捕获所有错误的最直接后果是噪音淹没信号。rea 的设计哲学是“关注用户能感知的异常”,比如页面白屏、功能无响应、接口数据渲染失败,而不是把所有 console 输出都收上来。
- 保证工具自身可观测。rea 本身也会出错,所以它的内部处理函数都有 try/catch 兜底,不会影响业务。但与此同时,我也给它留了一个 console 级别的调试开关,方便在开发环境排查 rea 自身的问题。
最后再分享一个小技巧。rea 上线后,我把它的上报接口单独做了每日“0 数据”巡检:如果连续一段时间一条错误都没有,那往往说明不是页面真的没有错误,而是监听器失效了,比如某个第三方库把全局事件劫持走。这种“无事发生”的警报,反而是最值得警惕的。
写 rea 这件小事的收获,远不止一个工具本身。它逼着我把“错误”当成系统的一等公民去设计:从捕获、标准化、存储到分析的每一步,都在加工数据,而不是被动接收数据。如果你所在的团队也经常被线上问题折磨,真的建议找一个周末,照着最小闭环自己试一版。工具未必一定要多复杂,能把“看不见的失败”变成“看得见的记录”,就已经值回票价了。