Sentry abnormal data reporting mechanism

Earlier we talked about what kind of exceptions are in Sentry’s exception monitoring scheme and how they should be handled. Let’s take a look at the reporting mechanism of these abnormal data.

Report the way

As far as the mainstream data reporting method is concerned, Sentry still adopts ajax reporting method. For better compatibility, the browser is initialized to determine if fetch is supported by the browser. If so, fetch is used, otherwise XHR is used. In addition, custom reporting modes are supported and have higher priorities than FETCH and XHR

class BaseBackend {
  if (this._options.transport) {
      return new this._options.transport(transportOptions);
  }
	if (supportsFetch()) { return new FetchTransport(transportOptions); };
 	return new XHRTransport(transportOptions);
}
Copy the code

Reporting process

In the case of unhandledrejection, first the global listener triggers the corresponding triggerHandlers

function instrumentUnhandledRejection() :void {
  _oldOnUnhandledRejectionHandler = global.onunhandledrejection;

  global.onunhandledrejection = function(e: any) :boolean {
    triggerHandlers('unhandledrejection', e);

    if (_oldOnUnhandledRejectionHandler) {
      // eslint-disable-next-line prefer-rest-params
      return _oldOnUnhandledRejectionHandler.apply(this.arguments);
    }
    return true;
  };
}
Copy the code

The corresponding handler triggers the captureEvent in Instrument. ts

 addInstrumentationHandler({
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      callback: (e: any) = > {
        currentHub.captureEvent(event, {
          originalException: error,
        });
        return;
      },
      type: 'unhandledrejection'});Copy the code

Trigger _captureEvent in Baseclient.ts

  protected_captureEvent(event: Event, hint? : EventHint, scope? : Scope): PromiseLike<string | undefined> {
    return this._processEvent(event, hint, scope).then(
      finalEvent= > {
        return finalEvent.event_id;
      },
      reason= > {
        logger.error(reason);
        return undefined; }); }Copy the code

Finally, go to the function method of the core main flow _processEvent

Core method _processEvent

The baseclient.ts _processEvent parameter event represents information about the event itself (event_id,timestamp,release) to be sent by sentry And so on), hint on behalf of the other related to the original exception information (originalException captureContext, data, etc.), the scope on behalf of metadata scope

	// The code is partially truncated
protected_processEvent(event: Event, hint? : EventHint, scope? : Scope): PromiseLike<Event> {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';
    if(! isTransaction &&typeof sampleRate === 'number' && Math.random() > sampleRate) {
      return SyncPromise.reject(
        new SentryError(
          `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate}) `,),); }return this._prepareEvent(event, scope, hint)
      .then(prepared= > {
        const beforeSendResult = beforeSend(prepared, hint);
		  if (isThenable(beforeSendResult)) {
          return (beforeSendResult as PromiseLike<Event | null>).then(
            event= > event,
            e= > {
              throw new SentryError(`beforeSend rejected with ${e}`); }); }return beforeSendResult;
      })
      .then(processedEvent= > {
        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 as Error});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

There are many processes in this section. Although it has been deleted, it still needs to be divided into several modules to explain and analyze

precondition

    if (!this._isEnabled()) {
      return SyncPromise.reject(new SentryError('SDK not enabled, will not send event.'));
    }
    const isTransaction = event.type === 'transaction';
    if(! isTransaction &&typeof sampleRate === 'number' && Math.random() > sampleRate) {
      return SyncPromise.reject(
        new SentryError(
          `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate}) `,),); }Copy the code

During initialization, whether enabled = false(the default value is true) is set. False indicates that Sentry cannot be used and data will not be reported. Preset sampleRate sampleRate. For example, sampleRate = 0.1 means 10% of data will be sent, suitable for very high daily activity.

Add general configuration information

Enclosing _prepareEvent (event, the scope, hint) is mainly to add each event needs to general information Such as the environment, the message, dist, release, breadcrumbs, and so on

Data processing function before reporting

BeforeSend is the function passed in by sentry. init, with an event,hint, and event. It is convenient for users to process and filter event data, etc

The data reported

        const session = scope && scope.getSession && scope.getSession();
        if(! isTransaction && session) {this._updateSessionFromEvent(session, processedEvent);
        }

        this._sendEvent(processedEvent);
        return processedEvent;


Copy the code

Check whether there is a session, if there is, update _sendEvent will point to the corresponding transport(since the browser is compatible with FETCH, the actual reporting method is fetch).

  public sendEvent(event: Event): PromiseLike<Response> {
    return this._sendRequest(eventToSentryRequest(event, this._api), event);
  }
Copy the code

Here we see that eventToSentryRequest is also executed before reporting, which is primarily serializing parameters

export function eventToSentryRequest(event: Event, api: API) :SentryRequest {

  const req: SentryRequest = {
    body: JSON.stringify(sdkInfo ? enhanceEventWithSdkInfo(event, api.metadata.sdk) : event),
    type: eventType,
    url: useEnvelope ? api.getEnvelopeEndpointWithUrlEncodedAuth() : api.getStoreEndpointWithUrlEncodedAuth(),
  };
 
  return req;
}

Copy the code

The last place in Fetch to implement reporting is Fetch. Ts _sendRequest

  private _sendRequest(sentryRequest: SentryRequest, originalPayload: Event | Session): PromiseLike<Response> {
    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: RequestInit = {
      body: sentryRequest.body,
      method: 'POST'.referrerPolicy: (supportsReferrerPolicy() ? 'origin' : ' ') as ReferrerPolicy,
    };

    return this._buffer.add(
      new SyncPromise<Response>((resolve, reject) = > {
        this._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

We can see that the _isRateLimited method in Sentry prevents too many of the same errors from happening at once. The final data format is

{
    "exception": {"values":[
            {
                "type":"UnhandledRejection"."value":"Non-Error promise rejection captured with value: 321"."mechanism": {"handled":false."type":"onunhandledrejection"}}},"level":"error"."platform":"javascript"."event_id":"a94cd62ee6064321a340ce396da78de0"."timestamp":1617443534.168."environment":"staging"."release":"1537345109360"."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"."User-Agent":"Mozilla / 5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36"}},"sdk": {"name":"sentry.javascript.browser"."version":"6.2.5"."integrations": []."packages":[
            {
                "name":"npm:@sentry/browser"."version":"6.2.5"}}}]Copy the code

conclusion

In fact, in the middle of writing this document, I suddenly realized a slightly embarrassing problem. I seem to have directly written down the reporting process without specifically writing down how to deal with the wrong data. But after all, the writing is written, early or spent more energy, restart is a bit of a waste of time. I decided to fill in a later article on Sentry’s handling of exception data. Ps: Because I have done monitoring SDK once before, the more I know about Sentry, THE more I feel my previous shortcomings, and also confirm some of my previous ideas, this series should continue to be no accident.

The resources

GitHub – getsentry/sentry-javascript: Official Sentry SDKs for Javascript

Parsing Sentry source code (three) | data report