sentry-javascript解析(一)fetch如何捕获

前言

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

前置准备

咱们首先看一下两个关键的工具方法前端

addInstrumentationHandler 二次封装原生方法

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

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收集相关的回调并调用对应方法对原生方法作二次封装。git

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,省去重复的遍历。github

// 做用域闭包
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中看到:typescript

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中缓存的回调,而后再将结果继续透传给后续的用户回调。markdown

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

接下来咱们再看一下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字段
    • 调用原生方法发送请求
    • 请求响应后,根据返回的状态再次遍历上一步添加的回调函数
      • 请求成功时,记录响应状态
      • 上报本次请求
  • 结束本次捕获
相关文章
相关标签/搜索