客户端错误监控原理分析

238 阅读6分钟

思考前端常见哪些错误?

  1. 脚本错误
  •     语法错误

  •     运行时错误

  •         同步错误

  •         异步错误

  •         Promise错误

  1. 网络错误
  •     资源加载错误
  •     自定义请求错误

我们最常用的解决错误的方式,你能想到哪些?

  1. try...catch(e){}
  2. Promise.reject() / Promise.catch()

try...catch(e){}的问题

  1. 无法捕获语法错误。
  2. 可以利用setTimeout捕获异步错误,但是无法捕获其他上下文的错误。

window.onerror()

比 try...catch(e){} 捕获错误信息能力更强大。

/**
 * @param {String}  msg    错误描述
 * @param {String}  url    报错文件
 * @param {Number}  row    行号
 * @param {Number}  col    列号
 * @param {Object}  error  错误Error对象
 */
 window.onerror = function (msg, url, row, col, error) {
  console.log('我知道错误了');
  return true; // 返回 true 的时候,异常不会向上抛出,控制台不会输出错误
};

windowOnerrorwindowOnerror

使用注意:

  • 应该在所有脚本之前被执行,以免有遗漏。
  • 一定要renturn true。否则异常还会向上抛出,出现在控制台上。

优点:

  • 可以捕获常见的语法、同步、异步错误等错误。

缺点:

  • 无法捕获Promise错误、网络错误。
  • 容易被覆盖,可能别人也在使用该事件监听。

原理:

  • 利用了事件冒泡,从而达到监听的作用。

网络错误

window.onerror无法捕获网络请求异常的错误,这是因为网络请求异常不会事件冒泡。所以,必须在事件捕获阶段将异常捕捉到。

<script>
window.addEventListener('error', (msg, url, row, col, error) => {
  console.log('我知道 404 错误了');
  console.log(
    msg, url, row, col, error
  );
  return true;
}, true);
</script>
<img src="./404.png" alt="">

原理:事件捕获阶段将异常捕捉到。

优点:

  • 不怕回调被覆盖,可以监听多个回调函数。

缺点:

  • 无法知道是404还是500等等。需要服务端的日志进行排查分析才可以。
  • 不销毁就会造成内存泄漏。

常见用途:

用户访问网站,图片 CDN 无法服务,图片加载不出来而开发人员没有察觉就尴尬了。

Promise 错误

我们在写Promise的时候,总是会忘记去写catch。导致Promise抛出的异常无法捕获,哪怕你使用try...catch(e){} 或 onerror也无能为力。

所以,在用到很多Promise实例时,最好添加一个 Promise 全局异常捕获事件:unhandledrejection。

window.addEventListener("unhandledrejection", function(e){
  e.preventDefault()
  console.log('我知道 promise 的错误了');
  console.log(e.reason);
  return true;
});
Promise.reject('promise error');
new Promise((resolve, reject) => {
  reject('promise error');
});
new Promise((resolve) => {
  resolve();
}).then(() => {
  throw 'promise error'
});

上报方式

客户端监听到异常需要上报给服务端,服务端将错误详情整合,进行响应。

方式:

  • 动态的创建img标签,请求长度有限制。
  • ajax

使用ajax类库,在抛出异常监听的之后请求,发送给服务端。

Script error 脚本错误是什么?

原因是引用了跨域脚本,浏览器处于安全考虑,不显示具体错误而是 Script error。

解决:

  • 所有资源切换到同一域名,这样会失去CDN优势。

  • 脚本配置crossorigin属性,服务端设置CORS。

  • script:<script src="http://www.xxx.com/index.js" crossorigin></script>

  • 服务端:Access-Control-Allow-Origin: You-allow-origin

crossorigin属性取值和响应头:

  • crossorigin="anonymous" (默认),CORS 不等于 You-allow-origin,不能带 cookie

  • crossorigin="use-credentials"Access-Control-Allow-Credentials: true ,CORS 不能设置为 *,能带 cookie。如果 CORS 不等于 You-allow-origin,浏览器不加载 js。

压缩代码如何定位到脚本异常位置?

问题就想标题一样明确了,但还是想举个例子:

源代码(存在错误):

function test() {
    noerror // <- 报错
}

test();

经 webpack 打包压缩后产生如下代码:

!function(n){function r(e){if(t[e])return t[e].exports;var o=t[e]={i:e,l:!1,exports:{}};return n[e].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var t={};r.m=n,r.c=t,r.i=function(n){return n},r.d=function(n,t,e){r.o(n,t)||Object.defineProperty(n,t,{configurable:!1,enumerable:!0,get:e})},r.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return r.d(t,"a",t),t},r.o=function(n,r){return Object.prototype.hasOwnProperty.call(n,r)},r.p="",r(r.s=0)}([function(n,r){function t(){noerror}t()}]);

代码如期报错,并上报相关信息:

{ 
  msg: 'Uncaught ReferenceError: noerror is not defined',
  url: 'http://127.0.0.1:8077/main.min.js',
  row: '1',
  col: '515' 
}

可见1和515是个什么鬼?我哪知道问题出在哪?

如何定位具体错误?

我们的决绝方案是考虑不将压缩后的文件扩大/半还原成源文件的形式,去解决该问题。

SouceMap 快速定位

什么是SouceMap?

它是一个文件信息,存储着源文件的信息,以及源文件与处理后文件的映射关系。

在定位压缩代码的报错时,通过错误信息的行列数与对应的 SouceMap 文件,处理后得到源文件的具体错误信息。

sourcemap_1

SourceMap 文件中的 sourcesContent 字段对应源代码内容,不希望将 SourceMap 文件发布到外网上,而是将其存储到脚本错误处理平台上,只用在处理脚本错误中。

通过 SourceMap 文件可以得到源文件的具体错误信息,结合 sourcesContent 上源文件的内容进行可视化展示,让报错信息一目了然!

基于 SourceMap 快速定位脚本报错方案。

sourcemap_2

使用

客户端通过webpack的devtool属性,对 SouceMap 文件进行配置。

webpack.config.js 配置如下:

module.exports = {
  entry: './js/main.js',
  output: {},
  mode: 'development',
  devtool: 'eval', // 主要看这个
  ......
};

devtool支持的值有许多,这里介绍几个。

eval:

当值为eval时,会将每个module块执行eval,执行之后不会生成 SouceMap 文件(.map文件),仅仅是在每个模块后,增加SouceURL来关联模块处理前后的对应关系。

(function(modules) { // webpackBootstrap
  "use strict";
  eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return printMe; });\n\nfunction printMe() {\n  console.log('11111111');\n}\n\n//# sourceURL=webpack:///./js/demo1.js?");

  /***/ "./js/main.js":
  /*!********************!*\
    !*** ./js/main.js ***!
    \********************/
  /*! no exports provided */
  /***/ 
  (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _demo1_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./demo1.js */ \"./js/demo1.js\");\n\n\nconsole.log('main.js');\n\n//# sourceURL=webpack:///./js/main.js?");
  })
})

每一个打包后的模块后面都增加了包含sourceURL的注释,sourceURL的值是压缩前存放的代码的位置,这样就通过sourceURL关联了压缩前后的代码。

**优点:**打包速度快,原因是不需要生成 SouceMap文件。

**缺点:**映射到转换后的代码,而不是原始代码,所以无法正确显示行列数。

source-map:

source-map会为每一个打包后的module生成独立的sourcemap文件。

修改package.json文件(注意:--devtool):

scripts: {
  "dev": "webpack-dev-server --progress --colors --devtool source-map --hot --inline",
}

运行npm run build,dist文件中会有.map文件。打包后的代码如下:

(function(module, __webpack_exports__, __webpack_require__) {

  "use strict";
  __webpack_require__.r(__webpack_exports__);
  /* harmony import */ var _demo1_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./demo1.js */ "./js/demo1.js");
  __webpack_require__(/*! ../styles/main.css */ "./styles/main.css");
  console.log('main.js');
/***/ }),

  /***/ "./styles/main.css":
  /*!*************************!*\
    !*** ./styles/main.css ***!
    \*************************/
  /*! no static exports found */
  /***/ (function(module, exports) {

  // removed by extract-text-webpack-plugin

  /***/ })

  /******/ });
  //# sourceMappingURL=bundle.js.map

如上打包后的代码最后面一句代码是 //# sourceMappingURL=bundle.js.map ,同时在dist目录下会针对每一个模块生成响应的 .map文件,
比如我们在dist目录中会生成 bundle.js.map文件,我们可以打开看下这个文件代码会如下:

{
  "version":3,
  "sources":[
    "webpack:///webpack/bootstrap","webpack:///./js/demo1.js",
    "webpack:///./js/main.js","webpack:///./styles/main.css"
  ],
  "names":["printMe","console","log","require"],
  "mappings":";AAAA;AACA;;AAEA;AACA...",
  "file":"bundle.js",
  "sourcesContent":[],
  "sourceRoot": ""
}

属性:

version: Source Map 的版本,目前为3.
sources: 转换前的文件,该项是一个数组,表示可能存在多个文件合并.
names: 转换前的所有变量名和属性名。
mappings: 记录位置信息的字符串。
sourcesContent: 转换前的文件内容列表,与sources列表依次对应。
sourceRoot: 转换前的文件所在的目录,如果与转换前的文件在同一个目录,该项为空。

优点:

  • 有具体的行列数。

缺点:

  • sourcesContent会暴漏信息,不希望将 SourceMap 文件发布到外网上。

总结:

关于客户端错误监控,以及上报,就暂时总结到这里了。webpack的devtool还有许多值可以进行配置,就不一一介绍了。用哪个值取决于公司需求了。