一文摸清前端监控自研实践(三)错误监控

12,312 阅读12分钟

前言

上篇文章我们分享了关于 用户行为监控 的内容,本文我们接着来看 错误异常监控 的方面

系列文章传送门

一文摸清前端监控实践要点(一)性能监控

一文摸清前端监控实践要点(二)行为监控

一文摸清前端监控实践要点(三)错误监控

腾讯三面:说说前端监控告警分析平台的架构设计和难点亮点?

应用的稳定情况

众所周知,无论进行发布前的单元测试集成测试人工测试进行再多轮,都会难免漏掉一些边缘的测试场景,甚至还有一些奇奇怪怪的玄学故障出现;而出现报错后,轻则某些数据页面无法访问重则导致客户数据出错

这时,一个完善的错误监控体系就派上很大的用场,它可以帮助我们做以下的事情:

  • 应用报错时,及时知晓线上应用出现了错误,及时安排修复止损;
  • 应用报错后,根据上报的用户行为追踪记录数据,迅速进行bug复现;
  • 应用报错后,通过上报的错误行列以及错误信息,找到报错源码并快速修正;
  • 数据采集后,进行分析提供宏观的 错误数、错误率、影响用户数等关键指标;

整体封装

// 错误类型
export enum mechanismType {
  JS = 'js',
  RS = 'resource',
  UJ = 'unhandledrejection',
  HP = 'http',
  CS = 'cors',
  VUE = 'vue',
}

// 格式化后的 异常数据结构体
export interface ExceptionMetrics {
  mechanism: Object;
  value?: string;
  type: string;
  stackTrace?: Object;
  pageInformation?: Object;
  breadcrumbs?: Array<behaviorStack>;
  errorUid: string;
  meta?: any;
}

// 初始化用参
export interface ErrorVitalsInitOptions {
  Vue: any;
}

// 判断是 JS异常、静态资源异常、还是跨域异常
export const getErrorKey = (event: ErrorEvent | Event) => {
  const isJsError = event instanceof ErrorEvent;
  if (!isJsError) return mechanismType.RS;
  return event.message === 'Script error.' ? mechanismType.CS : mechanismType.JS;
};

// 初始化的类
export default class ErrorVitals {
  private engineInstance: EngineInstance;

  // 已上报的错误 uid
  private submitErrorUids: Array<string>;

  constructor(engineInstance: EngineInstance, options: ErrorVitalsInitOptions) {
    const { Vue } = options;
    this.engineInstance = engineInstance;
    this.submitErrorUids = [];
    // 初始化 js错误
    this.initJsError();
    // 初始化 静态资源加载错误
    this.initResourceError();
    // 初始化 Promise异常
    this.initPromiseError();
    // 初始化 HTTP请求异常
    this.initHttpError();
    // 初始化 跨域异常
    this.initCorsError();
    // 初始化 Vue异常
    this.initVueError(Vue);
  }

  // 封装错误的上报入口,上报前,判断错误是否已经发生过
  errorSendHandler = (data: ExceptionMetrics) => {
    // 统一加上 用户行为追踪 和 页面基本信息
    const submitParams = {
      ...data,
      breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),
      pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),
    } as ExceptionMetrics;
    // 判断同一个错误在本次页面访问中是否已经发生过;
    const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);
    // 检查一下错误在本次页面访问中,是否已经产生过
    if (hasSubmitStatus) return;
    this.submitErrorUids.push(submitParams.errorUid);
    // 记录后清除 breadcrumbs
    this.engineInstance.userInstance.breadcrumbs.clear();
    // 一般来说,有报错就立刻上报;
    this.engineInstance.transportInstance.kernelTransportHandler(
      this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),
    );
  };

  // 初始化 JS异常 的数据获取和上报
  initJsError = (): void => {
    //... 详情代码在下
  };

  // 初始化 静态资源异常 的数据获取和上报
  initResourceError = (): void => {
    //... 详情代码在下
  };

  // 初始化 Promise异常 的数据获取和上报
  initPromiseError = (): void => {
    //... 详情代码在下
  };

  // 初始化 HTTP请求异常 的数据获取和上报
  initHttpError = (): void => {
    //... 详情代码在下
  };

  // 初始化 跨域异常 的数据获取和上报
  initCorsError = (): void => {
    //... 详情代码在下
  };

  // 初始化 Vue异常 的数据获取和上报
  initVueError = (app: Vue): void => {
    //... 详情代码在下
  };
}

生成错误 uid

首先,什么叫为每个错误生成 uid,这里生成的 uid 有什么用呢?答案其实很简单:

  • 一次用户访问(页签未关闭),上报过一次错误后,后续产生重复错误不再上报
  • 多个用户产生的同一个错误,在服务端可以归类,分析影响用户数、错误数等指标
  • 需要注意的是,对于同一个原因产生的同一个错误,生成的 uid 是相同的
// 对每一个错误详情,生成一串编码
export const getErrorUid = (input: string) => {
  return window.btoa(unescape(encodeURIComponent(input)));
};

错误堆栈

在做错误监控之前,我们先来了解一下什么是错误堆栈;我们写代码经常报错的时候能够看到,下图这样子类似的错误,一个错误加上很多条很多条的调用信息组成的错误;这就是抛出的 Error对象 里的 Stack错误堆栈,里面包含了很多信息:包括调用链文件名调用地址行列信息等等;而在下文的错误捕获中,我们也需要去对 Stack错误堆栈 进行解析;

248666eeb9c549a7af8a57a6a5022077.png

当然,解析这一长串的东西还是比较痛苦的,我这边就给出我的解析方法以供参考

// 正则表达式,用以解析堆栈split后得到的字符串
const FULL_MATCH =
  /^\s*at (?:(.*?) ?\()?((?:file|https?|blob|chrome-extension|address|native|eval|webpack|<anonymous>|[-a-z]+:|.*bundle|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;

// 限制只追溯10个
const STACKTRACE_LIMIT = 10;

// 解析每一行
export function parseStackLine(line: string) {
  const lineMatch = line.match(FULL_MATCH);
  if (!lineMatch) return {};
  const filename = lineMatch[2];
  const functionName = lineMatch[1] || '';
  const lineno = parseInt(lineMatch[3], 10) || undefined;
  const colno = parseInt(lineMatch[4], 10) || undefined;
  return {
    filename,
    functionName,
    lineno,
    colno,
  };
}

// 解析错误堆栈
export function parseStackFrames(error: Error) {
  const { stack } = error;
  // 无 stack 时直接返回
  if (!stack) return [];
  const frames = [];
  for (const line of stack.split('\n').slice(1)) {
    const frame = parseStackLine(line);
    if (frame) {
      frames.push(frame);
    }
  }
  return frames.slice(0, STACKTRACE_LIMIT);
}

调用 parseStackFrames() 方法将 error对象 传入后,我们可以看到解析的效果还是可以的:

image.png

JS运行异常

什么叫 JS运行异常 呢?其实很简单,当 JavaScript运行时产生的错误 就属于 JS运行异常

比如,我们未定义一个方法就直接调用它,它会报错:Uncaught ReferenceError: xxx is not defined,这就属于 JS运行异常

noEmit();   // 没有定义,直接调用
// 会报错:Uncaught ReferenceError: noEmit is not defined

那么,既然发生了错误,我们就需要去捕获它;而捕获JS运行异常有两种方法:

方法一

我们可以使用 window.onerror 来捕获全局的 JS运行异常,window.onerror 是一个全局变量,默认值为null。当有js运行时错误触发时,window会触发error事件,并执行 window.onerror(),借助这个特性,我们对 window.onerror 进行重写就可以捕获到代码中的异常;

window.onerror = (msg, url, row, col, error) => {
  const exception = {
    // 上报错误归类
    mechanism: {
      type: 'js'
    },
    // 错误信息
    value: msg,
    // 错误类型
    type: error.name || 'UnKnowun',
    // 解析后的错误堆栈
    stackTrace: {
      frames: parseStackFrames(error),
    },
    meta: {
      url, // 文件地址
      row, // 行号
      col, // 列号
    }
  };
  // 获取了报错详情,就可以走上报方法上报错误信息
  console.log('JS运行error', exception);
  return true; // 返回 true,阻止了默认事件执行,也就是原本将要在控制台打印的错误信息
};
方法二

我们还可以使用 window.addEventListener('error') 来捕获 JS运行异常;它会比 window.onerror 先触发

我们简单封装一下:

// 初始化 JS异常 的数据获取和上报
initJsError = (): void => {
  const handler = (event: ErrorEvent) => {
    // 阻止向上抛出控制台报错
    event.preventDefault();
    // 如果不是 JS异常 就结束
    if (getErrorKey(event) !== mechanismType.JS) return;
    const exception = {
      // 上报错误归类
      mechanism: {
        type: mechanismType.JS,
      },
      // 错误信息
      value: event.message,
      // 错误类型
      type: (event.error && event.error.name) || 'UnKnowun',
      // 解析后的错误堆栈
      stackTrace: {
        frames: parseStackFrames(event.error),
      },
      // 用户行为追踪 breadcrumbs 在 errorSendHandler 中统一封装
      // 页面基本信息 pageInformation 也在 errorSendHandler 中统一封装
      // 错误的标识码
      errorUid: getErrorUid(`${mechanismType.JS}-${event.message}-${event.filename}`),
      // 附带信息
      meta: {
        // file 错误所处的文件地址
        file: event.filename,
        // col 错误列号
        col: event.colno,
        // row 错误行号
        row: event.lineno,
      },
    } as ExceptionMetrics;
    // 一般错误异常立刻上报,不用缓存在本地
    this.errorSendHandler(exception);
  };
  window.addEventListener('error', (event) => handler(event), true);
};
两者的区别和选用

阅读了上文,我们了解到想要监控 JS运行异常 ,我们有两种方法可以选用,那么我们应该选用哪一种呢?或者说它们两者方法之间有什么区别呢?

  • 它们两者均可以捕获到 JS运行异常,但是 方法二除了可以监听 JS运行异常 之外,还可以同时捕获到 静态资源加载异常
  • onerror 可以接受多个参数。而 addEventListener('error') 只有一个保存所有错误信息的参数

我这边个人更加建议使用第二种 addEventListener('error') 的方式;原因很简单:不像方法一可以被 window.onerror 重新覆盖而且可以同时处理静态资源错误

错误类型

细心的同学应该看见了,上文的捕获中,有一个参数叫做 错误类型我们可以通过这个来快速判断错误是基于什么导致的,那么 JS运行时的错误类型常见的有哪些呢?

类型含义说明
SyntaxError语法错误语法错误
ReferenceError引用错误常见于引用了一个不存在的变量: let a = undefinedVariable;
RangeError有效范围错误数值变量或参数超出了其有效范围。 常见于 1.创建一个负长度数组 2.Number对象的方法参数超出范围:let b = new Array(-1)
TypeError类型错误常见于变量或参数不属于有效类型 let foo = 3;foo();
URIErrorURL处理函数错误使用全局URL处理函数错误,比如 decodeURIComponent('%');
  • 这里有一个点需要特别注意,我们主观感觉上的 SyntaxError 语法错误,除了用 eval() 执行的脚本以外,一般是不可以被捕获到的,比如我们编写一个正常的语法错误
const d d = 1;
// 控制台报错 :Uncaught SyntaxError: Missing initializer in const declaration
// 但是上述的捕获方法无法正常捕获错误;
  • 这明显上是一个语法上的错误,但是我们上述的 两个错误捕获方法都没办法捕获到错误
  • 只有在代码中通过 eval() 执行的代码脚本才可以正常捕获到错误信息;
eval('ddd fff');
// 控制台报错 VM149:1 Uncaught SyntaxError: Unexpected identifier
// 上文的错误捕获方法可以正常捕获到错误;
  • 那么,WHY

其实原因很简单, const d d = 1; 这种语法错误,在编译解析阶段就已经报错了,而拥有语法错误的脚本不会放入任务队列进行执行,自然也就不会有错误冒泡到我们的捕获代码;而我们使用 eval();在编译解析阶段一切正常,直到执行的时候才进行报错,自然我们就可以捕获到这段错误;

当然,现在代码检查这么好用,早在编写代码时这种语法错误就被避免掉了,一般我们碰不上语法错误的~

静态资源加载异常

有的时候,我们界面上的 img图片CDN资源 突然失效了、打不开了,就比如以下面这个为例子,我们往html中放进一个img,把它的路径设为请求不到的地址:

<img src="http://localhost:8888/nottrue.jpg">
// 会报错 GET http://localhost:8888/nottrue.jpg net::ERR_CONNECTION_REFUSED

那我们怎么去捕获到这种请求不到资源的、或者说静态资源失效的报错呢?很简单,只需要祭出 window.addEventListener('error') 就可以了

// 静态资源错误的 ErrorTarget
export interface ResourceErrorTarget {
  src?: string;
  tagName?: string;
  outerHTML?: string;
}

// 初始化 静态资源异常 的数据获取和上报
initResourceError = (): void => {
  const handler = (event: Event) => {
    event.preventDefault(); // 阻止向上抛出控制台报错
    // 如果不是跨域脚本异常,就结束
    if (getErrorKey(event) !== mechanismType.RS) return;
    const target = event.target as ResourceErrorTarget;
    const exception = {
      // 上报错误归类
      mechanism: {
        type: mechanismType.RS,
      },
      // 错误信息
      value: '',
      // 错误类型
      type: 'ResourceError',
      // 用户行为追踪 breadcrumbs 在 errorSendHandler 中统一封装
      // 页面基本信息 pageInformation 也在 errorSendHandler 中统一封装
      // 错误的标识码
      errorUid: getErrorUid(`${mechanismType.RS}-${target.src}-${target.tagName}`),
      // 附带信息
      meta: {
        url: target.src,
        html: target.outerHTML,
        type: target.tagName,
      },
    } as ExceptionMetrics;
    // 一般错误异常立刻上报,不用缓存在本地
    this.errorSendHandler(exception);
  };
  window.addEventListener('error', (event) => handler(event), true);
};

使用 addEventListener 捕获资源错误时,一定要将 第三个选项设为 true,因为资源错误没有冒泡,所以只能在捕获阶段捕获。同理,由于 window.onerror 是通过在冒泡阶段捕获错误,所以无法捕获资源错误。

Promise异常

什么叫 Promise异常 呢?其实就是我们使用 Promise 的过程中,当 Promise 被 reject 且没有被 catch 处理的时候,就会抛出 Promise异常;同样的,如果我们在使用 Promise 的过程中,报了JS的错误,同样也被以 Promise异常 的形式抛出:

下面我举两个会产生 Promise异常 的例子

Promise.resolve().then(() => console.log(c));
// Uncaught (in promise) ReferenceError: c is not defined
Promise.reject('reject了但是没有处理!')
// Uncaught (in promise) reject了但是没有处理!

而当抛出 Promise异常 时,会触发 unhandledrejection 事件,所以我们只需要去监听它就可以进行 Promise 异常 的捕获了,不过值得注意的一点是:相比与上面所述的直接获取报错的行号、列号等信息Promise异常 我们只能捕获到一个 报错原因 而已;

// 初始化 Promise异常 的数据获取和上报
initPromiseError = (): void => {
  const handler = (event: PromiseRejectionEvent) => {
    event.preventDefault(); // 阻止向上抛出控制台报错
    const value = event.reason.message || event.reason;
    const type = event.reason.name || 'UnKnowun';
    const exception = {
      // 上报错误归类
      mechanism: {
        type: mechanismType.UJ,
      },
      // 错误信息
      value,
      // 错误类型
      type,
      // 解析后的错误堆栈
      stackTrace: {
        frames: parseStackFrames(event.reason),
      },
      // 用户行为追踪 breadcrumbs 在 errorSendHandler 中统一封装
      // 页面基本信息 pageInformation 也在 errorSendHandler 中统一封装
      // 错误的标识码
      errorUid: getErrorUid(`${mechanismType.UJ}-${value}-${type}`),
      // 附带信息
      meta: {},
    } as ExceptionMetrics;
    // 一般错误异常立刻上报,不用缓存在本地
    this.errorSendHandler(exception);
  };

  window.addEventListener('unhandledrejection', (event) => handler(event), true);
};

HTTP请求异常

HTTP请求的捕获,我在前文中已经写过代码,可以回翻: 一文摸清前端监控实践要点(二)行为监控 HTTP 请求捕获

所谓 Http请求异常 也就是异步请求 HTTP 接口时的异常罢了,比如我调用了一个登录接口,但是我的传参不对,登录接口给我返回了 500 错误码,其实这个时候就已经产生了异常了;

是否属于 Promise异常

看到这里,其实有的同学可能会疑惑,我们现在的调用 HTTP 接口,一般也就是通过 async/await 这种基于Promise的解决异步的最终方案;那么,假如说请求了一个接口地址报了500,因为是基于 Promise 调用的接口,我们能够在上文的 Promise异常 捕获中,获取到一个错误信息(如下图);

但是有一个问题别忘记了,Promise异常捕获没办法获取报错的行列,我们只知道 Promise 报错了,报错的信息是 接口请求500;但是我们根本不知道是哪个接口报错了

1e3b1763cf19402fbd0988d356fcd590.png

所以说,我们对于 Http请求异常 的捕获需求就是:全局统一监控报错的具体接口请求状态码请求耗时以及请求参数等等;

而为了实现上述的监控需求,我们需要了解到:现在异步请求的底层原理都是调用的 XMLHttpRequest 或者 Fetch,我们只需要对这两个方法都进行 劫持 ,就可以往接口请求的过程中加入我们所需要的一些参数捕获;

代码实现
// 初始化 HTTP请求异常 的数据获取和上报
initHttpError = (): void => {
  const loadHandler = (metrics: httpMetrics) => {
    // 如果 status 状态码小于 400,说明没有 HTTP 请求错误
    if (metrics.status < 400) return;
    const value = metrics.response;
    const exception = {
      // 上报错误归类
      mechanism: {
        type: mechanismType.HP,
      },
      // 错误信息
      value,
      // 错误类型
      type: 'HttpError',
      // 错误的标识码
      errorUid: getErrorUid(`${mechanismType.HP}-${value}-${metrics.statusText}`),
      // 附带信息
      meta: {
        metrics,
      },
    } as ExceptionMetrics;
    // 一般错误异常立刻上报,不用缓存在本地
    this.errorSendHandler(exception);
  };
  proxyXmlHttp(null, loadHandler);
  proxyFetch(null, loadHandler);
};

跨域脚本错误

介绍

还有一种错误,平常我们较难遇到,那就是 跨域脚本错误 ,什么叫 跨域脚本错误 呢?比如说我们新建一个texterror.js 文件到 项目B 的 public 目录下以供外部访问;

// 新建的 texterror.js 文件
const a = new Array(-1);

可以看到,我们在 texterror.js 文件中写了一行会报错的代码,认真看过上文的同学应该知道,它会被捕获在 JS运行异常中,且错误类型为 RangeError ;而我们从 项目A 中引入它;

// 项目B的地址,和项目A端口不同;
<script async src="http://xxxxxx:8081/texterror.js"> </script>

加载后运行,我们自然能在控制台发现报错:而我们上文的代码捕获也有错误捕获到:

99e47b449f444af3be512799d4933ddf.png

image.png

但是我们发现,这里的 msg 信息是 Script error,也没有获取到行号列号文件名等的信息,这是怎么回事呢?

其实这是浏览器的一个安全机制当跨域加载的脚本中发生语法错误时,浏览器出于安全考虑,不会报告错误的细节,而只报告简单的 Script error。浏览器只允许同域下的脚本捕获具体错误信息,而其他脚本只知道发生了一个错误,但无法获知错误的具体内容(控制台仍然可以看到,JS脚本无法捕获),我们上文通过项目A去加载项目B的文件,自然产生了跨域;

处理

其实对于三方脚本的错误,我们是否捕获都可以,不过我们需要一点处理,如果不需要捕获的话,就不进行上报,如果需要捕获的话,只上报类型;我们甚至可以只关心自己的远端JS问题,去根据公司域名进行过滤 filename。

我们对上文的 window.addEventListener('error') 再加上对跨域资源的判断,以和正常的代码中错误区分开;

// 初始化 跨域异常 的数据获取和上报
initCorsError = (): void => {
  const handler = (event: ErrorEvent) => {
    // 阻止向上抛出控制台报错
    event.preventDefault();
    // 如果不是跨域脚本异常,就结束
    if (getErrorKey(event) !== mechanismType.CS) return;
    const exception = {
      // 上报错误归类
      mechanism: {
        type: mechanismType.CS,
      },
      // 错误信息
      value: event.message,
      // 错误类型
      type: 'CorsError',
      // 错误的标识码
      errorUid: getErrorUid(`${mechanismType.JS}-${event.message}`),
      // 附带信息
      meta: {},
    } as ExceptionMetrics;
    // 自行上报异常,也可以跨域脚本的异常都不上报;
    this.errorSendHandler(exception);
  };
  window.addEventListener('error', (event) => handler(event), true);
};
补充

看到了这里,可能还有的同学想了解:那么这种跨域的脚本错误我们就没有办法进行获取错误详情吗?答案还是有的:

我们只需要 开启跨域资源共享CORS(Cross Origin Resource Sharing),就可以捕获错误了~我们需要分两步来进行实现:

  1. 添加crossorigin="anonymous"属性。
<script src="http://xxxxxxxx/texterror.js" crossorigin="anonymous"></script>
  1. 添加跨域HTTP响应头
Access-Control-Allow-Origin: *

这两步完成后,允许了跨域,我们就可以在错误捕获脚本中获取到具体的错误信息拉!

Vue2、Vue3 错误捕获

  • Vue2 如果在组件渲染时出现运行错误,错误将会被传递至全局 Vue.config.errorHandler 配置函数;
  • Vue3Vue2,如果在组件渲染时出现运行错误,错误将会被传递至全局的 app.config.errorHandler 配置函数;

我们可以利用这两个钩子函数来进行错误捕获,由于是依赖于 Vue配置函数 的错误捕获,所以我们在初始化时,需要用户将 Vue实例 传进来;

获取报错组件名
export interface Vue {
  config: {
    errorHandler?: any;
    warnHandler?: any;
  };
}

export interface ViewModel {
  _isVue?: boolean;
  __isVue?: boolean;
  $root: ViewModel;
  $parent?: ViewModel;
  $props: { [key: string]: any };
  $options: {
    name?: string;
    propsData?: { [key: string]: any };
    _componentTag?: string;
    __file?: string;
  };
}

// 获取报错组件名
const classifyRE = /(?:^|[-_])(\w)/g;
const classify = (str: string) => str.replace(classifyRE, (c) => c.toUpperCase()).replace(/[-_]/g, '');
const ROOT_COMPONENT_NAME = '<Root>';
const ANONYMOUS_COMPONENT_NAME = '<Anonymous>';
export const formatComponentName = (vm: ViewModel, includeFile: Boolean) => {
  if (!vm) {
    return ANONYMOUS_COMPONENT_NAME;
  }
  if (vm.$root === vm) {
    return ROOT_COMPONENT_NAME;
  }
  const options = vm.$options;
  let name = options.name || options._componentTag;
  const file = options.__file;
  if (!name && file) {
    const match = file.match(/([^/\\]+)\.vue$/);
    if (match) {
      name = match[1];
    }
  }
  return (
    (name ? `<${classify(name)}>` : ANONYMOUS_COMPONENT_NAME) + (file && includeFile !== false ? ` at ${file}` : '')
  );
};
初始化封装
// 只需要在外部把初始化好的 Vue 对象传入即可~
// 初始化 Vue异常 的数据获取和上报
initVueError = (app: Vue): void => {
  app.config.errorHandler = (err: Error, vm: ViewModel, info: string): void => {
    const componentName = formatComponentName(vm, false);
    const exception = {
      // 上报错误归类
      mechanism: {
        type: mechanismType.VUE,
      },
      // 错误信息
      value: err.message,
      // 错误类型
      type: err.name,
      // 解析后的错误堆栈
      stackTrace: {
        frames: parseStackFrames(err),
      },
      // 错误的标识码
      errorUid: getErrorUid(`${mechanismType.JS}-${err.message}-${componentName}-${info}`),
      // 附带信息
      meta: {
        // 报错的Vue组件名
        componentName,
        // 报错的Vue阶段
        hook: info,
      },
    } as ExceptionMetrics;
    // 一般错误异常立刻上报,不用缓存在本地
    this.errorSendHandler(exception);
  };
};

React 错误捕获

React 一样也有官方提供的错误捕获,见文档:zh-hans.reactjs.org/docs/react-…

Vue 不同的是,我们需要自己定义一个高阶组件暴露给项目使用,我这里就不具体详写了,感兴趣的同学可以自己进行补全:

import * as React from 'react';
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
  }
  // ...
  componentDidCatch(error, info) {
    // "组件堆栈" 例子:
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
  }
  // ...
}

项目使用方只需要这样既可:

import React from "react";

<ErrorBoundary>
  <Example />
</ErrorBoundary>;

Source Map

我们的项目想要部署上线,就需要将项目源码经过混淆压缩babel编译转化等等的操作之后,生成最终的打包产物,再进行线上部署;而这样混淆后的代码,我们基本上无法阅读,即使在上文的错误监控里,我们获取了报错代码的行号、列号等关键信息,我们也无法找到具体的源码位置所在;这个时候就需要请出我们的 Sourcemap

Sourcemap 本质上是一个信息文件,里面储存着代码转换前后的对应位置信息。它记录了转换压缩后的代码所对应的转换前的源代码位置,是源代码和生产代码的映射。

我们通过种种打包工具打包后,如果开启了 Sourcemap 功能,就会在打包产物里发现后缀为 .map 的文件,通过对它的解析,我们就可以得到项目的源代码;

  • 我这里举例一个通过 nodejs 进行 SourceMap 解析的例子代码:
// 这里因为 npm 装了 babel,所以用的 import,正常 nodejs 下为 require
import sourceMap from 'source-map';    //source-map库
import fs from 'fs'                    //fs为nodejs读取文件的库
import rp from 'request-promise'

/**
 * @description:  用来解析 sourcemap 的函数方法
 * @param {*} sourceMapFile 传入的 .map 源文件
 * @param {*} line  报错行数
 * @param {*} column  报错列数
 * @param {*} offset  需要截取几行的代码
 * @return {*}
 */
export const sourceMapAnalysis = async (sourceMapFile, line, column, offset) => {
// 通过 sourceMap 库转换为sourceMapConsumer对象
  const consumer = await new sourceMap.SourceMapConsumer(sourceMapFile);
  // 传入要查找的行列数,查找到压缩前的源文件及行列数
  const sm = consumer.originalPositionFor({
    line, // 压缩后的行数
    column, // 压缩后的列数
  });
  // 压缩前的所有源文件列表
  const { sources } = consumer;
  // 根据查到的source,到源文件列表中查找索引位置
  const smIndex = sources.indexOf(sm.source);
  // 到源码列表中查到源代码
  const smContent = consumer.sourcesContent[smIndex];
  // 将源代码串按"行结束标记"拆分为数组形式
  const rawLines = smContent.split(/\r?\n/g);
  let begin = sm.line - offset;
  const end = sm.line + offset + 1;
  begin = begin < 0 ? 0 : begin;
  const context = rawLines.slice(begin, end);
  // 可以根据自己的需要,在末尾处加上 \n
  // const context = rawLines.slice(begin, end).join('\n');
  // 销毁
  consumer.destroy();
  return {
    // 报错的具体代码
    context,
    // 报错在文件的第几行
    originLine: sm.line + 1, // line 是从 0 开始数,所以 +1
    // source 报错的文件路径
    source: sm.source,
  }
};

// 请求线上的 .map 文件进行解析
export const loadMapFileByUrl = async (url)=>{
  return await rp(url)
}
const line = 9;
const column = 492621;
const rawSourceMap = JSON.parse(
  // 这里加载在本地的 .map 文件
  fs.readFileSync('./xxxxxxxxxxxxxxx.map','utf-8').toString()    // 路径自拟
);
const inlineSourceMap = JSON.parse(await loadMapFileByUrl('http://xxxxxxxxxxxx.map')) // 路径自换

// 从url获取 sourcemap 文件
// const res = await sourceMapAnalysis(inlineSourceMap,line,column,2)
// 从本地获取 sourcemap 文件
const res = await sourceMapAnalysis(rawSourceMap,line,column,2)

console.log(res);

效果如下:

d35fa52dcdb1439ebb64a112223b0848.png

注意:使用 Sourcemap 的同学注意在打包的时候,将 .map 文件和部署产物分离,不能部署到线上地址哦! 如果你将 .map 部署上去了,那么你项目的代码也就是直接明文跑在网页上,谁都可以查看未混淆的源码拉!

参考阅读

React componentDidCatch

Vue errorHandler

sentry-javascript 源码