零散专题37 前端代码异常监控
前端代码异常监控学习笔记,原文地址:前端代码异常监控实战@掘金
异常处理
JavaScript中的异常并不会导致JavaScript引擎崩溃,最多只会让当前执行的任务停止。
1 | <script> |
在对错误及哦啊本进行上报之前,需要对异常进行处理,程序首先要感知到脚本错误的发生,然后再进行异常上报
我们需要处理的错误根据导致的原因分类有以下几种:
- 语法错误
- 运行时错误
- 网络请求错误
iframe
中的错误
根据抛出错误的代码所属的任务队列可以分为:
- 同步错误
- 异步错误(非Promise)
- Promise错误
try...catch
异常处理
通过使用try...catch
结构包裹代码,当try
代码块发生错误时,catch
能捕获到错误的信息,页面可以继续执行。
1 | try { |
但是try...catch
处理异常的能力有限,只能捕获到运行时非异步错误,对于语法错误和异步错误都捕捉不到。
1 | try { |
异步错误:
1 | try { |
error
事件
可以在window.onerror
事件中捕获异常,它的捕获异常能力优于try...catch
,主要原因在于:
window.onerror
是全局捕获,而try...catch
针对的是具体的代码块window.onerror
可以捕获同步错误以及异步错误
1 | window.onerror = function (msg, url, row, col, error) { |
同步错误:
1 | console.log(error); // 同步错误 |
异步错误:
1 | setTimeout(() => { |
但是window.onerror
也无法捕获语法错误。所以在实际使用时,一般使用onerror
来捕获预料之外的错误,try...catch
用来在可预见情况下监控特定的情况,二者需要结合使用。
有几点需要注意
(1)window.onerror
只有在返回true
的时候,异常才不会向上抛出,否则即使捕获到了异常,控制台还是会显示Uncaught Error
:
1 | window.onerror = function (msg, url, row, col, error) { |
(2)包含window.onerror
代码的脚本最好写在所有脚本的最前面,如果放在某些脚本的后面,那么发生错误就无法捕获
1 | <script> |
(3)window.onerror
无法捕获网络异常的错误
例如当图片加载失败发返回404
的异常时,onerror
是捕获不到的:
1 | <script> |
这是因为网络请求的错误不会事件冒泡,所以必须在捕获阶段去捕获异常:
1 | <script> |
虽然可以捕获到网络请求的异常,但是无法具体判断HTTP的状态和返回码,需要配合服务端日志以及对网络请求进行拦截进行排查分析才可以。
unhandledrejection
try..catch
和window.onerror
都无法捕获在Promise中抛出的错误:
1 | window.onerror = function (e) { |
对于Promise来说,虽然可以再每一个实例后面添加catch
来捕获特定的实例中抛出的错误,但是通过unhandledrejection
来全局捕获异常也是非常必要的:
1 | window.addEventListener('unhandledrejection', e => { |
当然如果异常在Promise的实例中进行了捕获,那么在unhandledrejection
中就捕获不到了:
1 | window.addEventListener('unhandledrejection', e => { |
iframe
中的错误
如果页面中使用了<iframe>
,需要对引入的<iframe>
进行异常监控,否则如果引入的页面出现问题,内容显示不出来,但是我们却一无所知。
父窗口直接使用window.error
是无法直接捕获<iframe>
中的错误的,需要分下面几种情况进行处理:
(1)<iframe>
页面与当前页面同域名的话,可以直接给<iframe>
添加onerror
事件:
1 | window.frames[0].onerror = function (msg, url, row, col, error ) { |
(2)如果嵌入的<iframe>
中的页面与当前页面不是同一个域名,那么上面的方法就行不通了。如果我们拥有<iframe>
的控制权的话,可以通过与<iframe>
通信的方式将异常信息发送给主页面,通信的方式很多,比如postMessage
事件等
在iframe
中发送消息,注意需要使用window.top
进行发送,不能直接使用window
1 | // iframe 中 |
在主页面中需要监听message
事件:
1 | window.addEventListener('message', (e) => { |
(3)<iframe>
的域名非同源,并且自己没有控制权,那么除了看控制台之外,没有办法捕获。
异常上报
监控拿到报错信息后,需要将捕获到的错误信息上报到信息收集平台上,常用的上报方法有两种:
- 通过Ajax发送数据
- 通过动态创建
<img>
标签
通过Ajax发送数据就是平常的调用接口的形式进行上报即可,而动态创建<img>
标签主要是根据信息收集平台的要求,将<img>
的src
属性拼接指定的错误信息即可:
1 | function report(error) { |
Script Error
在线上的版本,静态资源一般会放在CDN,会导致长访问的页面与脚本来自不同的域名,,如果没有额外的配置,就会产生Script Error
这是因为浏览器同源策略导致的,浏览器出于安全性的考虑,当页面引用非同域名的外部脚本文件发生错误时,当前页面是没有权利知道报错信息的,取而代之的是输出Script Error
解决方法就是为页面上的<script>
标签添加crossOrigin
属性,可以取值anonymous
和use-credentials
,如果没有属性值或者非法属性值,会被浏览器默认做anonymous
,它的作用有三个:
- 让浏览器启动CORS访问检查,检查HTTP响应头的
Access-Control-Allow-Origin
- 对于传统
<script>
需要跨域获取的JS资源,暴露其报错的详细信息 - 对于
module script
,控制用于跨域请求的凭据模式
同时服务端需要在返回的响应头中Access-Control-Allow-Origin
为对应的域名
1 | // http://localhost:8080/index.html |
这样就可以获得外域脚本的详细的报错信息了,对于JSONP来跨域的时候遇到的错误可以据此解决:
1 | const script = document.createElement('script'); |
如果使用的第三方库加载异步脚本时,如果没有提供crossOrigin
的能力时,可以劫持document.createElement
,在动态生成的脚本上添加crossOrigin
字段,但是这种方式
有一定风险,需要谨慎使用
1 | document.createElement = (function() { |
压缩代码如何定位到脚本异常位置
线上的代码都经过了压缩、合并处理,当收到一条报错的信息的时候,可能根本没有办法直接获知变量代表什么、位置在哪里,此时的错误日志是无效的。
解决方法就是利用sourceamp来定位到错误代码的具体位置,详细内容参考《脚本错误量极致优化-让脚本错误一目了然》,最理想的方案可以尝试Sentry开源方案
收集信息太多
可以给网站设置采集率:
1 | Reporter.send = function (data) { |
这个采集率可以通过具体实际的情况来设定,方法多样化,可以使用一个随机数,也可以具体根据用户的某些特征来进行判定。
总结
异常捕获有三种常用的方式try...catch
、window.onerror
、unhandledrejection
,它们都没有办法捕获语法错误,所以在写代码的时候尽量通过IDE的提示和ESLint的检查避免语法错误
try...catch
可以捕获同步错误,无法捕获网络错误、异步错误和Promise的错误window.onerror
可以捕获同步错误、异步错误,需要在事件捕获阶段捕获网络错误(但是信息不详细),无法捕获Promise的错误unhandledrejection
专门用来捕获没有在Promise实例的catch
中捕获的Promise中抛出的错误
使用的时候一般:
- 使用
window.onerror
来捕获全局的同步错误、异步错误 - 用
unhandledrejection
来捕获全局的Promise错误 - 用
try...catch
来在预见情况下捕获特定的错误
<iframe>
中的错误如果是同源页面,可以直接为其添加onerror
事件,如果是非同源但是有控制权,可以通过postMessage
传递错误,如果是非同源也无控制权,那么无能为力。