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