最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 前端异常捕获上报

    正文概述 掘金(宇小墨)   2021-04-04   689

    使用 try...catch

    const func = () => {
      console.log('fun start')
      err
      console.log('fun end')
    }
    
    try {
      func()
    } catch (err) {
     console.log('err', err)
    }
    
    

    预览代码:trycatch捕获异常

    • 缺点:无法捕获 异步 错误
    const func = () => {
      console.log('fun start')
      err
      console.log('fun end')
    }
    
    try {
      setTimeout(() => {    
        func()
      })
    } catch (err) {
     console.log('err', err)
    }
    

    示例

    那应该怎么捕获异步的错误呢?

    window.onerror 捕获异步错误

    const func = () => {
      console.log('fun start')
      err
      console.log('fun end')
    }
    
    setTimeout(() => {    
        func()
      })
    
    window.onerror = (...args) => {
      console.log('args:', args)
    }
    

    示例

    这里我们可以发先,使用 window.onerror 捕获到了我们的异步错误。

    但是,它可以捕获到所有类型的错误吗?

    比如:资源加载地址错误?

    <img src="//xxsdfsdx.jpg" >
        
    window.onerror = (...args) => {
      console.log('args:', args)
    }
    

    示例:资源加载错误

    此时,我们看到该资源地址错误没有被 打印出来,那么我们该怎么捕获这种类型错误呢?

    window.addEventListener('error)

    资源地址错误怎么捕获?

    
    <img src="/xxx.png" />
    
     window.addEventListener('error', (event) => {
       console.log('event err:', event)
     }, true) // 第三个参数为 true ,选择捕获的方式监听
    

    示例:资源加载错误捕获

    promise 怎么捕获?

    • 使用 try...catch 无法捕获
    const asyncFunc = () => {
      return new Promise((res) => {
        err
      })
    }
    
    try {
        asyncFunc()
      } catch(e) {
        console.log('err:', e)
      }
    

    示例:try-catch无法捕获promise

    • 使用 addEventListener('unhandledrejection')
    const asyncFunc = () => {
      return new Promise((res) => {
        err
      })
    }
    
    asyncFunc()
    
    
     window.addEventListener('unhandledrejection', (event) => {
       console.log('event err:', event)
     })
    
    • promise 捕获

    问题:能否使用一个捕获方式捕获所有的错误?

    const asyncFunc = () => {
      return new Promise((res) => {
        err
      })
    }
    
    asyncFunc()
    
    // 主动抛出捕获到的 promise 类型的错误
     window.addEventListener('unhandledrejection', (event) => {
       throw event.reason
     })
     
     window.addEventListener('error', (err) => {
       console.log('err:', err)
     }, true)
    

    示例

    小结

    异常类型同步方法异步方法资源加载Promiseasync / await
    try/catchyyonerroryyaddEventListener('error')yyyaddEventListener('unhandledrejection')yy

    异常上报服务器

    动态创建 img 标签

    • 错误监听和上报代码
    // 上报错误
    function uploadError({lineno, colno, error: { stack }, message, filename }) {
        console.log('uploadError---', event)
        // 整理我们要的错误信息
        const errorInfo = {
            lineno,
            colno,
            stack,
            message,
            filename
        }
        // 错误信息序列化后使用 base64 编码,避免出现特殊字符导致的错误
        const str = window.btoa(JSON.stringify(errorInfo))
        
        // 创建图片,使用图片给错误收集的后端服务器发送一个 get 请求,
        // 上传的信息:错误资源,错误时间
        new Image().src = `http://localhost:7001/monitor/error?info=${str}`
    }
    
    window.addEventListener('unhandledrejection', (event) => {
      // 再次主动抛出
       throw event.reason
     })
    
    window.addEventListener('error', (err) => {
      	console.log('error:', err)
        // 上报错误
        uploadError(err)
    })
    
    • 后端收集错误

    • 搭建 eggjs 工程,具体参考 Egg.js官网

      npm i egg-init -g
      
      egg-init backend --type=simple
      
      cd backend
      
      npm i
      
      npm run dev
      
      • 编写 error 上传接口——添加路由
      // public/router.js
      
      module.exports = app => {
        const { router, controller } = app;
        router.get('/', controller.home.index);
        router.get('/monitor/error', controller.monitor.index)
      };
      
      • 编写 error 上传接口——编写接口,这里使用到了 Buffer-Nodejs
      // app/controller/monitor.js
      
      'use strict';
      
      const Controller = require('egg').Controller;
      
      class MonitorController extends Controller {
        async index() {
          const { ctx } = this;
          const { info } = ctx.query
          // Buffer 接受一个 base64 编码的数据
          const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
          console.log('error-info', json)
          ctx.body = 'hi, json';
        }
      }
      
      module.exports = MonitorController;
      
      
      • 编写 error 上传接口——测试
      const info = window.btoa(JSON.stringify({test: 'err'})) // "eyJ0ZXN0IjoiZXJyIn0="
      
      // rest-client 测试接口测试
      GET http://localhost:7001/monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=
      
      // 得到 log 结果:error-info { test: 'err' }
      

    eggjs 记入错误日志

    方式:

    • 可以使用 fs 写入文件进行记录
    • 也可以使用 log4j 这种成熟的日志库

    当然,在 eggjs 中是支持我们 自定义日志 的,那么我们使用这个功能定制一个前端错误日志就可以了。

    • /config/config.default.js 文件中
    config.customLogger = {
        frontendLogger: {
          file: path.join(appInfo.root, 'logs/frontend.log')
        }
      }
    
    • app/controller/monitor.js 文件进行日志收集
    async index() {
        const { ctx } = this;
        const { info } = ctx.query
        // Buffer 接受一个 base64 编码的数据
        const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
        console.log('error-info', json)
        // 写入日志
        this.ctx.getLogger('frontendLogger').error(json)
        ctx.body = 'hi, json';
      }
    
    • 测试
    // rest-client 测试
    GET http://localhost:7001/monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=
    
    • 结果:查看 /logs/frontend.log 文件中,有具体的日志信息
    2021-04-03 11:58:48,543 ERROR 2180 [-/127.0.0.1/-/4ms GET /monitor/error?info=eyJ0ZXN0IjoiZXJyIn0=] { test: 'err' }
    

    Vue项目中异常如何采集

    • 初始化 vue 项目
    npm i @vue/cli -g
    
    vue create vue-app
    
    cd vue-app
    
    yarn install
    
    yarn serve
    
    • 编写代码,制造 error
    // src/components/HelloWorld.vue
    
    // ... 省略部分代码
    export default {
        name: 'HelloWorld',
        props: {
            msg: String
        },
        mounted() {
            // methods 中没有定义方法 abc,报错 error
            abc()
        }
    }
    
    • 关闭 eslint ,减少影响,让前端服务能跑起来,新建/编辑 vue.config.js
    // /vue.config.js
    
    module.exports = {
        // close eslint setting
        devServer: {
            overlay: {
        		warning: true,
                errors: true
            }
        },
        lintOnSave: false
    }
    
    • 捕获错误
    // src/main.js
    
    // 在 vue 里面统一使用这个 方式捕获错误
    Vue.config.errorHandler = (err, vm, info) => {
        console.log('errHandler:', err)
        uploadError(err)
    }
    
    function uploadError({ message, stack }) {
      console.log('uploadError---')
      // 整理我们要的错误信息
      const errorInfo = {
          stack,
          message,
      }
      // 错误信息序列化后使用 base64 编码,避免出现特殊字符导致的错误
      const str = window.btoa(JSON.stringify(errorInfo))
      
      // 创建图片,使用图片给错误收集的后端服务器发送一个 get 请求,
      // 上传的信息:错误资源,错误时间
      new Image().src = `http://localhost:7001/monitor/error?info=${str}`
    }
    
    new Vue({
        render: h => h(App)
    }).$mounted('#app')
    
    • 打包 vue 项目,运行测试判断是否捕获错误
    yarn build
    
    cd dist
    
    hs
    
    • 删除 dist 目录中的 sourcemap 映射文件,此时报错定位代码就不是源代码了,而是压缩后的代码,不美观

    因为打包后的代码 js 文件主要有 2 种

    app.xxx.js
    app.xxx.js.map
    

    我们可以看看 .map 文件的内容结构:

    {
      "version": 3,
      "sources": [
        "webpack:///webpack/bootstrap",
        "webpack:///./src/App.vue",
        "webpack:///./src/components/HelloWorld.vue",
        "webpack:///./src/components/HelloWorld.vue?354f",
        "webpack:///./src/App.vue?eabf",
        "webpack:///./src/main.js",
        "webpack:///./src/assets/logo.png",
        "webpack:///./src/App.vue?7d22"
      ],
     "names": [
        "webpackJsonpCallback",
        "data",
        //...
      ],
     "mappings": "aACE,SAASA,EAAqBC...",
     "file": "js/app.9a4488cf.js",
     "sourcesContent": [" \t// install a JSONP callback..."],
     "sourceRoot": ""
    }
    

    主要包含了这些东西:

    • version Source map的版本,目前为3
    • soruces 转换后的文件名
    • names 转换前的所有变量名和属性名
    • mappings 记录位置信息的字符串
    • file 转换后的文件名
    • sourcesContent 源内容列表(可选,和源文件列表顺序一致)
    • sourceRoot 源文件根目录(可选)

    关于 source map 可以参考这 2 篇文章 source-map-阮一峰 和 Source Map 原理及源码探索 - Jooger的文章 - 知乎

    后面,我们将从 app.xxx.js.map 中进行解析,还原错误代码

    sourcemap 上传插件

    编写一个 UploadSourceMapWebpackPlugin 插件,用于每次打包代码的时候自动上传到服务器指定目录

    • 编写 webpack plugin
    // frontend/plugin/uploadSourceMapWebpackPlugin.js
    
    class UploadSourceMapWebpackPlugin {
      constructor(options) {
        this.options = options
      }
      apply(compiler) {
        console.log('UploadSourceMapWebpackPlugin apply')
      }
    }
    
    module.exports = UploadSourceMapWebpackPlugin
    
    • 配置 插件
    // /vue.config.js 
    // refer:https://cli.vuejs.org/zh/config/#configurewebpack
    const UploadSourceMapWebpackPlugin = require('./plugin/uploadSourceMapWebpackPlugin')
    
    module.exports = {
      configureWebpack: {
        plugins:[
          new UploadSourceMapWebpackPlugin({
            uploadUrl: 'http://localhost:7001/monitor/sourcemap'
          })
        ]
      },
      // close eslint setting
      devServer: {
        overlay: {
          warning: true,
          errors: true
        }
      },
      lintOnSave: false
    }
    
    • 打包测试
    yarn build
    
    # 此时,我们可以看到命令行中的 log
    Building for production...UploadSourceMapWebpackPlugin apply
    

    接下来,完成 UploadSourceMapWebpackPlugin 插件的详细功能

    const path = require('path')
    const glob = require('glob')
    const fs = require('fs')
    const http = require('http')
    
    
    class UploadSourceMapWebpackPlugin {
      constructor(options) {
        this.options = options
      }
      apply(compiler) {
        console.log('UploadSourceMapWebpackPlugin apply')
        // 定义在打包后执行
        compiler.hooks.done.tap('UploadSourceMapWebpackPlugin', async status => {
          // 读取 sourceMap 文件
          const list = glob.sync(path.join(status.compilation.outputOptions.path, `./**/*.{js.map,}`))
          console.log('list', list)
          // list [
          //   '/mnt/d/Desktop/err-catch-demo/vue-app/dist/js/app.d15f69c0.js.map',
          //   '/mnt/d/Desktop/err-catch-demo/vue-app/dist/js/chunk-vendors.f3b66fea.js.map'
          // ]
          for (let filename of list) {
            await this.upload(this.options.uploadUrl, filename)
          }
        })
    
      }
      upload(url, file) {
        return new Promise(resolve => {
          console.log('upload Map: ', file)
    
          const req = http.request(`${url}?name=${path.basename(file)}`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/octet-stream',
              Connection: 'keep-alive',
              'Transfer-Encoding': 'chunked'
            }
          });
          fs.createReadStream(file).on('data', (chunk) => {
            req.write(chunk)
          }).on('end', () => {
            req.end()
            resolve()
          })
        })
      }
    }
    
    module.exports = UploadSourceMapWebpackPlugin
    

    作用:

    在每一次 build done 的时候:

    • 读取 sourceMap 文件
    • 将读取的 sourceMap 文件上传到指定服务器中

    Eggjs 服务器 sourceMap 上传接口

    • 新增后端路由
    'use strict';
    
    /**
     * @param {Egg.Application} app - egg application
     */
    module.exports = app => {
      const { router, controller } = app;
      router.get('/', controller.home.index);
      router.get('/monitor/error', controller.monitor.index)
      + router.post('/monitor/sourcemap', controller.monitor.upload)
    };
    
    
    • 新增接口,文件信息写入
    'use strict';
    
    const Controller = require('egg').Controller;
    const path = require('path')
    const fs = require('fs')
    class MonitorController extends Controller {
      
      // ...
      async upload() {
        const { ctx } = this
        // 拿到的是一个 流
        const stream = ctx.req
        const filename = ctx.query.name
        const dir = path.join(this.config.baseDir, 'upload')
        // 判断 upload 是否存在
        if (!fs.existsSync(dir)) {
          fs.mkdirSync(dir)
        }
        const target = path.join(dir, filename)
        // 创建写入流写入信息
        console.log('writeFile====', target);
        const writeStream = fs.createWriteStream(target)
        stream.pipe(writeStream)
      }
    }
    
    module.exports = MonitorController;
    
    
    • 关闭 csrf
    config.security = {
        // 可能存在 scrf 风险,这里设置关闭
        csrf: {
          enable: false
        }
      }
    
    • 测试
    yarn build
    
    # egg-server log info
    writeFile==== D:\Desktop\err-catch-demo\backend\upload\app.d15f69c0.js.map
    writeFile==== D:\Desktop\err-catch-demo\backend\upload\chunk-vendors.f3b66fea.js.map
    

    Stack 解析函数

    编写测试用例:

    • 解析 error.stack 信息
    // /app/utils/stackparser.js
    
    
    const ErrorStackParser = require('error-stack-parser')
    const { SourceMapConsumer } = require('source-map')
    const path = require('path')
    const fs = require('fs')
    
    module.exports = class StackParser {
      constructor() {
        this.sourceMapDir = sourceMapDir
        this.consumers = {}
      }
    
      parseStackTrack(stack, message) {
        const error = new Error(message)
        error.stack = stack
        const stackFrame = ErrorStackParser.parse(error)
        return stackFrame
      }
    
      async getOriginalErrorStack(stackFrame) {
        const origin = []
        for (let v of stackFrame) {
          origin.push( await this.getOriginPosition(v))
        }
        return origin
      }
    
      // 从 sourceMap 文件读取错误信息
      async getOriginPosition(stackFrame) {
        let { columnNumber, lineNumber, fileName } = stackFrame
        fileName = path.basename(fileName)
        // 判断 consumers 是否存在
        let consumer = this.consumers[fileName]
        if (!consumer) {
          // 读取 sourceMap
          const sourceMapPath = path.resolve(this.sourceMapDir, filename + '.map')
          // 判断文件是否存在
          if (!fs.existSync(sourceMapPath)) {
            // 不存在则返回源文件
            return stackFrame
          }
          const content = fs.readFileSync(sourceMapPath, 'utf-8')
          consumer = await new SourceMapConsumer(content, null)
          this.consumers[filename] = consumer
        }
    
        const parseData = consumer.originalPositionFor({line: lineNumber, columnNumber})
        return parseData
    
      }
    
    }
    
    • 测试用例:
    // 如何通过sourcemap手工还原错误具体信息? https://www.zhihu.com/question/285449738
    // /app/utils/stackparser.spec.js
    
    const StackParser = require('../stackparser')
    
    const { resolve } = require('path')
    const { hasUncaughtExceptionCaptureCallback } = require('process')
    
    const error = {
      stack: '',
      message: '',
      filename: '',
    }
    
    it('stackparser-on-the-fly', async () => {
      const stackParser = new StackParser(__dirname)
      console.log('Stack:', error.stack)
      const stackFrame = stackParser.parseStackTrack(error.stack, error)
      stackFrame.map(v => {
        console.log('stackFrame: ', v)
      })
    
      const originStack = await stackParser.getOriginalErrorStack(stackFrame)
    
      console.log('originStack', originStack)
    
      // 断言,需要手动修改下面的断言信息
      expect(originStack[0]).toMathchObject({
        source: 'webpack:///src/index.js',
        line: 24,
        column: 4,
        name: 'xxx'
      })
    })
    

    参考资料:

    • source-map-阮一峰
    • Source Map 原理及源码探索 - Jooger的文章 - 知乎
    • 如何通过sourcemap手工还原错误具体信息?
    • webpack plugin
    • source-map/github
    • 前端错误监控以及上报方法总结
    • 如何做前端异常监控?
    • 前端异常监控

    起源地下载网 » 前端异常捕获上报

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元