前端监控异常数据采集实战:接口与JS错误全局捕获方案
本文详细讲解前端监控系统中异常数据的采集方法。重点介绍如何通过axios拦截器统一捕获接口异常,以及如何利用window.addEventListener和unhandledrejection事件全局捕获JS同步/异步错误及资源加载异常。最后结合Vue和React框架,演示如何获取用户、路由及环境等关键上下文数据,为精准定位问题提供完整方案。
前言
在前两篇文章中,我们探讨了前端建立监控系统的必要性,并梳理了搭建前端监控的整体流程。相信大家对前端监控的Why(为什么做)和What(做什么)已经有了清晰的认识。接下来,我们将重点解决How(怎么做)的问题,深入探讨具体的实现细节。
如果你对前端监控的基础概念尚不熟悉,建议先阅读之前的两篇文章:
- 为什么前端不能没有监控系统?
- 前端监控的总体搭建步骤
本文将聚焦于前端数据的采集环节,并首先从异常数据的收集入手进行详细介绍。
什么是异常数据?
所谓异常数据,是指在前端用户操作页面的过程中,因执行逻辑错误或资源加载失败而触发的错误。此时,浏览器通常会抛出相应的报错信息。
例如,当代码中使用了未声明的变量时,控制台会打印红色的错误信息以提示原因;又或是接口请求失败时,可以在网络面板中查看到异常,这包括请求发送阶段的异常或接口响应阶段的异常。
在实际开发场景中,前端捕获的异常主要分为两大类:接口异常和前端异常。下面我们将分别阐述这两类异常的捕获方法。
接口异常
接口异常必定在发起网络请求时触发。目前前端大部分的网络请求是通过 axios 发起的,因此,我们只需要获取 axios 可能抛出的异常即可。
如果你使用的是 Promise 的写法,则可以使用 .catch 来捕获异常:
axios
.post('/test')
.then((res) => {
console.log(res);
})
.catch((err) => {
// err 就是捕获到的错误对象
handleError(err);
});
如果你使用的是 async/await 的写法,则可以使用 try..catch.. 来捕获异常:
async () => {
try {
let res = await axios.post('/test');
console.log(res);
} catch (err) {
// err 就是捕获到的错误对象
handleError(err);
}
};
在成功捕获到异常后,统一交由 handleError 函数进行处理。该函数负责接收并处理异常数据,随后调用 上报接口 将处理后的异常数据上报至服务器,从而完成采集过程。
上述异常捕获逻辑在理论层面是可行的,但在实际操作中会面临第一个挑战:页面数量众多,难道要在每个请求处都手动包裹一层 catch 吗?
确实,如果是从零开始开发一个新项目,在初期规范每个请求都必须包裹 catch 也是可行的。然而,如果是在一个已经具有一定规模的老项目中接入前端监控,要求在每个页面或每个请求处手动添加 catch 显然是不现实的。
因此,为了最大程度地降低接入成本并减少代码侵入性,我们采用第二种方案:在 axios 拦截器中统一捕获异常。
在前端项目中,为了统一处理请求(例如 401 状态码的跳转或全局错误提示),通常会创建一个全局的 axios 实例,并为该实例添加拦截器。其他页面直接导入并使用这个实例,例如:
// 全局请求:src/request/axios.js
const instance = axios.create({
baseURL: 'https://api.test.com'
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
})
export default instance
然后在具体的业务页面中这样发起请求:
// a 页面:src/page/a.jsx
import http from '@/src/request/axios.js';
async () => {
let res = await http.post('/test');
console.log(res);
};
通过这种方式,我们发现每个页面的请求都会经过全局的 axios 实例。因此,我们只需要在全局请求的位置捕获异常即可,无需在每个页面单独处理,从而大幅降低了接入成本。
按照这个方案,接下来我们将在 src/request/axios.js 这个文件中实施具体代码。
拦截器中捕获异常
首先,我们为 axios 添加响应拦截器:
// 响应拦截器
instance.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
// 发生异常会走到这里
if (error.response) {
let response = error.response;
if (response.status >= 400) {
handleError(response);
}
} else {
handleError(null);
}
return Promise.reject(error);
},
);
响应拦截器的第二个参数是一个在发生错误时执行的函数,其参数即为异常对象。我们需要首先判断是否存在 error.response。如果存在,说明接口有响应(即接口连通),但返回了错误;如果不存在,则说明接口未连通,请求一直挂起,这通常意味着接口崩溃或网络超时。
如果存在响应,首先获取状态码,并根据状态码判断何时需要收集异常。这里的判断方式较为直接:只要状态码大于 400,就视为异常。此时拿到响应数据,并执行上报逻辑。
如果不存在响应,可以将其视为接口超时异常。在调用异常处理函数时,传入一个 null 即可。
前端异常
上文介绍了如何在 axios 拦截器中捕获接口异常,接下来我们将介绍如何捕获前端异常。
在前端代码中捕获异常,最常用的方式是使用 try..catch..。任何同步代码块都可以放入 try 块中,只要发生异常就会执行 catch:
try {
// 任意同步代码
} catch (err) {
console.log(err);
}
上文提到“任意同步代码”而非“任意代码”,主要是因为普通的 Promise 写法中,try..catch.. 无法捕获异步异常,必须使用 .catch() 来捕获,例如:
try {
Promise.reject(new Error('出错了')).catch((err) => console.log('1:', err));
} catch (err) {
console.log('2:', err);
}
将这段代码放入浏览器运行,打印结果是:
1: Error: 出错了
很明显,只有 .catch 捕获到了异常。不过,与接口异常的逻辑类似,这种方式处理当前页面的异常没有问题。但从整个应用的角度来看,这种捕获方式侵入性强且接入成本高,因此我们的思路依然是全局捕获。
全局捕获 JS 异常也比较简单,使用 window.addEventLinstener('error') 即可:
// js 错误捕获
window.addEventListener('error', (error) => {
// error 就是js的异常
});
为啥不用 window.onerror ?
这里很多小伙伴会有疑问,为什么不用 window.onerror 进行全局监听呢?window.addEventLinstener('error') 和 window.onerror 有什么区别呢?
首先,这两个函数的功能基本一致,都可以全局捕获 JS 异常。但是,有一类异常叫做资源加载异常,即在代码中引用了不存在的图片、JS、CSS 等静态资源导致的异常,例如:
const loadCss = ()=> {
let link = document.createElement('link')
link.type = 'text/css'
link.rel = 'stylesheet'
link.href = 'https://baidu.com/15.css'
document.getElementsByTagName('head')[10].append(link)
}
render() {
return <div>
<img src='./bbb.png'/>
<button onClick={loadCss}>加载样式<button/>
</div>
}
上述代码中的 baidu.com/15.css 和 bbb.png 是不存在的,JS 执行到这里肯定会报一个资源找不到的错误。但是默认情况下,上面两种 window 对象上的全局监听函数都监听不到这类异常。
因为资源加载的异常只会在当前元素触发,异常不会冒泡到 window,因此监听 window 上的异常是捕捉不到的。那怎么办呢?
如果你熟悉 DOM 事件机制就会明白,既然在冒泡阶段监听不到,那么在捕获阶段一定能监听到。
方法就是给 window.addEventListene 函数指定第三个参数,很简单就是 true,表示该监听函数会在捕获阶段执行,这样就能监听到资源加载异常了。
// 捕获阶段全局监听
window.addEventListene(
'error',
(error) => {
if (error.target != window) {
console.log(error.target.tagName, error.target.src);
}
handleError(error);
},
true,
);
上述方式可以很轻松地监听到图片加载异常,这就是为什么更推荐 window.addEventListene 的原因。不过要记得,第三个参数设为 true,监听事件捕获,就可以全局捕获到 JS 异常和资源加载异常。
需要特别注意,window.addEventListene 同样不能捕获 Promise 异常。不管是 Promise.then() 写法还是 async/await 写法,发生异常时都不能捕获。
因此,我们还需要全局监听一个 unhandledrejection 函数来捕获未处理的 Promise 异常。
// promise 错误捕获
window.addEventListener('unhandledrejection', (error) => {
// 打印异常原因
console.log(error.reason);
handleError(error);
// 阻止控制台打印
error.preventDefault();
});
unhandledrejection 事件会在 Promise 发生异常并且没有指定 catch 的时候触发,相当于一个全局的 Promise 异常兜底方案。这个函数会捕捉到运行时意外发生的 Promise 异常,这对我们排错非常有用。
默认情况下,Promise 发生异常且未被 catch 时,会在控制台打印异常。如果我们想阻止异常打印,可以使用上面的 error.preventDefault() 方法。
异常处理函数
前面我们在捕获到异常时调用了一个异常处理函数 handleError,所有的异常和上报逻辑统一在这个函数内处理,接下来我们实现这个函数。
const handleError = (error: any, type: 1 | 2) {
if(type == 1) {
// 处理接口异常
}
if(type == 2) {
// 处理前端异常
}
}
为了区分异常类型,函数新加了第二个参数 type 表示当前异常属于前端还是接口。在不同的场景中使用如下:
- 处理前端异常:
handleError(error, 1) - 处理接口异常:
handleError(error, 2)
处理接口异常
处理接口异常时,我们需要将拿到的 error 参数进行解析,然后取到需要的数据。接口异常一般需要的数据字段如下:
code:http 状态码url:接口请求地址method:接口请求方法params:接口请求参数error:接口报错信息
这些字段都可以在 error 参数中获取,方法如下:
const handleError = (error: any, type: 1 | 2) {
if(type == 1) {
// 此时的 error 响应,它的 config 字段中包含请求信息
let { url, method, params, data } = error.config
let err_data = {
url, method,
params: { query: params, body: data },
error: error.data?.message || JSON.stringify(error.data),
})
}
}
config 对象中的 params 表示 GET 请求的 query 参数,data 表示 POST 请求的 body 参数,所以我在处理参数的时候,将这两个参数合并为一个,用一个属性 params 来表示。
params: { query: params, body: data }
还有一个 error 属性表示错误信息,这个获取方式要根据你的接口返回格式来拿。要避免获取到接口可能返回的超长错误信息,多半是接口没处理,这样可能会导致写入数据失败,要提前与后台规定好。
处理前端异常
前端异常大多数就是 JS 异常,异常对应到 JS 的 Error 对象,在处理之前,我们先看 Error 有哪几种类型:
ReferenceError:引用错误RangeError:超出有效范围TypeError:类型错误URIError:URI 解析错误
这几类异常的引用对象都是 Error,因此可以这样获取:
const handleError = (error: any, type: 1 | 2) {
if(type == 2) {
let err_data = null
// 监测 error 是否是标准类型
if(error instanceof Error) {
let { name, message } = error
err_data = {
type: name,
error: message
}
} else {
err_data = {
type: 'other',
error: JSON.strigify(error)
}
}
}
}
上述判断中,首先判断异常是否是 Error 的实例。事实上绝大部分的代码异常都是标准的 JS Error,但我们这里还是判断一下,如果是的话直接获取异常类型和异常信息,不是的话将异常类型设置为 other 即可。
我们随便写一个异常代码,看一下捕获的结果:
function test() {
console.aaa('ccc');
}
test();
然后捕获到的异常是这样的:
const handleError = (error: any) => {
if (error instanceof Error) {
let { name, message } = error;
console.log(name, message);
// 打印结果:TypeError console.aaa is not a function
}
};
获取环境数据
获取环境数据的意思是,不管是接口异常还是前端异常,除了异常本身的数据之外,我们还需要一些其他信息来帮助我们更快更准地定位到哪里出错了。
这类数据我们称之为 “环境数据”,就是触发异常时所在的环境。比如是谁在哪个页面的哪个地方触发的错误,有了这些,我们就能马上找到错误来源,再根据异常信息解决错误。
环境数据至少包括下面这些:
app:应用的名称/标识env:应用环境,一般是开发,测试,生产version:应用的版本号user_id:触发异常的用户 IDuser_name:触发异常的用户名page_route:异常的页面路由page_title:异常的页面名称
app 和 version 都是应用配置,可以判断异常出现在哪个应用的哪个版本。这两个字段我建议直接获取 package.json 下的 name 和 version 属性,在应用升级的时候,及时修改 version 版本号即可。
其余的字段,需要根据框架的配置获取,下面我分别介绍在 Vue 和 React 中如何获取。
在 Vue 中
在 Vue 中获取用户信息一般都是直接从 Vuex 里面拿,如果你的用户信息没有存到 Vuex 里,从 localStorage 里获取也是一样的。
如果在 Vuex 里,可以这样实现:
import store from '@/store'; // vuex 导出目录
let user_info = store.state;
let user_id = user_info.id;
let user_name = user_info.name;
用户信息存在状态管理中,页面路由信息一般是在 vue-router 中定义。前端的路由地址可以直接从 vue-router 中获取,页面名称可以配置在 meta 中,如:
{
path: '/test',
name: 'test',
meta: {
title: '测试页面'
},
component: () => import('@/views/test/Index.vue')
},
这样配置之后,获取当前页面路由和页面名称就简单了:
window.vm = new Vue({...})
let route = vm.$route
let page_route = route.path
let page_title = route.meta.title
最后一步,我们再获取当前环境。当前环境用一个环境变量 VUE_APP_ENV 表示,有三个值:
dev:开发环境test:测试环境pro:生产环境
然后在根目录下新建三个环境文件,写入环境变量:
.env.development:VUE_APP_ENV=dev.env.staging:VUE_APP_ENV=test.env.production:VUE_APP_ENV=pro
现在获取 env 环境时就可以这么获取:
{
env: process.env.VUE_APP_ENV;
}
最后一步,执行打包时,传入模式以匹配对应的环境文件:
# 测试环境打包
$ num run build --mode staging
# 生产环境打包
$ num run build --mode production
获取到环境数据,再拼上异常数据,我们就准备好了数据等待上报了。
在 React 中
和 Vue 一样,用户信息可以直接从状态管理里拿。因为 React 中没有全局获取当前旅游的快捷方式,所以页面信息我也会放在状态管理里面。我用的状态管理是 Mobx,获取方式如下:
import { TestStore } from '@/stores'; // mobx 导出目录
let { user_info, cur_path, cur_page_title } = TestStore;
// 用户信息:user_info
// 页面信息:cur_path,cur_page_title
这样的话,就需要在每次切换页面时,更新 mobx 里的路由信息,怎么做呢?
其实在根路由页(一般是首页)的 useEffect 中监听即可:
import { useLocation } from 'react-router';
import { observer, useLocalObservable } from 'mobx-react';
import { TestStore } from '@/stores';
export default observer(() => {
const { pathname, search } = useLocation();
const test_inst = useLocalObservable(() => TestStore);
useEffect(() => {
test_inst.setCurPath(pathname, search);
}, [pathname]);
});
获取到用户信息和页面信息,接下来就是当前环境了。和 Vue 一样通过 --mode 来指定模式,并加载相应的环境变量,只不过设置方法略有不同。大多数的 React 项目可能都是用 create-react-app 创建的,我们以此为例介绍怎么修改。
首先,打开 scripts/start.js 文件,这是执行 npm run start 时执行的文件,我们在开头部分第 6 行加代码:
process.env.REACT_APP_ENV = 'dev';
没错,我们指定的环境变量就是 REACT_APP_ENV,因为只有 REACT_ 开头的环境变量可被读取。
然后再修改 scripts/build.js 文件的第 48 行,修改后如下:
if (argv.length >= 2 && argv[0] == '--mode') {
switch (argv[1]) {
case 'staging':
process.env.REACT_APP_ENV = 'test';
break;
case 'production':
process.env.REACT_APP_ENV = 'pro';
break;
default:
}
}
此时获取 env 环境时就可以这么获取:
{
env: process.env.REACT_APP_ENV;
}
总结
经过前面一系列操作,我们已经比较全面地获取到了异常数据,以及发生异常时的环境数据。接下来就是调用上报接口,将这些数据传给后台存起来,我们以后查找和追踪就很方便了。
如果你也需要前端监控,不妨花上半个小时,按照文中介绍的方法收集一下异常数据,相信对你很有帮助。
以上就是JavaScript架构搭建前端监控如何采集异常数据的详细内容,更多关于JavaScript前端监控采集异常数据的资料请关注本站其它相关文章!
Java和JavaScript名字像但没关系?详解两者及HTML的技术分工
2026-11-03 20:24:10
Node.js 中 CommonJS 与 ES Modules 模块规范的区别及使用
2026-11-03 15:35:13
先学JS还是先学Java?全栈开发视角的学习路径规划
2026-11-03 13:35:26
解决Node.js安装后提示“node不是内部或外部命令”
2026-11-03 08:39:43
生产环境升级 Node.js 的风险评估与 LTS 版本选择
2026-11-02 10:30:14
