前端异常监控与报警

我们的需求点?

  • 有些灵异问题只存在于线上特定的环境
  • 前端没有一个紧急问题报警的系统

异常分类

  • javascript异常
    • 语法错误
    • 运行时错误
    • script文件内错误(跨域和未跨域)
  • JS文件、CSS文件、img图片等(资源)的404错误(其实是有onerror事件的dom)
  • promise的异常捕获
  • ajax请求错误

异常级别

一般而言,我们会将收集信息的级别分为info,warn,error等,并在此基础上进行扩展。

当我们监控到异常发生时,可以将该异常划分到“重要——紧急”模型中分为A、B、C、D四个等级。有些异常,虽然发生了,但是并不影响用户的正常使用,用户其实并没有感知到,虽然理论上应该修复,但是实际上相对于其他异常而言,可以放在后面进行处理。

一般而言,越靠近右上角的异常会越快通知,保证相关人员能最快接收到信息,并进行处理。A级异常需要快速响应,甚至需要相关负责人知悉。

错误捕获

  1. 主动捕获(try catch / promise catch)
  2. 全局捕获(onerror / addEventListener)

try catch

通常,为了判断一段代码中是否存在异常,我们会这一写:

try {
var a = 1;
var b = a + c;
} catch (e) {
// 捕获处理
console.log(e); // ReferenceError: c is not defined
}
使用try catch能够很好的捕获异常并对应进行相应处理,不至于让页面挂掉,但是其存在一些弊端,比如需要在捕获异常的代码上进行包裹,会导致页面臃肿不堪,不适用于整个项目的异常捕获。

 

onerror事件

/** *

@param {String} msg 错误信息 *

@param {String} url 出错文件 *

@param {Number} row 行号 *

@param {Number} col 列号 *

@param {Object} error 错误详细信息 */

window.onerror = function (msg, url, row, col, error) {

console.log({ msg, url, row, col, error })

return true; // 注意,在返回 true 的时候,异常才不会继续向上抛出error;

};

打印如下:  {

msg: “Uncaught ReferenceError: error is not defined”,

url: “file:///Users/Desktop/test.html”,

row: 25,

col: 5,

error: ReferenceError: error is not defined at

}

 

通常我们使用window.onerror来捕获js脚本的错误信息。但是对于跨域调用的js脚本,onerror事件只会给出很少的报错信息:error: Script error.这个简单的信息很明显不足以看出脚本的具体错误,
通过为页面上的 script 标签添加 crossOrigin 属性完成跨域上报,别忘了服务器也设置 Access-Control-Allow-Origin 的响应头。(解决跨域的js脚本错误上报)
onerror事件 是无法捕获到网络异常的错误(资源加载失败,裸奔,图片显示异常等)。当我们遇到 <img src="./404.png"> 报 404 网络请求异常的时候,onerror 是无法帮助我们捕获到异常的。

 

window.addEventListener监听error事件

由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

<script>

/** * @param {String} event 监听事件 *

@param {function} function 出错文件 *

@param {Boolean}

useCapture 指定事件是否在捕获或冒泡阶段执行。 * true – 事件句柄在捕获阶段执行 * false- false- 默认。

事件句柄在冒泡阶段执行 */

window.addEventListener(‘error’, (error) => {

console.log(‘我知道 404 错误了’);

console.log( error ); return true; }, true);

</script>

 

<img src=”./404.png” alt=””>

 

promise异常捕获

现代的浏览器其实已经能够支持promise语法了,所以在promise异常捕获这一块我们也还是要注意一下.

  • 人工手动catch捕获(这个是基本的,和try…catch…是一样的).
  • 通过浏览器自带的unhandledrejection事件来监听全局没有catch的promise执行.但是这个的兼容性不是很好,具体可以看下unhandledrejection
<script>
  window.addEventListener('unhandledrejection', function(err) {
    console.log(err);
  });
</script>

new Promise(function(resolve, reject) {
  reject(new Error('haha'))
})

打印如下:

bubbles:false
cancelBubble:false
cancelable:true
composed:false
currentTarget:Window
defaultPrevented:false
eventPhase:0
isTrusted:true
path:Array(1)
promise:Promise // 捕获到的错误promise
reason:Error: haha at http://localhost:3000/promise_error:21:12 at Promise (<anonymous>) at http://localhost:3000/promise_error:20:3 // 其实就是错误栈
  message: "haha"
  stack: "Error: haha↵ at http://localhost:3000/promise_error:21:12↵ at Promise (<anonymous>)↵ at http://localhost:3000/promise_error:20:3"
returnValue:true
srcElement:Window
target:Window
timeStamp:55.190000000000005
type:"unhandledrejection"

 

异常如何上报

监控拿到报错信息之后,接下来就需要将捕捉到的错误信息发送到信息收集平台上,常用的发送形式主要有两种:

  • 通过 Ajax 发送数据(xhr和jquery)
  • 动态创建 img 标签的形式
    function report(error) {
      var reportUrl = 'http://xxxx/report';
      new Image().src = reportUrl + 'error=' + error;
    }
xhr的形式的一个弊端是url长度限制,上报的数据量要做好控制

sourceMap

我们目前的js文件都有做代码压缩、混淆,fis和webpack都比较常用uglifyJs,压缩之后的代码会导致我们很难定位到真实的报错位置
 解决方案是开启构建工具代码压缩的source-map功能:

例如:webpack中开启source-map功能:

module.exports = {

devtool: ‘#source-map’,

}
打包压缩的文件末尾会带上这样的注释:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}…;
//# sourceMappingURL=bundle.js.map
意思是该文件对应的map文件为bundle.js.map。下面便是一个source-map文件的内容,是一个JSON对象:

version: 3, // Source map的版本
sources: [“webpack:///webpack/bootstrap”, …], // 转换前的文件
names: [“installedModules”, “__webpack_require__”, …], // 转换前的所有变量名和属性名
mappings: “aACA,IAAAA,KAGA,SAAAC…”, // 记录位置信息的字符串
file: “bundle.js”, // 转换后的文件名
sourcesContent: [“// The module cache var installedModules = {};…”], // 源代码
sourceRoot: “” // 转换前的文件所在的目录

 

因为前端解析速度较慢,所以这里不做推荐,我们还是使用服务器解析。如果你的应用有node中间层,那么你完全可以将异常信息提交到中间层,然后解析map文件后将数据传递给后台服务器;

通过mozilla/source-map的SourceMapConsumer接口,可以通过将转换后的行号列号传入Consumer得到原始错误位置信息。相应的node代码如下

var fs = require(‘fs’)

var sourceMap = require(‘source-map’) // map文件

var rawSourceMapJsonData = fs.readFileSync(‘./dist/index.min.js.map’, ‘utf-8’)

rawSourceMapJsonData = JSON.parse(rawSourceMapJsonData)

var consumer = new sourceMap.SourceMapConsumer(rawSourceMapJsonData); // 打印出真实错误位置

console.log(consumer.originalPositionFor({line: 1, column: 220}))

 

通过上面方式得到错误行号。

 

MVVM框架

在MVVM框架中如果无法使用window.onerror来捕获异常,因为你的异常信息被框架自身的异常机制捕获了。比如Vue 2.x中我们应该这样捕获全局异常:

Vue.config.errorHandler = function (err, vm, info) {
let {
message, // 异常信息
name, // 异常名称
script, // 异常脚本url
line, // 异常行号
column, // 异常列号
stack // 异常堆栈信息
} = err;

// vm为抛出异常的 Vue 实例
// info为 Vue 特定的错误信息,比如错误所在的生命周期钩子
}
目前script、line、column这3个信息打印出来是undefined,不过这些信息在stack中都可以找到,可以通过正则匹配去进行获取,然后进行上报。

 

注意点

 

以上是异常捕获和上报的主要知识点和流程,还有一些需要注意的地方,比如你的应用访问量很大,那么一个小异常都可能会把你的服务器搞挂,所以上报的时候可以进行信息过滤和采样等,设置一个调控开关,服务器也可以对相似的异常进行过滤,在一个时间段内不进行多次存储。

 

后话

目前也有一些非常完善的前端监控系统存在,如sentrybugsnag等,针对目前我们的情况,拿来开源的第三方应该更方便一下,但是了解原理,摸清逻辑还是很有必要滴~~~~

发表评论

邮箱地址不会被公开。 必填项已用*标注