@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 → stop1. 初始化 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() ← 异步触发一次 drain4. 队列消费 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,然后调 drainQueueflush() 直接调用 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 特有的关注点(pagehideflush、transport自激防护、resource error 等)由monitor-web负责
