最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • sentry-javascript解析(二)XHR如何捕获

    正文概述 掘金(superior)   2021-02-17   733

    前言

    前置关于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请求结束,记录请求状态码
      • 遍历上一步添加的回调函数
        • 上报本次请求
    • 结束本次捕获

    起源地下载网 » sentry-javascript解析(二)XHR如何捕获

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元