@clipto/reporter 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clipto, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # @clipto/reporter
2
+
3
+ 通用数据上报 SDK:攒批、重试、有界队列、崩溃恢复,浏览器与 Node 双端通用。
4
+
5
+ - **浏览器端**:`fetch(keepalive)` 发送 + `sendBeacon` 退出兜底,队列持久化到 IndexedDB(Dexie,不支持时降级 localStorage)
6
+ - **Node 端**:axios 发送,队列持久化到 JSON 文件(conf,内部原子写,`persistDir` 配置后启用)
7
+ - **内置组件**:不手写底层 IO,双端差异仅发送与持久化,核心(入队 / 攒批 / 重试 / 溢出策略)平台无关
8
+ - 依赖按需动态加载:浏览器不会加载 axios / conf,Node 不会加载 Dexie
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ npm install @clipto/reporter
14
+ ```
15
+
16
+ ## 快速上手
17
+
18
+ ### 浏览器
19
+
20
+ ```ts
21
+ import { Reporter } from '@clipto/reporter';
22
+
23
+ const reporter = new Reporter({
24
+ endpoint: 'https://reporter.example.com/api/events',
25
+ baseProperties: { productId: 'my.app' },
26
+ });
27
+
28
+ reporter.track('user_action', { action: 'click', eid: 'btn' });
29
+ reporter.flush(); // 立即冲刷(跳过退避),页面卸载时 SDK 自动走 sendBeacon 兜底
30
+ ```
31
+
32
+ ### Node
33
+
34
+ ```ts
35
+ import { Reporter } from '@clipto/reporter';
36
+
37
+ const reporter = new Reporter({
38
+ endpoint: 'https://reporter.example.com/api/events',
39
+ platform: 'node', // 不传则按 typeof window 自动探测
40
+ persistDir: './data', // 配置后待发队列落盘,进程重启后恢复重发;不传仅内存
41
+ });
42
+
43
+ reporter.track('conversion', { conversionType: 'purchase', value: 99 });
44
+ await reporter.dispose(); // 停止并兜底 flush(进程退出信号自动触发)
45
+ ```
46
+
47
+ ### 双端注入自定义实现
48
+
49
+ ```ts
50
+ new Reporter({
51
+ endpoint,
52
+ transport: { send: (events) => http.post(endpoint, events) }, // 自定义发送
53
+ storage: { save, load, clear }, // 自定义持久化
54
+ });
55
+ ```
56
+
57
+ ## 配置项
58
+
59
+ | 配置 | 默认 | 说明 |
60
+ | --- | --- | --- |
61
+ | `endpoint` | 必填 | 上报端点 |
62
+ | `platform` | 自动探测 | `'browser'` / `'node'`,驱动内置发送与持久化实现 |
63
+ | `flushInterval` | 2000 | 攒批窗口(ms),0 表示关闭批量即时发送 |
64
+ | `maxBatchSize` | 10 | 单批最大条数,达到即刷 |
65
+ | `maxBatchBytes` | 64KB | 单批最大字节数,达到即刷 |
66
+ | `maxQueueSize` | 1000 | 队列最大条数 |
67
+ | `overflowPolicy` | `'drop-oldest'` | 队列溢出:`'drop-oldest'` / `'drop-newest'` / `'flush-now'` |
68
+ | `maxAttempts` | 3 | 单事件最大发送尝试次数,超限丢弃 |
69
+ | `backoffMs` | 1000 | 失败重发退避基数(ms),按 2^n 指数增长;`flush()` 跳过退避 |
70
+ | `baseProperties` | `{}` | 合并进每个事件 properties 的公共业务字段 |
71
+ | `context` | 无 | 上下文采集函数,每次入队实时调用并合并进 properties |
72
+ | `enrich` | 无 | 逐事件顶层字段补充(`sessionId` / `userId` / `deviceId` / `context`),每次入队实时调用 |
73
+ | `flushOnExit` | true | 退出兜底 flush(浏览器 sendBeacon / Node 进程信号) |
74
+ | `persist` | true | 浏览器端:队列持久化到 IndexedDB |
75
+ | `persistDir` | 无 | Node 端:持久化目录,设置后落盘 |
76
+ | `autoPageView` | false | 浏览器端:自动上报 page_view(初始化 + 页面重新可见) |
77
+ | `headers` | 无 | 自定义请求头(内置传输实现使用) |
78
+ | `debug` | false | 调试日志 |
79
+ | `transport` / `storage` | 无 | 注入自定义发送 / 持久化实现 |
80
+ | `onFlushed` / `onFailed` / `onDropped` | 无 | 批次成功 / 失败 / 事件丢弃(重试耗尽 / 溢出 / 非法)钩子 |
81
+
82
+ ## API
83
+
84
+ ```ts
85
+ interface IReporter {
86
+ /** 上报一条事件,返回事件 id(dispose 后或非法事件返回空串);immediate 跳过攒批立即发送 */
87
+ track(name: string, properties?: Record<string, unknown>, options?: {
88
+ immediate?: boolean;
89
+ timestamp?: number;
90
+ category?: string; // 事件类别顶层字段,默认 'custom'
91
+ }): string;
92
+ /** 立即冲刷队列(跳过退避等待),并等待在途批次完成 */
93
+ flush(): Promise<void>;
94
+ /** 队列中待发条数 */
95
+ readonly size: number;
96
+ /** 停止并兜底 flush(flushOnExit),之后拒绝新事件 */
97
+ dispose(): Promise<void>;
98
+ }
99
+ ```
100
+
101
+ ## 事件数据结构
102
+
103
+ 事件顶层字段与历史 SDK(renderer `event-tracker-sdk`)保持一致,上报信封:
104
+
105
+ ```jsonc
106
+ {
107
+ "events": [
108
+ {
109
+ "id": "evt_...",
110
+ "eventName": "user_action", // SDK 内部字段名 name,序列化输出为 eventName
111
+ "timestamp": 1756200000000,
112
+ "sessionId": "session_...", // enrich 提供,缺省核心按实例生成
113
+ "userId": null, // enrich 提供,缺省 null
114
+ "deviceId": "device_...", // enrich 提供,缺省核心按实例生成
115
+ "properties": { /* baseProperties + context + 业务属性 */ },
116
+ "category": "custom", // track options 提供,默认 'custom'
117
+ "context": {} // enrich 提供(页面 / 用户 / 浏览器 / 屏幕 / 视口)
118
+ }
119
+ ],
120
+ "metadata": { "sdkVersion": "1.0.0", "timestamp": 1756200000000, "batchId": "evt_..." }
121
+ }
122
+ ```
123
+
124
+ 事件 id 与 batchId 均为 `evt_${时间戳}_${随机串}` 格式;`attempts` 为 SDK 内部字段,不上报。
125
+
126
+ ## 注意事项
127
+
128
+ - **conf 与打包器**:Node 持久化依赖 `conf`(含 Node 内置模块),源码中 `import('conf')` 带 `webpackIgnore` 注释,消费方 webpack 等打包器会跳过它,由 Node 运行时直接解析;浏览器端永不执行该分支。
129
+ - **依赖按需加载**:`axios` / `conf` 仅在 Node 分支动态加载,`dexie` 仅在浏览器分支动态加载。
130
+ - **dispose 后不可再用**:`dispose()` 执行退出兜底 flush 并拒绝新事件(返回空串)。
package/dist/index.cjs ADDED
@@ -0,0 +1,557 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Reporter: () => Reporter,
34
+ createBrowserStorage: () => createBrowserStorage,
35
+ createBrowserTransport: () => createBrowserTransport,
36
+ createLocalStorageStorage: () => createLocalStorageStorage,
37
+ createNodeStorage: () => createNodeStorage,
38
+ createNodeTransport: () => createNodeTransport
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/id.ts
43
+ function generateEventId() {
44
+ return `evt_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
45
+ }
46
+
47
+ // src/builtins.ts
48
+ var SDK_VERSION = "1.0.0";
49
+ var DB_NAME = "CliptoReportDB";
50
+ var STORE_NAME = "clipto-reporting";
51
+ var QUEUE_KEY = "pending";
52
+ var LS_KEY = "clipto_report_pending_queue";
53
+ function buildPayload(events) {
54
+ return {
55
+ // attempts 为 SDK 内部字段,不上报;name 输出为 eventName(对齐旧 SDK)
56
+ events: events.map(
57
+ ({
58
+ id,
59
+ name,
60
+ timestamp,
61
+ sessionId,
62
+ userId,
63
+ deviceId,
64
+ properties,
65
+ category,
66
+ context
67
+ }) => ({
68
+ id,
69
+ eventName: name,
70
+ timestamp,
71
+ sessionId,
72
+ userId,
73
+ deviceId,
74
+ properties,
75
+ category,
76
+ context
77
+ })
78
+ ),
79
+ metadata: {
80
+ sdkVersion: SDK_VERSION,
81
+ timestamp: Date.now(),
82
+ batchId: generateEventId()
83
+ // 与事件 id 同格式(对齐旧 SDK)
84
+ }
85
+ };
86
+ }
87
+ function createBrowserTransport(endpoint, headers) {
88
+ const commonHeaders = {
89
+ "Content-Type": "application/json",
90
+ ...headers ?? {}
91
+ };
92
+ return {
93
+ async send(events) {
94
+ const response = await fetch(endpoint, {
95
+ method: "POST",
96
+ headers: commonHeaders,
97
+ body: JSON.stringify(buildPayload(events)),
98
+ keepalive: true
99
+ });
100
+ if (!response.ok) {
101
+ throw new Error(`report send failed: HTTP ${response.status}`);
102
+ }
103
+ },
104
+ sendSync(events) {
105
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") {
106
+ return false;
107
+ }
108
+ try {
109
+ const blob = new Blob([JSON.stringify(buildPayload(events))], {
110
+ type: "application/json"
111
+ });
112
+ return navigator.sendBeacon(endpoint, blob);
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+ };
118
+ }
119
+ async function createNodeTransport(endpoint, headers) {
120
+ const { default: axios } = await import("axios");
121
+ return {
122
+ async send(events) {
123
+ await axios.post(endpoint, buildPayload(events), {
124
+ headers,
125
+ timeout: 1e4
126
+ });
127
+ }
128
+ };
129
+ }
130
+ function createLocalStorageStorage() {
131
+ return {
132
+ save(events) {
133
+ try {
134
+ localStorage.setItem(LS_KEY, JSON.stringify(events));
135
+ } catch {
136
+ }
137
+ },
138
+ load() {
139
+ try {
140
+ const text = localStorage.getItem(LS_KEY);
141
+ return text ? JSON.parse(text) : [];
142
+ } catch {
143
+ return [];
144
+ }
145
+ },
146
+ clear() {
147
+ try {
148
+ localStorage.removeItem(LS_KEY);
149
+ } catch {
150
+ }
151
+ }
152
+ };
153
+ }
154
+ async function createBrowserStorage() {
155
+ try {
156
+ const { default: Dexie } = await import("dexie");
157
+ const db = new Dexie(DB_NAME);
158
+ db.version(1).stores({ queue: "key" });
159
+ await db.open();
160
+ const table = db.table(
161
+ "queue"
162
+ );
163
+ return {
164
+ async save(events) {
165
+ await table.put({ key: QUEUE_KEY, events });
166
+ },
167
+ async load() {
168
+ const row = await table.get(QUEUE_KEY);
169
+ return row ? row.events : [];
170
+ },
171
+ async clear() {
172
+ await table.delete(QUEUE_KEY);
173
+ }
174
+ };
175
+ } catch {
176
+ return createLocalStorageStorage();
177
+ }
178
+ }
179
+ async function createNodeStorage(persistDir) {
180
+ const { default: Conf } = await import(
181
+ /* webpackIgnore: true */
182
+ "conf"
183
+ );
184
+ const store = new Conf({
185
+ configName: STORE_NAME,
186
+ // 落盘为 persistDir/clipto-reporting.json
187
+ cwd: persistDir
188
+ });
189
+ return {
190
+ save(events) {
191
+ store.set(QUEUE_KEY, events);
192
+ },
193
+ load() {
194
+ return store.get(QUEUE_KEY) ?? [];
195
+ },
196
+ clear() {
197
+ store.delete(QUEUE_KEY);
198
+ }
199
+ };
200
+ }
201
+
202
+ // src/reporter.ts
203
+ var DEFAULT_FLUSH_INTERVAL = 2e3;
204
+ var DEFAULT_MAX_BATCH_SIZE = 10;
205
+ var DEFAULT_MAX_BATCH_BYTES = 64 * 1024;
206
+ var DEFAULT_MAX_QUEUE_SIZE = 1e3;
207
+ var DEFAULT_MAX_ATTEMPTS = 3;
208
+ var DEFAULT_BACKOFF_MS = 1e3;
209
+ var STORAGE_WRITE_THROTTLE_MS = 500;
210
+ var Reporter = class {
211
+ constructor(options) {
212
+ /** 会话 / 设备 id 兜底:enrich 未提供时按实例生成(对齐旧 SDK 格式) */
213
+ this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
214
+ this.deviceId = `device_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
215
+ /** 待发队列(内存中的事实源,持久化是它的影子) */
216
+ this.queue = [];
217
+ this.flushTimer = null;
218
+ this.retryTimer = null;
219
+ this.persistTimer = null;
220
+ this.persistDirty = false;
221
+ this.flushPromise = null;
222
+ this.disposed = false;
223
+ this.onExit = () => {
224
+ this.fire(this.dispose());
225
+ };
226
+ this.onPageHide = () => {
227
+ this.fire(this.dispose());
228
+ };
229
+ this.onVisibilityChange = () => {
230
+ if (document.visibilityState === "hidden") {
231
+ this.fire(this.flush());
232
+ } else if (this.opts.autoPageView) {
233
+ this.track("page_view");
234
+ }
235
+ };
236
+ if (!options.endpoint) {
237
+ throw new Error("Reporter: endpoint \u4E3A\u5FC5\u586B\u9879");
238
+ }
239
+ this.opts = options;
240
+ this.platform = options.platform ?? (typeof window !== "undefined" ? "browser" : "node");
241
+ this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
242
+ this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
243
+ this.maxBatchBytes = options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES;
244
+ this.maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
245
+ this.overflowPolicy = options.overflowPolicy ?? "drop-oldest";
246
+ this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
247
+ this.backoffMs = options.backoffMs ?? DEFAULT_BACKOFF_MS;
248
+ this.flushOnExit = options.flushOnExit ?? true;
249
+ if (options.transport) {
250
+ this.transportPromise = Promise.resolve(options.transport);
251
+ } else if (this.platform === "browser") {
252
+ this.transportPromise = Promise.resolve(
253
+ createBrowserTransport(options.endpoint, options.headers)
254
+ );
255
+ } else {
256
+ this.transportPromise = createNodeTransport(
257
+ options.endpoint,
258
+ options.headers
259
+ );
260
+ }
261
+ if (options.storage) {
262
+ this.storagePromise = Promise.resolve(options.storage);
263
+ } else if (options.persist === false) {
264
+ this.storagePromise = Promise.resolve(void 0);
265
+ } else if (this.platform === "browser") {
266
+ this.storagePromise = createBrowserStorage();
267
+ } else if (options.persistDir) {
268
+ this.storagePromise = createNodeStorage(options.persistDir);
269
+ } else {
270
+ this.storagePromise = Promise.resolve(void 0);
271
+ }
272
+ this.fire(this.init());
273
+ }
274
+ track(name, properties, options) {
275
+ if (this.disposed) {
276
+ this.log("dispose \u540E\u5FFD\u7565\u65B0\u4E8B\u4EF6:", name);
277
+ return "";
278
+ }
279
+ const enriched = this.opts.enrich?.() ?? {};
280
+ const event = {
281
+ id: generateEventId(),
282
+ name,
283
+ properties: {
284
+ ...this.opts.baseProperties ?? {},
285
+ ...this.opts.context?.() ?? {},
286
+ ...properties ?? {}
287
+ },
288
+ timestamp: options?.timestamp ?? Date.now(),
289
+ attempts: 0,
290
+ sessionId: enriched.sessionId ?? this.sessionId,
291
+ userId: enriched.userId ?? null,
292
+ deviceId: enriched.deviceId ?? this.deviceId,
293
+ category: options?.category ?? "custom",
294
+ context: enriched.context ?? {}
295
+ };
296
+ if (typeof name !== "string" || name.length === 0) {
297
+ this.opts.onDropped?.([event], "invalid");
298
+ return "";
299
+ }
300
+ if (options?.immediate) {
301
+ this.fire(this.sendBatch([event]));
302
+ return event.id;
303
+ }
304
+ this.enqueue(event);
305
+ if (this.flushInterval <= 0) {
306
+ this.fire(this.flush());
307
+ } else {
308
+ this.scheduleFlush();
309
+ }
310
+ return event.id;
311
+ }
312
+ flush() {
313
+ if (!this.flushPromise) {
314
+ if (this.retryTimer) {
315
+ clearTimeout(this.retryTimer);
316
+ this.retryTimer = null;
317
+ }
318
+ this.flushPromise = this.drain().finally(() => {
319
+ this.flushPromise = null;
320
+ });
321
+ }
322
+ return this.flushPromise;
323
+ }
324
+ get size() {
325
+ return this.queue.length;
326
+ }
327
+ async dispose() {
328
+ if (this.disposed) return;
329
+ this.disposed = true;
330
+ if (this.flushTimer) {
331
+ clearTimeout(this.flushTimer);
332
+ this.flushTimer = null;
333
+ }
334
+ if (this.retryTimer) {
335
+ clearTimeout(this.retryTimer);
336
+ this.retryTimer = null;
337
+ }
338
+ if (this.persistTimer) {
339
+ clearTimeout(this.persistTimer);
340
+ this.persistTimer = null;
341
+ }
342
+ if (this.platform === "browser") {
343
+ window.removeEventListener("pagehide", this.onPageHide);
344
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
345
+ } else {
346
+ const exitProcess = this.getExitProcess();
347
+ exitProcess.off("beforeExit", this.onExit);
348
+ exitProcess.off("SIGINT", this.onExit);
349
+ exitProcess.off("SIGTERM", this.onExit);
350
+ }
351
+ const transport = await this.transportPromise;
352
+ if (this.queue.length === 0) {
353
+ if (this.persistDirty) {
354
+ await this.persistNow([]);
355
+ }
356
+ return;
357
+ }
358
+ if (this.flushOnExit) {
359
+ const pending = this.queue.splice(0);
360
+ if (transport.sendSync?.(pending) ?? false) {
361
+ this.opts.onFlushed?.(pending);
362
+ await this.persistNow([]);
363
+ return;
364
+ }
365
+ try {
366
+ await transport.send(pending);
367
+ this.opts.onFlushed?.(pending);
368
+ await this.persistNow([]);
369
+ } catch (error) {
370
+ this.opts.onFailed?.(pending, error);
371
+ this.queue.push(...pending);
372
+ await this.persistNow();
373
+ }
374
+ return;
375
+ }
376
+ if (this.persistDirty) {
377
+ await this.persistNow();
378
+ }
379
+ }
380
+ /** 异步初始化:创建内置持久化、恢复上次会话队列、挂载退出兜底 */
381
+ async init() {
382
+ this.storage = await this.storagePromise;
383
+ if (this.disposed) return;
384
+ if (this.storage) {
385
+ try {
386
+ const restored = await this.storage.load();
387
+ if (restored.length > 0) {
388
+ this.enqueueRestored(restored);
389
+ }
390
+ } catch (error) {
391
+ this.log("restore pending queue failed:", error);
392
+ }
393
+ }
394
+ if (this.disposed) return;
395
+ if (this.platform === "browser") {
396
+ if (this.flushOnExit) {
397
+ window.addEventListener("pagehide", this.onPageHide);
398
+ }
399
+ if (this.flushOnExit || this.opts.autoPageView) {
400
+ document.addEventListener("visibilitychange", this.onVisibilityChange);
401
+ }
402
+ if (this.opts.autoPageView) {
403
+ this.track("page_view");
404
+ }
405
+ } else if (this.flushOnExit) {
406
+ const exitProcess = this.getExitProcess();
407
+ exitProcess.once("beforeExit", this.onExit);
408
+ exitProcess.once("SIGINT", this.onExit);
409
+ exitProcess.once("SIGTERM", this.onExit);
410
+ }
411
+ if (this.queue.length > 0) {
412
+ this.scheduleFlush();
413
+ this.schedulePersist();
414
+ }
415
+ }
416
+ /** 恢复队列入队:历史事件放队头保证先到先发,超限按溢出丢弃 */
417
+ enqueueRestored(events) {
418
+ let accepted = events;
419
+ if (accepted.length > this.maxQueueSize) {
420
+ this.opts.onDropped?.(accepted.slice(this.maxQueueSize), "overflow");
421
+ accepted = accepted.slice(0, this.maxQueueSize);
422
+ }
423
+ this.queue.unshift(...accepted);
424
+ }
425
+ enqueue(event) {
426
+ if (this.queue.length >= this.maxQueueSize) {
427
+ if (this.overflowPolicy === "drop-newest") {
428
+ this.opts.onDropped?.([event], "overflow");
429
+ return;
430
+ }
431
+ if (this.overflowPolicy === "drop-oldest") {
432
+ this.opts.onDropped?.([this.queue.shift()], "overflow");
433
+ } else {
434
+ this.fire(this.flush());
435
+ }
436
+ }
437
+ this.queue.push(event);
438
+ this.schedulePersist();
439
+ }
440
+ /** 攒批定时:延迟上报的实现,窗口内事件合并为一批 */
441
+ scheduleFlush() {
442
+ if (this.flushTimer || this.disposed || this.flushInterval <= 0) return;
443
+ this.flushTimer = setTimeout(() => {
444
+ this.flushTimer = null;
445
+ if (this.queue.length > 0) {
446
+ this.fire(this.flush());
447
+ }
448
+ }, this.flushInterval);
449
+ }
450
+ /** 串行清空队列;失败批次重新入队后由 retryTimer 再次触发 drain */
451
+ async drain() {
452
+ while (!this.disposed && !this.retryTimer && this.queue.length > 0) {
453
+ await this.sendBatch(this.takeBatch());
454
+ }
455
+ }
456
+ /** 取一批:受条数 / 字节数限制;首条必取,避免单事件超 maxBatchBytes 时队列卡死 */
457
+ takeBatch() {
458
+ const batch = [];
459
+ let bytes = 0;
460
+ while (this.queue.length > 0 && batch.length < this.maxBatchSize) {
461
+ const size = JSON.stringify(this.queue[0]).length;
462
+ if (batch.length > 0 && bytes + size > this.maxBatchBytes) break;
463
+ batch.push(this.queue.shift());
464
+ bytes += size;
465
+ }
466
+ return batch;
467
+ }
468
+ async sendBatch(batch) {
469
+ const transport = await this.transportPromise;
470
+ try {
471
+ await transport.send(batch);
472
+ this.opts.onFlushed?.(batch);
473
+ this.schedulePersist();
474
+ } catch (error) {
475
+ this.opts.onFailed?.(batch, error);
476
+ this.log("send batch failed:", error);
477
+ const updated = batch.map((event) => ({
478
+ ...event,
479
+ attempts: event.attempts + 1
480
+ }));
481
+ const retryable = updated.filter(
482
+ (event) => event.attempts < this.maxAttempts
483
+ );
484
+ const dead = updated.filter(
485
+ (event) => event.attempts >= this.maxAttempts
486
+ );
487
+ if (dead.length > 0) {
488
+ this.opts.onDropped?.(dead, "max-attempts");
489
+ }
490
+ if (retryable.length > 0) {
491
+ this.queue.unshift(...retryable);
492
+ this.schedulePersist();
493
+ this.scheduleRetry(retryable);
494
+ }
495
+ }
496
+ }
497
+ /** 失败退避:按批次内最大 attempts 计 2^n 指数增长,避免失败批次频繁重试 */
498
+ scheduleRetry(retryable) {
499
+ if (this.retryTimer) return;
500
+ const maxAttemptsInBatch = Math.max(
501
+ ...retryable.map((event) => event.attempts)
502
+ );
503
+ const delay = this.backoffMs * 2 ** (maxAttemptsInBatch - 1);
504
+ this.retryTimer = setTimeout(() => {
505
+ this.retryTimer = null;
506
+ this.fire(this.flush());
507
+ }, delay);
508
+ }
509
+ /** 持久化节流:窗口内多次变更只落盘一次,避免每次入队全量序列化 */
510
+ schedulePersist() {
511
+ if (!this.storage) return;
512
+ this.persistDirty = true;
513
+ if (this.persistTimer) return;
514
+ this.persistTimer = setTimeout(() => {
515
+ this.persistTimer = null;
516
+ if (!this.persistDirty) return;
517
+ this.persistDirty = false;
518
+ this.fire(this.persistNow());
519
+ }, STORAGE_WRITE_THROTTLE_MS);
520
+ }
521
+ async persistNow(events = this.queue) {
522
+ if (!this.storage) return;
523
+ try {
524
+ await this.storage.save(events);
525
+ } catch (error) {
526
+ this.log("persist queue failed:", error);
527
+ }
528
+ }
529
+ log(...args) {
530
+ if (this.opts.debug) {
531
+ console.log("[reporter]", ...args);
532
+ }
533
+ }
534
+ /**
535
+ * Node 进程退出事件句柄:Electron 渲染进程 typings 里 process 只声明了 'loaded'
536
+ * 事件,与 @types/node 不一致,显式收窄到本 SDK 用到的信号,兼容两端编译器
537
+ */
538
+ getExitProcess() {
539
+ return process;
540
+ }
541
+ /** 安全触发异步任务:吞掉 rejection,避免未处理异常 */
542
+ fire(promise) {
543
+ promise.catch((error) => {
544
+ this.log("async task failed:", error);
545
+ });
546
+ }
547
+ };
548
+ // Annotate the CommonJS export names for ESM import in node:
549
+ 0 && (module.exports = {
550
+ Reporter,
551
+ createBrowserStorage,
552
+ createBrowserTransport,
553
+ createLocalStorageStorage,
554
+ createNodeStorage,
555
+ createNodeTransport
556
+ });
557
+ //# sourceMappingURL=index.cjs.map