sentry-javascript解析(二)XHR如何捕獲

前言

前置關於addInstrumentationHandlerfill方法能夠在第一篇文章中瞭解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

XHR錯誤捕獲

指定url捕獲

這裏與fetch方法捕獲是共用的方法,在sentry初始化的時候,咱們能夠經過tracingOrigins捕獲哪些urlsentry經過做用域閉包緩存全部應該捕獲的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',
    });
}
複製代碼

高階函數封裝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封裝了XMLHttpRequestopensend方法,並且在用戶調用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

總結

對比sentryfetch的封裝,咱們能夠發現二者大部分仍是神似的,咱們按照步驟總結一下:fetch

  • 由用戶配置traceXHR確認開啓XHR捕獲,配置tracingOrigins確認要捕獲的url
  • 經過shouldCreateSpanForRequest添加對XHR的聲明週期的回調
    • 內部調用instrumentXHR對全局的XHR作二次封裝
      • 封裝opensend方法,其中在調用open方法時會封裝onreadystatechange方法/事件
  • 用戶調用XHRopen方法
    • 緩存本次請求的methodurl
    • 封裝onreadystatechange方法/事件
    • 調用原生open方法
  • 用戶調用XHRsend方法
    • 遍歷上一步添加的回調函數
      • 建立惟一事務用於上報信息
      • 在請求頭中添加sentry-trace字段
    • 調用原生send方法
  • onreadystatechange狀態改變觸發回調
    • 若是當前狀態爲4請求結束,記錄請求狀態碼
    • 遍歷上一步添加的回調函數
      • 上報本次請求
  • 結束本次捕獲
相關文章
相關標籤/搜索