onerway-analytics 1.2.0 → 1.5.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/README.md +17 -4
- package/dist/bundle/index.js +319 -170
- package/dist/bundle/index.js.map +4 -4
- package/dist/core/analytics.d.ts +2 -2
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js +35 -108
- package/dist/core/delivery-queue.d.ts +41 -0
- package/dist/core/delivery-queue.d.ts.map +1 -0
- package/dist/core/delivery-queue.js +248 -0
- package/dist/core/sender.d.ts +7 -3
- package/dist/core/sender.d.ts.map +1 -1
- package/dist/core/sender.js +42 -46
- package/dist/types/index.d.ts +5 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +7 -3
- package/dist/core/analytics.test.d.ts +0 -2
- package/dist/core/analytics.test.d.ts.map +0 -1
- package/dist/core/analytics.test.js +0 -169
- package/dist/core/ga-adapter.d.ts +0 -11
- package/dist/core/ga-adapter.d.ts.map +0 -1
- package/dist/core/ga-adapter.js +0 -66
- package/dist/core/ga-loader.d.ts +0 -10
- package/dist/core/ga-loader.d.ts.map +0 -1
- package/dist/core/ga-loader.js +0 -34
- package/dist/core/ga-manager.d.ts +0 -24
- package/dist/core/ga-manager.d.ts.map +0 -1
- package/dist/core/ga-manager.js +0 -64
package/README.md
CHANGED
|
@@ -214,7 +214,8 @@ const analytics = createAnalytics({
|
|
|
214
214
|
| `keyEvents` | `string[]` | — | 关键事件名单,命中的事件立即发送 |
|
|
215
215
|
| `bufferFlushInterval` | `number` | `5000` | 缓冲区定时刷新间隔(ms) |
|
|
216
216
|
| `bufferMaxSize` | `number` | `25` | 缓冲区达到此数量立即刷新 |
|
|
217
|
-
| `maxRetryCount` | `number` | `3` |
|
|
217
|
+
| `maxRetryCount` | `number` | `3` | 同一事件重发预算(含离页补发),上限 5;预算跨页面保留 |
|
|
218
|
+
| `persistKeyEvents` | `boolean` | `false` | 将关键事件保存到本地直到 HTTP 确认;仅对属性可安全持久化的应用启用 |
|
|
218
219
|
| `sessionTimeout` | `number` | `1800000` | 会话超时(ms),默认 30 分钟 |
|
|
219
220
|
| `performance.enabled` | `boolean` | `true` | Core Web Vitals 采集开关 |
|
|
220
221
|
| `gtm.enabled` | `boolean` | — | GTM 双发开关 |
|
|
@@ -314,9 +315,13 @@ const analytics = createAnalytics({
|
|
|
314
315
|
## 可靠性机制
|
|
315
316
|
|
|
316
317
|
- **缓冲区 + 批量发送**:事件先入缓冲区,满 `bufferMaxSize` 或到 `bufferFlushInterval` 后批量发送
|
|
317
|
-
-
|
|
318
|
-
-
|
|
319
|
-
-
|
|
318
|
+
- **有界重试**:网络错误、408、429、5xx 最多重试 `maxRetryCount` 次(上限 5),指数退避加随机抖动;其他 4xx 终止。fetch/XHR 均有 10 秒超时。
|
|
319
|
+
- **可选关键事件持久化**:`persistKeyEvents: true` 时,关键事件发送前保存完整 payload,HTTP 2xx 后移除;重启保留 event_id 和原页面归属,按 appKey/serverUrl 隔离。自动事件不落盘;队列最多 500 条、24 小时有效,超出重试预算或不可重试错误终止。存储不可用时降为内存发送。
|
|
320
|
+
- **页面生命周期**:visibilitychange(hidden) / pagehide 提前刷新,pageshow/visible 恢复正常发送;原请求与离页副本任一 HTTP 2xx 即确认,另一个副本后续失败不会恢复已确认事件。全部请求使用 apiKey header;正常使用普通 fetch,隐藏时使用 keepalive,缺少 fetch 时正常页面降级异步 XHR。无 URI apiKey 或同步 XHR。
|
|
321
|
+
- **首次即时发送**:正常页面的首次请求直接发送,不受两个并发的队列限制;只有失败重试和历史恢复共用两个并发。浏览器仍决定实际网络调度。
|
|
322
|
+
- **离页合批**:收集所有未确认事件(包括正常请求在途的副本),关键事件优先,按相同 client/session/context/marketing/user 等请求上下文合并。一次回调内直接提交全部预算内批次,不等待前一批响应。
|
|
323
|
+
- **总字节控制**:单批及同一隐藏周期离页提交总量限制为 48 KiB UTF-8 字节,同时扣除尚未完成的 keepalive 在途字节;不同生命周期回调不重复提交同一隐藏周期已提交的事件。单事件超大或待确认队列满时丢弃新增事件。SDK 无法控制其他库占用的 keepalive 额度。
|
|
324
|
+
- **送达边界**:服务端必须按 event_id 去重;HTTP 2xx 仅作为传输确认。离页后后续排队请求不保证继续执行,已持久化的关键事件等下次同源访问恢复;用户不再访问、存储不可用、超过上限/有效期/重试预算时仍可能丢失。自动事件不保证跨页面补发。
|
|
320
325
|
- **采样控制**:`sampleRate` 在客户端决定是否上报,降低高流量场景的服务器压力
|
|
321
326
|
|
|
322
327
|
---
|
|
@@ -335,3 +340,11 @@ analytics = createAnalytics({ ... });
|
|
|
335
340
|
analytics = createAnalytics({ ..., autoTrackPageView: false });
|
|
336
341
|
analytics.track('page_view', { page_url: location.href });
|
|
337
342
|
```
|
|
343
|
+
|
|
344
|
+
## 发送可靠性验证(1.3.2)
|
|
345
|
+
|
|
346
|
+
- `npm ci`,`npm run typecheck`,`npm test -- --runInBand`。
|
|
347
|
+
- `npm run test:browser`:Playwright + 本地 HTTP/CORS 接收端,覆盖密集请求、断网恢复、刷新、原生 pagehide、关闭重开、401。默认使用 macOS 已安装 Chrome;其他环境可设置 `CHROME_PATH` 或先安装 Playwright Chromium。
|
|
348
|
+
- `npm pack` 会重新构建模块及浏览器包,排除测试产物。生产 APISIX 鉴权、真实入库和统计去重需要部署后单独验收;本地测试不调用生产接口。
|
|
349
|
+
|
|
350
|
+
- `npm run test:browser:comparison`:使用相邻收银台实际安装的 Axios 对照 V1 传输(可用 `CHECKOUT_DIR` 指定仓库路径)。每场景双写 24 个事件,覆盖正常立即跳转、已有请求积压的慢网跳转、慢网关闭;离开后不再访问或初始化 SDK,接收端比较 V1 已收到的事件是否都包含在 V2 中。V1 省略指纹等待,使其发送时机更有利;这不是完整支付业务 E2E,也不代表生产零漏报。
|
package/dist/bundle/index.js
CHANGED
|
@@ -67,7 +67,7 @@ var OnrwayAnalytics = (() => {
|
|
|
67
67
|
prod: "HUk5Kogyuv9zH5qDA2qSCjVEnhanSkf8",
|
|
68
68
|
production: "HUk5Kogyuv9zH5qDA2qSCjVEnhanSkf8"
|
|
69
69
|
};
|
|
70
|
-
var SDK_VERSION = "1.
|
|
70
|
+
var SDK_VERSION = "1.3.2";
|
|
71
71
|
var DEFAULT_CONFIG = {
|
|
72
72
|
debug: false,
|
|
73
73
|
autoTrack: true,
|
|
@@ -322,110 +322,275 @@ var OnrwayAnalytics = (() => {
|
|
|
322
322
|
};
|
|
323
323
|
|
|
324
324
|
// src/core/sender.ts
|
|
325
|
+
var DeliveryError = class extends Error {
|
|
326
|
+
constructor(message, retryable) {
|
|
327
|
+
super(message);
|
|
328
|
+
this.retryable = retryable;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
325
331
|
var Sender = class _Sender {
|
|
326
|
-
static async send(payload, serverUrl, apiKey) {
|
|
327
|
-
if (
|
|
328
|
-
|
|
332
|
+
static async send(payload, serverUrl, apiKey, keepalive = false) {
|
|
333
|
+
if (typeof fetch === "function" && typeof AbortController !== "undefined") {
|
|
334
|
+
if (keepalive && !_Sender.supportsKeepalive()) {
|
|
335
|
+
throw new DeliveryError("Keepalive unavailable", true);
|
|
336
|
+
}
|
|
337
|
+
return _Sender.fetchSend(payload, serverUrl, apiKey, keepalive);
|
|
329
338
|
}
|
|
339
|
+
if (keepalive) throw new DeliveryError("Unload transport unavailable", true);
|
|
330
340
|
return _Sender.xhrSend(payload, serverUrl, apiKey);
|
|
331
341
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
const url = _Sender.buildBeaconUrl(serverUrl, apiKey);
|
|
337
|
-
const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
|
|
338
|
-
const success = navigator.sendBeacon(url, blob);
|
|
339
|
-
if (!success) {
|
|
340
|
-
throw new Error("sendBeacon rejected payload");
|
|
341
|
-
}
|
|
342
|
+
// Compatibility entry point: authenticated delivery must support custom headers.
|
|
343
|
+
static sendBeacon(payload, serverUrl, apiKey) {
|
|
344
|
+
return _Sender.send(payload, serverUrl, apiKey, true);
|
|
342
345
|
}
|
|
343
|
-
static async fetchSend(payload, serverUrl, apiKey) {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
346
|
+
static async fetchSend(payload, serverUrl, apiKey, keepalive = false) {
|
|
347
|
+
const controller = new AbortController();
|
|
348
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
349
|
+
try {
|
|
350
|
+
const response = await fetch(serverUrl, {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: { "Content-Type": "application/json", apiKey },
|
|
353
|
+
body: JSON.stringify(payload),
|
|
354
|
+
keepalive,
|
|
355
|
+
signal: controller.signal
|
|
356
|
+
});
|
|
357
|
+
if (!response.ok) throw _Sender.httpError(response.status);
|
|
358
|
+
} finally {
|
|
359
|
+
clearTimeout(timer);
|
|
355
360
|
}
|
|
356
361
|
}
|
|
357
|
-
static
|
|
362
|
+
static httpError(status) {
|
|
363
|
+
return new DeliveryError(`HTTP ${status}`, status === 408 || status === 429 || status >= 500);
|
|
364
|
+
}
|
|
365
|
+
static xhrSend(payload, serverUrl, apiKey) {
|
|
358
366
|
return new Promise((resolve, reject) => {
|
|
359
367
|
const xhr = new XMLHttpRequest();
|
|
360
|
-
xhr.open("POST", serverUrl);
|
|
368
|
+
xhr.open("POST", serverUrl, true);
|
|
361
369
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
362
370
|
xhr.setRequestHeader("apiKey", apiKey);
|
|
363
371
|
xhr.timeout = 1e4;
|
|
364
|
-
xhr.onload = () =>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
xhr.onerror = () => reject(new Error("XHR network error"));
|
|
372
|
-
xhr.ontimeout = () => reject(new Error("XHR timeout"));
|
|
372
|
+
xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(_Sender.httpError(xhr.status));
|
|
373
|
+
xhr.onerror = () => reject(new DeliveryError("XHR network error", true));
|
|
374
|
+
xhr.ontimeout = () => reject(new DeliveryError("XHR timeout", true));
|
|
375
|
+
xhr.onabort = () => reject(new DeliveryError("XHR aborted", true));
|
|
373
376
|
xhr.send(JSON.stringify(payload));
|
|
374
377
|
});
|
|
375
378
|
}
|
|
376
379
|
static supportsKeepalive() {
|
|
377
|
-
|
|
378
|
-
return typeof Request !== "undefined" && "keepalive" in Request.prototype;
|
|
379
|
-
} catch {
|
|
380
|
-
return false;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
static buildBeaconUrl(serverUrl, apiKey) {
|
|
384
|
-
const separator = serverUrl.includes("?") ? "&" : "?";
|
|
385
|
-
return `${serverUrl}${separator}apiKey=${encodeURIComponent(apiKey)}`;
|
|
380
|
+
return typeof Request !== "undefined" && "keepalive" in Request.prototype;
|
|
386
381
|
}
|
|
387
382
|
};
|
|
388
383
|
|
|
389
|
-
// src/core/
|
|
390
|
-
var
|
|
391
|
-
var
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
384
|
+
// src/core/delivery-queue.ts
|
|
385
|
+
var MAX_BYTES = 48 * 1024;
|
|
386
|
+
var MAX_EVENTS = 500;
|
|
387
|
+
var MAX_AGE = 24 * 60 * 60 * 1e3;
|
|
388
|
+
var DeliveryQueue = class {
|
|
389
|
+
constructor(options) {
|
|
390
|
+
this.options = options;
|
|
391
|
+
this.jobs = [];
|
|
392
|
+
this.activeRetries = 0;
|
|
393
|
+
this.hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
394
|
+
this.timer = null;
|
|
395
|
+
this.stopped = false;
|
|
396
|
+
this.hiddenSent = /* @__PURE__ */ new Set();
|
|
397
|
+
this.hiddenSpent = 0;
|
|
398
|
+
this.keepaliveBytes = 0;
|
|
399
|
+
this.maxAttempts = 1 + Math.min(5, Math.max(0, options.maxRetries || 0));
|
|
400
|
+
this.storagePrefix = `__analytics_delivery_v1__:${encodeURIComponent(options.appKey)}:${encodeURIComponent(options.serverUrl)}:`;
|
|
401
|
+
if (options.persistKeyEvents && options.apiKey) this.recover();
|
|
402
|
+
this.pumpRetries();
|
|
403
|
+
}
|
|
404
|
+
enqueue(payload) {
|
|
405
|
+
if (this.stopped || !this.options.apiKey) return;
|
|
406
|
+
const accepted = [];
|
|
407
|
+
for (const event of payload.events) {
|
|
408
|
+
if (this.jobs.length >= MAX_EVENTS) break;
|
|
409
|
+
const single = { ...payload, events: [event] };
|
|
410
|
+
if (this.bytes(single) > MAX_BYTES) continue;
|
|
411
|
+
const job = {
|
|
412
|
+
payload: single,
|
|
413
|
+
attempts: 0,
|
|
414
|
+
createdAt: Date.now(),
|
|
415
|
+
nextAt: 0,
|
|
416
|
+
active: 0,
|
|
417
|
+
hiddenActive: false,
|
|
418
|
+
terminal: false
|
|
419
|
+
};
|
|
420
|
+
this.jobs.push(job);
|
|
421
|
+
this.persist(job);
|
|
422
|
+
accepted.push(job);
|
|
423
|
+
}
|
|
424
|
+
if (this.hidden) {
|
|
425
|
+
this.flushHidden();
|
|
426
|
+
} else {
|
|
427
|
+
for (const batch of this.batches(accepted, MAX_BYTES)) this.start(batch.payload, batch.jobs, false, false);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
setHidden(hidden) {
|
|
431
|
+
if (hidden && !this.hidden) {
|
|
432
|
+
this.hiddenSent.clear();
|
|
433
|
+
this.hiddenSpent = 0;
|
|
434
|
+
}
|
|
435
|
+
this.hidden = hidden;
|
|
436
|
+
if (this.timer) {
|
|
437
|
+
clearTimeout(this.timer);
|
|
438
|
+
this.timer = null;
|
|
439
|
+
}
|
|
440
|
+
if (!hidden) this.pumpRetries();
|
|
441
|
+
}
|
|
442
|
+
/** Submit the whole affordable rescue set synchronously; do not wait for ACKs. */
|
|
443
|
+
flushHidden() {
|
|
444
|
+
if (this.stopped || !this.hidden) return;
|
|
445
|
+
const available = Math.min(MAX_BYTES - this.hiddenSpent, MAX_BYTES - this.keepaliveBytes);
|
|
446
|
+
if (available <= 0) return;
|
|
447
|
+
this.prune();
|
|
448
|
+
const candidates = this.jobs.filter((job) => !job.terminal && !job.hiddenActive && !this.hiddenSent.has(job) && job.attempts < this.maxAttempts).sort((a2, b2) => Number(b2.payload.events[0].key_event) - Number(a2.payload.events[0].key_event));
|
|
449
|
+
let remaining = available;
|
|
450
|
+
for (const batch of this.batches(candidates, available, true)) {
|
|
451
|
+
const bytes = this.bytes(batch.payload);
|
|
452
|
+
if (bytes > remaining) continue;
|
|
453
|
+
remaining -= bytes;
|
|
454
|
+
this.hiddenSpent += bytes;
|
|
455
|
+
for (const job of batch.jobs) this.hiddenSent.add(job);
|
|
456
|
+
this.start(batch.payload, batch.jobs, true, false);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
bytes(payload) {
|
|
460
|
+
return new Blob([JSON.stringify(payload)]).size;
|
|
461
|
+
}
|
|
462
|
+
batches(jobs, budget, totalBudget = false) {
|
|
463
|
+
const result = [];
|
|
464
|
+
let remaining = budget;
|
|
465
|
+
const groups = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const job of jobs) {
|
|
467
|
+
const { events, ...envelope } = job.payload;
|
|
468
|
+
const key = JSON.stringify(envelope);
|
|
469
|
+
let group = groups.get(key);
|
|
470
|
+
const proposed = { ...envelope, events: [...group?.payload.events || [], ...events].map((event, index) => ({ ...event, batch_event_index: index })) };
|
|
471
|
+
const delta = this.bytes(proposed) - (group ? this.bytes(group.payload) : 0);
|
|
472
|
+
if (totalBudget && delta > remaining) continue;
|
|
473
|
+
if (this.bytes(proposed) > budget) {
|
|
474
|
+
group = void 0;
|
|
398
475
|
}
|
|
399
|
-
|
|
400
|
-
|
|
476
|
+
if (!group) {
|
|
477
|
+
group = { payload: { ...envelope, events: events.map((event, index) => ({ ...event, batch_event_index: index })) }, jobs: [] };
|
|
478
|
+
groups.set(key, group);
|
|
479
|
+
result.push(group);
|
|
480
|
+
} else {
|
|
481
|
+
group.payload = proposed;
|
|
482
|
+
}
|
|
483
|
+
group.jobs.push(job);
|
|
484
|
+
if (totalBudget) remaining -= delta;
|
|
401
485
|
}
|
|
486
|
+
return result;
|
|
402
487
|
}
|
|
403
|
-
|
|
488
|
+
prune() {
|
|
489
|
+
for (const job of [...this.jobs]) {
|
|
490
|
+
if (!job.active && (job.terminal || Date.now() - job.createdAt >= MAX_AGE || job.attempts >= this.maxAttempts)) this.remove(job);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
pumpRetries() {
|
|
494
|
+
if (this.stopped || this.hidden) return;
|
|
495
|
+
if (this.timer) {
|
|
496
|
+
clearTimeout(this.timer);
|
|
497
|
+
this.timer = null;
|
|
498
|
+
}
|
|
499
|
+
this.prune();
|
|
500
|
+
while (this.activeRetries < 2) {
|
|
501
|
+
const job = this.jobs.find((item) => !item.active && item.nextAt <= Date.now());
|
|
502
|
+
if (!job) break;
|
|
503
|
+
this.start(job.payload, [job], false, true);
|
|
504
|
+
}
|
|
505
|
+
const waiting = this.jobs.filter((job) => !job.active);
|
|
506
|
+
if (waiting.length && this.activeRetries < 2) {
|
|
507
|
+
const delay = Math.max(1, Math.min(...waiting.map((job) => job.nextAt)) - Date.now());
|
|
508
|
+
this.timer = setTimeout(() => this.pumpRetries(), delay);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
start(payload, jobs, keepalive, retry) {
|
|
512
|
+
const bytes = keepalive ? this.bytes(payload) : 0;
|
|
513
|
+
this.keepaliveBytes += bytes;
|
|
514
|
+
if (retry) this.activeRetries++;
|
|
515
|
+
for (const job of jobs) {
|
|
516
|
+
job.active++;
|
|
517
|
+
job.attempts++;
|
|
518
|
+
if (keepalive) job.hiddenActive = true;
|
|
519
|
+
this.persist(job);
|
|
520
|
+
}
|
|
521
|
+
void this.deliver(payload, jobs, keepalive, retry, bytes);
|
|
522
|
+
}
|
|
523
|
+
async deliver(payload, jobs, keepalive, retry, bytes) {
|
|
404
524
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
525
|
+
await Sender.send(payload, this.options.serverUrl, this.options.apiKey, keepalive);
|
|
526
|
+
for (const job of jobs) this.remove(job);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
for (const job of jobs) {
|
|
529
|
+
if (!this.jobs.includes(job)) continue;
|
|
530
|
+
if (error instanceof DeliveryError && !error.retryable) job.terminal = true;
|
|
531
|
+
job.nextAt = Date.now() + Math.min(3e4, 1e3 * 2 ** (job.attempts - 1)) + Math.floor(Math.random() * 250);
|
|
532
|
+
}
|
|
533
|
+
} finally {
|
|
534
|
+
for (const job of jobs) {
|
|
535
|
+
job.active--;
|
|
536
|
+
if (keepalive) job.hiddenActive = false;
|
|
537
|
+
if (!this.jobs.includes(job)) continue;
|
|
538
|
+
if (!job.active && (job.terminal || job.attempts >= this.maxAttempts)) this.remove(job);
|
|
539
|
+
else this.persist(job);
|
|
540
|
+
}
|
|
541
|
+
this.keepaliveBytes -= bytes;
|
|
542
|
+
if (retry) this.activeRetries--;
|
|
543
|
+
this.pumpRetries();
|
|
412
544
|
}
|
|
413
545
|
}
|
|
414
|
-
|
|
415
|
-
|
|
546
|
+
key(eventId) {
|
|
547
|
+
return this.storagePrefix + eventId;
|
|
416
548
|
}
|
|
417
|
-
|
|
418
|
-
|
|
549
|
+
persist(job) {
|
|
550
|
+
if (!this.options.persistKeyEvents || !job.payload.events[0].key_event) return;
|
|
551
|
+
safeLocalStorageSet(this.key(job.payload.events[0].event_id), JSON.stringify({
|
|
552
|
+
payload: job.payload,
|
|
553
|
+
attempts: job.attempts,
|
|
554
|
+
createdAt: job.createdAt,
|
|
555
|
+
nextAt: job.nextAt
|
|
556
|
+
}));
|
|
419
557
|
}
|
|
420
|
-
|
|
558
|
+
remove(job) {
|
|
559
|
+
if (!this.jobs.includes(job)) return;
|
|
560
|
+
this.jobs = this.jobs.filter((item) => item !== job);
|
|
561
|
+
this.hiddenSent.delete(job);
|
|
562
|
+
if (this.options.persistKeyEvents) safeLocalStorageRemove(this.key(job.payload.events[0].event_id));
|
|
563
|
+
}
|
|
564
|
+
recover() {
|
|
421
565
|
try {
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
566
|
+
const keys = Object.keys(localStorage).filter((key) => key.startsWith(this.storagePrefix));
|
|
567
|
+
for (const key of keys) {
|
|
568
|
+
try {
|
|
569
|
+
const job = JSON.parse(safeLocalStorageGet(key) || "null");
|
|
570
|
+
if (!job || !Number.isFinite(job.createdAt) || !Number.isInteger(job.attempts) || job.attempts < 0 || !Number.isFinite(job.nextAt) || Date.now() - job.createdAt >= MAX_AGE || job.attempts >= this.maxAttempts || !Array.isArray(job.payload?.events) || job.payload.events.length !== 1 || !job.payload.events[0].key_event || this.key(job.payload.events[0].event_id) !== key || this.bytes(job.payload) > MAX_BYTES || this.jobs.length >= MAX_EVENTS) {
|
|
571
|
+
safeLocalStorageRemove(key);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
this.jobs.push({ ...job, active: 0, hiddenActive: false, terminal: false });
|
|
575
|
+
} catch {
|
|
576
|
+
safeLocalStorageRemove(key);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
427
579
|
} catch {
|
|
428
|
-
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
clear() {
|
|
583
|
+
for (const job of [...this.jobs]) this.remove(job);
|
|
584
|
+
if (this.timer) {
|
|
585
|
+
clearTimeout(this.timer);
|
|
586
|
+
this.timer = null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
destroy() {
|
|
590
|
+
this.stopped = true;
|
|
591
|
+
if (this.timer) {
|
|
592
|
+
clearTimeout(this.timer);
|
|
593
|
+
this.timer = null;
|
|
429
594
|
}
|
|
430
595
|
}
|
|
431
596
|
};
|
|
@@ -1006,8 +1171,8 @@ var OnrwayAnalytics = (() => {
|
|
|
1006
1171
|
this.userId = null;
|
|
1007
1172
|
this.userProps = {};
|
|
1008
1173
|
this.initialized = false;
|
|
1009
|
-
this.retrying = 0;
|
|
1010
1174
|
this.unloadHandler = null;
|
|
1175
|
+
this.visibilityHandler = null;
|
|
1011
1176
|
this.gtm = new GTMManager();
|
|
1012
1177
|
this.config = {
|
|
1013
1178
|
debug: false,
|
|
@@ -1025,9 +1190,13 @@ var OnrwayAnalytics = (() => {
|
|
|
1025
1190
|
this.batchPageId = this.generateBatchPageId();
|
|
1026
1191
|
this.sessionManager = new SessionManager(this.config.sessionTimeout);
|
|
1027
1192
|
this.sampled = this.resolveSampling();
|
|
1028
|
-
|
|
1029
|
-
this.
|
|
1030
|
-
|
|
1193
|
+
this.delivery = new DeliveryQueue({
|
|
1194
|
+
serverUrl: this.config.serverUrl,
|
|
1195
|
+
apiKey: this.resolveApiKey(),
|
|
1196
|
+
appKey,
|
|
1197
|
+
maxRetries: this.config.maxRetryCount,
|
|
1198
|
+
persistKeyEvents: this.config.persistKeyEvents === true && this.sampled && !this.config.disableServer
|
|
1199
|
+
});
|
|
1031
1200
|
this.registerBeforeUnload();
|
|
1032
1201
|
if (this.config.autoTrack && isBrowser()) {
|
|
1033
1202
|
initAutoTrack(this, this.config.autoTrackSelector);
|
|
@@ -1117,6 +1286,7 @@ var OnrwayAnalytics = (() => {
|
|
|
1117
1286
|
this.userId = null;
|
|
1118
1287
|
this.userProps = {};
|
|
1119
1288
|
this.buffer = [];
|
|
1289
|
+
this.delivery.clear();
|
|
1120
1290
|
if (this.bufferTimer) {
|
|
1121
1291
|
clearTimeout(this.bufferTimer);
|
|
1122
1292
|
this.bufferTimer = null;
|
|
@@ -1219,10 +1389,6 @@ var OnrwayAnalytics = (() => {
|
|
|
1219
1389
|
this.bufferTimer = null;
|
|
1220
1390
|
}
|
|
1221
1391
|
if (this.buffer.length === 0) return;
|
|
1222
|
-
if (this.retrying > 0) {
|
|
1223
|
-
this.scheduleBufferFlush();
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
1392
|
const events = this.buffer.splice(0, this.buffer.length);
|
|
1227
1393
|
events.forEach((e2, i2) => {
|
|
1228
1394
|
e2.batch_event_index = i2;
|
|
@@ -1235,39 +1401,12 @@ var OnrwayAnalytics = (() => {
|
|
|
1235
1401
|
this.log("Flushing key event:", event.event_name);
|
|
1236
1402
|
this.sendBatch([event], 0);
|
|
1237
1403
|
}
|
|
1238
|
-
|
|
1239
|
-
if (events.length
|
|
1240
|
-
const apiKey = this.resolveApiKey();
|
|
1241
|
-
if (!apiKey) {
|
|
1242
|
-
this.log("No API key configured, dropping events");
|
|
1243
|
-
for (const event of events) {
|
|
1244
|
-
StorageQueue.enqueue(event);
|
|
1245
|
-
}
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
const payload = this.buildPayload(events);
|
|
1404
|
+
sendBatch(events, _retryCount) {
|
|
1405
|
+
if (!events.length || this.config.disableServer) return;
|
|
1249
1406
|
try {
|
|
1250
|
-
|
|
1251
|
-
this.log(`Batch sent successfully: ${events.length} events`);
|
|
1407
|
+
this.delivery.enqueue(this.buildPayload(events));
|
|
1252
1408
|
} catch (error) {
|
|
1253
|
-
this.log(
|
|
1254
|
-
if (retryCount < this.config.maxRetryCount) {
|
|
1255
|
-
const delay = 1e3 * Math.pow(2, retryCount);
|
|
1256
|
-
this.log(`Retrying in ${delay}ms...`);
|
|
1257
|
-
this.retrying += 1;
|
|
1258
|
-
setTimeout(() => {
|
|
1259
|
-
this.retrying -= 1;
|
|
1260
|
-
this.sendBatch(events, retryCount + 1);
|
|
1261
|
-
if (this.buffer.length > 0) {
|
|
1262
|
-
this.scheduleBufferFlush();
|
|
1263
|
-
}
|
|
1264
|
-
}, delay);
|
|
1265
|
-
} else {
|
|
1266
|
-
this.log("Max retries reached, persisting events to localStorage");
|
|
1267
|
-
for (const event of events) {
|
|
1268
|
-
StorageQueue.enqueue(event);
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1409
|
+
this.log("Unable to prepare analytics payload:", error);
|
|
1271
1410
|
}
|
|
1272
1411
|
}
|
|
1273
1412
|
buildPayload(events) {
|
|
@@ -1281,30 +1420,15 @@ var OnrwayAnalytics = (() => {
|
|
|
1281
1420
|
events
|
|
1282
1421
|
};
|
|
1283
1422
|
}
|
|
1284
|
-
recoverFailedEvents() {
|
|
1285
|
-
if (this.config.disableServer) return;
|
|
1286
|
-
if (!this.sampled) return;
|
|
1287
|
-
const failedEvents = StorageQueue.recover();
|
|
1288
|
-
if (failedEvents.length > 0) {
|
|
1289
|
-
this.log(`Recovered ${failedEvents.length} failed events from storage`);
|
|
1290
|
-
for (const event of failedEvents) {
|
|
1291
|
-
event.batch_ordering_id = this.generateBatchOrderingId();
|
|
1292
|
-
event.batch_page_id = this.batchPageId;
|
|
1293
|
-
event.batch_event_index = 0;
|
|
1294
|
-
}
|
|
1295
|
-
const batchSize = this.config.bufferMaxSize;
|
|
1296
|
-
for (let i2 = 0; i2 < failedEvents.length; i2 += batchSize) {
|
|
1297
|
-
const batch = failedEvents.slice(i2, i2 + batchSize);
|
|
1298
|
-
batch.forEach((e2, idx) => {
|
|
1299
|
-
e2.batch_event_index = idx;
|
|
1300
|
-
});
|
|
1301
|
-
this.sendBatch(batch, 0);
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
1423
|
destroy() {
|
|
1424
|
+
this.flushBuffer();
|
|
1425
|
+
this.delivery.destroy();
|
|
1426
|
+
if (this.visibilityHandler) {
|
|
1427
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
1428
|
+
window.removeEventListener("pageshow", this.visibilityHandler);
|
|
1429
|
+
this.visibilityHandler = null;
|
|
1430
|
+
}
|
|
1306
1431
|
if (this.unloadHandler) {
|
|
1307
|
-
window.removeEventListener("beforeunload", this.unloadHandler);
|
|
1308
1432
|
window.removeEventListener("pagehide", this.unloadHandler);
|
|
1309
1433
|
this.unloadHandler = null;
|
|
1310
1434
|
}
|
|
@@ -1315,41 +1439,22 @@ var OnrwayAnalytics = (() => {
|
|
|
1315
1439
|
}
|
|
1316
1440
|
registerBeforeUnload() {
|
|
1317
1441
|
if (!isBrowser() || this.config.disableServer) return;
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
const url = `${this.config.serverUrl}${this.config.serverUrl.includes("?") ? "&" : "?"}apiKey=${encodeURIComponent(apiKey)}`;
|
|
1330
|
-
const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
|
|
1331
|
-
const success = navigator.sendBeacon(url, blob);
|
|
1332
|
-
if (!success) throw new Error("sendBeacon rejected");
|
|
1333
|
-
} else {
|
|
1334
|
-
try {
|
|
1335
|
-
const xhr = new XMLHttpRequest();
|
|
1336
|
-
xhr.open("POST", this.config.serverUrl, false);
|
|
1337
|
-
xhr.setRequestHeader("Content-Type", "application/json");
|
|
1338
|
-
xhr.setRequestHeader("apiKey", apiKey);
|
|
1339
|
-
xhr.timeout = 2e3;
|
|
1340
|
-
xhr.send(JSON.stringify(payload));
|
|
1341
|
-
} catch {
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
} catch {
|
|
1345
|
-
for (const event of events) {
|
|
1346
|
-
StorageQueue.enqueue(event);
|
|
1347
|
-
}
|
|
1442
|
+
this.unloadHandler = () => {
|
|
1443
|
+
this.delivery.setHidden(true);
|
|
1444
|
+
this.flushBuffer();
|
|
1445
|
+
this.delivery.flushHidden();
|
|
1446
|
+
};
|
|
1447
|
+
this.visibilityHandler = () => {
|
|
1448
|
+
const hidden = document.visibilityState === "hidden";
|
|
1449
|
+
this.delivery.setHidden(hidden);
|
|
1450
|
+
if (hidden) {
|
|
1451
|
+
this.flushBuffer();
|
|
1452
|
+
this.delivery.flushHidden();
|
|
1348
1453
|
}
|
|
1349
1454
|
};
|
|
1350
|
-
this.unloadHandler
|
|
1351
|
-
|
|
1352
|
-
window.addEventListener("
|
|
1455
|
+
window.addEventListener("pagehide", this.unloadHandler);
|
|
1456
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
1457
|
+
window.addEventListener("pageshow", this.visibilityHandler);
|
|
1353
1458
|
}
|
|
1354
1459
|
trackPageView() {
|
|
1355
1460
|
if (!isBrowser()) return;
|
|
@@ -1364,6 +1469,50 @@ var OnrwayAnalytics = (() => {
|
|
|
1364
1469
|
function createAnalytics(config) {
|
|
1365
1470
|
return new Analytics(config);
|
|
1366
1471
|
}
|
|
1472
|
+
|
|
1473
|
+
// src/core/storage.ts
|
|
1474
|
+
var MAX_EVENT_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1475
|
+
var StorageQueue = class _StorageQueue {
|
|
1476
|
+
static enqueue(event) {
|
|
1477
|
+
try {
|
|
1478
|
+
const queue = _StorageQueue.readQueue();
|
|
1479
|
+
queue.push({ event, storedAt: Date.now() });
|
|
1480
|
+
while (queue.length > MAX_STORAGE_ITEMS) {
|
|
1481
|
+
queue.shift();
|
|
1482
|
+
}
|
|
1483
|
+
safeLocalStorageSet(STORAGE_KEYS.FAILED_QUEUE, JSON.stringify(queue));
|
|
1484
|
+
} catch {
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
static recover() {
|
|
1488
|
+
try {
|
|
1489
|
+
const queue = _StorageQueue.readQueue();
|
|
1490
|
+
_StorageQueue.clear();
|
|
1491
|
+
const now = Date.now();
|
|
1492
|
+
const events = queue.filter((item) => now - item.storedAt < MAX_EVENT_AGE_MS).map((item) => item.event);
|
|
1493
|
+
return events;
|
|
1494
|
+
} catch {
|
|
1495
|
+
return [];
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
static clear() {
|
|
1499
|
+
safeLocalStorageRemove(STORAGE_KEYS.FAILED_QUEUE);
|
|
1500
|
+
}
|
|
1501
|
+
static getQueueSize() {
|
|
1502
|
+
return _StorageQueue.readQueue().length;
|
|
1503
|
+
}
|
|
1504
|
+
static readQueue() {
|
|
1505
|
+
try {
|
|
1506
|
+
const raw = safeLocalStorageGet(STORAGE_KEYS.FAILED_QUEUE);
|
|
1507
|
+
if (!raw) return [];
|
|
1508
|
+
const parsed = JSON.parse(raw);
|
|
1509
|
+
if (!Array.isArray(parsed)) return [];
|
|
1510
|
+
return parsed;
|
|
1511
|
+
} catch {
|
|
1512
|
+
return [];
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1367
1516
|
return __toCommonJS(index_exports);
|
|
1368
1517
|
})();
|
|
1369
1518
|
//# sourceMappingURL=index.js.map
|