前端代码异常监控实战

前端性能与异常上报

2018/08/22 · 基础技术 ·
性能

原文出处: counterxing   

原文出处: happylindz   

概述

对于后台开发来说,记录日志是一种非常常见的开发习惯,通常我们会使用try...catch代码块来主动捕获错误、对于每次接口调用,也会记录下每次接口调用的时间消耗,以便我们监控服务器接口性能,进行问题排查。

刚进公司时,在进行Node.js的接口开发时,我不太习惯每次排查问题都要通过跳板机登上服务器看日志,后来慢慢习惯了这种方式。

举个例子:

JavaScript

/** * 获取列表数据 * @parma req, res */ exports.getList = async
function (req, res) { //获取请求参数 const openId =
req.session.userinfo.openId; logger.info(`handler getList, user openId
is ${openId}`); try { // 拿到列表数据 const startTime = new
Date().getTime(); let res = await ListService.getListFromDB(openId);
logger.info(`handler getList, ListService.getListFromDB cost time ${new
Date().getTime() – startDate}`); // 对数据处理,返回给前端 // … }
catch(error) { logger.error(`handler getList is error,
${JSON.stringify(error)}`); } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取列表数据
* @parma req, res
*/
exports.getList = async function (req, res) {
    //获取请求参数
    const openId = req.session.userinfo.openId;
    logger.info(`handler getList, user openId is ${openId}`);
 
    try {
        // 拿到列表数据
        const startTime = new Date().getTime();
        let res = await ListService.getListFromDB(openId);
        logger.info(`handler getList, ListService.getListFromDB cost time ${new Date().getTime() – startDate}`);
        // 对数据处理,返回给前端
        // …
    } catch(error) {
        logger.error(`handler getList is error, ${JSON.stringify(error)}`);
    }
};

以下代码经常会出现在用Node.js的接口中,在接口中会统计查询DB所耗时间、亦或是统计RPC服务调用所耗时间,以便监测性能瓶颈,对性能做优化;又或是对异常使用try ... catch主动捕获,以便随时对问题进行回溯、还原问题的场景,进行bug的修复。

而对于前端来说呢?可以看以下的场景。

最近在进行一个需求开发时,偶尔发现webgl渲染影像失败的情况,或者说影像会出现解析失败的情况,我们可能根本不知道哪张影像会解析或渲染失败;又或如最近开发的另外一个需求,我们会做一个关于webgl渲染时间的优化和影像预加载的需求,如果缺乏性能监控,该如何统计所做的渲染优化和影像预加载优化的优化比例,如何证明自己所做的事情具有价值呢?可能是通过测试同学的黑盒测试,对优化前后的时间进行录屏,分析从进入页面到影像渲染完成到底经过了多少帧图像。这样的数据,可能既不准确、又较为片面,设想测试同学并不是真正的用户,也无法还原真实的用户他们所处的网络环境。回过头来发现,我们的项目,虽然在服务端层面做好了日志和性能统计,但在前端对异常的监控和性能的统计。对于前端的性能与异常上报的可行性探索是有必要的。

前言

之前在对公司的前端代码脚本错误进行排查,试图降低 JS Error
的错误量,结合自己之前的经验对这方面内容进行了实践并总结,下面就此谈谈我对前端代码异常监控的一些见解。

本文大致围绕下面几点展开讨论:

  1. JS 处理异常的方式
  2. 上报方式
  3. 异常监控上报常见问题

异常捕获

对于前端来说,我们需要的异常捕获无非为以下两种:

  • 接口调用情况;
  • 页面逻辑是否错误,例如,用户进入页面后页面显示白屏;

对于接口调用情况,在前端通常需要上报客户端相关参数,例如:用户OS与浏览器版本、请求参数(如页面ID);而对于页面逻辑是否错误问题,通常除了用户OS与浏览器版本外,需要的是报错的堆栈信息及具体报错位置。

JS 异常处理

对于 Javascript 而言,我们面对的仅仅只是异常,异常的出现不会直接导致 JS
引擎崩溃,最多只会使当前执行的任务终止。

  1. 当前代码块将作为一个任务压入任务队列中,JS
    线程会不断地从任务队列中提取任务执行。
  2. 当任务执行过程中出现异常,且异常没有捕获处理,则会一直沿着调用栈一层层向外抛出,最终终止当前任务的执行。
  3. JS 线程会继续从任务队列中提取下一个任务继续执行。
JavaScript

<script> error console.log('永远不会执行'); </script>
<script> console.log('我继续执行') </script>

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5a707ba987416418324373-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba987416418324373-2">
2
</div>
<div class="crayon-num" data-line="crayon-5a707ba987416418324373-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba987416418324373-4">
4
</div>
<div class="crayon-num" data-line="crayon-5a707ba987416418324373-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba987416418324373-6">
6
</div>
<div class="crayon-num" data-line="crayon-5a707ba987416418324373-7">
7
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5a707ba987416418324373-1" class="crayon-line">
&lt;script&gt;
</div>
<div id="crayon-5a707ba987416418324373-2" class="crayon-line crayon-striped-line">
  error
</div>
<div id="crayon-5a707ba987416418324373-3" class="crayon-line">
  console.log('永远不会执行');
</div>
<div id="crayon-5a707ba987416418324373-4" class="crayon-line crayon-striped-line">
&lt;/script&gt;
</div>
<div id="crayon-5a707ba987416418324373-5" class="crayon-line">
&lt;script&gt;
</div>
<div id="crayon-5a707ba987416418324373-6" class="crayon-line crayon-striped-line">
  console.log('我继续执行')
</div>
<div id="crayon-5a707ba987416418324373-7" class="crayon-line">
&lt;/script&gt;
</div>
</div></td>
</tr>
</tbody>
</table>

图片 1

在对脚本错误进行上报之前,我们需要对异常进行处理,程序需要先感知到脚本错误的发生,然后再谈异常上报。

脚本错误一般分为两种:语法错误,运行时错误。

下面就谈谈几种异常监控的处理方式:

异常捕获方法

try-catch 异常处理

try-catch 在我们的代码中经常见到,通过给代码块进行 try-catch
进行包装后,当代码块发生出错时 catch
将能捕捉到错误的信息,页面也将可以继续执行。

但是 try-catch
处理异常的能力有限,只能捕获捉到运行时非异步错误,对于语法错误和异步错误就显得无能为力,捕捉不到。

全局捕获

可以通过全局监听异常来捕获,通过window.onerror或者addEventListener,看以下例子:

JavaScript

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo,
error) { console.log(‘errorMessage: ‘ + errorMessage); // 异常信息
console.log(‘scriptURI: ‘ + scriptURI); // 异常文件路径
console.log(‘lineNo: ‘ + lineNo); // 异常行号 console.log(‘columnNo: ‘ +
columnNo); // 异常列号 console.log(‘error: ‘ + error); // 异常堆栈信息
// … // 异常上报 }; throw new Error(‘这是一个错误’);

1
2
3
4
5
6
7
8
9
10
window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
  console.log(‘errorMessage: ‘ + errorMessage); // 异常信息
  console.log(‘scriptURI: ‘ + scriptURI); // 异常文件路径
  console.log(‘lineNo: ‘ + lineNo); // 异常行号
  console.log(‘columnNo: ‘ + columnNo); // 异常列号
  console.log(‘error: ‘ + error); // 异常堆栈信息
  // …
  // 异常上报
};
throw new Error(‘这是一个错误’);

图片 2

通过window.onerror事件,可以得到具体的异常信息、异常文件的URL、异常的行号与列号及异常的堆栈信息,再捕获异常后,统一上报至我们的日志服务器。

亦或是,通过window.addEventListener方法来进行异常上报,道理同理:

JavaScript

window.addEventListener(‘error’, function() { console.log(error); // …
// 异常上报 }); throw new Error(‘这是一个错误’);

1
2
3
4
5
6
window.addEventListener(‘error’, function() {
  console.log(error);
  // …
  // 异常上报
});
throw new Error(‘这是一个错误’);

图片 3

示例:运行时错误

JavaScript

try { error // 未定义变量 } catch(e) { console.log(‘我知道错误了’);
console.log(e); }

1
2
3
4
5
6
try {
  error    // 未定义变量
} catch(e) {
  console.log(‘我知道错误了’);
  console.log(e);
}

图片 4

然而对于语法错误和异步错误就捕捉不到了。

try… catch

使用try... catch虽然能够较好地进行异常捕获,不至于使得页面由于一处错误挂掉,但try ... catch捕获方式显得过于臃肿,大多代码使用try ... catch包裹,影响代码可读性。

示例:语法错误

JavaScript

try { var error = ‘error’; // 大写分号 } catch(e) {
console.log(‘我感知不到错误’); console.log(e); }

1
2
3
4
5
6
try {
  var error = ‘error’;   // 大写分号
} catch(e) {
  console.log(‘我感知不到错误’);
  console.log(e);
}

图片 5

一般语法错误在编辑器就会体现出来,常表现的错误信息为: Uncaught
SyntaxError: Invalid or unexpected token xxx
这样。但是这种错误会直接抛出异常,常使程序崩溃,一般在编码时候容易观察得到。

常见问题

示例:异步错误

JavaScript

try { setTimeout(() => { error // 异步错误 }) } catch(e) {
console.log(‘我感知不到错误’); console.log(e); }

1
2
3
4
5
6
7
8
try {
  setTimeout(() => {
    error        // 异步错误
  })
} catch(e) {
  console.log(‘我感知不到错误’);
  console.log(e);
}

图片 6

除非你在 setTimeout 函数中再套上一层
try-catch,否则就无法感知到其错误,但这样代码写起来比较啰嗦。

跨域脚本无法准确捕获异常

通常情况下,我们会把静态资源,如JavaScript脚本放到专门的静态资源服务器,亦或者CDN,看以下例子:

<!DOCTYPE html> <html> <head>
<title></title> </head> <body> <script
type=”text/javascript”> // 在index.html window.onerror =
function(errorMessage, scriptURI, lineNo, columnNo, error) {
console.log(‘errorMessage: ‘ + errorMessage); // 异常信息
console.log(‘scriptURI: ‘ + scriptURI); // 异常文件路径
console.log(‘lineNo: ‘ + lineNo); // 异常行号 console.log(‘columnNo: ‘ +
columnNo); // 异常列号 console.log(‘error: ‘ + error); // 异常堆栈信息
// … // 异常上报 }; </script> <script
src=”./error.js”></script> </body> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
  <script type="text/javascript">
    // 在index.html
    window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
      console.log(‘errorMessage: ‘ + errorMessage); // 异常信息
      console.log(‘scriptURI: ‘ + scriptURI); // 异常文件路径
      console.log(‘lineNo: ‘ + lineNo); // 异常行号
      console.log(‘columnNo: ‘ + columnNo); // 异常列号
      console.log(‘error: ‘ + error); // 异常堆栈信息
      // …
      // 异常上报
    };
 
  </script>
  <script src="./error.js"></script>
</body>
</html>

JavaScript

// error.js throw new Error(‘这是一个错误’);

1
2
// error.js
throw new Error(‘这是一个错误’);

图片 7

结果显示,跨域之后window.onerror根本捕获不到正确的异常信息,而是统一返回一个Script error

解决方案:对script标签增加一个crossorigin=”anonymous”,并且服务器添加Access-Control-Allow-Origin

<script src=””
crossorigin=”anonymous”></script>

1
<script src="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>

window.onerror 异常处理

window.onerror 捕获异常能力比 try-catch
稍微强点,无论是异步还是非异步错误,onerror 都能捕获到运行时错误。

示例:运行时同步错误

JavaScript

/** * @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(‘我知道错误了’); console.log({ msg, url, row, col,
error }) return true; }; error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @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(‘我知道错误了’);
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
error

图片 8

示例:异步错误

JavaScript

window.onerror = function (msg, url, row, col, error) {
console.log(‘我知道异步错误了’); console.log({ msg, url, row, col, error
}) return true; }; setTimeout(() => { error; });

1
2
3
4
5
6
7
8
9
10
window.onerror = function (msg, url, row, col, error) {
  console.log(‘我知道异步错误了’);
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
setTimeout(() => {
  error;
});

图片 9

然而 window.onerror
对于语法错误还是无能为力,所以我们在写代码的时候要尽可能避免语法错误的,不过一般这样的错误会使得整个页面崩溃,还是比较容易能够察觉到的。

在实际的使用过程中,onerror 主要是来捕获预料之外的错误,而 try-catch
则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。

需要注意的是,window.onerror 函数只有在返回 true
的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示
Uncaught Error: xxxxx。

图片 10

关于 window.onerror 还有两点需要值得注意

  1. 对于 onerror 这种全局捕获,最好写在所有 JS
    脚本的前面,因为你无法保证你写的代码是否出错,如果写在后面,一旦发生错误的话是不会被
    onerror 捕获到的。
  2. 另外 onerror 是无法捕获到网络异常的错误。

当我们遇到 <img src="./404.png">报 404 网络请求异常的时候,onerror
是无法帮助我们捕获到异常的。

JavaScript

<script> window.onerror = function (msg, url, row, col, error) {
console.log(‘我知道异步错误了’); console.log({ msg, url, row, col, error
}) return true; }; </script> <img src=”./404.png”>

1
2
3
4
5
6
7
8
9
10
<script>
  window.onerror = function (msg, url, row, col, error) {
    console.log(‘我知道异步错误了’);
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
<img src="./404.png">

图片 11

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

JavaScript

<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=””>

1
2
3
4
5
6
7
8
9
10
<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="">

图片 12

这点知识还是需要知道,要不然用户访问网站,图片 CDN
无法服务,图片加载不出来而开发人员没有察觉就尴尬了。

sourceMap

通常在生产环境下的代码是经过webpack打包后压缩混淆的代码,所以我们可能会遇到这样的问题,如图所示:

图片 13

我们发现所有的报错的代码行数都在第一行了,为什么呢?这是因为在生产环境下,我们的代码被压缩成了一行:

JavaScript

!function(e){var n={};function r(o){if(n[o])return n[o].exports;var
t=n[o]={i:o,l:!1,exports:{}};return
e[o].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=e,r.c=n,r.d=function(e,n,o){r.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},r.r=function(e){“undefined”!=typeof
Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:”Module”}),Object.defineProperty(e,”__esModule”,{value:!0})},r.t=function(e,n){if(1&n&&(e=r(e)),8&n)return
e;if(4&n&&”object”==typeof e&&e&&e.__esModule)return e;var
o=Object.create(null);if(r.r(o),Object.defineProperty(o,”default”,{enumerable:!0,value:e}),2&n&&”string”!=typeof
e)for(var t in e)r.d(o,t,function(n){return e[n]}.bind(null,t));return
o},r.n=function(e){var n=e&&e.__esModule?function(){return
e.default}:function(){return e};return
r.d(n,”a”,n),n},r.o=function(e,n){return
Object.prototype.hasOwnProperty.call(e,n)},r.p=””,r(r.s=0)}([function(e,n){throw
window.onerror=function(e,n,r,o,t){console.log(“errorMessage:
“+e),console.log(“scriptURI: “+n),console.log(“lineNo:
“+r),console.log(“columnNo: “+o),console.log(“error: “+t);var
l={errorMessage:e||null,scriptURI:n||null,lineNo:r||null,columnNo:o||null,stack:t&&t.stack?t.stack:null};if(XMLHttpRequest){var
u=new
XMLHttpRequest;u.open(“post”,”/middleware/errorMsg”,!0),u.setRequestHeader(“Content-Type”,”application/json”),u.send(JSON.stringify(l))}},new
Error(“这是一个错误”)}]);

1
!function(e){var n={};function r(o){if(n[o])return n[o].exports;var t=n[o]={i:o,l:!1,exports:{}};return e[o].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=e,r.c=n,r.d=function(e,n,o){r.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,n){if(1&n&&(e=r(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var t in e)r.d(o,t,function(n){return e[n]}.bind(null,t));return o},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=0)}([function(e,n){throw window.onerror=function(e,n,r,o,t){console.log("errorMessage: "+e),console.log("scriptURI: "+n),console.log("lineNo: "+r),console.log("columnNo: "+o),console.log("error: "+t);var l={errorMessage:e||null,scriptURI:n||null,lineNo:r||null,columnNo:o||null,stack:t&&t.stack?t.stack:null};if(XMLHttpRequest){var u=new XMLHttpRequest;u.open("post","/middleware/errorMsg",!0),u.setRequestHeader("Content-Type","application/json"),u.send(JSON.stringify(l))}},new Error("这是一个错误")}]);

在我的开发过程中也遇到过这个问题,我在开发一个功能组件库的时候,使用npm link了我的组件库,但是由于组件库被npm link后是打包后的生产环境下的代码,所有的报错都定位到了第一行。

解决办法是开启webpacksource-map,我们利用webpack打包后的生成的一份.map的脚本文件就可以让浏览器对错误位置进行追踪了。此处可以参考webpack
document。

其实就是webpack.config.js中加上一行devtool: 'source-map',如下所示,为示例的webpack.config.js

JavaScript

var path = require(‘path’); module.exports = { devtool: ‘source-map’,
mode: ‘development’, entry: ‘./client/index.js’, output: { filename:
‘bundle.js’, path: path.resolve(__dirname, ‘client’) } }

1
2
3
4
5
6
7
8
9
10
var path = require(‘path’);
module.exports = {
    devtool: ‘source-map’,
    mode: ‘development’,
    entry: ‘./client/index.js’,
    output: {
        filename: ‘bundle.js’,
        path: path.resolve(__dirname, ‘client’)
    }
}

webpack打包后生成对应的source-map,这样浏览器就能够定位到具体错误的位置:

图片 14

开启source-map的缺陷是兼容性,目前只有Chrome浏览器和Firefox浏览器才对source-map支持。不过我们对这一类情况也有解决办法。可以使用引入npm库来支持source-map,可以参考mozilla/source-map。这个npm库既可以运行在客户端也可以运行在服务端,不过更为推荐的是在服务端使用Node.js对接收到的日志信息时使用source-map解析,以避免源代码的泄露造成风险,如下代码所示:

JavaScript

const express = require(‘express’); const fs = require(‘fs’); const
router = express.Router(); const sourceMap = require(‘source-map’);
const path = require(‘path’); const resolve = file =>
path.resolve(__dirname, file); // 定义post接口 router.get(‘/error/’,
async function(req, res) { // 获取前端传过来的报错对象 let error =
JSON.parse(req.query.error); let url = error.scriptURI; // 压缩文件路径
if (url) { let fileUrl = url.slice(url.indexOf(‘client/’)) + ‘.map’; //
map文件路径 // 解析sourceMap let consumer = await new
sourceMap.SourceMapConsumer(fs.readFileSync(resolve(‘../’ + fileUrl),
‘utf8’)); // 返回一个promise对象 // 解析原始报错数据 let result =
consumer.originalPositionFor({ line: error.lineNo, // 压缩后的行号
column: error.columnNo // 压缩后的列号 }); console.log(result); } });
module.exports = router;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const express = require(‘express’);
const fs = require(‘fs’);
const router = express.Router();
const sourceMap = require(‘source-map’);
const path = require(‘path’);
const resolve = file => path.resolve(__dirname, file);
// 定义post接口
router.get(‘/error/’, async function(req, res) {
    // 获取前端传过来的报错对象
    let error = JSON.parse(req.query.error);
    let url = error.scriptURI; // 压缩文件路径
    if (url) {
        let fileUrl = url.slice(url.indexOf(‘client/’)) + ‘.map’; // map文件路径
        // 解析sourceMap
        let consumer = await new sourceMap.SourceMapConsumer(fs.readFileSync(resolve(‘../’ + fileUrl), ‘utf8’)); // 返回一个promise对象
        // 解析原始报错数据
        let result = consumer.originalPositionFor({
            line: error.lineNo, // 压缩后的行号
            column: error.columnNo // 压缩后的列号
        });
        console.log(result);
    }
});
module.exports = router;

如下图所示,我们已经可以看到,在服务端已经成功解析出了具体错误的行号、列号,我们可以通过日志的方式进行记录,达到了前端异常监控的目的。

图片 15

Promise 错误

通过 Promise 可以帮助我们解决异步回调地狱的问题,但是一旦 Promise
实例抛出异常而你没有用 catch 去捕获的话,onerror 或 try-catch
也无能为力,无法捕捉到错误。

JavaScript

window.addEventListener(‘error’, (msg, url, row, col, error) => {
console.log(‘我感知不到 promise 错误’); console.log( msg, url, row, col,
error ); }, true); Promise.reject(‘promise error’); new
Promise((resolve, reject) => { reject(‘promise error’); }); new
Promise((resolve) => { resolve(); }).then(() => { throw ‘promise
error’ });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.addEventListener(‘error’, (msg, url, row, col, error) => {
  console.log(‘我感知不到 promise 错误’);
  console.log(
    msg, url, row, col, error
  );
}, true);
Promise.reject(‘promise error’);
new Promise((resolve, reject) => {
  reject(‘promise error’);
});
new Promise((resolve) => {
  resolve();
}).then(() => {
  throw ‘promise error’
});

图片 16

虽然在写 Promise 实例的时候养成最后写上 catch
函数是个好习惯,但是代码写多了就容易糊涂,忘记写 catch。

所以如果你的应用用到很多的 Promise 实例的话,特别是你在一些基于 promise
的异步库比如 axios
等一定要小心,因为你不知道什么时候这些异步请求会抛出异常而你并没有处理它,所以你最好添加一个
Promise 全局异常捕获事件 unhandledrejection。

JavaScript

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’ });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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’
});

图片 17

当然,如果你的应用没有做 Promise
全局异常处理的话,那很可能就像某乎首页这样:

图片 18

Vue捕获异常

在我的项目中就遇到这样的问题,使用了js-tracker这样的插件来统一进行全局的异常捕获和日志上报,结果发现我们根本捕获不到Vue组件的异常,查阅资料得知,在Vue中,异常可能被Vue自身给try ... catch了,不会传到window.onerror事件触发,那么我们如何把Vue组件中的异常作统一捕获呢?

使用Vue.config.errorHandler这样的Vue全局配置,可以在Vue指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和Vue
实例。

JavaScript

Vue.config.errorHandler = function (err, vm, info) { // handle error //
`info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子 // 只在
2.2.0+ 可用 }

1
2
3
4
5
Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 只在 2.2.0+ 可用
}

React中,可以使用ErrorBoundary组件包括业务组件的方式进行异常捕获,配合React 16.0+新出的componentDidCatch API,可以实现统一的异常捕获和日志上报。

JavaScript

class ErrorBoundary extends React.Component { constructor(props) {
super(props); this.state = { hasError: false }; }
componentDidCatch(error, info) { // Display fallback UI this.setState({
hasError: true }); // You can also log the error to an error reporting
service logErrorToMyService(error, info); } render() { if
(this.state.hasError) { // You can render any custom fallback UI return
<h1>Something went wrong.</h1>; } return
this.props.children; } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }
 
  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

使用方式如下:

<ErrorBoundary> <MyWidget /> </ErrorBoundary>

1
2
3
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

异常上报方式

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

  1. 通过 Ajax 发送数据
  2. 动态创建 img 标签的形式
JavaScript

function report(error) { var reportUrl = 'http://xxxx/report'; new
Image().src = reportUrl + 'error=' + error; }

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5a707ba98744f433416112-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba98744f433416112-2">
2
</div>
<div class="crayon-num" data-line="crayon-5a707ba98744f433416112-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba98744f433416112-4">
4
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5a707ba98744f433416112-1" class="crayon-line">
function report(error) {
</div>
<div id="crayon-5a707ba98744f433416112-2" class="crayon-line crayon-striped-line">
  var reportUrl = 'http://xxxx/report';
</div>
<div id="crayon-5a707ba98744f433416112-3" class="crayon-line">
  new Image().src = reportUrl + 'error=' + error;
</div>
<div id="crayon-5a707ba98744f433416112-4" class="crayon-line crayon-striped-line">
}
</div>
</div></td>
</tr>
</tbody>
</table>

实例 – 动态创建 img 标签进行上报

发表评论

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