學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDK

前言

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

學習源碼總體架構系列文章以下:html

1.學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫
2.學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫
3.學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫
4.學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDK
5.學習 vuex 源碼總體架構,打造屬於本身的狀態管理庫
6.學習 axios 源碼總體架構,打造屬於本身的請求庫
7.學習 koa 源碼的總體架構,淺析koa洋蔥模型原理和co原理
8.學習 redux 源碼總體架構,深刻理解 redux 及其中間件原理前端

感興趣的讀者能夠點擊閱讀。vue

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

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

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

本文示例等源代碼在這個人github博客中github blog sentry,須要的讀者能夠點擊查看,若是以爲不錯,能夠順便star一下。ios

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

前端錯誤監控知識

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

前端錯誤的分類

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對比便可找出圖片資源未加載項目

Error事件捕獲代碼示例

window.addEventListener('error', function(e) {
 console.log('捕獲', e) }, true) // 這裏只有捕獲才能觸發事件,冒泡是不能觸發 複製代碼

上報錯誤的基本原理

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

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

Image上報錯誤方式: (new Image()).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:發生錯誤的列號(數字)
errorError對象(對象)

MDN unhandledrejection

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

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.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); } 複製代碼

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

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 構造函數

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以前,先提一下繼承、繼承靜態屬性和方法。

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

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

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

// 繼承靜態方法和屬性
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 構造函數

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

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 構造函數。

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) {
 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@若川

initAndBind 函數之 getCurrentHub().bindClient()

繼續看 initAndBind 的另外一條線。

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

獲取當前的控制中心 Hub,再把new BrowserClient() 的實例對象綁定在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, 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); } 複製代碼

衍生的函數 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; } 複製代碼

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]; }; 複製代碼

小結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, 若川!');
複製代碼

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

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 捕獲。

window.onerror 和 window.onunhandledrejection 捕獲 錯誤

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

func();
複製代碼

Promise 不捕獲錯誤

new Promise(() => {
 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);  }  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

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

可謂是異曲同工,行文至此就基本已經結束,最後總結一下。

總結

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倉庫

筆者往期文章

面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符
前端使用puppeteer 爬蟲生成《React.js 小書》PDF併合並

關於

做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客-若川,使用vuepress重構了,閱讀體驗可能更好些
掘金專欄,歡迎關注~
segmentfault前端視野專欄,歡迎關注~
知乎前端視野專欄,歡迎關注~
github blog,相關源碼和資源都放在這裏,求個star^_^~

歡迎加微信交流 微信公衆號

可能比較有趣的微信公衆號,長按掃碼關注。歡迎加筆者微信ruochuan12(註明來源,基原本者不拒),拉您進【前端視野交流羣】,長期交流學習~

若川視野
若川視野

本文使用 mdnice 排版

相關文章
相關標籤/搜索