@sentry/browser
是所有前端监控的基类了
假如所有的 init 是在 Gobal 层面初始化一个单例的,带有全局作用域的 hub 对象
那么从代码角度来讲,withScope 本身他就是一个马上会执行传入函数的函数,只不过带了点作用域罢了
看了一些官方文档之后,对源码的理解也更深了
browser
和 node
在 init 时候,用的不同的实现,一个是 BrowserClient
一个是 NodeClient
而 core
里的 initAndBind
是一个完全抽象的 func
接受一个 function /class 和 option
if (options.debug === true) {
logger.enable();
}
const hub = getCurrentHub();
const client = new clientClass(options);
hub.bindClient(client);
getCurrentHub
:
export function getCurrentHub(): Hub {
// Get main carrier (global for every environment)
const 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());
}
// 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
实际上就是在全局挂载 __SENTRY__
的方法
export function getMainCarrier(): Carrier {
const carrier = getGlobalObject();
carrier.__SENTRY__ = carrier.__SENTRY__ || { //好奇这种写法会触发装饰器的 set 吗?
extensions: {},
hub: undefined,
};
return carrier;
}
然后最后在给 hub 赋值为 new Hub()
export function getHubFromCarrier(carrier: Carrier): 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;
}
其实这个逻辑很 js,很多场景都遇到过
比如我们插 mongodb ,没有那个 collection, 我们插会自己去 create 然后再插,本质上是一样的
Hun 可以理解成一个 Stack,初始化带个空对象,[{}]
还有一个 _lastEventId
首先默认构建一个全局的作用域 new Scope()
赋给这个空对象
作用域里面是用于检测的一大坨玩意
hub.bindClient(client);
就是给栈顶对象,绑定 client
这样空对象,又有 client 又有 scope 了
我们已经挂载了全局的对象,里面有 hub
logger
和 globalEventProcessors
我们手动记录错误的 api 做了啥呢?
看三个 api
from @sentry/minimal
这个包可以理解成一个共享包,其他包都用上了
从源码的角度看,里面就一个文件很少,而这里面几乎所有的方法都最终调用了 callOnHub
做了些啥?针对 hub 实例方法的一个封装罢了,实际上还是调用的 hub 上,对应 key 的 function,让我们回到 hub
三者在做了一些参数转化之后。全部去 this._invokeClient
, 这个方法就去取 栈顶对象的 scope, client
然后
private _invokeClient<M extends keyof Client>(method: M, ...args: any[]): void {
const { scope, client } = this.getStackTop();
if (client && client[method]) {
(client as any)[method](...args, scope);
}
}
所以,最终又回到了 Client 那里
找不到这几个方法,向上找 BaseClient
和 BrowserBackend
public captureException(exception: any, hint?: EventHint, scope?: Scope): string | undefined {
let eventId: string | undefined = hint && hint.event_id;
this._process(
this._getBackend()
.eventFromException(exception, hint)
.then(event => this._captureEvent(event, hint, scope))
.then(result => {
eventId = result;
}),
);
return eventId;
}
同理可得 _getBackend
拿到的是 BrowserBackend
this._process
是一个 微任务处理 + 计数,一看代码就知道
protected _process<T>(promise: PromiseLike<T>): void {
this._processing += 1;
promise.then(
value => {
this._processing -= 1;
return value;
},
reason => {
this._processing -= 1;
return reason;
},
);
}
接下来就是 最重要的 _processEvent
就是它把事件和信息,送给了 Sentry
我们先只看 this._sendEvent
其他还有很多的概念,比如 session
transaction
以后再讲
BaseBackend
public sendEvent(event: Event): void {
this._transport.sendEvent(event).then(null, reason => {
logger.error(`Error while sending event: ${reason}`);
});
}
终于,快到那里了,先看看 _transport
构造器里初始化了一个 transports/noop
看来这个也是根据,不同平台实现的
回到 @sentry/browser
//backend.ts
protected _setupTransport(): Transport {
if (!this._options.dsn) {
// We return the noop transport here in case there is no Dsn.
return super._setupTransport();
}
const transportOptions = {
...this._options.transportOptions,
dsn: this._options.dsn,
_metadata: this._options._metadata,
};
if (this._options.transport) {
return new this._options.transport(transportOptions);
}
if (supportsFetch()) {
return new FetchTransport(transportOptions);
}
return new XHRTransport(transportOptions);
}
一下子就看懂了,又可以自定义,支持 fetch 就 fetch ,最差还有 xhr
browser/transports
更加懂了
梳理一下很有收获,得画一个目录思维导图