前端页面监控-异常
在系统运行过程中,前端页面可能会出现各种原因导致的异常,比如js运行错误,资源加载异常等。
开发人员可以通过浏览器开发工具(控制台)跟踪和定位相关问题,但客户和一般的运维人员是无法帮我们排查此类问题的。
所以对前端页面异常的监控就很有必要,通过将监控到的异常信息发送回服务器端,开发人员可以方便的定位和解决问题,从而增强系统的健壮性。
监控前端页面异常的主要思路是通过在window上添加异常监听器来实现。
常见的前端页面错误有以下三类:
- js运行时异常
- 资源加载异常(js/css/img)
- api调用异常(ajax)
对于api调用异常,一方面应该会有相应提示,另外一方面服务器端也会有相应的日志可查,所以在此不做介绍,本文主要介绍前两种异常的监控和相关处理。
js运行时异常
js运行时异常根据监听事件类型的不同分为以下两类。
- 一般js运行时异常
对于一般的运行时异常,可以通过window.addEventListener('error', handler)来捕获处理。
/** * taozh1982@gmail.com * www.zhangyiheng.com */ window.addEventListener('error', function (event) { console.log(event.type); //"error" console.log(event.lineno); //发生异常的行号,ex)20 console.log(event.colno); //发生异常的列号,ex)10 console.log(event.message); //错误消息,ex)Uncaught TypeError: Cannot set property 'error' of undefined console.log(event.error); //错误对象,包括消息和错误堆栈信息 console.log(event.target); //引发异常的对象 event.preventDefault(); //添加preventDefault可以阻止浏览器默认的异常处理(输出到console控制台)。 sendToServer(event.error); }, true); // true表示在捕获阶段事件 //测试代码,由于abc对象不存在引发的异常 window.abc.error = '123';
- Promise未捕获异常
当在Promise中调用reject,并且错误信息没有被处理的时候,会抛出一个unhandledrejection类型的异常事件, 但是这个错误不会被window.addEventListener('error', listener)捕获,需要添加window.addEventListener('unhandledrejection', handler)来捕获处理。
/** * taozh1982@gmail.com * www.zhangyiheng.com */ window.addEventListener('unhandledrejection', function (event) { console.log(event.type); //"unhandledrejection" console.log(event.reason); //异常原因 console.log(event.promise); //引发异常的Promise对象 event.preventDefault(); //添加preventDefault可以阻止浏览器默认的异常处理(输出到console控制台)。 sendToServer(event.reason); }, true); // true表示在捕获阶段事件 //测试代码,未捕获Promise的reject引发的异常 new Promise(function (resolve, reject) { reject('promise reject'); });
资源加载异常
资源加载异常也可以通过window.addEventListener('error', handler)来捕获处理,清注意,因为网络请求异常事件不会冒泡,添加的必须是捕获阶段的事件监听。
/**
* taozh1982@gmail.com
* www.zhangyiheng.com
*/
window.addEventListener('error', function (event) {
console.log(event.type); //"error"
console.log(event.target); //引发异常的对象,ex)img
event.preventDefault(); //添加preventDefault可以阻止浏览器默认的异常处理(输出到console控制台)。
},true); //此处必须为true(捕获阶段事件),否则监听不到
//测试代码,资源不存在引发的异常
<img src="../resource/a.png">
<script src="../resource/a.js"> </script>
<link href="../resource/a.css" rel="stylesheet" type="text/css">
一般js运行时异常和资源加载异常,都可以通过window.addEventListener('error', handler)来处理,要区分这两种异常,可以通过event的类型进行判断判断
/**
* taozh1982@gmail.com
* www.zhangyiheng.com
*/
window.addEventListener('error', function (event) {
if(event instanceof Event){
console.log("resource error");
}else if(event instanceof ErrorEvent){
console.log("js error");
}
}, true);
请注意,以上关于js运行时异常和资源加载异常的事件监听要放在页面的最前面,否则可能无法捕获到异常的发生。
异常处理
捕获到的异常,可以通过ajax发送到服务器端,记入log日志,以方便后续跟踪查询。
简单示例如下:
/**
* taozh1982@gmail.com
* www.zhangyiheng.com
*/
function sendToServer(errorData) {
var xhr = new XMLHttpRequest();
xhr.open("post", "/api/page-monitor/");
xhr.send(errorData)
}
实现代码
以下我的实现代码,将其引入到页面最前面即可(需要修改服务器地址),实现了异常和性能监控,性能监控部分请参考 前端页面监控-性能。
/**
* taozh1982@gmail.com
* www.zhangyiheng.com
*/
(function (window) {
var $monitor_server_url = "";//服务器地址
var $monitor_performance_navigation = {//导航计时阈值
duration: 10000
};
var $monitor_performance_resource = {//资源计时阈值
duration: 10000
};
var $Monitor = {
init: function () {
$Monitor.initErrMonitor();
$Monitor.initPerformanceMonitor();
},
initErrMonitor: function () {
window.addEventListener('error', function (event) {
if (event instanceof ErrorEvent) {
$Monitor.handleJSErr(event);
} else if (event instanceof Event) {
$Monitor.handleResourceErr(event);
}
}, true);
window.addEventListener('unhandledrejection', $Monitor.handleUnhandledRejection, true);
},
handleJSErr: function (event) {
// console.log("js error");
$Monitor.sendErrToServer({
error_type: "javascript",
error: {
lineno: event.lineno,
colno: event.colno,
message: event.message,
stack: event.error.stack
}
}, event);
},
handleResourceErr: function (event) {
// console.log("resource error");
$Monitor.sendErrToServer({
error_type: "resource",
error: {
target: event.target.outerHTML
}
}, event);
},
handleUnhandledRejection: function (event) {
// console.log("unhandledrejection");
$Monitor.sendErrToServer({
error_type: "unhandledrejection",
error: {
reason: event.reason,
promise: event.promise
}
}, event);
},
sendErrToServer: function (errData, event) {
errData.monitor_type = "error";
$Monitor._sendToServer(errData);
},
initPerformanceMonitor: function () {
window.addEventListener('load', function (event) {
setTimeout($Monitor.handlePerformance, 1000);
});
},
handlePerformance: function () {
var navigationTiming = $Monitor.getNavigationTiming();
var timing = {resource: []};
if ($Monitor.checkPerformance(navigationTiming, $monitor_performance_navigation) === false) {
timing.navigation = navigationTiming;
}
var resourceTiming = $Monitor.getResourceTiming();
resourceTiming.forEach(function (resourceTimingItem) {
if ($Monitor.checkPerformance(resourceTimingItem, $monitor_performance_resource) === false) {
timing.resource.push(resourceTimingItem);
}
});
if (timing.navigation || timing.resource.length > 0) {
$Monitor.sendPerformanceToServer({
timing: timing
});
}
},
sendPerformanceToServer: function (performanceData) {
performanceData.monitor_type = "performance";
$Monitor._sendToServer(performanceData);
},
checkPerformance: function (timing, monitor_performance) {
if (!timing || !monitor_performance) {
return true;
}
var keys = Object.keys(timing);
var len = keys.length;
for (var i = 0; i < len; i++) {
var key = keys[i];
if (monitor_performance.hasOwnProperty(key)) {
if (timing[key] > monitor_performance[key]) {
return false;
}
}
}
return true
},
getNavigationTiming: function () {
var performance = window.performance;
if (!performance) {
return null;
}
var navigationTiming = performance.getEntriesByType('navigation')[0];
if (!navigationTiming) {
navigationTiming = performance;
var navigation = navigationTiming.navigation;
var type = navigation.type;
switch (type) {
case 0:
type = "navigate";
break;
case 1:
type = "reload";
break;
case 2:
type = "back_forward ";
break;
}
navigationTiming = navigationTiming.timing.toJSON();
navigationTiming.startTime = navigationTiming.startTime || navigationTiming.navigationStart;
navigationTiming.name = window.location.href;
navigationTiming.type = type;
navigationTiming.initiatorType = "navigation";
navigationTiming.redirectCount = navigation.redirectCount;
navigationTiming.duration = navigationTiming.loadEventEnd - navigationTiming.startTime;
} else {
navigationTiming = navigationTiming.toJSON();
}
return Object.assign($Monitor._getPerformanceTiming(navigationTiming), {
/*
导航类型
navigate/reload/back_forward/prerender
*/
type: navigationTiming.type,
/*
执行 onload 回调函数的时间
-- 减少onload回调函数操作,
*/
loadEvent: navigationTiming.loadEventEnd - navigationTiming.loadEventStart,
/*
解析dom树时间
-- 减少dom树嵌套
*/
domReady: navigationTiming.domComplete - navigationTiming.domInteractive
});
},
getResourceTiming: function () {
var timingArr = [];
var performance = window.performance;
if (!performance) {
return timingArr;
}
performance.getEntriesByType('resource').forEach(function (timing) {
timingArr.push($Monitor._getPerformanceTiming(timing.toJSON()))
});
return timingArr;
},
_getPerformanceTiming: function (timingObj) {
return {
/*
地址
*/
name: timingObj.name,
/*
导航/资源的类型,navigation/link/script/css/img/iframe
*/
initiatorType: timingObj.initiatorType,
/*
重定向计数
*/
redirectCount: timingObj.redirectCount | 0,
/*
重定向时间
-- 直接使用重定向之后的地址,可以减少重定向耗时。
*/
redirect: timingObj.redirectEnd - timingObj.redirectStart,
/*
DNS查询时间
-- 减少DNS的请求次数
-- DNS预获取
*/
domainLookup: timingObj.domainLookupEnd - timingObj.domainLookupStart,
/*
TCP连接时间
-- 减少HTTP的请求数量,
1#资源合并
2#长链接
*/
connect: timingObj.connectEnd - timingObj.connectStart,
/*
SSL/TLS协商时间
*/
secureConnection: timingObj.connectEnd - timingObj.secureConnectionStart,
/*
第一字节的到达时间,浏览器请求页面与从服务器接收到信息的第一字节之间的时间
*/
ttfb: timingObj.responseStart - timingObj.requestStart,
/*
内容下载时间
-- 内容压缩,gzip
*/
contentDownload: timingObj.responseEnd - timingObj.responseStart,
/*
整个导航完成/资源可用时间
*/
duration: timingObj.duration
}
},
_sendToServer: function (monitorData) {
if (!$monitor_server_url) {
return;
}
window.setTimeout(function () {
monitorData.url = window.location.href;
var httpRequest = new XMLHttpRequest();
httpRequest.open("POST", $monitor_server_url);
httpRequest.setRequestHeader("Content-Type", "application/json; charset=utf-8");
httpRequest.send(JSON.stringify(monitorData));
}, 100);
}
};
$Monitor.init();
})(window);
//test code
//js error test
/* window.abc.error = '123'; */
//unhandledrejection test
/*
new Promise(function (resolve, reject) {
reject('promise reject');
});
*/
//resource error
/* <img src="../resource/notfound.png"> *