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

若川大约 17 分钟

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

1. 前言

大家好,我是若川open in new window。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动open in new window,感兴趣的可以点此扫码加我微信 ruochuan02 参与open in new window。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》open in new window,目前是掘金关注人数(4.1k+人)第一的专栏,写有20余篇源码文章。

这是学习源码整体架构系列第四篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。

本文仓库地址open in new windowgit clone https://github.com/lxchuan12/sentry-analysis.git

要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是太好了

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库open in new window
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库open in new window
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库open in new window
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDKopen in new window
5.学习 vuex 源码整体架构,打造属于自己的状态管理库open in new window
6.学习 axios 源码整体架构,打造属于自己的请求库open in new window
7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理open in new window
8.学习 redux 源码整体架构,深入理解 redux 及其中间件原理open in new window

感兴趣的读者可以点击阅读。
其他源码计划中的有:expressopen in new windowvue-rotueropen in new windowreact-reduxopen in new window 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。

源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。
所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。

导读
本文通过梳理前端错误监控知识、介绍sentry错误监控原理、sentry初始化、Ajax上报、window.onerror、window.onunhandledrejection几个方面来学习sentry的源码。

开发微信小程序,想着搭建小程序错误监控方案。最近用了丁香园 开源的Sentry 小程序 SDKsentry-miniappopen in new window。 顺便研究下sentry-javascript仓库open in new window 的源码整体架构,于是有了这篇文章。

本文分析的是打包后未压缩的源码,源码总行数五千余行,链接地址是:https://browser.sentry-cdn.com/5.7.1/bundle.jsopen in new window, 版本是v5.7.1

本文示例等源代码在这我的github博客中github blog sentryopen in new window,需要的读者可以点击查看,如果觉得不错,可以顺便star一下。

看源码前先来梳理下前端错误监控的知识。

2. 前端错误监控知识

摘抄自 慕课网视频教程:前端跳槽面试必备技巧open in new window
别人做的笔记:前端跳槽面试必备技巧-4-4 错误监控类open in new window

2.1 前端错误的分类

1.即时运行错误:代码错误

try...catch

window.onerror (也可以用DOM2事件监听)

2.资源加载错误

object.onerror: dom对象的onerror事件

performance.getEntries()

Error事件捕获

3.使用performance.getEntries()获取网页图片加载错误

var allImgs = document.getElementsByTagName('image')

var loadedImgs = performance.getEntries().filter(i => i.initiatorType === 'img')

最后allImsloadedImgs对比即可找出图片资源未加载项目

2.2 Error事件捕获代码示例

window.addEventListener('error', function(e) {
  console.log('捕获', e)
}, true) // 这里只有捕获才能触发事件,冒泡是不能触发

2.3 上报错误的基本原理

1.采用Ajax通信的方式上报

2.利用Image对象上报 (主流方式)

Image上报错误方式: (new Image()).src = 'https://lxchuan12.cn/error?name=若川'

3. Sentry 前端异常监控基本原理

1.重写 window.onerror 方法、重写 window.onunhandledrejection 方法

如果不了解onerror和onunhandledrejection方法的读者,可以看相关的MDN文档。这里简要介绍一下:

MDN GlobalEventHandlers.onerroropen in new window

window.onerror = function (message, source, lineno, colno, error) {
	console.log('message, source, lineno, colno, error', message, source, lineno, colno, error);
}

参数:
message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event
source:发生错误的脚本URL(字符串)
lineno:发生错误的行号(数字)
colno:发生错误的列号(数字)
errorError对象(对象)

MDN unhandledrejectionopen in new window

Promisereject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件;这可能发生在 window 下,但也可能发生在 Worker 中。 这对于调试回退错误处理非常有用。

Sentry 源码可以搜索 global.onerror 定位到具体位置

 GlobalHandlers.prototype._installGlobalOnErrorHandler = function () {
	// 代码有删减
	// 这里的 this._global 在浏览器中就是 window
	this._oldOnErrorHandler = this._global.onerror;
	this._global.onerror = function (msg, url, line, column, error) {}
	// code ...
 }

同样,可以搜索global.onunhandledrejection 定位到具体位置

GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function () {
	// 代码有删减
	this._oldOnUnhandledRejectionHandler = this._global.onunhandledrejection;
	this._global.onunhandledrejection = function (e) {}
}

2.采用Ajax上传

支持 fetch 使用 fetch,否则使用 XHR

BrowserBackend.prototype._setupTransport = function () {
	// 代码有删减
	if (supportsFetch()) {
		return new FetchTransport(transportOptions);
	}
	return new XHRTransport(transportOptions);
};

2.1 fetch

FetchTransport.prototype.sendEvent = function (event) {
	var defaultOptions = {
		body: JSON.stringify(event),
		method: 'POST',
		referrerPolicy: (supportsReferrerPolicy() ? 'origin' : ''),
	};
	return this._buffer.add(global$2.fetch(this.url, defaultOptions).then(function (response) { return ({
		status: exports.Status.fromHttpCode(response.status),
	}); }));
};

2.2 XMLHttpRequest

XHRTransport.prototype.sendEvent = function (event) {
	var _this = this;
	return this._buffer.add(new SyncPromise(function (resolve, reject) {
		// 熟悉的 XMLHttpRequest
		var request = new XMLHttpRequest();
		request.onreadystatechange = function () {
			if (request.readyState !== 4) {
				return;
			}
			if (request.status === 200) {
				resolve({
					status: exports.Status.fromHttpCode(request.status),
				});
			}
			reject(request);
		};
		request.open('POST', _this.url);
		request.send(JSON.stringify(event));
	}));
}

接下来主要通过Sentry初始化、如何Ajax上报window.onerror、window.onunhandledrejection三条主线来学习源码。

如果看到这里,暂时不想关注后面的源码细节,直接看后文小结1和2的两张图。或者可以点赞或收藏这篇文章,后续想看了再看。

4. Sentry 源码入口和出口

var Sentry = (function(exports){
	// code ...

    var SDK_NAME = 'sentry.javascript.browser';
	var SDK_VERSION = '5.7.1';

	// code ...
	// 省略了导出的Sentry的若干个方法和属性
	// 只列出了如下几个
    exports.SDK_NAME = SDK_NAME;
    exports.SDK_VERSION = SDK_VERSION;
	// 重点关注 captureMessage
    exports.captureMessage = captureMessage;
	// 重点关注 init
    exports.init = init;

    return exports;
}({}));

5. Sentry.init 初始化 之 init 函数

初始化

// 这里的dsn,是sentry.io网站会生成的。
Sentry.init({ dsn: 'xxx' });
// options 是 {dsn: '...'}
function init(options) {
	// 如果options 是undefined,则赋值为 空对象
	if (options === void 0) { options = {}; }
	// 如果没传 defaultIntegrations 则赋值默认的
	if (options.defaultIntegrations === undefined) {
		options.defaultIntegrations = defaultIntegrations;
	}
	// 初始化语句
	if (options.release === undefined) {
		var window_1 = getGlobalObject();
		// 这是给  sentry-webpack-plugin 插件提供的,webpack插件注入的变量。这里没用这个插件,所以这里不深究。
		// 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);
}

5.1 getGlobalObject、inNodeEnv 函数

很多地方用到这个函数getGlobalObject。其实做的事情也比较简单,就是获取全局对象。浏览器中是window

/**
 * 判断是否是node环境
 * Checks whether we're in the Node.js or Browser environment
 *
 * @returns Answer to given question
 */
function isNodeEnv() {
	// tslint:disable:strict-type-predicates
	return Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]';
}
var fallbackGlobalObject = {};
/**
 * Safely get global scope object
 *
 * @returns Global scope object
 */
function getGlobalObject() {
	return (isNodeEnv()
	// 是 node 环境 赋值给 global
		? global
		: typeof window !== 'undefined'
			? window
			// 不是 window self 不是undefined 说明是 Web Worker 环境
			: typeof self !== 'undefined'
				? self
				// 都不是,赋值给空对象。
				: fallbackGlobalObject);

继续看 initAndBind 函数

6. initAndBind 函数之 new BrowserClient(options)

function initAndBind(clientClass, options) {
	// 这里没有开启debug模式,logger.enable() 这句不会执行
	if (options.debug === true) {
		logger.enable();
	}
	getCurrentHub().bindClient(new clientClass(options));
}

可以看出 initAndBind(),第一个参数是 BrowserClient 构造函数,第二个参数是初始化后的options。 接着先看 构造函数 BrowserClient。 另一条线 getCurrentHub().bindClient() 先不看。

6.1 BrowserClient 构造函数

var BrowserClient = /** @class */ (function (_super) {
	// `BrowserClient` 继承自`BaseClient`
	__extends(BrowserClient, _super);
	/**
	 * Creates a new Browser SDK instance.
	 *
	 * @param options Configuration options for this SDK.
	 */
	function BrowserClient(options) {
		if (options === void 0) { options = {}; }
		// 把`BrowserBackend`,`options`传参给`BaseClient`调用。
		return _super.call(this, BrowserBackend, options) || this;
	}
	return BrowserClient;
}(BaseClient));

从代码中可以看出BrowserClient 继承自BaseClient,并且把BrowserBackendoptions传参给BaseClient调用。

先看 BrowserBackend,这里的BaseClient,暂时不看。

BrowserBackend之前,先提一下继承、继承静态属性和方法。

6.2 __extends、extendStatics 打包代码实现的继承

未打包的源码是使用ES6 extends实现的。这是打包后的对ES6extends的一种实现。

如果对继承还不是很熟悉的读者,可以参考我之前写的文章。面试官问:JS的继承open in new window

// 继承静态方法和属性
var extendStatics = function(d, b) {
	// 如果支持 Object.setPrototypeOf 这个函数,直接使用
	// 不支持,则使用原型__proto__ 属性,
	// 如何还不支持(但有可能__proto__也不支持,毕竟是浏览器特有的方法。)
	// 则使用for in 遍历原型链上的属性,从而达到继承的目的。
	extendStatics = Object.setPrototypeOf ||
		({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
		function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
	return extendStatics(d, b);
};

function __extends(d, b) {
	extendStatics(d, b);
	// 申明构造函数__ 并且把 d 赋值给 constructor
	function __() { this.constructor = d; }
	// (__.prototype = b.prototype, new __()) 这种逗号形式的代码,最终返回是后者,也就是 new __()
	// 比如 (typeof null, 1) 返回的是1
	// 如果 b === null 用Object.create(b) 创建 ,也就是一个不含原型链等信息的空对象 {}
	// 否则使用 new __() 返回
	d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}

不得不说这打包后的代码十分严谨,上面说的我的文章《面试官问:JS的继承》中没有提到不支持__proto__的情况。看来这文章可以进一步严谨修正了。 让我想起Vue源码中对数组检测代理判断是否支持__proto__的判断。

// vuejs 源码:https://github.com/vuejs/vue/blob/dev/dist/vue.js#L526-L527
// can we use __proto__?
var hasProto = '__proto__' in {};

看完打包代码实现的继承,继续看 BrowserBackend 构造函数

6.3 BrowserBackend 构造函数 (浏览器后端)

var BrowserBackend = /** @class */ (function (_super) {
    __extends(BrowserBackend, _super);
	function BrowserBackend() {
		return _super !== null && _super.apply(this, arguments) || this;
	}
	/**
	 * 设置请求
	 */
	BrowserBackend.prototype._setupTransport = function () {
		if (!this._options.dsn) {
			// We return the noop transport here in case there is no Dsn.
			// 没有设置dsn,调用BaseBackend.prototype._setupTransport 返回空函数
			return _super.prototype._setupTransport.call(this);
		}
		var transportOptions = __assign({}, this._options.transportOptions, { dsn: this._options.dsn });
		if (this._options.transport) {
			return new this._options.transport(transportOptions);
		}
		// 支持Fetch则返回 FetchTransport 实例,否则返回 XHRTransport实例,
		// 这两个构造函数具体代码在开头已有提到。
		if (supportsFetch()) {
			return new FetchTransport(transportOptions);
		}
		return new XHRTransport(transportOptions);
	};
	// code ...
	return BrowserBackend;
}(BaseBackend));

BrowserBackend 又继承自 BaseBackend

6.3.1 BaseBackend 构造函数 (基础后端)

/**
 * This is the base implemention of a Backend.
 * @hidden
 */
var BaseBackend = /** @class */ (function () {
	/** Creates a new backend instance. */
	function BaseBackend(options) {
		this._options = options;
		if (!this._options.dsn) {
			logger.warn('No DSN provided, backend will not do anything.');
		}
		// 调用设置请求函数
		this._transport = this._setupTransport();
	}
	/**
	 * Sets up the transport so it can be used later to send requests.
	 * 设置发送请求空函数
	 */
	BaseBackend.prototype._setupTransport = function () {
		return new NoopTransport();
	};
	// code ...
	BaseBackend.prototype.sendEvent = function (event) {
		this._transport.sendEvent(event).then(null, function (reason) {
			logger.error("Error while sending event: " + reason);
		});
	};
	BaseBackend.prototype.getTransport = function () {
		return this._transport;
	};
	return BaseBackend;
}());

通过一系列的继承后,回过头来看 BaseClient 构造函数。

6.3.2 BaseClient 构造函数(基础客户端)

var BaseClient = /** @class */ (function () {
	/**
	 * Initializes this client instance.
	 *
	 * @param backendClass A constructor function to create the backend.
	 * @param options Options for the client.
	 */
	function BaseClient(backendClass, options) {
		/** Array of used integrations. */
		this._integrations = {};
		/** Is the client still processing a call? */
		this._processing = false;
		this._backend = new backendClass(options);
		this._options = options;
		if (options.dsn) {
			this._dsn = new Dsn(options.dsn);
		}
		if (this._isEnabled()) {
			this._integrations = setupIntegrations(this._options);
		}
	}
	// code ...
	return BaseClient;
}());

6.4 小结1. new BrowerClient 经过一系列的继承和初始化

可以输出下具体new clientClass(options)之后的结果:

function initAndBind(clientClass, options) {
	if (options.debug === true) {
		logger.enable();
	}
	var client = new clientClass(options);
	console.log('new clientClass(options)', client);
	getCurrentHub().bindClient(client);
	// 原来的代码
	// getCurrentHub().bindClient(new clientClass(options));
}

最终输出得到这样的数据。我画了一张图表示。重点关注的原型链用颜色标注了,其他部分收缩了。

sentry new BrowserClient 实例图 By@若川
sentry new BrowserClient 实例图 By@若川

7. initAndBind 函数之 getCurrentHub().bindClient()

继续看 initAndBind 的另一条线。

function initAndBind(clientClass, options) {
	if (options.debug === true) {
		logger.enable();
	}
	getCurrentHub().bindClient(new clientClass(options));
}

获取当前的控制中心 Hub,再把new BrowserClient() 的实例对象绑定在Hub上。

7.1 getCurrentHub 函数

// 获取当前Hub 控制中心
function getCurrentHub() {
	// Get main carrier (global for every environment)
	var registry = getMainCarrier();
	// 如果没有控制中心在载体上,或者它的版本是老版本,就设置新的。
	// If there's no hub, or its an old API, assign a new one
	if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) {
		setHubOnCarrier(registry, new Hub());
	}
	// node 才执行
	// Prefer domains over global if they are there (applicable only to Node environment)
	if (isNodeEnv()) {
		return getHubFromActiveDomain(registry);
	}
	// 返回当前控制中心来自载体上。
	// Return hub that lives on a global object
	return getHubFromCarrier(registry);
}

7.2 衍生的函数 getMainCarrier、getHubFromCarrier

function getMainCarrier() {
	// 载体 这里是window
	// 通过一系列new BrowerClient() 一系列的初始化
	// 挂载在  carrier.__SENTRY__ 已经有了三个属性,globalEventProcessors, hub, logger
	var carrier = getGlobalObject();
	carrier.__SENTRY__ = carrier.__SENTRY__ || {
		hub: undefined,
	};
	return carrier;
}
// 获取控制中心 hub 从载体上
function getHubFromCarrier(carrier) {
	// 已经有了则返回,没有则new Hub
	if (carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub) {
		return carrier.__SENTRY__.hub;
	}
	carrier.__SENTRY__ = carrier.__SENTRY__ || {};
	carrier.__SENTRY__.hub = new Hub();
	return carrier.__SENTRY__.hub;
}

7.3 bindClient 绑定客户端在当前控制中心上

Hub.prototype.bindClient = function (client) {
	// 获取最后一个
	var top = this.getStackTop();
	// 把 new BrowerClient() 实例 绑定到top上
	top.client = client;
};
Hub.prototype.getStackTop = function () {
	// 获取最后一个
	return this._stack[this._stack.length - 1];
};

7.4 小结2. 经过一系列的继承和初始化

再回过头来看 initAndBind函数

function initAndBind(clientClass, options) {
	if (options.debug === true) {
		logger.enable();
	}
	var client = new clientClass(options);
	console.log(client, options, 'client, options');
	var currentHub = getCurrentHub();
	currentHub.bindClient(client);
	console.log('currentHub', currentHub);
	// 源代码
	// getCurrentHub().bindClient(new clientClass(options));
}

最终会得到这样的Hub实例对象。笔者画了一张图表示,便于查看理解。

Hub 实例关系图
Hub 实例关系图

初始化完成后,再来看具体例子。 具体 captureMessage 函数的实现。

Sentry.captureMessage('Hello, 若川!');

8. captureMessage 函数

通过之前的阅读代码,知道会最终会调用Fetch接口,所以直接断点调试即可,得出如下调用栈。 接下来描述调用栈的主要流程。

captureMessage 断点调试图
captureMessage 断点调试图

调用栈主要流程:

captureMessage

function captureMessage(message, level) {
	var syntheticException;
	try {
		throw new Error(message);
	}
	catch (exception) {
		syntheticException = exception;
	}
	// 调用 callOnHub 方法
	return callOnHub('captureMessage', message, level, {
		originalException: message,
		syntheticException: syntheticException,
	});
}

=> callOnHub

/**
 * This calls a function on the current hub.
 * @param method function to call on hub.
 * @param args to pass to function.
 */
function callOnHub(method) {
	// 这里method 传进来的是 'captureMessage'
	// 把method除外的其他参数放到args数组中
	var args = [];
	for (var _i = 1; _i < arguments.length; _i++) {
		args[_i - 1] = arguments[_i];
	}
	// 获取当前控制中心 hub
	var hub = getCurrentHub();
	// 有这个方法 把args 数组展开,传递给 hub[method] 执行
	if (hub && hub[method]) {
		// tslint:disable-next-line:no-unsafe-any
		return hub[method].apply(hub, __spread(args));
	}
	throw new Error("No hub defined or " + method + " was not found on the hub, please open a bug report.");
}

=> Hub.prototype.captureMessage

接着看Hub.prototype 上定义的 captureMessage 方法

Hub.prototype.captureMessage = function (message, level, hint) {
	var eventId = (this._lastEventId = uuid4());
	var finalHint = hint;
	// 代码有删减
	this._invokeClient('captureMessage', message, level, __assign({}, finalHint, { event_id: eventId }));
	return eventId;
};

=> Hub.prototype._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.
 */
Hub.prototype._invokeClient = function (method) {
	// 同样:这里method 传进来的是 'captureMessage'
	// 把method除外的其他参数放到args数组中
	var _a;
	var args = [];
	for (var _i = 1; _i < arguments.length; _i++) {
		args[_i - 1] = arguments[_i];
	}
	var top = this.getStackTop();
	// 获取控制中心的 hub,调用客户端也就是new BrowerClient () 实例中继承自 BaseClient 的 captureMessage 方法
	// 有这个方法 把args 数组展开,传递给 hub[method] 执行
	if (top && top.client && top.client[method]) {
		(_a = top.client)[method].apply(_a, __spread(args, [top.scope]));
	}
};

=> BaseClient.prototype.captureMessage

BaseClient.prototype.captureMessage = function (message, level, hint, scope) {
	var _this = this;
	var eventId = hint && hint.event_id;
	this._processing = true;
	var promisedEvent = isPrimitive(message)
		? this._getBackend().eventFromMessage("" + message, level, hint)
		: this._getBackend().eventFromException(message, hint);
		// 代码有删减
	promisedEvent
		.then(function (event) { return _this._processEvent(event, hint, scope); })
	// 代码有删减
	return eventId;
};

最后会调用 _processEvent 也就是

=> BaseClient.prototype._processEvent

这个函数最终会调用

_this._getBackend().sendEvent(finalEvent);

也就是

=> BaseBackend.prototype.sendEvent

BaseBackend.prototype.sendEvent = function (event) {
	this._transport.sendEvent(event).then(null, function (reason) {
		logger.error("Error while sending event: " + reason);
	});
};

=> FetchTransport.prototype.sendEvent 最终发送了请求

8.1 FetchTransport.prototype.sendEvent

FetchTransport.prototype.sendEvent = function (event) {
	var defaultOptions = {
		body: JSON.stringify(event),
		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' : ''),
	};
	// global$2.fetch(this.url, defaultOptions) 使用fetch发送请求
	return this._buffer.add(global$2.fetch(this.url, defaultOptions).then(function (response) { return ({
		status: exports.Status.fromHttpCode(response.status),
	}); }));
};

看完 Ajax 上报 主线,再看本文的另外一条主线 window.onerror 捕获。

9. window.onerror 和 window.onunhandledrejection 捕获 错误

例子:调用一个未申明的变量。

func();

Promise 不捕获错误

new Promise(() => {
	fun();
})
.then(res => {
	console.log('then');
})

9.1 captureEvent

调用栈主要流程:

window.onerror

GlobalHandlers.prototype._installGlobalOnErrorHandler = function () {
	if (this._onErrorHandlerInstalled) {
		return;
	}
	var self = this; // tslint:disable-line:no-this-assignment
	// 浏览器中这里的 this._global.  就是window
	this._oldOnErrorHandler = this._global.onerror;
	this._global.onerror = function (msg, url, line, column, error) {
		var currentHub = getCurrentHub();
		// 代码有删减
		currentHub.captureEvent(event, {
			originalException: error,
		});
		if (self._oldOnErrorHandler) {
			return self._oldOnErrorHandler.apply(this, arguments);
		}
		return false;
	};
	this._onErrorHandlerInstalled = true;
};

window.onunhandledrejection

GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function () {
	if (this._onUnhandledRejectionHandlerInstalled) {
		return;
	}
	var self = this; // tslint:disable-line:no-this-assignment
	this._oldOnUnhandledRejectionHandler = this._global.onunhandledrejection;
	this._global.onunhandledrejection = function (e) {
		// 代码有删减
		var currentHub = getCurrentHub();
		currentHub.captureEvent(event, {
			originalException: error,
		});
		if (self._oldOnUnhandledRejectionHandler) {
			return self._oldOnUnhandledRejectionHandler.apply(this, arguments);
		}
		return false;
	};
	this._onUnhandledRejectionHandlerInstalled = true;
};

共同点:都会调用currentHub.captureEvent

currentHub.captureEvent(event, {
	originalException: error,
});

=> Hub.prototype.captureEvent

最终又是调用 _invokeClient ,调用流程跟 captureMessage 类似,这里就不再赘述。

this._invokeClient('captureEvent')

=> Hub.prototype._invokeClient

=> BaseClient.prototype.captureEvent

=> BaseClient.prototype._processEvent

=> BaseBackend.prototype.sendEvent

=> FetchTransport.prototype.sendEvent

最终同样是调用了这个函数发送了请求。

可谓是殊途同归,行文至此就基本已经结束,最后总结一下。

10. 总结

Sentry-JavaScript源码高效利用了JS的原型链机制。可谓是惊艳,值得学习。

本文通过梳理前端错误监控知识、介绍sentry错误监控原理、sentry初始化、Ajax上报、window.onerror、window.onunhandledrejection几个方面来学习sentry的源码。还有很多细节和构造函数没有分析。

总共的构造函数(类)有25个,提到的主要有9个,分别是:Hub、BaseClient、BaseBackend、BaseTransport、FetchTransport、XHRTransport、BrowserBackend、BrowserClient、GlobalHandlers

其他没有提到的分别是 SentryError、Logger、Memo、SyncPromise、PromiseBuffer、Span、Scope、Dsn、API、NoopTransport、FunctionToString、InboundFilters、TryCatch、Breadcrumbs、LinkedErrors、UserAgent

这些构造函数(类)中还有很多值得学习,比如同步的Promise(SyncPromise)。 有兴趣的读者,可以看这一块官方仓库中采用typescript写的源码SyncPromiseopen in new window,也可以看打包后出来未压缩的代码。

读源码比较耗费时间,写文章记录下来更加费时间(比如写这篇文章跨度十几天...),但收获一般都比较大。

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持。万分感谢。

11. 推荐阅读

知乎滴滴云:超详细!搭建一个前端错误监控系统open in new window
掘金BlackHole1:JavaScript集成Sentryopen in new window
丁香园 开源的Sentry 小程序 SDKsentry-miniappopen in new window
sentry官网open in new window
sentry-javascript仓库open in new window

笔者往期文章

面试官问:JS的继承open in new window
面试官问:JS的this指向open in new window
面试官问:能否模拟实现JS的call和apply方法open in new window
面试官问:能否模拟实现JS的bind方法open in new window
面试官问:能否模拟实现JS的new操作符open in new window
前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并open in new window

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
若川的博客open in new window,使用vuepress重构了,阅读体验可能更好些
掘金专栏open in new window,欢迎关注~
segmentfault前端视野专栏open in new window,欢迎关注~
知乎前端视野专栏open in new window,欢迎关注~
语雀前端视野专栏open in new window,新增语雀专栏,欢迎关注~
github blogopen in new window,相关源码和资源都放在这里,求个star_~

微信公众号 若川视野

可能比较有趣的微信公众号,长按扫码关注(回复pdf获取前端优质书籍pdf)。欢迎加笔者微信ruochuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~

若川视野
若川视野
欢迎扫码加我微信
拉你进源码共读群
一起学习源码