掌握主动权——系统错误监控助你降成本、提效率(上)

384 阅读9分钟

“我正在参加「掘金·启航计划」”

背景

在项目开发环境时,使用浏览器调试可以比较清晰的看到报错信息;那个文件报错,哪个字段报错,可以很容易、快速定位到报错源头然后去修复。

如图开发环境报错: image.png

我们能明显的看出来该错误说:TypeError:无法读取未定义的财产(读取“abc”)

发生错误的文件是 TestPage.vue

但是对于上线后的项目来说,一般都是会有代码压缩、混淆、禁用console.log等处理,如果线上项目代码出现报错问题,这个时候使用浏览器调试就不太方便,很难快速定位到出错位置。

如图生产环境报错: image.png

可以看到说:TypeError:无法读取未定义的财产(读取“abc”)
但是出现错误的文件却是:chunk-a8c0af14.xxx.js

还有些报错问题可能出现了之后,我们开发人员没发现,用户发现了,但用户也没反馈。这种情况报错问题就变的复现难!导致系统存在问题,不稳定,问题不好定位追溯。这些就是项目目前存在的一大痛点!

生产环境主要问题:

  • 报错定位溯源难
  • 报错问题复现难

在实践这个事情的过程中,遇到了很多问题,也在网上查阅了相关文章资料学习,一路坎坷,颇有收获。谨以此篇记录自己学习成长的过程。

需求

根据上面分析发现的问题,我们就有了需求,那就是我们需要一个前端项目的错误监控平台。对我们的系统中发生的这些错误进行收集、上报、解析、回溯

这样的话,生产环境有报错问题就会被记录,我们的项目得到了一个实时的监控,就不是在裸奔啦!

虽然目前已经有很多成熟的系统监控相关框架,但还是想在自己的项目中从0到1的尝试一下。

目的

通过对项目中错误信息的监控,做到 错误收集错误上报问题回溯
让我们在生产环境中,可以快速定位到错误信息的溯源。 追溯系统出现报错问题时,节约时间成本,一劳永逸!并且能够统计到系统存在的bug。

1、错误收集

错误收集之前,我先把我的前端项目中的错误类型大体分为:框架错误、脚本错误、资源加载错误三大类,然后分别处理对应类型的错误。

错误信息格式化

在不同浏览器不同框架上,抛出来的error是不一样的。
比如:Vue.config.errorHandler中拿到的error是Vue处理后的信息,我们无法很直观的拿到colum和line信息。

这里我们使用这个库 TraceKit 来解决这个问题。 他的作用就是格式化 error,使得不同环境下的 error 输出相同格式的错误信息。

  • 使用tracekit
    安装:npm i tracekit
    使用:
    import TraceKit from 'tracekit'
    TraceKit.report.subscribe((errorReport) => {
        const { message, stack } = errorReport || {}
        const obj = {
          message,
          stack: {
            column: stack[0].column,
            line: stack[0].line,
            func: stack[0].func,
            url: stack[0].url
          }
        }
        // 上报信息
        imgPostData(obj)
      })
    

这里在格式后错误后直接上报了错误,后面会讲到!

错误类型区分

框架错误(vue)

框架错误:指的不是框架层面的错误,而是指框架提供了 API 来捕获全局错误

我的项目主要使用vue框架,所以先针对该框架进行错误捕获处理,如果是react或其他框架也是一样的道理

由于Vue会捕获所有Vue单文件组件或者Vue.extend继承的代码,所以在Vue里面出现的错误,并不会直接被 window.onerror捕获,而是会抛给Vue.config.errorHandler

原因: Vue里抛出的异常都是Vue文件经过vue-loader转义之后的JavaScript抛出的
所以使用window.onerror捕获到的异常都是 Script Error

所以我们在 Vue.config.errorHandler 中捕获错误时,不让它的报错信息展示到控制台,使用try catch进行捕获,然后把错误回调内容交给 TraceKit 格式化一下。这样我们在.vue文件中的代码如果有错误情况就会被捕获后,在控制台显示。

vueErrorHandler () {
    Vue.config.errorHandler = function (err, vm, info) {
      try {
        console.log('-----vue-error')
        // 使用TraceKit处理错误
        TraceKit.report(err)
      } catch (e) {
        console.error('vue-errorHandler错误:', e)
      }
    }
  }

其实在vue项目中针对框架错误进行监听上报处理后就已经可以捕获大部分错误了,因为我们项目中大部分内容都是在.vue中实现的。

为了监控的更全面我们继续往下

脚本错误(js错误、资源加载错误)

  • 全局js错误

    在浏览器环境中,我们可以监听 onError 事件,所以js错误,可以直接使用 window.addEventListener('error') 去监听。

  • 资源加载错误

    img图片、script、css、font等资源的加载错误都属于资源加载错误。

    <img> 或 <script>加载失败时,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数。

    但这些error事件不会向上冒泡到window。不过,这些error事件能被window.addEventListener('error')捕获。也就是说,面对资源加载失败的错误,只能用window.addEventListerner('error'), 而 window.onerror方式无效。

所以以上两种错误我们都用window.addEventListerner('error')来监听;然后通过event.target.src || event.target.href 判断是否有值,如果有说明是资源加载

window.addEventListener('error', event => (){ 
  if (event.target && (event.target.src || event.target.href)) {
    // 资源错误 
    const obj = {
       src: event.target.src || event.target.href,
       // ...
    }
  }
}, true);

资源加载错误时,没有对应的堆栈信息,我们只能拿到出错资源的src地址。这里可以根据自己的需求来自定义一个 stack对象,存储当前资源的关键信息,用于后面的错误上报。

比如我们可以从 event.target 中拿到 baseURI 来判断当前资源错误时发生在哪个页面。

Promise 异步错误(待完善)

window.addEventListener('unhandledrejection')

网络请求错误(待完善)

  • 接口异常

接口异常可以 重写xhr 的open send方法,监控 load、error、abort,进行上报。

例如:

function networkHandle(): void { 
    let originSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function (){
    console.log('send', arguments);
    this.addEventListener('loadend', () => { 
    console.log('loadend')
    if (this.status !== 200) { 
    //网络请求挂了,捕获 
    }
}) 

originSend.apply(this, arguments) }}

2、上报错误

错误类型区分完后就要进行上报错误了,但上报错误方式也有很多种。

上报错误方式

image对象方式上报(推荐)

动态创建img标签进⾏上报,页面不需要刷新,没有跨域的问题。(只要在js中new出Image对象就能发起请求,⽽且没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点。)

   function imgPostData (data) {
     // 通过img方式进行错误信息上报
    const img = new Image()
    const url = '/api/img?data='
    img.src = url + JSON.stringify(data) 
   }   

存在问题:

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

ajax方式(不推荐)

发现错误时,上传错误到接口进行存储。
存在问题:

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

sendBeacon(待验证)

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

由于初次探究,加上我的项目中错误内容也比较小,暂时直接使用image方式,后续可以深入研究一下。 如果大家有实践过更优雅的方式欢迎评论区交流!

3、上传sourcemap文件

由于我们的项目经过打包后都是压缩处理过的代码,我们上报上去的错误信息也是被压缩过的,这时候就还需要把 sourcemap 源码文件上传到服务器,方便后续服务端去解析出源代码的真实错误的具体位置信息。

其实一般情况,生产环境是不建议上传sourcemap文件的!上传sourcemap需要考虑 .map 文件在线上可能带来的风险问题,因此必须要 权衡 使用 sourcemap 的组合方式。

实现自定义webpack插件

sourcemap源文件的上传,我们自己写一个webpack插件来完成这个工作。

首先vue.config文件中一定要先开启productionSourceMap: true

然后根据自己的情况去配置 devtool: 'source-map'

此时执行npm run build打包时,我们的dist目录下会出现.js.map的文件,我们需要把这些文件上传到服务器。

根目录创建 plugins目录,创建自己要实现的webpack插件 sourceMap-uploader-plugin.js文件

贴一下代码:

const fs = require('fs')
const path = require('path')
const axios = require('axios')

class SourceMapUploaderPlugin {
  constructor(options) {
    this.options = options
  }
  async apply (compiler) {
      compiler.hooks.done.tapPromise('SourceMapUploaderPlugin', async ({ compilation }) => {
      // assets包含了当前编译过程中产生的所有资源文件的信息。
      const { assets } = compilation
      // 拿到当前输出目录的绝对路径
      const outputPath = compilation.getPath(compilation.outputOptions.path, {})
      this.upload(assets, outputPath)
    })
  }

  /**
   * @description: source-map文件上传
   * @param {*} assets
   * @param {*} outputPath
   * @return {*}
   */
  async upload (assets, outputPath) {
    const mapsArr = []
    for (const fileName in assets) {
      // 获取打包后的所有 source-map文件
      if (fileName.endsWith('.js.map')) {
        const filePath = path.join(outputPath, fileName)
        const fileContent = fs.readFileSync(filePath, 'utf8')
        mapsArr.push({
          fileName: fileName.replace('maps', ''), // 替换掉打包时输出目录的 maps
          fileContent
        })
      }
    }
    try {
      // 上传sourcemap
      const result = await axios.post(this.options.uploadURL, {
        mapsArr
      })
    } catch (error) {
      console.error(`错误: ${error}`)
    }
  }

}

module.exports = SourceMapUploaderPlugin

使用自定义插件

此时我们直接在配置文件中引入上面实现的插件来使用:

const SourceMapUploaderPlugin = require('../plugins/sourceMap-uploader-plugin')

 plugins: [
      // 上传SourceMap文件
      new SourceMapUploaderPlugin({
        uploadURL: 'xxx/upload'
      }),
     ]

此时,打包时就会将 sourceMap 文件上传到服务端(前提是上传接口已实现!)

结束

截止目前我们算初步完成了前端部分主要工作,细节内容还需要进一步完善。接下来主要内容就是要在服务端实现上传、上报接口、对错误信息进行解析、保存信息到数据库、错误信息回显。

由于时间原因,后续内容下篇文章继续完善,欢迎大家评论区讨论交流。

如果文章对你有帮助,可以点赞、评论、收藏、转发互动支持哈😀😀😀
点击链接 学习交流群(前端微信群) 加vx拉你进 前端学习交流群 让我们一起 好好学习(🐟🐟🐟)吧😎😎😎