學習 sentry 源碼架構,打造屬於自己的前端異常監控平臺

2021-03-02 前端布道師

前言

這是學習源碼整體架構第四篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。文章學習的是打包整合後的代碼,不是實際倉庫中的拆分的代碼。

其餘三篇分別是:

1.學習 jQuery 源碼整體架構,打造屬於自己的 js 類庫

2.學習underscore源碼整體架構,打造屬於自己的函數式編程類庫

3.學習 lodash 源碼整體架構,打造屬於自己的函數式編程類庫

感興趣的讀者可以點擊閱讀。

導讀
本文通過梳理前端錯誤監控知識、介紹 sentry錯誤監控原理、 sentry初始化、 Ajax上報、 window.onerror、window.onunhandledrejection幾個方面來學習 sentry的源碼。

開發微信小程序,想著搭建小程序錯誤監控方案。最近用了丁香園 開源的 Sentry 小程序 SDKsentry-miniapp。順便研究下 sentry-javascript倉庫 的源碼整體架構,於是有了這篇文章。

本文分析的是打包後未壓縮的源碼,源碼總行數五千餘行,連結地址是:https://browser.sentry-cdn.com/5.7.1/bundle.js, 版本是 v5.7.1。

本文示例等原始碼在這我的 github博客中github blog sentry,需要的讀者可以點擊查看,如果覺得不錯,可以順便 star一下。

看源碼前先來梳理下前端錯誤監控的知識。

前端錯誤監控知識

摘抄自 慕課網視頻教程:前端跳槽面試必備技巧
別人做的筆記:前端跳槽面試必備技巧-4-4 錯誤監控類

前端錯誤的分類

1.即時運行錯誤:代碼錯誤

try...catch

window.onerror (也可以用 DOM2事件監聽)

2.資源加載錯誤

object.onerror: dom對象的 onerror事件

performance.getEntries()

Error事件捕獲

3.使用 performance.getEntries()獲取網頁圖片加載錯誤

varallImgs=document.getElementsByTagName('image')

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

最後 allIms和 loadedImgs對比即可找出圖片資源未加載項目

Error事件捕獲代碼示例

window.addEventListener('error', function(e) {

console.log('捕獲', e)

}, true) // 這裡只有捕獲才能觸發事件,冒泡是不能觸發

上報錯誤的基本原理

1.採用 Ajax通信的方式上報

2.利用 Image對象上報 (主流方式)

Image上報錯誤方式:(newImage()).src='https://lxchuan12.cn/error?name=若川'

Sentry 前端異常監控基本原理

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

如果不了解 onerror和onunhandledrejection方法的讀者,可以看相關的 MDN文檔。這裡簡要介紹一下:

MDN GlobalEventHandlers.onerror

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:發生錯誤的列號(數字)
error:Error對象(對象)

MDN unhandledrejection

當 Promise 被 reject 且沒有 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()) {

returnnewFetchTransport(transportOptions);

}

returnnewXHRTransport(transportOptions);

};

2.1 fetch

FetchTransport.prototype.sendEvent = function(event) {

var defaultOptions = {

body: JSON.stringify(event),

method: 'POST',

referrerPolicy: (supportsReferrerPolicy() ? 'origin': ''),

};

returnthis._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;

returnthis._buffer.add(newSyncPromise(function(resolve, reject) {

// 熟悉的 XMLHttpRequest

var request = newXMLHttpRequest();

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的兩張圖。或者可以點讚或收藏這篇文章,後續想看了再看。

Sentry 源碼入口和出口

varSentry= (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.io網站會生成的。

Sentry.init({ dsn: 'xxx'});

// options 是 {dsn: '...'}

function init(options) {

// 如果options 是undefined,則賦值為 空對象

if(options === void0) { 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);

}

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

returnObject.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 函數

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() 先不看。

BrowserClient 構造函數

varBrowserClient= /** @class */(function(_super) {

// `BrowserClient` 繼承自`BaseClient`

__extends(BrowserClient, _super);

/**

* Creates a new Browser SDK instance.

*

* @param options Configuration options for this SDK.

*/

functionBrowserClient(options) {

if(options === void0) { options = {}; }

// 把`BrowserBackend`,`options`傳參給`BaseClient`調用。

return _super.call(this, BrowserBackend, options) || this;

}

returnBrowserClient;

}(BaseClient));

從代碼中可以看出:BrowserClient 繼承自 BaseClient,並且把 BrowserBackend, options傳參給 BaseClient調用。

先看 BrowserBackend,這裡的 BaseClient,暫時不看。

看 BrowserBackend之前,先提一下繼承、繼承靜態屬性和方法。

__extends、extendStatics 打包代碼實現的繼承

未打包的源碼是使用 ES6extends實現的。這是打包後的對 ES6的 extends的一種實現。

如果對繼承還不是很熟悉的讀者,可以參考我之前寫的文章。面試官問:JS的繼承

// 繼承靜態方法和屬性

var extendStatics = function(d, b) {

// 如果支持 Object.setPrototypeOf 這個函數,直接使用

// 不支持,則使用原型__proto__ 屬性,

// 如何還不支持(但有可能__proto__也不支持,畢竟是瀏覽器特有的方法。)

// 則使用for in 遍歷原型鏈上的屬性,從而達到繼承的目的。

extendStatics = Object.setPrototypeOf ||

({ __proto__: [] } instanceofArray&& 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 構造函數

BrowserBackend 構造函數 (瀏覽器後端)

varBrowserBackend= /** @class */(function(_super) {

__extends(BrowserBackend, _super);

functionBrowserBackend() {

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) {

returnnewthis._options.transport(transportOptions);

}

// 支持Fetch則返回 FetchTransport 實例,否則返回 XHRTransport實例,

// 這兩個構造函數具體代碼在開頭已有提到。

if(supportsFetch()) {

returnnewFetchTransport(transportOptions);

}

returnnewXHRTransport(transportOptions);

};

// code ...

returnBrowserBackend;

}(BaseBackend));

BrowserBackend 又繼承自 BaseBackend。

BaseBackend 構造函數 (基礎後端)

/**

* This is the base implemention of a Backend.

* @hidden

*/

varBaseBackend= /** @class */(function() {

/** Creates a new backend instance. */

functionBaseBackend(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() {

returnnewNoopTransport();

};

// 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() {

returnthis._transport;

};

returnBaseBackend;

}());

通過一系列的繼承後,回過頭來看 BaseClient 構造函數。

BaseClient 構造函數(基礎客戶端)

varBaseClient= /** @class */(function() {

/**

* Initializes this client instance.

*

* @param backendClass A constructor function to create the backend.

* @param options Options for the client.

*/

functionBaseClient(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 = newDsn(options.dsn);

}

if(this._isEnabled()) {

this._integrations = setupIntegrations(this._options);

}

}

// code ...

returnBaseClient;

}());

小結1. new BrowerClient 經過一系列的繼承和初始化

可以輸出下具體 newclientClass(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));

}

最終輸出得到這樣的數據。我畫了一張圖表示。重點關注的原型鏈用顏色標註了,其他部分收縮了。


initAndBind 函數之 getCurrentHub().bindClient()

繼續看 initAndBind 的另一條線。

function initAndBind(clientClass, options) {

if(options.debug === true) {

logger.enable();

}

getCurrentHub().bindClient(new clientClass(options));

}

獲取當前的控制中心 Hub,再把 newBrowserClient() 的實例對象綁定在 Hub上。

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, newHub());

}

// 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);

}

衍生的函數 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 = newHub();

return carrier.__SENTRY__.hub;

}

bindClient 綁定客戶端在當前控制中心上

Hub.prototype.bindClient = function(client) {

// 獲取最後一個

var top = this.getStackTop();

// 把 new BrowerClient() 實例 綁定到top上

top.client = client;

};

Hub.prototype.getStackTop = function() {

// 獲取最後一個

returnthis._stack[this._stack.length - 1];

};

小結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實例對象。筆者畫了一張圖表示,便於查看理解。

初始化完成後,再來看具體例子。具體 captureMessage 函數的實現。

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

captureMessage 函數

通過之前的閱讀代碼,知道會最終會調用 Fetch接口,所以直接斷點調試即可,得出如下調用棧。接下來描述調用棧的主要流程。

調用棧主要流程:

captureMessage

function captureMessage(message, level) {

var syntheticException;

try{

thrownewError(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));

}

thrownewError("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 最終發送了請求

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發送請求

returnthis._buffer.add(global$2.fetch(this.url, defaultOptions).then(function(response) { return({

status: exports.Status.fromHttpCode(response.status),

}); }));

};

看完 Ajax上報 主線,再看本文的另外一條主線 window.onerror 捕獲。

window.onerror 和 window.onunhandledrejection 捕獲 錯誤

例子:調用一個未申明的變量。

func();

Promise 不捕獲錯誤

newPromise(() => {

fun();

})

.then(res => {

console.log('then');

})

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);

}

returnfalse;

};

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);

}

returnfalse;

};

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

最終同樣是調用了這個函數發送了請求。

可謂是殊途同歸,行文至此就基本已經結束,最後總結一下。

總結

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寫的源碼SyncPromise,也可以看打包後出來未壓縮的代碼。

讀源碼比較耗費時間,寫文章記錄下來更加費時間(比如寫這篇文章跨度十幾天...),但收穫一般都比較大。

如果讀者發現有不妥或可改善之處,再或者哪裡沒寫明白的地方,歡迎評論指出。另外覺得寫得不錯,對您有些許幫助,可以點讚、評論、轉發分享,也是對筆者的一種支持。萬分感謝。

推薦閱讀

知乎滴滴云:超詳細!搭建一個前端錯誤監控系統
掘金BlackHole1:JavaScript集成Sentry
丁香園 開源的 Sentry 小程序 SDKsentry-miniapp
sentry官網
sentry-javascript倉庫

❤️ 看完兩件事

如果你覺得這篇內容對你有所幫助,我想邀請你幫我兩個小忙:



推薦閱讀

Chrome 80發布,新特性將對用戶產生深遠影響

金三銀四?這20道高頻面試題值得了解下

淺談讓前端頭疼的性能監控~

來自騰訊CDC團隊的前端異常監控解決方案~

基於 React 的可視化編輯平臺實踐

頁面可視化配置搭建工具技術要點


相關焦點

  • 免費適用於個人開發者的異常監控服務:sentry
    免費適用於個人開發者的異常監控服務:sentry 隨著一些雲服務的發展與興起,應用部署及上線的成本越來越低,甚至可以實現零成本部署。本系列文章將介紹如何使用薅羊毛的方式來搭建各個項目。
  • 前端架構師是打雜的麼?前端架構師的核心工作是什麼?
    在今天之前, 我對前端架構的理解一直是廣義的, 即架構本身要解決的就是複雜性, 將複雜的東西簡化, 以便更好的維護, 前端也脫離不開這個範疇, 但是今天因為要寫轉正ppt在構思腦圖的時候, 我突然意識到前端架構其實是有更明確地含義, 並且在這些年, 我們這些在不同領域從事前端架構工作的架構師都有自己的一些理解
  • Vue/jQuery:讀懂前端源碼,工資少說加5K
    相信很多人都試圖去看過源碼,絕大多數是看不下去,沒有及時獲得正向反饋是一個原因,或者說,毫無章法的讀源碼使你並不清楚自己錯過了什麼
  • 手把手帶你入門前端工程化——超詳細教程
    在前端用得最多的就是單元測試(主要是端到端測試我用得很少,不熟),這裡著重講解一下。單元測試單元測試就是對一個函數、一個組件、一個類做的測試,它針對的粒度比較小。它應該怎麼寫呢?根據正確性寫測試,即正確的輸入應該有正常的結果。根據異常寫測試,即錯誤的輸入應該是錯誤的結果。
  • 閒聊前端監控系統
    搭建前端監控系統(二)JS錯誤監控篇如果你是一位前端工程師,那你一定不止一次去解決一些頑固的線上問題,你也曾想方設法復現用戶的bug,結果可能都不太理想。怎樣定位前端線上問題,一直以來,都是很頭疼的問題,因為它發生於用戶的一系列操作之後。
  • 2020年國內前端團隊都做了些什麼?
    前端應用監控體系逐漸完善和服務化前端監控包括行為監控、異常監控、性能監控等,為了在競爭日趨激烈的時存活下去, 企業不得不對應用做好全方面的分析, 保障應用的可監測和穩定性.一般而言,一個監控系統,大致可以分為四個階段:日誌採集、日誌存儲、統計與分析、報告和警告。
  • 「立體化監控告警平臺」-架構師之路年終總結
    第一篇,體系化立體化監控告警平臺的建設。畫外音:如果貴司監控告警平臺建設不夠完善,讀完這一篇一定會有收穫。點擊標題,立刻閱讀相關文章。《一分鐘搞定「http接口」監控框架》常見http監控玩法常見http監控的弊端通用可擴展http監控平臺架構簡版http監控框架架構100行偽代碼搞定http監控畫外音:如果沒有http接口監控,這一篇能夠快速搞一個簡版。
  • 學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理
    前言這是學習源碼整體架構系列第七篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。學習源碼整體架構系列文章如下:感興趣的讀者可以點擊閱讀。
  • 安卓系統開發之源碼架構簡介
    Android源碼就像一片大海,要去通讀源碼就好像要把海水舀幹,窮盡一生也難以完成。我們學習Android開發並不需要去了解每一個局部細節,是要在了解整體結構的基礎上,用到哪一塊就去閱讀哪一塊內容。逐步把每一塊內容從上層到底層串聯在一起,慢慢的就能窺見Android的全貌。
  • 學習 redux 源碼整體架構,深入理解 redux 及其中間件原理
    這是學習源碼整體架構系列第八篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。要是有人說到怎麼讀源碼,正在讀文章的你能推薦我的源碼系列文章,那真是太好了。
  • 利用教育源碼進行線上教育平臺開發要有具體計劃
    隨著線上教育越來越融入到我們的工作和生活中,讓我們的學習和生活也發生了大的變化,而且利用教育源碼進行線上教育平臺開發後,有越來越多的人選擇線上教育這種學習和培訓方式,但是我們知道,線上教育平臺開發的技術要求比較高,那麼如何進行開發呢?
  • 前端有必要去學Node.js嗎?這還用問?
    Node近兩年已經成為前端知識棧必備技能之一。
  • 一文徹底搞懂前端監控
    一、前端監控現狀近年來,前端監控是越來越火,目前已經有很多成熟的產品供我們選擇使用,如下圖所示有這麼多監控平臺,那為什麼還要學習自研前端監控?二、前端監控的目的 提升用戶體驗 更快的發現發現異常、定位異常、解決異常 了解業務數據,指導產品升級——數據驅動的思想三、前端監控的流程
  • 華為21級大佬深度解析MyBatis源碼架構設計原理,我闖進了京東~
    架構中需要變化的部分, 定要能夠非常清晰地被識別出來若架構中某部分發生變化,則該變化不會影響到其他部分。若架構中某部分需要擴展,則該擴展也不會影響到其他部分架構能做到關注點分離,才能做到真正意義上的解耦,這是架構師們需要努力實現的目標。如果大家要 問, 關注點分離做得最有效的落地實踐是什麼?我們首先能想到的就是「前後端分離」。
  • 架構師之路--談架構師的基本素養和日誌處理
    看過我前面博文的朋友可能還會記得我文章裡有自己隨手的塗鴉,還有不著調的舞蹈和唱歌視頻,歌詞都是自己編的。架構師最新作品:<母子飆歌嚇跑羊>[汗]。 博客頁首的橫幅是我本人的PS作品,裡面那個女孩子是P的自己[擦汗]。網頁三劍客我是用的很熟的。被大家吐槽的醜醜的博客頁面也是自己的前端作品[再次擦汗]。知識面決定一名架構師的能力和靈感。一名成功的架構師對知識儲備要廣。
  • 「前端+應用」兩大監控利器商業化首發 ARMS領跑APM市場
    首發商用的ARMS目前涵蓋應用監控和前端監控兩大功能。由此,ARMS的商業化正式填補了阿里雲在APM(Application Performance Management)領域空白。基於ARMS,用戶可以高效完成應用和前端的性能管理,可視化監控各項性能指標,並做出實時預警和監控。
  • Selenium3源碼之異常模塊篇
    Selenium3 Python3源碼分析系列以短文方式進行分享閱讀源碼是掌握Selenium的最好方式
  • 論前端技術和前端工程之辯
    參考在大學中學過的計算機科學與技術, 我相信對於前端技術而言, 主要包含的應該是這些所以從這個角度看, 你會發現你日常工作中 90% 以上的內容和前端技術毫不相關, 回想下自己今天的工作內容吧.毫無意義?
  • 架構師學習 java架構師學習需要具備哪些能力
    架構師學習 java架構師學習需要具備哪些能力2020/7/30 15:22:22 來源:法治中國 【字體:大 中 小】【收藏本頁】【列印】【關閉】核心提示:IT行業中沒有人對java不熟悉的,而java架構師是近年來很吃香的,想要進行架構師學習,需要專業的平臺進行系統性的學習才能掌握架構師必備的一些能力
  • 前端工程師的進階之路
    大家不妨試試去理解一下這幾個問題的根本點,試試從源碼裡來找到答案。理解源碼的同時,有利於對框架本身的理解,對於提升解決問題的能力,以及減少出現BUG的機率,有所幫助。合理的跨界,可以讓架構師對於業務的整體有深層次的認識,針對各種問題可以提出非前端之外的解決方案。嘗鮮技術是不斷發展的,作為一個架構師,不斷學習新的技術是非常重要的,這裡所說的嘗鮮,就是要對技術保持一定的熱情,不能只滿足於現狀,說白了講就是要不斷的學。