sentry
对于前端工程师来讲并不陌生,本文主要经过源码讲解sentry
是如何实现捕获各类错误。javascript
sentry
基本使用方法:官方地址咱们首先看一下两个关键的工具方法前端
咱们首先来看@sentry/uitls
的instrument.ts
文件的addInstrumentationHandler
方法:java
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
收集相关的回调并调用对应方法对原生方法作二次封装。git
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;
}
复制代码
在sentry
初始化的时候,咱们能够经过tracingOrigins
捕获哪些url
,sentry
经过做用域闭包缓存全部应该捕获的url
,省去重复的遍历。github
// 做用域闭包
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
中看到:typescript
if (traceFetch) {
addInstrumentationHandler({
callback: (handlerData: FetchData) => {
fetchCallback(handlerData, shouldCreateSpan, spans);
},
type: '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
中缓存的回调,而后再将结果继续透传给后续的用户回调。markdown
接下来咱们再看一下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
字段