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
字段