Sentry 在 web 端的主要应用集中在错误捕获和性能数据上报。本文旨在总结如何优化 Sentry 的上报方式,以实现更丰富的信息收集和减少接口请求量。本文将分享一些个人经验,以规范上报格式并提供实用的优化方案。请将本文视为一篇启发性的文章,希望能为你提供有价值的参考。
Sentry 的搭建可以参考这个文章:一口气完成 Sentry Docker 部署,本文不会赘述
本文针对的是 Sentry 在 React 端的上报优化,对于 web 端的上报同样具有一定的参考意义,主要总结的是以下内容:
根据 Sentry 的官方的使用例子,Sentry 会以同步的方式在程序启动时进行初始化。那么为了防止 Sentry 的初始化会堵塞进程或者 Sentry 加载过程中自身发生了错误导致程序初始化失败,那么我们可以把它变为了异步加载,如下:
async function runSentry() {
const Sentry = await import("@sentry/react");
Sentry.init(sentryOptions);
// 给全局加上 Sengtry 实例,方便用于手动上报
window.Sentry = Sentry;
}
runSentry();
异步加载 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();
还有一种情况就是在 runSentry
之前已经发生了会导致系统崩溃的错误(例如 SyntaxError
),让整个程序停止执行,这时候连 Sentry 都不会被初始化。遇到这种情况,我们依然希望 Sentry 能被初始化,然后上报这个致命错误。
这种情况下因为无法判断 Sentry 完成加载的时机,因此可以增加一个倒计时来触发 Sentry 的初始化,在出现错误之后的 n(ms) 后如果 Sentry 还没有初始化完毕,则自动触发 runSentry
。
注:上方的 n(ms) 可以是任意值,只要你觉得正常情况下你的网站中 Sentry 可以在这个时间内完成加载即可。这里建议可以设置为正常情况下你的 web 程序的首屏渲染结束的平均时间(这需要你自己进行统计),可以使用 LCP
或 FCP
的平均时间。
下方是示例:
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();
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
事件的性能上报并不是绝对的,任何时候你都要考虑项目的需求,如果你需要记录路由跳转性能,自然是不用关闭的。
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。
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 的干净是一个好的规范。
为了对上报信息进行好的分类、增强 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;
},
});
可以留意到,上面的示例中我们修改了 tags
、extra
、level
这几个值,下面会说明为什么本文会建议修改这几个。
这几个变量在 sentry 后台中的呈现分别为:
tags
:标签(tags: Record<string, string>
)extra
:附加信息(extra: any
)level
:严重程度(level: "debug" | "error" | "fatal" | "log" | "info" |
"warning"
)其中 tags
、extra
、level
均可以通过上面 beforeSend
的第一个参数 event
直接进行修改,而【错误类型】和【错误值】则建议使用下文要讲的自定义异常类型的方式进行自定义(即使这两个也可以直接修改 event
进行自定义)
对于可控的异常,例如可以被拦截器捕获的网络错误、手动上报的业务埋点等异常,建议自行封装异常类型,并配合统一的 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许可证)