前言
sentry
对于前端工程师来说并不陌生,本文主要通过源码讲解sentry
是如何实现捕获各种错误。
- sentry-javascript仓库地址
sentry
基本使用方法:官方地址
前置准备
我们首先看一下两个关键的工具方法
addInstrumentationHandler 二次封装原生方法
我们首先来看@sentry/uitls
的instrument.ts
文件的addInstrumentationHandler
方法:
function addInstrumentationHandler(handler: InstrumentHandler): void {
if (
!handler ||
typeof handler.type !== 'string' ||
typeof handler.callback !== 'function'
) {
return;
}
// 初始化对应type的回调
handlers[handler.type] = handlers[handler.type] || [];
// 添加回调队列
(handlers[handler.type] as InstrumentHandlerCallback[]).push(handler.callback);
instrument(handler.type);
}
// 全局闭包
const instrumented: { [key in InstrumentHandlerType]?: boolean } = {};
function instrument(type: InstrumentHandlerType): void {
if (instrumented[type]) {
return;
}
// 全局闭包防止重复封装
instrumented[type] = true;
switch (type) {
case 'console':
instrumentConsole();
break;
case 'dom':
instrumentDOM();
break;
case 'xhr':
instrumentXHR();
break;
case 'fetch':
instrumentFetch();
break;
case 'history':
instrumentHistory();
break;
case 'error':
instrumentError();
break;
case 'unhandledrejection':
instrumentUnhandledRejection();
break;
default:
logger.warn('unknown instrumentation type:', type);
}
}
addInstrumentationHandler
收集相关的回调并调用对应方法对原生方法做二次封装。
fill 用高阶函数包装给定的对象方法
function fill(
source: { [key: string]: any }, // 目标对象
name: string, // 覆盖字段名
replacementFactory: (...args: any[]) => any // 封装的高阶函数
): void {
// 不存在的字段不做封装
if (!(name in source)) {
return;
}
// 原生方法
const original = source[name] as () => any;
// 高阶函数
const wrapped = replacementFactory(original) as WrappedFunction;
if (typeof wrapped === 'function') {
try {
// 为高阶函数指定一个空对象原型
wrapped.prototype = wrapped.prototype || {};
Object.defineProperties(wrapped, {
__sentry_original__: {
enumerable: false,
value: original,
},
});
} catch (_Oo) {
}
}
// 覆盖原生方法
source[name] = wrapped;
}
fetch错误捕获
指定url捕获
在sentry
初始化的时候,我们可以通过tracingOrigins
捕获哪些url
,sentry
通过作用域闭包缓存所有应该捕获的url
,省去重复的遍历。
// 作用域闭包
const urlMap: Record<string, boolean> = {};
// 用于判断当前url是否应该被捕获
const defaultShouldCreateSpan = (url: string): boolean => {
if (urlMap[url]) {
return urlMap[url];
}
const origins = tracingOrigins;
// 缓存url省去重复遍历
urlMap[url] =
origins.some((origin: string | RegExp) => isMatchingPattern(url, origin)) &&
!isMatchingPattern(url, 'sentry_key');
return urlMap[url];
};
添加捕获回调
接下来,我们在@sentry/browser
中看到:
if (traceFetch) {
addInstrumentationHandler({
callback: (handlerData: FetchData) => {
fetchCallback(handlerData, shouldCreateSpan, spans);
},
type: 'fetch',
});
}
高阶函数封装fetch
按照上面的代码我们可以准确看出通过type: 'fetch'
接下来应该执行instrumentFetch
方法,我们来看一下这个方法的代码:
function instrumentFetch(): void {
if (!supportsNativeFetch()) {
return;
}
fill(global, 'fetch', function(originalFetch) {
// 封装后的fetch方法
return function(...args: any[]): void {
const handlerData = {
args,
fetchData: {
method: getFetchMethod(args),
url: getFetchUrl(args),
},
startTimestamp: Date.now(),
};
// 依次执行fetch type的回调方法
triggerHandlers('fetch', {
...handlerData,
});
// 通过apply重新指向this
return originalFetch.apply(global, args).then(
// 请求成功
(response: Response) => {
triggerHandlers('fetch', {
...handlerData,
endTimestamp: Date.now(),
response,
});
return response;
},
// 请求失败
(error: Error) => {
triggerHandlers('fetch', {
...handlerData,
endTimestamp: Date.now(),
error,
});
throw error;
},
);
};
});
}
我们可以通过上面的代码发现sentry
封装了fetch
方法,在请求结束之后,优先遍历了在addInstrumentationHandler
中缓存的回调,然后再将结果继续透传给后续的用户回调。
捕获回调函数内都做了什么
接下来我们再看一下fetch
回调中都做了哪些事情
export function fetchCallback(
handlerData: FetchData, // 整合的数据内容过
shouldCreateSpan: (url: string) => boolean, // 用于判断当前url是否需要捕获
spans: Record<string, Span>,
): void {
// 获取用户配置
const currentClientOptions = getCurrentHub().getClient()?.getOptions();
if (
!(currentClientOptions && hasTracingEnabled(currentClientOptions)) ||
!(handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url))
) {
return;
}
// 请求结束,只处理包含事务id的请求
if (handlerData.endTimestamp && handlerData.fetchData.__span) {
const span = spans[handlerData.fetchData.__span];
if (span) {
const response = handlerData.response;
if (response) {
span.setHttpStatus(response.status);
}
span.finish();
delete spans[handlerData.fetchData.__span];
}
return;
}
// 开始请求,创建一个事务
const activeTransaction = getActiveTransaction();
if (activeTransaction) {
const span = activeTransaction.startChild({
data: {
...handlerData.fetchData,
type: 'fetch',
},
description: `${handlerData.fetchData.method} ${handlerData.fetchData.url}`,
op: 'http',
});
// 添加唯一id
handlerData.fetchData.__span = span.spanId;
// 记录唯一id
spans[span.spanId] = span;
// 根据fetch的用法第一个参数可以是 请求地址 或者是 Request对象
const request = (handlerData.args[0] = handlerData.args[0] as string | Request);
// 根据fetch的用法第二个参数是请求的相关配置项
const options = (handlerData.args[1] = (handlerData.args[1] as { [key: string]: any }) || {});
// 默认取配置项的headers(可能为undefined)
let headers = options.headers;
if (isInstanceOf(request, Request)) {
// 如果request是Request对象,则headers使用Request的
headers = (request as Request).headers;
}
if (headers) {
// 用户已经设置了headers,则在请求头添加sentry-trace字段
if (typeof headers.append === 'function') {
headers.append('sentry-trace', span.toTraceparent());
} else if (Array.isArray(headers)) {
headers = [...headers, ['sentry-trace', span.toTraceparent()]];
} else {
headers = { ...headers, 'sentry-trace': span.toTraceparent() };
}
} else {
// 用户未设置headers
headers = { 'sentry-trace': span.toTraceparent() };
}
// 这里借用了options声明时会初始化handlerData.args[1],使用引用类型覆盖了fetch的请求头
options.headers = headers;
}
}
总结
到此我们就可以知道sentry
是如何在fetch
中捕获信息的,我们按照步骤总结一下:
- 由用户配置
traceFetch
确认开启fetch
捕获,配置tracingOrigins
确认要捕获的url
- 通过
shouldCreateSpanForRequest
添加对fetch
的声明周期的回调- 内部调用
instrumentFetch
对全局的fetch
做二次封装
- 内部调用
- 用户通过
fetch
发送请求- 整合上报信息
- 遍历上一步添加的回调函数
- 创建唯一事务用于上报信息
- 在
fetch
请求头中添加sentry-trace
字段
- 调用原生方法发送请求
- 请求响应后,根据返回的状态再次遍历上一步添加的回调函数
- 请求成功时,记录响应状态
- 上报本次请求
- 结束本次捕获
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!