为什么要做前端代码异常采集?好问题!
为了用户能安心用产品,不至于时不时“卡壳”崩溃。
为了能高效定位线上代码的异常并提供简单提示信息。
为了程序猿同胞们能睡个好觉。
本文完整示例请移步github:FEerrorLog
捕获异常的方法
js捕获异常的方法,两三个而已。
- try...catch 优缺点已有很多论述和解决方案,本文的异常采集并未建立在该方法之上,只是少量使用。
- window.onerror和方法3类似但不如方法3强大,因此未选用此方法。
- window.addEventListener('error',function(){},true),采用此方法。
前端异常包含两部分:
第一部分:window.onerror()能捕获到的异常,当然如果用addEventListener无论冒泡还是捕获阶段也能捕获到该异常。
第二部分:资源加载失败,即<img>
、<script>
标签上的onerror,这个异常无法通过冒泡到达window,但是可以在捕获阶段拿到,这就是为什么要将addEventListener
第三个参数置成true
了。
注意:为保证该异常采集脚本能执行到,不被先行执行的脚本里面的报错阻断,该脚本要放到最前面。
可能的异常及采集方案
- 资源加载失败,样式、图片、脚本文件的请求异常,比如js加载404了
- js脚本异常,即控制台常见的Error信息
- 检测HTML劫持,比如被运营商强行注入标签或脚本
- 页面样式丢失,CSS 展现异常
1. 资源加载失败
http异常用js几乎抓不到有用异常信息,但是404异常可以进行简单处理,此时是不会执行onerror的回调函数的。因为在addEventListener捕获到的异常信息中你可以发现,对应于onerror的五个回调参数根本不存在了,但是addEventListener中除这五个外,还有其他可以用的key,如果想获取加载失败的资源是哪个,可以去target中找些有用信息,我使用的是e.target.outerHTML
"HttpError at " + (e.target.baseURI || location.href) + " outerHTML:" + e.target.outerHTML
2. js异常
一般需要采集的信息:
- 异常的提示信息,会直接告诉你是什么异常。这是识别一个异常的最重要依据,即e.message中的信息。
- JS 文件名:异常发生在那个文件中。是堆栈信息中最顶层的那个文件。即e.filename。
- 异常所在行、列:异常的具体位置。行信息各浏览器基本还是一致的,列信息的差别较大,仅供参考。
- 堆栈信息:异常信息发生的堆栈,也是函数调用的堆栈信息,每下一层都是上一层的运行环境。即e.error.stack。每一层都包含类型、文件、行、列信息。但是注意堆栈信息可能会比较多,可以根据需要截取上报。safari和firfox的e.error.stack中不包含以上1,2,3的信息,只有堆栈信息,而chrome和IE中都包含,此处需要做兼容处理。
- 发生异常的设备信息,可以从window.navigator中选取自己需要的信息,或者直接使用window.navigator.userAgent
- 发生异常的时间点,不多说。
js主动抛出异常
js异常除了可以是系统抛出的几类异常,还可以是开发者利用throw关键字主动抛出异常。值:
可以是字符串、数字逻辑值或对象使用方式:
//利用Error对象或其实例,采集到的异常系统自动为其添加了堆栈信息,和系统抛出异常基本类似
throw new Error('Problem description.')
throw Error('Problem description.')
//直接利用关键字抛出内容,会完全复写event.error的内容,不推荐使用。
throw 'Problem description.'
throw null
另外:
console.error()和throw new Error()抛出的错误信息是有本质区别的。前者不会阻断js运行,也不会被error事件捕捉到,只是在控制台打印错误信息。
以下方式可以阻止异常信息在控制台中显示,线上可以自行收集异常信息后阻止外人看到控制台报错,开发环境不建议使用。
window.addEventListener('error', (function(e) {
console.log("-----errorEvent----", e)
e.preventDefault() //这里换成 return false或return true均不行!
}), true);
window.onerror = function(msg, url, line, col, error) {
console.log("------errorInfo---",msg, url, line, col, error)
return true; //这里用return false不行!
}
3. 检测html劫持
我选用的方案是保存真实环境中的html信息,并对比原html,检测是否有被篡改。
采集html文档用到的是document.documentElement.outerHTML。但是有一点需要注意,上面已提到,该文件需要放在最前面,所以直接用该方法拿到的可能只有<head>
中的html。因此如果想拿到完整的页面信息,需要将采集时间点放到onload以后。
//所有io操作最好都try...catch一下,这里是防止储存的信息超过localStorage的最大限制。关于最大限制是多少已经有不少人说过了,大家可以选择性看一些,如果有必要可以亲测一下。
window.addEventListener("load", function() {
try {
localStorage.setItem("hawkeyeHtml", document.documentElement.outerHTML);
} catch (e) {
//超过限度时,chrome和safari的e.name为'QuotaExceededError',FF的e.name为'NS_ERROR_DOM_QUOTA_REACHED'
console.error(e)
}
})
4. 页面样式丢失
尚未做此数据采集和监控,目前考虑大体思路和html劫持类似,保存截屏图片,用一定算法和正常样式下做对比,超过一定差异值即判定为样式异常。
数据处理方式:
- 上报监控服务器:便于统一监控异常数量类型等
方案:我司仍选择img的属性src进行上报,一是考虑到解析性能,二是考虑到要为多站点服务,方便跨域上报。 - html页面打印前端异常:便于快速准确定位某使用环境崩溃原因
方案:采集到信息储存在localstorage中,注意控制储存的条数。在需要查看异常信息的网站中增加该页面,取出进行简单处理即可。
缺陷处理
- 线上代码是混淆压缩的,无行号
解决方案:线上也用sourceMap来解决,至于sourMap是什么,怎么使用,根据项目不同,可自行google。 - 跨域的js,异常信息只有一个"Script error"。虽然js拿不到任何其他异常信息,但是控制台能打印出全部异常信息。所以
解决方案一:匹配到"Script error",引导开者去控制台查看或直接过滤掉。
解决方案二:解决跨域问题,分两步。一:静态资源请求需要加多一个Access-Control-Allow-Origin头部。二:<scrip>
标签加上crossorigin属性 - 跨域的js,获取不到js文件名,会拿到
at <anonymous>:1:1
酱紫的信息,暂时没找到解决方案