Solution:用户无感,数据排队处理

我们想象你的餐厅刚开门,后台 POS 系统还在启动,但门口已经有顾客进来点单了。服务员不能说"系统没好,这笔单不算"——他们先用手写单记录下来,等 POS 系统就绪后,再把手写单一笔一笔录进去。

那叠手写单就是代码里的"队列",就绪后的 POS 系统(实际的数据后端,比如 Datadog)就是"水槽(Sink)"。从代码上看,src/services/analytics/index.ts 是整个分析系统的入口,其他所有模块调用 logEvent() 来记录事件:

// src/services/analytics/index.ts
const eventQueue: QueuedEvent[] = []   // 手写单堆放处
let sink: AnalyticsSink | null = null  // POS 系统,启动时为空

export function logEvent(eventName: string, metadata: LogEventMetadata): void {
  if (sink === null) {
    eventQueue.push({ eventName, metadata, async: false })  // POS 没好:先写手写单
    return
  }
  sink.logEvent(eventName, metadata)  // POS 好了:直接录入
}

POS 就绪后,相当于服务员收到通知"可以开始录手写单了"——但不是立刻放下正在服务的顾客,而是等这一桌结完账再去:

// src/services/analytics/index.ts
export function attachAnalyticsSink(newSink: AnalyticsSink): void {
  if (sink !== null) return  // 幂等:POS 已经开了,重复通知不处理
  sink = newSink
  if (eventQueue.length > 0) {
    const queuedEvents = [...eventQueue]
    eventQueue.length = 0
    queueMicrotask(() => {        // 异步冲刷:等这轮忙完再录,不耽误正在进行的服务
      for (const event of queuedEvents) {
        sink!.logEvent(event.eventName, event.metadata)
      }
    })
  }
}

queueMicrotask 正是"等当前这轮任务做完,插到下一个空隙再处理"的机制。启动时每毫秒都很宝贵,不能因为冲刷历史事件而拖慢启动速度。