preface

Sentry is no stranger to front-end engineers. This article explains how sentry is implemented to catch various errors through source code.

  • Sentry -javascript repository address
  • sentryBasic usage:The official address

Lead to

Let’s start by looking at two key tool approaches

AddInstrumentationHandler secondary packaging native method

We start with the @ the sentry/uitls instrument. The ts file addInstrumentationHandler method:

function addInstrumentationHandler(handler: InstrumentHandler) :void {
  if (
    !handler || 
    typeofhandler.type ! = ='string' || 
    typeofhandler.callback ! = ='function'  
  ) {
    return;
  }
  // Initializes the corresponding type of callback
  handlers[handler.type] = handlers[handler.type] || [];
  // Add callback queue
  (handlers[handler.type] as InstrumentHandlerCallback[]).push(handler.callback);
  instrument(handler.type);
}
// Global closure
const instrumented: { [key inInstrumentHandlerType]? :boolean } = {};

function instrument(type: InstrumentHandlerType) :void {
  if (instrumented[type]) {
    return;
  }
  Global closures prevent double encapsulation
  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); }}Copy the code

AddInstrumentationHandler collect callback and call the corresponding method of native method for secondary packaging.

Fill wraps a given object method with a higher-order function

function fill(
    source: { [key: string] :any },  // Target object
    name: string.// Override the field namereplacementFactory: (... args:any[]) = >any // Encapsulate higher-order functions
) :void {
  // Non-existent fields are not encapsulated
  if(! (namein source)) {
    return;
  }
  // The native method
  const original = source[name] as() = >any;
  // Higher order function
  const wrapped = replacementFactory(original) as WrappedFunction;

  if (typeof wrapped === 'function') {
    try {
      // Specify an empty object prototype for higher-order functions
      wrapped.prototype = wrapped.prototype || {};
      Object.defineProperties(wrapped, {
        __sentry_original__: {
          enumerable: false.value: original,
        },
      });
    } catch (_Oo) {
    }
  }
  // override native methods
  source[name] = wrapped;
}
Copy the code

Fetch error capture

Specifying URL capture

At sentry initialization, which urls can be captured using tracingOrigins, sentry caches all urls that should be captured using scoped closures, eliminating repeated traversal.

// Scope closure
const urlMap: Record<string.boolean> = {};
// Determine whether the current URL should be captured
const defaultShouldCreateSpan = (url: string) :boolean= > {
  if (urlMap[url]) {
    return urlMap[url];
  }
  const origins = tracingOrigins;
  // Cache urls without repeated traversal
  urlMap[url] =
    origins.some((origin: string | RegExp) = >isMatchingPattern(url, origin)) && ! isMatchingPattern(url,'sentry_key');
  return urlMap[url];
};
Copy the code

Add a capture callback

Next, we see in @sentry/browser:

if (traceFetch) {
    addInstrumentationHandler({
        callback: (handlerData: FetchData) = > {
            fetchCallback(handlerData, shouldCreateSpan, spans);
        },
        type: 'fetch'}); }Copy the code

Higher-order functions encapsulate fetch

InstrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch instrumentFetch

function instrumentFetch() :void {
  if(! supportsNativeFetch()) {return;
  }

  fill(global.'fetch'.function(originalFetch) {
    // The wrapped fetch method
    return function(. args:any[]) :void {
      const handlerData = {
        args,
        fetchData: {
          method: getFetchMethod(args),
          url: getFetchUrl(args),
        },
        startTimestamp: Date.now(),
      };
      // Execute the callback methods of fetch Type in sequence
      triggerHandlers('fetch', {
        ...handlerData,
      });
      // Redirect to this with apply
      return originalFetch.apply(global, args).then(
      	// The request succeeded
        (response: Response) = > {
          triggerHandlers('fetch', {
            ...handlerData,
            endTimestamp: Date.now(),
            response,
          });
          return response;
        },
        // The request failed
        (error: Error) = > {
          triggerHandlers('fetch', {
            ...handlerData,
            endTimestamp: Date.now(),
            error,
          });
          throwerror; }); }; }); }Copy the code

We can use the above code found sentry encapsulates the fetch method, after the end of the request, the priority traversal in addInstrumentationHandler cache callbacks, then continue through to the result to subsequent user callback.

What happens inside the capture callback function

Let’s take a look at what happens in the FETCH callback

export function fetchCallback(
  handlerData: FetchData, // Integrate the data content too
  shouldCreateSpan: (url: string) = >boolean.// Determine whether the current URL needs to be captured
  spans: Record<string, Span>, 
) :void {
  // Get the user configuration
  constcurrentClientOptions = getCurrentHub().getClient()? .getOptions();if(! (currentClientOptions && hasTracingEnabled(currentClientOptions)) || ! (handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url)) ) {return;
  }
  // Only the request containing the transaction ID is processed
  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;
  }
  // Start the request and create a transaction
  const activeTransaction = getActiveTransaction();
  if (activeTransaction) {
    const span = activeTransaction.startChild({
      data: {
        ...handlerData.fetchData,
        type: 'fetch',},description: `${handlerData.fetchData.method} ${handlerData.fetchData.url}`.op: 'http'});// Add a unique ID
    handlerData.fetchData.__span = span.spanId;
    // Record the unique ID
    spans[span.spanId] = span;
    // Depending on how fetch is used, the first argument can be either the Request address or the Request object
    const request = (handlerData.args[0] = handlerData.args[0] as string | Request);
    // According to the use of fetch, the second argument is the relevant configuration item of the request
    const options = (handlerData.args[1] = (handlerData.args[1] as { [key: string] :any}) | | {});// The configuration item is headers by default (possibly undefined).
    let headers = options.headers;
    if (isInstanceOf(request, Request)) {
      // If request is a request object, headers uses request
      headers = (request as Request).headers;
    }
    if (headers) {
      // If the user has set headers, add a sentry-trace field to the request header
      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 {
      // The user does not set headers
      headers = { 'sentry-trace': span.toTraceparent() };
    }
    // We initialize handlerData.args[1] when we invoke the options declaration, overwriting the fetch header with the reference typeoptions.headers = headers; }}Copy the code

conclusion

At this point, we can know how sentry captures information in the FETCH. Let’s summarize the steps:

  • User configurationtraceFetchConfirm to openfetchCapture, configuretracingOriginsConfirm what to captureurl
  • throughshouldCreateSpanForRequestTo add tofetchThe callback of the declaration cycle
    • Internal callsinstrumentFetchThe globalfetchDo secondary packaging
  • By the userfetchSend the request
    • Integrate reporting information
    • Iterate over the callback function you added in the previous step
      • Create a unique transaction for reporting information
      • infetchIn the request headersentry-tracefield
    • Call the native method to send the request
    • After requesting the response, iterate over the callback function added in the previous step based on the returned status
      • When the request succeeds, record the response status
      • Report this Request
  • End the capture