前端监控与错误追踪:异常捕获与上报
前端监控与错误追踪:从异常捕获到数据上报
为什么需要前端监控
前端环境复杂多变:不同浏览器、操作系统、网络状况、用户操作都可能引发意料之外的错误。由于这些错误发生在用户端,开发者在自己的设备上难以复现。一套完善的监控体系能够帮助我们:
- 及时发现线上问题,避免用户流失
- 定位错误根源,获取错误堆栈、用户行为路径等上下文
- 量化影响范围,了解错误发生的频率、影响用户数
- 优化产品体验,结合性能数据持续改进
本教程重点讲解如何在前端项目中捕获异常并将错误信息上报到服务端,为监控系统提供原始数据。
前端错误分类
在捕获异常之前,需要先了解前端可能出现的错误类型。
JavaScript 执行错误
运行时产生的错误,例如类型错误、引用错误、语法错误(在 eval 或动态脚本中)等。这类错误会导致当前代码块停止执行。
资源加载错误
当页面中的静态资源(图片、CSS、JavaScript 文件等)加载失败时触发,例如 404 或网络超时。这类错误不会冒泡,需要在捕获阶段处理。
异步错误
- Promise 内部异常:未被
.catch()捕获的 rejected Promise - async/await 中的异常:如果未使用 try/catch,会被视为未处理的 Promise 拒绝
- setTimeout / setInterval 回调中的错误
跨域脚本错误
当页面引用了不同源的 JavaScript 文件,且该文件内部发生错误时,浏览器出于安全考虑只会抛出 "Script error.",无法获取真实的错误信息和堆栈。需要配置跨域资源共享(CORS)来解决。
框架层面的错误
现代前端框架(React、Vue 等)会通过错误边界(Error Boundary)或全局错误处理器捕获组件渲染期间的错误,以避免整个应用崩溃。
异常捕获手段
try...catch
最基础的同步错误捕获方式,能捕获其作用域内抛出的异常。
try {
// 可能出错的代码
undefinedFunction();
} catch (error) {
console.error('捕获到错误:', error.message);
// 上报错误
}
局限性:无法捕获异步回调中的错误(除非回调内部也使用 try...catch),也无法捕获语法错误(解析阶段即失败)和异步 Promise 拒绝。
window.onerror
全局捕获运行时错误,包括代码执行错误、语法错误(部分)和异步任务中的错误(如 setTimeout 内的错误)。它能够提供错误消息、源 URL、行号、列号和 Error 对象。
window.onerror = function(message, source, lineno, colno, error) {
// 将错误信息组装上报
const errorInfo = {
type: 'js',
message,
filename: source,
line: lineno,
column: colno,
stack: error ? error.stack : ''
};
report(errorInfo);
// 返回 true 可以阻止浏览器默认的错误处理(如控制台报错)
return true;
};
注意:window.onerror 无法捕获资源加载错误和 Promise 错误。
window.addEventListener('error')
使用事件监听器方式捕获错误,比 window.onerror 更灵活,可以捕获资源加载错误(需将监听器绑定在捕获阶段)。
// 捕获 JavaScript 运行时错误(冒泡阶段)
window.addEventListener('error', (event) => {
// 当 event 是一个 ErrorEvent 时,表示 JS 错误
if (event.error) {
report({
type: 'js',
message: event.message,
filename: event.filename,
line: event.lineno,
col: event.colno,
stack: event.error.stack
});
}
});
// 捕获资源加载错误(捕获阶段,因为资源加载错误不会冒泡)
window.addEventListener('error', (event) => {
const target = event.target || event.srcElement;
if (target && (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
report({
type: 'resource',
url: target.src || target.href,
outerHTML: target.outerHTML
});
}
}, true); // 第三个参数 true 表示在捕获阶段处理
unhandledrejection 事件
专门用于捕获未处理的 Promise 拒绝(rejected 且未附有 rejection handler)。
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault(); // 阻止控制台默认错误输出(部分浏览器)
const reason = event.reason;
report({
type: 'promise',
message: reason?.message || reason,
stack: reason?.stack || ''
});
});
框架专属的错误处理
不同框架提供了集中捕获组件错误的方式。
React 错误边界(Error Boundary)
定义一个错误边界组件,捕获子组件树中的渲染错误,并提供降级 UI。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
report({ type: 'react', error, componentStack: errorInfo.componentStack });
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
Vue 全局错误处理器
app.config.errorHandler = (err, instance, info) => {
report({
type: 'vue',
message: err.message,
stack: err.stack,
component: instance?.$options.name,
info
});
};
跨域脚本捕获(Script error 解决)
为跨域加载的 <script> 标签添加 crossorigin="anonymous" 属性,同时服务器必须返回 Access-Control-Allow-Origin: * 头(或指定允许的源)。这样浏览器才会暴露详细错误信息。
<script src="https://cdn.example.com/app.js" crossorigin="anonymous"></script>
错误上报方案设计
上报数据的合理结构
一条高质量的错误日志应包含以下字段:
| 字段 | 说明 |
|---|---|
| errorId | 错误唯一标识(可根据 message、filename 和 stack 计算 hash,用于聚合) |
| type | 错误类型:js / resource / promise / vue / react |
| message | 错误信息 |
| filename | 出错文件(如果有) |
| line / column | 行列号 |
| stack | 错误堆栈 |
| pageUrl | 当前页面地址 |
| userAgent | 用户浏览器信息 |
| timestamp | 客户端发生时间 |
| userId | 用户标识(如已登录) |
| extra | 自定义附加信息(如当前组件状态、路由信息) |
上报时机与方式
选择上报方式:通常使用 navigator.sendBeacon() 或构建一个隐藏 Image 对象进行上报。它们能保证在页面卸载时仍然发送数据,且不会阻塞页面。
function report(data) {
const url = '/api/error/report';
// 优先使用 sendBeacon
if (navigator.sendBeacon) {
navigator.sendBeacon(url, JSON.stringify(data));
} else {
// 降级为 Image 打点
const img = new Image();
img.src = `${url}?data=${encodeURIComponent(JSON.stringify(data))}`;
}
}
批量上报与采样:为了避免高频错误导致请求过多,可以在客户端做简单的缓冲与节流。例如收集一定数量的错误后合并发送,或按时间间隔聚合发送。同时设置采样率(如只上报 10% 的用户错误),减轻服务端压力。
用户行为回溯
单纯一个错误堆栈有时难以复现问题。在错误发生时,附加上用户最近的操作路径(点击、输入、路由跳转等)可以大幅提升排查效率。常见的实现方式是在全局记录最近 N 个事件,并在上报时带上这些行为快照。
const breadcrumbs = [];
window.addEventListener('click', (e) => {
breadcrumbs.push({
type: 'click',
target: e.target.tagName,
time: Date.now()
});
if (breadcrumbs.length > 20) breadcrumbs.shift();
});
Source Map 还原
压缩后的线上代码上报的行列号并不对应源代码。服务端收到错误后,需要利用 Source Map 文件将错误位置映射回原始源码。在上报栈信息中包含文件名、行列号,服务端通过 source-map 库完成解析,展示可读的堆栈信息。
注意:Source Map 文件不应部署到公开可访问的 CDN,以免源代码泄露。通常只在监控平台内部使用。
数据安全与隐私
上报数据时需过滤敏感信息(如用户密码、身份证号),避免意外收集和泄露隐私。可在序列化时实现白名单或黑名单字段过滤。
自建监控系统 vs. 第三方服务
- 自建:灵活可控,数据私密,但需要维护采集 SDK、存储、可视化后台,成本较高。
- 第三方服务(如 Sentry、Fundebug、阿里云 ARMS 等):接入简单,功能全面,提供错误聚合、报警、发布追踪等,适合快速集成。
无论选择哪种方式,核心原理都是上述的捕获与上报机制。
实战注意事项
避免错误上报引发死循环
如果上报逻辑本身出错,可能会陷入“出错了 → 上报 → 上报失败 → 触发错误捕获 → 再次上报”的死循环。处理办法:
- 使用独立的 try/catch 包裹上报代码
- 设置最大重试次数,或限制同一错误的上报频率
区分开发与生产环境
开发模式下可将错误更详细地输出到控制台,关闭上报或使用不同的上报端点,防止开发过程中的调试数据污染生产数据集。
多页面与微前端场景
如果是多页面应用或微前端架构,错误监听应当在整个应用的最顶层注册,并注意避免重复监听。错误信息可以携带应用名称、子应用标识等信息,便于定位归属。
小结
前端监控的基石是异常捕获与上报。通过组合使用 window.onerror、error 事件、unhandledrejection 以及框架提供的错误边界,我们可以全面捕获各类前端异常。上报时采用轻量且可靠的方式,携带充足上下文(包括堆栈、页面信息、用户行为),并为服务端解析 Source Map 做好准备,才能真正建立起高效、可用的错误追踪体系。
建议从最基础的全局错误监听开始,逐步完善上报字段、增加 Promise 错误捕获、整合用户行为回溯,最终形成完整的监控闭环。