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

前言

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

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

监听异常信息

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

  • 运行时异常
  • 资源加载异常
  • 未捕获的 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 ,来判断是资源加载异常还是运行时异常。

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

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

如果是资源异常,则上报具体的资源 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);

接口异常

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

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 方法,获取到请求 urlmethod
  • 拓展 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);
});

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

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

最后

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

相关推荐
贩卖纯净水.4 分钟前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家1 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
阿伟来咯~2 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai3 小时前
uniapp
前端·javascript·vue.js·uni-app
帅比九日3 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos