Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于前端异常监控 #21

Open
liangbus opened this issue Dec 10, 2019 · 0 comments
Open

关于前端异常监控 #21

liangbus opened this issue Dec 10, 2019 · 0 comments

Comments

@liangbus
Copy link
Owner

liangbus commented Dec 10, 2019

前言

在公司有做过一些简单的异常监控,但是并没有很全面,自己调研整理了一下常用的方案,以备日后完善系统之用

为降低脚本执行的错误率,更快定位前端异常的情况,很多前端团队都有自己的前端监控系统,市面上也有一些颇受好评的第三方异常监控系统,如 sentry. 本文结合自己的一些见解和实践,围绕下面几个大的方向去讨论。

  • 异常捕获/处理
  • 异常上报
  • 常见问题

异常捕获/处理

前端的常见的异常主要分为两类,JS 脚本异常及网络相关异常

  1. Script 代码异常又分为很多种,常见的如下
  2. JS 语法错误(中文分号,缺少括号等)
  3. JS 运行时错误(变量未声明,Can't read property 'xxx' of undefined 等)
  4. Promise 抛出异常
  5. 外部脚本异常(如iframe,跨域脚本)
  6. 资源加载异常

常见异常示例

<script>
  error
  console.log('You wont see me.')
</script>
<script>
  console.log('You will see me!!!')
</script>

输出

Uncaught ReferenceError: error is not defined
You will see me!!!
这里两个 script 标签属于两个不同的代码块,属于不同的宏任务,(JS 单线程的工作方式就是不停的去队列将任务取出来执行,具体细节参考 JS 的事件循环机制,本文不展开说明)因此可以看到第一个 log 被 JS 报错给阻断了,异常没有被捕获,但是这并不影响第二个任务块的执行

下面讨论下针对对以上异常的捕获方式

1. try-catch

最熟悉不过的就是 try-catch,还是用上面的例子

<script>
  try{
    error
  }
  catch(err) {
    console.log('We Caught Error -> ', err)
  }
  console.log('Check if you can see me')
</script>
<script>
  console.log('You will see me!!!')
</script>

输出

We Caught Error -> ReferenceError: error is not defined at error-script-demo.html:14
Check if you can see me
You will see me!!!
这时异常被 catch 掉了,所以不会阻断当前任务的执行,两个 log 都正常输出

另外 try-catch 无法捕获 JS 语法错误,举个🌰

<script>
  try{
    function()
  }
  catch(err) {
    console.log('We Caught Error -> ', err)
  }
  console.log('Check if you can see me')
</script>
<script>
  console.log('You will see me!!!')
</script>

随便写了个错误的写法,来看输出

Uncaught SyntaxError: Function statements require a function name
You will see me!!!

可以看到第一个任务中的异常没有被捕获到,从而后面的代码被阻断了
这种语法错误类型的异常,只要在项目中像使用类似 ESlint 这种代码检测工具,一般都能提前发现并避免,否则就需要好好检查自己项目的开发流程及测试流程了

同时 try-catch 对异步的异常也无法进行捕获,如下

<script>
  try{
    setTimeout(() => {
      error.log()
    }, 0)
  }
  catch(err) {
    console.log('We Caught Error -> ', err)
  }
  console.log('Check if you can see me')
</script>
<script>
  console.log('You will see me!!!')
</script>

输出

Check if you can see me
You will see me!!!
Uncaught ReferenceError: error is not defined at error-script-demo.html:11

try-catch 我们常用于 JS 运行时可预知可能会出现错误的地方捕获异常,通常是针对特定的业务场景,针对性比较强,且对代码的执行效率有所影响,因此在一个项目中,并不会大量使用。

window.onerror

window.onerror 是一个全局异常捕获事件,但能够捕获到未被捕获的脚本异常
注意,是未被捕获
window.onerror 可以捕获到语法错误(在 Chrome 浏览器测试,版本 80.0.3987.132(正式版本) 64 位,自己测试是可以的,但是在网上看到一些文章说不可以,具体自行测试吧),也可以捕获到异步产生的异常,同时在其回调函数上,有充足的异常信息,可以看出,它比 try-catch 功能要更强大一些

window.onerror = function(message, source, lineno, colno, error) { ... }
<script>
  window.onerror = function(message, source, lineno, colno, error) {
    console.info('Inside window.onerror', message, source)
    console.info('source', source)
    console.info('lineno, colno', lineno, colno, error)
    console.error('tag', error)
  }
</script>
<script>
  try{
    error.log()
  } catch(err) {
    console.error('Caught error', err)
  }
  function()
  console.log('Check if you can see me')
</script>
<script>
  setTimeout(() => {
    foo()
  }, 0)
  console.log('You will see me!!!')
</script>

输出
image

另外 window.onerror 无法捕获到 http 异常,比如图片加载失败,示例

<script>
  window.onerror = function(message, source, lineno, colno, error) {
    console.info('Inside window.onerror', message, source)
    console.info('source', source)
    console.info('lineno, colno', lineno, colno, error)
    console.error('tag', error)
  }
</script>
<body>
  <img src="http://www.abc.com/img/1234.jpg">
</body>

image

控制台只有一条异常输出,这是浏览器自己输出的,我们并没有捕获到

从上面可以看到,即使我们已经通过 window.onerror 方法捕获了异常,但是浏览器还是会输出自己的异常打印信息(浏览器默认的,并非我们手动打印的信息),此时我们可以通过 return true 来阻止其在控制台打印额外的信息,默认返回值是 false

GET http://www.abc.com/img/1234.jpg net::ERR_CONNECTION_RESET

更多见 MDN 上详细说明 GlobalEventHandlers.onerror

window.addEventListener

对于一些 http 请求相关的异常,我们可以通过 addEventListener 给 window 注册 error 事件

<script>
  window.onerror = function(err) {
    console.log('caught by window.onerror : ', err)
  }
  window.addEventListener('error', function(err) {
    console.log('Inside window.addEventListner -> ', err)
  }, true)
</script>
<body>
  <img src="http://abcd.b.com/img/abcd.jpg" alt="">
</body>

可以看到图片请求失败的异常被捕获到了
注意 addEventListener 的第三个参数,它的参数名为 useCapture,我把它设置为 true(默认为 false),因为 http 相关异常不会事件冒泡,因此必须在其捕获阶段将其捕获,但是 EventListener 捕获不了 http 的状态码,也就是 404, 500 这些都拿不到,还是得配合后端接口日志来进行排查

image

注意:

  • 无论是 addEventListener 还是 onerror 都尽量写在页面的前面,避免无法捕获在其声明之前产生的异常
  • addEventListener 多次声明会多次调用,注意合理使用
  • onerror 通过返回值控制浏览器的默认异常输出
  • 通过 JS 创建 img 标签并设置 src 属性,但是并没有插入文档中,当加载异常时, addEventListener 是不能捕获到的
<script>
window.addEventListener('error', function(err) {
    console.log('Inside window.addEventListner -> ', err)
}, true)
var img = new Image()
img.src = 'https://a.vpimg2.com/upload/flow/2020/03/31/97/158564544872915.jpg'
</script>

image

因为 window.onerror 有详细的调用栈信息,更适合去捕获一些脚本方面的异常,而 addEventListener error 事件,更适合捕获一些网络加载相关的异常,因此可以加以区分

// 仅处理资源加载错误
window.addEventListener('error', (event) => {
  let target = event.target || event.srcElement;
  let isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
  // console.log('isEl', isElementTarget);
  if (!isElementTarget) return false;
  const url = target.src || target.href;
  // 上报资源地址
  console.log('资源加载位置', event.path);
  console.error('静态资源错误捕获:','resource load exception:', url);
}, true);// 关于这里为什么不可以用 e.preventDefault() 来阻止默认打印,是因为这个错误,我们是捕获阶段获取到的,而不是冒泡;

Promise catch

<script>
  window.addEventListener('error', function(err) {
    console.log('Inside window.addEventListner -> ', err)
  }, true)
  window.onerror = function(message, source, lineno, colno, error) {
    console.info('Inside window.onerror', message, source)
    return true
  }
</script>
<script>
  Promise.reject('You are nothing!')
</script>

输出

Uncaught (in promise) You are nothing!

对于 Promise 抛出的异常,之前使用的 try-catch, window.onerror 和 addEventListener error 都无法捕获,只能够使用 Promise 自己的 catch 方法以及全局提供的 unhandledrejection 来捕获。

注意,当 Promise 的异常被自己 catch 掉的话,是不会触发 unhandledrejection 事件的,该事件如其名,只处理未被捕获的 Promise rejection,这也可以看成对 Promise 存在永远无法捕获的异常一个兜底处理方案吧。

window.addEventListener('unhandledrejection', function(err) {
    console.log('uuuuuuuuuuuuuuuuuuuuunhandleRejection -> ', err)
  }, true)

crossorigin

对于一些跨域的外部脚本,由于同源策略限制,如果其执行出现异常的话,会直接报 Script Error,没有其他任何信息。

解决思路无非两种

  1. 同源化策略,内联 js 或者使用相同域
  2. 跨源资源共享机制

方式 1 简单粗暴解决了问题,但是也带来更大的问题,无法利用好文件缓存和 CDN 的优势

接下来主要说说方式2
先来看看具体问题,这里我引用了一个外部脚本,这个脚本对其他库有依赖,我没有引就会报错

<script>
  window.addEventListener('error', function(err) {
    console.log('Inside window.addEventListner -> ', err)
  }, true)
  window.onerror = function(message, source, lineno, colno, error) {
    console.info('Inside window.onerror', message, source)
    console.info('source', source)
    console.info('lineno, colno', lineno, colno, error)
    console.error('error', error)
    return true
  }
</script>
<script src="https://shop.vipstatic.com/js/public/core3.1.0-hash-71efefef.js?2018010403"></script>

可以看到 window.onerror 和 addEventListener error 事件都只能拿到一个 Script Error 错误信息

image

跨域资源共享,可以通过给 script 加 crossorigin 属性,增加 crossorigin 属性后,浏览器将自动在请求头中添加一个 Origin 字段,发起一个 跨来源资源共享 请求。Origin 向服务端表明了请求来源,服务端将根据来源判断是否正常响应,若为合法请求,后端在响应头上设置 Access-Control-Allow-Origin,否则依然会报跨域异常

<script src="https://shop.vipstatic.com/js/public/core3.1.0-hash-71efefef.js?2018010403" crossorigin></script>

这时再强制刷新一下(注意是强制,因为有可能会读缓存从而继续报错)

image

这时可以看到,已经有完整的报错信息了。

image

iframe 异常

我们来看下用之前的异常捕获能否捕获到 iframe 中的异常

<script>
  window.addEventListener('error', function(err) {
    console.log('Inside window.addEventListner -> ', err)
  }, true)
  window.addEventListener('unhandledrejection', function(err) {
    console.log('uuuuuuuuuuuuuuuuuuuuunhandleRejection -> ', err)
  }, true)
  window.onerror = function(message, source, lineno, colno, error) {
    console.info('Inside window.onerror', message, source)
    console.info('source', source)
    console.info('lineno, colno', lineno, colno, error)
    console.error('error', error)
    return true
  }
</script>
<body>
  live-server-demo
  <iframe src="http://127.0.0.1:5501/index.html" frameborder="0" style="display: block;border: 1px solid #cfcfcf;"></iframe>
</body>

// http://127.0.0.1:5501/index.html
<body>
  <p>I am in an iframe</p>
  LIVE-SERVER-TEST
  <script>
    error
  </script>
</body>

可以看到,控制台有报错,但只有一条浏览器自己的报错信息,并没有触发我的事件声明

image

那如何监听到 iframe 里面的错误呢?
对于是同源的 iframe ,我们可以通过 window.frames 这个对象拿到页面上的 iframe 对象,它是一个类数组对象,我们可以这样:

<iframe src="./index.html" frameborder="0" style="width: 800px; height: 500px;display: block;border: 1px solid #cfcfcf;"></iframe>
 <script>
   window.frames[0].onerror = function(message, source, lineno, colno, error) {
     console.log('Caught inner iframe ERROR!')
     console.log({
       message, source, lineno, colno, error
     })
     return true
   }
 </script>

成功捕获到异常

image

但是对于非同源的,这种方法是行不通的,需要使用到一些额外的通信手段了,因为对 iframe 不是很熟悉,所以就不详情展开了,具体可以参考文章 跨域,你需要知道的全在这里

另外还有一些框架相关的异常捕获 api,像 react 的 Error Boundaries,这里不一一展开说明,自行查看官方文档即可。

异常上报

前面介绍了这么多异常的类型和捕获的方法,那前端拿到了异常信息之后,要怎么上报呢?常见的有两种方案

  1. ajax 上报数据
  2. 图片 src 属性上报

第1种就是封装个接口,像平时发送请求一样发送数据,但是鉴于 ajax 本身也可能会产生异常,所以大家还是第2种方式居多

方式2 利用的是 img 标签的 src 属性,它可以发起一个单向 get 请求,因为上报日志我们并不需要获取响应数据,所以单向即可满足要求(也许会节省带网络带宽什么的,但我没有求证),并且 src 还具备跨域能力,非常方便,业界很多大厂像淘宝,京东都是使用 img src 来发送请求

参考:
前端监控体系怎么搭建?
脚本错误量极致优化-监控上报与Script error

@liangbus liangbus changed the title 关于前端异常上报 关于前端异常监控 Apr 1, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant