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

前言

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

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

监听异常信息

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

  • 运行时异常
  • 资源加载异常
  • 未捕获的 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);
});

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

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

最后

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

相关推荐
架构技术专栏3 小时前
Agent 工具调用:从工具声明到执行反馈
前端·人工智能·后端
别动我齐刘海4 小时前
C++ 工程化学习
java·linux·前端·c++·学习·机器学习·机器人
志尊宝5 小时前
Vue3 零基础每日笔记(071):暗黑模式与主题切换——CSS 变量 + Element Plus 暗黑方案
前端·css·笔记·vue·html·前端开发·软件开发
Flynt5 小时前
Deno 官宣一年后停更:我把它的招牌特性在 Node 26 和 Bun 里挨个找了一遍
node.js·deno·bun
刘发财5 小时前
前端如何生成10000页PDF文件?
前端·javascript·开源
A接拉起0076 小时前
2026 年 JavaScript 开发者需要了解的知识
前端·javascript·后端
用户11481867894846 小时前
面试官:大屏项目里十几个模块同时渲染上万条数据,怎么做到不卡?
前端·性能优化
前端市界6 小时前
Pro React Admin: 统一工程底座,承载多种应用...
前端·github
葡萄城技术团队6 小时前
dsh-spreadjs-editor 当前支持什么:安装前需要了解的几件事
前端
三*一7 小时前
优化修整算法
前端·javascript·算法