前端错误追踪系统搭建实战与关键问题解决方案

小春芳 优化 阅读 2,751
赞 33 收藏
二维码
手机扫码查看
反馈

谁更灵活?谁更省事?

我去年在做一个后台管理系统的前端重构时,被错误追踪这事整得挺崩溃的。不是没加监控——我们一开始用了 Sentry,配好了,也上报了,但上线后发现:真实用户遇到的白屏、卡死、API 401 后无限重试,压根没进 Sentry。查日志要翻三四个地方,还得靠用户截图描述“点了按钮没反应”,最后定位到是某个 Promise 链里 catch 被吞了,还顺带把 window.onerror 给屏蔽了。

前端错误追踪系统搭建实战与关键问题解决方案

那次之后我就下定决心:不光要看“能不能上报”,更要看“它到底会不会漏报”、“我改起来麻不麻烦”、“出问题时能不能一眼看出是哪行 JS 崩的”。所以这次我把手头用过、踩过、评估过的几个主流方案拉出来硬刚了一把:原生 window.onerror + window.addEventListener('unhandledrejection')、Sentry(v7)、Bugsnag(v7)、还有我最近自己撸的一个轻量版——叫 errlog-lite(开源在 GitHub,不是广告,后面会贴核心代码)。

先说结论:我日常开发选 errlog-lite,中大型项目上 Sentry,小项目或合规敏感场景用 Bugsnag

别急着划走,这个结论不是拍脑袋。下面全是我自己搭环境、模拟各种崩法(Promise reject 不 catch、React Error Boundary 漏掉的、setTimeout 里的 throw、Web Worker 报错、甚至 Service Worker install 失败)后的真实反馈。

原生方案:最透明,但也最“裸”

我一度以为“自己写最可控”,直到写了第三遍才发现:原生方案根本不是“轻量”,是“缺斤少两”。它连 source map 解析都不做,堆栈全是压缩后的 app.8a3f2.js:1:12345,你得手动 sourcemap 查,而且 unhandledrejection 默认只捕获未 catch 的 Promise,如果某人写了 .catch(() => {}) 然后里面又 throw 了——对不起,这算“已处理”,原生方案直接无视。

代码看着干净:

window.onerror = (msg, url, line, col, err) => {
  console.log('onerror', { msg, url, line, col, err });
  // 这里发请求上报...
};

window.addEventListener('unhandledrejection', e => {
  console.log('unhandledrejection', e.reason);
  // 注意:e.reason 可能是 string,也可能是 Error 实例
});

但实际用起来,漏报率比我预估的高 30%+。尤其在 React/Vue 项目里,组件生命周期里的异步错误经常绕过这两条。我最后放弃它的原因很简单:**不是不好,是太费劲。每次新项目都要补一堆兼容逻辑,比如捕获 fetch 错误、监听 console.error(有些库就靠这个抛错)、还要手动 patch setTimeoutsetInterval……折腾了半天发现,不如直接抄个现成的。

Sentry:功能最全,但配置像考公

我承认,Sentry 是目前生态最成熟、上下文最丰富的方案。Source map 自动上传、用户行为回溯(Replay)、Performance 数据联动、Release 关联、Issue 聚类……全都有。我们上一个中后台系统用了它,确实帮我们快速定位到一个“登录态失效后,页面持续轮询 API 导致内存暴涨”的问题。

但它有个致命问题:**太重,且默认配置会骗你**。比如它默认开启 normalizeDepth: 3,结果你上报一个嵌套很深的 error 对象,关键字段全被截断了;又比如它默认过滤掉 ResizeObserver loop limit exceeded 这种 Chrome 特有警告——可我们偏偏就因为这个报错导致 React 渲染卡死,而 Sentry 完全没记录。

另外,Sentry 的 SDK 初始化必须在所有业务代码前执行,否则漏报。我之前在一个微前端项目里,主应用加载了 Sentry,子应用却没传 context,结果子应用的错误全变成 unknown。调了两天才发现是 SDK 实例没共享。

它适合谁?团队有专人维护监控平台、有 CI/CD 自动上传 sourcemap、能接受每月 5000 次免费事件(超出要钱)。不适合谁?就一个前端、赶工期、不想花半天配 DSN 和 release 版本号的项目。

Bugsnag:安静、稳定、有点贵

Bugsnag 给我的感觉就是:一个穿衬衫打领带的运维大叔。不吵不闹,不出错,也不给你惊喜。它的默认行为比 Sentry 更保守,比如对 Promise rejection 的捕获更激进(连 .catch(() => {}) 里再 throw 都能抓到),而且对跨域脚本的 error 信息处理更友好(Sentry 在某些情况下会显示 Script error.,Bugsnag 能拿到部分 URL)。

但它唯一让我皱眉的是定价策略——免费版只保留 7 天数据,且不支持自建服务端。我们有个客户项目要求所有日志落本地,Bugsnag 直接 pass。另外,它对 React/Vue 的集成是“插件式”的,不像 Sentry 那样深度耦合框架生命周期,所以有些 hook 内部错误它就看不到。

errlog-lite:我写的“够用就行”方案

上面三个都试过之后,我决定自己撸一个最小闭环:只做三件事——捕获全部 JS 错误、自动收集 UA/URL/时间戳、支持 source map 解析(通过 CDN 加载)、轻量(gzip 后 < 3KB)。它没有 Dashboard,不上云,所有上报都 POST 到你自己的接口,比如 https://jztheme.com/api/errlog

核心就这几十行:

export function initErrLog(options = {}) {
  const { endpoint = '/api/errlog', includeStack = true } = options;

  const send = (payload) => {
    fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    }).catch(() => {});
  };

  window.onerror = (msg, url, line, col, err) => {
    send({
      type: 'js-error',
      message: msg,
      url,
      line,
      column: col,
      stack: includeStack && err?.stack ? err.stack : undefined,
      userAgent: navigator.userAgent,
      href: location.href,
      timestamp: Date.now()
    });
  };

  window.addEventListener('unhandledrejection', e => {
    send({
      type: 'unhandledrejection',
      reason: e.reason instanceof Error ? e.reason.stack : String(e.reason),
      href: location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now()
    });
  });
}

我用它上线了两个内部工具项目,至今没漏过一次白屏。为啥?因为它不做任何“智能过滤”,也不猜你想要什么上下文——你让它上报啥,它就上报啥。source map 解析我交给后端做了(Nginx + sourcemap-explorer),前端只管传原始 stack。简单、可控、不甩锅。

缺点也很明显:没 UI、没聚合、没告警。但它胜在——**我改一行代码就能加个字段,不需要等 SDK 发版,也不用翻文档找 config key**。

我的选型逻辑

  • 个人小项目 / 内部工具 / MVP 验证:直接上 errlog-lite。3 分钟接入,日志全在自己库里,想怎么查怎么查。
  • 中大型业务系统 / 有专职运维 / 需要长期迭代:Sentry。别省那点配置时间,它的 Issue 聚类和 Release tracking 真的救过我命。
  • 政企 / 金融 / 合规强要求 / 必须私有化部署:Bugsnag 自建版 or Sentry Self-Hosted。Bugsnag 私有化成本高,但文档更清晰;Sentry 功能多,但部署和升级稍复杂。

最后提醒一句:不管选哪个,**千万别只依赖前端错误监控**。我们后来加了一层后端兜底——所有前端上报的错误,后端也记一条 audit log,哪怕只是存个 trace_id。因为真有用户开着浏览器禁用 JS,或者拦截了你的上报请求,你得留个后门。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如怎么结合 performance.mark 做错误前性能分析、怎么给错误打业务标签(比如「支付失败」「导出超时」),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论