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

1,738 阅读4分钟

前言

前置关于addInstrumentationHandlerfill方法可以在第一篇文章中了解sentry-javascript解析(一)fetch如何捕获

前置准备

我们首先重新复习一下如何使用XHR发送一个请求。

// 来源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的。

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中看到:

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方法。

捕获回调函数内都做了什么

接下来我们再看一下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请求头。在请求结束后,上报本次请求相关信息。

总结

对比sentryfetch的封装,我们可以发现两者大部分还是神似的,我们按照步骤总结一下:

  • 由用户配置traceXHR确认开启XHR捕获,配置tracingOrigins确认要捕获的url
  • 通过shouldCreateSpanForRequest添加对XHR的声明周期的回调
    • 内部调用instrumentXHR对全局的XHR做二次封装
      • 封装opensend方法,其中在调用open方法时会封装onreadystatechange方法/事件
  • 用户调用XHRopen方法
    • 缓存本次请求的methodurl
    • 封装onreadystatechange方法/事件
    • 调用原生open方法
  • 用户调用XHRsend方法
    • 遍历上一步添加的回调函数
      • 创建唯一事务用于上报信息
      • 在请求头中添加sentry-trace字段
    • 调用原生send方法
  • onreadystatechange状态改变触发回调
    • 如果当前状态为4请求结束,记录请求状态码
    • 遍历上一步添加的回调函数
      • 上报本次请求
  • 结束本次捕获