前端错误监控以及上报

lxf2023-03-12 19:15:01

前端错误捕获方法

前端捕获错误的方法:

  • try..catch:捕获的异常必须是线程执行进入到try...catch且try...catch未执行完的时候抛出来。

    • 语法异常在语法检查阶段就报错了,线程尚未进入try...catch代码块,所以无法捕获到异常。

      try {
          a.
      }catch(e) {
          console.log('catch error:', e)
      }
      
    • 不能捕获setTimeout或者Promise中的错误。以下错误都不能捕获。如果想捕获,要将try...catch放入到异步代码内部。

              try {
                  new Promise((res, rej) => {
                      rej('promise reject error')
                      // throw new Error('promise throw error')
                  })
              } catch (e) {
                  console.log('catch error:', e)
              }
      ​
      ​
              try {
                  setTimeout(() => {
                      throw new Error('setTimeout throw error')
                  }, 0)
              } catch (e) {
                  console.log('catch error:', e)
              }
      
    • 能捕获async 异常

              async function fn() {
                  try {
                      let res = await new Promise((res, rej) => {
                          // rej('my reject err') // unhandledrejection 可以处理
                          throw Error('my throw error') // unhandledrejection 可以处理
                      })
                  } catch (err) {
                      console.log('catch err', err)
                  }
              }
      ​
              fn()
      
  • window.onerror:当资源加载失败或无法使用时,会在Window对象触发error事件,无法捕获promise错误,可以捕获setTimeout错误。

    • 当加载自不同域的脚本中发生语法错误时,浏览器为避免信息泄露的安全风险,语法错误的细节将不会报告给浏览器console中,而是使用"Script error."信息代替。解决办法是为 script 标签添加 crossOrigin 属性,并且服务端配置Access-Control-Alow-Origin:*
  • unhandledrejection:当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件

不同场景错误处理方式

总结先行: addEventListener('error') + addEventListener('unhandledrejection') 的方式恰好能够覆盖5种异常错误(同步任务,普通异步任务,promise任务,async任务,资源加载)的捕获。

可以将unhandledrejection捕获到的错误throw出来让error进行捕获之后统一上报。

  • 跨域资源加载问题:window.addEventListener('error',()=>{}),并且script 标签添加 crossOrigin 属性,并且服务端配置Access-Control-Alow-Origin

  • 定时器内部函数抛出错误:window.onerror或者window.addEventListener('error',()=>{})

  • 静态资源加载的异常:window.addEventListener('error')可以捕获,但是window.onerror不能捕获

  • 网络请求的异常:axios的响应拦截器

  • 线上压缩代码:开启sourceMap

  • promise:常常配置catchhandler进行处理,没有处理的rejected的promise通过unhandledrejection

    // 能触发 unhandledrejection ,因为未显式处理reason 
    Promise.reject('error').then() 
    Promise.reject('error').then(console.log) 
    ​
    // 不能触发 unhandledrejection ,因为已处理reason 
    Promise.reject('error').then(console.log, console.log) 
    // 不能触发 unhandledrejection ,因为没处理reason,直接抛出异常 
    Promise.reject('error')
    
  • React捕获错误:错误边界(Error Boundaries)

    • 部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误。

    • 错误边界无法捕获以下场景中产生的错误:

      • 事件处理(了解更多)
      • 异步代码(例如 setTimeoutrequestAnimationFrame 回调函数)
      • 服务端渲染
      • 它自身抛出来的错误(并非它的子组件)
  • Vue捕获错误:

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

      • 从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子是 undefined 时,被捕获的错误会通过 console.error 输出而避免应用崩溃。
      • 从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了。
      • 从 2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理。
      • 错误追踪服务 Sentry 和 Bugsnag 都通过此选项提供了官方支持。
    • 生命周期钩子-errorCaptured:在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传

      • 传播规则:

        • 默认情况下,如果全局的 config.errorHandler 被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。
        • 如果一个组件的 inheritance chain (继承链)或 parent chain (父链)中存在多个 errorCaptured 钩子,则它们将会被相同的错误逐个唤起。
        • 如果此 errorCaptured 钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的 config.errorHandler
        • 一个 errorCaptured 钩子能够返回 false 以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的 errorCaptured 钩子和全局的 config.errorHandler

错误信息上报

捕获到错误信息后进行上报,对于前端监控很重要。

上报的方式有三种:

ajax进行上报

发现错误的时候上传错误到接口进行存储。

但是存在一些问题:

  • 有严格的跨域限制
  • 上报请求可能会阻塞业务
  • 请求容易丢失(被浏览器强制cancel)

image上报

由于图片天然可跨域,又能兼容所有的浏览器,而js和css等其他资源文件则可能出现安全拦截和跨域加载问题。

let img = new Image()
img.src='请求的url'

但由于是一个get请求,上报的数据量在不同的浏览器下上限不一致(2kb-8kb),这就可能出现超出长度限制而无法上报完整数据的情况。因此,图片上报也是一个“不安全”的方式。

sendBeacon

navigator.sendBeacon() 方法可用于通过 HTTP POST 将少量数据 异步 传输到 Web 服务器。

它主要用于将统计数据发送到 Web 服务器,同时避免了用传统技术

这个方法主要用于满足统计和诊断代码的需要,这些代码通常尝试在卸载(unload)文档之前向 Web 服务器发送数据。过早的发送数据可能导致错过收集数据的机会。然而,对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在 unload 事件处理器中产生的异步 XMLHttpRequest

navigator.sendBeacon(url, data);

使用 sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能,这意味着:

  • 数据发送是可靠的。
  • 数据异步传输。
  • 不影响下一导航的载入。