npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gomain-fe/monitor-core

v0.1.4

Published

Runtime-agnostic core for @gomain-fe/monitor-web

Readme

SDK Core 工作流程

monitor-core 提供运行时无关的实例工厂 createMonitor(config),管理插件生命周期、事件队列、去重、调度与发送。不绑定浏览器 API,不感知 transport 自激防护等 Web 端关注点。

整体生命周期

createMonitor → use(plugin)* → start → capture/flush → stop

1. 初始化 createMonitor(config)

createMonitor({ appId, dsn, transport, context, release, limits, ... })
    │
    ├─ 校验: appId 必填; dsn|transport 至少一个 (违反时同步抛错)
    ├─ 合并 limits: defaultLimits { maxQueueSize: 50 } ← config.limits
    ├─ 初始化内部状态:
    │     ├─ plugins: MonitorPlugin[]
    │     ├─ pluginTeardowns: Array<() => void>
    │     ├─ queue: Array<{ event, key }>   ← 事件与其 dedup key 配对
    │     ├─ queuedKeys: Set<string>         ← O(1) 去重查询
    │     ├─ draining: boolean              ← drain 重入锁
    │     ├─ idleDrainHandle: number | undefined
    │     └─ started: boolean
    └─ 返回 monitor 实例 (未启动状态)

2. 启动 monitor.start()

monitor.start()
    │
    ├─ 已 started → 直接 return (重复启动安全)
    ├─ started = true
    └─ 遍历 plugins[], 调用 setupPlugin(plugin)
         ├─ plugin.setup(monitor)
         │     ├─ 返回 function → push 进 pluginTeardowns
         │     └─ 返回 void     → 跳过
         └─ setup 抛错 → reportSdkError(stage: 'plugin', pluginName)
                          (异常被吞,不影响其他插件)

monitor.use(plugin) 在 start 之前调用时只入列;在 start 之后调用立即 setup。

3. 事件采集 monitor.capture(event)

capture(event)
    │
    ├─ [门] !started → 静默丢弃
    ├─ [指纹] key = stringifyQueueEvent(event)
    │         字段: type/message/url/tags/extra
    │         key 只在入队这一次计算,后续直接随 { event, key } 配对存取
    ├─ [去重] queuedKeys.has(key) → 静默丢弃
    ├─ [溢出] queue.length >= maxQueueSize
    │         → queue.shift() (丢最旧)
    │         → queuedKeys.delete(被丢事件的 key)  ← key 同步释放,允许重新入队
    ├─ 入队: queue.push({ event, key }) + queuedKeys.add(key)
    └─ scheduleIdleDrain()  ← 异步触发一次 drain

4. 队列消费 drainQueue()

drainQueue()
    │
    ├─ [锁] draining 为 true → return  (避免并发重入)
    ├─ cancelIdleDrain()              (撤销待触发的 idle handle)
    ├─ draining = true
    ├─ try:
    │     ├─ batch = queue.splice(0)      ← 一次取出当前全部事件
    │     ├─ batch 为空 → return
    │     ├─ 释放 dedup keys: batch.forEach(item => queuedKeys.delete(item.key))
    │     │
    │     ├─ [context] 解析 config.context
    │     │     ├─ function → await it()  (支持同步 / 异步)
    │     │     ├─ object   → 直接使用
    │     │     └─ 抛错     → reportSdkError(stage: 'context')
    │     │                    context 保持 undefined,事件继续发送
    │     │
    │     ├─ payloads = batch.map(({ event }) => ({
    │     │     ...event,
    │     │     timestamp: event.timestamp ?? Date.now(),
    │     │     appId, release, context
    │     │   }))
    │     │
    │     └─ try await (config.transport ?? defaultTransport)(payloads)
    │           catch error → console.error,不重入队列
    │                          (避免 transport 故障走同一通道形成风暴)
    └─ finally: draining = false

关于"sdk-error 风暴"的处理

context 解析失败时直接 reportSdkError,不再做 events[0].type !== 'sdk-error' 之类的特判。原因:

  • reportSdkError 本身用 monitor.capture 入队,会经过 dedup 过滤
  • 同样的 context 失败产生的 sdk-error 事件指纹相同,自然被合并
  • 即便首批已 splice,新 sdk-error 进入下一轮 drain;若再次失败仍产生相同指纹 → 仍被 dedup,不会无限放大

5. 默认 transport defaultTransport

仅在用户未传 config.transport 时生效。流程:

defaultTransport(payloads)
    │
    ├─ !config.dsn → return            (createMonitor 已校验,理论不达)
    ├─ navigator.sendBeacon 可用?
    │     ├─ sendBeacon(dsn, JSON) === true → 成功,return
    │     └─ false                           → 继续 fetch 兜底
    └─ await fetch(dsn, { method: POST, body: JSON, ... })
  • sendBeacon 不经过 fetch 补丁,默认路径天然不会触发请求 AOP 自激
  • fetch 兜底由 monitor-web 的 isMonitorDsn 自排除处理(详见 monitor-web/src/error/request-error-handling.md)

6. 调度 scheduleIdleDrain / cancelIdleDrain

scheduleIdleDrain()
    │
    ├─ 已有 idleDrainHandle → return (合并多次 capture 的触发)
    ├─ requestIdleCallback 可用?
    │     ├─ 是 → idleDrainHandle = requestIdleCallback(drainQueue)
    │     └─ 否 → idleDrainHandle = setTimeout(drainQueue, 16)
    └─ 真正执行时清空 handle,然后调 drainQueue

flush() 直接调用 drainQueue()(同步触发并 await 返回),不依赖 idle 调度。

7. 停止 monitor.stop()

monitor.stop()
    │
    ├─ !started → return
    ├─ started = false
    └─ pluginTeardowns.splice(0).reverse().forEach(task => task())
         (LIFO 顺序:后注册的先清理)

stop 不主动 drain;若有待发送事件,需用户在 stop 前 await monitor.flush()(monitor-web 已在 pagehide/beforeunload 自动 flush)。

8. toErrorEvent(type, error, meta?) 归一化

把任意 unknown 错误值归一化为 MonitorEvent:

| 输入 | message | |---|---| | Error | error.message | | string | 原字符串 | | undefined | 'undefined' | | 其他对象 | JSON.stringify(error)(循环引用退化为 String(error)) |

capture 入参仅传 error: Error,由 normalizeMonitorEvent 解析为 message 后剔除 error。非 Error 可用 toMonitorError / toErrorEvent 转换。

meta.tags / meta.extra 透传到事件字段。

关键设计点

  • dedup key 只算一次:入队、溢出、drain 都从 { event, key } 配对里直接读 key,不重复 JSON.stringify
  • drain 重入锁:draining 标志保证同一时刻只有一个 drain 在跑;flush() 与 idle 触发不会竞争
  • transport 失败不重入队:走 console.error 提示,避免 SDK 自身故障变成事件风暴
  • SDK 自身异常:所有 setup/context 抛错统一走 reportSdkError,以 sdk-error 事件正常进入队列与 dedup
  • 运行时无关:不引用 window / DOM / fetch 补丁等;Web 特有的关注点(pagehide flush、transport 自激防护、resource error 等)由 monitor-web 负责