One, foreword

Recently, I am very interested in front-end monitoring, so I used Sentry, an excellent open source library in front-end monitoring, and studied the Sentry source code to sort out this series of articles, hoping to help you better understand the principle of front-end monitoring.

In this series of articles, I will explain the whole process in an easy way, combining the API on the official website with the flow chart.

Here are the topics I have completed and plan for my next post:

  • Understand Sentry initialization (this article)
  • How does Sentry handle error data
  • How does Sentry report the error when he gets it?

How to debug Sentry source code you can follow the following (my version is: “5.29.2”) :

  • git clone [email protected]:getsentry/sentry-javascript.git
  • Go to Packages/Browser for NPM I download dependencies
  • Then NPM run Build in Packages/Browser to generate build folder
  • Into the packages/browser/examples open index. The HTML can happy debugging (here suggest that use the Live Server open)
  • Note: packages/browser/examples of bundle. Js source after the packaging, he pointed to the packages/browser/build/bundle. Js. You’ll also find bundle.es6.js in the build directory. If you want to read it in es6 mode, you can replace it with bundle.es6.js

Second, the introduction

We can initialize Sentry as follows:

Sentry.init({
  dsn: 'xxxxxx'.autoSessionTracking: true.enabled: true.release: 'xxxxx'.environment: 'development'.default_integrations: true.ignoreErrors: [/PickleRick_\d\d/.'RangeError'].denyUrls: ['external-lib.js'].allowUrls: ['http://localhost:5500'.'https://browser.sentry-cdn']});Copy the code

Here need to understand a few concepts for easy reading source code:

  • DSN: The identity of the configuration required by the Sentry SDK. It consists of several parts, including protocol, public key and key, server address, and project identifier.

  • Release: indicates the version number. Release can be associated with sourceMap, and with a release version number, we can better find errors.

  • Hub: You can think of the Hub as the central point our SDK uses to route events to Sentry, also known as the control center. When you call init (), a hub is created on which a client client and a blank scope are created. The center is then associated with the current thread and holds the range stack internally.

  • Scope: Will contain useful information that should be sent with the event. For example, a context or breadcrumb Breadcrumbs will be stored on the scope. When a scope is pushed, it inherits all data from its parent scope, and when it goes off the stack, all changes are restored.

  • Client: indicates the client.

  • Context: indicates the context. Provide additional context data. Typically, this is data relevant to the current user and environment, and this context is shared between any problems captured during its lifetime. You can also customize the context, see the official website

  • Integrations: List of names used to identify enabled integration. The list should contain all enabled integrations, including the default ones. The default integration is included because different SDK versions may contain different default integrations.

  • Crumbs: Just the crumbs. Use breadcrumbs to create a trace of what happened before the event happened. These events are very similar to traditional logging, but can record richer structured data.

Three, entrance

In the packages/browser/SRC/SDK. Ts can be seen in the Sentry. The init entrance, the options is the user of the incoming parameters

export function init(options: BrowserOptions = {}) :void {
  if (options.defaultIntegrations === undefined) {
    options.defaultIntegrations = defaultIntegrations;
  }
  if (options.release === undefined) {
    const window = getGlobalObject<Window>();
    // This supports the variable that sentry-webpack-plugin injects
    if (window.SENTRY_RELEASE && window.SENTRY_RELEASE.id) {
      options.release = window.SENTRY_RELEASE.id; }}if (options.autoSessionTracking === undefined) {
    options.autoSessionTracking = false;
  }
  initAndBind(BrowserClient, options);
  if(options.autoSessionTracking) { startSessionTracking(); }}Copy the code

Analysis:

  • The entire initialization code is clear. If the user does not pass in defaultIntegrations, Release, and autoSessionTracking values, the assignment is performed.
  • InitAndBind is the focus, creating a new SDK client instance and binding it to the hub.

I will focus on initAndBind at the end, while other parts will be more in-depth based on the official website

Four, defaultIntegrations

Take a look at the official website:

System integrations are integrations enabled by default that integrate into the standard library or the interpreter itself. Sentry documents them so you can see what they do and that they can be disabled if they cause issues. To disable all system integrations, set default_integrations=False when calling init().

System integration is integration that is enabled by default and integrates into Standard Library or Interpreter. Sentry logs them so you can see what they do and they can be disabled if they cause problems. To disable all system integrations, set default_integrations = False when init () is called.

So when you init it you can set default_integrations = false to prevent it from assigning.


Now let’s look at the default values for defaultIntegrations:

In the packages/browser/SRC/SDK. Ts

// Default integration
export const defaultIntegrations = [
  new CoreIntegrations.InboundFilters(),
  new CoreIntegrations.FunctionToString(),
  new TryCatch(),
  new Breadcrumbs(),
  new GlobalHandlers(),
  new LinkedErrors(),
  new UserAgent(),
];
Copy the code

As you can see, defaultIntegrations integrates many classes by default. Let’s look at what each class does. (The implementation of each class is not relevant in this article, but will be covered in a future article.)

4.1 new CoreIntegrations. InboundFilters ()

Inbound data filters allow you to determine which errors, if any, Sentry should ignore. Explore these by navigating to [Project] » Project Settings » Inbound Filters.

Data filters allow you to determine which errors, if any, Sentry should ignore. Navigate to [Project] » Project Settings » Inbound Filters. To explore these.

This integration allows you to ignore specific errors based on the type, message, or URLs in a given exception. It ignores errors that start with Script error or Javascript error: Script error by default. To configure this integration, use ignoreErrors.denyUrls, and allowUrls SDK options directly. Keep in mind that denyURL and allowURL work only for captured exceptions, not raw message events.

This integration allows you to ignore specific errors based on type, message, or URL in a given exception. It ignores errors that begin with Script error or Javascript error: Script error by default. To configure this integration, use ignoreErrors, denyURL, and allowUrLSDK SDK directly. Keep in mind that denyURL and allowURL are only good for catching error exceptions, not normal messages.

Description:

It ignores errors that begin with Script error or Javascript error: Script error by default

This is actually related to window.onerror compatibility, which for script tags requires the crossorigin attribute to be added to the script tag and cross-domain on the server side. If you do not use this property, the error message will simply display Script error


Let’s use some test examples to deepen our understanding:

Set the init

Sentry.init({
	// ...
  ignoreErrors: [/PickleRick_\d\d/.'RangeError'].denyUrls: ['external-lib.js'].allowUrls: ['http://localhost:5500'.'https://browser.sentry-cdn']});Copy the code

Test cases

  <body>
    <button id="deny-url">denyUrls example</button>
    <button id="allow-url">allowUrls example</button>
    <button id="ignore-message">ignoreError message example</button>
    <button id="ignore-type">ignoreError type example</button>
  </body>
  <script>
    document.addEventListener('DOMContentLoaded'.() = > {
      / / the first
      document.querySelector('#deny-url').addEventListener('click'.() = > {
        console.log('click');
        const script = document.createElement('script');
        script.crossOrigin = 'anonymous';
        script.src =
          'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-l ib.js';
        document.body.appendChild(script);
      });
      / / the second
      document.querySelector('#allow-url').addEventListener('click'.() = > {
        const script = document.createElement('script');
        script.crossOrigin = 'anonymous';
        script.src =
          'https://rawgit.com/kamilogorek/cb67dafbd0e12b782bdcc1fbcaed2b87/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/lib.js';
        document.body.appendChild(script);
      });
      / / the third kind
      document.querySelector('#ignore-message').addEventListener('click'.() = > {
        throw new Error('Exception that will be ignored because of this keyword => PickleRick_42 <=');
      });
      / / the fourth
      document.querySelector('#ignore-type').addEventListener('click'.() = > {
        throw new RangeError("Exception that will be ignored because of it's type");
      });
    });
  </script>
Copy the code

Take a look at the results of the four buttons:

Description:

  • The first hits denyUrls
  • The second doesn’t fit into allowUrls
  • The third error message contains the PickleRick_42 related field
  • The fourth is hitting RangeError, which ignoreErrors ignores

4.2 new CoreIntegrations.FunctionToString

This integration allows the SDK to provide original functions and method names, even when our error or breadcrumbs handlers wrap them.

This integration enables the SDK to provide the original function and method names even if our errors or breadcrumbs wrap the original function

Look at the source code

    setupOnce() {
      originalFunctionToString = Function.prototype.toString;
      Function.prototype.toString = function(. args) {
        const context = this.__sentry_original__ || this;
        return originalFunctionToString.apply(context, args);
      };
    }
Copy the code

Analysis:

  • So the context is going to fetch and say fetchthis.__sentry_original__Or this itself (mainly for functions wrapped in errors or breadcrumbs)

4.3 new TryCatch ()

This integration wraps native time and events APIs (setTimeout.setInterval.requestAnimationFrame.addEventListener/removeEventListener) in try/catch blocks to handle async exceptions

This integrated packaging try/catch block in the original time and event apis (setTimeout and setInterval requestAnimationFrame addEventListener/removeEventListener) To handle asynchronous exceptions

4.4 new Breadcrumbs ()

This integration wraps native APIs to capture breadcrumbs. By default, the Sentry SDK wraps all APIs.

This integration wraps native apis to capture breadcrumbs.

Options:

{ beacon: boolean; // Log HTTP requests done with the Beacon API
  console: boolean; // Log calls to `console.log`, `console.debug`, etc
  dom: boolean; // Log all click and keypress events
  fetch: boolean; // Log HTTP requests done with the Fetch API
  history: boolean; // Log calls to `history.pushState` and friends
  sentry: boolean; // Log whenever we send an event to the server
  xhr: boolean; // Log HTTP requests done with the XHR API
}
Copy the code

So Breadcrumbs contain the following information:

  • Custom breadcrumb information
  • Console information of the console
  • UI click and press dom events
  • Fetch request
  • Historical error message
  • Whether to send it to the server
  • XHR request

4.5 new GlobalHandlers ()

This integration attaches global handlers to capture uncaught exceptions and unhandled rejections.

This integration attaches global handlers to catch uncaught exceptions and unhandled rejections

Alternative options:

{
  onerror: boolean;
  onunhandledrejection: boolean;
}
Copy the code

This error message in the next article to play front-end monitoring, a comprehensive analysis of Sentry source code (two) | Sentry how to handle error data explained to

4.6 new LinkedErrors ()

This integration allows you to configure linked errors. They’ll be recursively read up to a specified limit and lookup will be performed by a specific key. By default, the Sentry SDK sets the limit to five and the key used is cause.

This integration allows you to configure link errors. They will be read recursively to the specified limit and the lookup will be performed by the specified key. By default, the Sentry SDK sets the limit to 5 and uses the key cause.

Alternative options:

{
  key: string;
  limit: number;
}
Copy the code

4.7 new UserAgent ()

This integration attaches user-agent information to the event, which allows us to correctly catalog and tag them with specific OS, browser, and version information.

This integration attaches user agent information to events, which allows us to properly catalog them and tag them with specific operating system, browser, and version information.

4.8 summarize

DefaultIntegrations integrates many methods for Sentry, which uses type plug-ins to integrate various properties into the Sentry. This makes the code look very clean, and you can see what the integrations do.

Five, the release

Sentry will try to automatically configure a release, but it is also recommended to set it manually to ensure that the release is integrated with your deployment or synchronized with sourcemap uploads.

Sentry Release +sourceMap


Look at the source code:

  if (options.release === undefined) {
    const window = getGlobalObject<Window>();
    // This supports the variable that sentry-webpack-plugin injects
    if (window.SENTRY_RELEASE && window.SENTRY_RELEASE.id) {
      options.release = window.SENTRY_RELEASE.id; }}Copy the code

Analysis:

  • If release is not set, by default, the SDK will try to read this value from the SENTRY_RELEASE environment variable
  • The purpose of getGlobalObject here is toGet global variables by distinguishing whether the current environment is Node or browser.

Six, autoSessionTracking

When set to true, the SDK will send session events to Sentry. This is supported in all browser SDKs, emitting one session per pageload to Sentry.

This is disabled by default, and when enabled the SDK will send the session to Sentry. This is supported in all browser SDKS, and each page sends a session to Sentry.

  if (options.autoSessionTracking) {
    startSessionTracking();
  }
Copy the code

If the user passes in autoSessionTracking as true and starts calling startSessionTracking, let’s look at the startSessionTracking method

6.1 startSessionTracking

  function startSessionTracking() {
	// ...
    let loadResolved = document.readyState === 'complete';
    let fcpResolved = false;
    const possiblyEndSession = () = > {
      if(fcpResolved && loadResolved) { hub.endSession(); }};const resolveWindowLoaded = () = > {
      loadResolved = true;
      possiblyEndSession();
      window.removeEventListener('load', resolveWindowLoaded);
    };
    hub.startSession();
    if(! loadResolved) {window.addEventListener('load', resolveWindowLoaded);
    }
	// ...
  }
Copy the code

Here’s how session tracking works:

  1. Call the hub.startSession() method, which creates a Session with a new Session

  2. Then listen to see if the document is loaded and call hub.endSession() if it is.

  3. Look at the source code:

        endSession() {
          const { scope, client } = this.getStackTop();
          if(! scope)return;
          const session = scope.getSession && scope.getSession();
          if (session) {
            session.close();
            if(client && client.captureSession) { client.captureSession(session); } scope.setSession(); }}Copy the code

    Analysis:

    • Call session.close() is a call to session.update() to update the current session value and record the current session status.
    • If there is a client client, captureSession method will be called to send data to the client.
    • Finally scope.setSession() saves the session to the scope

initAndBind

InitAndBind initializes the client with New BrowserClient and binds the client to the Hub control center

Take a look at the source code:

 initAndBind(BrowserClient, options);
---------------------------------------------------------------------
function initAndBind(clientClass, options) {
    if (options.debug === true) {
        utils_1.logger.enable();
    }
    var hub = hub_1.getCurrentHub();
    var client = new clientClass(options);
    hub.bindClient(client);
}
Copy the code

Analysis:

  • InitAndBind () the first argument is BrowserClien representing the client class to be instantiated, and the second argument is options after initialization
  • So we can divide the analysis into three lines: the first is the client instantiated by BrowserClient, the second is to get the current control center Hub, and the third is to bind the client to the current control center Hub

7.1 BrowserClient

Look at the source code:

export class BrowserClient extends BaseClient<BrowserBackend.BrowserOptions> {
  public constructor(options: BrowserOptions = {}) {
    super(BrowserBackend, options);
  }
  // ...
}
Copy the code

Analysis:

  • BrowserClient inherits BaseClient and passes in BrowserBackend
  • BaseClient here provides some public methods such as captureEvent, captureException, captureMessage to report the news
  • BrowserBackend has the _setupTransport method to determine whether ajax uploads go fetch or XHR

7.2 getCurrentHub ()

Gets the current control center Hub

Look at the source code:

function getCurrentHub() {
    // Get main carrier (global for every environment)
    var registry = getMainCarrier();
    if(! hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(exports.API_VERSION)) {
        setHubOnCarrier(registry, new Hub());
    }
    // Prefer domains over global if they are there (applicable only to Node environment)
    if (utils_1.isNodeEnv()) {
        return getHubFromActiveDomain(registry);
    }
    // Return hub that lives on a global object
    return getHubFromCarrier(registry);
}
Copy the code

Analysis:

  • GetMainCarrier checks whether the global is already mounted with the __ SENTRY __ attribute, and assigns an initial value to the global.

        carrier.__SENTRY__ = carrier.__SENTRY__ || {
          extensions: {},
          hub: undefined};Copy the code
  • Then if there is no control center on the carrier, or if its version is an old one, setHubOnCarrier is called to set the new one

      function setHubOnCarrier(carrier, hub) {
          if(! carrier)return false;
          carrier.__SENTRY__ = carrier.__SENTRY__ || {};
          carrier.__SENTRY__.hub = hub;
          return true;
      }
    Copy the code
  • If the node environment executes getHubFromActiveDomain, this is not the point of the discussion

  • Finally, go back to the hub and get the current control center

      function getHubFromCarrier(carrier) {
          if (carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub)
              return carrier.__SENTRY__.hub;
          carrier.__SENTRY__ = carrier.__SENTRY__ || {};
          carrier.__SENTRY__.hub = new Hub();
          return carrier.__SENTRY__.hub;
      }
    Copy the code

7.3 bindClient

Once you have the Hub client, bind the client of the New BrowserClient instance to the current control center

Look at the source code:

    Hub.prototype.bindClient = function (client) {
        var top = this.getStackTop();
        top.client = client;
        if(client && client.setupIntegrations) { client.setupIntegrations(); }};Copy the code

Analysis:

  • GetStackTop returns the last hub to join the queue

  • Top.client = client binds the new BrowerClient() instance to top

  • SetupIntegrations traverses instal for defaultIntegrations or custom integrations, providing clients with various capabilities.

      function setupIntegrations(options) {
          const integrations = {};
          getIntegrationsToSetup(options).forEach(integration= > {
              integrations[integration.name] = integration;
              setupIntegration(integration);
          });
          return integrations;
      }
    Copy the code

    GetIntegrationsToSetup is just get integrations

Eight, summary

Finally, we summarize the Sentry initialization with a flow chart:

The user passes in the required properties or methods via sentry. init. Sentry processes these properties and methods, and finally registers a client, binds to the control hub, and provides the client with various capabilities through integration.

Ix. Reference materials

  • The Sentry’s official website
  • Learn the overall architecture of Sentry source code and build their own front-end exception monitoring SDK
  • The Sentry warehouse