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 configurationtraceXHRConfirm to openXHRCapture, configuretracingOriginsConfirm what to captureurl
  • throughshouldCreateSpanForRequestTo add toXHRThe callback of the declaration cycle
    • Internal callsinstrumentXHRThe globalXHRDo secondary packaging
      • encapsulationopen,sendMethod, which is calledopenMethod will encapsulateonreadystatechangeMethod/event
  • The user callsXHRtheopenmethods
    • Cached for this requestmethod,url
    • encapsulationonreadystatechangeMethod/event
    • Call nativeopenmethods
  • The user callsXHRthesendmethods
    • Iterate over the callback function you added in the previous step
      • Create a unique transaction for reporting information
      • Add in the request headersentry-tracefield
    • Call nativesendmethods
  • onreadystatechangeA state change triggers a callback
    • If the current state is4The request is complete, and the request status code is recorded
    • Iterate over the callback function you added in the previous step
      • Report this Request
  • End the capture