此页面需要javascript支持,请在浏览器中启用javascript

sentry前端监控for browser(1)

性能
Performance
js
browser
sentry
共1027个字,阅读时间 5 分钟
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://icebreaker.top/articles/2021/1/24-sentry-browser-1

基类包

@sentry/browser 是所有前端监控的基类了

假如所有的 init 是在 Gobal 层面初始化一个单例的,带有全局作用域的 hub 对象

What's a Scope, what's a Hub

那么从代码角度来讲,withScope 本身他就是一个马上会执行传入函数的函数,只不过带了点作用域罢了

看了一些官方文档之后,对源码的理解也更深了

init 做了些啥

browsernode 在 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 然后再插,本质上是一样的

Hub

Hun 可以理解成一个 Stack,初始化带个空对象,[{}] 还有一个 _lastEventId

首先默认构建一个全局的作用域 new Scope() 赋给这个空对象
作用域里面是用于检测的一大坨玩意

hub.bindClient(client);就是给栈顶对象,绑定 client

这样空对象,又有 client 又有 scope 了

初始化完成

我们已经挂载了全局的对象,里面有 hub loggerglobalEventProcessors

我们手动记录错误的 api 做了啥呢?
看三个 api

  • captureException
  • captureEvent
  • captureMessage

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 那里

BrowserClient

找不到这几个方法,向上找 BaseClientBrowserBackend

BaseClient

  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

看来这个也是根据,不同平台实现的

transport

回到 @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

XHRTransport

browser/transports

更加懂了

梳理一下很有收获,得画一个目录思维导图