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

前言

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

系列文章传送门

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

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

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

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

应用的稳定情况

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

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

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

整体封装

 
ts
复制代码
// 错误类型
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 是相同的
 
ts
复制代码
// 对每一个错误详情,生成一串编码
export const getErrorUid = (input: string) => {
  return window.btoa(unescape(encodeURIComponent(input)));
};

错误堆栈

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

248666eeb9c549a7af8a57a6a5022077.png

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

 
js
复制代码
// 正则表达式,用以解析堆栈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运行异常

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

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

方法一

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

 
js
复制代码
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 先触发

我们简单封装一下:

 
ts
复制代码
// 初始化 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();
URIError URL处理函数错误 使用全局URL处理函数错误,比如 decodeURIComponent('%');
  • 这里有一个点需要特别注意,我们主观感觉上的 SyntaxError 语法错误,除了用 eval() 执行的脚本以外,一般是不可以被捕获到的,比如我们编写一个正常的语法错误
 
js
复制代码
const d d = 1;
// 控制台报错 :Uncaught SyntaxError: Missing initializer in const declaration
// 但是上述的捕获方法无法正常捕获错误;
  • 这明显上是一个语法上的错误,但是我们上述的 两个错误捕获方法都没办法捕获到错误
  • 只有在代码中通过 eval() 执行的代码脚本才可以正常捕获到错误信息;
 
js
复制代码
eval('ddd fff');
// 控制台报错 VM149:1 Uncaught SyntaxError: Unexpected identifier
// 上文的错误捕获方法可以正常捕获到错误;
  • 那么,WHY

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

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

静态资源加载异常

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

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

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

 
ts
复制代码
// 静态资源错误的 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异常 的例子

 
js
复制代码
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异常 我们只能捕获到一个 报错原因 而已;

 
ts
复制代码
// 初始化 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;但是我们根本不知道是哪个接口报错了

 
js
复制代码

1e3b1763cf19402fbd0988d356fcd590.png

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

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

代码实现
 
ts
复制代码
// 初始化 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 目录下以供外部访问;

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

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

 
js
复制代码
// 项目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') 再加上对跨域资源的判断,以和正常的代码中错误区分开;

 
ts
复制代码
// 初始化 跨域异常 的数据获取和上报
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"属性。
 
js
复制代码
<script src="http://xxxxxxxx/texterror.js" crossorigin="anonymous"></script>
  1. 添加跨域HTTP响应头
 
js
复制代码
Access-Control-Allow-Origin: *

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

Vue2、Vue3 错误捕获

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

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

获取报错组件名
 
ts
复制代码
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}` : '')
  );
};
初始化封装
 
ts
复制代码
// 只需要在外部把初始化好的 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 不同的是,我们需要自己定义一个高阶组件暴露给项目使用,我这里就不具体详写了,感兴趣的同学可以自己进行补全:

 
js
复制代码
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
  }
  // ...
}

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

 
js
复制代码
import React from "react";

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

Source Map

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

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

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

  • 我这里举例一个通过 nodejs 进行 SourceMap 解析的例子代码:
 
js
复制代码
// 这里因为 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 源码

 
文章被收录于专栏:
cover
一文摸清前端监控实践要点
这个专栏将分几篇文章,将前端监控的实践要点一一说明~
相关小册
「基于 Node 的 DevOps 实战」封面
VIP
基于 Node 的 DevOps 实战
CookieBoty 创作等级LV.5
VIP.5 如鱼得水
2285购买
¥19.95
¥39.9
首单券后价
「TypeScript 类型体操通关秘籍」封面
VIP
TypeScript 类型体操通关秘籍
¥29.95
¥59.9
首单券后价
评论
avatar
 
表情
图片
Ctrl + Enter
全部评论 20
最新
最热
 
用户1931051731661的头像
删除
写得很好, 但是有没有一种可能 'UnKnowun' 拼错了.
展开
收起
点赞
回复
  • 屏蔽作者: 用户1931051731661
  • 举报
 
丶从头喜欢你的头像
删除
前端工程师 6月前
请问有github仓库吗?
展开
收起
点赞
回复
  • 屏蔽作者: 丶从头喜欢你
  • 举报
 
你不要过来的头像
删除
请问下,sourceMap那里,line和cloumn写固定值是什么意思
展开
收起
点赞
1
  • 屏蔽作者: 你不要过来
  • 举报
avatar
删除
(作者) 8月前
我写的是个demo,真正使用的时候,将固定值换成传入的值就可以了
展开
收起
点赞
回复
  • 屏蔽作者: 菜猫子neko
  • 举报
易缕尘光的头像
删除
前端开发工程师 11月前
老哥 那这些监控了,没有可视化 那怎么看数据啊
展开
收起
点赞
1
  • 屏蔽作者: 易缕尘光
  • 举报
avatar
删除
(作者) 11月前
搞一个可视化呀,有数据了可视化还难做嘛
展开
收起
点赞
回复
  • 屏蔽作者: 菜猫子neko
  • 举报
Dolary的头像
删除
12月前
大佬代码可以上传到git嘛,想学习一下项目的结构
展开
收起
点赞
1
  • 屏蔽作者: Dolary
  • 举报
avatar
删除
(作者) 12月前
结构、架构可以先看第四篇文章;具体代码的上传最近稍忙,没时间搞[呲牙]
展开
收起
点赞
回复
  • 屏蔽作者: 菜猫子neko
  • 举报
Ranger的头像
删除
前端 @ 南京 1年前
EngineInstance这个数据结构没有写出来
展开
收起
点赞
1
  • 屏蔽作者: Ranger
  • 举报
avatar
删除
(作者) 1年前
EngineInstance 可以看第四篇文章的 SDK架构设计 部分一起理解;
展开
收起
点赞
回复
  • 屏蔽作者: 菜猫子neko
  • 举报
galaxy_s10的头像
删除
前端开发 1年前
牛逼
展开
收起
点赞
回复
  • 屏蔽作者: galaxy_s10
  • 举报
 
luoqiang0831的头像
删除
如果是css样式中的背景图片加载失败是否能获取到呢
展开
收起
点赞
1
  • 屏蔽作者: luoqiang0831
  • 举报
avatar
删除
(作者) 1年前
如果是以background-image 方式设置的,那获取不到呢。
展开
收起
点赞
回复
  • 屏蔽作者: 菜猫子neko
  • 举报
溪言的头像
删除
前端工程师 @ BUG制造商 1年前
大佬没有传git代码吗?
展开
收起
点赞
5
  • 屏蔽作者: 溪言
  • 举报
avatar
删除
(作者) 1年前
简单写了一份的代码已经贴在文中啦,更详细的日后有时间时在Github开源一份;
展开
收起
点赞
回复
  • 屏蔽作者: 菜猫子neko
  • 举报
avatar
删除
1年前
这周末有空吗,我觉得挺合适哈哈[呲牙]
展开
收起
简单写了一份的代码已经贴在文中啦,更详细的日后有时间时在Github开源一份;
点赞
回复
  • 屏蔽作者: yuKe
  • 举报
查看更多回复
菜猫子neko的头像
删除
(作者) 前端 @ 阿里巴巴 1年前
创作不易,如果这篇文章对你有所帮助,还请点赞或评论支持一下[灵光一现]
展开
收起
点赞
回复
  • 屏蔽作者: 菜猫子neko
  • 举报
 
112
20
收藏
  • 菜猫子neko
    1年前
    本文分享如何自研前端监控系统之(一)性能监控;我们都听说过性能的重要性。但谈起性能,以及让网站"速度提升"时,我们具体指的是什么?而本篇文章将分享:如何准确地测量用户的网页性能体验。
    • 9850
    • 48
  • 羽飞
    2年前
    在我司线上运行的是近亿级别的广告页面,这样线上如果裸奔,出现了什么问题不知道,后置在业务端发现,被业务方询问,这种场景很尴尬。
    • 4.0w
    • 138
    一篇讲透自研的前端错误监控
  • 菜猫子neko
    1年前
    本文分享如何自研前端监控系统之(二)行为监控;来谈谈用户行为监控的内容;来看看我们应该如何分析PV、UV、用户的行为追踪、用户的特征信息等;
    • 5960
    • 12
  • 字节架构前端
    5月前
    简述 Slardar 前端监控自18年底开始建设以来,从仅仅作为 Sentry 的替代,经历了一系列迭代和发展,目前做为一个监控解决方案,已经应用到抖音、西瓜、今日头条等众多业务线。 据21年下旬统计
    • 4.1w
    • 58
  • Godiswill
    3年前
    作为一个前端,在开发过程即便十分小心,自测充分,在不同用户复杂的操作下也难免会出现程序员意想不到的问题,给公司或个人带来巨大的损失。 这时一款能够及时上报错误和能够帮助程序员很好的解决错误的前端错误监控系统就必不可少了。 接下来我们就聊聊常见的错误发生与处理。 ... 可以阅读…
    • 1.3w
    • 7
  • 又在吃鱼
    2年前
    作为程序员,每次开发完自测充分的时候,但还是会有线上异常情况。如何快速发现 或者提前监控到这些异常的出现呢,是不是需要一个错误监控系统? fundebug、sentry、bat的... 有免费版的付费版,免费版。付费版和免费版其实差不多,免费够我们用了,付费的就是他啥都帮你弄好…
    • 1.2w
    • 45
  • yuxiaoliang
    4年前
    在线上项目中,需要统计产品中用户行为和使用情况,从而可以从用户和产品的角度去了解用户群体,从而升级和迭代产品,使其更加贴近用户。用户行为数据可以通过前端数据监控的方式获得,除此之外,前端还需要实现性能监控和异常监控。性能监控包括首屏加载时间、白屏时间、http请求时间和http…
    • 7.1w
    • 37
  • 推啊前端团队
    2年前
    这次由我们团队的羽飞同学带来我们自研的错误监控平台,欢迎各位看官老爷指正和吐槽。在我司线上运行的是近亿级别的广告页面,这样线上如果裸奔,出现了什么问题不知道......
    • 1223
    • 5
    一篇讲透自研的前端错误监控
  • hpoenixf
    5年前
    写代码难免会碰到错误。因此,在项目上线后,我们还需要主动对项目的错误进行收集,不能等用户发现错误,再联系我们,我们再去处理。 本文章为前端进阶系列的一部分,更多内容可查看 https://github.com/hpoenixf/hpoenixf.github.io
    • 8247
    • 10
  • 前端早早聊
    3年前
    前端早早聊大会,前端成长的新起点,与掘金联合举办。 加微信 codingdreamer 进大会专属内推群,赢在新的起跑线。 我今天分享的主题是“如何实现一套多端错误监控平台”。先来做一个简单的自我介绍,我是来自贝贝-大前端架构组的 Allan ,目前致利于集团错误监控系统维护以…
    • 1.5w
    • 40
  • 小君
    8月前
    调研目前主流监控平台开发实践思路;自行开发sdk嵌入project进行:异常报错、性能分析、行为统计;
    • 3091
    • 2
    自研搭建前端监控平台🔥
  • 一:及时代码运行错误:也称为代码错误。这个错误往往是程序员在代码书写时造成的,比如语法错误、逻辑错误,这样的错误通常在测试阶段就会被发现,但是也可能存在“漏网之鱼”。 二:资源加载错误:这个错误通常是找不到文件(404)或者是文件加载超时造成的。 浏览器获取网页时,会对网页中每…
    • 5388
    • 5
  • 前端新能源
    5年前

作者:菜猫子neko
链接:https://juejin.cn/post/7100841779854835719/
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2023-07-26 11:50  GaoYanbing  阅读(91)  评论(0编辑  收藏  举报