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/dist/core/analytics.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { API_KEY_MAP, DEFAULT_CONFIG, STORAGE_KEYS, } from '../types';
|
|
2
2
|
import { SessionManager } from './session-manager';
|
|
3
|
-
import {
|
|
4
|
-
import { StorageQueue } from './storage';
|
|
3
|
+
import { DeliveryQueue } from './delivery-queue';
|
|
5
4
|
import { collectContext, collectMarketing } from './context-collector';
|
|
6
5
|
import { initAutoTrack } from './auto-track';
|
|
7
6
|
import { initErrorMonitor } from './error-monitor';
|
|
@@ -17,8 +16,8 @@ export class Analytics {
|
|
|
17
16
|
this.userId = null;
|
|
18
17
|
this.userProps = {};
|
|
19
18
|
this.initialized = false;
|
|
20
|
-
this.retrying = 0;
|
|
21
19
|
this.unloadHandler = null;
|
|
20
|
+
this.visibilityHandler = null;
|
|
22
21
|
this.gtm = new GTMManager();
|
|
23
22
|
this.config = {
|
|
24
23
|
debug: false,
|
|
@@ -37,9 +36,13 @@ export class Analytics {
|
|
|
37
36
|
this.batchPageId = this.generateBatchPageId();
|
|
38
37
|
this.sessionManager = new SessionManager(this.config.sessionTimeout);
|
|
39
38
|
this.sampled = this.resolveSampling();
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
39
|
+
this.delivery = new DeliveryQueue({
|
|
40
|
+
serverUrl: this.config.serverUrl,
|
|
41
|
+
apiKey: this.resolveApiKey(),
|
|
42
|
+
appKey,
|
|
43
|
+
maxRetries: this.config.maxRetryCount,
|
|
44
|
+
persistKeyEvents: this.config.persistKeyEvents === true && this.sampled && !this.config.disableServer,
|
|
45
|
+
});
|
|
43
46
|
this.registerBeforeUnload();
|
|
44
47
|
if (this.config.autoTrack && isBrowser()) {
|
|
45
48
|
initAutoTrack(this, this.config.autoTrackSelector);
|
|
@@ -131,6 +134,7 @@ export class Analytics {
|
|
|
131
134
|
this.userId = null;
|
|
132
135
|
this.userProps = {};
|
|
133
136
|
this.buffer = [];
|
|
137
|
+
this.delivery.clear();
|
|
134
138
|
if (this.bufferTimer) {
|
|
135
139
|
clearTimeout(this.bufferTimer);
|
|
136
140
|
this.bufferTimer = null;
|
|
@@ -242,11 +246,6 @@ export class Analytics {
|
|
|
242
246
|
}
|
|
243
247
|
if (this.buffer.length === 0)
|
|
244
248
|
return;
|
|
245
|
-
// Wait for inflight retries to settle before sending a new batch
|
|
246
|
-
if (this.retrying > 0) {
|
|
247
|
-
this.scheduleBufferFlush();
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
249
|
const events = this.buffer.splice(0, this.buffer.length);
|
|
251
250
|
events.forEach((e, i) => {
|
|
252
251
|
e.batch_event_index = i;
|
|
@@ -260,42 +259,14 @@ export class Analytics {
|
|
|
260
259
|
this.log('Flushing key event:', event.event_name);
|
|
261
260
|
this.sendBatch([event], 0);
|
|
262
261
|
}
|
|
263
|
-
|
|
264
|
-
if (events.length
|
|
265
|
-
return;
|
|
266
|
-
const apiKey = this.resolveApiKey();
|
|
267
|
-
if (!apiKey) {
|
|
268
|
-
this.log('No API key configured, dropping events');
|
|
269
|
-
for (const event of events) {
|
|
270
|
-
StorageQueue.enqueue(event);
|
|
271
|
-
}
|
|
262
|
+
sendBatch(events, _retryCount) {
|
|
263
|
+
if (!events.length || this.config.disableServer)
|
|
272
264
|
return;
|
|
273
|
-
}
|
|
274
|
-
const payload = this.buildPayload(events);
|
|
275
265
|
try {
|
|
276
|
-
|
|
277
|
-
this.log(`Batch sent successfully: ${events.length} events`);
|
|
266
|
+
this.delivery.enqueue(this.buildPayload(events));
|
|
278
267
|
}
|
|
279
268
|
catch (error) {
|
|
280
|
-
this.log(
|
|
281
|
-
if (retryCount < this.config.maxRetryCount) {
|
|
282
|
-
const delay = 1000 * Math.pow(2, retryCount);
|
|
283
|
-
this.log(`Retrying in ${delay}ms...`);
|
|
284
|
-
this.retrying += 1;
|
|
285
|
-
setTimeout(() => {
|
|
286
|
-
this.retrying -= 1;
|
|
287
|
-
this.sendBatch(events, retryCount + 1);
|
|
288
|
-
if (this.buffer.length > 0) {
|
|
289
|
-
this.scheduleBufferFlush();
|
|
290
|
-
}
|
|
291
|
-
}, delay);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
this.log('Max retries reached, persisting events to localStorage');
|
|
295
|
-
for (const event of events) {
|
|
296
|
-
StorageQueue.enqueue(event);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
269
|
+
this.log('Unable to prepare analytics payload:', error);
|
|
299
270
|
}
|
|
300
271
|
}
|
|
301
272
|
buildPayload(events) {
|
|
@@ -309,32 +280,15 @@ export class Analytics {
|
|
|
309
280
|
events,
|
|
310
281
|
};
|
|
311
282
|
}
|
|
312
|
-
recoverFailedEvents() {
|
|
313
|
-
if (this.config.disableServer)
|
|
314
|
-
return;
|
|
315
|
-
if (!this.sampled)
|
|
316
|
-
return;
|
|
317
|
-
const failedEvents = StorageQueue.recover();
|
|
318
|
-
if (failedEvents.length > 0) {
|
|
319
|
-
this.log(`Recovered ${failedEvents.length} failed events from storage`);
|
|
320
|
-
for (const event of failedEvents) {
|
|
321
|
-
event.batch_ordering_id = this.generateBatchOrderingId();
|
|
322
|
-
event.batch_page_id = this.batchPageId;
|
|
323
|
-
event.batch_event_index = 0;
|
|
324
|
-
}
|
|
325
|
-
const batchSize = this.config.bufferMaxSize;
|
|
326
|
-
for (let i = 0; i < failedEvents.length; i += batchSize) {
|
|
327
|
-
const batch = failedEvents.slice(i, i + batchSize);
|
|
328
|
-
batch.forEach((e, idx) => {
|
|
329
|
-
e.batch_event_index = idx;
|
|
330
|
-
});
|
|
331
|
-
this.sendBatch(batch, 0);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
283
|
destroy() {
|
|
284
|
+
this.flushBuffer();
|
|
285
|
+
this.delivery.destroy();
|
|
286
|
+
if (this.visibilityHandler) {
|
|
287
|
+
document.removeEventListener('visibilitychange', this.visibilityHandler);
|
|
288
|
+
window.removeEventListener('pageshow', this.visibilityHandler);
|
|
289
|
+
this.visibilityHandler = null;
|
|
290
|
+
}
|
|
336
291
|
if (this.unloadHandler) {
|
|
337
|
-
window.removeEventListener('beforeunload', this.unloadHandler);
|
|
338
292
|
window.removeEventListener('pagehide', this.unloadHandler);
|
|
339
293
|
this.unloadHandler = null;
|
|
340
294
|
}
|
|
@@ -346,49 +300,22 @@ export class Analytics {
|
|
|
346
300
|
registerBeforeUnload() {
|
|
347
301
|
if (!isBrowser() || this.config.disableServer)
|
|
348
302
|
return;
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
|
|
362
|
-
const url = `${this.config.serverUrl}${this.config.serverUrl.includes('?') ? '&' : '?'}apiKey=${encodeURIComponent(apiKey)}`;
|
|
363
|
-
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
|
|
364
|
-
const success = navigator.sendBeacon(url, blob);
|
|
365
|
-
if (!success)
|
|
366
|
-
throw new Error('sendBeacon rejected');
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
// XHR sync as fallback — tries to send but doesn't block page unload
|
|
370
|
-
try {
|
|
371
|
-
const xhr = new XMLHttpRequest();
|
|
372
|
-
xhr.open('POST', this.config.serverUrl, false);
|
|
373
|
-
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
374
|
-
xhr.setRequestHeader('apiKey', apiKey);
|
|
375
|
-
xhr.timeout = 2000;
|
|
376
|
-
xhr.send(JSON.stringify(payload));
|
|
377
|
-
}
|
|
378
|
-
catch {
|
|
379
|
-
// XHR failed during unload
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
catch {
|
|
384
|
-
for (const event of events) {
|
|
385
|
-
StorageQueue.enqueue(event);
|
|
386
|
-
}
|
|
303
|
+
this.unloadHandler = () => {
|
|
304
|
+
this.delivery.setHidden(true);
|
|
305
|
+
this.flushBuffer();
|
|
306
|
+
this.delivery.flushHidden();
|
|
307
|
+
};
|
|
308
|
+
this.visibilityHandler = () => {
|
|
309
|
+
const hidden = document.visibilityState === 'hidden';
|
|
310
|
+
this.delivery.setHidden(hidden);
|
|
311
|
+
if (hidden) {
|
|
312
|
+
this.flushBuffer();
|
|
313
|
+
this.delivery.flushHidden();
|
|
387
314
|
}
|
|
388
315
|
};
|
|
389
|
-
this.unloadHandler
|
|
390
|
-
|
|
391
|
-
window.addEventListener('
|
|
316
|
+
window.addEventListener('pagehide', this.unloadHandler);
|
|
317
|
+
document.addEventListener('visibilitychange', this.visibilityHandler);
|
|
318
|
+
window.addEventListener('pageshow', this.visibilityHandler);
|
|
392
319
|
}
|
|
393
320
|
trackPageView() {
|
|
394
321
|
if (!isBrowser())
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AnalyticsPayload } from '../types';
|
|
2
|
+
interface Options {
|
|
3
|
+
serverUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
appKey: string;
|
|
6
|
+
maxRetries: number;
|
|
7
|
+
persistKeyEvents: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** First attempts are immediate. Only retries/recovery share a two-request pool. */
|
|
10
|
+
export declare class DeliveryQueue {
|
|
11
|
+
private options;
|
|
12
|
+
private jobs;
|
|
13
|
+
private activeRetries;
|
|
14
|
+
private hidden;
|
|
15
|
+
private timer;
|
|
16
|
+
private stopped;
|
|
17
|
+
private storagePrefix;
|
|
18
|
+
private maxAttempts;
|
|
19
|
+
private hiddenSent;
|
|
20
|
+
private hiddenSpent;
|
|
21
|
+
private keepaliveBytes;
|
|
22
|
+
constructor(options: Options);
|
|
23
|
+
enqueue(payload: AnalyticsPayload): void;
|
|
24
|
+
setHidden(hidden: boolean): void;
|
|
25
|
+
/** Submit the whole affordable rescue set synchronously; do not wait for ACKs. */
|
|
26
|
+
flushHidden(): void;
|
|
27
|
+
private bytes;
|
|
28
|
+
private batches;
|
|
29
|
+
private prune;
|
|
30
|
+
private pumpRetries;
|
|
31
|
+
private start;
|
|
32
|
+
private deliver;
|
|
33
|
+
private key;
|
|
34
|
+
private persist;
|
|
35
|
+
private remove;
|
|
36
|
+
private recover;
|
|
37
|
+
clear(): void;
|
|
38
|
+
destroy(): void;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=delivery-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-queue.d.ts","sourceRoot":"","sources":["../../src/core/delivery-queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAgB5C,UAAU,OAAO;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,oFAAoF;AACpF,qBAAa,aAAa;IAYZ,OAAO,CAAC,OAAO;IAX3B,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,MAAM,CAA4E;IAC1F,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,cAAc,CAAK;gBAEP,OAAO,EAAE,OAAO;IAOpC,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAqBxC,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAWhC,kFAAkF;IAClF,WAAW,IAAI,IAAI;IAmBnB,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,OAAO;IA6Bf,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,KAAK;YAaC,OAAO;IAyBrB,OAAO,CAAC,GAAG;IAEX,OAAO,CAAC,OAAO;IAOf,OAAO,CAAC,MAAM;IAOd,OAAO,CAAC,OAAO;IAoBf,KAAK,IAAI,IAAI;IAKb,OAAO,IAAI,IAAI;CAIhB"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Sender, DeliveryError } from './sender';
|
|
2
|
+
import { safeLocalStorageGet, safeLocalStorageSet, safeLocalStorageRemove } from '../utils';
|
|
3
|
+
const MAX_BYTES = 48 * 1024;
|
|
4
|
+
const MAX_EVENTS = 500;
|
|
5
|
+
const MAX_AGE = 24 * 60 * 60 * 1000;
|
|
6
|
+
/** First attempts are immediate. Only retries/recovery share a two-request pool. */
|
|
7
|
+
export class DeliveryQueue {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.jobs = [];
|
|
11
|
+
this.activeRetries = 0;
|
|
12
|
+
this.hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden';
|
|
13
|
+
this.timer = null;
|
|
14
|
+
this.stopped = false;
|
|
15
|
+
this.hiddenSent = new Set();
|
|
16
|
+
this.hiddenSpent = 0;
|
|
17
|
+
this.keepaliveBytes = 0;
|
|
18
|
+
this.maxAttempts = 1 + Math.min(5, Math.max(0, options.maxRetries || 0));
|
|
19
|
+
this.storagePrefix = `__analytics_delivery_v1__:${encodeURIComponent(options.appKey)}:${encodeURIComponent(options.serverUrl)}:`;
|
|
20
|
+
if (options.persistKeyEvents && options.apiKey)
|
|
21
|
+
this.recover();
|
|
22
|
+
this.pumpRetries();
|
|
23
|
+
}
|
|
24
|
+
enqueue(payload) {
|
|
25
|
+
if (this.stopped || !this.options.apiKey)
|
|
26
|
+
return;
|
|
27
|
+
const accepted = [];
|
|
28
|
+
for (const event of payload.events) {
|
|
29
|
+
if (this.jobs.length >= MAX_EVENTS)
|
|
30
|
+
break;
|
|
31
|
+
const single = { ...payload, events: [event] };
|
|
32
|
+
if (this.bytes(single) > MAX_BYTES)
|
|
33
|
+
continue;
|
|
34
|
+
const job = { payload: single, attempts: 0, createdAt: Date.now(), nextAt: 0,
|
|
35
|
+
active: 0, hiddenActive: false, terminal: false };
|
|
36
|
+
this.jobs.push(job);
|
|
37
|
+
this.persist(job);
|
|
38
|
+
accepted.push(job);
|
|
39
|
+
}
|
|
40
|
+
if (this.hidden) {
|
|
41
|
+
this.flushHidden();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Preserve normal batch delivery, but never queue a first attempt behind retries.
|
|
45
|
+
for (const batch of this.batches(accepted, MAX_BYTES))
|
|
46
|
+
this.start(batch.payload, batch.jobs, false, false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
setHidden(hidden) {
|
|
50
|
+
if (hidden && !this.hidden) {
|
|
51
|
+
this.hiddenSent.clear();
|
|
52
|
+
this.hiddenSpent = 0;
|
|
53
|
+
}
|
|
54
|
+
this.hidden = hidden;
|
|
55
|
+
if (this.timer) {
|
|
56
|
+
clearTimeout(this.timer);
|
|
57
|
+
this.timer = null;
|
|
58
|
+
}
|
|
59
|
+
if (!hidden)
|
|
60
|
+
this.pumpRetries();
|
|
61
|
+
// Caller flushes the SDK buffer before flushHidden, so critical jobs take priority.
|
|
62
|
+
}
|
|
63
|
+
/** Submit the whole affordable rescue set synchronously; do not wait for ACKs. */
|
|
64
|
+
flushHidden() {
|
|
65
|
+
if (this.stopped || !this.hidden)
|
|
66
|
+
return;
|
|
67
|
+
const available = Math.min(MAX_BYTES - this.hiddenSpent, MAX_BYTES - this.keepaliveBytes);
|
|
68
|
+
if (available <= 0)
|
|
69
|
+
return;
|
|
70
|
+
this.prune();
|
|
71
|
+
const candidates = this.jobs.filter(job => !job.terminal && !job.hiddenActive &&
|
|
72
|
+
!this.hiddenSent.has(job) && job.attempts < this.maxAttempts)
|
|
73
|
+
.sort((a, b) => Number(b.payload.events[0].key_event) - Number(a.payload.events[0].key_event));
|
|
74
|
+
let remaining = available;
|
|
75
|
+
for (const batch of this.batches(candidates, available, true)) {
|
|
76
|
+
const bytes = this.bytes(batch.payload);
|
|
77
|
+
if (bytes > remaining)
|
|
78
|
+
continue;
|
|
79
|
+
remaining -= bytes;
|
|
80
|
+
this.hiddenSpent += bytes;
|
|
81
|
+
for (const job of batch.jobs)
|
|
82
|
+
this.hiddenSent.add(job);
|
|
83
|
+
this.start(batch.payload, batch.jobs, true, false);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
bytes(payload) {
|
|
87
|
+
return new Blob([JSON.stringify(payload)]).size;
|
|
88
|
+
}
|
|
89
|
+
batches(jobs, budget, totalBudget = false) {
|
|
90
|
+
const result = [];
|
|
91
|
+
let remaining = budget;
|
|
92
|
+
// Context/client/session/marketing/user fields must match before events can share a request.
|
|
93
|
+
const groups = new Map();
|
|
94
|
+
for (const job of jobs) {
|
|
95
|
+
const { events, ...envelope } = job.payload;
|
|
96
|
+
const key = JSON.stringify(envelope);
|
|
97
|
+
let group = groups.get(key);
|
|
98
|
+
const proposed = { ...envelope, events: [...(group?.payload.events || []), ...events]
|
|
99
|
+
.map((event, index) => ({ ...event, batch_event_index: index })) };
|
|
100
|
+
const delta = this.bytes(proposed) - (group ? this.bytes(group.payload) : 0);
|
|
101
|
+
if (totalBudget && delta > remaining)
|
|
102
|
+
continue;
|
|
103
|
+
if (this.bytes(proposed) > budget) {
|
|
104
|
+
group = undefined;
|
|
105
|
+
}
|
|
106
|
+
if (!group) {
|
|
107
|
+
group = { payload: { ...envelope, events: events.map((event, index) => ({ ...event, batch_event_index: index })) }, jobs: [] };
|
|
108
|
+
groups.set(key, group);
|
|
109
|
+
result.push(group);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
group.payload = proposed;
|
|
113
|
+
}
|
|
114
|
+
group.jobs.push(job);
|
|
115
|
+
if (totalBudget)
|
|
116
|
+
remaining -= delta;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
prune() {
|
|
121
|
+
for (const job of [...this.jobs]) {
|
|
122
|
+
if (!job.active && (job.terminal || Date.now() - job.createdAt >= MAX_AGE || job.attempts >= this.maxAttempts))
|
|
123
|
+
this.remove(job);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
pumpRetries() {
|
|
127
|
+
if (this.stopped || this.hidden)
|
|
128
|
+
return;
|
|
129
|
+
if (this.timer) {
|
|
130
|
+
clearTimeout(this.timer);
|
|
131
|
+
this.timer = null;
|
|
132
|
+
}
|
|
133
|
+
this.prune();
|
|
134
|
+
while (this.activeRetries < 2) {
|
|
135
|
+
const job = this.jobs.find(item => !item.active && item.nextAt <= Date.now());
|
|
136
|
+
if (!job)
|
|
137
|
+
break;
|
|
138
|
+
this.start(job.payload, [job], false, true);
|
|
139
|
+
}
|
|
140
|
+
const waiting = this.jobs.filter(job => !job.active);
|
|
141
|
+
if (waiting.length && this.activeRetries < 2) {
|
|
142
|
+
const delay = Math.max(1, Math.min(...waiting.map(job => job.nextAt)) - Date.now());
|
|
143
|
+
this.timer = setTimeout(() => this.pumpRetries(), delay);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
start(payload, jobs, keepalive, retry) {
|
|
147
|
+
const bytes = keepalive ? this.bytes(payload) : 0;
|
|
148
|
+
this.keepaliveBytes += bytes;
|
|
149
|
+
if (retry)
|
|
150
|
+
this.activeRetries++;
|
|
151
|
+
for (const job of jobs) {
|
|
152
|
+
job.active++;
|
|
153
|
+
job.attempts++;
|
|
154
|
+
if (keepalive)
|
|
155
|
+
job.hiddenActive = true;
|
|
156
|
+
this.persist(job);
|
|
157
|
+
}
|
|
158
|
+
void this.deliver(payload, jobs, keepalive, retry, bytes);
|
|
159
|
+
}
|
|
160
|
+
async deliver(payload, jobs, keepalive, retry, bytes) {
|
|
161
|
+
try {
|
|
162
|
+
await Sender.send(payload, this.options.serverUrl, this.options.apiKey, keepalive);
|
|
163
|
+
for (const job of jobs)
|
|
164
|
+
this.remove(job);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
for (const job of jobs) {
|
|
168
|
+
if (!this.jobs.includes(job))
|
|
169
|
+
continue; // A concurrent rescue/normal request may have ACKed already.
|
|
170
|
+
if (error instanceof DeliveryError && !error.retryable)
|
|
171
|
+
job.terminal = true;
|
|
172
|
+
job.nextAt = Date.now() + Math.min(30000, 1000 * 2 ** (job.attempts - 1)) + Math.floor(Math.random() * 250);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
for (const job of jobs) {
|
|
177
|
+
job.active--;
|
|
178
|
+
if (keepalive)
|
|
179
|
+
job.hiddenActive = false;
|
|
180
|
+
if (!this.jobs.includes(job))
|
|
181
|
+
continue;
|
|
182
|
+
// Wait for both original and rescue outcomes before retiring a failed event.
|
|
183
|
+
if (!job.active && (job.terminal || job.attempts >= this.maxAttempts))
|
|
184
|
+
this.remove(job);
|
|
185
|
+
else
|
|
186
|
+
this.persist(job);
|
|
187
|
+
}
|
|
188
|
+
this.keepaliveBytes -= bytes;
|
|
189
|
+
if (retry)
|
|
190
|
+
this.activeRetries--;
|
|
191
|
+
this.pumpRetries();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
key(eventId) { return this.storagePrefix + eventId; }
|
|
195
|
+
persist(job) {
|
|
196
|
+
if (!this.options.persistKeyEvents || !job.payload.events[0].key_event)
|
|
197
|
+
return;
|
|
198
|
+
safeLocalStorageSet(this.key(job.payload.events[0].event_id), JSON.stringify({
|
|
199
|
+
payload: job.payload, attempts: job.attempts, createdAt: job.createdAt, nextAt: job.nextAt,
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
remove(job) {
|
|
203
|
+
if (!this.jobs.includes(job))
|
|
204
|
+
return;
|
|
205
|
+
this.jobs = this.jobs.filter(item => item !== job);
|
|
206
|
+
this.hiddenSent.delete(job);
|
|
207
|
+
if (this.options.persistKeyEvents)
|
|
208
|
+
safeLocalStorageRemove(this.key(job.payload.events[0].event_id));
|
|
209
|
+
}
|
|
210
|
+
recover() {
|
|
211
|
+
try {
|
|
212
|
+
const keys = Object.keys(localStorage).filter(key => key.startsWith(this.storagePrefix));
|
|
213
|
+
for (const key of keys) {
|
|
214
|
+
try {
|
|
215
|
+
const job = JSON.parse(safeLocalStorageGet(key) || 'null');
|
|
216
|
+
if (!job || !Number.isFinite(job.createdAt) || !Number.isInteger(job.attempts) || job.attempts < 0 ||
|
|
217
|
+
!Number.isFinite(job.nextAt) || Date.now() - job.createdAt >= MAX_AGE || job.attempts >= this.maxAttempts ||
|
|
218
|
+
!Array.isArray(job.payload?.events) || job.payload.events.length !== 1 ||
|
|
219
|
+
!job.payload.events[0].key_event || this.key(job.payload.events[0].event_id) !== key ||
|
|
220
|
+
this.bytes(job.payload) > MAX_BYTES || this.jobs.length >= MAX_EVENTS) {
|
|
221
|
+
safeLocalStorageRemove(key);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
this.jobs.push({ ...job, active: 0, hiddenActive: false, terminal: false });
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
safeLocalStorageRemove(key);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch { /* Storage unavailable: delivery continues in memory. */ }
|
|
232
|
+
}
|
|
233
|
+
clear() {
|
|
234
|
+
for (const job of [...this.jobs])
|
|
235
|
+
this.remove(job);
|
|
236
|
+
if (this.timer) {
|
|
237
|
+
clearTimeout(this.timer);
|
|
238
|
+
this.timer = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
destroy() {
|
|
242
|
+
this.stopped = true;
|
|
243
|
+
if (this.timer) {
|
|
244
|
+
clearTimeout(this.timer);
|
|
245
|
+
this.timer = null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
package/dist/core/sender.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { AnalyticsPayload } from '../types';
|
|
2
|
+
export declare class DeliveryError extends Error {
|
|
3
|
+
retryable: boolean;
|
|
4
|
+
constructor(message: string, retryable: boolean);
|
|
5
|
+
}
|
|
2
6
|
export declare class Sender {
|
|
3
|
-
static send(payload: AnalyticsPayload, serverUrl: string, apiKey: string): Promise<void>;
|
|
7
|
+
static send(payload: AnalyticsPayload, serverUrl: string, apiKey: string, keepalive?: boolean): Promise<void>;
|
|
4
8
|
static sendBeacon(payload: AnalyticsPayload, serverUrl: string, apiKey: string): Promise<void>;
|
|
5
|
-
static fetchSend(payload: AnalyticsPayload, serverUrl: string, apiKey: string): Promise<void>;
|
|
9
|
+
static fetchSend(payload: AnalyticsPayload, serverUrl: string, apiKey: string, keepalive?: boolean): Promise<void>;
|
|
10
|
+
private static httpError;
|
|
6
11
|
static xhrSend(payload: AnalyticsPayload, serverUrl: string, apiKey: string): Promise<void>;
|
|
7
12
|
private static supportsKeepalive;
|
|
8
|
-
private static buildBeaconUrl;
|
|
9
13
|
}
|
|
10
14
|
//# sourceMappingURL=sender.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sender.d.ts","sourceRoot":"","sources":["../../src/core/sender.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE5C,qBAAa,MAAM;WACJ,IAAI,CAAC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"sender.d.ts","sourceRoot":"","sources":["../../src/core/sender.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE5C,qBAAa,aAAc,SAAQ,KAAK;IACF,SAAS,EAAE,OAAO;gBAA1C,OAAO,EAAE,MAAM,EAAS,SAAS,EAAE,OAAO;CACvD;AAED,qBAAa,MAAM;WACJ,IAAI,CAAC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjH,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;WAIjF,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBtH,OAAO,CAAC,MAAM,CAAC,SAAS;IAIxB,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe3F,OAAO,CAAC,MAAM,CAAC,iBAAiB;CAGjC"}
|
package/dist/core/sender.js
CHANGED
|
@@ -1,65 +1,61 @@
|
|
|
1
|
+
export class DeliveryError extends Error {
|
|
2
|
+
constructor(message, retryable) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.retryable = retryable;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
1
7
|
export class Sender {
|
|
2
|
-
static async send(payload, serverUrl, apiKey) {
|
|
3
|
-
if (
|
|
4
|
-
|
|
8
|
+
static async send(payload, serverUrl, apiKey, keepalive = false) {
|
|
9
|
+
if (typeof fetch === 'function' && typeof AbortController !== 'undefined') {
|
|
10
|
+
if (keepalive && !Sender.supportsKeepalive()) {
|
|
11
|
+
throw new DeliveryError('Keepalive unavailable', true);
|
|
12
|
+
}
|
|
13
|
+
return Sender.fetchSend(payload, serverUrl, apiKey, keepalive);
|
|
5
14
|
}
|
|
15
|
+
if (keepalive)
|
|
16
|
+
throw new DeliveryError('Unload transport unavailable', true);
|
|
6
17
|
return Sender.xhrSend(payload, serverUrl, apiKey);
|
|
7
18
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
// Compatibility entry point: authenticated delivery must support custom headers.
|
|
20
|
+
static sendBeacon(payload, serverUrl, apiKey) {
|
|
21
|
+
return Sender.send(payload, serverUrl, apiKey, true);
|
|
22
|
+
}
|
|
23
|
+
static async fetchSend(payload, serverUrl, apiKey, keepalive = false) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), 10000);
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(serverUrl, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json', apiKey },
|
|
30
|
+
body: JSON.stringify(payload),
|
|
31
|
+
keepalive,
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
throw Sender.httpError(response.status);
|
|
11
36
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const success = navigator.sendBeacon(url, blob);
|
|
15
|
-
if (!success) {
|
|
16
|
-
throw new Error('sendBeacon rejected payload');
|
|
37
|
+
finally {
|
|
38
|
+
clearTimeout(timer);
|
|
17
39
|
}
|
|
18
40
|
}
|
|
19
|
-
static
|
|
20
|
-
|
|
21
|
-
method: 'POST',
|
|
22
|
-
headers: {
|
|
23
|
-
'Content-Type': 'application/json',
|
|
24
|
-
apiKey,
|
|
25
|
-
},
|
|
26
|
-
body: JSON.stringify(payload),
|
|
27
|
-
keepalive: true,
|
|
28
|
-
});
|
|
29
|
-
if (!response.ok) {
|
|
30
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
31
|
-
}
|
|
41
|
+
static httpError(status) {
|
|
42
|
+
return new DeliveryError(`HTTP ${status}`, status === 408 || status === 429 || status >= 500);
|
|
32
43
|
}
|
|
33
|
-
static
|
|
44
|
+
static xhrSend(payload, serverUrl, apiKey) {
|
|
34
45
|
return new Promise((resolve, reject) => {
|
|
35
46
|
const xhr = new XMLHttpRequest();
|
|
36
|
-
xhr.open('POST', serverUrl);
|
|
47
|
+
xhr.open('POST', serverUrl, true);
|
|
37
48
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
38
49
|
xhr.setRequestHeader('apiKey', apiKey);
|
|
39
50
|
xhr.timeout = 10000;
|
|
40
|
-
xhr.onload = () =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
else {
|
|
45
|
-
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
xhr.onerror = () => reject(new Error('XHR network error'));
|
|
49
|
-
xhr.ontimeout = () => reject(new Error('XHR timeout'));
|
|
51
|
+
xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(Sender.httpError(xhr.status));
|
|
52
|
+
xhr.onerror = () => reject(new DeliveryError('XHR network error', true));
|
|
53
|
+
xhr.ontimeout = () => reject(new DeliveryError('XHR timeout', true));
|
|
54
|
+
xhr.onabort = () => reject(new DeliveryError('XHR aborted', true));
|
|
50
55
|
xhr.send(JSON.stringify(payload));
|
|
51
56
|
});
|
|
52
57
|
}
|
|
53
58
|
static supportsKeepalive() {
|
|
54
|
-
|
|
55
|
-
return typeof Request !== 'undefined' && 'keepalive' in Request.prototype;
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
static buildBeaconUrl(serverUrl, apiKey) {
|
|
62
|
-
const separator = serverUrl.includes('?') ? '&' : '?';
|
|
63
|
-
return `${serverUrl}${separator}apiKey=${encodeURIComponent(apiKey)}`;
|
|
59
|
+
return typeof Request !== 'undefined' && 'keepalive' in Request.prototype;
|
|
64
60
|
}
|
|
65
61
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -102,6 +102,10 @@ export interface AnalyticsConfig {
|
|
|
102
102
|
gtm?: GTMConfig;
|
|
103
103
|
disableServer?: boolean;
|
|
104
104
|
sampleRate?: number;
|
|
105
|
+
/** Persist key events until HTTP acknowledgement (24h / 500-event bound).
|
|
106
|
+
* Enable only for applications whose event properties are safe to store locally.
|
|
107
|
+
*/
|
|
108
|
+
persistKeyEvents?: boolean;
|
|
105
109
|
/** @deprecated use appKey instead */
|
|
106
110
|
appId?: string;
|
|
107
111
|
}
|
|
@@ -135,7 +139,7 @@ export interface RetryEntry {
|
|
|
135
139
|
timer: ReturnType<typeof setTimeout> | null;
|
|
136
140
|
}
|
|
137
141
|
export declare const API_KEY_MAP: Record<string, string>;
|
|
138
|
-
export declare const SDK_VERSION = "1.
|
|
142
|
+
export declare const SDK_VERSION = "1.3.2";
|
|
139
143
|
export declare const DEFAULT_CONFIG: {
|
|
140
144
|
readonly debug: false;
|
|
141
145
|
readonly autoTrack: true;
|