One, foreword

Recently, I am very interested in front-end monitoring, so I use the excellent open source library in front-end monitoring, so I use the excellent open source library Sentry in front-end monitoring, and study the Sentry source code, and 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
  • How does Sentry handle error data
  • How does Sentry report error data when it 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

CurrentHub. CaptureEvent Is called to report the error data processed by Sentry.

Let’s take a look at the mainstream data reporting methods:

  • Reporting using Ajax communication (Sentry style)

  • The IMG request is reported, and the URL parameter contains error information

    SRC = ‘docs. Sentry. IO /error? The error…


Sentry reports using Ajax communication. I mentioned this in the 7.1 BrowserClient initialization in Sentry

BrowserBackend has the _setupTransport method to determine whether ajax uploads go fetch or XHR

In order to be compatible with older browsers, fetch is not supported, so it is determined at initialization whether FETCH or XHR is used for Ajax communication.

Let’s see how the source code is written:

class BaseBackend {
    constructor(options) {
    	// ...
      this._transport = this._setupTransport();
    }    
	_setupTransport() {
   	// ...
      if (supportsFetch()) {
        return new FetchTransport(transportOptions);
      }
      return newXHRTransport(transportOptions); }}Copy the code

supportsFetch:

  /**
   * Tells whether current environment supports Fetch API
   * {@link supportsFetch}.
   *
   * @returns Answer to the given question.
   */
  function supportsFetch() {
    if(! ('fetch' in getGlobalObject())) {
      return false;
    }
    try {
      new Headers();
      new Request(' ');
      new Response();
      return true;
    } catch (e) {
      return false; }}Copy the code

Analysis:

  • GetGlobalObject is getGlobalObject
  • The code is very simple to see if there is a fetch method globally
  • In the endthis._transportThere is a method to send the request

The next step is detailed analysis:

Third, data reporting

Let’s look at captureEvent source:

    captureEvent(event, hint) {
      const eventId = (this._lastEventId = uuid4());
      this._invokeClient('captureEvent', event, Object.assign(Object.assign({}, hint), { event_id: eventId }));
      return eventId;
    }
Copy the code

Analysis:

  • You can see that captureEvent actually calls _invokeClient

3.1 _invokeClient

    /**
     * Internal helper function to call a method on the top client if it exists.
     *
     * @param method The method to call on the client.
     * @param args Arguments to pass to the client function.
     */
    _invokeClient(method, ... args) {
      const { scope, client } = this.getStackTop();
      if (client && client[method]) {
        client[method](...args, scope);
      }
    }
Copy the code

Analysis:

  • _invokeClient is aUnified scheduling method, and execute the captureEvent method.

3.2 captureEvent

   captureEvent(event, hint, scope) {
      let eventId = hint && hint.event_id;
      this._process(
        this._captureEvent(event, hint, scope).then(result= >{ eventId = result; }));return eventId;
    }
Copy the code

Analysis:

  • _process is the process controller that records the current step

3.3 _captureEvent

    _captureEvent(event, hint, scope) {
      return this._processEvent(event, hint, scope).then(
        finalEvent= > {
          return finalEvent.event_id;
        },
        reason= > {
          logger.error(reason);
          return undefined; }); }Copy the code

Analysis:

  • _processEvent isFocus of implementation
  • Event_id Event ID is returned

Key points: 3.4 _processEvent

_processEvent is the key to data reporting. It processes the event (error or message) and sends it to the Sentry. It can also add crumbs and context information to the event, provided the platform information such as the user’S IP address is available.

    _processEvent(event, hint, scope) {
      const { beforeSend, sampleRate } = this.getOptions();
      if (!this._isEnabled()) {
        return SyncPromise.reject(new SentryError('SDK not enabled, will not send event.'));
      }
      const isTransaction = event.type === 'transaction';
      // 1.0 === 100% events are sent
      // 0.0 === 0% events are sent
      // Sampling for transaction happens somewhere else
      if(! isTransaction &&typeof sampleRate === 'number' && Math.random() > sampleRate) {
        return SyncPromise.reject(new SentryError('This event has been sampled, will not send event.'));
      }
      return this._prepareEvent(event, scope, hint)
        .then(prepared= > {
          if (prepared === null) {
            throw new SentryError('An event processor returned null, will not send event.');
          }
          const isInternalException = hint && hint.data && hint.data.__sentry__ === true;
          if(isInternalException || isTransaction || ! beforeSend) {return prepared;
          }
          const beforeSendResult = beforeSend(prepared, hint);
          if (typeof beforeSendResult === 'undefined') {
            throw new SentryError('`beforeSend` method has to return `null` or a valid event.');
          } else if (isThenable(beforeSendResult)) {
            return beforeSendResult.then(
              event= > event,
              e= > {
                throw new SentryError(`beforeSend rejected with ${e}`); }); }return beforeSendResult;
        })
        .then(processedEvent= > {
          if (processedEvent === null) {
            throw new SentryError('`beforeSend` returned `null`, will not send event.');
          }
          const session = scope && scope.getSession && scope.getSession();
          if(! isTransaction && session) {this._updateSessionFromEvent(session, processedEvent);
          }
          this._sendEvent(processedEvent);
          return processedEvent;
        })
        .then(null.reason= > {
          if (reason instanceof SentryError) {
            throw reason;
          }
          this.captureException(reason, {
            data: {
              __sentry__: true,},originalException: reason,
          });
          throw new SentryError(
            `Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: ${reason}`,); }); }Copy the code

Because it involves a lot of content, SO I break it down into several large pieces of detailed analysis

(1) check

      const { beforeSend, sampleRate } = this.getOptions();
      if (!this._isEnabled()) {
        return SyncPromise.reject(new SentryError('SDK not enabled, will not send event.'));
      }
      const isTransaction = event.type === 'transaction';
      // 1.0 === 100% events are sent
      // 0.0 === 0% events are sent
      // Sampling for transaction happens somewhere else
      if(! isTransaction &&typeof sampleRate === 'number' && Math.random() > sampleRate) {
        return SyncPromise.reject(new SentryError('This event has been sampled, will not send event.'));
Copy the code

This paragraph is mainly to determine whether the conditions for reporting are met:

  • The event represents the event sent to the Sentry, the hint represents additional information about the original exception, and the scope contains the scope of the event metadata

  • _isEnabled is mainly used to determine whether enabled is set to false or DSN is empty in the parameter passed by the user, which will cause SDK to be unavailable and cannot be sent. So if the client doesn’t receive the message, don’t panic to see what value sentry. init is passing

      _isEnabled() {
          return this.getOptions().enabled ! = =false && this._dsn ! = =undefined;
        }
    Copy the code
  • SyncPromise basically simulates a Promise

  • The sampled component is tied to Performance and is essentially sampling.

    We can actually pass in tracesSampleRate with sentry.init to control the percentage chance that each transaction will be sent to Sentry. (For example, if you set tracesSampleRate to 0.2, about 20 percent of transactions will be logged and sent.) :

    Sentry.init({
      // ...
    
      tracesSampleRate: 0.2});Copy the code

    It is of type number because tos can also be set to Boolean

    For example, when you create a transaction and know whether you want to send the transaction to Sentry, you can use the sentry. startTransaction method directly to the transaction constructor. In this case, transactions are not subject to tracesSampleRate, tracesSampler is not run, and sent transactions are not overwritten.

    Sentry.startTransaction({
      name: "Search from navbar".sampled: true});Copy the code

    For more information, please refer to the official website

(2) _prepareEvent adds general information

      return this._prepareEvent(event, scope, hint)
        .then(prepared= > {
          if (prepared === null) {
            throw new SentryError('An event processor returned null, will not send event.');
          }
Copy the code

Analysis:

  • Prepareevent adds general information for the event, including the release number obtained from options, the environment, the breadcrumbs obtained from scope, and the context

  • Before _prepareEvent returns the event, there is a self._shouldDropEvent to check if the sentry. init filter is set to ignoreErrors, denyUrls, allowUrls, etc. Prepared will also return NULL and exit the event without reporting the event

              if (self._shouldDropEvent(event, options)) {
                return null;
              }
    Copy the code
  • There’s a lot of detail involved in _prepareEvent that’s not really relevant to this article, but I’ll cover it if you’re interested.

(3) Callback function before beforeSend data is reported

          const beforeSendResult = beforeSend(prepared, hint);
          if (typeof beforeSendResult === 'undefined') {
            throw new SentryError('`beforeSend` method has to return `null` or a valid event.');
          } else if (isThenable(beforeSendResult)) {
            return beforeSendResult.then(
              event= > event,
              e= > {
                throw new SentryError(`beforeSend rejected with ${e}`); }); }return beforeSendResult;
Copy the code

BeforeSend is called immediately before the event is sent to the server, where the user passes in the beforeSend method.

For example, avoid sending email messages

Sentry.init({
  beforeSend(event) {
    // Modify the event here
    if (event.user) {
      // Don't send user's email address
      delete event.user.email;
    }
    returnevent; }});Copy the code

(4) Serialize the wrong data

          const session = scope && scope.getSession && scope.getSession();
          if(! isTransaction && session) {this._updateSessionFromEvent(session, processedEvent);
          }
          this._sendEvent(processedEvent);
          return processedEvent;
Copy the code

Analysis:

  • Call _updateSessionFromEvent to update the session. See the session topic later

  • _sendEvent tells the backend to send events.

        _sendEvent(event) {
          const integration = this.getIntegration(Breadcrumbs);
          if (integration) {
            integration.addSentryBreadcrumb(event);
          }
          super._sendEvent(event); } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -_sendEvent(event) {
          this._getBackend().sendEvent(event); } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --sendEvent(event) {
          this._transport.sendEvent(event).then(null.reason= > {
            logger.error(`Error while sending event: ${reason}`); }); } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --sendEvent(event) {
          return this._sendRequest(eventToSentryRequest(event, this._api), event);
        }
    Copy the code

    Analysis:

    • You’ll get the crumbs when you send them

    • And then get the backend, BrowserBackend, to execute sendEvent(Backend is also BrowserBackend passed in by BrowserClient, see this initialization article).

    • _transport is actually the initialization that determines whether to go XHR or fetch, so fetch is used here.

    • eventToSentryRequest:

      EventToSentryRequest handles event serialization, generating body, URL, and Type

        function eventToSentryRequest(event, api) {
        // ...
          const req = {
            body: JSON.stringify(event),
            type: event.type || 'event'.url: useEnvelope ? api.getEnvelopeEndpointWithUrlEncodedAuth() : api.getStoreEndpointWithUrlEncodedAuth(),
          };
      // ...
          return req;
        }
      Copy the code

      Finally, let’s see that the format of the final reported data obtained through our exploration of several articles is as follows:

            body: "{ exception: { values: [ { type: 'Error', value: 'externalLibrary method broken: 1610509422407', stacktrace: { frames: [ { colno: 1, filename: 'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-l ib.js', function: '?', in_app: true, lineno: 5, }, { colno: 9, filename: 'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-l ib.js', function: 'externalLibrary', in_app: true, lineno: 2, }, ], }, mechanism: { handled: false, type: 'onerror' }, }, ], }, platform: 'javascript', sdk: { name: 'sentry.javascript.browser', packages: [{ name: 'NPM :@sentry/browser', version: '5.29.2'}], version: '5.29.2', Integrations: [ 'InboundFilters', 'FunctionToString', 'TryCatch', 'Breadcrumbs', 'GlobalHandlers', 'LinkedErrors', 'UserAgent', ], }, Event_id: 'aec2b5cdf4b34efa92c4766ea76a2f4b, timestamp: 1610509422.9, and the environment:' staging 'release: '1537345109360', crumbs: [{timestamp: 1610509411.46, category: 'console', data: {arguments: [ 'currentHub', { _version: 3, _stack: '[Array]', _lastEventId: 'aec2b5cdf4b34efa92c4766ea76a2f4b' }, ], logger: 'console',}, level: 'log', message: 'currentHub [object object]',}, {timestamp: 1610509411.462, category: 'console', data: { arguments: ['Time Hooker Works!'], logger: 'console' }, level: 'log', message: 'Time Hooker Works!',}, {timestamp: 1610509411.52, category: 'uI.click ', message: 'body > button#plainObject'}, {timestamp: 1610509415.083, category: 'uI.click ', message: 'body > button#deny-url'}, {timestamp: 1610509416.768, category: 'ui.click', message: 'body > button#deny-url'}, {timestamp: 1610509422.405, category: 'sentry.event', event_id: 'b91c3bbff53047b7b6b40cd87a82c88e', message: 'Error: externalLibrary method broken: 1610509417092', }, ], request: {url: 'http://127.0.0.1:5500/packages/browser/examples/index.html', headers: {Referer: 'http://127.0.0.1:5500/packages/browser/examples/index.html', 'the user-agent' : 'Mozilla / 5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',},}, tags: {},}".type: 'event'.url: 'https://sentry.io/api/297378/store/? sentry_key=363a337c11a64611be4845ad6e24f3ac&sentry_version=7'.Copy the code

      Analysis:

      • The exception in the body is the error mentioned in the previous article
      • The SDK is the version that we use specifically, and the integration
      • Breadcrumbs. How do you make them
      • Request is the content of the current path that initiates the request
      • Urls are apis sent to the back end

(5)_sendRequest sends the request

    / * * *@param sentryRequest Prepared SentryRequest to be delivered
     * @param originalPayload Original payload used to create SentryRequest
     */
    _sendRequest(sentryRequest, originalPayload) {
      if (this._isRateLimited(sentryRequest.type)) {
        return Promise.reject({
          event: originalPayload,
          type: sentryRequest.type,
          reason: `Transport locked till The ${this._disabledUntil(sentryRequest.type)} due to too many requests.`.status: 429}); }const options = {
        body: sentryRequest.body,
        method: 'POST'.// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
        // https://caniuse.com/#feat=referrer-policy
        // It doesn't. And it throw exception instead of ignoring this parameter...
        // REF: https://github.com/getsentry/raven-js/issues/1233
        referrerPolicy: supportsReferrerPolicy() ? 'origin' : ' '};if (this.options.fetchParameters ! = =undefined) {
        Object.assign(options, this.options.fetchParameters);
      }
      if (this.options.headers ! = =undefined) {
        options.headers = this.options.headers;
      }
      return this._buffer.add(
        new SyncPromise((resolve, reject) = > {
          global$3
            .fetch(sentryRequest.url, options)
            .then(response= > {
              const headers = {
                'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
                'retry-after': response.headers.get('Retry-After'),};this._handleResponse({
                requestType: sentryRequest.type, response, headers, resolve, reject, }); }) .catch(reject); })); }Copy the code

Analysis:

  • _isRateLimited prevents too many of the same errors from happening at once

  • Then there is some processing of options, merging and assigning

  • This._buffer. add adds the promise to the buffer queue

  • Then wait for a request to be made to the server. Below is a screenshot of the request

(6)_handleResponse handles the returned request

Let’s look at the returned data first

Now look at what _handleResponse does to the data

    /** * Handle Sentry repsonse for promise-based transports. */
    _handleResponse({ requestType, response, headers, resolve, reject }) {
      const status = exports.Status.fromHttpCode(response.status);
      /** * "The name is case-insensitive." * https://developer.mozilla.org/en-US/docs/Web/API/Headers/get */
      const limited = this._handleRateLimit(headers);
      if (limited) logger.warn(`Too many requests, backing off until: The ${this._disabledUntil(requestType)}`);
      if (status === exports.Status.Success) {
        resolve({ status });
        return;
      }
      reject(response);
    }
Copy the code

Analysis:

  • From 200 to 300 is a Success, from 429 is a restricted elimit, from 400 to 500 is Invalid, from 500 to 500 is Failed, other Unknown, so the status here is Success
  • Then success is Resolve

(6) When the request fails

Let’s go back to _processEvent

    .then(null.reason= > {
          if (reason instanceof SentryError) {
            throw reason;
          }
          this.captureException(reason, {
            data: {
              __sentry__: true,},originalException: reason,
          });
          throw new SentryError(
            `Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: ${reason}`,); });Copy the code

Analysis:

  • The first argument is passed null, and if it’s successfully sent it doesn’t actually get executed here and it just exits
  • If something goes wrong, it callsCaptureException to report an error

At this point, the whole automatic reporting process is complete, so let’s look at unsolicited reporting

4. CaptureException and captureMessage actively report data

CaptureException is uploading an error object

CaptureMessage passes a message, which can contain either an error message or a normal message

Then we look at their source code

CaptureException:

      BaseClient.prototype.captureException(exception, hint, scope) {
          let eventId = hint && hint.event_id;
          this._process(this._getBackend()
              .eventFromException(exception, hint)
              .then(event= > this._captureEvent(event, hint, scope))
              .then(result= > {
              eventId = result;
          }));
          return eventId;
      }
     
Copy the code

CaptureMessage:

      BaseClient.prototype.captureMessage = function (message, level, hint, scope) {
        var _this = this;
        var eventId = hint && hint.event_id;
        var promisedEvent = utils_1.isPrimitive(message)
            ? this._getBackend().eventFromMessage(String(message), level, hint)
            : this._getBackend().eventFromException(message, hint);
        this._process(promisedEvent
            .then(function (event) { return _this._captureEvent(event, hint, scope); })
            .then(function (result) {
            eventId = result;
        }));
        return eventId;
    };
Copy the code

How to handle messages was covered in the previous article, so the emphasis here is on the _captureEvent method. Okay

Analysis:

  • Both captureMessage and captureException call eventFromException to process the message

  • CaptureMessage needs to determine whether the message message is a primitive type. The primitive type goes eventFromMessage and the reference type goes eventFromException to process the message.

  • How to handle messages was covered in the previous article, so the emphasis here is on the _captureEvent method. Okay

       _captureEvent(event, hint, scope) {
          return this._processEvent(event, hint, scope).then(
            finalEvent= > {
              return finalEvent.event_id;
            },
            reason= > {
              logger.error(reason);
              return undefined; }); }Copy the code

    It’s just calling the _processEvent method

At this point, the whole data report content is completed

Five, the summary

Finally, let’s look at the whole data reporting process through a flow chart:

Vi. Reference materials

  • The Sentry’s official website
  • The Sentry warehouse