从 0 到 1 搭建前端异常监控系统

3,276 阅读10分钟

本篇文章读后,你将GET的技能:

●收集前端错误(原生、React、Vue)

●编写错误上报逻辑

●利用Egg.js编写一个错误日志采集服务

●编写webpack插件自动上传sourcemap

●利用sourcemap还原压缩代码源码位置

●利用Jest进行单元测试

有没有心动的感觉?赶紧学起来吧!

如何捕获异常

JS异常:

js异常的特点是,出现不会导致JS引擎崩溃,最多只会终止当前执行的任务。

比如一个页面有两个按钮,如果点击按钮导致页面发生异常,这个时候页面不会崩溃。

只是这个按钮的功能失效,其他按钮还会有效☟

上面的例子我们用setTimeout分别启动了两个任务。

虽然第一个任务执行了一个错误的方法。程序执行停止了。但是另外一个任务并没有收到影响。

其实如果你不打开控制台都看不到发生了错误。好像是错误是在静默中发生的。

下面我们来看看这样的错误该如何收集。

try-catch:

JS作为一门高级语言我们首先想到的使用try-catch来收集。

如果在函数中错误没有被捕获,错误会上抛。

image.png

控制台中打印出的分别是错误信息和错误堆栈。

读到这里大家可能会想那就在最底层做一个错误try-catch不就好了吗。

确实作为一个从java转过来的程序员也是这么想的。

但是理想很丰满,现实很骨感。我们看看下一个例子。

image.png

大家注意运行结果,异常并没有被捕获。

这是因为JS的try-catch功能非常有限一遇到异步就不好用了。

那总不能为了收集错误给所有的异步都加一个try-catch吧,太坑爹了。

其实你想想异步任务其实也不是由代码形式上的上层调用的就比如本例中的setTimeout。

大家想想eventloop就明白啦,其实这些异步函数都是就好比一群没娘的孩子出了错误找不到家大人。

当然我也想过一些黑魔法来处理这个问题比如代理执行或者用过的异步方法。

算了还是还是再看看吧。

异常任务捕获

window.onerror:

window.onerror 最大的好处就是同步任务、异步任务都可捕获。

image.png

onerror返回值

onerror还有一个问题大家要注意 如果返回true 就不会被上抛了。

不然控制台中还会看到错误日志。

监听error事件:

文件中的位置☟

window.addEventListener('error',() => {})

其实 onerror 固然好但是还是有一类异常无法捕获。这就是网络异常的错误。

比如下面的例子。

<img src="./xxxxx.png">

试想一下我们如果页面上要显示的图片突然不显示了,而我们浑然不知那就是麻烦了。

addEventListener就是☟

运行结果如下☟

Promise异常捕获:

Promise 的出现主要是为了让我们解决回调地域问题。基本是我们程序开发的标配了。

虽然我们提倡使用 es7 async/await 语法来写。

但是不排除很多祖传代码还是存在Promise写法。

new Promise((resolve, reject) => {
  abcxxx()
});

这种情况无论是onerror还是监听错误事件都是无法捕获的。

image.png

除非每个Promise都添加一个catch方法。

但显然,我们不能这样做。

window.addEventListener("unhandledrejection", e => {
 console.log('unhandledrejection',e)
});

我们可以考虑将unhandledrejection事件捕获的错误抛出交由错误事件统一处理就可以了。

async/await异常捕获:

实际上async/await语法本质还是Promise语法。

区别就是async方法可以被上层的try/catch捕获。

如果不去捕获的话就会和Promise一样,需要用unhandledrejection事件捕获。

这样的话我们只需要在全局增加unhandlerejection就好了。

小结:

实际上我们可以将unhandledrejection事件抛出的异常再次抛出就可以统一通过error事件进行处理了。

最终用代码表示如下:

前端工程化

Webpack工程化:

现在是前端工程化的时代,工程化导出的代码一般都是被压缩混淆后的。

比如:

setTimeout(() => {
    xxx(1223)
}, 1000)

出错的代码指向被压缩后的JS文件,而JS文件长下图这个样子。

如果想将错误和原有的代码关联起来,那就需要sourcemap文件的帮忙了。

sourceMap是什么?

简单说,sourceMap就是一个文件,里面储存着位置信息。

仔细点说,这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。

那么如何利用sourceMap还原异常代码发生的位置这个问题,我们到异常分析这个章节再讲。

VUE 工程

利用vue-cli工具直接创建一个项目。

为了测试的需要我们暂时关闭eslint 这里面还是建议大家全程打开eslint。

在vue.config.js进行配置

我们故意在(文件位置☟)

src/components/HelloWorld.vue

这个时候 错误会在控制台中被打印出来,但是错误事件并没有监听到。

errorHandle 句柄:

为了对Vue发生的异常进行统一的上报,需要利用vue提供的errorHandle句柄。

一旦Vue发生异常都会调用这个方法。

我们在src/main.js

React 工程:

npx create-react-app react-sample

cd react-sample

yarn start

我们用useEffect hooks 制造一个错误:

并且在src/index.js中增加错误事件监听逻辑:

window.addEventListener('error', args => {
    console.log('error', error)
})

但是从运行结果看虽然输出了错误日志但是还是服务捕获。

Error Boundary 组件

错误边界仅可以捕获其子组件的错误。

错误边界无法捕获其自身的错误。

如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。

这也类似于 JavaScript 中 catch {} 的工作机制。

创建ErrorBoundary组件

在src/index.js中包裹App标签☟

最终运行的结果:

异常上报如何选择通讯方式

动态创建img标签:

其实上报就是要将捕获的异常信息发送到后端。最常用的方式首推动态创建标签方式。

因为这种方式无需加载任何通讯库,而且页面是无需刷新的。

基本上目前包括百度统计 Google统计都是基于这个原理做的埋点。

new Image().src = 'http://localhost:7001/monitor/error'+ '?info=xxxxxx'

通过动态创建一个img,浏览器就会向服务器发送get请求。

可以把你需要上报的错误数据放在querystring字符串中,利用这种方式就可以将错误上报到服务器了。

Ajax上报:

实际上我们也可以用ajax的方式上报错误,这和我们在业务程序中并没有什么区别。

###上报哪些数据:

###上报哪些数据:

我们先看一下error事件参数:

其中核心的应该是错误栈,其实我们定位错误最主要的就是错误栈。

错误堆栈中包含了绝大多数调试有关的信息。其中包括了异常位置(行号,列号),异常信息

###上报数据序列化:

由于通讯的时候只能以字符串方式传输,我们需要将对象进行序列化处理。

大概分成以下三步:

1、将异常数据从属性中解构出来,存入一个JSON对象

2、将JSON对象转换为字符串

3、将字符串转换为Base64

当然在后端也要做对应的反向操作 这个我们后面再说。

异常上报的后端服务器

搭建egg.js工程:

异常上报的数据一定是要有一个后端服务接收才可以。

我们就以比较流行的开源框架eggjs为例来演示

# 全局安装egg-cli
npm i egg-init -g 

# 创建后端项目
egg-init backend --type=simple

cd backend
npm i

# 启动项目
npm run dev

编写error上传接口:

首先在app/router.js添加一个新的路由

创建一个新的:

controller (app/controller/monitor)

看一下接收后的结果 ☟

记入日志文件:

下一步就是将错误记入日志。实现的方法可以自己用fs写,也可以借助log4js这样成熟的日志库。

当然在eggjs中是支持我们定制日志那么就用这个功能定制一个前端错误日志好了。

在/config/config.default.js中增加一个定制日志配置

在/app/controller/monitor.js中添加日志记录:

最后实现的效果:

Webpack插件实现SourceMap上传

谈到异常分析最重要的工作其实是将webpack混淆压缩的代码还原。

创建Webpack插件:

/source-map/plugin(文件位置)

加载webpack插件:

webpack.config.js(文件位置)

添加sourceMap读取逻辑:

在apply函数中增加读取sourcemap文件的逻辑

/plugin/uploadSourceMapWebPlugin.js

实现http上传功能:

服务器端添加上传接口:

/backend/app/router.js(文件位置)

添加sourcemap上传接口:

/backend/app/controller/monitor.js

最终效果:

执行webpack打包时调用插件sourcemap被上传至服务器。

解析ErrorStack

考虑到这个功能需要较多逻辑,我们准备把他开发成一个独立的函数并且用Jest来做单元测试:

先看一下我们的需求☟

搭建Jest框架:

首先创建一个/utils/stackparser.js文件☟

在同级目录下创建测试文件stackparser.spec.js

以上需求我们用Jest表示就是

整理如下:

下面我们运行Jest

npx jest stackparser --watch

显示运行失败,原因很简单因为我们还没有实现对吧。

下面我们就实现一下这个方法

反序列Error对象:

首先创建一个新的Error对象 将错误栈设置到Error中。

然后利用error-stack-parser这个npm库来转化为stackFrame

运行效果如下☟

解析ErrorStack:

下一步我们将错误栈中的代码位置转换为源码位置

我们再用Jest测试一下☟

这时我们再看一下结果:

这样一来测试就通过啦~

将源码位置记入日志:

记录完成后,我们再来看一下运行效果:

结束了这一步,我们的ErrorStack工作就完成了。

需要运用的两种开源框架

Fundebug:

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。

自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、荔枝FM、掌门1对1、核桃编程、微脉等众多品牌企业。

Sentry:

Sentry 是一个开源的实时错误追踪系统,可以帮助开发者实时监控并修复异常问题。

它主要专注于持续集成、提高效率并且提升用户体验。

Sentry 分为服务端和客户端 SDK,前者可以直接使用它家提供的在线服务,也可以本地自行搭建;

后者提供了对多种主流语言和框架的支持,包括 React、Angular、Node、Django、RoR、PHP、Laravel、Android、.NET、JAVA 等。

同时它可提供了和其他流行服务集成的方案,例如 GitHub、GitLab、bitbuck、heroku、slack、Trello 等。

目前公司的项目也都在逐步应用上 Sentry 进行错误日志管理。

总结:

截止到目前为止,我们把前端异常监控的基本功能算是形成了一个MVP(最小化可行产品)。

后面需要升级的还有很多,对错误日志的分析和可视化方面可以使用ELK。

发布和部署可以采用Docker。对eggjs的上传和上报最好要增加权限控制功能。

本作品系 转载(阅读原文