从0到1搭建前端异常监控系统(Vue + Webpack + Node.js + Egg.js + Jest)(下)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 从0到1搭建前端异常监控系统(Vue + Webpack + Node.js + Egg.js + Jest)

4.2 ErrorBoundary标签


错误边界仅可以捕获其子组件的错误。错误边界无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。这也类似于 JavaScript 中 catch {} 的工作机制。


创建ErrorBoundary组件


import React from 'react'; 
export default class ErrorBoundary extends React.Component {
    constructor(props) {
      super(props);
    }
    componentDidCatch(error, info) {
      // 发生异常时打印错误
      console.log('componentDidCatch',error)
    }
    render() {
      return this.props.children;
    }
  }


在src/index.js中包裹App标签


import ErrorBoundary from './ErrorBoundary'
ReactDOM.render(
    <ErrorBoundary>
        <App />
    </ErrorBoundary>
    , document.getElementById('root'));


最终运行的结果


网络异常,图片无法展示
|


5. 跨域代码异常


(待...)


6. IFrame异常


(待...)


上一篇我们主要谈到的JS错误如何收集。这篇我们说说异常如何上报和分析。


三、异常上报


1. 选择通讯方式


1.1 动态创建img标签


其实上报就是要将捕获的异常信息发送到后端。最常用的方式首推动态创建标签方式。因为这种方式无需加载任何通讯库,而且页面是无需刷新的。基本上目前包括百度统计 Google统计都是基于这个原理做的埋点。


new Image().src = 'http://localhost:7001/monitor/error'+ '?info=xxxxxx'


网络异常,图片无法展示
|


通过动态创建一个img,浏览器就会向服务器发送get请求。可以把你需要上报的错误数据放在querystring字符串中,利用这种方式就可以将错误上报到服务器了。


1.2 Ajax上报


实际上我们也可以用ajax的方式上报错误,这和我们再业务程序中并没有什么区别。在这里就不赘述。


2.1 上报哪些数据


网络异常,图片无法展示
|


我们先看一下error事件参数:


属性名称 含义 类型
message 错误信息 string
filename 异常的资源url string
lineno 异常行号 int
colno 异常列号 int
error 错误对象 object
error.message 错误信息 string
error.stack 错误信息 string


其中核心的应该是错误栈,其实我们定位错误最主要的就是错误栈。


错误堆栈中包含了绝大多数调试有关的信息。其中包括了异常位置(行号,列号),异常信息


有兴趣的同学可以看看这篇文章


github.com/dwqs/blog/i…


2.2 上报数据序列化


由于通讯的时候只能以字符串方式传输,我们需要将对象进行序列化处理。


大概分成以下三步:


  • 将异常数据从属性中解构出来存入一个JSON对象


  • 将JSON对象转换为字符串


  • 将字符串转换为Base64


当然在后端也要做对应的反向操作 这个我们后面再说。


window.addEventListener('error', args => {
  console.log(
    'error event:', args
  );
  uploadError(args)
  return true;
}, true);
function uploadError({
    lineno,
    colno,
    error: {
      stack
    },
    timeStamp,
    message,
    filename
  }) {
    // 过滤
    const info = {
      lineno,
      colno,
      stack,
      timeStamp,
      message,
      filename
    }
    // const str = new Buffer(JSON.stringify(info)).toString("base64");
    const str = window.btoa(JSON.stringify(info))
    const host = 'http://localhost:7001/monitor/error'
    new Image().src = `${host}?info=${str}`
}


四、异常收集


异常上报的数据一定是要有一个后端服务接收才可以。


我们就以比较流行的开源框架eggjs为例来演示


1. 搭建eggjs工程


# 全局安装egg-cli
npm i egg-init -g 
# 创建后端项目
egg-init backend --type=simple
cd backend
npm i
# 启动项目
npm run dev


2. 编写error上传接口


首先在app/router.js添加一个新的路由


module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  // 创建一个新的路由
  router.get('/monitor/error', controller.monitor.index);
};


创建一个新的controller (app/controller/monitor)


'use strict';
const Controller = require('egg').Controller;
const { getOriginSource } = require('../utils/sourcemap')
const fs = require('fs')
const path = require('path')
class MonitorController extends Controller {
  async index() {
    const { ctx } = this;
    const { info } = ctx.query
    const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
    console.log('fronterror:', json)
    ctx.body = '';
  }
}
module.exports = MonitorController;


网络异常,图片无法展示
|


看一下接收后的结果


3. 记入日志文件


下一步就是讲错误记入日志。实现的方法可以自己用fs写,也可以借助log4js这样成熟的日志库。


当然在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
    const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
    console.log('fronterror:', json)
    // 记入错误日志
    this.ctx.getLogger('frontendLogger').error(json)
    ctx.body = '';
  }


最后实现的效果


网络异常,图片无法展示
|


五、异常分析


谈到异常分析最重要的工作其实是将webpack混淆压缩的代码还原。


1. Webpack插件实现SourceMap上传


在webpack的打包时会产生sourcemap文件,这个文件需要上传到异常监控服务器。这个功能我们试用webpack插件完成。


1.1 创建webpack插件


/source-map/plugin


const fs = require('fs')
var http = require('http');
class UploadSourceMapWebpackPlugin {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    // 打包结束后执行
    compiler.hooks.done.tap("upload-sourcemap-plugin", status => {
      console.log('webpack runing')
    });
  }
}
module.exports = UploadSourceMapWebpackPlugin;


1.2 加载webpack插件


webpack.config.js


// 自动上传Map
UploadSourceMapWebpackPlugin = require('./plugin/uploadSourceMapWebPackPlugin')
plugins: [
    // 添加自动上传插件
    new UploadSourceMapWebpackPlugin({
      uploadUrl:'http://localhost:7001/monitor/sourcemap',
      apiKey: 'kaikeba'
    })
  ],


1.3 添加读取sourcemap读取逻辑


在apply函数中增加读取sourcemap文件的逻辑


/plugin/uploadSourceMapWebPlugin.js


const glob = require('glob')
const path = require('path')
apply(compiler) {
  console.log('UploadSourceMapWebPackPlugin apply')
  // 定义在打包后执行
  compiler.hooks.done.tap('upload-sourecemap-plugin', async status => {
    // 读取sourcemap文件
    const list = glob.sync(path.join(status.compilation.outputOptions.path, `./**/*.{js.map,}`))
    for (let filename of list) {
      await this.upload(this.options.uploadUrl, filename)
    }
  })
}


1.4 实现http上传功能


upload(url, file) {
  return new Promise(resolve => {
    console.log('uploadMap:', 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()
    });
  })
}


1.5服务器端添加上传接口


/backend/app/router.js


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)
};


添加sourcemap上传接口


/backend/app/controller/monitor.js


async upload() {
    const { ctx } = this
    const stream = ctx.req
    const filename = ctx.query.name
    const dir = path.join(this.config.baseDir, 'uploads')
    // 判断upload目录是否存在
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir)
    }
    const target = path.join(dir, filename)
    const writeStream = fs.createWriteStream(target)
    stream.pipe(writeStream)
}


最终效果:


执行webpack打包时调用插件sourcemap被上传至服务器。


网络异常,图片无法展示
|


2. 解析ErrorStack


考虑到这个功能需要较多逻辑,我们准备把他开发成一个独立的函数并且用Jest来做单元测试


先看一下我们的需求


输入 stack(错误栈) ReferenceError: xxx is not defined\n' + ' at http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js:1:1392'
SourceMap
输出 源码错误栈 { source: 'webpack:///src/index.js', line: 24, column: 4, name: 'xxx' }


搭建Jest框架


网络异常,图片无法展示
|


首先创建一个/utils/stackparser.js文件


module.exports = class StackPaser {
    constructor(sourceMapDir) {
        this.consumers = {}
        this.sourceMapDir = sourceMapDir
    }
}


在同级目录下创建测试文件stackparser.spec.js


以上需求我们用Jest表示就是


const StackParser = require('../stackparser')
const { resolve } = require('path')
const error = {
    stack: 'ReferenceError: xxx is not defined\n' +
        '    at http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js:1:1392',
    message: 'Uncaught ReferenceError: xxx is not defined',
    filename: 'http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js'
}
it('stackparser on-the-fly', async () => {
    const stackParser = new StackParser(__dirname)
    // 断言 
    expect(originStack[0]).toMatchObject(
        {
            source: 'webpack:///src/index.js',
            line: 24,
            column: 4,
            name: 'xxx'
        }
    )
})


整理如下:


下面我们运行Jest


npx jest stackparser --watch


网络异常,图片无法展示
|


显示运行失败,原因很简单因为我们还没有实现对吧。下面我们就实现一下这个方法。


2.1 反序列Error对象


首先创建一个新的Error对象 将错误栈设置到Error中,然后利用error-stack-parser这个npm库来转化为stackFrame


const ErrorStackParser = require('error-stack-parser')
/**
 * 错误堆栈反序列化
 * @param {*} stack 错误堆栈
 */
parseStackTrack(stack, message) {
  const error = new Error(message)
  error.stack = stack
  const stackFrame = ErrorStackParser.parse(error)
  return stackFrame
}


运行效果


网络异常,图片无法展示
|


2.2 解析ErrorStack


下一步我们将错误栈中的代码位置转换为源码位置


const { SourceMapConsumer } = require("source-map");
async getOriginalErrorStack(stackFrame) {
        const origin = []
        for (let v of stackFrame) {
            origin.push(await this.getOriginPosition(v))
        }
        // 销毁所有consumers
        Object.keys(this.consumers).forEach(key => {
            console.log('key:',key)
            this.consumers[key].destroy()
        })
        return origin
    }
    async getOriginPosition(stackFrame) {
        let { columnNumber, lineNumber, fileName } = stackFrame
        fileName = path.basename(fileName)
        console.log('filebasename',fileName)
        // 判断是否存在
        let consumer = this.consumers[fileName]
        if (consumer === undefined) {
            // 读取sourcemap
            const sourceMapPath = path.resolve(this.sourceMapDir, fileName + '.map')
            // 判断目录是否存在
            if(!fs.existsSync(sourceMapPath)){
                return stackFrame
            }
            const content = fs.readFileSync(sourceMapPath, 'utf8')
            consumer = await new SourceMapConsumer(content, null);
            this.consumers[fileName] = consumer
        }
        const parseData = consumer.originalPositionFor({ line:lineNumber, column:columnNumber })
        return parseData
    }


我们用Jest测试一下


it('stackparser on-the-fly', async () => {
    const stackParser = new StackParser(__dirname)
    console.log('Stack:',error.stack)
    const stackFrame = stackParser.parseStackTrack(error.stack, error.message)
    stackFrame.map(v => {
        console.log('stackFrame', v)
    })
    const originStack = await stackParser.getOriginalErrorStack(stackFrame)
    // 断言 
    expect(originStack[0]).toMatchObject(
        {
            source: 'webpack:///src/index.js',
            line: 24,
            column: 4,
            name: 'xxx'
        }
    )
})


网络异常,图片无法展示
|


看一下结果测试通过。


2.3 将源码位置记入日志


async index() {
    console.log
    const { ctx } = this;
    const { info } = ctx.query
    const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
    console.log('fronterror:', json)
    // 转换为源码位置
    const stackParser = new StackParser(path.join(this.config.baseDir, 'uploads'))
    const stackFrame = stackParser.parseStackTrack(json.stack, json.message)
    const originStack = await stackParser.getOriginalErrorStack(stackFrame)
    this.ctx.getLogger('frontendLogger').error(json,originStack)
    ctx.body = '';
  }


运行效果:


网络异常,图片无法展示
|


六、开源框架实战


1. Sentry


Sentry 是一个开源的实时错误追踪系统,可以帮助开发者实时监控并修复异常问题。它主要专注于持续集成、提高效率并且提升用户体验。Sentry 分为服务端和客户端 SDK,前者可以直接使用它家提供的在线服务,也可以本地自行搭建;后者提供了对多种主流语言和框架的支持,包括 React、Angular、Node、Django、RoR、PHP、Laravel、Android、.NET、JAVA 等。同时它可提供了和其他流行服务集成的方案,例如 GitHub、GitLab、bitbuck、heroku、slack、Trello 等。目前公司的项目也都在逐步应用上 Sentry 进行错误日志管理。


2. Fundebug


Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、荔枝FM、掌门1对1、核桃编程、微脉等众多品牌企业。欢迎免费试用!


总结


截止到目前为止,我们把前端异常监控的基本功能算是形成了一个MVP(最小化可行产品)。后面需要升级的还有很多,对错误日志的分析和可视化方面可以使用ELK。发布和部署可以采用Docker。对eggjs的上传和上报最好要增加权限控制功能。

参考代码位置: github.com/su37josephx…

欢迎指正,欢迎Star。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
25天前
|
Web App开发 JavaScript 前端开发
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念,包括事件驱动、单线程模型和模块系统;探讨其安装配置、核心模块使用、实战应用如搭建 Web 服务器、文件操作及实时通信;分析项目结构与开发流程,讨论其优势与挑战,并通过案例展示 Node.js 在实际项目中的应用,旨在帮助开发者更好地掌握这一强大工具。
44 1
|
28天前
|
JavaScript 前端开发 程序员
前端原生Js批量修改页面元素属性的2个方法
原生 Js 的 getElementsByClassName 和 querySelectorAll 都能获取批量的页面元素,但是它们之间有些细微的差别,稍不注意,就很容易弄错!
|
25天前
|
JavaScript 前端开发 Java
springboot解决js前端跨域问题,javascript跨域问题解决
本文介绍了如何在Spring Boot项目中编写Filter过滤器以处理跨域问题,并通过一个示例展示了使用JavaScript进行跨域请求的方法。首先,在Spring Boot应用中添加一个实现了`Filter`接口的类,设置响应头允许所有来源的跨域请求。接着,通过一个简单的HTML页面和jQuery发送AJAX请求到指定URL,验证跨域请求是否成功。文中还提供了请求成功的响应数据样例及请求效果截图。
springboot解决js前端跨域问题,javascript跨域问题解决
|
13天前
|
存储 JavaScript NoSQL
Node.js新作《循序渐进Node.js企业级开发实践》简介
《循序渐进Node.js企业级开发实践》由清华大学出版社出版,基于Node.js 22.3.0编写,包含26个实战案例和43个上机练习,旨在帮助读者从基础到进阶全面掌握Node.js技术,适用于初学者、进阶开发者及全栈工程师。
40 9
|
27天前
|
缓存 JavaScript 前端开发
JavaScript 与 DOM 交互的基础及进阶技巧,涵盖 DOM 获取、修改、创建、删除元素的方法,事件处理,性能优化及与其他前端技术的结合,助你构建动态交互的网页应用
本文深入讲解了 JavaScript 与 DOM 交互的基础及进阶技巧,涵盖 DOM 获取、修改、创建、删除元素的方法,事件处理,性能优化及与其他前端技术的结合,助你构建动态交互的网页应用。
40 5
|
25天前
|
缓存 前端开发 JavaScript
JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式
本文深入解析了JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式(Hash路由和History路由)、优点及挑战,并通过实际案例分析,帮助开发者更好地理解和应用这一关键技术,提升用户体验。
63 1
|
29天前
|
JSON 前端开发 JavaScript
聊聊 Go 语言中的 JSON 序列化与 js 前端交互类型失真问题
在Web开发中,后端与前端的数据交换常使用JSON格式,但JavaScript的数字类型仅能安全处理-2^53到2^53间的整数,超出此范围会导致精度丢失。本文通过Go语言的`encoding/json`包,介绍如何通过将大整数以字符串形式序列化和反序列化,有效解决这一问题,确保前后端数据交换的准确性。
35 4
|
1月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
111 1
|
1月前
|
移动开发 前端开发 JavaScript
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
于辰在大学期间带领团队参考网易游戏官网的部分游戏页面,开发了一系列前端实训作品。项目包括首页、2021校园招聘页面和明日之后游戏页面,涉及多种特效实现,如动态图片切换和人物聚合效果。作品源码已上传至CSDN,视频效果可在CSDN预览。
40 0
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
|
1月前
|
JavaScript 前端开发 中间件
JS服务端技术—Node.js知识点
本文介绍了Node.js中的几个重要模块,包括NPM、Buffer、fs模块、path模块、express模块、http模块以及mysql模块。每部分不仅提供了基础概念,还推荐了相关博文供深入学习。特别强调了express模块的使用,包括响应相关函数、中间件、Router和请求体数据解析等内容。文章还讨论了静态资源无法访问的问题及其解决方案,并总结了一些通用设置。适合Node.js初学者参考学习。
39 1
下一篇
DataWorks