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

    正文概述 掘金(superior)   2021-02-16   921

    前言

    sentry对于前端工程师来说并不陌生,本文主要通过源码讲解sentry是如何实现捕获各种错误。

    • sentry-javascript仓库地址
    • sentry基本使用方法:官方地址

    前置准备

    我们首先看一下两个关键的工具方法

    addInstrumentationHandler 二次封装原生方法

    我们首先来看@sentry/uitlsinstrument.ts文件的addInstrumentationHandler方法:

    function addInstrumentationHandler(handler: InstrumentHandler): void {
      if (
        !handler || 
        typeof handler.type !== 'string' || 
        typeof handler.callback !== 'function'  
      ) {
        return;
      }
      // 初始化对应type的回调
      handlers[handler.type] = handlers[handler.type] || [];
      // 添加回调队列
      (handlers[handler.type] as InstrumentHandlerCallback[]).push(handler.callback);
      instrument(handler.type);
    }
    // 全局闭包
    const instrumented: { [key in InstrumentHandlerType]?: boolean } = {};
    
    function instrument(type: InstrumentHandlerType): void {
      if (instrumented[type]) {
        return;
      }
      // 全局闭包防止重复封装
      instrumented[type] = true;
    
      switch (type) {
        case 'console':
          instrumentConsole();
          break;
        case 'dom':
          instrumentDOM();
          break;
        case 'xhr':
          instrumentXHR();
          break;
        case 'fetch':
          instrumentFetch();
          break;
        case 'history':
          instrumentHistory();
          break;
        case 'error':
          instrumentError();
          break;
        case 'unhandledrejection':
          instrumentUnhandledRejection();
          break;
        default:
          logger.warn('unknown instrumentation type:', type);
      }
    }
    

    addInstrumentationHandler收集相关的回调并调用对应方法对原生方法做二次封装。

    fill 用高阶函数包装给定的对象方法

    function fill(
        source: { [key: string]: any },  // 目标对象
        name: string, // 覆盖字段名
        replacementFactory: (...args: any[]) => any // 封装的高阶函数
    ): void {
      // 不存在的字段不做封装
      if (!(name in source)) {
        return;
      }
      // 原生方法
      const original = source[name] as () => any;
      // 高阶函数
      const wrapped = replacementFactory(original) as WrappedFunction;
    
      if (typeof wrapped === 'function') {
        try {
          // 为高阶函数指定一个空对象原型
          wrapped.prototype = wrapped.prototype || {};
          Object.defineProperties(wrapped, {
            __sentry_original__: {
              enumerable: false,
              value: original,
            },
          });
        } catch (_Oo) {
        }
      }
      // 覆盖原生方法
      source[name] = wrapped;
    }
    

    fetch错误捕获

    指定url捕获

    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 (traceFetch) {
        addInstrumentationHandler({
            callback: (handlerData: FetchData) => {
                fetchCallback(handlerData, shouldCreateSpan, spans);
            },
            type: 'fetch',
        });
    }
    

    高阶函数封装fetch

    按照上面的代码我们可以准确看出通过type: 'fetch'接下来应该执行instrumentFetch方法,我们来看一下这个方法的代码:

    function instrumentFetch(): void {
      if (!supportsNativeFetch()) {
        return;
      }
    
      fill(global, 'fetch', function(originalFetch) {
        // 封装后的fetch方法
        return function(...args: any[]): void {
          const handlerData = {
            args,
            fetchData: {
              method: getFetchMethod(args),
              url: getFetchUrl(args),
            },
            startTimestamp: Date.now(),
          };
          // 依次执行fetch type的回调方法 
          triggerHandlers('fetch', {
            ...handlerData,
          });
          // 通过apply重新指向this
          return originalFetch.apply(global, args).then(
          	// 请求成功
            (response: Response) => {
              triggerHandlers('fetch', {
                ...handlerData,
                endTimestamp: Date.now(),
                response,
              });
              return response;
            },
            // 请求失败
            (error: Error) => {
              triggerHandlers('fetch', {
                ...handlerData,
                endTimestamp: Date.now(),
                error,
              });
              throw error;
            },
          );
        };
      });
    }
    

    我们可以通过上面的代码发现sentry封装了fetch方法,在请求结束之后,优先遍历了在addInstrumentationHandler中缓存的回调,然后再将结果继续透传给后续的用户回调。

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

    接下来我们再看一下fetch回调中都做了哪些事情

    export function fetchCallback(
      handlerData: FetchData, // 整合的数据内容过
      shouldCreateSpan: (url: string) => boolean, // 用于判断当前url是否需要捕获
      spans: Record<string, Span>, 
    ): void {
      // 获取用户配置
      const currentClientOptions = getCurrentHub().getClient()?.getOptions();
      
      if (
        !(currentClientOptions && hasTracingEnabled(currentClientOptions)) ||
        !(handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url))
      ) {
        return;
      }
      // 请求结束,只处理包含事务id的请求
      if (handlerData.endTimestamp && handlerData.fetchData.__span) {
        const span = spans[handlerData.fetchData.__span];
        if (span) {
          const response = handlerData.response;
          if (response) {
            span.setHttpStatus(response.status);
          }
          span.finish();
    
          delete spans[handlerData.fetchData.__span];
        }
        return;
      }
      // 开始请求,创建一个事务
      const activeTransaction = getActiveTransaction();
      if (activeTransaction) {
        const span = activeTransaction.startChild({
          data: {
            ...handlerData.fetchData,
            type: 'fetch',
          },
          description: `${handlerData.fetchData.method} ${handlerData.fetchData.url}`,
          op: 'http',
        });
        // 添加唯一id
        handlerData.fetchData.__span = span.spanId;
        // 记录唯一id
        spans[span.spanId] = span;
        // 根据fetch的用法第一个参数可以是 请求地址 或者是 Request对象
        const request = (handlerData.args[0] = handlerData.args[0] as string | Request);
        // 根据fetch的用法第二个参数是请求的相关配置项
        const options = (handlerData.args[1] = (handlerData.args[1] as { [key: string]: any }) || {});
        // 默认取配置项的headers(可能为undefined)
        let headers = options.headers;
        if (isInstanceOf(request, Request)) {
          // 如果request是Request对象,则headers使用Request的
          headers = (request as Request).headers;
        }
        if (headers) {
          // 用户已经设置了headers,则在请求头添加sentry-trace字段
          if (typeof headers.append === 'function') {
            headers.append('sentry-trace', span.toTraceparent());
          } else if (Array.isArray(headers)) {
            headers = [...headers, ['sentry-trace', span.toTraceparent()]];
          } else {
            headers = { ...headers, 'sentry-trace': span.toTraceparent() };
          }
        } else {
          // 用户未设置headers
          headers = { 'sentry-trace': span.toTraceparent() };
        }
        // 这里借用了options声明时会初始化handlerData.args[1],使用引用类型覆盖了fetch的请求头
        options.headers = headers;
      }
    }
    

    总结

    到此我们就可以知道sentry是如何在fetch中捕获信息的,我们按照步骤总结一下:

    • 由用户配置traceFetch确认开启fetch捕获,配置tracingOrigins确认要捕获的url
    • 通过shouldCreateSpanForRequest添加对fetch的声明周期的回调
      • 内部调用instrumentFetch对全局的fetch做二次封装
    • 用户通过fetch发送请求
      • 整合上报信息
      • 遍历上一步添加的回调函数
        • 创建唯一事务用于上报信息
        • fetch请求头中添加sentry-trace字段
      • 调用原生方法发送请求
      • 请求响应后,根据返回的状态再次遍历上一步添加的回调函数
        • 请求成功时,记录响应状态
        • 上报本次请求
    • 结束本次捕获

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

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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