源码共读10:学习 sentry 源码整体架构

208 阅读2分钟

sentry-javascript Github

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是学习源码整体架构系列第四篇,链接: 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 错误监控类

前端错误分类

  1. 即使运行错误:代码错误
  • try...cache...
  • window.error(也可以用DOM2事件监听)
  1. 资源加载错误
  • object.oneror: dom对象的onerror事件
  • performance.getEntries()
  • Error事件捕获
  1. 使用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) // 这里只有捕获才能触发事件,冒泡不能触发

上报错误的基本原理

  1. 通过Ajax通讯的方式上报
  2. 利用Image对象上报(主流方式)

Sentry前端异常监控基本原理

  1. 重写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) {}
}
  1. 采用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.onerrorwindow.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));
  
}

总结:

whiteboard_exported_image (2).png

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源码共读,每一次脑海里闪过努力的念头,都是未来的你在向你求救。