@eventra_dev/eventra-sdk 1.1.2 → 1.1.4
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 +82 -21
- package/dist/index.cjs +441 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -3
- package/dist/index.d.ts +32 -3
- package/dist/index.mjs +441 -86
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -25,23 +25,127 @@ __export(index_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(index_exports);
|
|
26
26
|
|
|
27
27
|
// src/client.ts
|
|
28
|
+
var SDK_NAME = "@eventra_dev/eventra-sdk";
|
|
29
|
+
var DEFAULT_ENDPOINT = "https://api.eventra.dev/api/v1/ingest/batch";
|
|
30
|
+
var SDK_VERSION = "1.1.4";
|
|
31
|
+
var LEADER_KEY = "__eventra_leader__";
|
|
32
|
+
var LEADER_TTL_MS = 4e3;
|
|
33
|
+
var QUEUE_KEY = "__eventra_q__";
|
|
34
|
+
var CHANNEL_NAME = "eventra-sdk";
|
|
35
|
+
var MAX_EVENT_NAME = 64;
|
|
36
|
+
var MAX_USER_ID = 120;
|
|
37
|
+
var MAX_PROPERTIES_JSON_BYTES = 32e3;
|
|
38
|
+
var MAX_PROPERTIES_DEPTH = 8;
|
|
39
|
+
var DEFAULT_MAX_PAYLOAD_BYTES = 6e4;
|
|
40
|
+
var DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
|
|
41
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
42
|
+
var CIRCUIT_FAILURE_THRESHOLD = 5;
|
|
43
|
+
var CIRCUIT_COOLDOWN_MS = 5e3;
|
|
44
|
+
var activeInstances = 0;
|
|
28
45
|
function detectRuntime() {
|
|
29
46
|
const g = globalThis;
|
|
30
|
-
if (typeof window !== "undefined") return "browser";
|
|
47
|
+
if (typeof window !== "undefined" && !g.EdgeRuntime) return "browser";
|
|
31
48
|
if (g.EdgeRuntime) return "edge";
|
|
32
49
|
if (g.process?.env?.AWS_LAMBDA_FUNCTION_NAME) return "serverless";
|
|
33
50
|
if (g.process?.versions?.node) return "node";
|
|
34
51
|
return "unknown";
|
|
35
52
|
}
|
|
36
|
-
function
|
|
37
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
53
|
+
function uuidV4() {
|
|
54
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
38
55
|
return crypto.randomUUID();
|
|
39
56
|
}
|
|
40
|
-
|
|
57
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
58
|
+
const bytes = new Uint8Array(16);
|
|
59
|
+
crypto.getRandomValues(bytes);
|
|
60
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
61
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
62
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
63
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
64
|
+
}
|
|
65
|
+
throw new Error("Eventra: crypto API unavailable \u2014 cannot generate idempotencyKey");
|
|
41
66
|
}
|
|
42
67
|
function sleep(ms) {
|
|
43
68
|
return new Promise((r) => setTimeout(r, ms));
|
|
44
69
|
}
|
|
70
|
+
function truncate(value, max) {
|
|
71
|
+
if (value === void 0) return void 0;
|
|
72
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
73
|
+
}
|
|
74
|
+
function validateEventName(name) {
|
|
75
|
+
const trimmed = name.trim();
|
|
76
|
+
if (!trimmed) {
|
|
77
|
+
throw new Error("Eventra: event name is required");
|
|
78
|
+
}
|
|
79
|
+
if (trimmed.length > MAX_EVENT_NAME) {
|
|
80
|
+
throw new Error(`Eventra: event name exceeds max length (${MAX_EVENT_NAME})`);
|
|
81
|
+
}
|
|
82
|
+
return trimmed;
|
|
83
|
+
}
|
|
84
|
+
function backoffMs(attempt, base) {
|
|
85
|
+
const exp = Math.min(DEFAULT_MAX_RETRY_DELAY_MS, base * 2 ** attempt);
|
|
86
|
+
const jitter = exp * (0.5 + Math.random() * 0.5);
|
|
87
|
+
return jitter;
|
|
88
|
+
}
|
|
89
|
+
function utf8ByteLength(value) {
|
|
90
|
+
if (typeof TextEncoder !== "undefined") {
|
|
91
|
+
return new TextEncoder().encode(value).length;
|
|
92
|
+
}
|
|
93
|
+
const g = globalThis;
|
|
94
|
+
if (g.Buffer) {
|
|
95
|
+
return g.Buffer.byteLength(value, "utf8");
|
|
96
|
+
}
|
|
97
|
+
return value.length;
|
|
98
|
+
}
|
|
99
|
+
function mergeQueues(...queues) {
|
|
100
|
+
const seen = /* @__PURE__ */ new Set();
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const q of queues) {
|
|
103
|
+
for (const e of q) {
|
|
104
|
+
if (seen.has(e.idempotencyKey)) continue;
|
|
105
|
+
seen.add(e.idempotencyKey);
|
|
106
|
+
out.push(e);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function validateProperties(properties) {
|
|
112
|
+
const depth = (obj, level) => {
|
|
113
|
+
if (level > MAX_PROPERTIES_DEPTH) {
|
|
114
|
+
throw new Error(`Eventra: properties exceed max depth (${MAX_PROPERTIES_DEPTH})`);
|
|
115
|
+
}
|
|
116
|
+
if (obj === null || typeof obj !== "object") return;
|
|
117
|
+
if (Array.isArray(obj)) {
|
|
118
|
+
for (const item of obj) depth(item, level + 1);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
for (const value of Object.values(obj)) {
|
|
122
|
+
depth(value, level + 1);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
depth(properties, 0);
|
|
126
|
+
const json = JSON.stringify(properties);
|
|
127
|
+
if (utf8ByteLength(json) > MAX_PROPERTIES_JSON_BYTES) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Eventra: properties exceed max size (${MAX_PROPERTIES_JSON_BYTES} bytes)`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return properties;
|
|
133
|
+
}
|
|
134
|
+
function estimateEnvelopeBytes(events, sdk, sentAt) {
|
|
135
|
+
return utf8ByteLength(
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
sentAt,
|
|
138
|
+
sdk,
|
|
139
|
+
events
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
var RetryableDeliveryError = class extends Error {
|
|
144
|
+
constructor(message = "retryable delivery error") {
|
|
145
|
+
super(message);
|
|
146
|
+
this.name = "RetryableDeliveryError";
|
|
147
|
+
}
|
|
148
|
+
};
|
|
45
149
|
var Storage = class {
|
|
46
150
|
constructor() {
|
|
47
151
|
this.enabled = false;
|
|
@@ -69,62 +173,167 @@ var Storage = class {
|
|
|
69
173
|
} catch {
|
|
70
174
|
}
|
|
71
175
|
}
|
|
176
|
+
remove(key) {
|
|
177
|
+
if (!this.enabled) return;
|
|
178
|
+
try {
|
|
179
|
+
localStorage.removeItem(key);
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
72
183
|
};
|
|
73
184
|
var Leader = class {
|
|
74
|
-
constructor(
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
|
|
185
|
+
constructor(onLeadershipChange) {
|
|
186
|
+
this.onLeadershipChange = onLeadershipChange;
|
|
187
|
+
this.tabId = uuidV4();
|
|
188
|
+
this.isLeader = false;
|
|
189
|
+
if (typeof localStorage === "undefined") {
|
|
190
|
+
this.isLeader = true;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.tick();
|
|
194
|
+
this.election = setInterval(() => this.tick(), LEADER_TTL_MS / 2);
|
|
195
|
+
if (typeof window !== "undefined") {
|
|
196
|
+
this.onStorage = (e) => {
|
|
197
|
+
if (e.key === LEADER_KEY) this.tick();
|
|
81
198
|
};
|
|
82
|
-
|
|
199
|
+
window.addEventListener("storage", this.onStorage);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
tick() {
|
|
203
|
+
const was = this.isLeader;
|
|
204
|
+
if (this.isLeader) {
|
|
205
|
+
if (!this.verifyLease()) {
|
|
206
|
+
this.isLeader = false;
|
|
207
|
+
} else {
|
|
208
|
+
this.renew();
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
this.isLeader = this.tryAcquire();
|
|
212
|
+
if (this.isLeader) {
|
|
213
|
+
if (!this.heartbeat) {
|
|
214
|
+
this.heartbeat = setInterval(() => this.renew(), LEADER_TTL_MS / 2);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!this.isLeader && this.heartbeat) {
|
|
219
|
+
clearInterval(this.heartbeat);
|
|
220
|
+
this.heartbeat = void 0;
|
|
221
|
+
}
|
|
222
|
+
if (was !== this.isLeader) {
|
|
223
|
+
this.onLeadershipChange?.(this.isLeader);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
tryAcquire() {
|
|
227
|
+
try {
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
const raw = localStorage.getItem(LEADER_KEY);
|
|
230
|
+
if (!raw) {
|
|
231
|
+
this.renew();
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
const parsed = JSON.parse(raw);
|
|
235
|
+
if (now - parsed.ts > LEADER_TTL_MS) {
|
|
236
|
+
this.renew();
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
if (parsed.tabId === this.tabId) {
|
|
240
|
+
this.renew();
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
} catch {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
verifyLease() {
|
|
249
|
+
try {
|
|
250
|
+
const raw = localStorage.getItem(LEADER_KEY);
|
|
251
|
+
if (!raw) return false;
|
|
252
|
+
const parsed = JSON.parse(raw);
|
|
253
|
+
return parsed.tabId === this.tabId && Date.now() - parsed.ts <= LEADER_TTL_MS;
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
renew() {
|
|
259
|
+
try {
|
|
260
|
+
localStorage.setItem(
|
|
261
|
+
LEADER_KEY,
|
|
262
|
+
JSON.stringify({ ts: Date.now(), tabId: this.tabId })
|
|
263
|
+
);
|
|
264
|
+
} catch {
|
|
83
265
|
}
|
|
84
266
|
}
|
|
85
267
|
canSend() {
|
|
86
268
|
return this.isLeader;
|
|
87
269
|
}
|
|
88
270
|
destroy() {
|
|
89
|
-
this.
|
|
271
|
+
if (this.election) clearInterval(this.election);
|
|
272
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
273
|
+
if (this.onStorage && typeof window !== "undefined") {
|
|
274
|
+
window.removeEventListener("storage", this.onStorage);
|
|
275
|
+
}
|
|
276
|
+
if (this.isLeader && typeof localStorage !== "undefined") {
|
|
277
|
+
try {
|
|
278
|
+
const raw = localStorage.getItem(LEADER_KEY);
|
|
279
|
+
if (raw) {
|
|
280
|
+
const parsed = JSON.parse(raw);
|
|
281
|
+
if (parsed.tabId === this.tabId) {
|
|
282
|
+
localStorage.removeItem(LEADER_KEY);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
90
288
|
}
|
|
91
289
|
};
|
|
92
290
|
var Eventra = class {
|
|
93
291
|
constructor(options) {
|
|
94
292
|
this.queue = [];
|
|
293
|
+
this.sending = null;
|
|
95
294
|
this.inFlight = false;
|
|
96
295
|
this.destroyed = false;
|
|
296
|
+
this.shuttingDown = false;
|
|
97
297
|
this.storage = new Storage();
|
|
98
298
|
this.leader = null;
|
|
99
299
|
this.failureCount = 0;
|
|
100
300
|
this.circuitOpenUntil = 0;
|
|
301
|
+
this.circuitHalfOpen = false;
|
|
101
302
|
this.exitHandlers = [];
|
|
102
|
-
if (!options.apiKey) throw new Error("apiKey required");
|
|
103
|
-
|
|
303
|
+
if (!options.apiKey) throw new Error("Eventra: apiKey required");
|
|
304
|
+
activeInstances++;
|
|
305
|
+
if (activeInstances > 1 && typeof console !== "undefined") {
|
|
306
|
+
console.warn(
|
|
307
|
+
"Eventra: multiple SDK instances detected \u2014 duplicate timers and sends are possible"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
104
310
|
this.options = options;
|
|
105
311
|
this.apiKey = options.apiKey;
|
|
106
|
-
this.endpoint = options.endpoint;
|
|
312
|
+
this.endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
107
313
|
this.runtime = detectRuntime();
|
|
108
314
|
this.fetch = options.fetchImpl ?? globalThis.fetch;
|
|
109
315
|
if (!this.fetch) {
|
|
110
|
-
throw new Error("fetch not available \u2014 provide fetchImpl");
|
|
316
|
+
throw new Error("Eventra: fetch not available \u2014 provide fetchImpl");
|
|
111
317
|
}
|
|
112
318
|
this.maxBatch = options.maxBatchSize ?? 50;
|
|
113
319
|
this.maxQueue = options.maxQueueSize ?? 1e4;
|
|
320
|
+
this.maxPayloadBytes = options.maxPayloadBytes ?? DEFAULT_MAX_PAYLOAD_BYTES;
|
|
114
321
|
this.flushInterval = options.flushInterval ?? 2e3;
|
|
115
322
|
this.retries = options.maxRetries ?? 3;
|
|
116
323
|
this.retryDelay = options.retryBaseDelayMs ?? 300;
|
|
117
324
|
this.sdkInfo = {
|
|
118
|
-
name:
|
|
119
|
-
version:
|
|
325
|
+
name: SDK_NAME,
|
|
326
|
+
version: SDK_VERSION,
|
|
120
327
|
runtime: this.runtime
|
|
121
328
|
};
|
|
122
329
|
if (this.runtime === "browser") {
|
|
123
|
-
|
|
124
|
-
|
|
330
|
+
this.loadAndMergeQueue();
|
|
331
|
+
this.setupQueueSync();
|
|
125
332
|
}
|
|
126
333
|
if (this.runtime === "browser" && options.multiTabMode === "leader") {
|
|
127
|
-
this.leader = new Leader(
|
|
334
|
+
this.leader = new Leader((isLeader) => {
|
|
335
|
+
if (isLeader) void this.flush();
|
|
336
|
+
});
|
|
128
337
|
}
|
|
129
338
|
if (!options.disableTimer) {
|
|
130
339
|
this.startTimer();
|
|
@@ -136,23 +345,31 @@ var Eventra = class {
|
|
|
136
345
|
this.setupProcessExit();
|
|
137
346
|
}
|
|
138
347
|
}
|
|
139
|
-
// ================= PUBLIC =================
|
|
140
348
|
track(name, options) {
|
|
141
|
-
if (this.destroyed) return;
|
|
349
|
+
if (this.destroyed || this.shuttingDown) return;
|
|
350
|
+
let eventName;
|
|
351
|
+
let properties;
|
|
352
|
+
try {
|
|
353
|
+
eventName = validateEventName(name);
|
|
354
|
+
properties = validateProperties(options?.properties ?? {});
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const message = err instanceof Error ? err.message : "invalid event";
|
|
357
|
+
throw new Error(message);
|
|
358
|
+
}
|
|
142
359
|
if (this.queue.length >= this.maxQueue) {
|
|
143
360
|
this.options.onEventsDropped?.(1);
|
|
144
361
|
return;
|
|
145
362
|
}
|
|
146
363
|
const event = {
|
|
147
|
-
idempotencyKey:
|
|
148
|
-
name,
|
|
149
|
-
userId: options?.userId,
|
|
150
|
-
properties
|
|
364
|
+
idempotencyKey: uuidV4(),
|
|
365
|
+
name: eventName,
|
|
366
|
+
userId: truncate(options?.userId, MAX_USER_ID),
|
|
367
|
+
properties,
|
|
151
368
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
152
369
|
};
|
|
153
370
|
this.queue.push(event);
|
|
154
371
|
if (this.runtime === "browser") {
|
|
155
|
-
this.
|
|
372
|
+
this.syncQueue();
|
|
156
373
|
}
|
|
157
374
|
if (this.queue.length >= this.maxBatch) {
|
|
158
375
|
void this.flush();
|
|
@@ -163,54 +380,134 @@ var Eventra = class {
|
|
|
163
380
|
}
|
|
164
381
|
async flush() {
|
|
165
382
|
if (this.inFlight || this.destroyed) return;
|
|
166
|
-
if (!this.queue.length) return;
|
|
383
|
+
if (!this.queue.length && !this.sending?.length) return;
|
|
167
384
|
if (this.leader && !this.leader.canSend()) return;
|
|
168
|
-
|
|
169
|
-
this.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this.
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
if (now < this.circuitOpenUntil) return;
|
|
387
|
+
if (this.circuitOpenUntil > 0 && now >= this.circuitOpenUntil) {
|
|
388
|
+
this.circuitHalfOpen = true;
|
|
389
|
+
this.circuitOpenUntil = 0;
|
|
173
390
|
}
|
|
391
|
+
this.inFlight = true;
|
|
174
392
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
393
|
+
while (this.queue.length > 0) {
|
|
394
|
+
const batch = this.buildBatch();
|
|
395
|
+
if (!batch.length) break;
|
|
396
|
+
this.sending = batch;
|
|
397
|
+
try {
|
|
398
|
+
await this.send(batch);
|
|
399
|
+
this.removeDelivered(batch);
|
|
400
|
+
this.failureCount = 0;
|
|
401
|
+
this.circuitHalfOpen = false;
|
|
402
|
+
} catch (err) {
|
|
403
|
+
if (err instanceof RetryableDeliveryError) {
|
|
404
|
+
this.failureCount++;
|
|
405
|
+
if (this.circuitHalfOpen || this.failureCount >= CIRCUIT_FAILURE_THRESHOLD) {
|
|
406
|
+
this.circuitOpenUntil = Date.now() + CIRCUIT_COOLDOWN_MS;
|
|
407
|
+
this.circuitHalfOpen = false;
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
} finally {
|
|
413
|
+
this.sending = null;
|
|
414
|
+
}
|
|
415
|
+
if (this.circuitOpenUntil > Date.now()) break;
|
|
181
416
|
}
|
|
182
|
-
this.
|
|
183
|
-
|
|
184
|
-
this.circuitOpenUntil = Date.now() + 5e3;
|
|
417
|
+
if (this.runtime === "browser") {
|
|
418
|
+
this.syncQueue();
|
|
185
419
|
}
|
|
186
420
|
} finally {
|
|
187
421
|
this.inFlight = false;
|
|
188
422
|
}
|
|
189
423
|
}
|
|
424
|
+
/** Flush pending events, then tear down timers and listeners */
|
|
425
|
+
async shutdown() {
|
|
426
|
+
if (this.shuttingDown) return;
|
|
427
|
+
this.shuttingDown = true;
|
|
428
|
+
try {
|
|
429
|
+
await this.flush();
|
|
430
|
+
} finally {
|
|
431
|
+
this.destroy();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
190
434
|
destroy() {
|
|
435
|
+
if (this.destroyed) return;
|
|
191
436
|
this.destroyed = true;
|
|
437
|
+
activeInstances = Math.max(0, activeInstances - 1);
|
|
192
438
|
if (this.timer) clearInterval(this.timer);
|
|
193
439
|
this.leader?.destroy();
|
|
440
|
+
if (this.channel) {
|
|
441
|
+
this.channel.close();
|
|
442
|
+
this.channel = void 0;
|
|
443
|
+
}
|
|
444
|
+
if (this.onStorageQueue && typeof window !== "undefined") {
|
|
445
|
+
window.removeEventListener("storage", this.onStorageQueue);
|
|
446
|
+
}
|
|
194
447
|
for (const off of this.exitHandlers) {
|
|
195
448
|
off();
|
|
196
449
|
}
|
|
197
450
|
}
|
|
198
|
-
|
|
451
|
+
buildBatch() {
|
|
452
|
+
const batch = [];
|
|
453
|
+
const sentAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
454
|
+
let index = 0;
|
|
455
|
+
while (index < this.queue.length && batch.length < this.maxBatch) {
|
|
456
|
+
const event = this.queue[index];
|
|
457
|
+
const candidate = [...batch, event];
|
|
458
|
+
const bytes = estimateEnvelopeBytes(candidate, this.sdkInfo, sentAt);
|
|
459
|
+
if (bytes > this.maxPayloadBytes) {
|
|
460
|
+
if (batch.length === 0) {
|
|
461
|
+
const poisonKey = event.idempotencyKey;
|
|
462
|
+
this.dropPoisonEvent(event);
|
|
463
|
+
if (this.queue[index]?.idempotencyKey === poisonKey) {
|
|
464
|
+
index++;
|
|
465
|
+
}
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
batch.push(event);
|
|
471
|
+
index++;
|
|
472
|
+
}
|
|
473
|
+
return batch;
|
|
474
|
+
}
|
|
475
|
+
dropPoisonEvent(event) {
|
|
476
|
+
this.removeDelivered([event]);
|
|
477
|
+
this.options.onEventsDropped?.(1);
|
|
478
|
+
this.options.onDeliveryFailed?.({ status: 413, events: [event] });
|
|
479
|
+
if (this.runtime === "browser") {
|
|
480
|
+
this.syncQueue();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
removeDelivered(batch) {
|
|
484
|
+
const keys = new Set(batch.map((e) => e.idempotencyKey));
|
|
485
|
+
this.queue = this.queue.filter((e) => !keys.has(e.idempotencyKey));
|
|
486
|
+
}
|
|
199
487
|
async send(events) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
488
|
+
let payload;
|
|
489
|
+
try {
|
|
490
|
+
payload = JSON.stringify({
|
|
491
|
+
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
492
|
+
sdk: this.sdkInfo,
|
|
493
|
+
events
|
|
494
|
+
});
|
|
495
|
+
} catch {
|
|
496
|
+
this.options.onDeliveryFailed?.({ status: 0, events });
|
|
497
|
+
this.removeDelivered(events);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const payloadBytes = utf8ByteLength(payload);
|
|
501
|
+
if (payloadBytes > this.maxPayloadBytes) {
|
|
502
|
+
this.dropPoisonEvents(events);
|
|
207
503
|
return;
|
|
208
504
|
}
|
|
209
|
-
|
|
210
|
-
|
|
505
|
+
const useKeepalive = this.runtime === "browser" && typeof document !== "undefined" && document.visibilityState === "hidden" && payloadBytes <= this.maxPayloadBytes;
|
|
506
|
+
const maxAttempts = Math.max(1, this.retries);
|
|
507
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
508
|
+
const controller = new AbortController();
|
|
509
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
211
510
|
try {
|
|
212
|
-
const controller = new AbortController();
|
|
213
|
-
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
214
511
|
const res = await this.fetch(this.endpoint, {
|
|
215
512
|
method: "POST",
|
|
216
513
|
headers: {
|
|
@@ -218,20 +515,86 @@ var Eventra = class {
|
|
|
218
515
|
"x-api-key": this.apiKey
|
|
219
516
|
},
|
|
220
517
|
body: payload,
|
|
221
|
-
signal: controller.signal
|
|
518
|
+
signal: controller.signal,
|
|
519
|
+
keepalive: useKeepalive
|
|
222
520
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
521
|
+
if (res.status === 429 || res.status >= 500) {
|
|
522
|
+
throw new RetryableDeliveryError();
|
|
523
|
+
}
|
|
524
|
+
if (res.status >= 400 && res.status < 500) {
|
|
525
|
+
this.options.onDeliveryFailed?.({ status: res.status, events });
|
|
526
|
+
this.removeDelivered(events);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (!res.ok) {
|
|
530
|
+
throw new RetryableDeliveryError();
|
|
531
|
+
}
|
|
226
532
|
return;
|
|
227
|
-
} catch {
|
|
228
|
-
attempt
|
|
229
|
-
if (
|
|
230
|
-
|
|
533
|
+
} catch (err) {
|
|
534
|
+
const isLast = attempt >= maxAttempts - 1;
|
|
535
|
+
if (isLast) {
|
|
536
|
+
throw err instanceof RetryableDeliveryError ? err : new RetryableDeliveryError();
|
|
537
|
+
}
|
|
538
|
+
await sleep(backoffMs(attempt + 1, this.retryDelay));
|
|
539
|
+
} finally {
|
|
540
|
+
clearTimeout(timeout);
|
|
231
541
|
}
|
|
232
542
|
}
|
|
233
543
|
}
|
|
234
|
-
|
|
544
|
+
dropPoisonEvents(events) {
|
|
545
|
+
for (const event of events) {
|
|
546
|
+
this.dropPoisonEvent(event);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
loadAndMergeQueue() {
|
|
550
|
+
const remote = this.storage.get(QUEUE_KEY);
|
|
551
|
+
if (Array.isArray(remote)) {
|
|
552
|
+
this.queue = mergeQueues(this.queue, remote);
|
|
553
|
+
}
|
|
554
|
+
this.trimQueue();
|
|
555
|
+
}
|
|
556
|
+
syncQueue() {
|
|
557
|
+
const remote = this.storage.get(QUEUE_KEY);
|
|
558
|
+
const merged = mergeQueues(
|
|
559
|
+
Array.isArray(remote) ? remote : [],
|
|
560
|
+
this.queue
|
|
561
|
+
);
|
|
562
|
+
this.queue = this.trimQueue(merged);
|
|
563
|
+
this.storage.set(QUEUE_KEY, this.queue);
|
|
564
|
+
this.broadcastQueue();
|
|
565
|
+
}
|
|
566
|
+
trimQueue(queue = this.queue) {
|
|
567
|
+
if (queue.length <= this.maxQueue) return queue;
|
|
568
|
+
const dropped = queue.length - this.maxQueue;
|
|
569
|
+
this.options.onEventsDropped?.(dropped);
|
|
570
|
+
return queue.slice(-this.maxQueue);
|
|
571
|
+
}
|
|
572
|
+
broadcastQueue() {
|
|
573
|
+
try {
|
|
574
|
+
this.channel?.postMessage({
|
|
575
|
+
type: "queue-sync",
|
|
576
|
+
events: this.queue
|
|
577
|
+
});
|
|
578
|
+
} catch {
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
setupQueueSync() {
|
|
582
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
583
|
+
this.channel = new BroadcastChannel(CHANNEL_NAME);
|
|
584
|
+
this.channel.onmessage = (e) => {
|
|
585
|
+
if (e.data?.type !== "queue-sync" || !Array.isArray(e.data.events)) return;
|
|
586
|
+
this.queue = mergeQueues(this.queue, e.data.events);
|
|
587
|
+
this.queue = this.trimQueue();
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
if (typeof window !== "undefined") {
|
|
591
|
+
this.onStorageQueue = (e) => {
|
|
592
|
+
if (e.key !== QUEUE_KEY) return;
|
|
593
|
+
this.loadAndMergeQueue();
|
|
594
|
+
};
|
|
595
|
+
window.addEventListener("storage", this.onStorageQueue);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
235
598
|
startTimer() {
|
|
236
599
|
this.timer = setInterval(() => {
|
|
237
600
|
void this.flush();
|
|
@@ -243,37 +606,29 @@ var Eventra = class {
|
|
|
243
606
|
void this.flush();
|
|
244
607
|
}
|
|
245
608
|
};
|
|
609
|
+
const pageHide = () => {
|
|
610
|
+
void this.flush();
|
|
611
|
+
};
|
|
246
612
|
window.addEventListener("visibilitychange", handler);
|
|
613
|
+
window.addEventListener("pagehide", pageHide);
|
|
247
614
|
this.exitHandlers.push(() => {
|
|
248
615
|
window.removeEventListener("visibilitychange", handler);
|
|
616
|
+
window.removeEventListener("pagehide", pageHide);
|
|
249
617
|
});
|
|
250
618
|
}
|
|
251
619
|
setupProcessExit() {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
} catch {
|
|
257
|
-
}
|
|
620
|
+
const proc = globalThis.process;
|
|
621
|
+
if (!proc?.on) return;
|
|
622
|
+
const onSignal = () => {
|
|
623
|
+
void this.shutdown();
|
|
258
624
|
};
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
};
|
|
262
|
-
const sigint = async () => {
|
|
263
|
-
await flushSafe();
|
|
264
|
-
g.exit();
|
|
265
|
-
};
|
|
266
|
-
g?.on?.("beforeExit", beforeExit);
|
|
267
|
-
g?.on?.("SIGINT", sigint);
|
|
625
|
+
proc.once("SIGINT", onSignal);
|
|
626
|
+
proc.once("SIGTERM", onSignal);
|
|
268
627
|
this.exitHandlers.push(() => {
|
|
269
|
-
|
|
270
|
-
|
|
628
|
+
proc.off?.("SIGINT", onSignal);
|
|
629
|
+
proc.off?.("SIGTERM", onSignal);
|
|
271
630
|
});
|
|
272
631
|
}
|
|
273
|
-
persist() {
|
|
274
|
-
if (this.runtime !== "browser") return;
|
|
275
|
-
this.storage.set("__eventra_q__", this.queue);
|
|
276
|
-
}
|
|
277
632
|
};
|
|
278
633
|
// Annotate the CommonJS export names for ESM import in node:
|
|
279
634
|
0 && (module.exports = {
|