🚀前端错误异常监控实战🚀

前言

在上一篇文章------ 🚀前端监控全链路解析+全栈实现🚀 中,我们提到了日志+监控的全链路实现。这篇文章一起再来讨论一下异常监控的时间,这对于我们排查生产问题来说十分有效。

之前的文章已经介绍过如何上报以及如何查询上报后的异常,这里就不再赘述,主要讨论的是异常的采集方式。

监听异常信息

由于语法错误我们一般在编译的时候就能发现,所以我们收集以下几种异常并上报:

  • 运行时异常
  • 资源加载异常
  • 未捕获的 Promise
  • 接口异常

运行时异常+资源加载异常

先看代码:

js 复制代码
  handleResourceError = (event) => {
    const errorInfo = {
      tagName: event.target.tagName,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
      targetUrl: event.target.src || event.target.href,
    };
    this.log(ERROR_TYPE.RESOURCE, errorInfo);
  };

  handleRuntimeError = (event) => {
    const errorInfo = {
      message: event.message,
      source: event.filename,
      lineno: event.lineno,
      colno: event.colno,
      error: event.error ? event.error.stack : null,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
    };
    this.log(ERROR_TYPE.RUNTIME, errorInfo);
  };

  handleError = (event) => {
    if (event.target && (event.target.src || event.target.href)) {
      // 资源加载错误
      this.handleResourceError(event);
    } else {
      // 运行时错误
      this.handleRuntimeError(event);
    }
  };

handleError 函数用于捕获运行时异常,通过判断异常事件的 target ,来判断是资源加载异常还是运行时异常。

因为我们加载资源总是通过 img 、 link 、 script 、 a 标签等,而路径属性一般都是src 或者 href 。

所以可以通过这种方式来区分,或者也可以通过 traget 的 tagName 属性来区分。

如果是资源异常,则上报具体的资源 url ;如果是运行时异常,则上报文件名、行号、列号,以及堆栈信息,后续我们会用到这种信息配合 SourceMap 来模拟定位生产问题。

最后通过以下的方式监听异常事件:

js 复制代码
window.addEventListener("error", this.handleError, true);

未捕获的promise

同样先看代码:

js 复制代码
  handlePromiseRejection = (event) => {
    const errorInfo = {
      message: event.reason,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
    };
    this.log(ERROR_TYPE.PROMISE, errorInfo);
  };

当捕获到未处理的 Promise reject 时,会触发 unhandledrejection 事件,这个时候就可以捕获到这个异常。 event.reason 就是 reject() 中传入的参数,即错误原因。

通过以下的方式来监听异常事件:

js 复制代码
window.addEventListener("unhandledrejection", this.handlePromiseRejection);

接口异常

接口异常我们可以通过拦截 AJAX 和 Fetch 来实现。具体的拦截方式如下:

AJAX

js 复制代码
  initpAjaxInterceptor = () => {
    const open = XMLHttpRequest.prototype.open;
    const send = XMLHttpRequest.prototype.send;
    const setRequestHeader = XMLHttpRequest.prototype.setRequestHeader;

    XMLHttpRequest.prototype.open = function (method, url) {
      this._method = method;
      this._url = url;
      this._requestHeaders = {};
      open.apply(this, arguments);
    };

    XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
      this._requestHeaders[header] = value; //
      setRequestHeader.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function (body) {
      this.addEventListener("load", () => {
        if (this.status >= 400) {
          const errorData = {
            method: this._method,
            url: this._url,
            requestHeaders: this._requestHeaders,
            requestBody: body,
            status: this.status,
            statusText: this.statusText,
            responseText: this.responseText,
          };
          console.log("errorData", errorData);
        }
      });
      send.apply(this, arguments);
    };
  };

解释一下上面的代码:

  • 拓展 open 方法,获取到请求 url 和 method
  • 拓展 setRequestHeader 方法,获取到请求头
  • 拓展 send 方法,获取到请求体

在请求结束后查看状态码,如果状态异常,则上报异常数据。

Fetch

js 复制代码
  initFetchInterceptor = () => {
    const originalFetch = window.fetch;

    window.fetch = async function (input, init) {
      const method = (init && init.method) || "GET";
      const url = typeof input === "string" ? input : input.url;
      const headers = (init && init.headers) || {};
      const body = (init && init.body) || null;

      try {
        const response = await originalFetch(input, init);
        if (!response.ok) {
          const errorData = {
            method: method,
            url: url,
            requestHeaders: headers,
            requestBody: body,
            status: response.status,
            statusText: response.statusText,
            responseText: await response.text(),
          };
          console.log("errorData", errorData);
        }

        return response;
      } catch (error) {
        const errorData = {
          method: method,
          url: url,
          requestHeaders: headers,
          requestBody: body,
          errorMessage: error.message,
        };

        console.log("errorData", errorData);
        throw error; // 重新抛出错误
      }
    };
  };

对于 Fetch 请求的拦截也是大同小异,甚至更好处理一些,因为它的请求方式、请求头、请求体都是放在一个对象里面,我们可以更轻易的获取。

SourceMap

收集到了异常,特别是运行时异常之后,如何快速定位?我们生产上的代码都是打包混淆过的,要怎么定位到源代码? 这个时候就需要用到 SourceMap 了。

它可以将编译后的、混淆或压缩的代码映射回原始的源代码,方便我们调试与定位问题。

在 vite 中,主要可以通过如下方式配置 sourcemap :

build.sourcemap :此选项用于配置在生产构建时是否生成 source map 。可以设置为以下几种值:

  • false:不生成 source map。
  • true:生成 source map 文件。
  • 'inline':生成内联的 source map ,将 source map 内容嵌入到生成的 JavaScript 文件中。
  • 'hidden':生成 source map 文件,但不在生成的 JavaScript 文件中引用 source map 文件。

以上是更详细的解释:

  1. true:

    • 含义:生成单独的 source map 文件。
    • 用途 :当你在生产环境中使用这个选项时,Vite 会为每个生成的 JavaScript 文件创建一个对应的 .map 文件,这样在调试时可以使用浏览器的开发者工具查看源代码的原始版本。对于需要进行错误监控和调试的生产应用,这是一个非常有用的设置。
  2. 'inline' :

    • 含义 :将 source map 内容嵌入到生成的 JavaScript 文件中,而不是生成单独的 .map 文件。
    • 用途:这种方式可以减少文件数量,使得在某些情况下(例如在开发时或小型项目中)更容易使用,因为不需要加载额外的 source map 文件。它会将所有的源代码信息直接放在生成的 JavaScript 文件中。缺点是文件大小会增加,因为 source map 数据会直接包含在 JavaScript 文件中。
  3. 'hidden' :

    • 含义:生成 source map 文件,但在生成的 JavaScript 文件中不引用这个文件。
    • 用途:这种方式适用于你想要保留 source map 以便于调试或错误监控,但不希望在生产环境中暴露源代码信息。浏览器在加载生成的 JavaScript 文件时不会尝试加载对应的 source map 文件,但该文件仍然存在,可以在需要时手动使用。这在某些情况下能够保护源代码,同时保持调试的灵活性。

使用

首先我们这里有一个运行时报错

点堆栈进去如下

其实看的不是太清楚,不好定位,接入监控之后,有如下的信息:

首先我们已经捕捉到了一个报错,在这些错误信息上报以及sourceMap文件上报之后,我们开始利用这些信息来快速定位问题。

根据 source 字段可以确定,我们需要找的是 index-Z0uUDqvI.js.map 这个 soucemap 文件。

可以写一个这样的脚本:

js 复制代码
const fs = require("fs");
const { SourceMapConsumer } = require("source-map");
const path = require("path");

// 读取 source map 文件
const rawSourceMap = fs.readFileSync(
  path.resolve(__dirname, "./dist/assets/index-Z0uUDqvI.js.map"),
  "utf8"
);

// 解析 source map 文件
SourceMapConsumer.with(rawSourceMap, null, (consumer) => {
  const generatedLine = 187;
  const generatedColumn = 35258;

  const pos = consumer.originalPositionFor({
    line: generatedLine,
    column: generatedColumn,
  });

  console.log("Source:", pos.source);
  console.log("Line:", pos.line);
  console.log("Column:", pos.column);
});

然后执行一下,看看结果:

可以快速的定位到出错的地方:

最后

以上就是本文的全部内容,如果你觉得有意思的话,点点关注点点赞吧~

相关推荐
hhwyqwqhhwy26 分钟前
设备树之 provider 和 consumer
java·linux·前端
垆边人似月.2 小时前
Python中如何使用get函数实现频次查询
服务器·前端·python
moshuying2 小时前
DeepSeek开源生产级推理框架DeepInfer:单集群一天处理155万亿token,这意味着什么?
前端
子兮曰3 小时前
PHP漏洞危机,你的网站升级了吗?
前端·后端·php
jerrywus3 小时前
给 Pi 装上生图插件:从登录报错到画出一只 200×200 的猫
前端·agent·claude
陈随易3 小时前
搞钱点子之生态鱼缸
前端·后端·程序员
PBitW3 小时前
从空目录到上线:我全程用 TRAE 做了一款微信小游戏合集(含一次完整架构重构)
前端·trae
不爱学英文的码字机器3 小时前
从Q1迁到T1,同一个编号已经换了含义
前端·数据库·microsoft
云和数据.ChenGuang3 小时前
光哥说Python 循环导入(Circular Import)深度解析:成因、排查与解决方案
开发语言·前端·人工智能·python·langchain
avi91113 小时前
【AI教人】在Inspector显示所有Delegate-Odin Inspector之三
java·前端·算法·delegate·inspector·editor扩展·unity editor