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(),
];
InboundFilters
和FunctionToString
是Sentry/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));
};