Sentry的異常數據上報機制

Sentry的異常數據上報機制

以前咱們聊過了Sentry的異常監控方案中具體有那幾種異常,以及大概的處理方式。此次咱們來了解一下,這些異常數據的上報機制是怎麼樣的。javascript

上報方式

就目前瞭解到的,主流的數據上報方式 而言,Sentry仍是採用的ajax上報的方式。爲了有更好的兼容性,在初始化的時候會去判斷瀏覽器是否支持fetch,支持就使用fetch不然是xhr。同時也支持自定義的上報方式,且優先級會高於fetch和xhrhtml

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

上報流程

以unhandledrejection爲例,首先是 全局監聽 觸發對應的triggerHandlersjava

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;
  };
}

對應的handler觸發instrument.ts中的 captureEventgit

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

觸發baseclient.ts 中的_captureEventgithub

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;
      },
    );
  }

最後走到核心主流程的函數方法上_processEventajax

核心方法_processEvent

baseclient.ts _processEvent 參數event表明sentry要發送的事件自己的信息(event_id,timestamp,release
等等),hint表明其餘的一些和原始異常相關的信息(captureContext,data,originalException等等),scope表明元數據的做用域typescript

// 代碼有部分刪減
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}`,
        );
      });
  }

這一塊的流程比較多,雖然已作刪減,仍是須要分紅幾個模塊來說解分析npm

前置條件

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})`,
        ),
      );
    }

前面基本是對是否知足上報的條件進行校驗,初始化的時候是否設置了enabled = false(默認爲true),爲false即Sentry不可以使用,不會上報數據。設置的sampleRate採樣率。好比設置了sampleRate = 0.1即會有10%的數據會被髮送,適用於日活很是大的情形。json

添加通用配置信息

this._prepareEvent(event, scope, hint)
主要是添加每一個事件都須要的通用信息 如environment,message,dist,release, breadcrumbs等等api

數據上報前的處理函數

beforeSend其實就是Sentry.init傳入的函數,入參即爲event,hint,最後返回event。便於使用方對event數據作處理過濾,等等

數據上報

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

        this._sendEvent(processedEvent);
        return processedEvent;

判斷是否有session,有則更新
_sendEvent則指向對應的transport(由於瀏覽器兼容fetch,則本次實際上報方式是使用fetch)

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

這裏咱們看到,在上報前還會執行eventToSentryRequest,這個方法主要是在序列化參數

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;
}

Fetch中最後實現上報的地方爲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 ${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);
      }),
    );
  }

咱們能夠看到sentry中經過_isRateLimited方法來防止一瞬間太多相同的錯誤發生。
最終上報的數據格式爲

{
    "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"
            }
        ]
    }
}

總結

其實這篇文檔在寫到一半的時候,我忽然意識到一個略顯尷尬的問題,我好像沒有具體寫錯誤數據是如何處理的,就直接寫了上報的流程。可是畢竟寫都寫了,前期仍是花了比較多的精力,從新開始就有點浪費時間了。因而我決定在後面的一篇中補充上,Sentry對於異常數據的處理。ps: 由於本身以前作過一次監控SDK,在對Sentry瞭解的越多後,感受到了本身以前的不少不足,同時也印證了本身以前的一些想法,這個系列不出意外應該還會持續下去。

參考資料

GitHub - getsentry/sentry-javascript: Official Sentry SDKs for Javascript
解析Sentry源碼(三)| 數據上報

相關文章
相關標籤/搜索