Sentry 在 React 端的上报格式规范和上报优化方案

发布于 2023/4/15, 编辑于 2024/2/5

Sentry 在 web 端的主要应用集中在错误捕获和性能数据上报。本文旨在总结如何优化 Sentry 的上报方式,以实现更丰富的信息收集和减少接口请求量。本文将分享一些个人经验,以规范上报格式并提供实用的优化方案。请将本文视为一篇启发性的文章,希望能为你提供有价值的参考。

前言

Sentry 的搭建可以参考这个文章:一口气完成 Sentry Docker 部署,本文不会赘述

本文针对的是 Sentry 在 React 端的上报优化,对于 web 端的上报同样具有一定的参考意义,主要总结的是以下内容:

  1. Sentry 的异步初始化
  2. Sentry 性能上报优化
  3. 报文信息优化

一、Sentry 初始化优化方案

1.1. 异步初始化

根据 Sentry 的官方的使用例子,Sentry 会以同步的方式在程序启动时进行初始化。那么为了防止 Sentry 的初始化会堵塞进程或者 Sentry 加载过程中自身发生了错误导致程序初始化失败,那么我们可以把它变为了异步加载,如下:

async function runSentry() {
  const Sentry = await import("@sentry/react");
  Sentry.init(sentryOptions);
  // 给全局加上 Sengtry 实例,方便用于手动上报
  window.Sentry = Sentry;
}

runSentry();

1.2. 处理 Sentry 初始化前的错误

1.2.1. 捕获初始化前的错误

异步加载 Sentry 会带来另一个问题,那就是如果程序初始化时的同步任务发生了错误,这些错误就会丢失在 Sentry 初始化前。

为了解决这一点,可以在程序初始化的最开始先注册一个全局的错误监听,收集这些没被 Sentry 捕获的错误,然后在 Sentry 初始化完成后统一上报,如下:

const unhandledErrors: Error[] = [];

function addUnhandledErrors(e) {
  if (!window.Sentry) {
    unhandledErrors.push(e.error);
  }
}

window.addEventListener("error", addUnhandledErrors);

function reportUnhandledErrors() {
  if (unhandledErrors.length !== 0 && window.Sentry) {
    unhandledErrors.forEach((error) => {
      // 手动上报这些错误
      window.Sentry.captureException(error);
    });
  }
  // 上报结束后取消这个全局监听
  window.removeEventListener("error", addUnhandledErrors);
}

function runSentry() {
  const Sentry = await import("@sentry/react");
  Sentry.init(sentryOptions);
  window.Sentry = Sentry;
  // Sentry 初始化完成后消化未被捕获的错误
  reportUnhandledErrors();
}

runSentry();

1.2.2. 系统崩溃导致 Sentry 无法初始化的情况

还有一种情况就是在 runSentry 之前已经发生了会导致系统崩溃的错误(例如 SyntaxError),让整个程序停止执行,这时候连 Sentry 都不会被初始化。遇到这种情况,我们依然希望 Sentry 能被初始化,然后上报这个致命错误。

这种情况下因为无法判断 Sentry 完成加载的时机,因此可以增加一个倒计时来触发 Sentry 的初始化,在出现错误之后的 n(ms) 后如果 Sentry 还没有初始化完毕,则自动触发 runSentry

注:上方的 n(ms) 可以是任意值,只要你觉得正常情况下你的网站中 Sentry 可以在这个时间内完成加载即可。这里建议可以设置为正常情况下你的 web 程序的首屏渲染结束的平均时间(这需要你自己进行统计),可以使用 LCPFCP 的平均时间。

下方是示例:

const unhandledErrors: Error[] = [];

function createCountdown(
  countdownDuration: number,
  callback: () => void
): () => void {
  let timer: NodeJS.Timeout | null = null;

  return function startCountdown(): void {
    // 不允许重复生成倒计时
    if (timer) {
      return;
    }

    timer = setTimeout(() => {
      callback();
      timer = null;
    }, countdownDuration);
  };
}

// 15000 为一个月里统计的线上 web 程序的 lcp 平均时长
// 视你的 web 程序的实际情况而定
const sentryCountdown = createCountdown(15000, () => {
  if (unhandledErrors.length !== 0 && !window.Sentry) {
    runSentry();
  }
});

function addUnhandledErrors(e) {
  if (!window.Sentry) {
    unhandledErrors.push(e.error);
    // 出错后启动倒计时
    sentryCountdown();
  }
}

window.addEventListener("error", addUnhandledErrors);

function runSentry() {
  const Sentry = await import("@sentry/react");
  Sentry.init(sentryOptions);
  window.Sentry = Sentry;
  reportUnhandledErrors();
}

runSentry();

二、性能上报优化

2.1. 性能事务优化

Sentry 收集的 web 程序性能信息主要是通过插件 BrowserTracing 实现的,基本使用如下:

Sentry.init({
  ...sentryOptions,
  integrations: [...otherIntegrations, new BrowserTracing()],
});

这样使用的话,Sentry 会在首屏加载结束(记为 pageload 事件)和路由跳转事件(记为 navigation 事件)都会进行采集相应的性能数据,并上报。

而考虑到路由切换会比较频繁,并且性能数据量会较多(10kb ~ 30kb),所以可以选择关闭路由跳转的性能上报:

Sentry.init({
  ...sentryOptions,
  integrations: [
    ...otherIntegrations,
    new BrowserTracing({
      /** 文档说明:
       * Flag to enable/disable creation of `navigation` transaction on history changes.
       *
       * Default: true
       */
      startTransactionOnLocationChange: false,
    }),
  ],
});

或者

Sentry.init({
  ...sentryOptions,
  integrations: [
    ...otherIntegrations,
    new BrowserTracing({
      // 只触发 pageload 事件
      beforeNavigate: (context) => {
        if (context.op === "pageload") {
          return context;
        }
      },
    }),
  ],
});

不过关闭 navigation 事件的性能上报并不是绝对的,任何时候你都要考虑项目的需求,如果你需要记录路由跳转性能,自然是不用关闭的。

2.2. LCP 上报

LCP (Large Content Print)事件是评估 web 性能的一个重要标准,即首屏最大元素的渲染时间,但是它的记录时间很特殊,并不是在最大元素的渲染完成的时机,而是用户第一次操作(如点击页面)时才会正式记录下来,例如 LCP 时间为 15000ms,但是 web 性能监控并不会在 15000ms 时存储这个时间,而是在用户第一次操作页面后才会存下这个 15000ms

这就导致了 Sentry 一般无法在首屏性能上报之前获得 LCP 时间(除非用户在上报前操作了页面),所以大多数情况下看 Sentry 后台的性能数据中都是没有 LCP 的。对于旧版本的 Sentry,一般会延长上报等待时间 idleTimeout 来延迟上报尽可能在用户操作后进行性能上报:

Sentry.init({
  ...sentryOptions,
  integrations: [
    ...otherIntegrations,
    new BrowserTracing({
      /** 文档说明:
       * The time to wait in ms until the transaction will be finished during an idle state. An idle state is defined
       * by a moment where there are no in-progress spans.
       *
       * The transaction will use the end timestamp of the last finished span as the endtime for the transaction.
       * If there are still active spans when this the `idleTimeout` is set, the `idleTimeout` will get reset.
       * Time is in ms.
       *
       * Default: 1000
       */
      idleTimeout: 50000,
    }),
  ],
});

但是这种方法依然无法百分百保证 LCP 能被收集到,因为总是存在用户一直不操作页面的可能性。但是在 Sentry 7.42.0 以及之后版本,Sentry 进行了调整,它会模拟一次页面操作,从而让浏览器产生 LCP 记录。所以请尽可能使用 7.42.0 或更高版本的 Sentry。

三、报文信息优化

3.1. 过滤上下文信息

Sentry 在上报任何信息(错误日志、性能日志等)时都会携带这次事件的上下文,这些上下文包括打印信息、接口调用等,如果你的 web 程序没有对打印日志进行处理,例如在生产环境中也产生打印信息(console.log),那么就会导致 Sentry 的上报请求体积过大。

为了解决这个问题,可以使用 Sentry 的钩子 beforeBreadcrumb,对上下文信息进行过滤处理,以下是一个简单的示例:

Sentry.init({
  ...sentryOptions,
  beforeBreadcrumb: (breadcrumb) => {
    // 过滤 console.log 和 console.warning
    if (
      breadcrumb.category === "console" &&
      ["log", "warning"].includes(breadcrumb.level!)
    ) {
      return null;
    }
    return breadcrumb;
  },
});

注:这样的过滤同样不是必须的,如果你需要这些信息进行 debug,那么你就不需要过滤。但是保持 web 程序在线上环境的 console 的干净是一个好的规范。

3.2. 上报信息格式优化

为了对上报信息进行好的分类、增强 Sentry 后台上报信息的可读性,可以在 Sentry 上报前统一对这些信息进行处理、规范上报的格式。为了实现这一点,可以利用 Sentry 钩子 beforeSend

Sentry.init({
  ...sentryOptions,
  beforeSend: (event, hint) => {
    // hint.originalException 为捕获到的原始异常实例
    // 可以通过自定义通用的 handleError 来判断此次异常的各种数据
    // 从而修改上报事件的属性,提供更多有效信息
    const { tags, extra, level } = handleError(hint.originalException as any);
    event.tags = tags; // 定义这次上报的 tags
    event.extra = extra; // 添加附加信息
    event.level = level; // 定义上报级别
    return event;
  },
});

可以留意到,上面的示例中我们修改了 tagsextralevel 这几个值,下面会说明为什么本文会建议修改这几个。

3.2.1. 自定义哪些上报值?

这几个变量在 sentry 后台中的呈现分别为:

  • tags:标签(tags: Record<string, string>
    tags
  • extra:附加信息(extra: any
    extra
  • level:严重程度(level: "debug" | "error" | "fatal" | "log" | "info" | "warning"
    level
  • 除此以外,还应该注重【错误类型】和【错误值】的规范性:
    name

其中 tagsextralevel 均可以通过上面 beforeSend 的第一个参数 event 直接进行修改,而【错误类型】和【错误值】则建议使用下文要讲的自定义异常类型的方式进行自定义(即使这两个也可以直接修改 event 进行自定义)

3.2.2. 自定义异常类型

对于可控的异常,例如可以被拦截器捕获的网络错误、手动上报的业务埋点等异常,建议自行封装异常类型,并配合统一的 handleError 函数来规范上报格式,以下是一个网络错误的例子:

// 一、首先,axios 的网络拦截器捕获到了网络错误
axiosInstance.interceptors.response.use(
  (obj: AxiosResponse) => {
    // do something...
  },
  (obj: AxiosResponse) => {
    // 构造一个 NetworkError 实例,进行手动上报
    window.Sentry.captureException(new NetworkError(obj);
  }
);

// 二、构造函数 NetworkError 会根据 axios 的响应实例构造 Sentry 需要的异常信息
export class NetworkError extends Error {
    /** 附加值 */
    extra: any;
    /** tag */
    type: string = 'error.network';
    /** 错误等级 */
    level: string = 'error';

    constructor(response: AxiosResponse) {
        // 构造参数为【错误值】
        super(`${response.request.responseURL}`);
        // name 为【错误类型】
        this.name = `Network Error ${response.data.code}`;
        // 处理附加信息,getExtra 中可以任意取需要的值进行返回
        this.extra = getExtra(response);
    }
}

// 三、异常进入 beforeSend 进行统一处理
Sentry.init({
  ...sentryOptions,
  beforeSend: (event, hint) => {
    // hint.originalException 就是捕获的 NetworkError 实例
    const { tags, extra, level } = handleError(hint.originalException as any);
    event.tags = tags; // 定义这次上报的 tags
    event.extra = extra; // 添加附加信息
    event.level = level; // 定义上报级别
    return event;
  },
});
// 处理错误信息
const handleError = (error: NetworkError) => {
    const info: SentryInfo = {
        extra: undefined,
        tags: {},
        report: true,
        level: 'error',
    };
    if (error.extra) {
        info.extra = error.extra;
    }
    if (error.type) {
        info.tags.type = error.type;
    }
    if (error.level) {
        info.level = error.level || 'error';
    }
    return info;
};

经过上述封装,上报后的异常信息就会在 Sentry 后台中展示出有序分类、可读性强异常信息,从而提升排查故障的效率。

总结

以上是本文总结的一些 Sentry 上报优化的优化方案,从 Sentry 初始化、性能日志优化、上报信息优化三个方面提出了优化方案,可以为 React 端或 web 端的 Sentry 上报制定规范提供参考。

点击这里前往 Github 查看原文,交流意见~

文档信息

版权声明:自由转载 - 非商用 - 非衍生 - 保持署名(创意共享3.0许可证