preface
Front about addInstrumentationHandler and fill method can understand in the first article, sentry – javascript parsing (a) the fetch how to capture
Lead to
Let’s start by reviewing how to send a request using XHR.
MDN / / source
const req = new XMLHttpRequest();
req.addEventListener("load".(res) = > console.log(res));
req.open("GET"."http://www.example.org/example.txt");
req.send();
Copy the code
Let’s take a look at how Sentry captures XHR.
XHR error capture
Specifying URL capture
This method is shared with the fetch method. During sentry initialization, which urls can be captured using tracingOrigins, sentry caches all urls that should be captured using the scope closure, eliminating repeated traversal.
// Scope closure
const urlMap: Record<string.boolean> = {};
// Determine whether the current URL should be captured
const defaultShouldCreateSpan = (url: string) :boolean= > {
if (urlMap[url]) {
return urlMap[url];
}
const origins = tracingOrigins;
// Cache urls without repeated traversal
urlMap[url] =
origins.some((origin: string | RegExp) = >isMatchingPattern(url, origin)) && ! isMatchingPattern(url,'sentry_key');
return urlMap[url];
};
Copy the code
Add a capture callback
Next, we see in @sentry/browser:
if (traceXHR) {
addInstrumentationHandler({
callback: (handlerData: XHRData) = > {
xhrCallback(handlerData, shouldCreateSpan, spans);
},
type: 'xhr'}); }Copy the code
Higher-order functions encapsulate XHR
According to the code of addInstrumentationHandler we can accurately see through type: ‘XHR should execute next instrumentXHR method, we take a look at the way the code:
function instrumentXHR() {
if(! ('XMLHttpRequest' in global)) {
return;
}
const requestKeys: XMLHttpRequest[] = [];
const requestValues: Array<any= > [] [];const xhrproto = XMLHttpRequest.prototype;
// Encapsulate XHR's open method
fill(
xhrproto,
'open'.function(originalOpen: () => void) {
return function(this: SentryWrappedXMLHttpRequest, ... args:any[]) {
const xhr = this;
const url = args[1];
// Cache the method and URL of this request
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/)) {
// If this is a POST request and the request address contains sentry_key, add __sentry_own_request__ to indicate that this request was sent for sentry report
xhr.__sentry_own_request__ = true;
}
// readyState change callback
const onreadystatechangeHandler = function() :void {
// 4 Indicates that the request is complete
if (xhr.readyState === 4) {
try {
if (xhr.__sentry_xhr__) {
// Record the response statusxhr.__sentry_xhr__.status_code = xhr.status; }}catch (e) {
}
try {
const requestPos = requestKeys.indexOf(xhr);
if(requestPos ! = = -1) {
// Cache request content when send is displayed
requestKeys.splice(requestPos);
const args = requestValues.splice(requestPos)[0];
if (xhr.__sentry_xhr__ && args[0]! = =undefined) {
xhr.__sentry_xhr__.body = args[0] asXHRSendInput; }}}catch (e) {
/* do nothing */
}
// Iterate over the XHR corresponding callback
triggerHandlers('xhr', {
args,
endTimestamp: Date.now(),
startTimestamp: Date.now(), xhr, }); }};if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
// If onReadyStatechange is a method, use a higher-order function to wrap the onReadyStatechange method
fill(xhr, 'onreadystatechange'.function(original: WrappedFunction) :Function {
return function(. readyStateArgs:any[]) :void {
onreadystatechangeHandler();
return original.apply(xhr, readyStateArgs);
};
});
} else {
// If not, listen for the onreadyStatechange event
xhr.addEventListener('readystatechange', onreadystatechangeHandler);
}
// Native method calls
return originalOpen.apply(xhr, args);
};
});
// Encapsulate XHR's send method
fill(xhrproto, 'send'.function(originalSend: () => void) : () = >void {
return function(this: SentryWrappedXMLHttpRequest, ... args:any[]) :void {
// Cache the request and request parameters for this request
requestKeys.push(this);
requestValues.push(args);
// Iterate over XHR's corresponding callback
triggerHandlers('xhr', {
args,
startTimestamp: Date.now(),
xhr: this});// Native method calls
return originalSend.apply(this, args);
};
});
}
Copy the code
As you can see from the above code, Sentry encapsulates the OPEN and send methods of XMLHttpRequest, and the onReadyStatechange method when the user calls the open method.
What happens inside the capture callback function
Let’s take a look at what happens in the XHR callback
function xhrCallback(
handlerData: XHRData, // Data after the concatenation
shouldCreateSpan: (url: string) = >boolean.// Determine whether the current URL should be captured
spans: Record<string, Span>, // Global cache transactions
) :void {
// Get the current configuration of the user
constcurrentClientOptions = 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;
}
// Get the method and URL recorded when the method is open
const xhr = handlerData.xhr.__sentry_xhr__;
if (handlerData.endTimestamp && handlerData.xhr.__sentry_xhr_span_id__) {
// End of request
const span = spans[handlerData.xhr.__sentry_xhr_span_id__];
if (span) {
// Record the response status code
span.setHttpStatus(xhr.status_code);
span.finish();
delete spans[handlerData.xhr.__sentry_xhr_span_id__];
}
return;
}
Create a new transaction
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'});// Add something unique
handlerData.xhr.__sentry_xhr_span_id__ = span.spanId;
spans[handlerData.xhr.__sentry_xhr_span_id__] = span;
if (handlerData.xhr.setRequestHeader) {
try {
// Add a sentry-trace field to the request header when XHR requests it
handlerData.xhr.setRequestHeader('sentry-trace', span.toTraceparent());
} catch (_) {
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.}}}}Copy the code
From the above code, we can see that sentry adds the sentry-trace header to the request through the setRequestHeader method when sending the request. After the request is complete, the information related to the request is reported.
conclusion
By comparing the encapsulation of Fetch by Sentry, we can find that the two are mostly similar. Let’s summarize the following steps:
- User configuration
traceXHR
Confirm to openXHR
Capture, configuretracingOrigins
Confirm what to captureurl
- through
shouldCreateSpanForRequest
To add toXHR
The callback of the declaration cycle- Internal calls
instrumentXHR
The globalXHR
Do secondary packaging- encapsulation
open
,send
Method, which is calledopen
Method will encapsulateonreadystatechange
Method/event
- encapsulation
- Internal calls
- The user calls
XHR
theopen
methods- Cached for this request
method
,url
- encapsulation
onreadystatechange
Method/event - Call native
open
methods
- Cached for this request
- The user calls
XHR
thesend
methods- Iterate over the callback function you added in the previous step
- Create a unique transaction for reporting information
- Add in the request header
sentry-trace
field
- Call native
send
methods
- Iterate over the callback function you added in the previous step
onreadystatechange
A state change triggers a callback- If the current state is
4
The request is complete, and the request status code is recorded - Iterate over the callback function you added in the previous step
- Report this Request
- If the current state is
- End the capture