前端错误监控实战:从捕获到告警的完整方案

长孙梓涵 前端 阅读 829
赞 14 收藏
二维码
手机扫码查看
反馈

上线后用户报错,但控制台干干净净?

上周项目刚上线,测试同学说“一切正常”,结果第二天运营就甩来一张截图:用户点了个按钮,页面白屏了。我赶紧打开 DevTools,刷新页面,啥也没有——控制台清清爽爽,连个 warning 都没有。但用户那边就是炸了。这种“你本地跑得欢,线上死得惨”的情况,真让人抓狂。

前端错误监控实战:从捕获到告警的完整方案

一开始我以为是资源加载失败,比如 JS 文件 404 了。但查了 CDN 日志,所有静态资源都 200。后来又怀疑是某个第三方 SDK 报错被吞了,但把所有可疑的 script 标签一个个注释掉,问题依旧。折腾了半天,突然意识到:**有些错误根本不会在控制台打印出来**,尤其是异步回调、Promise reject、或者跨域脚本里的错误。这时候我才想起来,我们压根没做全局错误监控!

先上 window.onerror,结果只抓到一半

赶紧补救。最简单的办法当然是监听 window.onerror。我立马加了几行代码:

window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到错误:', { message, source, lineno, colno, error });
  // 这里应该上报到后端
  return true; // 阻止默认错误处理(比如控制台打印)
};

本地测试了一下,同步错误确实能抓到。比如故意写个 undefinedVar.foo(),马上就能收到日志。但当我用 setTimeout 包一下,或者用 fetch 模拟网络错误时,onerror 完全没反应!

查了 MDN 才想起来:window.onerror 对 Promise reject 是无能为力的。而且如果错误发生在跨域脚本里(比如从 CDN 加载的 vendor.js),出于安全限制,拿到的 message 可能只是 “Script error.”,连具体哪行出错都不知道。这坑我以前踩过,但这次又忘了。

Promise 错误怎么破?得靠 unhandledrejection

既然 Promise 的 reject 没被捕获会静默失败,那必须单独监听。好在现代浏览器提供了 unhandledrejection 事件:

window.addEventListener('unhandledrejection', event => {
  console.log('未处理的 Promise 拒绝:', event.reason);
  // 上报逻辑
  event.preventDefault(); // 阻止默认行为(比如控制台警告)
});

加上这段后,Promise 相关的错误终于能捕获了。但问题又来了:有些错误既不是同步抛出,也不是顶层 Promise reject,而是发生在 Vue 或 React 组件内部。比如 Vue 里某个方法调用了一个不存在的函数,这种错误会被框架自己的错误处理机制吃掉,根本不会冒泡到 window 层。

框架层的错误,得进框架内部捞

我们项目用的是 Vue 2。Vue 提供了全局的 Vue.config.errorHandler,专门用来捕获组件渲染和 watcher 中的错误。赶紧补上:

import Vue from 'vue';

Vue.config.errorHandler = (err, vm, info) => {
  console.log('Vue 组件错误:', { err, info });
  // 上报
};

React 的话就得用 Error Boundary 了,不过那是组件级别的,得在顶层包一层。但不管怎样,**不同环境的错误得用不同的钩子去抓**,这点让我有点烦躁——能不能统一处理?

核心代码就这几行:整合所有错误源

最后我搞了个简单的错误监控模块,把能想到的错误源都兜住。代码其实不复杂,关键是要覆盖全面:

// errorMonitor.js

function reportError(errorInfo) {
  // 这里替换成你自己的上报接口
  fetch('https://jztheme.com/api/error-report', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(errorInfo)
  }).catch(() => {
    // 上报失败也别让错误扩散
  });
}

// 1. 全局同步错误
window.onerror = function(message, source, lineno, colno, error) {
  const isScriptError = message === 'Script error.' || message === '';
  if (isScriptError) {
    // 跨域脚本错误,信息太少,但至少记录一下
    reportError({
      type: 'script-error',
      message: 'External script error',
      url: source,
      line: lineno,
      col: colno
    });
  } else {
    reportError({
      type: 'js-error',
      message,
      stack: error?.stack || '',
      url: source,
      line: lineno,
      col: colno
    });
  }
  return true;
};

// 2. 未处理的 Promise reject
window.addEventListener('unhandledrejection', event => {
  reportError({
    type: 'unhandled-promise',
    reason: event.reason?.toString() || 'Unknown reason',
    stack: event.reason?.stack || ''
  });
  event.preventDefault();
});

// 3. Vue 错误(根据项目框架调整)
if (typeof Vue !== 'undefined') {
  Vue.config.errorHandler = (err, vm, info) => {
    reportError({
      type: 'vue-error',
      message: err.message,
      stack: err.stack,
      component: vm?.$options?.name || 'unknown',
      info
    });
  };
}

这段代码我放在入口文件最顶部,确保尽早执行。亲测有效:现在无论是同步错误、异步错误、Promise 拒绝还是 Vue 组件崩了,都能上报到后端。后台看板一目了然,再也不用靠用户截图猜错了。

踩坑提醒:这三点一定注意

  • 跨域脚本的错误信息是残缺的。如果你的 JS 打包后部署在 CDN,记得给 script 标签加上 crossorigin="anonymous",同时服务器要返回 Access-Control-Allow-Origin 头。不然 onerror 里只能拿到 “Script error.”,毫无用处。
  • 别在错误处理函数里再抛错。比如 reportError 里用了 fetch,如果 fetch 本身出错(比如网络断了),千万别让它抛出去,否则可能陷入无限循环。所以我在 fetch 外面包了 .catch(() => {})
  • Vue 的 errorHandler 只在生产模式生效。开发环境下 Vue 会直接把错误 throw 出来,方便调试。所以测试错误上报时,一定要用 production build。

改完之后,大部分错误都能捕获了。不过还有个小问题:某些极端情况下的内存泄漏错误(比如递归爆栈)可能连 onerror 都来不及触发。但这属于极少数场景,目前没遇到,暂时不管了。

不是最优解,但够用

其实更专业的做法是用 Sentry 或者自研一套完整的监控系统,带 source map 解析、错误聚合、用户行为回溯等功能。但我们小团队资源有限,这个轻量级方案已经能解决 90% 的问题了。毕竟,能快速定位线上问题是第一位的,没必要一上来就搞重武器。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如怎么优雅地处理 React 的 Error Boundary 上报,或者如何避免重复上报相同错误,我都想听听你的经验。

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

暂无评论