首页/文章/前端监控

前端小玩具~错误捕获定位工具

2024-10-12
24539 分钟
...

正文

我们在平时的工作中,开发功能的同时不可能把场景考虑的面面俱到,而生产环境往往情况是非常复杂的,用户录入进去的数据总是千奇百怪,那如果遇到问题的话,我们又要如何进行排查呢?总不可能让用户录个屏吧哈哈~所以我们就出现了前端埋点的操作,不过埋点的方向以及文章都挺多的,也都挺复杂的,这篇文章我们就讲一个比较有趣的错误捕获思路

我们平时在使用框架开发遇到bug时,比如Vue,如果是在本地环境,我们在控制台可以很容易的找到出现问题的文件,甚至点击进入即可直接定位到我们的文件中对应报错的位置,这样排查问题就比较方便。而在生产环境,我们可以配置sourcemap,就也能比较方便的定位到问题出现的地方。但这样的话就会出现一个问题,首先上传到服务器的包体积就会因为生成了很多map文件而变得很大,其次我们的网站代码会非常容易暴露甚至是直接被调试,而且这样子也仅仅是我们自测的时候去发现问题,无法监测到用户端到底是做了什么操作才出现的问题。

那么,有没有一个方法是可以监控到客户端用户操作时,出现问题的代码位置呢?

思考:

综上,我们这次要做的这个工具的目的就比较明确了:

  • 错误捕获
  • 错误分析/错误定位
  • 错误收集/日志输出

前置

在错误捕获之前,我们先提前了解一个服务端的库——source-map

使用source-map库,我们可以通过向该库暴露出的方法中传入bug出现的文件对应的map文件,以及错误的行数列数,通过对应的方法解析后,我们可以得到该错误出现的源文件以及具体在源文件中的定位

至此,我们明确了错误捕获中,我们主要就是想拿四个信息:

  • 错误的message信息
  • 错误出现的文件名
  • 错误行数
  • 错误列数

那么,我们可不可以设计这样一个流程呢?

  • 1.在配置文件中将sourcemap的配置打开,从而使得项目打包后会生成map文件
  • 2.通过编写webpack插件,监听webpack打包完成钩子,在打包完成后触发,将生成的map文件自动上传到我们的服务器上。
  • 3.然后在前端,通过错误捕获,将报错信息传给我们的服务器,由服务器根据报错信息再结合map文件,最终解析出我们的报错行数,同时形成日志输出出来并记录下来。

这样的话,我们就可以非常方便的捕获错误,监控生产问题,同时也实现了一个简单的webpack插件(又可以拿去和面试官吹水了~)。

错误捕获

onerror

前端的错误捕获我们最常见的当然是window.onerror了,我们可以通过定义window.onerror函数来对全局错误进行捕获。

window.onerror = function(message, source, lineno, colno) {
  console.log(message)
  console.log(source)
  console.log(lineno)
  console.log(colno)
}

通过window.onerror我们很容易可以拿到我们想要的具体信息。

errorHandler

window.onerror并不能捕获到框架组件生命周期的错误,所以我们可以再补充一个框架的错误捕获,以Vue为例:

...
const app = createApp(App)
app.use(store).use(router).mount('#app')

app.config.errorHandler = function (err, vm, info) {
  console.log(err)
  console.log(vm)
  console.log(info)
};

我们在errorHandler事件中,可以拿到错误对象errvue实例错误信息。这里我们并不能像上面onerror错误捕获一样很方便的取出出错的行数列数,但我们能够拿到一个完整的错误堆栈对象,那么我们就可以对错误对象的堆栈信息进行处理,提取出我们想要的行数列数

这里用到了一个堆栈解析工具——StackTrace-Parser

npm install stacktrace-parser
app.config.errorHandler = function (err, vm, info) {
    const errInfo = stackTraceParser.parse(err.stack)[0]
    const message = err.message 
    const lineno = errInfo.lineNumber 
    const colno = errInfo.column 
    const source = errInfo.file 
    ...
  };
补充

错误捕获还有一个onunhandledrejection的事件,用于捕获Promise类型的错误,但是经过尝试发现不是很好去拿到错误的定位信息,同时,考虑到一般Promise我们会使用catch去处理异常的操作,所以这里就暂时不处理这个类型的错误事件了。

至此,我们的捕获相关的逻辑已经完成,剩下的就是如何设计服务端,如何将这些信息传递给服务端并完成解析了。

错误分析/错误定位

服务端,我们设计两个接口,一个用于上传map文件(upload),一个用于接收错误信息(sendErrorLog)。

上传接口就不多说了,主要就是在前端打包完成之后,服务端接收传过来的map文件。我们主要看一下接收错误信息的接口逻辑。

const handleErrorMessage = require("./utils/index");
...
app.post("/sendErrorLog", (req, res) => {
  handleErrorMessage(req.body);
  res.send("hello");
});
const fs = require("fs");
const { SourceMapConsumer } = require("source-map");
const path = require("path");


const arr = fs.readdirSync(path.resolve(__dirname, "../uploads"));
const sourceMap = {};
for (let i = 0; i < arr.length; i++) {
  fs.readFile(
    path.resolve(__dirname, "../uploads", arr[i]),
    "utf-8",
    function (err, data) {
      if (err) {
        return err;
      }
      sourceMap[arr[i]] = data;
    }
  );
}

module.exports = function handleErrorMessage(message) {
  const errorLine = message.lineno;
  const errorCol = message.colno;
  const jsName = message.source.split("/").pop();
  const sourceName = jsName + ".map";
  
  if (!sourceMap[sourceName]) {
    sourceMap[sourceName] = fs.readFileSync(
      path.resolve(__dirname, "../uploads", sourceName),
      "utf-8"
    );
  }
  SourceMapConsumer.with(sourceMap[sourceName], null, (consumer) => {
    
    const originalPosition = consumer.originalPositionFor({
      line: errorLine,
      column: errorCol,
    });

    console.log("Error occurred at:");
    console.log("file:" + originalPosition.source);
    console.log("line:" + originalPosition.line);
    console.log("column:" + originalPosition.column);
    console.log("message:" + message.message);
  });
};

整体的思路就是:

  • 服务器启动时读取upload文件夹下的所有map文件,将对应文件的内容读取出来
  • sendErrorLog接口被调用后,通过source-map库去解析错误信息
  • 输出错误日志

这里考虑到一般服务器我们都是一直启动的状态,所以在调用解析逻辑之前,先判断souceMap数据是否已经读取出来,如果没有读取出来,再同步去读取,之后再去解析错误信息。

完善前端逻辑

接口已经有了,这里我们再回过头完善一下前端的逻辑

首先,我们根据前面对错误捕获的了解,完成一下错误上传的逻辑,:

import axios from 'axios'
import * as stackTraceParser from 'stacktrace-parser';
...


if (process.env.NODE_ENV == "production") {

  app.config.errorHandler = function (err, vm, info) {
    const errInfo = stackTraceParser.parse(err.stack)[0]
    const message = err.message
    const lineno = errInfo.lineNumber
    const colno = errInfo.column
    const source = errInfo.file
    axios
      .post("http://127.0.0.1:3000/sendErrorLog", {
        message,
        lineno,
        colno,
        source,
      })
      .then((data) => {
        console.log(data);
      });
  };

  
  window.onerror = function(message, source, lineno, colno) {
    axios
        .post("http://127.0.0.1:3000/sendErrorLog", {
          message,
          lineno,
          colno,
          source,
        })
        .then((data) => {
          console.log(data);
        });
  }
}

然后,我们开始实现map文件上传的逻辑。

我们先去找一个webpack打包完成输出文件后的钩子——afterEmit

在这个钩子触发时,说明打包文件已经被输出出来了,我们可以去读取打包文件的js文件夹,从中过滤出map文件,上传至服务器,同时在打包文件中将map文件进行删除操作。

const pluginName = "SendMapWebpackPlugin";
const fs = require("fs");
const axios = require("axios");
const path = require('path')

class SendMapWebpackPlugin {
  apply(compiler) {
    const outputPath = compiler.options.output.path;
    compiler.hooks.afterEmit.tap(pluginName, (compilation) => {
      console.log("webpack 构建");
      console.log(process.env.NODE_ENV);
      if (process.env.NODE_ENV == "production") {
        fs.readdir(outputPath + "/js", function (err, data) {
          if (data) {
            data.forEach((v) => {
              
              if (v.endsWith(".map")) {
                const file = fs.readFileSync(
                  path.resolve(__dirname, "../dist/js", v),
                  "utf-8"
                );
                axios({
                  url: "http://127.0.0.1:3000/upload",
                  method: "post",
                  data: { file, fileName: v },
                  headers: {
                    "Content-Type": "application/octet-stream",
                  },
                })
                  .then((res) => {
                    console.log("success");
                    fs.rm(path.resolve(__dirname, "../dist/js", v), (err) => {
                      if(err) {
                        console.log(err)
                        return
                      }
                      console.log('delete success')
                    })
                  })
                  .catch((err) => {
                    console.log(err);
                  });
              }
            });
          }
        });
      }
    });
  }
}
...

测试效果

逻辑写完了,我们在前端代码中留下一些bug来测试一下效果。

 

然后,我们执行npm run build打包操作。

可以看到我们打包完成后的dist文件夹中,已经没有了map文件

而在服务端,我们接收到了这些map文件

上传map文件逻辑没有问题,接下来,我们看一下错误解析逻辑。

我们可以在本地安装一个serve包,便于我们快捷的以dist文件夹为基础起一个小型服务器

将dist文件夹在终端中打开,执行执行serve -p 8080

点击按钮触发bug,我们可以看到错误已被成功捕获,并将对应的信息通过接口传递给服务端。

 

在服务端的输出中,我们可以看到已对错误进行了解析,错误发生的定位信息已经输出出来了,对照前端文件中错误发生的位置也是没有问题的~

最后附上这个玩具的Demo地址,有兴趣的掘友可以玩一下~

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
前端监控2025-05-29
前端错误收集方式
在讲错误收集方式之前,先理清一下前端编码时有那些常见错误 &nbsp; &nbsp; &nbsp; &nbsp; 前端错误收集是保障用户体验、排查线上问题的关键手段。本文将从多个角度详解前端错误收集的各种方式,包括不同框架(如 React、Vue)、JavaScript 执行环境中的同步/异步/Promise 错误、资源加载错误(如图片、script 加载失败)等。以下是全面的错误捕获机制总结: &nbsp; &nbsp; 🌐 一、通...
学习
前端监控2024-09-19
🍻 前端服务监控原理与手写开源监控框架SDK前端服务监控概述
前端监控的目的很明确,无非就是让我们的产品更完善,更符合我们和用户的需求。运营与产品团队需要关注用户在产品内的行为记录,通过用户的行为记录来优化产品,研发与测试团队则需要关注产品的性能以及异常,确保产品的性能体验以及安全迭代。 而一个完整的前端监控平台至少需要包括三个部分: 数据采集与上报、数据整理和存储、数据展示 。算上需要监控的项目的话,也就是说,至少需要4个项目才能完整的记录前端监控的内容。 下图是一个完整的前端监控平台需要处理和...
学习
前端监控2024-08-19
前端数据监控平台
比如客户说:你们的页面咋白屏了? 开发:具体可以给我说一下,白屏之前你做了什么吗? 客户:我就点了一下按钮 开发:你能告诉我是哪个页面的哪个按钮吗? 最后,客户极其不耐烦的地丢过来一张没有缺少url的白屏给你看,试图证明他说的是真的,真白屏了。 最后不再理睬你! 开发人员试图找到解决方案,想快速解决线上bug,但是客户却以为你在质疑他。 真的有点秀才遇上兵有理说不清,其实真的没有谁对谁错,只是双方立场不同,作为客户,你的系统bug了,我...
学习
前端监控2024-02-20
前端实现埋点&监控
实现埋点功能的意义主要体现在以下几个方面: 0. 数据采集:埋点是数据采集领域(尤其是用户行为数据采集领域)的术语,它针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。通过埋点,可以收集到用户在应用中的所有行为数据,例如页面浏览、按钮点击、表单提交等。 1. 数据分析:采集的数据可以帮助业务人员分析网站或者App的使用情况、用户行为习惯等,是后续建立用户画像、用户行为路径等数据产品的基础。通过数据分析,企业可以更好地了解...
学习
前端监控2023-12-19
前端埋点重要环节:前端如何录制用户的操作过程
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。 背景 公司有很多的项目,但是并不是每一个项目都很重要,其实重要的项目就那么几个,上面也是很重视这几个项目,尤其是对一些生产问题的关注度很高。 这几天上面交代下来了,需要对这些项目做一些用户行为的记录,主要是为了更好地还原用户在某一个时间点的操作过程 注意点 想要完成这个需求,仔细想了一下,需要注意几个点: 跨框架使用: 这些项目有vue、...
学习
前端监控2023-11-23
Web性能监测的利器Performance Observer
前言 前段时间在研究前端异常监控平台,在思考性能监控时,想到了浏览器自带的观察者以及页面生命周期API 。于是在翻查资料时发现了, API。正好趁着这个机会给大家好好讲讲Performance Observer API Performance Observer(性能观察者) 定义 是一种JavaScript API,用于监测页面性能指标,如资源加载时间、页面渲染时间等。它可以触发回调函数,以便你收集和分析页面性能数据。 优点 1. 性能...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录