最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 玩转前端监控,全面解析Sentry源码(三)| 数据上报

    正文概述 掘金(TTtttt)   2021-01-17   636

    一、前言

    最近对前端监控很有兴趣,所以去使用了前端监控里优秀的开源库最近对前端监控很有兴趣,所以去使用了前端监控里优秀的开源库Sentry,并且研究了下Sentry源码,整理出这一系列的文章,希望能帮助大家更好地了解前端监控原理。

    这一系列的文章我将结合官网api与流程图的方式,通俗易懂地讲解整个流程。

    以下是我已完成和计划下一篇文章的主题:

    • 搞懂Sentry初始化
    • Sentry如何处理错误数据
    • Sentry拿到错误数据后,又是如何上报呢(本文)?
    • 计划:Sentry如何集成面包屑Breadcrumbs
    • 计划:Sentry如何处理session
    • ...

    这里给觉得看源码很困难的小伙伴一些建议:

    • 推荐先看一些文章或者官网文档,并且相应的api要熟悉。然后根据主线走,支线可以大概了解是做什么的,别深究。
    • 有时候可能分不清主次,这个时候可以画流程图,这也是我快速了解源码的方法。你画着画着,原本一开始看不懂的源码,可能在后续的某个地方就关联了起来。
    • 最后就是要去多思考,比如:从源码中我学到了哪些优秀的写法,用到了什么设计模式,我如何应用到实际项目中,碰到bug是不是能够通过自己知道的运行流程快速定位到bug大概可能出现的位置等等。

    而关于Sentry源码如何调试大家可以按照如下:

    • git clone git@github.com:getsentry/sentry-javascript.git
    • 进入到 packages/browser 进行npm i 下载依赖
    • 进入到 packages/browser/examples 打开index.html就可以开心调试了(这里建议用Live Server打开)
    • 说明:packages/browser/examples 下的bundle.js就是打包后的源码,他指向了packages/browser/build/bundle.js。这时候你会发现build目录下还有bundle.es6.js,如果你想使用es6的方式去阅读可以将文件替换成bundle.es6.js

    二、导读

    通过我们上一篇文章错误处理后,我们得到了经过Sentry处理好的错误数据,这时候需要调用currentHub.captureEvent进行数据上报。

    我们先来看看主流的数据上报方式有:

    • 采用Ajax通信的方式上报(Sentry采用的方式)

    • img请求上报, url参数带上错误信息

      比如:(new Image()).src = 'https://docs.sentry.io/error?error=error’


    而Sentry上报的方式是采用Ajax通信。我在Sentry会在初始化的7.1 BrowserClient中的有提到

    为了兼容低版本浏览器不支持fetch,所以在初始化的时候就确定ajax通信采用的是fetch还是xhr。

    我们来看看源码怎么写的:

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

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

    分析:

    • getGlobalObject是获取全局对象
    • 代码很简单就看全局上是不是有fetch方法
    • 最终this._transport上就有了对应发送请求的方法

    接下来就是详细的步骤分析:

    三、数据上报

    我们先看captureEvent源码:

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

    分析:

    • 可以看到captureEvent实际上是调用_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);
          }
        }
    

    分析:

    • _invokeClient 是个统一的调度方法,取到scope,执行captureEvent方法。

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

    分析:

    • _process 就是流程控制器,记录当前的步骤

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

    分析:

    • _processEvent是实现的重点
    • 最后会返回event_id 事件id

    3.4 重点:_processEvent

    _processEvent是数据上报的关键,这里主要处理事件(错误或信息),并将其发送给Sentry,同时也可以为事件添加面包屑breadcrumbscontext上下文信息,当然前提是有平台的信息比如用户ip地址等。

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

    因为涉及内容很多,所以我分成几个大块详细讲解分析

    (1) 校验

          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.'));
    

    这一段主要是去判断是否满足上报的条件:

    • 参数中event代表发送给Sentry的事件,hint代表包含有关原始异常的其他信息,scope包含事件元数据的作用域

    • _isEnabled这里主要是为了判断用户传入的参数里是不是设置了enabled为false或者dsn为空的情况,这会导致SDK无法使用,无法发送。所以如果当客户端接受不到信息的时候,不要慌看看自己Sentry.init都传了什么值

        _isEnabled() {
            return this.getOptions().enabled !== false && this._dsn !== undefined;
          }
      
    • SyncPromise其实就是模拟了一个Promise

    • sampled 这一块是与Performance性能挂钩的,它其实就是采样

      我们在Sentry.init的时候其实可以传入tracesSampleRate去控制每个事务都有几个百分比的机会被发送到 Sentry。(例如,如果你将 tracesSampleRate 设置为0.2,大约20% 的事务将被记录并发送。):

      Sentry.init({
        // ...
        
        tracesSampleRate: 0.2,
      });
      

      sampled这里判断是不是number类型是因为,sampled也可以设置为boolean值

      比如在创建事务时知道是否希望将事务发送到 Sentry,于是可以采用Sentry.startTransaction方法直接给事务构造函数。这时候,事务就不会受到 tracesSampleRate 的约束,也不会运行 tracesSampler,发送的事务也不会被覆盖。

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

      更多相关内容可参考官网

    (2) _prepareEvent添加通用的信息

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

    分析:

    • _prepareEvent 主要是为event事件添加通用的信息,包含了从options里获取的发布的版本号release,和环境environment,从作用域scope获取的面包屑breadcrumbs和上下文context等等

    • 在_prepareEvent返回事件前,会有一个self._shouldDropEvent进行判断,如果在Sentry.init中设置了ignoreErrors,denyUrls,allowUrls等数据过滤并且命中的时候,此时会返回null,因此prepared此时也会返回null,会退出事件,不进行事件上报

                if (self._shouldDropEvent(event, options)) {
                  return null;
                }
      
    • _prepareEvent其实涉及了很多细节,这里与本文没有太多关联,如果有兴趣我再专门讲解。

    (3) beforeSend 数据上报前的回调函数

              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;
    

    beforeSend会在事件发送到服务器之前立即调用,而这里的beforeSend其实就是用户传入beforeSend方法

    比如:避免发送邮箱信息

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

    (4) 序列化错误数据

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

    分析:

    • 有session就调用_updateSessionFromEvent 就是从事件event 获取信息,更新session。具体就看之后的session专题

    • _sendEvent就是告诉backend后端去发送事件

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

      分析:

      • 发送的时候会获取到Breadcrumbs面包屑

      • 然后获取后端也就是BrowserBackend去执行sendEvent(说明一下backend也就是BrowserClient传入的BrowserBackend,可以看这初始化那篇文章)

      • _transport 其实就是之前初始化已经辨别是走xhr还是fetch,这里走fetch(也就是导读分析的部分)

      • eventToSentryRequest:

        eventToSentryRequest主要是处理event序列化,生成body,url,type

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

        最后我们看看,经过我们多篇文章探索所得出的最终上报数据的格式如下:

              body: "{
                exception: {
                  values: [
                    {
                      type: 'Error',
                      value: 'externalLibrary method broken: 1610509422407',
                      stacktrace: {
                        frames: [
                          {
                            colno: 1,
                            filename:
                              'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js',
                            function: '?',
                            in_app: true,
                            lineno: 5,
                          },
                          {
                            colno: 9,
                            filename:
                              'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.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,
                environment: 'staging',
                release: '1537345109360',
                breadcrumbs: [
                  {
                    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',
                    'User-Agent':
                      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (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',
        

        分析:

        • body里的exception就是上一篇文章讲过的错误内容
        • sdk就是我们具体使用sdk的版本,还有集成
        • breadcrumbs面包屑内容,之后专题会讲解如何生成
        • request 就是当前发起请求的路径内容
        • url是发向后端的api

    (5)_sendRequest发送请求

        /**
         * @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 ${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);
            }),
          );
        }
    

    分析:

    • _isRateLimited就是防止一瞬间太多相同的错误发生

    • 然后就是对options的一些处理,该合并该赋值赋值

    • this._buffer.add 就是把promise加入buffer队列中

    • 之后等待向服务器发起请求,下面是请求的截图

    玩转前端监控,全面解析Sentry源码(三)| 数据上报

    (6)_handleResponse处理返回的请求

    我们先看看返回的数据

    玩转前端监控,全面解析Sentry源码(三)| 数据上报

    接着看看_handleResponse对数据都做了哪些处理

        /**
         * 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: ${this._disabledUntil(requestType)}`);
          if (status === exports.Status.Success) {
            resolve({ status });
            return;
          }
          reject(response);
        }
    

    分析:

    • fromHttpCode就是返回状态200到300是Success,429就是被限制RateLimit,400到500为Invalid,500以上就是Failed,其他Unknown,所以这里的status是Success
    • 然后成功就是resolve

    (6) 请求失败时

    我们回过头看_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}`,
              );
            });
    

    分析:

    • 第一个参数传入null,如果成功发送其实是不会执行到这里会直接退出
    • 而出错就会调用captureException去上报错误

    到此,整个自动上报的过程就完成了,接下来我们看看主动上报

    四、captureException和captureMessage主动上报数据

    captureException是上传一个错误对象

    captureMessage则上传递一个消息,这个消息即可以包含错误信息,也可以是普通消息

    然后我们看看它们的源码

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

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

    这里的关于如何处理消息已经在上一篇讲解过了,所以这里重点是讲解_captureEvent方法

    分析:

    • 对于错误消息captureMessage和captureException都会调用eventFromException去处理消息

    • 而captureMessage需要判断信息message是不是基本类型,基本类型走eventFromMessage,引用类型走eventFromException 去处理message。

    • 这里的关于如何处理消息已经在上一篇讲解过了,所以这里重点是讲解_captureEvent方法

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

      其实就是调用_processEvent方法

    到此,整个数据上报的内容就完成了

    五、总结

    最后我们通过一张流程图来看看整个数据上报的过程:

    玩转前端监控,全面解析Sentry源码(三)| 数据上报

    六、参考资料

    • Sentry官网
    • Sentry仓库

    起源地下载网 » 玩转前端监控,全面解析Sentry源码(三)| 数据上报

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元