本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是
学习源码整体架构系列第四篇,链接: juejin.cn/post/684490… 。
更新:20240630 new BrowserClient(options)
导读:
从sentry错误监控原理、sentry初始化、Ajax上报、window.onerror、window.onunhandledrejection几个方面来学习源码。 本文分析源码为打包后未压缩的源码,源码总行数五千余行,链接地址是:browser.sentry-cdn.com/5.7.1/bundl…
先梳理一下前端错误监控的知识
前端错误监控知识
摘抄自慕课网视频教程:前端跳槽面试必备技巧 来自于别人做的笔记 前端跳槽面试必备技巧-4-4 错误监控类
前端错误分类
- 即使运行错误:代码错误
- try...cache...
- window.error(也可以用DOM2事件监听)
- 资源加载错误
- object.oneror: dom对象的onerror事件
- performance.getEntries()
- Error事件捕获
- 使用performance.getEntries()获取网页图片加载错误
- var allImgs = document.getElementsByTageName('image')
- var loadedImgs = performence.getEntries().filter(i => i.initiatorType === 'image');
- 最后allImgs和loadedImgs对比即可找出图片资源未加载项目。
Error事件捕获代码示例
window.addEventListener('error', function(e) {
console.log('捕获', e);
}, true) // 这里只有捕获才能触发事件,冒泡不能触发
上报错误的基本原理
- 通过Ajax通讯的方式上报
- 利用Image对象上报(主流方式)
- Image上报错误方式:(new Image()).src = 'www.example.com/error?name=…'
Sentry前端异常监控基本原理
- 重写window.error方法、重写window.onunhandledrejection方法
/**
* message 发生错误的字符串
* source 发生错误脚本的URL
* lineno 发生错误的行号
* colno 发生错误的列号
* error Error对象(对象)
*/
window.onerror = function(message, source, lineno, colno, error) {
console.log('message, source, lineno, colno, error', message, source, lineno, colno, error)
}
当Promise被reject并且没有reject处理器时,会触发onunhandledrejection事件;这可能发生在window中,也可能发生在Worker中。这对调试回退错误处理非常有用。
Sentry源码可以搜索global.onerror定位到具体位置。
Globalhandlers.prototype._installGlobalOnErrorHandler = function () {
// code...
// 代码有删减
// 这里的this._global在浏览器就是window
this._oldOnErrorHanlder = this._global.onerror;
this._global.onerror = function (msg, url, line, column, error) {}
// code...
}
同样,Sentry可以搜索到global.onunhandledrejection定位具体位置
GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function() {
// code
// 代码有删减
this._oldOnUnhandledRejectionHandler = this._global.onunhandledrejection;
this._global.onunhandledrejection = function(e) {}
}
- 采用Ajax方式上传
支持fetch,用fetch,否则用XHR
BrowserBackend.prototype._setupTransport = function () {
// code...
// 代码有删减
if (supportsFetch) {
return new FetchTransport(transportOptions);
}
return new XHRTransport(transprotOptions);
}
2.1 fetch
FetchTransport.prototype.sendEvent = function(event) {
var defaultOptions = {
body: JSON.stringify(event),
method: 'POST',
referrerPolicy: (supportsReferrerPlicy() ? 'origin' : ''),
}
return this._buffer.add(global$2.fetch(this.url, defaultOptions).then(function (response) {return ({
status: exports.Status.fromHttpCode(response.status);
});
}));
};
2.2 XHR
XHRTransport.prototype.sendEvent = function(event) {
var _this = this;
return this._buffer.add(new SyncPromise(function (reslove, 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.onunhadnledrejection三条主线分析源码。
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;
}({}));
Sentry.init 初始化 之 init函数
初始化
// dsn 是Sentry网站生成的
Sentry.init({ dsn: 'xxx' });
// 这里options是 { dsn: 'xxx' }
function init (options) {
// 如果options 是undefined,则复制为空对象
if (options === void 0) {options = {};}
// 如果没传defaultIntegrations,则赋值默认的
if (default.defaultIntegrations === undefined) {
default.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(BrowerClient, options);
}
getGlobalObject、inNodeEnv
/**
* 判断是否是node环境
**/
function isNodeEnv() {
return Object.prototype.toString().call(typeof process !== 'undefined' ? process : 0) === '[object process]'
}
var fallbackGlobalObject = {};
function getGlobalObject() {
return (isNodeEnv()
// 是node 环境 赋值给 global
? global
: typeof window !== 'undefined'
? window
// 不是window self 不是undefined 说明是Web Worker环境
: typeof self !== 'undefined'
? self
// 都不是,赋值空对象
: fallbackGlobalObject
)
}
initAndBind 函数之 new BrowserClient(options)
function initAndBind(clientClass, options) {
// 这里没有开启debug模式,logger.enable() 这句不执行
if (options.debug === true) {
logger.enable();
}
getCurrentHub().bindlClient(new clientClass(options));
}
initAndBind第一个参数是BrowserClient构造函数,第二个参数是初始化后的options.
先看BrowserClient构造函数再看另一条线getCurrentHub().bindlClient()
BrowserClient
var BrowserClient = /** @class */ (function (_super) {
// BrowserClien继承自 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));
由代码可以看出,BrowserClientClient继承自BaseClient,并且把BrowserBackend,options传参给BaseClient调用。
先看 BrowserBackend,这里的BaseClient暂时不看。
这里补一下_extends、extendStatistics打包代码实现的继承
var extendStatics = function(d, b) {
// 如果支持Objefct.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);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
BrowserBackend 构造函数 (浏览器后端)
var BrowserBackend = /** @class */ (function (_super) {
__extends(BrowserBackend, _super);
function BrowserBackend() {
return _super !== null && _super.apply(this, arguments) || this;
}
/**
* @inheritDoc
*/
// 设置请求
BrowserBackend.prototype._setupTransport = function () {
if (!this._options.dsn) {
// 没有设置dsn,调用baseBackend.prototype._setupTransport 返回空函数
// We return the noop transport here in case there is no Dsn.
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。
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...
/**
* @inheritDoc
*/
BaseBackend.prototype.sendEvent = function (event) {
this._transport.sendEvent(event).then(null, function (reason) {
logger.error("Error while sending event: " + reason);
});
};
/**
* @inheritDoc
*/
BaseBackend.prototype.getTransport = function () {
return this._transport;
};
return BaseBackend;
}());
经过一系列继承,再回过头看BaseClient构造函数
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;
}());
小结1. new BrowerClient 经过一系列的继承和初始化
最终可以输出具体new clientClass(options)之后的结果:
function initAndBind(clientClass, options) {
// 这里没有开启debug模式,logger.enable() 这句不执行
if (options.debug === true) {
logger.enable();
}
var client = new clientClass(options);
console.log('new clientClass(options)', client);
getCurrentHub().bindlClient(client);
// 原来的代码
// getCurrentHub().bindlClient(new clientClass(options));
}
总结:
captureMessage函数
调用栈主要流程:
function captureException(exception) {
var syntheticException;
try {
throw new Error('Sentry syntheticException');
}
catch (exception) {
syntheticException = exception;
}
// 调用 callOnhub方法
return callOnHub('captureException', exception, {
originalException: exception,
syntheticException: syntheticException,
});
}
---> callOnHub
/**
* method 在这里是 'captureException'
*/
function callOnHub(method) {
var args = [];
// 将除了method除外的其他参数放到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.captureException
Hub.prototype.captureException = function (exception, hint) {
var eventId = (this._lastEventId = uuid4());
var finalHint = hint;
// ...code代码有删减
this._invokeClient('captureException', exception, __assign({}, finalHint, { event_id: eventId }));
return eventId;
};
---> Hub.prototype._invokeClient
/**
* 同样,method 在这里是 'captureException'
*/
Hub.prototype._invokeClient = function (method) {
var _a;
var args = [];
// 把method除外的其他参数放到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);
//最后调用BaseClient.prototype._processEvent
promisedEvent
.then(function (event) { return _this._processEvent(event, hint, scope); })
.then(function (finalEvent) {
// We need to check for finalEvent in case beforeSend returned null
eventId = finalEvent && finalEvent.event_id;
_this._processing = false;
})
.then(null, function (reason) {
logger.error(reason);
_this._processing = false;
});
return eventId;
};
---> BaseClient.prototype._processEvent
BaseClient.prototype._processEvent = function (event, hint, scope) {
// ...code 代码有删减
_this._getBackend().sendEvent(finalEvent);
// code...
};
---> 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最终发送了请求 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),
}); }));
};
未完待续...
此文章为2024年05月Day1源码共读,每一次脑海里闪过努力的念头,都是未来的你在向你求救。