Skip to content

错误监听实现原理

监听js错误和资源加载错误

ts
 handleError(event: ErrorTarget) {
    console.log('Error occurred:', event);

    const target = event.target;
    
    // 区分导致错误的目标是否为 DOM 元素 (资源加载错误会有 localName,如 img/script)
    // 如果没有 target 或者 target 没有 localName,则认为是 JS 执行错误
    if (!target || (event.target && !event.target.localName)) {
      // vue和react捕获的报错使用event解析,异步错误使用event.error解析
      // ErrorStackParser 用于解析错误堆栈,获取文件名、行列号
      const stackFrame = ErrorStackParser.parse(!target ? event : event.error)[0];
      const { fileName, columnNumber, lineNumber } = stackFrame;
      
      // 组装错误上报数据
      const errorData = {
        type: EVENTTYPES.ERROR,
        status: STATUS_CODE.ERROR,
        time: getTimestamp(),
        message: event.message,
        fileName,
        line: lineNumber,
        column: columnNumber
      };

      // 将错误记录添加到行为栈(面包屑)中
      breadcrumb.push({
        type: EVENTTYPES.ERROR,
        category: breadcrumb.getCategory(EVENTTYPES.ERROR),
        data: errorData,
        time: getTimestamp(),
        status: STATUS_CODE.ERROR
      });

      // 生成错误的唯一 hash,用于去重判断
      const hash: string = getErrorUid(
        `${EVENTTYPES.ERROR}-${event.message}-${fileName}-${columnNumber}`
      );
      
      // 开启repeatCodeError第一次报错才上报
      // 如果允许重复上报,或者该错误 hash 尚未存在(即第一次出现),则进行上报
      if (!options.repeatCodeError || (options.repeatCodeError && !hashMapExist(hash))) {
        return transportData.send(errorData);
      }
    }

    // 资源加载报错 (target 存在且有 localName)
    if (target?.localName) {
      // 提取资源加载的详细信息
      const data = resourceTransform(target);
      
      // 记录资源错误到行为栈
      breadcrumb.push({
        type: EVENTTYPES.RESOURCE,
        category: breadcrumb.getCategory(EVENTTYPES.RESOURCE),
        status: STATUS_CODE.ERROR,
        time: getTimestamp(),
        data
      });
      
      // 上报资源加载错误
      return transportData.send({
        ...data,
        type: EVENTTYPES.RESOURCE,
        status: STATUS_CODE.ERROR
      });
    }
  },

监听Promise异常未捕获的错误

ts
handleUnhandleRejection(event: PromiseRejectionEvent) {
    const stackFrame = ErrorStackParser.parse(event.reason)[0];
    const { fileName, columnNumber, lineNumber } = stackFrame;
    const message = unknownToString(event.reason.message || event.reason.stack);
    const data = {
      type: EVENTTYPES.UNHANDLEDREJECTION,
      status: STATUS_CODE.ERROR,
      time: getTimestamp(),
      message,
      fileName,
      line: lineNumber,
      column: columnNumber
    };

    breadcrumb.push({
      type: EVENTTYPES.UNHANDLEDREJECTION,
      category: breadcrumb.getCategory(EVENTTYPES.UNHANDLEDREJECTION),
      time: getTimestamp(),
      status: STATUS_CODE.ERROR,
      data
    });
    const hash: string = getErrorUid(
      `${EVENTTYPES.UNHANDLEDREJECTION}-${message}-${fileName}-${columnNumber}`
    );
    // 开启repeatCodeError第一次报错才上报
    if (!options.repeatCodeError || (options.repeatCodeError && !hashMapExist(hash))) {
      transportData.send(data);
    }
  },

handleError 是监控 SDK 中用于统一处理全局捕获到的错误的核心函数。它主要负责区分错误的类型(代码执行错误 vs 资源加载错误),并执行相应的数据解析、上下文记录(面包屑)、去重以及上报逻辑。

核心流程

该函数主要接受一个 ErrorTarget 类型的 event 对象,根据 event.target 的特征来判断错误类型。

1. 错误类型区分

  • 代码执行错误 (Code Error):

    • 包括 JS 运行时错误、Promise 异常(部分场景)、框架(Vue/React)捕获的错误。
    • 判断依据: event.target 不存在,或者 event.target 存在但没有 localName 属性(非 DOM 元素)。
  • 资源加载错误 (Resource Error):

    • 包括图片、脚本、CSS 链接等资源加载失败。
    • 判断依据: event.target 存在且具有 localName 属性(如 img, script, link)。
    • 注意: 资源加载错误不会冒泡,因此必须在捕获阶段(capturing phase)进行获取。SDK 在注册 error 事件监听时设置了 useCapture: true 以确保能捕获此类错误。
  • Promise异常未捕获的错误

    • 通过unhandledrejection事件捕获

2. 代码执行错误处理流程

当判定为代码错误时,执行以下步骤:

  1. 堆栈解析: 使用 ErrorStackParser 解析错误对象(eventevent.error),提取关键堆栈帧信息(文件名 fileName、行号 lineNumber、列号 columnNumber)。
  2. 构建错误数据: 组装标准化的错误上报数据对象 (errorData),包含错误类型、时间戳、错误信息及堆栈位置。
  3. 记录面包屑 (Breadcrumb): 将错误信息压入用户行为栈,类型为 EVENTTYPES.ERROR,用于还原错误发生前的用户路径。
  4. 生成 Hash 指纹: 基于 错误类型 + 错误信息 + 文件名 + 列号 生成唯一的 Hash 字符串。
  5. 上报与去重:
    • 检查配置项 options.repeatCodeError
    • 如果允许重复上报,或者该错误 Hash 尚不存在(通过 hashMapExist(hash) 校验),则调用 transportData.send(errorData) 进行上报。

3. 资源加载错误处理流程

当判定为资源错误时,执行以下步骤:

  1. 信息提取: 调用 resourceTransform(target) 从 DOM 元素中提取资源 URL、标签名等信息。
  2. 记录面包屑: 将资源错误信息压入用户行为栈,类型为 EVENTTYPES.RESOURCE
  3. 直接上报: 组装数据并调用 transportData.send 进行上报。

关键代码解析

typescript
// 区分逻辑
if (!target || (event.target && !event.target.localName)) {
  // === 代码错误处理 ===
  // 1. 解析堆栈 (兼容同步与异步/框架错误)
  const stackFrame = ErrorStackParser.parse(!target ? event : event.error)[0];
  
  // 2. 生成 Hash 用于去重
  const hash = getErrorUid(`${EVENTTYPES.ERROR}-${event.message}...`);
  
  // 3. 校验并上报
  if (!options.repeatCodeError || !hashMapExist(hash)) {
    transportData.send(errorData);
  }
}

if (target?.localName) {
  // === 资源错误处理 ===
  // 1. 提取 DOM 资源信息
  const data = resourceTransform(target);
  
  // 2. 直接上报
  transportData.send({ ...data, type: EVENTTYPES.RESOURCE ... });
}