上一篇文章简单的认识了前端常见的一些异常,及其各自出现场景,这是前端监控的第二篇文章,主要讲述大致大致需要使用那些技术,下一篇讲完成一个实际的
sdk
为什么要做前端监控
快速定位线上问题,优化线上产品体验,捕获一些由于特殊情况导致的无法重现的客户问题
我们要实现的功能
- 1)后端接口异常监控
比如:某个接口报错500
或者503
- 2)前端页面显示错误(资源文件异常监控)
页面图片或者某个js
加载失败或者找不到资源404
比如:某个图片不支持跨域503
- 3)前端代码错误监控
某个函数内部代码逻辑错误导致的报错 - 4)支持浏览器关闭依然可以收集到错误信息
- 5)支持
source-map
定位源码错误位置 - 6)支持自定义接口错误信息描述
自己画的简单原型
https://modao.cc/app/791e27d2ee703f6eb878b31236f151e006189032?simulator_type=device&sticky
以上这些需求足以满足对前端监控的基本需求,当人如果要做的复杂的话,还需要最uv
这类的指标,我们这里就不做了,其实这些也是很简单,只是添加一些数据维度的指标而已
1.) 处理业务代码错误部分
开始之前先必须知道它的区别
/*
* @param msg{String}: 错误消息
* @param url{String}: 发生错误页面的url
* @param line{Number}: 发生错误的代码行
*/
window.onerror = function(msg, url, line){
return true;
}
特性:
- 能捕获到js执行错误,不能捕获带有src的标签元素的加载错误。
- 参数对应5个值(错误信息,所在文件,行,列,错误信息)
- 函数体内用return true可以不让异常信息输出到控制台
/*
* @param event{} 错误对象
*/
window.addEventListener("error", function(event){
event.preventDefault();
},true);
特性:
- 为捕获状态时(第三个参数为
true
)能捕获到js
执行错误,也能捕获带有src
的标签元素的加载错误。 - 为冒泡状态时(第三个参数为
false
)能捕获到js
执行错误,不能捕获带有src
的标签元素的加载错误。 - 参数对应1个值,异常事件,错误信息都在里面
- 函数体内用
preventDefault
可以不让异常信息输出到控制台
捕获到的异常
//网络资源类型异常
//业务代码类型异常
所以:
因为window.addEventListener("error")
不仅仅能够监听代码层面的异常,而已还能监控静态资源的异常,为了满足前面的需求,所以我们肯定需要选用window.addEventListener("error")
来做处理
window.addEventListener
捕获到的异常是这样的,
如何处理前端接口层面的监控
既然我们是做sdk那当然就需要考虑sdk技术兼容性问题,但是前端无论使用vue,react
还是angular
最终统一的数据请求部分都是ajax
,无论用的什么第三方库,最底层的还是ajax
,所以需要监控接口层面的异常,我们只需要拦截ajax
请求即可
一个拦截ajax请求的库ajax-hook
Ajax-hook是一个精巧的用于拦截浏览器XMLHttpRequest的库,它可以在XMLHttpRequest对象发起请求之前和收到响应内容之后获得处理权。通过它你可以在底层对请求和响应进行一些预处理。
安装
yarn add ajax-hook
大致的使用情况是这样的:
import {proxy} from "ajax-hook"
function onAddError() {
// ...
// 存储异常数据
// 记得一定要先存在本地缓存,然后根据自己的上报规则按规则上报
//先只写伪代码,下篇文章具体实现
saveError();
}
function saveError(){
}
proxy({
//XMLHttpRequest请求发起前进入
onRequest: (config, handler) => {
handler.next(config);
},
//XMLHttpRequest请求发生错误时进入,比如超时;注意,不包括http状态码错误,
//如404仍然会认为请求成功
onError: (err, handler) => {
onAddError(err); //存储异常
handler.next(err)
},
//XMLHttpRequest请求成功后进入,response中包含了请求参数和响应结果
onResponse: (response, handler) => {
onAddError(err); //存储异常
handler.next(response)
}
})
捕获到的异常是这样的
现在,我们便拦截了浏览器中通过XMLHttpRequest
发起的所有网络请求!在请求发起前,会先进入onRequest
钩子,调用handler.next(config)
请求继续,如果请求成功,则会进入onResponse
钩子,如果请求发生错误,则会进入onError
至此异常捕获部分基本完成了,如果你需要捕获WebSocket
的异常,就需要在WebSocket
的onerror
抛出异常了或者重写webSocket
类了,其他类似的WebWorker
,postMessage
的异常也是需要做异常监听的,或者当他们发生异常的时候,在实际的异常函数中抛出一个自定义异常事件,然后自己sdk
去捕获这个事件
还有个就是,前端现在基本都是使用了Promise
,所以必要对Promise
的异常做独立监听
window.addEventListener("unhandledrejection", event => {
console.log(`error: ${event}`);
});
2.) 异常数据传递部分
至此异常监控部分的功能就完成了,现在要做的是把捕获到的异常上报到数据库,那怎么保证浏览器关闭后,存储起来的异常数据也能够上报成功呢?
navigator.sendBeacon
navigator.sendBeacon()
方法可用于通过HTTP
将少量数据异步传输到Web
服务器。
- 数据可靠,浏览器关闭请求也照样能发
- 异步执行,不会影响下一页面的加载
- 同时不会延迟页面的卸载或影响下一导航的载入性能
API
使用简单
浏览器兼容性很乐观
使用
// 浏览器卸载事件
window.addEventListener('unload',logData,false);
function logData(){
//上报错误信息
navigator.sendBeacon("http://npmhook/api/sendLog",{
//参数
});
}
如果浏览器不支持使用navigator.sendBeacon
,就使用创建Image
标签的形式发送数据
const img = new Image();
img.onload =()=>{}
img.src = `http://npmhook/api/sendLog?data='数据'`;
3.) 需要收集的数据部分
{
userId:"账号标识ID",
url:"当前页面URL",
browser:"所属浏览器",
version:"浏览器版本",
system:"所属系统",
referer:"入口页面",
jsPath:"异常js文件路径",
errorObj:"异常错误信息json字符串",
connection:"连接网络情况",
...//额外传入的参数
}
4.) sdk编写(简洁版)
import {proxy} from "ajax-hook";
import user from "../../services/interface/user";
/***异常类 */
function HookErrorSdk(config){
this.api="http://127.0.0.1:4002";
this.formObj=config.formObj||{};
this.userId=config.userId;
this.appkey=config.appkey;
this.httpCodeMap=config.httpCodeMap||{},
this.AppErrorList=[];
this.connection=navigator.connection
|| navigator.mozConnection
|| navigator.webkitConnection;
/*发送上报数据
* @param param object 错误对象
*/
this.ToString=function(param){
return JSON.stringify(param);
}
/*组装ajax异常
* @param event object 错误对象
*/
this.getAjaxError=function(event){
// ... 处理自定义错误httpCodeMap
return {
jsPath:event.filename,
errorObj:JSON.stringify({
message:event.error.message,
stack:event.error.stack
})
}
}
/*组装业务代码异常
* @param event object 错误对象
* @param type string 错误类型
*/
this.getErrorObj=function(event,type){
const errObj=type==="ajax"?
this.getAjaxError(event)
:this.getCodeError(event);
return Object.assign({
userId:this.userId,
url:document.location.href,
browser:navigator.userAgent,
version:navigator.appVersion,
system:navigator.platform,
referer:document.referrer,
connection:this.connection.type
},errObj)
}
/*收集异常
* @param event object 错误对象
*/
this.getCodeError=function(event){
return {
jsPath:event.filename,
errorObj:this.ToString({
message:event.error.message,
stack:event.error.stack
})
}
}
/*注册监听
* @param sdkLisConfig object 监听配置
*/
this.addListeners=function(sdkLisConfig){
//根据返回的监听配置注册对应的监听事件,我这里全部写上了
window.addEventListener("error", function(event){
this.addError(event,"js");
event.preventDefault();
},true);
window.addEventListener('unload',()=>{
//卸载的时候需要把数据上报上去,具体看自己怎么传数据
this.ajax();
});
window.addEventListener("unhandledrejection", event => {
this.addError(err,"js");
});
}
/*注册ajax 代理拦截
*/
this.initAjaxProxy=function(){
proxy({
//请求发起前进入
onRequest: (config, handler) => {
handler.next(config);
},
//请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功
onError: (err, handler) => {
this.addError(err,"ajax");
handler.next(err)
},
//请求成功后进入
onResponse: (response, handler) => {
this.addError(response,"ajax");
handler.next(response)
}
})
}
/*注册sdk
* @param config object 错误对象
* @param success function 错误对象
* @param fail function 错误对象
*/
this.initSdk=function(config,success,fail){
let xmlHttp = window.XMLHttpRequest ?
new XMLHttpRequest()
: new ActiveXObject('Microsoft.XMLHTTP');
xmlHttp.open("post", this.api+"/api/login", true);
xmlHttp.setRequestHeader('Content-Type','application/json;charset=UTF-8');
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
let sdkLisConfig = JSON.parse(xmlHttp.responseText);
success(sdkLisConfig);
} else {
fail(xmlHttp.responseText);
}
};
xmlHttp.send(this.ToString({appkey:config.appkey}));
}
/*注册sdk,new 对象的时候就调用了
* @param config object 错误对象
* @param () function 错误对象
* @param () function 错误对象
*/
this.initSdk(config,(sdkLisConfig)=>{
//注册成功
this.addListeners();
this.AppErrorList=localStorage.getItem("AppErrorList")?
JSON.parse(localStorage.getItem("AppErrorList")):[];
let timeObj=setInterval(() => {
if(this.AppErrorList.length>0){
this.postError(this.AppErrorList[0]);
}
}, config.timeNum||5000);
window.addEventListener('unload',()=>{
clearTimeout(timeObj);
},false);
},(error)=>{
//出错失败
return new Error(`注册异常${error}`)
})
}
/*发送上报数据
* @param param object 错误对象
* @param () function 错误对象
* @param () function 错误对象
*/
HookErrorSdk.prototype.ajax=function(param,success, fail){
if(navigator.sendBeacon&&typeof(navigator.sendBeacon)==="function"){
navigator.sendBeacon(this.api+"/api/sendLog",this.ToString(param)).then((res)=>{
success(res)
}).catch((res)=>{
fail(res)
})
}else{
let img = new Image();
img.onload =function (res){
success(res);
}
img.onerror=function(res){
fail(res);
}
img.src = this.api+`?data=`+this.ToString(param);
}
}
/*代码异常
* @param param object 错误对象
* @param type string 错误类型
*/
HookErrorSdk.prototype.addError=function(event,type){
let error=this.getAxaxError(event,type);
this.AppErrorList.push(Object.assign(error,this.formObj));
localStorage.setItem("AppErrorList",this.ToString(this.AppErrorList));
}
/*最终的上报错误
* @param obj object 错误信息对象
*/
HookErrorSdk.prototype.postError=function(obj){
this.ajax(obj,()=>{
this.AppErrorList.shift();
localStorage.setItem("AppErrorList",this.ToString(this.AppErrorList));
},()=>{
return new Error("错误收集服务异常")
})
}
HookErrorSdk.js
内部应该包含这些功能
1.根据appKey
注册SDK
,获取使用权限,获取需要上报的错误规则
2.使用window.addEventListener('error')
开启全局资源文件和业务代码异常监听(WebSocket
,WebWorker
,postMessage
)根据自己需要记得也要考虑
3.使用window.addEventListener('unhandledrejection')
开启Promise
异常监控
4.使用ajax-hook
拦截ajax捕获接口异常监控
5.使用localStorage
存储异常数据避免数据丢失
6.使用setInterval
定时检测异常
7.使用navigator.sendBeacon
或者创建image
形式上报异常数据
//自己最终写成的sdk,`Hook`是我自己的前缀
import HookErrorSdk from 'HookErrorSdk';
const hookSdk=new HookErrorSdk({
userId:"88888888",
//appkey
appKey: 'hook-aa-g8g4yc2d',
//自定义http本项目错误code 未配置则取接口返回的错误信息
httpCodeMap:{
801:"token失效"
},
//额外传入的参数
formObj:{}
})
5.) 后台解析异常部分
至此前端异常捕获部分的技术点都好了,那我们收集到异常后,如何定位异常源码位置呢?资源异常我们直接能够根据资源的名称就可以明显看出异常点,所以这个不需要考虑定位源码位置,而接口异常也是如此,根据接口url
和参数就可以定位了
最为主要的是要还原业务代码的异常,前面我们通过window.addEventListener
捕获了业务代码的异常截图如下
其中图中异常部分有个,stack
对象,利用stack
我们可以根据stacktracey
这个库来获取具体代码异常位置,然后定位源码位置
source-map.js
用于解析sourcemap
文件
stacktracey.js
用于解析错误信息的stack
最后
欢迎关注我
至此整个流程已经确定,大致的编码结构已经完成,下一篇将是如何具体优化成一个可以使用的sdk,最后关注我,下篇文章见!