前置關於addInstrumentationHandler
和fill
方法能夠在第一篇文章中瞭解sentry-javascript解析(一)fetch如何捕獲javascript
咱們首先從新複習一下如何使用XHR
發送一個請求。java
// 來源mdn
const req = new XMLHttpRequest();
req.addEventListener("load", (res) => console.log(res));
req.open("GET", "http://www.example.org/example.txt");
req.send();
複製代碼
接下來咱們看看sentry
是如何捕獲XHR
的。typescript
這裏與fetch
方法捕獲是共用的方法,在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
中看到:markdown
if (traceXHR) {
addInstrumentationHandler({
callback: (handlerData: XHRData) => {
xhrCallback(handlerData, shouldCreateSpan, spans);
},
type: 'xhr',
});
}
複製代碼
按照addInstrumentationHandler
的代碼咱們能夠準確看出經過type: 'xhr'
接下來應該執行instrumentXHR
方法,咱們來看一下這個方法的代碼:閉包
function instrumentXHR() {
if (!('XMLHttpRequest' in global)) {
return;
}
const requestKeys: XMLHttpRequest[] = [];
const requestValues: Array<any>[] = [];
const xhrproto = XMLHttpRequest.prototype;
// 封裝XHR的open方法
fill(
xhrproto,
'open',
function(originalOpen: () => void) {
return function(this: SentryWrappedXMLHttpRequest, ...args: any[]) {
const xhr = this;
const url = args[1];
// 緩存本次請求的method和url
xhr.__sentry_xhr__ = {
method: isString(args[0]) ? args[0].toUpperCase() : args[0],
url: args[1],
};
if (isString(url) && xhr.__sentry_xhr__.method === 'POST' && url.match(/sentry_key/)) {
// 若是是post請求,且請求地址中包含了sentry_key字樣,則添加__sentry_own_request__標誌這次請求爲sentry上報發出的
xhr.__sentry_own_request__ = true;
}
// readyState變化回調
const onreadystatechangeHandler = function(): void {
// 4表示請求結束
if (xhr.readyState === 4) {
try {
if (xhr.__sentry_xhr__) {
// 記錄響應狀態
xhr.__sentry_xhr__.status_code = xhr.status;
}
} catch (e) {
}
try {
const requestPos = requestKeys.indexOf(xhr);
if (requestPos !== -1) {
// 彈出send時緩存的請求內容
requestKeys.splice(requestPos);
const args = requestValues.splice(requestPos)[0];
if (xhr.__sentry_xhr__ && args[0] !== undefined) {
xhr.__sentry_xhr__.body = args[0] as XHRSendInput;
}
}
} catch (e) {
/* do nothing */
}
// 遍歷xhr對應回調
triggerHandlers('xhr', {
args,
endTimestamp: Date.now(),
startTimestamp: Date.now(),
xhr,
});
}
};
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
// 若是onreadystatechange是一個方法,則使用高階函數封裝onreadystatechange方法
fill(xhr, 'onreadystatechange', function(original: WrappedFunction): Function {
return function(...readyStateArgs: any[]): void {
onreadystatechangeHandler();
return original.apply(xhr, readyStateArgs);
};
});
} else {
// 不然直接監聽onreadystatechange事件
xhr.addEventListener('readystatechange', onreadystatechangeHandler);
}
// 原生方法調用
return originalOpen.apply(xhr, args);
};
});
// 封裝XHR的send方法
fill(xhrproto, 'send', function(originalSend: () => void): () => void {
return function(this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
// 緩存本次請求的request和請求參數
requestKeys.push(this);
requestValues.push(args);
// 遍歷xhr對應的回調
triggerHandlers('xhr', {
args,
startTimestamp: Date.now(),
xhr: this,
});
// 原生方法調用
return originalSend.apply(this, args);
};
});
}
複製代碼
咱們能夠經過上面的代碼瞭解到,sentry
封裝了XMLHttpRequest
的open
、send
方法,並且在用戶調用open
方法時會封裝onreadystatechange
方法。app
接下來咱們再看一下XHR
回調中都作了哪些事情函數
function xhrCallback( handlerData: XHRData, // 拼接後的數據 shouldCreateSpan: (url: string) => boolean, // 用於判斷當前url是否應該被捕獲 spans: Record<string, Span>, // 全局緩存事務 ): void {
// 獲取用戶當前的配置
const currentClientOptions = getCurrentHub().getClient()?.getOptions();
if (
!(currentClientOptions && hasTracingEnabled(currentClientOptions)) ||
!(handlerData.xhr && handlerData.xhr.__sentry_xhr__ && shouldCreateSpan(handlerData.xhr.__sentry_xhr__.url)) ||
handlerData.xhr.__sentry_own_request__
) {
return;
}
// 獲取在open方法時記錄的method和url
const xhr = handlerData.xhr.__sentry_xhr__;
if (handlerData.endTimestamp && handlerData.xhr.__sentry_xhr_span_id__) {
// 請求結束
const span = spans[handlerData.xhr.__sentry_xhr_span_id__];
if (span) {
// 記錄響應狀態碼
span.setHttpStatus(xhr.status_code);
span.finish();
delete spans[handlerData.xhr.__sentry_xhr_span_id__];
}
return;
}
// 建立一個新的事務
const activeTransaction = getActiveTransaction();
if (activeTransaction) {
const span = activeTransaction.startChild({
data: {
...xhr.data,
type: 'xhr',
method: xhr.method,
url: xhr.url,
},
description: `${xhr.method} ${xhr.url}`,
op: 'http',
});
// 添加事物惟一標誌
handlerData.xhr.__sentry_xhr_span_id__ = span.spanId;
spans[handlerData.xhr.__sentry_xhr_span_id__] = span;
if (handlerData.xhr.setRequestHeader) {
try {
// xhr請求時,在請求頭添加sentry-trace字段
handlerData.xhr.setRequestHeader('sentry-trace', span.toTraceparent());
} catch (_) {
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
}
}
}
}
複製代碼
經過上面的代碼,咱們能夠了解到在發送請求的時候,sentry
會經過setRequestHeader
方法添加sentry-trace
請求頭。在請求結束後,上報本次請求相關信息。post
對比sentry
對fetch
的封裝,咱們能夠發現二者大部分仍是神似的,咱們按照步驟總結一下:fetch
traceXHR
確認開啓XHR
捕獲,配置tracingOrigins
確認要捕獲的url
shouldCreateSpanForRequest
添加對XHR
的聲明週期的回調
instrumentXHR
對全局的XHR
作二次封裝
open
、send
方法,其中在調用open
方法時會封裝onreadystatechange
方法/事件XHR
的open
方法
method
、url
onreadystatechange
方法/事件open
方法XHR
的send
方法
sentry-trace
字段send
方法onreadystatechange
狀態改變觸發回調
4
請求結束,記錄請求狀態碼