1

最近在做前端的监控时,研究了一下sentry的异常监控方案,特此记录一下。
主要介绍 window.onerror、window.onunhandledrejection 以及请求上报 三个方面来了解sentry源码
文中用到的sentry代码版本为5.16.0-beta.5,地址为 sentry-javascript/packages at master · getsentry/sentry-javascript · GitHub
关于前端监控的一些知识可以见 前端监控的思考

Sentry前端异常监控基本原理

异常捕获原理之重写window.onerror方法

let _oldOnErrorHandler: OnErrorEventHandler = null;
/** JSDoc */
function instrumentError(): void {
  _oldOnErrorHandler = global.onerror;
  global.onerror = function (msg: any, url: any, line: any, column: any, error: any): boolean {
    triggerHandlers('error', {
      column,
      error,
      line,
      msg,
      url,
    });

    if (_oldOnErrorHandler) {
      return _oldOnErrorHandler.apply(this, arguments);
    }
    return false;
  };
}

具体的处理流程为

class GlobalHandlers {
      // 收集全局的 window.onerror错误
  /** JSDoc */
  private _installGlobalOnErrorHandler(): void {
    if (this._onErrorHandlerInstalled) {
      return;
    }
    // 通过事件订阅的方式,指定当 发生特定类型的错误(参数中的type字段)时,触发callBack事件
    addInstrumentationHandler({
        /*
            msg:错误信息(字符串)。可用于HTML onerror=""处理程序中的event。
            url:发生错误的脚本URL(字符串)
            line:发生错误的行号(数字)
            column:发生错误的列号(数字)
            error:Error对象(对象)或 其他类型
        */
      callback: (data: { msg: any; url: any; line: any; column: any; error: any }) => {
        const error = data.error;
        const currentHub = getCurrentHub();
        const hasIntegration = currentHub.getIntegration(GlobalHandlers);
        const isFailedOwnDelivery = error && error.__sentry_own_request__ === true;

        if (!hasIntegration || shouldIgnoreOnError() || isFailedOwnDelivery) {
          return;
        }

        const client = currentHub.getClient();

        const event = isPrimitive(error)  // 判断是否为基本类型
          ? this._eventFromIncompleteOnError(data.msg, data.url, data.line, data.column)// 仅上报错误附加信息,没有具体错误内容
          : this._enhanceEventWithInitialFrame(
            // 返回Event 对象,包括错误的堆栈位置的错误信息
            /** eg 堆栈位置的错误信息
             * "Error: test error
                at Module../src/main.js (http://dev.xiguacity.club:3001/static/js/main.bundle.js:96299:68)
                at __webpack_require__ (http://dev.xiguacity.club:3001/static/js/main.bundle.js:790:30)
                at fn (http://dev.xiguacity.club:3001/static/js/main.bundle.js:101:20)
                at Object.1 (http://dev.xiguacity.club:3001/static/js/main.bundle.js:101544:18)
                at __webpack_require__ (http://dev.xiguacity.club:3001/static/js/main.bundle.js:790:30)
                at http://dev.xiguacity.club:3001/static/js/main.bundle.js:857:37
                at http://dev.xiguacity.club:3001/static/js/main.bundle.js:860:10"
             */
            eventFromUnknownInput(error, undefined, {
              attachStacktrace: client && client.getOptions().attachStacktrace,// 是否收集错误的堆栈信息
              rejection: false,
            }),
            data.url, 
            data.line, 
            data.column, 
          );
        // 相当于 Object.assign(event.exception!.values![0], {  handled: false, type: 'onerror' })
        addExceptionMechanism(event, {
          handled: false,
          type: 'onerror',
        });
        // 添加eventId 序列化数据 之后发送数据 ajax fetch or beacon
        currentHub.captureEvent(event, {
          originalException: error,
        });
      },
      type: 'error',
    });

    this._onErrorHandlerInstalled = true;
  }
     
}

异常捕获原理之重写window.unhandleRejection方法

let _oldOnUnhandledRejectionHandler: ((e: any) => void) | null = null;
/** JSDoc */
function instrumentUnhandledRejection(): void {
  _oldOnUnhandledRejectionHandler = global.onunhandledrejection;

  global.onunhandledrejection = function (e: any): boolean {
    triggerHandlers('unhandledrejection', e);
    if (_oldOnUnhandledRejectionHandler) {
      return _oldOnUnhandledRejectionHandler.apply(this, arguments);
    }
    return true;
  };
}

具体异常处理的流程为

  /** JSDoc */
  private _installGlobalOnUnhandledRejectionHandler(): void {
    if (this._onUnhandledRejectionHandlerInstalled) {
      return;
    }

    addInstrumentationHandler({
      callback: (e: any) => {
        let error = e;

        // dig the object of the rejection out of known event types
        try {
          // PromiseRejectionEvents store the object of the rejection under 'reason'
          // see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
          if ('reason' in e) {
            error = e.reason;
          }
          // something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents
          // to CustomEvents, moving the `promise` and `reason` attributes of the PRE into
          // the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec
          // see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and
          // https://github.com/getsentry/sentry-javascript/issues/2380
          else if ('detail' in e && 'reason' in e.detail) {
            error = e.detail.reason;
          }
        } catch (_oO) {
          // no-empty
        }
        // 后续与 _installGlobalOnErrorHandler流程一致 省略不写
      
    this._onUnhandledRejectionHandlerInstalled = true;
  }

异常上报机制

protected _setupTransport(): Transport {
    if (!this._options.dsn) {
      // We return the noop transport here in case there is no Dsn.
      return super._setupTransport();
    }

    const transportOptions = {
      ...this._options.transportOptions,
      dsn: this._options.dsn,
    };
        // 若有自定义的上报方式,则使用自定义的
    if (this._options.transport) {
      return new this._options.transport(transportOptions);
    }
        // 默认先使用fetch上报,否则使用xhr
    if (supportsFetch()) {
      return new FetchTransport(transportOptions);
    }
    return new XHRTransport(transportOptions);
  }

未完待续中…

参考文章

学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK


notself
134 声望13 粉丝

引用和评论

0 条评论