rwson

rwson

一个前端开发

Sentry源码阅读

在平时做前端开发时,特别是互联网业务,我们需要及时知道发布后的项目,在运行时有哪些问题,在什么平台或者什么条件下会抛出异常,从而更精确的定位问题,及时修复线上bug

Sentry 做为一款开源的监控平台,对各大框架或者语言都有支持,今天我们一起来讨论下它的源码结构。本文分析的是打包完没有压缩的版本,完整地址在这里

捕获异常的几种方式

//	运行错误
//	https://developer.mozilla.org/zh-CN/docs/Web/API/GlobalEventHandlers/onerror
try catch

window.onerror = xxx
window.addEventListener('error', xxx)
//	其中try catch只能捕获其包裹的代码段里的异常,我们不能对每一个代码段都用try catch包起来,这样可读性太差了
//	window.onerror能捕获所有运行时异常,但是对于资源加载异常无法捕获
//	window.addEventListener('error', xxx)较为完善,既能捕获运行时异常,也能捕获资源加载的异常


//	Promise异常
//	https://developer.mozilla.org/zh-CN/docs/Web/Events/unhandledrejection
window.onunhandledrejection = xxx
window.addEventListener('unhandledrejection', xxx)
//	对于Promise来说,用捕获运行时错误的方式无法捕获到其抛出的异常,所以就需要用'unhandledrejection'来捕获

//	接口异常(XMLHttpRequest,fetch)
//	代理内部方法,此处省略

错误上报的方案

  • ajax通信,向后台发送错误信息

  • new Image().src = 'xxxx'上报,这也是主流方式

前面分析的都是异常原生里面捕获和上报的一些方式,现在我们看看Sentry是怎么处理的:

Sentry.init 初始化

//	dsn是我们用Sentry部署的监控平台上新建项目时生成的,
Sentry.init({
  dsn: 'https://xxxx'
});

init 源码:

function init(options) {
	//	如果options为undefined, 则把options赋值为空对象
  if (options === void 0) { options = {}; }
  
  //	如果没有指定集成哪些捕获钩子,则默认全部集成
  if (options.defaultIntegrations === undefined) {
    options.defaultIntegrations = defaultIntegrations;
  }
  
  //	这里是跟webpack和sourceMap集成相关的一些东西,项目里没用的,不做深究
  if (options.release === undefined) {
    //	获取当前运行环境, Nodejs、浏览器
    var window_1 = getGlobalObject();
    // This supports the variable that sentry-webpack-plugin injects
    if (window_1.SENTRY_RELEASE && window_1.SENTRY_RELEASE.id) {
      options.release = window_1.SENTRY_RELEASE.id;
    }
  }
  
  //	初始化并且绑定
  initAndBind(BrowserClient, options);
}

上面init源码里第二个if里面用到了defaultIntegrations,这边是所有Sentry内置的异常捕获钩子,最主要的也是这边,下面先看看defaultIntegrations有哪些钩子:

var defaultIntegrations = [
  new InboundFilters(),
  new FunctionToString(),
  new TryCatch(),
  new Breadcrumbs(),
  new GlobalHandlers(),
  new LinkedErrors(),
  new UserAgent(),
];

InboundFiltersFunctionToStringSentry/core里面实现的,先不做分析,从TryCatch来分析

TryCatch是给内置一些API加上try catch包裹

function TryCatch() {
  /** JSDoc */
  this._ignoreOnError = 0;
  /**
             * @inheritDoc
             */
  this.name = TryCatch.id;
}
/** JSDoc */
TryCatch.prototype._wrapTimeFunction = function (original) {
  return function () {
    var args = [];
    for (var _i = 0; _i < arguments.length; _i++) {
      args[_i] = arguments[_i];
    }
    var originalCallback = args[0];
    args[0] = wrap(originalCallback, {
      mechanism: {
        data: { function: getFunctionName(original) },
        handled: true,
        type: 'instrument',
      },
    });
    return original.apply(this, args);
  };
};
/** JSDoc */
TryCatch.prototype._wrapRAF = function (original) {
  return function (callback) {
    return original(wrap(callback, {
      mechanism: {
        data: {
          function: 'requestAnimationFrame',
          handler: getFunctionName(original),
        },
        handled: true,
        type: 'instrument',
      },
    }));
  };
};
/** JSDoc */
TryCatch.prototype._wrapEventTarget = function (target) {
  var global = getGlobalObject();
  var proto = global[target] && global[target].prototype;
  if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
    return;
  }
  fill(proto, 'addEventListener', function (original) {
    return function (eventName, fn, options) {
      try {
        // tslint:disable-next-line:no-unbound-method strict-type-predicates
        if (typeof fn.handleEvent === 'function') {
          fn.handleEvent = wrap(fn.handleEvent.bind(fn), {
            mechanism: {
              data: {
                function: 'handleEvent',
                handler: getFunctionName(fn),
                target: target,
              },
              handled: true,
              type: 'instrument',
            },
          });
        }
      }
      catch (err) {
        // can sometimes get 'Permission denied to access property "handle Event'
      }
      return original.call(this, eventName, wrap(fn, {
        mechanism: {
          data: {
            function: 'addEventListener',
            handler: getFunctionName(fn),
            target: target,
          },
          handled: true,
          type: 'instrument',
        },
      }), options);
    };
  });
  fill(proto, 'removeEventListener', function (original) {
    return function (eventName, fn, options) {
      var callback = fn;
      try {
        callback = callback && (callback.__sentry_wrapped__ || callback);
      }
      catch (e) {
        // ignore, accessing __sentry_wrapped__ will throw in some Selenium environments
      }
      return original.call(this, eventName, callback, options);
    };
  });
};
/**
         * Wrap timer functions and event targets to catch errors
         * and provide better metadata.
         */
TryCatch.prototype.setupOnce = function () {
  this._ignoreOnError = this._ignoreOnError;
  var global = getGlobalObject();
  fill(global, 'setTimeout', this._wrapTimeFunction.bind(this));
  fill(global, 'setInterval', this._wrapTimeFunction.bind(this));
  fill(global, 'requestAnimationFrame', this._wrapRAF.bind(this));
  [
    'EventTarget',
    'Window',
    'Node',
    'ApplicationCache',
    'AudioTrackList',
    'ChannelMergerNode',
    'CryptoOperation',
    'EventSource',
    'FileReader',
    'HTMLUnknownElement',
    'IDBDatabase',
      'IDBRequest',
      'IDBTransaction',
      'KeyOperation',
      'MediaController',
      'MessagePort',
      'ModalWindow',
      'Notification',
      'SVGElementInstance',
      'Screen',
      'TextTrack',
      'TextTrackCue',
      'TextTrackList',
      'WebSocket',
      'WebSocketWorker',
      'Worker',
      'XMLHttpRequest',
      'XMLHttpRequestEventTarget',
      'XMLHttpRequestUpload',
    ].forEach(this._wrapEventTarget.bind(this));
  };