Sentry的异样数据上报机制

之前咱们聊过了Sentry的异样监控计划中具体有那几种异样,以及大略的解决形式。这次咱们来理解一下,这些异样数据的上报机制是怎么样的。

上报形式

就目前理解到的,支流的数据上报形式 而言,Sentry还是采纳的ajax上报的形式。为了有更好的兼容性,在初始化的时候会去判断浏览器是否反对fetch,反对就应用fetch否则是xhr。同时也反对自定义的上报形式,且优先级会高于fetch和xhr

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

上报流程

以unhandledrejection为例,首先是 全局监听 触发对应的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;  };}

对应的handler触发instrument.ts中的 captureEvent

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

触发baseclient.ts 中的_captureEvent

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

最初走到外围主流程的函数办法上_processEvent

外围办法_processEvent

baseclient.ts _processEvent 参数event代表sentry要发送的事件自身的信息(event_id,timestamp,release
等等),hint代表其余的一些和原始异样相干的信息(captureContext,data,originalException等等),scope代表元数据的作用域

    // 代码有局部删减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}`,        );      });  }

这一块的流程比拟多,尽管已做删减,还是须要分成几个模块来解说剖析

前置条件

    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%的数据会被发送,实用于日活十分大的情景。

增加通用配置信息

this._prepareEvent(event, scope, hint)
次要是增加每个事件都须要的通用信息 如environment,message,dist,release, breadcrumbs等等

数据上报前的处理函数

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源码(三)| 数据上报