@clipto/reporter 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/id.ts","../src/builtins.ts","../src/reporter.ts"],"sourcesContent":["// ============================================================\n// 通用数据上报 SDK —— 类型接口 + 核心实现与内置适配器\n// 定位:替代 renderer 现有 event-tracker-sdk,平台无关、去硬编码。\n// 浏览器端与 Node 端是强需求,双端共用同一核心(入队、攒批、重试、有界队列),\n// 差异仅在发送与持久化:可注入自定义实现,不注入时按 platform 选项\n// (缺省自动探测 typeof window)使用内置实现(实现见 reporter.ts / builtins.ts):\n// 浏览器: sendBeacon(退出兜底)→ fetch(keepalive)+ Dexie/IndexedDB(复用\n// 项目现有依赖,默认开启,不支持时降级 localStorage;写入节流,\n// 避免每次入队全量序列化阻塞主线程);\n// Node: axios 请求 + conf 持久化(electron-store 的纯 Node 内核,Electron\n// main 与纯 Node 服务端通吃,persistDir 配置后启用,未配置仅内存;\n// JSON 文件 + 内部原子写)。\n// ============================================================\n\n/** 事件被丢弃的原因 */\nexport type DropReason = 'max-attempts' | 'overflow' | 'invalid';\n\n/** 上报事件(track 入队后的形态;顶层字段对齐旧 SDK TrackedEvent) */\nexport interface IReportEvent {\n /** 核心生成的唯一 id */\n id: string;\n /** 事件名(上报信封中序列化为 eventName,对齐旧 SDK) */\n name: string;\n /** 业务属性(baseProperties 与 context 已合并) */\n properties: Record<string, unknown>;\n /** 事件时间(ms) */\n timestamp: number;\n /** 已尝试发送次数(SDK 内部字段,不上报) */\n attempts: number;\n /** 会话 id(enrich 提供,否则核心按实例生成) */\n sessionId?: string;\n /** 用户 id(enrich 提供,否则 null) */\n userId?: string | null;\n /** 设备 id(enrich 提供,否则核心按实例生成) */\n deviceId?: string;\n /** 事件类别(enrich / track options 提供,默认 'custom') */\n category?: string;\n /** 采集上下文:页面 / 用户 / 浏览器 / 屏幕 / 视口(enrich 提供,否则空对象) */\n context?: Record<string, unknown>;\n}\n\n/** 发送适配器:如何把一批事件发出去(浏览器: sendBeacon → fetch;Node: https / axios) */\nexport interface IReportTransport {\n /** 发送一批事件;resolve 视为成功,reject 进入重试 */\n send(events: IReportEvent[]): Promise<void>;\n /** 退出兜底时的同步尽力发送(浏览器端为 navigator.sendBeacon);返回是否已受理 */\n sendSync?(events: IReportEvent[]): boolean;\n}\n\n/** 持久化适配器:待发队列跨页面刷新 / 进程重启存活(浏览器 IndexedDB / Node conf) */\nexport interface IReportStorage {\n /** 整体覆盖保存当前待发队列(实现需幂等;写入节流由核心负责) */\n save(events: IReportEvent[]): void | Promise<void>;\n /** 读取上次保存的待发队列 */\n load(): IReportEvent[] | Promise<IReportEvent[]>;\n /** 清空 */\n clear(): void | Promise<void>;\n}\n\n/** 构造配置 */\nexport interface IReporterOptions {\n /** 上报端点,必填(浏览器与 Node 共用) */\n endpoint: string;\n /** 运行平台:显式声明,驱动内置发送 / 持久化实现;缺省自动探测(typeof window) */\n platform?: 'browser' | 'node';\n /** 攒批窗口(ms),即延迟上报的固定间隔;0 表示关闭批量,事件即时发送 */\n flushInterval?: number; // 默认 2000(对齐旧 SDK batchTimeout)\n /** 单批最大条数,达到即刷 */\n maxBatchSize?: number; // 默认 10(对齐旧 SDK)\n /** 单批最大字节数(JSON 序列化估算),达到即刷 */\n maxBatchBytes?: number; // 默认 64 * 1024\n /** 队列最大条数,防止内存无限增长 */\n maxQueueSize?: number; // 默认 1000\n /** 队列溢出处理 */\n overflowPolicy?: 'drop-oldest' | 'drop-newest' | 'flush-now'; // 默认 'drop-oldest'\n /** 单个事件最大发送尝试次数,超限丢弃 */\n maxAttempts?: number; // 默认 3(对齐旧 SDK)\n /** 失败重发退避基础延迟(ms),按 2^n 指数增长 */\n backoffMs?: number; // 默认 1000(对齐旧 SDK retryDelay)\n /** 合并进每个事件 properties 的公共业务字段(替代旧 SDK base_properties) */\n baseProperties?: Record<string, unknown>;\n /** 上下文采集函数,每次入队时实时调用并合并进 properties(替代旧 SDK 硬编码的页面/浏览器/设备信息) */\n context?: () => Record<string, unknown>;\n /** 逐事件顶层字段补充函数(sessionId/userId/deviceId/context),每次入队时实时调用,缺省值由核心兜底 */\n enrich?: () => Partial<\n Pick<IReportEvent, 'sessionId' | 'userId' | 'deviceId' | 'context'>\n >;\n /** 页面卸载 / 进程退出时兜底 flush(浏览器端走 sendBeacon) */\n flushOnExit?: boolean; // 默认 true\n /** 浏览器端:队列是否持久化到本地(优先 IndexedDB,不支持时降级 localStorage) */\n persist?: boolean; // 默认 true(对齐旧 SDK 兜底行为)\n /** Node 端:持久化目录,设置后待发队列落盘(跨进程重启存活);不传仅内存 */\n persistDir?: string;\n /** 浏览器端:自动上报 page_view(初始化 + 页面重新可见) */\n autoPageView?: boolean; // 默认 false(对齐旧 SDK)\n /** 自定义请求头(内置传输实现使用) */\n headers?: Record<string, string>;\n /** 打开调试日志 */\n debug?: boolean; // 默认 false\n\n /** 可选注入:发送适配器;不传按运行环境内置 */\n transport?: IReportTransport;\n /** 可选注入:持久化适配器;不传按运行环境内置(浏览器 Dexie→localStorage 降级 / Node conf,未配 persistDir 仅内存) */\n storage?: IReportStorage;\n\n /** 一批发送成功且已从队列移除 */\n onFlushed?(events: IReportEvent[]): void;\n /** 一批发送失败(将按策略重试或丢弃) */\n onFailed?(events: IReportEvent[], error: unknown): void;\n /** 事件被丢弃(重试耗尽 / 溢出 / 非法) */\n onDropped?(events: IReportEvent[], reason: DropReason): void;\n}\n\n/** 公共接口 */\nexport interface IReporter {\n /** 上报一条事件并返回事件 id(dispose 后或非法事件返回空串);immediate 事件跳过攒批立即发送 */\n track(\n name: string,\n properties?: Record<string, unknown>,\n options?: {\n immediate?: boolean;\n timestamp?: number;\n /** 事件类别(顶层字段,默认 'custom') */\n category?: string;\n },\n ): string;\n /** 立即冲刷队列(跳过退避等待),并等待在途批次完成 */\n flush(): Promise<void>;\n /** 队列中待发条数 */\n readonly size: number;\n /** 停止并兜底 flush(flushOnExit),之后拒绝新事件 */\n dispose(): Promise<void>;\n}\n\n/** 实现类的构造签名:new Reporter(options) 得到 IReporter */\nexport interface IReporterConstructor {\n new (options: IReporterOptions): IReporter;\n}\n\n// ============================================================\n// 实现与内置适配器\n// ============================================================\nexport { Reporter } from './reporter';\nexport {\n createBrowserStorage,\n createBrowserTransport,\n createLocalStorageStorage,\n createNodeStorage,\n createNodeTransport,\n} from './builtins';\n","/**\n * 事件 / 批次 id:对齐旧 SDK generateEventId 格式(evt_时间戳_随机串)\n */\nexport function generateEventId(): string {\n return `evt_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;\n}\n","// ============================================================\n// 内置 transport / storage —— 复用现有组件,不新造轮子:\n// 浏览器发送: 原生 fetch(keepalive)+ navigator.sendBeacon(退出兜底)\n// Node 发送: axios(项目现有依赖,动态加载,不进 renderer 包)\n// 浏览器持久化: Dexie(项目现有依赖,与 @renderer/common/idb 同款;\n// IndexedDB 不可用时降级 localStorage)\n// Node 持久化: conf(electron-store 的纯 Node 内核,Electron main 与\n// 纯 Node 服务端通吃,JSON 文件 + 内部原子写),整体覆盖保存\n// payload 沿用旧 SDK 的 { events, metadata } 信封,便于后端平滑过渡\n// ============================================================\nimport type { IReportEvent, IReportStorage, IReportTransport } from './index';\nimport { generateEventId } from './id';\n\nconst SDK_VERSION = '1.0.0'; // 对齐旧 SDK sdkVersion,便于后端平滑过渡\nconst DB_NAME = 'CliptoReportDB';\nconst STORE_NAME = 'clipto-reporting';\nconst QUEUE_KEY = 'pending';\nconst LS_KEY = 'clipto_report_pending_queue';\n\n/** 上报信封事件:顶层字段与旧 SDK TrackedEvent 完全对齐(name 序列化为 eventName) */\ninterface IEventPayloadEvent {\n id: string;\n eventName: string;\n timestamp: number;\n sessionId?: string;\n userId?: string | null;\n deviceId?: string;\n properties: Record<string, unknown>;\n category?: string;\n context?: Record<string, unknown>;\n}\n\n/** 上报信封:对齐旧 SDK { events, metadata } 结构 */\ninterface IEventPayload {\n events: IEventPayloadEvent[];\n metadata: {\n sdkVersion: string;\n timestamp: number;\n batchId: string;\n };\n}\n\nfunction buildPayload(events: IReportEvent[]): IEventPayload {\n return {\n // attempts 为 SDK 内部字段,不上报;name 输出为 eventName(对齐旧 SDK)\n events: events.map(\n ({\n id,\n name,\n timestamp,\n sessionId,\n userId,\n deviceId,\n properties,\n category,\n context,\n }) => ({\n id,\n eventName: name,\n timestamp,\n sessionId,\n userId,\n deviceId,\n properties,\n category,\n context,\n }),\n ),\n metadata: {\n sdkVersion: SDK_VERSION,\n timestamp: Date.now(),\n batchId: generateEventId(), // 与事件 id 同格式(对齐旧 SDK)\n },\n };\n}\n\n/** 浏览器发送:fetch(keepalive)走正常路径;sendBeacon 仅退出兜底(sendSync) */\nexport function createBrowserTransport(\n endpoint: string,\n headers?: Record<string, string>,\n): IReportTransport {\n const commonHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...(headers ?? {}),\n };\n return {\n async send(events) {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: commonHeaders,\n body: JSON.stringify(buildPayload(events)),\n keepalive: true,\n });\n if (!response.ok) {\n throw new Error(`report send failed: HTTP ${response.status}`);\n }\n },\n sendSync(events) {\n if (\n typeof navigator === 'undefined' ||\n typeof navigator.sendBeacon !== 'function'\n ) {\n return false;\n }\n try {\n // sendBeacon 不支持自定义请求头,与旧 SDK 行为一致\n const blob = new Blob([JSON.stringify(buildPayload(events))], {\n type: 'application/json',\n });\n return navigator.sendBeacon(endpoint, blob);\n } catch {\n return false;\n }\n },\n };\n}\n\n/** Node 发送:动态加载项目现有依赖 axios(仅在 Node 路径执行) */\nexport async function createNodeTransport(\n endpoint: string,\n headers?: Record<string, string>,\n): Promise<IReportTransport> {\n const { default: axios } = await import('axios');\n return {\n async send(events) {\n await axios.post(endpoint, buildPayload(events), {\n headers,\n timeout: 10000,\n });\n },\n };\n}\n\n/** 降级实现:localStorage(容量有限,仅兜底) */\nexport function createLocalStorageStorage(): IReportStorage {\n return {\n save(events) {\n try {\n localStorage.setItem(LS_KEY, JSON.stringify(events));\n } catch {\n // 配额满或不可用:静默降级为不持久化\n }\n },\n load() {\n try {\n const text = localStorage.getItem(LS_KEY);\n return text ? (JSON.parse(text) as IReportEvent[]) : [];\n } catch {\n return [];\n }\n },\n clear() {\n try {\n localStorage.removeItem(LS_KEY);\n } catch {\n // ignore\n }\n },\n };\n}\n\n/** 浏览器持久化:优先 Dexie(IndexedDB),不可用时降级 localStorage */\nexport async function createBrowserStorage(): Promise<IReportStorage> {\n try {\n // 动态加载项目现有依赖 Dexie:仅在浏览器路径执行,不进 main 进程\n const { default: Dexie } = await import('dexie');\n const db = new Dexie(DB_NAME);\n db.version(1).stores({ queue: 'key' });\n await db.open(); // 提前探活,失败走 localStorage 降级\n\n const table = db.table<{ key: string; events: IReportEvent[] }, string>(\n 'queue',\n );\n return {\n async save(events) {\n await table.put({ key: QUEUE_KEY, events });\n },\n async load() {\n const row = await table.get(QUEUE_KEY);\n return row ? row.events : [];\n },\n async clear() {\n await table.delete(QUEUE_KEY);\n },\n };\n } catch {\n // IndexedDB 不可用(隐私模式 / 极老浏览器):降级 localStorage\n return createLocalStorageStorage();\n }\n}\n\n/** Node 持久化:conf(纯 Node 可用,JSON 文件 + 内部原子写),整体覆盖保存 */\nexport async function createNodeStorage(\n persistDir: string,\n): Promise<IReportStorage> {\n // 动态加载:仅在 Node 路径执行,renderer 永不进入该分支。\n // webpackIgnore:让打包器跳过 conf(依赖 Node 内置模块,web 目标无法打包),\n // 运行时由 Node 直接解析;SDK 作为 npm 包发布时 conf 随依赖安装。\n // 注意:打包后的 main 进程若启用该分支,需将 conf 加入\n // release/app/package.json dependencies(同 better-sqlite3 等原生依赖模式)。\n const { default: Conf } = await import(/* webpackIgnore: true */ 'conf');\n const store = new Conf<Record<string, IReportEvent[]>>({\n configName: STORE_NAME, // 落盘为 persistDir/clipto-reporting.json\n cwd: persistDir,\n });\n return {\n save(events) {\n store.set(QUEUE_KEY, events);\n },\n load() {\n return store.get(QUEUE_KEY) ?? [];\n },\n clear() {\n store.delete(QUEUE_KEY);\n },\n };\n}\n","// ============================================================\n// Reporter 核心实现 —— 平台无关,差异通过 transport / storage 注入\n// 复用现有组件:setTimeout(攒批 / 退避定时);事件 id 对齐旧 SDK\n// generateEventId 格式(见 id.ts);\n// 入队、攒批、重试、有界队列即 SDK 本身,项目内 better-queue / p-queue\n// 面向任务队列语义,与攒批上报不匹配,故核心手写保持精简。\n// ============================================================\nimport type {\n IReportEvent,\n IReporter,\n IReporterOptions,\n IReportStorage,\n IReportTransport,\n} from './index';\nimport {\n createBrowserStorage,\n createBrowserTransport,\n createNodeStorage,\n createNodeTransport,\n} from './builtins';\nimport { generateEventId } from './id';\n\nconst DEFAULT_FLUSH_INTERVAL = 2000; // 攒批窗口(ms),即延迟上报间隔\nconst DEFAULT_MAX_BATCH_SIZE = 10; // 单批最大条数,达到即刷\nconst DEFAULT_MAX_BATCH_BYTES = 64 * 1024; // 单批最大字节数,达到即刷\nconst DEFAULT_MAX_QUEUE_SIZE = 1000; // 队列最大条数\nconst DEFAULT_MAX_ATTEMPTS = 3; // 单事件最大发送尝试次数,超限丢弃\nconst DEFAULT_BACKOFF_MS = 1000; // 重试退避基数,按 2^n 指数增长\nconst STORAGE_WRITE_THROTTLE_MS = 500; // 持久化写入节流窗口\n\n/** Node 进程退出信号:兼容 Electron 渲染进程与 @types/node 对 process 的 typings 差异 */\ntype ExitSignal = 'beforeExit' | 'SIGINT' | 'SIGTERM';\n\ntype ExitProcessHandle = {\n once(event: ExitSignal, listener: () => void): unknown;\n off(event: ExitSignal, listener: () => void): unknown;\n};\n\nexport class Reporter implements IReporter {\n private readonly opts: IReporterOptions;\n\n private readonly platform: 'browser' | 'node';\n\n private readonly flushInterval: number;\n\n private readonly maxBatchSize: number;\n\n private readonly maxBatchBytes: number;\n\n private readonly maxQueueSize: number;\n\n private readonly overflowPolicy: NonNullable<\n IReporterOptions['overflowPolicy']\n >;\n\n private readonly maxAttempts: number;\n\n private readonly backoffMs: number;\n\n private readonly flushOnExit: boolean;\n\n private readonly transportPromise: Promise<IReportTransport>;\n\n private readonly storagePromise: Promise<IReportStorage | undefined>;\n\n /** 会话 / 设备 id 兜底:enrich 未提供时按实例生成(对齐旧 SDK 格式) */\n private readonly sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;\n\n private readonly deviceId = `device_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;\n\n /** 待发队列(内存中的事实源,持久化是它的影子) */\n private readonly queue: IReportEvent[] = [];\n\n /** init 完成后指向内置 / 注入的持久化实现 */\n private storage: IReportStorage | undefined;\n\n private flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n\n private persistTimer: ReturnType<typeof setTimeout> | null = null;\n\n private persistDirty = false;\n\n private flushPromise: Promise<void> | null = null;\n\n private disposed = false;\n\n private readonly onExit = (): void => {\n this.fire(this.dispose());\n };\n\n private readonly onPageHide = (): void => {\n this.fire(this.dispose());\n };\n\n private readonly onVisibilityChange = (): void => {\n if (document.visibilityState === 'hidden') {\n // 页面切后台:冲刷一次(非卸载,保持接收新事件)\n this.fire(this.flush());\n } else if (this.opts.autoPageView) {\n this.track('page_view');\n }\n };\n\n constructor(options: IReporterOptions) {\n if (!options.endpoint) {\n throw new Error('Reporter: endpoint 为必填项');\n }\n this.opts = options;\n this.platform =\n options.platform ?? (typeof window !== 'undefined' ? 'browser' : 'node');\n this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;\n this.maxBatchBytes = options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES;\n this.maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;\n this.overflowPolicy = options.overflowPolicy ?? 'drop-oldest';\n this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n this.backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;\n this.flushOnExit = options.flushOnExit ?? true;\n\n // 注入优先;未注入按平台内置(复用现有组件,见 builtins.ts)\n if (options.transport) {\n this.transportPromise = Promise.resolve(options.transport);\n } else if (this.platform === 'browser') {\n this.transportPromise = Promise.resolve(\n createBrowserTransport(options.endpoint, options.headers),\n );\n } else {\n this.transportPromise = createNodeTransport(\n options.endpoint,\n options.headers,\n );\n }\n\n if (options.storage) {\n this.storagePromise = Promise.resolve(options.storage);\n } else if (options.persist === false) {\n this.storagePromise = Promise.resolve(undefined);\n } else if (this.platform === 'browser') {\n this.storagePromise = createBrowserStorage(); // Dexie → localStorage 降级\n } else if (options.persistDir) {\n this.storagePromise = createNodeStorage(options.persistDir);\n } else {\n this.storagePromise = Promise.resolve(undefined); // Node 未配置 persistDir:仅内存\n }\n\n this.fire(this.init());\n }\n\n track(\n name: string,\n properties?: Record<string, unknown>,\n options?: { immediate?: boolean; timestamp?: number; category?: string },\n ): string {\n if (this.disposed) {\n this.log('dispose 后忽略新事件:', name);\n return '';\n }\n\n // 顶层字段:enrich 实时补充,缺省由核心兜底(对齐旧 SDK TrackedEvent)\n const enriched = this.opts.enrich?.() ?? {};\n const event: IReportEvent = {\n id: generateEventId(),\n name,\n properties: {\n ...(this.opts.baseProperties ?? {}),\n ...(this.opts.context?.() ?? {}),\n ...(properties ?? {}),\n },\n timestamp: options?.timestamp ?? Date.now(),\n attempts: 0,\n sessionId: enriched.sessionId ?? this.sessionId,\n userId: enriched.userId ?? null,\n deviceId: enriched.deviceId ?? this.deviceId,\n category: options?.category ?? 'custom',\n context: enriched.context ?? {},\n };\n\n if (typeof name !== 'string' || name.length === 0) {\n this.opts.onDropped?.([event], 'invalid');\n return '';\n }\n\n if (options?.immediate) {\n // 立即发送:跳过攒批,失败仍走重试(重新入队)\n this.fire(this.sendBatch([event]));\n return event.id;\n }\n\n this.enqueue(event);\n if (this.flushInterval <= 0) {\n // 关闭攒批:事件即时发送\n this.fire(this.flush());\n } else {\n this.scheduleFlush();\n }\n return event.id;\n }\n\n flush(): Promise<void> {\n if (!this.flushPromise) {\n // 显式冲刷:跳过退避立即重试(退避只约束自动重试)\n if (this.retryTimer) {\n clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n this.flushPromise = this.drain().finally(() => {\n this.flushPromise = null;\n });\n }\n return this.flushPromise;\n }\n\n get size(): number {\n return this.queue.length;\n }\n\n async dispose(): Promise<void> {\n if (this.disposed) return;\n this.disposed = true;\n\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = null;\n }\n if (this.retryTimer) {\n clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n if (this.persistTimer) {\n clearTimeout(this.persistTimer);\n this.persistTimer = null;\n }\n\n if (this.platform === 'browser') {\n window.removeEventListener('pagehide', this.onPageHide);\n document.removeEventListener('visibilitychange', this.onVisibilityChange);\n } else {\n const exitProcess = this.getExitProcess();\n exitProcess.off('beforeExit', this.onExit);\n exitProcess.off('SIGINT', this.onExit);\n exitProcess.off('SIGTERM', this.onExit);\n }\n\n const transport = await this.transportPromise;\n\n if (this.queue.length === 0) {\n // 队列已空:若持久化有脏标记,写空队列收敛,避免恢复已发送事件造成重复上报\n if (this.persistDirty) {\n await this.persistNow([]);\n }\n return;\n }\n\n if (this.flushOnExit) {\n const pending = this.queue.splice(0);\n // 优先同步尽力发送(sendBeacon,页面卸载场景 fetch 可能来不及)\n if (transport.sendSync?.(pending) ?? false) {\n this.opts.onFlushed?.(pending);\n await this.persistNow([]);\n return;\n }\n try {\n await transport.send(pending);\n this.opts.onFlushed?.(pending);\n await this.persistNow([]);\n } catch (error) {\n this.opts.onFailed?.(pending, error);\n this.queue.push(...pending); // 放回队列持久化,下次会话恢复重发\n await this.persistNow();\n }\n return;\n }\n\n // 退出兜底关闭:剩余队列仅持久化,下次启动恢复\n if (this.persistDirty) {\n await this.persistNow();\n }\n }\n\n /** 异步初始化:创建内置持久化、恢复上次会话队列、挂载退出兜底 */\n private async init(): Promise<void> {\n this.storage = await this.storagePromise;\n if (this.disposed) return;\n\n if (this.storage) {\n try {\n const restored = await this.storage.load();\n if (restored.length > 0) {\n this.enqueueRestored(restored);\n }\n } catch (error) {\n this.log('restore pending queue failed:', error);\n }\n }\n if (this.disposed) return;\n\n if (this.platform === 'browser') {\n if (this.flushOnExit) {\n window.addEventListener('pagehide', this.onPageHide);\n }\n if (this.flushOnExit || this.opts.autoPageView) {\n document.addEventListener('visibilitychange', this.onVisibilityChange);\n }\n if (this.opts.autoPageView) {\n this.track('page_view');\n }\n } else if (this.flushOnExit) {\n const exitProcess = this.getExitProcess();\n exitProcess.once('beforeExit', this.onExit);\n exitProcess.once('SIGINT', this.onExit);\n exitProcess.once('SIGTERM', this.onExit);\n }\n\n if (this.queue.length > 0) {\n this.scheduleFlush();\n this.schedulePersist();\n }\n }\n\n /** 恢复队列入队:历史事件放队头保证先到先发,超限按溢出丢弃 */\n private enqueueRestored(events: IReportEvent[]): void {\n let accepted = events;\n if (accepted.length > this.maxQueueSize) {\n this.opts.onDropped?.(accepted.slice(this.maxQueueSize), 'overflow');\n accepted = accepted.slice(0, this.maxQueueSize);\n }\n this.queue.unshift(...accepted);\n }\n\n private enqueue(event: IReportEvent): void {\n if (this.queue.length >= this.maxQueueSize) {\n if (this.overflowPolicy === 'drop-newest') {\n this.opts.onDropped?.([event], 'overflow');\n return;\n }\n if (this.overflowPolicy === 'drop-oldest') {\n this.opts.onDropped?.([this.queue.shift()!], 'overflow');\n } else {\n // flush-now:队列满时先冲刷腾位,不丢事件\n this.fire(this.flush());\n }\n }\n this.queue.push(event);\n this.schedulePersist();\n }\n\n /** 攒批定时:延迟上报的实现,窗口内事件合并为一批 */\n private scheduleFlush(): void {\n if (this.flushTimer || this.disposed || this.flushInterval <= 0) return;\n this.flushTimer = setTimeout(() => {\n this.flushTimer = null;\n if (this.queue.length > 0) {\n this.fire(this.flush());\n }\n }, this.flushInterval);\n }\n\n /** 串行清空队列;失败批次重新入队后由 retryTimer 再次触发 drain */\n private async drain(): Promise<void> {\n while (!this.disposed && !this.retryTimer && this.queue.length > 0) {\n // eslint-disable-next-line no-await-in-loop\n await this.sendBatch(this.takeBatch());\n }\n }\n\n /** 取一批:受条数 / 字节数限制;首条必取,避免单事件超 maxBatchBytes 时队列卡死 */\n private takeBatch(): IReportEvent[] {\n const batch: IReportEvent[] = [];\n let bytes = 0;\n while (this.queue.length > 0 && batch.length < this.maxBatchSize) {\n const size = JSON.stringify(this.queue[0]).length;\n if (batch.length > 0 && bytes + size > this.maxBatchBytes) break;\n batch.push(this.queue.shift()!);\n bytes += size;\n }\n return batch;\n }\n\n private async sendBatch(batch: IReportEvent[]): Promise<void> {\n const transport = await this.transportPromise;\n try {\n await transport.send(batch);\n this.opts.onFlushed?.(batch);\n this.schedulePersist(); // 该批已移出队列,持久化剩余\n } catch (error) {\n this.opts.onFailed?.(batch, error);\n this.log('send batch failed:', error);\n\n const updated = batch.map((event) => ({\n ...event,\n attempts: event.attempts + 1,\n }));\n const retryable = updated.filter(\n (event) => event.attempts < this.maxAttempts,\n );\n const dead = updated.filter(\n (event) => event.attempts >= this.maxAttempts,\n );\n if (dead.length > 0) {\n this.opts.onDropped?.(dead, 'max-attempts');\n }\n if (retryable.length > 0) {\n this.queue.unshift(...retryable); // 保持原顺序放回队头\n this.schedulePersist();\n this.scheduleRetry(retryable);\n }\n }\n }\n\n /** 失败退避:按批次内最大 attempts 计 2^n 指数增长,避免失败批次频繁重试 */\n private scheduleRetry(retryable: IReportEvent[]): void {\n if (this.retryTimer) return;\n const maxAttemptsInBatch = Math.max(\n ...retryable.map((event) => event.attempts),\n );\n const delay = this.backoffMs * 2 ** (maxAttemptsInBatch - 1);\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n this.fire(this.flush());\n }, delay);\n }\n\n /** 持久化节流:窗口内多次变更只落盘一次,避免每次入队全量序列化 */\n private schedulePersist(): void {\n if (!this.storage) return;\n this.persistDirty = true;\n if (this.persistTimer) return;\n this.persistTimer = setTimeout(() => {\n this.persistTimer = null;\n if (!this.persistDirty) return;\n this.persistDirty = false;\n this.fire(this.persistNow());\n }, STORAGE_WRITE_THROTTLE_MS);\n }\n\n private async persistNow(events: IReportEvent[] = this.queue): Promise<void> {\n if (!this.storage) return;\n try {\n await this.storage.save(events);\n } catch (error) {\n this.log('persist queue failed:', error);\n }\n }\n\n private log(...args: unknown[]): void {\n if (this.opts.debug) {\n // eslint-disable-next-line no-console\n console.log('[reporter]', ...args);\n }\n }\n\n /**\n * Node 进程退出事件句柄:Electron 渲染进程 typings 里 process 只声明了 'loaded'\n * 事件,与 @types/node 不一致,显式收窄到本 SDK 用到的信号,兼容两端编译器\n */\n private getExitProcess(): ExitProcessHandle {\n return process as unknown as ExitProcessHandle;\n }\n\n /** 安全触发异步任务:吞掉 rejection,避免未处理异常 */\n private fire(promise: Promise<unknown>): void {\n promise.catch((error) => {\n this.log('async task failed:', error);\n });\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/id.ts","../src/builtins.ts","../src/collect.ts","../src/page.ts","../src/reporter.ts"],"sourcesContent":["// ============================================================\n// 通用数据上报 SDK —— 类型接口 + 核心实现与内置适配器\n// 定位:替代 renderer 现有 event-tracker-sdk,平台无关、去硬编码。\n// 浏览器端与 Node 端是强需求,双端共用同一核心(入队、攒批、重试、有界队列),\n// 差异仅在发送与持久化:可注入自定义实现,不注入时按 platform 选项\n// (缺省自动探测 typeof window)使用内置实现(实现见 reporter.ts / builtins.ts):\n// 浏览器: sendBeacon(退出兜底)→ fetch(keepalive)+ Dexie/IndexedDB(复用\n// 项目现有依赖,默认开启,不支持时降级 localStorage;写入节流,\n// 避免每次入队全量序列化阻塞主线程);\n// Node: axios 请求 + conf 持久化(electron-store 的纯 Node 内核,Electron\n// main 与纯 Node 服务端通吃,persistDir 配置后启用,未配置仅内存;\n// JSON 文件 + 内部原子写)。\n// 数据模型:SDK 只约定上报信封(顶层身份字段 + app / device / page / user /\n// properties 五个容器)与 SDK 自身行为(身份生成、pageView 汇总、\n// 曝光采集);容器内容由接入方通过统一 context 提供(静态对象或\n// 实时函数),SDK 不定义业务字段语义、不做业务硬编码,埋点数据的\n// 具体字段要求属于接入方埋点方案,由项目侧类型层约束。\n// ============================================================\n\n/** 事件被丢弃的原因 */\nexport type DropReason = 'max-attempts' | 'overflow' | 'invalid';\n\n/** 签名函数:对序列化后的上报信封生成签名等附加键值(如 X-Clipto-Timestamp / X-Clipto-Signature),返回值作为请求头随包发送,服务端校验防止伪造数据污染;算法与密钥由接入方决定(如 WebCrypto HMAC);可同步或异步返回 */\nexport type IReportSigner = (\n payload: string,\n) => Record<string, string> | Promise<Record<string, string>>;\n\n/** 业务节点:app / device / page / user / properties 的统一形态;具体字段与取值由接入方埋点方案约束,SDK 不感知语义、原样透传 */\nexport type ReportNode = Record<string, unknown>;\n\n/** 业务上下文:统一承载各业务节点;可传静态对象,或传函数在每次入队时实时计算(读取最新登录态 / 页面状态) */\nexport interface IEventContext {\n /** 应用信息:产品标识 / 环境 / 平台 / 版本 / 渠道归因等,由接入方配置 */\n app?: ReportNode;\n /** 设备信息:与 SDK 自动采集的硬件 / 系统 / 浏览器信息在同一节点内按字段合并,接入方提供的字段值优先;ip / country 等由服务端填充 */\n device?: ReportNode;\n /** 页面信息:与 SDK 自动采集的页面快照合并;pageId 等接入方定义字段在此提供 */\n page?: ReportNode;\n /** 用户信息:账号 / 订阅 / 权益 / 实验分组等,由接入方账号体系提供 */\n user?: ReportNode;\n /** 合并进每个事件 properties 的公共业务字段;track() 传入的 properties 优先级更高 */\n properties?: ReportNode;\n}\n\n/** 事件名:接入方埋点方案定义的稳定事件名,同一行为跨 Web、iOS、Android、Mac、Windows 尽量同名;以下为 SDK 预置事件名,业务事件可继续扩展 */\nexport type EventName =\n | 'pageView' // 页面使用汇总:一次页面访问结束时上报一条,携带本次访问的停留 / 活跃 / 滚动汇总与 endReason,并复用进入页面时的 visitId\n | 'elementExposure' // 功能曝光:仅接入方注册的关键入口 / 内容;可见面积 ≥50% 且连续可见 ≥1s 时,单元素单事件上报,同一页面访问内按 pageViewId + elementId + placementId 去重\n | 'user_action' // 现有业务事件:功能操作、成功与失败(失败写入 errorCode / errorMessage)\n | (string & {});\n\n/** 上报事件(网络信封结构:身份字段由 SDK 生成,业务节点由接入方 context 提供或 SDK 自动采集) */\nexport interface IReportEvent {\n /** SDK 自动生成:每条事件唯一 ID,用于网络重试与服务端幂等去重;同一事件重试时复用原值 */\n eventId: string;\n /** 埋点方案定义的稳定事件名,研发接入;同一行为跨端尽量同名 */\n eventName: string;\n /** 登录账号 id:账号系统提供、SDK 自动读取;未登录为 null,登录后携带,退出后恢复 null;接入方不手工填写 */\n userId: string | null;\n /** 匿名 id:SDK 自动生成,首次匿名访问 / 安装时生成,登录后继续保留;清浏览器数据、换浏览器或重装 App 后可能生成新值 */\n anonymousId: string;\n /** 会话 id:SDK 会话模块生成;页面切换保持不变,同源多 tab 共享一个会话(任一 tab 活动即刷新活跃时间);连续 30 分钟无活动、退出或切换账号后,下一次活动生成新值 */\n visitId: string;\n /** 设备 / 安装实例 id:SDK 自动生成并持久化;Web&PC 与 google client id 一致,iOS&Android 为 IDFV */\n clientId: string;\n /** 事件实际发生的毫秒时间戳,由客户端时钟产生(SDK 自动生成) */\n timestamp: number;\n /** 应用信息(接入方 context 提供,节点始终存在,内容由埋点方案约束) */\n app: ReportNode;\n /** 设备信息(SDK 自动采集 + context 合并) */\n device?: ReportNode;\n /** 页面信息(SDK 自动采集 + context 合并) */\n page?: ReportNode;\n /** 用户信息(接入方 context 提供) */\n user?: ReportNode;\n /** 业务事件属性(context.properties 与 track properties 合并) */\n properties?: ReportNode;\n}\n\n/** 队列内部事件:上报事件 + SDK 内部字段(序列化上报前剥离) */\nexport interface IQueuedEvent extends IReportEvent {\n /** 已尝试发送次数,达到 maxAttempts 即丢弃 */\n attempts: number;\n}\n\n/** 发送适配器:如何把一批事件发出去(浏览器: sendBeacon → fetch;Node: https / axios) */\nexport interface IReportTransport {\n /** 发送一批事件(纯上报结构,内部字段如 attempts 已由核心剥离);resolve 视为成功,reject 进入重试 */\n send(events: IReportEvent[]): Promise<void>;\n /** 退出兜底时的同步尽力发送(浏览器端为 navigator.sendBeacon);返回是否已受理 */\n sendSync?(events: IReportEvent[]): boolean;\n}\n\n/** 持久化适配器:待发队列跨页面刷新 / 进程重启存活(浏览器 IndexedDB / Node conf) */\nexport interface IReportStorage {\n /** 整体覆盖保存当前待发队列(含重试计数,恢复后继续计次;实现需幂等;写入节流由核心负责) */\n save(events: IQueuedEvent[]): void | Promise<void>;\n /** 读取上次保存的待发队列 */\n load(): IQueuedEvent[] | Promise<IQueuedEvent[]>;\n /** 清空 */\n clear(): void | Promise<void>;\n}\n\n/** 构造配置 */\nexport interface IReporterOptions {\n /** 上报端点,必填(浏览器与 Node 共用) */\n endpoint: string;\n /** 业务上下文:统一提供 app / device / page / user / properties 业务节点;静态对象或实时函数(每次入队时调用);\n * 与 SDK 自动采集的 device / page 在同一节点内按字段合并,接入方提供的字段值优先 */\n context?: IEventContext | (() => IEventContext);\n /** 运行平台:显式声明,驱动内置发送 / 持久化实现;缺省自动探测(typeof window) */\n platform?: 'browser' | 'node';\n /** 攒批窗口(ms),即延迟上报的固定间隔;0 表示关闭批量,事件即时发送 */\n flushInterval?: number; // 默认 2000(对齐旧 SDK batchTimeout)\n /** 单批最大条数,达到即刷 */\n maxBatchSize?: number; // 默认 10(对齐旧 SDK)\n /** 单批最大字节数(JSON 序列化估算),达到即刷 */\n maxBatchBytes?: number; // 默认 64 * 1024\n /** 队列最大条数,防止内存无限增长 */\n maxQueueSize?: number; // 默认 1000\n /** 队列溢出处理 */\n overflowPolicy?: 'drop-oldest' | 'drop-newest' | 'flush-now'; // 默认 'drop-oldest'\n /** 单个事件最大发送尝试次数,超限丢弃 */\n maxAttempts?: number; // 默认 3(对齐旧 SDK)\n /** 失败重发退避基础延迟(ms),按 2^n 指数增长 */\n backoffMs?: number; // 默认 1000(对齐旧 SDK retryDelay)\n /** 登录账号 id 提供函数:每次入队时实时读取(由接入方账号体系提供);未提供时恒为 null */\n userId?: () => string | null;\n /** 页面卸载 / 进程退出时兜底 flush(浏览器端走 sendBeacon) */\n flushOnExit?: boolean; // 默认 true\n /** 浏览器端:队列是否持久化到本地(优先 IndexedDB,不支持时降级 localStorage) */\n persist?: boolean; // 默认 true(对齐旧 SDK 兜底行为)\n /** Node 端:持久化目录,设置后待发队列落盘(跨进程重启存活);不传仅内存 */\n persistDir?: string;\n /** 浏览器端:自动管理页面访问并上报 pageView(进入页面建立访问上下文,离开页面时汇总上报) */\n autoPageView?: boolean; // 默认 false(对齐旧 SDK)\n /** 自定义请求头(内置传输实现使用) */\n headers?: Record<string, string>;\n /** 可选签名:每个批次发送前对序列化信封调用 signer,返回的键值对(通常为 X-Clipto-Timestamp / X-Clipto-Signature 等签名头)作为请求头随包发送,服务端校验防伪造/污染;\n * payload 为信封 JSON 序列化(含 metadata.timestamp,可作签名时间戳);\n * 仅内置 transport 生效(自定义 transport 自行处理);sendBeacon 不支持自定义请求头,\n * 配置 signer 后退出兜底自动回退为带签名头的异步发送(keepalive fetch) */\n signer?: IReportSigner;\n /** 打开调试日志 */\n debug?: boolean; // 默认 false\n\n /** 可选注入:发送适配器;不传按运行环境内置 */\n transport?: IReportTransport;\n /** 可选注入:持久化适配器;不传按运行环境内置(浏览器 Dexie→localStorage 降级 / Node conf,未配 persistDir 仅内存) */\n storage?: IReportStorage;\n\n /** 一批发送成功且已从队列移除 */\n onFlushed?(events: IReportEvent[]): void;\n /** 一批发送失败(将按策略重试或丢弃) */\n onFailed?(events: IReportEvent[], error: unknown): void;\n /** 事件被丢弃(重试耗尽 / 溢出 / 非法) */\n onDropped?(events: IReportEvent[], reason: DropReason): void;\n}\n\n/** 公共接口 */\nexport interface IReporter {\n /** 上报一条事件并返回 eventId(dispose 后或非法事件返回空串);immediate 事件跳过攒批立即发送;事件名见 EventName */\n track(\n eventName: EventName,\n properties?: ReportNode,\n options?: { immediate?: boolean },\n ): string;\n /** 立即冲刷队列(跳过退避等待),并等待在途批次完成 */\n flush(): Promise<void>;\n /** 队列中待发条数 */\n readonly size: number;\n /** 当前登录账号 id:实时调用 userId provider;未提供时恒为 null */\n readonly userId: string | null;\n /** 匿名 id:首次生成后持久化不变;供外部转化跟踪等场景读取 */\n readonly anonymousId: string;\n /** 设备 / 安装实例 id:Web&PC 与 google client id 一致;供外部转化跟踪等场景读取 */\n readonly clientId: string;\n /** 当前会话 id:会话轮换后返回新值(同源多 tab 共享);供外部转化跟踪等场景读取 */\n readonly visitId: string;\n /** 停止并兜底 flush(flushOnExit),之后拒绝新事件 */\n dispose(): Promise<void>;\n}\n\n/** 实现类的构造签名:new Reporter(options) 得到 IReporter */\nexport interface IReporterConstructor {\n new (options: IReporterOptions): IReporter;\n}\n\n// ============================================================\n// 实现与内置适配器\n// ============================================================\nexport { Reporter } from './reporter';\nexport {\n createBrowserStorage,\n createBrowserTransport,\n createLocalStorageStorage,\n createNodeStorage,\n createNodeTransport,\n} from './builtins';\n","/**\n * 事件 / 批次 id:对齐旧 SDK generateEventId 格式(evt_时间戳_随机串)\n */\nexport function generateEventId(): string {\n return `evt_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;\n}\n","// ============================================================\n// 内置 transport / storage —— 复用现有组件,不新造轮子:\n// 浏览器发送: 原生 fetch(keepalive)+ navigator.sendBeacon(退出兜底)\n// Node 发送: axios(项目现有依赖,动态加载,不进 renderer 包)\n// 浏览器持久化: Dexie(项目现有依赖,与 @renderer/common/idb 同款;\n// IndexedDB 不可用时降级 localStorage)\n// Node 持久化: conf(electron-store 的纯 Node 内核,Electron main 与\n// 纯 Node 服务端通吃,JSON 文件 + 内部原子写),整体覆盖保存\n// payload 沿用旧 SDK 的 { events, metadata } 信封,便于后端平滑过渡;\n// events 为上报事件原样透传(SDK 内部字段已在核心剥离)。\n// ============================================================\nimport type {\n IQueuedEvent,\n IReportEvent,\n IReportSigner,\n IReportStorage,\n IReportTransport,\n} from './index';\nimport { generateEventId } from './id';\n\nconst SDK_VERSION = '1.0.0'; // 对齐旧 SDK sdkVersion,便于后端平滑过渡\nconst DB_NAME = 'CliptoReportDB';\nconst STORE_NAME = 'clipto-reporting';\nconst QUEUE_KEY = 'pending';\nconst LS_KEY = 'clipto_report_pending_queue';\n\n/** 上报信封:对齐旧 SDK { events, metadata } 结构;签名由 signer 生成后以请求头随包发送 */\ninterface IEventPayload {\n events: IReportEvent[];\n metadata: {\n sdkVersion: string;\n timestamp: number;\n batchId: string;\n };\n}\n\nfunction buildPayload(events: IReportEvent[]): IEventPayload {\n return {\n events,\n metadata: {\n sdkVersion: SDK_VERSION,\n timestamp: Date.now(),\n batchId: generateEventId(), // 与事件 id 同格式(对齐旧 SDK)\n },\n };\n}\n\n/** 生成签名头:signer 返回的键值对作为请求头随包发送(如 X-Clipto-Timestamp / X-Clipto-Signature);签名失败随发送失败进入重试 */\nasync function signHeaders(\n payload: IEventPayload,\n signer?: IReportSigner,\n): Promise<Record<string, string>> {\n if (!signer) return {};\n return signer(JSON.stringify(payload));\n}\n\n/** 浏览器发送:fetch(keepalive)走正常路径;sendBeacon 仅退出兜底(sendSync) */\nexport function createBrowserTransport(\n endpoint: string,\n headers?: Record<string, string>,\n signer?: IReportSigner,\n): IReportTransport {\n const commonHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...(headers ?? {}),\n };\n return {\n async send(events) {\n const payload = buildPayload(events);\n const requestHeaders = {\n ...commonHeaders,\n ...(await signHeaders(payload, signer)),\n };\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(payload),\n keepalive: true,\n });\n if (!response.ok) {\n throw new Error(`report send failed: HTTP ${response.status}`);\n }\n },\n sendSync(events) {\n if (\n typeof navigator === 'undefined' ||\n typeof navigator.sendBeacon !== 'function' ||\n signer\n ) {\n // sendBeacon 不支持自定义请求头:配置 signer 时放弃 beacon,\n // 由调用方回退带签名头的异步发送(keepalive fetch)\n return false;\n }\n try {\n const blob = new Blob([JSON.stringify(buildPayload(events))], {\n type: 'application/json',\n });\n return navigator.sendBeacon(endpoint, blob);\n } catch {\n return false;\n }\n },\n };\n}\n\n/** Node 发送:动态加载项目现有依赖 axios(仅在 Node 路径执行) */\nexport async function createNodeTransport(\n endpoint: string,\n headers?: Record<string, string>,\n signer?: IReportSigner,\n): Promise<IReportTransport> {\n const { default: axios } = await import('axios');\n return {\n async send(events) {\n const payload = buildPayload(events);\n await axios.post(endpoint, payload, {\n headers: {\n ...(headers ?? {}),\n ...(await signHeaders(payload, signer)),\n },\n timeout: 10000,\n });\n },\n };\n}\n\n/** 降级实现:localStorage(容量有限,仅兜底) */\nexport function createLocalStorageStorage(): IReportStorage {\n return {\n save(events) {\n try {\n localStorage.setItem(LS_KEY, JSON.stringify(events));\n } catch {\n // 配额满或不可用:静默降级为不持久化\n }\n },\n load() {\n try {\n const text = localStorage.getItem(LS_KEY);\n return text ? (JSON.parse(text) as IQueuedEvent[]) : [];\n } catch {\n return [];\n }\n },\n clear() {\n try {\n localStorage.removeItem(LS_KEY);\n } catch {\n // ignore\n }\n },\n };\n}\n\n/** 浏览器持久化:优先 Dexie(IndexedDB),不可用时降级 localStorage */\nexport async function createBrowserStorage(): Promise<IReportStorage> {\n try {\n // 动态加载项目现有依赖 Dexie:仅在浏览器路径执行,不进 main 进程\n const { default: Dexie } = await import('dexie');\n const db = new Dexie(DB_NAME);\n db.version(1).stores({ queue: 'key' });\n await db.open(); // 提前探活,失败走 localStorage 降级\n\n const table = db.table<{ key: string; events: IQueuedEvent[] }, string>(\n 'queue',\n );\n return {\n async save(events) {\n await table.put({ key: QUEUE_KEY, events });\n },\n async load() {\n const row = await table.get(QUEUE_KEY);\n return row ? row.events : [];\n },\n async clear() {\n await table.delete(QUEUE_KEY);\n },\n };\n } catch {\n // IndexedDB 不可用(隐私模式 / 极老浏览器):降级 localStorage\n return createLocalStorageStorage();\n }\n}\n\n/** Node 持久化:conf(纯 Node 可用,JSON 文件 + 内部原子写),整体覆盖保存 */\nexport async function createNodeStorage(\n persistDir: string,\n): Promise<IReportStorage> {\n // 动态加载:仅在 Node 路径执行,renderer 永不进入该分支。\n // webpackIgnore:让打包器跳过 conf(依赖 Node 内置模块,web 目标无法打包),\n // 运行时由 Node 直接解析;SDK 作为 npm 包发布时 conf 随依赖安装。\n // 注意:打包后的 main 进程若启用该分支,需将 conf 加入\n // release/app/package.json dependencies(同 better-sqlite3 等原生依赖模式)。\n const { default: Conf } = await import(/* webpackIgnore: true */ 'conf');\n const store = new Conf<Record<string, IQueuedEvent[]>>({\n configName: STORE_NAME, // 落盘为 persistDir/clipto-reporting.json\n cwd: persistDir,\n });\n return {\n save(events) {\n store.set(QUEUE_KEY, events);\n },\n load() {\n return store.get(QUEUE_KEY) ?? [];\n },\n clear() {\n store.delete(QUEUE_KEY);\n },\n };\n}\n","// ============================================================\n// 内置采集:设备 / 页面快照。\n// 只采集通用、平台可得的字段,不做业务口径约束;采集结果与接入方\n// context 在同一节点内按字段合并,接入方提供的字段值优先。\n// ============================================================\nimport { UAParser } from 'ua-parser-js';\nimport type { ReportNode } from './index';\n\ninterface IUserAgentInfo {\n browserName?: string;\n browserVersion?: string;\n osName?: string;\n osVersion?: string;\n}\n\n/**\n * 浏览器 UA 解析:复用社区成熟的 ua-parser-js(活跃维护),不自行维护 UA 规则。\n * 仅做输出归一化,字段命名与取值口径对齐 SDK 既有格式:\n * iOS Safari 解出 \"Mobile Safari\" 归一为 \"Safari\",移动端 Chrome 解出\n * \"Mobile Chrome\" 归一为 \"Chrome\",\"Mac OS\" 归一为 \"macOS\"。\n * (库同时解出设备型号 / 厂商 / 类型,规范确认 device 字段口径后可直接使用)\n */\nfunction parseUserAgent(ua: string): IUserAgentInfo {\n const parsed = new UAParser(ua);\n const browser = parsed.getBrowser();\n const os = parsed.getOS();\n const info: IUserAgentInfo = {};\n if (browser.name) {\n info.browserName =\n browser.name === 'Mobile Safari'\n ? 'Safari'\n : browser.name === 'Mobile Chrome'\n ? 'Chrome'\n : browser.name;\n }\n if (browser.version) info.browserVersion = browser.version;\n if (os.name) info.osName = os.name === 'Mac OS' ? 'macOS' : os.name;\n if (os.version) info.osVersion = os.version;\n return info;\n}\n\n/** 浏览器端设备快照:同步读取,每次入队时合并(语言等字段保持实时) */\nexport function collectBrowserDevice(): ReportNode {\n if (typeof navigator === 'undefined') return {};\n const { browserName, browserVersion, osName, osVersion } = parseUserAgent(\n navigator.userAgent || '',\n );\n const device: ReportNode = {};\n if (browserName) device.browserName = browserName;\n if (browserVersion) device.browserVersion = browserVersion;\n if (osName) device.osName = osName;\n if (osVersion) device.osVersion = osVersion;\n // 平台:优先 UA Client Hints,退化 navigator.platform(已废弃但仍可读)\n const uaData = (navigator as Navigator & {\n userAgentData?: { platform?: string };\n }).userAgentData;\n if (uaData?.platform) device.platform = uaData.platform;\n else if (navigator.platform) device.platform = navigator.platform;\n if (navigator.language) device.language = navigator.language;\n return device;\n}\n\n/** Node 端设备快照:动态加载 os 模块(不进浏览器包),init 时采集一次缓存 */\nexport async function collectNodeDevice(): Promise<ReportNode> {\n try {\n const os = await import('os');\n const device: ReportNode = {\n platform: os.platform(),\n osVersion: os.release(),\n arch: os.arch(),\n memSize: Math.round(os.totalmem() / 1024 ** 3), // 统一 GB\n };\n const type = os.type();\n device.osName =\n type === 'Darwin' ? 'macOS' : type === 'Windows_NT' ? 'Windows' : type;\n const cpu = os.cpus()[0]?.model;\n if (cpu) device.cpu = cpu;\n return device;\n } catch {\n return {}; // os 模块不可用:跳过采集\n }\n}\n\n/** 页面快照:事件发生时的地址 / 标题 / 来源(browser 专用) */\nexport function collectPageSnapshot(): ReportNode {\n if (typeof document === 'undefined') return {};\n const page: ReportNode = {};\n if (location.href) page.url = location.href;\n if (document.title) page.title = document.title;\n if (document.referrer) page.referrer = document.referrer;\n return page;\n}\n","// ============================================================\n// 页面访问跟踪(browser 专用,autoPageView 开启时启用):\n// 进入页面建立访问上下文(生成 pageViewId,记录进入时快照),累计\n// durationMs / activeMs / scrollCount / maxScrollDepthPct;\n// 离开时返回本次访问的汇总 page 节点,供 pageView 事件使用。\n// 规则:连续 60s 无操作暂停累计活跃时长;连续滚动间隔 ≤500ms\n// 计为一次手势;深度按可见区底端占文档总高百分比计,范围 0–100。\n// ============================================================\nimport type { ReportNode } from './index';\nimport { collectPageSnapshot } from './collect';\nimport { generateEventId } from './id';\n\nexport type PageEndReason =\n | 'routeChange'\n | 'back'\n | 'close'\n | 'background'\n | 'sessionTimeout'\n | 'unknown';\n\nconst SCROLL_GESTURE_GAP_MS = 500; // 连续滚动间隔 ≤500ms 计为一次手势\nconst IDLE_PAUSE_MS = 60_000; // 连续无操作 60s 暂停累计活跃时长\nconst METRIC_TICK_MS = 1000; // 汇总指标采样周期\n\ninterface IPageVisit {\n pageViewId: string;\n enteredAt: number;\n /** 进入页面时的 url / title / referrer 快照(pageView 汇总事件复用) */\n snapshot: ReportNode;\n activeMs: number;\n scrollCount: number;\n maxScrollDepthPct: number;\n lastActivityAt: number;\n lastScrollAt: number;\n}\n\nexport interface IPageTracker {\n /** 是否有进行中的页面访问 */\n readonly active: boolean;\n /** 进入页面:生成新 pageViewId 并开始累计(进行中时忽略) */\n startVisit(): void;\n /** 离开页面:结束访问并返回汇总 page 节点(无进行中访问时返回 undefined) */\n endVisit(reason: PageEndReason): ReportNode | undefined;\n /** 进行中访问的 pageViewId */\n pageViewId(): string | undefined;\n /** 清理监听与定时器(不结束访问,调用方先 endVisit) */\n dispose(): void;\n}\n\n/** 创建页面访问跟踪器;onActivity 在检测到用户操作时回调(用于刷新会话活跃时间) */\nexport function createPageTracker(onActivity: () => void): IPageTracker {\n let visit: IPageVisit | null = null;\n let interval: ReturnType<typeof setInterval> | null = null;\n\n const touch = (): void => {\n if (!visit) return;\n visit.lastActivityAt = Date.now();\n onActivity();\n };\n\n const updateMaxDepth = (): void => {\n if (!visit) return;\n const doc = document.documentElement;\n if (!doc) return;\n const total = Math.max(doc.scrollHeight, 1);\n const depth = Math.round(\n ((window.scrollY + window.innerHeight) / total) * 100,\n );\n visit.maxScrollDepthPct = Math.max(\n visit.maxScrollDepthPct,\n Math.min(depth, 100),\n );\n };\n\n const onScroll = (): void => {\n if (!visit) return;\n const now = Date.now();\n if (now - visit.lastScrollAt > SCROLL_GESTURE_GAP_MS) {\n visit.scrollCount += 1;\n }\n visit.lastScrollAt = now;\n touch();\n updateMaxDepth();\n };\n\n const onInput = (): void => touch();\n\n const tick = (): void => {\n if (!visit) return;\n if (\n document.visibilityState === 'visible' &&\n Date.now() - visit.lastActivityAt <= IDLE_PAUSE_MS\n ) {\n visit.activeMs += METRIC_TICK_MS;\n }\n updateMaxDepth();\n };\n\n const removeListeners = (): void => {\n window.removeEventListener('scroll', onScroll);\n window.removeEventListener('pointerdown', onInput, { capture: true });\n window.removeEventListener('keydown', onInput, { capture: true });\n if (interval) {\n clearInterval(interval);\n interval = null;\n }\n };\n\n const startVisit = (): void => {\n if (visit) return;\n const now = Date.now();\n visit = {\n pageViewId: `pv_${generateEventId()}`,\n enteredAt: now,\n snapshot: collectPageSnapshot(),\n activeMs: 0,\n scrollCount: 0,\n maxScrollDepthPct: 0,\n lastActivityAt: now,\n lastScrollAt: 0,\n };\n window.addEventListener('scroll', onScroll, { passive: true });\n window.addEventListener('pointerdown', onInput, {\n capture: true,\n passive: true,\n });\n window.addEventListener('keydown', onInput, { capture: true, passive: true });\n interval = setInterval(tick, METRIC_TICK_MS);\n };\n\n const endVisit = (reason: PageEndReason): ReportNode | undefined => {\n if (!visit) return undefined;\n removeListeners();\n const finished = visit;\n visit = null;\n // SPA 常在 effect 中异步设置标题,进入时快照可能滞后:离开时用当前\n // document.title 刷新(url / referrer 保持进入时快照——routeChange /\n // back 的离开瞬间地址已是新路由,而标题仍是旧页面稳定后的值,语义正确)\n if (document.title) {\n finished.snapshot.title = document.title;\n }\n return {\n ...finished.snapshot,\n pageViewId: finished.pageViewId,\n durationMs: Date.now() - finished.enteredAt,\n activeMs: finished.activeMs,\n scrollCount: finished.scrollCount,\n maxScrollDepthPct: finished.maxScrollDepthPct,\n endReason: reason,\n };\n };\n\n return {\n get active(): boolean {\n return visit !== null;\n },\n startVisit,\n endVisit,\n pageViewId: (): string | undefined => visit?.pageViewId,\n dispose: removeListeners,\n };\n}\n","// ============================================================\n// Reporter 核心实现 —— 平台无关,差异通过 transport / storage 注入;\n// 业务数据(app / device / page / user / properties)通过统一 context\n// 注入,SDK 只负责自身行为:身份字段生成与持久化、会话轮换、\n// 页面访问跟踪(pageView 汇总)、入队、攒批、重试退避、有界队列。\n// 复用现有组件:setTimeout(攒批 / 退避定时);事件 id 对齐旧 SDK\n// generateEventId 格式(见 id.ts)。\n// ============================================================\nimport type {\n EventName,\n IEventContext,\n IReportEvent,\n IReporter,\n IReporterOptions,\n IReportStorage,\n IReportTransport,\n IQueuedEvent,\n ReportNode,\n} from './index';\nimport {\n createBrowserStorage,\n createBrowserTransport,\n createNodeStorage,\n createNodeTransport,\n} from './builtins';\nimport {\n collectBrowserDevice,\n collectNodeDevice,\n collectPageSnapshot,\n} from './collect';\nimport {\n createPageTracker,\n type IPageTracker,\n type PageEndReason,\n} from './page';\nimport { generateEventId } from './id';\n\nconst DEFAULT_FLUSH_INTERVAL = 2000; // 攒批窗口(ms),即延迟上报间隔\nconst DEFAULT_MAX_BATCH_SIZE = 10; // 单批最大条数,达到即刷\nconst DEFAULT_MAX_BATCH_BYTES = 64 * 1024; // 单批最大字节数,达到即刷\nconst DEFAULT_MAX_QUEUE_SIZE = 1000; // 队列最大条数\nconst DEFAULT_MAX_ATTEMPTS = 3; // 单事件最大发送尝试次数,超限丢弃\nconst DEFAULT_BACKOFF_MS = 1000; // 重试退避基数,按 2^n 指数增长\nconst STORAGE_WRITE_THROTTLE_MS = 500; // 持久化写入节流窗口\nconst VISIT_IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 会话有效期:连续无活动 30 分钟\nconst SESSION_SAVE_THROTTLE_MS = 10_000; // 会话活跃时间落盘节流,避免滚动等高频活动频繁写\n\n/** 浏览器端身份持久化键 */\nconst ANONYMOUS_ID_KEY = 'clipto_report_anonymous_id';\nconst CLIENT_ID_KEY = 'clipto_report_client_id';\nconst SESSION_KEY = 'clipto_report_session';\n\n/** Node 进程退出信号:兼容 Electron 渲染进程与 @types/node 对 process 的 typings 差异 */\ntype ExitSignal = 'beforeExit' | 'SIGINT' | 'SIGTERM';\n\ntype ExitProcessHandle = {\n once(event: ExitSignal, listener: () => void): unknown;\n off(event: ExitSignal, listener: () => void): unknown;\n};\n\n/** 会话状态:visitId + 最近活跃时间 */\ninterface ISession {\n visitId: string;\n lastActivityMs: number;\n}\n\n/** context 归一化:静态对象包装为函数,每次入队时实时读取 */\nfunction normalizeContext(\n context: IReporterOptions['context'],\n): () => IEventContext {\n if (typeof context === 'function') return context;\n return () => context ?? {};\n}\n\n/** 发送前剥离 SDK 内部字段,transport 收到纯上报结构 */\nfunction stripAttempts(events: IQueuedEvent[]): IReportEvent[] {\n return events.map((event) => {\n const { attempts: _attempts, ...rest } = event;\n return rest;\n });\n}\n\nexport class Reporter implements IReporter {\n private readonly opts: IReporterOptions;\n\n private readonly platform: 'browser' | 'node';\n\n private readonly contextFn: () => IEventContext;\n\n private readonly flushInterval: number;\n\n private readonly maxBatchSize: number;\n\n private readonly maxBatchBytes: number;\n\n private readonly maxQueueSize: number;\n\n private readonly overflowPolicy: NonNullable<\n IReporterOptions['overflowPolicy']\n >;\n\n private readonly maxAttempts: number;\n\n private readonly backoffMs: number;\n\n private readonly flushOnExit: boolean;\n\n private readonly transportPromise: Promise<IReportTransport>;\n\n private readonly storagePromise: Promise<IReportStorage | undefined>;\n\n /** 匿名 id:首次生成后持久化(浏览器 localStorage / Node 进程内);对外只读访问见同名 getter */\n private readonly _anonymousId: string;\n\n /** 设备 / 安装实例 id:Web&PC 读取 google client id(_ga),否则生成并持久化;Node 进程内生成;对外只读访问见同名 getter */\n private readonly _clientId: string;\n\n /** 会话落盘时间戳(节流用);声明在 session 之前:session 初始化会触发强制落盘 */\n private sessionSaveAt = 0;\n\n /** 会话共享读时间戳(节流用):多 tab 共享会话,活动时定期重读 localStorage 最新值 */\n private sessionReadAt = 0;\n\n /** 会话:visitId 与最近活跃时间(30 分钟无活动轮换);localStorage 持久化,同源多 tab 共享 */\n private session: ISession;\n\n /** Node 端设备快照(init 时采集一次,避免每次入队动态加载 os) */\n private nodeDevice: ReportNode = {};\n\n /** 页面访问跟踪(browser + autoPageView 时启用) */\n private pageTracker: IPageTracker | null = null;\n\n /** history 补丁的原始引用(dispose 时还原) */\n private historyPatch: {\n pushState: typeof history.pushState;\n replaceState: typeof history.replaceState;\n } | null = null;\n\n /** 待发队列(内存中的事实源,持久化是它的影子) */\n private readonly queue: IQueuedEvent[] = [];\n\n /** init 完成后指向内置 / 注入的持久化实现 */\n private storage: IReportStorage | undefined;\n\n private flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n private retryTimer: ReturnType<typeof setTimeout> | null = null;\n\n private persistTimer: ReturnType<typeof setTimeout> | null = null;\n\n private persistDirty = false;\n\n private flushPromise: Promise<void> | null = null;\n\n private disposed = false;\n\n private readonly onExit = (): void => {\n this.fire(this.dispose());\n };\n\n private readonly onPageHide = (): void => {\n // 关闭 / 刷新页面:结束本次访问(close),随后 dispose 兜底 flush(sendBeacon)\n this.endPageVisit(this.session.visitId, 'close');\n this.fire(this.dispose());\n };\n\n private readonly onPageShow = (): void => {\n // bfcache 恢复:重新可见时开启新的页面访问\n if (document.visibilityState === 'visible') {\n this.pageTracker?.startVisit();\n }\n };\n\n private readonly onVisibilityChange = (): void => {\n if (document.visibilityState === 'hidden') {\n // 页面切后台:结束本次访问(background),冲刷一次(非卸载,保持接收新事件)\n this.endPageVisit(this.session.visitId, 'background');\n this.fire(this.flush());\n } else {\n this.pageTracker?.startVisit();\n }\n };\n\n private readonly onPopState = (): void => {\n this.endPageVisit(this.session.visitId, 'back');\n this.pageTracker?.startVisit();\n };\n\n private readonly onStorage = (event: StorageEvent): void => {\n if (event.key !== SESSION_KEY || !event.newValue) return;\n // 多 tab 共享会话:其他 tab 的活动 / 轮换实时同步到本 tab\n try {\n this.adoptSharedSession(JSON.parse(event.newValue) as ISession);\n } catch {\n // 解析失败:忽略,下次活动时重读兜底\n }\n };\n\n constructor(options: IReporterOptions) {\n if (!options.endpoint) {\n throw new Error('Reporter: endpoint 为必填项');\n }\n this.opts = options;\n this.platform =\n options.platform ?? (typeof window !== 'undefined' ? 'browser' : 'node');\n this.contextFn = normalizeContext(options.context);\n this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;\n this.maxBatchBytes = options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES;\n this.maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;\n this.overflowPolicy = options.overflowPolicy ?? 'drop-oldest';\n this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n this.backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;\n this.flushOnExit = options.flushOnExit ?? true;\n\n this._anonymousId = this.loadOrCreateAnonymousId();\n this._clientId = this.loadOrCreateClientId();\n this.session = this.loadOrCreateSession();\n\n // 注入优先;未注入按平台内置(复用现有组件,见 builtins.ts)\n if (options.transport) {\n this.transportPromise = Promise.resolve(options.transport);\n } else if (this.platform === 'browser') {\n this.transportPromise = Promise.resolve(\n createBrowserTransport(options.endpoint, options.headers, options.signer),\n );\n } else {\n this.transportPromise = createNodeTransport(\n options.endpoint,\n options.headers,\n options.signer,\n );\n }\n\n if (options.storage) {\n this.storagePromise = Promise.resolve(options.storage);\n } else if (options.persist === false) {\n this.storagePromise = Promise.resolve(undefined);\n } else if (this.platform === 'browser') {\n this.storagePromise = createBrowserStorage(); // Dexie → localStorage 降级\n } else if (options.persistDir) {\n this.storagePromise = createNodeStorage(options.persistDir);\n } else {\n this.storagePromise = Promise.resolve(undefined); // Node 未配置 persistDir:仅内存\n }\n\n this.fire(this.init());\n }\n\n track(\n eventName: EventName,\n properties?: ReportNode,\n options?: { immediate?: boolean },\n ): string {\n if (this.disposed) {\n this.log('dispose 后忽略新事件:', eventName);\n return '';\n }\n\n if (typeof eventName !== 'string' || eventName.length === 0) {\n this.opts.onDropped?.([this.buildEvent('', properties)], 'invalid');\n return '';\n }\n\n // 活动刷新:会话超时则轮换 visitId,并结束旧页面访问(sessionTimeout)\n this.touchSession();\n\n const event = this.buildEvent(eventName, properties);\n\n if (options?.immediate) {\n // 立即发送:跳过攒批,失败仍走重试(重新入队)\n this.fire(this.sendBatch([{ ...event, attempts: 0 }]));\n return event.eventId;\n }\n\n this.enqueue(event);\n if (this.flushInterval <= 0) {\n // 关闭攒批:事件即时发送\n this.fire(this.flush());\n } else {\n this.scheduleFlush();\n }\n return event.eventId;\n }\n\n flush(): Promise<void> {\n if (!this.flushPromise) {\n // 显式冲刷:跳过退避立即重试(退避只约束自动重试)\n if (this.retryTimer) {\n clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n this.flushPromise = this.drain().finally(() => {\n this.flushPromise = null;\n });\n }\n return this.flushPromise;\n }\n\n get size(): number {\n return this.queue.length;\n }\n\n /** 匿名 id(只读):首次生成后持久化不变;供外部转化跟踪等场景读取 */\n get anonymousId(): string {\n return this._anonymousId;\n }\n\n /** 设备 / 安装实例 id(只读):Web&PC 与 google client id 一致;供外部转化跟踪等场景读取 */\n get clientId(): string {\n return this._clientId;\n }\n\n /** 当前会话 id(只读):会话轮换后返回新值;供外部转化跟踪等场景读取 */\n get visitId(): string {\n return this.session.visitId;\n }\n\n /** 当前登录账号 id(只读):实时调用 userId provider,未提供恒为 null */\n get userId(): string | null {\n return this.opts.userId?.() ?? null;\n }\n\n async dispose(): Promise<void> {\n if (this.disposed) return;\n this.disposed = true;\n\n if (this.flushTimer) {\n clearTimeout(this.flushTimer);\n this.flushTimer = null;\n }\n if (this.retryTimer) {\n clearTimeout(this.retryTimer);\n this.retryTimer = null;\n }\n if (this.persistTimer) {\n clearTimeout(this.persistTimer);\n this.persistTimer = null;\n }\n\n if (this.platform === 'browser') {\n window.removeEventListener('pagehide', this.onPageHide);\n window.removeEventListener('pageshow', this.onPageShow);\n window.removeEventListener('storage', this.onStorage);\n document.removeEventListener('visibilitychange', this.onVisibilityChange);\n this.restoreHistory();\n if (this.pageTracker) {\n // 兜底:仍有进行中的访问(应用主动销毁)按 unknown 结束\n this.endPageVisit(this.session.visitId, 'unknown');\n this.pageTracker.dispose();\n this.pageTracker = null;\n }\n this.saveSession(true);\n } else {\n const exitProcess = this.getExitProcess();\n exitProcess.off('beforeExit', this.onExit);\n exitProcess.off('SIGINT', this.onExit);\n exitProcess.off('SIGTERM', this.onExit);\n }\n\n const transport = await this.transportPromise;\n\n if (this.queue.length === 0) {\n // 队列已空:若持久化有脏标记,写空队列收敛,避免恢复已发送事件造成重复上报\n if (this.persistDirty) {\n await this.persistNow([]);\n }\n return;\n }\n\n if (this.flushOnExit) {\n const pending = this.queue.splice(0);\n const wire = stripAttempts(pending);\n // 优先同步尽力发送(sendBeacon,页面卸载场景 fetch 可能来不及)\n if (transport.sendSync?.(wire) ?? false) {\n this.opts.onFlushed?.(wire);\n await this.persistNow([]);\n return;\n }\n try {\n await transport.send(wire);\n this.opts.onFlushed?.(wire);\n await this.persistNow([]);\n } catch (error) {\n this.opts.onFailed?.(wire, error);\n this.queue.push(...pending); // 放回队列持久化,下次会话恢复重发\n await this.persistNow();\n }\n return;\n }\n\n // 退出兜底关闭:剩余队列仅持久化,下次启动恢复\n if (this.persistDirty) {\n await this.persistNow();\n }\n }\n\n /** 异步初始化:创建内置持久化、采集 Node 设备快照、恢复上次会话队列、挂载退出兜底 */\n private async init(): Promise<void> {\n this.storage = await this.storagePromise;\n if (this.platform === 'node') {\n this.nodeDevice = await collectNodeDevice();\n }\n if (this.disposed) return;\n\n if (this.storage) {\n try {\n const restored = await this.storage.load();\n if (restored.length > 0) {\n this.enqueueRestored(restored);\n }\n } catch (error) {\n this.log('restore pending queue failed:', error);\n }\n }\n if (this.disposed) return;\n\n if (this.platform === 'browser') {\n window.addEventListener('storage', this.onStorage);\n if (this.flushOnExit) {\n window.addEventListener('pagehide', this.onPageHide);\n }\n if (this.flushOnExit || this.opts.autoPageView) {\n document.addEventListener('visibilitychange', this.onVisibilityChange);\n }\n if (this.opts.autoPageView) {\n this.pageTracker = createPageTracker(() => this.touchSession());\n this.pageTracker.startVisit(); // SDK 初始化视为进入页面\n this.patchHistory();\n window.addEventListener('pageshow', this.onPageShow);\n }\n } else if (this.flushOnExit) {\n const exitProcess = this.getExitProcess();\n exitProcess.once('beforeExit', this.onExit);\n exitProcess.once('SIGINT', this.onExit);\n exitProcess.once('SIGTERM', this.onExit);\n }\n\n if (this.queue.length > 0) {\n this.scheduleFlush();\n this.schedulePersist();\n }\n }\n\n /** 组装上报事件:身份字段由 SDK 填充,业务节点 = SDK 采集 + context 合并(接入方值优先) */\n private buildEvent(\n eventName: EventName,\n properties?: ReportNode,\n overrides?: Partial<IReportEvent>,\n ): IReportEvent {\n const context = this.contextFn();\n const device = {\n ...this.nodeDevice,\n ...collectBrowserDevice(),\n ...(context.device ?? {}),\n };\n const page = {\n ...collectPageSnapshot(),\n ...this.pageSnapshot(),\n ...(context.page ?? {}),\n };\n const mergedProperties = { ...(context.properties ?? {}), ...(properties ?? {}) };\n\n if (!context.app) {\n this.log('context 未提供 app 节点,以空对象上报');\n }\n\n return {\n eventId: generateEventId(),\n eventName,\n userId: this.opts.userId?.() ?? null,\n anonymousId: this._anonymousId,\n visitId: this.session.visitId,\n clientId: this._clientId,\n timestamp: Date.now(),\n app: context.app ?? {},\n ...(Object.keys(device).length > 0 ? { device } : {}),\n ...(Object.keys(page).length > 0 ? { page } : {}),\n ...(context.user ? { user: context.user } : {}),\n ...(Object.keys(mergedProperties).length > 0\n ? { properties: mergedProperties }\n : {}),\n ...overrides,\n };\n }\n\n /** 进行中页面访问的 pageViewId(非访问期返回空) */\n private pageSnapshot(): ReportNode {\n if (!this.pageTracker?.active) return {};\n const pageViewId = this.pageTracker.pageViewId();\n return pageViewId ? { pageViewId } : {};\n }\n\n /** 会话活跃:track 调用与页面交互均触发;超时轮换 visitId 并结束旧页面访问 */\n private touchSession(): void {\n const now = Date.now();\n // 多 tab 共享会话:活动时定期重读共享值,避免本 tab 长期后台后误判超时\n if (now - this.sessionReadAt >= SESSION_SAVE_THROTTLE_MS) {\n this.sessionReadAt = now;\n this.refreshSessionFromShared();\n }\n if (now - this.session.lastActivityMs > VISIT_IDLE_TIMEOUT_MS) {\n // 会话超时:结束旧页面访问(复用进入页面时的旧 visitId),轮换新会话并开启新访问\n const oldVisitId = this.session.visitId;\n this.endPageVisit(oldVisitId, 'sessionTimeout');\n this.session = { visitId: this.newId('visit'), lastActivityMs: now };\n this.saveSession(true);\n this.pageTracker?.startVisit();\n } else {\n this.session.lastActivityMs = now;\n this.saveSession();\n }\n }\n\n /** 结束当前页面访问并上报 pageView 汇总事件(复用进入页面时的 visitId) */\n private endPageVisit(visitId: string, reason: PageEndReason): void {\n if (!this.pageTracker?.active) return;\n const summary = this.pageTracker.endVisit(reason);\n if (!summary) return;\n const context = this.contextFn();\n this.enqueue(\n this.buildEvent('pageView', undefined, {\n visitId,\n page: { ...summary, ...(context.page ?? {}) },\n }),\n );\n this.scheduleFlush();\n }\n\n /** SPA 路由变化检测:pushState / replaceState 视为 routeChange,popstate 视为 back */\n private patchHistory(): void {\n if (this.historyPatch) return;\n const endVisit = (reason: PageEndReason): void => {\n this.endPageVisit(this.session.visitId, reason);\n this.pageTracker?.startVisit();\n };\n const pushState = history.pushState.bind(history);\n const replaceState = history.replaceState.bind(history);\n history.pushState = function (\n ...args: Parameters<typeof history.pushState>\n ): void {\n const result = pushState(...args);\n endVisit('routeChange');\n return result;\n };\n history.replaceState = function (\n ...args: Parameters<typeof history.replaceState>\n ): void {\n const result = replaceState(...args);\n endVisit('routeChange');\n return result;\n };\n window.addEventListener('popstate', this.onPopState);\n this.historyPatch = { pushState, replaceState };\n }\n\n private restoreHistory(): void {\n if (!this.historyPatch) return;\n history.pushState = this.historyPatch.pushState;\n history.replaceState = this.historyPatch.replaceState;\n window.removeEventListener('popstate', this.onPopState);\n this.historyPatch = null;\n }\n\n /** 恢复队列入队:历史事件放队头保证先到先发,超限按溢出丢弃;过滤旧格式(无 eventId)事件 */\n private enqueueRestored(events: IQueuedEvent[]): void {\n let accepted = events.filter(\n (event) =>\n typeof event.eventId === 'string' &&\n typeof event.eventName === 'string',\n );\n if (accepted.length < events.length) {\n this.log(`跳过 ${events.length - accepted.length} 条旧格式持久化事件`);\n }\n accepted = accepted.map((event) => ({\n ...event,\n attempts: typeof event.attempts === 'number' ? event.attempts : 0,\n }));\n if (accepted.length > this.maxQueueSize) {\n this.opts.onDropped?.(accepted.slice(this.maxQueueSize), 'overflow');\n accepted = accepted.slice(0, this.maxQueueSize);\n }\n this.queue.unshift(...accepted);\n }\n\n private enqueue(event: IReportEvent): void {\n const queued: IQueuedEvent = { ...event, attempts: 0 };\n if (this.queue.length >= this.maxQueueSize) {\n if (this.overflowPolicy === 'drop-newest') {\n this.opts.onDropped?.([event], 'overflow');\n return;\n }\n if (this.overflowPolicy === 'drop-oldest') {\n this.opts.onDropped?.([this.queue.shift()!], 'overflow');\n } else {\n // flush-now:队列满时先冲刷腾位,不丢事件\n this.fire(this.flush());\n }\n }\n this.queue.push(queued);\n this.schedulePersist();\n }\n\n /** 攒批定时:延迟上报的实现,窗口内事件合并为一批 */\n private scheduleFlush(): void {\n if (this.flushTimer || this.disposed || this.flushInterval <= 0) return;\n this.flushTimer = setTimeout(() => {\n this.flushTimer = null;\n if (this.queue.length > 0) {\n this.fire(this.flush());\n }\n }, this.flushInterval);\n }\n\n /** 串行清空队列;失败批次重新入队后由 retryTimer 再次触发 drain */\n private async drain(): Promise<void> {\n while (!this.disposed && !this.retryTimer && this.queue.length > 0) {\n // eslint-disable-next-line no-await-in-loop\n await this.sendBatch(this.takeBatch());\n }\n }\n\n /** 取一批:受条数 / 字节数限制;首条必取,避免单事件超 maxBatchBytes 时队列卡死 */\n private takeBatch(): IQueuedEvent[] {\n const batch: IQueuedEvent[] = [];\n let bytes = 0;\n while (this.queue.length > 0 && batch.length < this.maxBatchSize) {\n const size = JSON.stringify(this.queue[0]).length;\n if (batch.length > 0 && bytes + size > this.maxBatchBytes) break;\n batch.push(this.queue.shift()!);\n bytes += size;\n }\n return batch;\n }\n\n private async sendBatch(batch: IQueuedEvent[]): Promise<void> {\n const transport = await this.transportPromise;\n const wire = stripAttempts(batch);\n try {\n await transport.send(wire);\n this.opts.onFlushed?.(wire);\n this.schedulePersist(); // 该批已移出队列,持久化剩余\n } catch (error) {\n this.opts.onFailed?.(wire, error);\n this.log('send batch failed:', error);\n\n const updated = batch.map((event) => ({\n ...event,\n attempts: event.attempts + 1,\n }));\n const retryable = updated.filter(\n (event) => event.attempts < this.maxAttempts,\n );\n const dead = updated.filter(\n (event) => event.attempts >= this.maxAttempts,\n );\n if (dead.length > 0) {\n this.opts.onDropped?.(dead, 'max-attempts');\n }\n if (retryable.length > 0) {\n this.queue.unshift(...retryable); // 保持原顺序放回队头\n this.schedulePersist();\n this.scheduleRetry(retryable);\n }\n }\n }\n\n /** 失败退避:按批次内最大 attempts 计 2^n 指数增长,避免失败批次频繁重试 */\n private scheduleRetry(retryable: IQueuedEvent[]): void {\n if (this.retryTimer) return;\n const maxAttemptsInBatch = Math.max(\n ...retryable.map((event) => event.attempts),\n );\n const delay = this.backoffMs * 2 ** (maxAttemptsInBatch - 1);\n this.retryTimer = setTimeout(() => {\n this.retryTimer = null;\n this.fire(this.flush());\n }, delay);\n }\n\n /** 持久化节流:窗口内多次变更只落盘一次,避免每次入队全量序列化 */\n private schedulePersist(): void {\n if (!this.storage) return;\n this.persistDirty = true;\n if (this.persistTimer) return;\n this.persistTimer = setTimeout(() => {\n this.persistTimer = null;\n if (!this.persistDirty) return;\n this.persistDirty = false;\n this.fire(this.persistNow());\n }, STORAGE_WRITE_THROTTLE_MS);\n }\n\n private async persistNow(\n events: IQueuedEvent[] = this.queue,\n ): Promise<void> {\n if (!this.storage) return;\n try {\n await this.storage.save(events);\n } catch (error) {\n this.log('persist queue failed:', error);\n }\n }\n\n // ============================================================\n // 身份与会话持久化(browser: localStorage;Node: 进程内)。\n // 会话存 localStorage 而非 sessionStorage:同源多 tab 共享一个会话,\n // \"连续 30 分钟无活动\"是用户级语义,不应按 tab 拆分;跨 tab 同步靠\n // storage 事件(实时)+ 活动时重读(兜底)。\n // ============================================================\n\n /** 匿名 id:localStorage 持久化,首次匿名访问生成后保留;Node 无持久化介质,进程内生成 */\n private loadOrCreateAnonymousId(): string {\n const stored = this.readPersistent(ANONYMOUS_ID_KEY);\n if (stored) return stored;\n const fresh = this.newId('anonymous');\n this.writePersistent(ANONYMOUS_ID_KEY, fresh);\n return fresh;\n }\n\n /** clientId:Web&PC 与 google client id(_ga cookie)一致;否则本地生成并持久化 */\n private loadOrCreateClientId(): string {\n if (typeof document !== 'undefined') {\n const ga = document.cookie.match(/(?:^|;\\s*)_ga=GA1\\.\\d\\.([^;]+)/);\n if (ga?.[1]) return ga[1];\n const stored = this.readPersistent(CLIENT_ID_KEY);\n if (stored) return stored;\n }\n // TODO: Node 端跨重启持久化(如 persistDir 落盘),当前进程内生成\n const fresh = this.newId('client');\n this.writePersistent(CLIENT_ID_KEY, fresh);\n return fresh;\n }\n\n /** 会话恢复:localStorage(同源多 tab 共享)中 30 分钟窗口内的会话复用,否则新建 */\n private loadOrCreateSession(): ISession {\n const now = Date.now();\n try {\n const text = this.readPersistent(SESSION_KEY);\n if (text) {\n const stored = JSON.parse(text) as ISession;\n if (\n stored.visitId &&\n now - stored.lastActivityMs <= VISIT_IDLE_TIMEOUT_MS\n ) {\n return stored;\n }\n }\n } catch {\n // 解析失败:新建会话\n }\n const fresh = { visitId: this.newId('visit'), lastActivityMs: now };\n this.session = fresh;\n this.saveSession(true);\n return fresh;\n }\n\n /** 会话落盘(localStorage):节流 10s,避免滚动等高频活动频繁写;force 用于轮换 / 退出兜底 */\n private saveSession(force = false): void {\n const now = Date.now();\n if (\n !force &&\n this.sessionSaveAt > 0 &&\n now - this.sessionSaveAt < SESSION_SAVE_THROTTLE_MS\n ) {\n return;\n }\n this.sessionSaveAt = now;\n this.writePersistent(SESSION_KEY, JSON.stringify(this.session));\n }\n\n /** 采纳共享会话:visitId 变化视为其他 tab 轮换了会话,结束本 tab 旧页面访问并开启新访问 */\n private adoptSharedSession(stored: ISession): void {\n if (!stored.visitId || stored.visitId === this.session.visitId) {\n if (stored.visitId) this.session = stored; // 仅活跃时间更新\n return;\n }\n const oldVisitId = this.session.visitId;\n this.endPageVisit(oldVisitId, 'sessionTimeout');\n this.pageTracker?.startVisit();\n this.session = stored;\n }\n\n /** 活动时重读共享会话(兜底 storage 事件丢失):storage 事件只在其他 tab 触发 */\n private refreshSessionFromShared(): void {\n try {\n const text = this.readPersistent(SESSION_KEY);\n if (text) {\n this.adoptSharedSession(JSON.parse(text) as ISession);\n }\n } catch {\n // 解析失败:忽略,保持内存会话\n }\n }\n\n private readPersistent(key: string): string | null {\n // 本地持久化仅浏览器生效,Node 端进程内生成;按平台短路,\n // 避免命中 Node ≥22 实验性 localStorage 全局(引用即触发告警)\n if (this.platform !== 'browser') return null;\n try {\n return localStorage.getItem(key);\n } catch {\n return null; // 隐私模式 / 配额限制:退化为进程内生成\n }\n }\n\n private writePersistent(key: string, value: string): void {\n if (this.platform !== 'browser') return;\n try {\n localStorage.setItem(key, value);\n } catch {\n // 不可写:静默退化\n }\n }\n\n /** 生成带前缀的实例 id(格式对齐旧 SDK session_ / device_) */\n private newId(prefix: string): string {\n return `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;\n }\n\n private log(...args: unknown[]): void {\n if (this.opts.debug) {\n // eslint-disable-next-line no-console\n console.log('[reporter]', ...args);\n }\n }\n\n /**\n * Node 进程退出事件句柄:Electron 渲染进程 typings 里 process 只声明了 'loaded'\n * 事件,与 @types/node 不一致,显式收窄到本 SDK 用到的信号,兼容两端编译器\n */\n private getExitProcess(): ExitProcessHandle {\n return process as unknown as ExitProcessHandle;\n }\n\n /** 安全触发异步任务:吞掉 rejection,避免未处理异常 */\n private fire(promise: Promise<unknown>): void {\n promise.catch((error) => {\n this.log('async task failed:', error);\n });\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  declare class Reporter implements IReporter {
2
2
  private readonly opts;
3
3
  private readonly platform;
4
+ private readonly contextFn;
4
5
  private readonly flushInterval;
5
6
  private readonly maxBatchSize;
6
7
  private readonly maxBatchBytes;
@@ -11,9 +12,22 @@ declare class Reporter implements IReporter {
11
12
  private readonly flushOnExit;
12
13
  private readonly transportPromise;
13
14
  private readonly storagePromise;
14
- /** 会话 / 设备 id 兜底:enrich 未提供时按实例生成(对齐旧 SDK 格式) */
15
- private readonly sessionId;
16
- private readonly deviceId;
15
+ /** 匿名 id:首次生成后持久化(浏览器 localStorage / Node 进程内);对外只读访问见同名 getter */
16
+ private readonly _anonymousId;
17
+ /** 设备 / 安装实例 id:Web&PC 读取 google client id(_ga),否则生成并持久化;Node 进程内生成;对外只读访问见同名 getter */
18
+ private readonly _clientId;
19
+ /** 会话落盘时间戳(节流用);声明在 session 之前:session 初始化会触发强制落盘 */
20
+ private sessionSaveAt;
21
+ /** 会话共享读时间戳(节流用):多 tab 共享会话,活动时定期重读 localStorage 最新值 */
22
+ private sessionReadAt;
23
+ /** 会话:visitId 与最近活跃时间(30 分钟无活动轮换);localStorage 持久化,同源多 tab 共享 */
24
+ private session;
25
+ /** Node 端设备快照(init 时采集一次,避免每次入队动态加载 os) */
26
+ private nodeDevice;
27
+ /** 页面访问跟踪(browser + autoPageView 时启用) */
28
+ private pageTracker;
29
+ /** history 补丁的原始引用(dispose 时还原) */
30
+ private historyPatch;
17
31
  /** 待发队列(内存中的事实源,持久化是它的影子) */
18
32
  private readonly queue;
19
33
  /** init 完成后指向内置 / 注入的持久化实现 */
@@ -26,19 +40,39 @@ declare class Reporter implements IReporter {
26
40
  private disposed;
27
41
  private readonly onExit;
28
42
  private readonly onPageHide;
43
+ private readonly onPageShow;
29
44
  private readonly onVisibilityChange;
45
+ private readonly onPopState;
46
+ private readonly onStorage;
30
47
  constructor(options: IReporterOptions);
31
- track(name: string, properties?: Record<string, unknown>, options?: {
48
+ track(eventName: EventName, properties?: ReportNode, options?: {
32
49
  immediate?: boolean;
33
- timestamp?: number;
34
- category?: string;
35
50
  }): string;
36
51
  flush(): Promise<void>;
37
52
  get size(): number;
53
+ /** 匿名 id(只读):首次生成后持久化不变;供外部转化跟踪等场景读取 */
54
+ get anonymousId(): string;
55
+ /** 设备 / 安装实例 id(只读):Web&PC 与 google client id 一致;供外部转化跟踪等场景读取 */
56
+ get clientId(): string;
57
+ /** 当前会话 id(只读):会话轮换后返回新值;供外部转化跟踪等场景读取 */
58
+ get visitId(): string;
59
+ /** 当前登录账号 id(只读):实时调用 userId provider,未提供恒为 null */
60
+ get userId(): string | null;
38
61
  dispose(): Promise<void>;
39
- /** 异步初始化:创建内置持久化、恢复上次会话队列、挂载退出兜底 */
62
+ /** 异步初始化:创建内置持久化、采集 Node 设备快照、恢复上次会话队列、挂载退出兜底 */
40
63
  private init;
41
- /** 恢复队列入队:历史事件放队头保证先到先发,超限按溢出丢弃 */
64
+ /** 组装上报事件:身份字段由 SDK 填充,业务节点 = SDK 采集 + context 合并(接入方值优先) */
65
+ private buildEvent;
66
+ /** 进行中页面访问的 pageViewId(非访问期返回空) */
67
+ private pageSnapshot;
68
+ /** 会话活跃:track 调用与页面交互均触发;超时轮换 visitId 并结束旧页面访问 */
69
+ private touchSession;
70
+ /** 结束当前页面访问并上报 pageView 汇总事件(复用进入页面时的 visitId) */
71
+ private endPageVisit;
72
+ /** SPA 路由变化检测:pushState / replaceState 视为 routeChange,popstate 视为 back */
73
+ private patchHistory;
74
+ private restoreHistory;
75
+ /** 恢复队列入队:历史事件放队头保证先到先发,超限按溢出丢弃;过滤旧格式(无 eventId)事件 */
42
76
  private enqueueRestored;
43
77
  private enqueue;
44
78
  /** 攒批定时:延迟上报的实现,窗口内事件合并为一批 */
@@ -53,6 +87,22 @@ declare class Reporter implements IReporter {
53
87
  /** 持久化节流:窗口内多次变更只落盘一次,避免每次入队全量序列化 */
54
88
  private schedulePersist;
55
89
  private persistNow;
90
+ /** 匿名 id:localStorage 持久化,首次匿名访问生成后保留;Node 无持久化介质,进程内生成 */
91
+ private loadOrCreateAnonymousId;
92
+ /** clientId:Web&PC 与 google client id(_ga cookie)一致;否则本地生成并持久化 */
93
+ private loadOrCreateClientId;
94
+ /** 会话恢复:localStorage(同源多 tab 共享)中 30 分钟窗口内的会话复用,否则新建 */
95
+ private loadOrCreateSession;
96
+ /** 会话落盘(localStorage):节流 10s,避免滚动等高频活动频繁写;force 用于轮换 / 退出兜底 */
97
+ private saveSession;
98
+ /** 采纳共享会话:visitId 变化视为其他 tab 轮换了会话,结束本 tab 旧页面访问并开启新访问 */
99
+ private adoptSharedSession;
100
+ /** 活动时重读共享会话(兜底 storage 事件丢失):storage 事件只在其他 tab 触发 */
101
+ private refreshSessionFromShared;
102
+ private readPersistent;
103
+ private writePersistent;
104
+ /** 生成带前缀的实例 id(格式对齐旧 SDK session_ / device_) */
105
+ private newId;
56
106
  private log;
57
107
  /**
58
108
  * Node 进程退出事件句柄:Electron 渲染进程 typings 里 process 只声明了 'loaded'
@@ -64,9 +114,9 @@ declare class Reporter implements IReporter {
64
114
  }
65
115
 
66
116
  /** 浏览器发送:fetch(keepalive)走正常路径;sendBeacon 仅退出兜底(sendSync) */
67
- declare function createBrowserTransport(endpoint: string, headers?: Record<string, string>): IReportTransport;
117
+ declare function createBrowserTransport(endpoint: string, headers?: Record<string, string>, signer?: IReportSigner): IReportTransport;
68
118
  /** Node 发送:动态加载项目现有依赖 axios(仅在 Node 路径执行) */
69
- declare function createNodeTransport(endpoint: string, headers?: Record<string, string>): Promise<IReportTransport>;
119
+ declare function createNodeTransport(endpoint: string, headers?: Record<string, string>, signer?: IReportSigner): Promise<IReportTransport>;
70
120
  /** 降级实现:localStorage(容量有限,仅兜底) */
71
121
  declare function createLocalStorageStorage(): IReportStorage;
72
122
  /** 浏览器持久化:优先 Dexie(IndexedDB),不可用时降级 localStorage */
@@ -76,42 +126,70 @@ declare function createNodeStorage(persistDir: string): Promise<IReportStorage>;
76
126
 
77
127
  /** 事件被丢弃的原因 */
78
128
  type DropReason = 'max-attempts' | 'overflow' | 'invalid';
79
- /** 上报事件(track 入队后的形态;顶层字段对齐旧 SDK TrackedEvent) */
129
+ /** 签名函数:对序列化后的上报信封生成签名等附加键值( X-Clipto-Timestamp / X-Clipto-Signature),返回值作为请求头随包发送,服务端校验防止伪造数据污染;算法与密钥由接入方决定(如 WebCrypto HMAC);可同步或异步返回 */
130
+ type IReportSigner = (payload: string) => Record<string, string> | Promise<Record<string, string>>;
131
+ /** 业务节点:app / device / page / user / properties 的统一形态;具体字段与取值由接入方埋点方案约束,SDK 不感知语义、原样透传 */
132
+ type ReportNode = Record<string, unknown>;
133
+ /** 业务上下文:统一承载各业务节点;可传静态对象,或传函数在每次入队时实时计算(读取最新登录态 / 页面状态) */
134
+ interface IEventContext {
135
+ /** 应用信息:产品标识 / 环境 / 平台 / 版本 / 渠道归因等,由接入方配置 */
136
+ app?: ReportNode;
137
+ /** 设备信息:与 SDK 自动采集的硬件 / 系统 / 浏览器信息在同一节点内按字段合并,接入方提供的字段值优先;ip / country 等由服务端填充 */
138
+ device?: ReportNode;
139
+ /** 页面信息:与 SDK 自动采集的页面快照合并;pageId 等接入方定义字段在此提供 */
140
+ page?: ReportNode;
141
+ /** 用户信息:账号 / 订阅 / 权益 / 实验分组等,由接入方账号体系提供 */
142
+ user?: ReportNode;
143
+ /** 合并进每个事件 properties 的公共业务字段;track() 传入的 properties 优先级更高 */
144
+ properties?: ReportNode;
145
+ }
146
+ /** 事件名:接入方埋点方案定义的稳定事件名,同一行为跨 Web、iOS、Android、Mac、Windows 尽量同名;以下为 SDK 预置事件名,业务事件可继续扩展 */
147
+ type EventName = 'pageView' | 'elementExposure' | 'user_action' | (string & {});
148
+ /** 上报事件(网络信封结构:身份字段由 SDK 生成,业务节点由接入方 context 提供或 SDK 自动采集) */
80
149
  interface IReportEvent {
81
- /** 核心生成的唯一 id */
82
- id: string;
83
- /** 事件名(上报信封中序列化为 eventName,对齐旧 SDK) */
84
- name: string;
85
- /** 业务属性(baseProperties context 已合并) */
86
- properties: Record<string, unknown>;
87
- /** 事件时间(ms) */
150
+ /** SDK 自动生成:每条事件唯一 ID,用于网络重试与服务端幂等去重;同一事件重试时复用原值 */
151
+ eventId: string;
152
+ /** 埋点方案定义的稳定事件名,研发接入;同一行为跨端尽量同名 */
153
+ eventName: string;
154
+ /** 登录账号 id:账号系统提供、SDK 自动读取;未登录为 null,登录后携带,退出后恢复 null;接入方不手工填写 */
155
+ userId: string | null;
156
+ /** 匿名 id:SDK 自动生成,首次匿名访问 / 安装时生成,登录后继续保留;清浏览器数据、换浏览器或重装 App 后可能生成新值 */
157
+ anonymousId: string;
158
+ /** 会话 id:SDK 会话模块生成;页面切换保持不变,同源多 tab 共享一个会话(任一 tab 活动即刷新活跃时间);连续 30 分钟无活动、退出或切换账号后,下一次活动生成新值 */
159
+ visitId: string;
160
+ /** 设备 / 安装实例 id:SDK 自动生成并持久化;Web&PC 与 google client id 一致,iOS&Android 为 IDFV */
161
+ clientId: string;
162
+ /** 事件实际发生的毫秒时间戳,由客户端时钟产生(SDK 自动生成) */
88
163
  timestamp: number;
89
- /** 已尝试发送次数(SDK 内部字段,不上报) */
164
+ /** 应用信息(接入方 context 提供,节点始终存在,内容由埋点方案约束) */
165
+ app: ReportNode;
166
+ /** 设备信息(SDK 自动采集 + context 合并) */
167
+ device?: ReportNode;
168
+ /** 页面信息(SDK 自动采集 + context 合并) */
169
+ page?: ReportNode;
170
+ /** 用户信息(接入方 context 提供) */
171
+ user?: ReportNode;
172
+ /** 业务事件属性(context.properties 与 track properties 合并) */
173
+ properties?: ReportNode;
174
+ }
175
+ /** 队列内部事件:上报事件 + SDK 内部字段(序列化上报前剥离) */
176
+ interface IQueuedEvent extends IReportEvent {
177
+ /** 已尝试发送次数,达到 maxAttempts 即丢弃 */
90
178
  attempts: number;
91
- /** 会话 id(enrich 提供,否则核心按实例生成) */
92
- sessionId?: string;
93
- /** 用户 id(enrich 提供,否则 null) */
94
- userId?: string | null;
95
- /** 设备 id(enrich 提供,否则核心按实例生成) */
96
- deviceId?: string;
97
- /** 事件类别(enrich / track options 提供,默认 'custom') */
98
- category?: string;
99
- /** 采集上下文:页面 / 用户 / 浏览器 / 屏幕 / 视口(enrich 提供,否则空对象) */
100
- context?: Record<string, unknown>;
101
179
  }
102
180
  /** 发送适配器:如何把一批事件发出去(浏览器: sendBeacon → fetch;Node: https / axios) */
103
181
  interface IReportTransport {
104
- /** 发送一批事件;resolve 视为成功,reject 进入重试 */
182
+ /** 发送一批事件(纯上报结构,内部字段如 attempts 已由核心剥离);resolve 视为成功,reject 进入重试 */
105
183
  send(events: IReportEvent[]): Promise<void>;
106
184
  /** 退出兜底时的同步尽力发送(浏览器端为 navigator.sendBeacon);返回是否已受理 */
107
185
  sendSync?(events: IReportEvent[]): boolean;
108
186
  }
109
187
  /** 持久化适配器:待发队列跨页面刷新 / 进程重启存活(浏览器 IndexedDB / Node conf) */
110
188
  interface IReportStorage {
111
- /** 整体覆盖保存当前待发队列(实现需幂等;写入节流由核心负责) */
112
- save(events: IReportEvent[]): void | Promise<void>;
189
+ /** 整体覆盖保存当前待发队列(含重试计数,恢复后继续计次;实现需幂等;写入节流由核心负责) */
190
+ save(events: IQueuedEvent[]): void | Promise<void>;
113
191
  /** 读取上次保存的待发队列 */
114
- load(): IReportEvent[] | Promise<IReportEvent[]>;
192
+ load(): IQueuedEvent[] | Promise<IQueuedEvent[]>;
115
193
  /** 清空 */
116
194
  clear(): void | Promise<void>;
117
195
  }
@@ -119,6 +197,9 @@ interface IReportStorage {
119
197
  interface IReporterOptions {
120
198
  /** 上报端点,必填(浏览器与 Node 共用) */
121
199
  endpoint: string;
200
+ /** 业务上下文:统一提供 app / device / page / user / properties 业务节点;静态对象或实时函数(每次入队时调用);
201
+ * 与 SDK 自动采集的 device / page 在同一节点内按字段合并,接入方提供的字段值优先 */
202
+ context?: IEventContext | (() => IEventContext);
122
203
  /** 运行平台:显式声明,驱动内置发送 / 持久化实现;缺省自动探测(typeof window) */
123
204
  platform?: 'browser' | 'node';
124
205
  /** 攒批窗口(ms),即延迟上报的固定间隔;0 表示关闭批量,事件即时发送 */
@@ -135,22 +216,23 @@ interface IReporterOptions {
135
216
  maxAttempts?: number;
136
217
  /** 失败重发退避基础延迟(ms),按 2^n 指数增长 */
137
218
  backoffMs?: number;
138
- /** 合并进每个事件 properties 的公共业务字段(替代旧 SDK base_properties) */
139
- baseProperties?: Record<string, unknown>;
140
- /** 上下文采集函数,每次入队时实时调用并合并进 properties(替代旧 SDK 硬编码的页面/浏览器/设备信息) */
141
- context?: () => Record<string, unknown>;
142
- /** 逐事件顶层字段补充函数(sessionId/userId/deviceId/context),每次入队时实时调用,缺省值由核心兜底 */
143
- enrich?: () => Partial<Pick<IReportEvent, 'sessionId' | 'userId' | 'deviceId' | 'context'>>;
219
+ /** 登录账号 id 提供函数:每次入队时实时读取(由接入方账号体系提供);未提供时恒为 null */
220
+ userId?: () => string | null;
144
221
  /** 页面卸载 / 进程退出时兜底 flush(浏览器端走 sendBeacon) */
145
222
  flushOnExit?: boolean;
146
223
  /** 浏览器端:队列是否持久化到本地(优先 IndexedDB,不支持时降级 localStorage) */
147
224
  persist?: boolean;
148
225
  /** Node 端:持久化目录,设置后待发队列落盘(跨进程重启存活);不传仅内存 */
149
226
  persistDir?: string;
150
- /** 浏览器端:自动上报 page_view(初始化 + 页面重新可见) */
227
+ /** 浏览器端:自动管理页面访问并上报 pageView(进入页面建立访问上下文,离开页面时汇总上报) */
151
228
  autoPageView?: boolean;
152
229
  /** 自定义请求头(内置传输实现使用) */
153
230
  headers?: Record<string, string>;
231
+ /** 可选签名:每个批次发送前对序列化信封调用 signer,返回的键值对(通常为 X-Clipto-Timestamp / X-Clipto-Signature 等签名头)作为请求头随包发送,服务端校验防伪造/污染;
232
+ * payload 为信封 JSON 序列化(含 metadata.timestamp,可作签名时间戳);
233
+ * 仅内置 transport 生效(自定义 transport 自行处理);sendBeacon 不支持自定义请求头,
234
+ * 配置 signer 后退出兜底自动回退为带签名头的异步发送(keepalive fetch) */
235
+ signer?: IReportSigner;
154
236
  /** 打开调试日志 */
155
237
  debug?: boolean;
156
238
  /** 可选注入:发送适配器;不传按运行环境内置 */
@@ -166,17 +248,22 @@ interface IReporterOptions {
166
248
  }
167
249
  /** 公共接口 */
168
250
  interface IReporter {
169
- /** 上报一条事件并返回事件 id(dispose 后或非法事件返回空串);immediate 事件跳过攒批立即发送 */
170
- track(name: string, properties?: Record<string, unknown>, options?: {
251
+ /** 上报一条事件并返回 eventId(dispose 后或非法事件返回空串);immediate 事件跳过攒批立即发送;事件名见 EventName */
252
+ track(eventName: EventName, properties?: ReportNode, options?: {
171
253
  immediate?: boolean;
172
- timestamp?: number;
173
- /** 事件类别(顶层字段,默认 'custom') */
174
- category?: string;
175
254
  }): string;
176
255
  /** 立即冲刷队列(跳过退避等待),并等待在途批次完成 */
177
256
  flush(): Promise<void>;
178
257
  /** 队列中待发条数 */
179
258
  readonly size: number;
259
+ /** 当前登录账号 id:实时调用 userId provider;未提供时恒为 null */
260
+ readonly userId: string | null;
261
+ /** 匿名 id:首次生成后持久化不变;供外部转化跟踪等场景读取 */
262
+ readonly anonymousId: string;
263
+ /** 设备 / 安装实例 id:Web&PC 与 google client id 一致;供外部转化跟踪等场景读取 */
264
+ readonly clientId: string;
265
+ /** 当前会话 id:会话轮换后返回新值(同源多 tab 共享);供外部转化跟踪等场景读取 */
266
+ readonly visitId: string;
180
267
  /** 停止并兜底 flush(flushOnExit),之后拒绝新事件 */
181
268
  dispose(): Promise<void>;
182
269
  }
@@ -185,4 +272,4 @@ interface IReporterConstructor {
185
272
  new (options: IReporterOptions): IReporter;
186
273
  }
187
274
 
188
- export { type DropReason, type IReportEvent, type IReportStorage, type IReportTransport, type IReporter, type IReporterConstructor, type IReporterOptions, Reporter, createBrowserStorage, createBrowserTransport, createLocalStorageStorage, createNodeStorage, createNodeTransport };
275
+ export { type DropReason, type EventName, type IEventContext, type IQueuedEvent, type IReportEvent, type IReportSigner, type IReportStorage, type IReportTransport, type IReporter, type IReporterConstructor, type IReporterOptions, type ReportNode, Reporter, createBrowserStorage, createBrowserTransport, createLocalStorageStorage, createNodeStorage, createNodeTransport };