moveo-one-segment-destination-web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/index.cjs +591 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +136 -0
- package/dist/index.d.ts +136 -0
- package/dist/index.js +588 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var DEFAULTS = {
|
|
5
|
+
endpoint: "https://api.moveo.one/api/analytic/external/segment-destination",
|
|
6
|
+
debug: false,
|
|
7
|
+
gzip: true,
|
|
8
|
+
batchSize: 20,
|
|
9
|
+
flushIntervalMs: 3e4,
|
|
10
|
+
maxQueueBytes: 5e6,
|
|
11
|
+
maxQueueAgeMs: 7 * 24 * 60 * 60 * 1e3,
|
|
12
|
+
maxRetries: 10
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/store.ts
|
|
16
|
+
var encoder = new TextEncoder();
|
|
17
|
+
function byteLength(s) {
|
|
18
|
+
return encoder.encode(s).length;
|
|
19
|
+
}
|
|
20
|
+
var LocalStorageEventStore = class {
|
|
21
|
+
constructor(apiKey) {
|
|
22
|
+
this.current = [];
|
|
23
|
+
this.currentByteCount = 0;
|
|
24
|
+
this.prefix = `moveo-one-segment/${hash(apiKey)}`;
|
|
25
|
+
this.seqKey = `${this.prefix}:seq`;
|
|
26
|
+
this.currentKey = `${this.prefix}:current`;
|
|
27
|
+
this.batchPrefix = `${this.prefix}:b:`;
|
|
28
|
+
const raw = localStorage.getItem(this.currentKey);
|
|
29
|
+
if (raw) {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
this.current = Array.isArray(parsed) ? parsed : [];
|
|
33
|
+
} catch {
|
|
34
|
+
this.current = [];
|
|
35
|
+
}
|
|
36
|
+
this.currentByteCount = this.current.reduce((n, e) => n + byteLength(e), 0);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
append(eventJson) {
|
|
40
|
+
this.current.push(eventJson);
|
|
41
|
+
this.currentByteCount += byteLength(eventJson);
|
|
42
|
+
localStorage.setItem(this.currentKey, JSON.stringify(this.current));
|
|
43
|
+
}
|
|
44
|
+
currentCount() {
|
|
45
|
+
return this.current.length;
|
|
46
|
+
}
|
|
47
|
+
currentBytes() {
|
|
48
|
+
return this.currentByteCount;
|
|
49
|
+
}
|
|
50
|
+
sealCurrent() {
|
|
51
|
+
if (this.current.length === 0) return;
|
|
52
|
+
const seq = this.nextSeq();
|
|
53
|
+
const key = `${this.batchPrefix}${pad(seq)}`;
|
|
54
|
+
const batch = {
|
|
55
|
+
seq,
|
|
56
|
+
attempts: 0,
|
|
57
|
+
createdAtMs: Date.now(),
|
|
58
|
+
events: this.current
|
|
59
|
+
};
|
|
60
|
+
localStorage.setItem(key, JSON.stringify(batch));
|
|
61
|
+
localStorage.removeItem(this.currentKey);
|
|
62
|
+
this.current = [];
|
|
63
|
+
this.currentByteCount = 0;
|
|
64
|
+
}
|
|
65
|
+
batches() {
|
|
66
|
+
return this.sealedKeys().map((key) => this.readBatch(key)).filter((b) => b !== null).sort((a, b) => a.seq - b.seq);
|
|
67
|
+
}
|
|
68
|
+
delete(batch) {
|
|
69
|
+
localStorage.removeItem(batch.id);
|
|
70
|
+
}
|
|
71
|
+
bumpAttempt(batch) {
|
|
72
|
+
const stored = this.readStored(batch.id);
|
|
73
|
+
if (!stored) return;
|
|
74
|
+
stored.attempts += 1;
|
|
75
|
+
localStorage.setItem(batch.id, JSON.stringify(stored));
|
|
76
|
+
}
|
|
77
|
+
enforceLimits(maxBytes, maxAgeMs, debug) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
for (const key of this.sealedKeys()) {
|
|
80
|
+
const stored = this.readStored(key);
|
|
81
|
+
if (stored && now - stored.createdAtMs > maxAgeMs) {
|
|
82
|
+
if (debug) console.warn(`[MoveoOne] Evicting aged batch ${key}`);
|
|
83
|
+
localStorage.removeItem(key);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const remaining = this.sealedKeys().map((key) => ({ key, stored: this.readStored(key) })).filter((x) => x.stored !== null).sort((a, b) => a.stored.seq - b.stored.seq);
|
|
87
|
+
let total = remaining.reduce(
|
|
88
|
+
(n, x) => n + x.stored.events.reduce((m, e) => m + byteLength(e), 0),
|
|
89
|
+
0
|
|
90
|
+
);
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (total > maxBytes && i < remaining.length) {
|
|
93
|
+
const x = remaining[i];
|
|
94
|
+
total -= x.stored.events.reduce((m, e) => m + byteLength(e), 0);
|
|
95
|
+
if (debug) console.warn(`[MoveoOne] Evicting over-budget batch ${x.key}`);
|
|
96
|
+
localStorage.removeItem(x.key);
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
nextSeq() {
|
|
101
|
+
const seq = Number(localStorage.getItem(this.seqKey) ?? "0") || 0;
|
|
102
|
+
localStorage.setItem(this.seqKey, String(seq + 1));
|
|
103
|
+
return seq;
|
|
104
|
+
}
|
|
105
|
+
sealedKeys() {
|
|
106
|
+
const keys = [];
|
|
107
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
108
|
+
const key = localStorage.key(i);
|
|
109
|
+
if (key && key.startsWith(this.batchPrefix)) keys.push(key);
|
|
110
|
+
}
|
|
111
|
+
return keys;
|
|
112
|
+
}
|
|
113
|
+
readStored(key) {
|
|
114
|
+
const raw = localStorage.getItem(key);
|
|
115
|
+
if (!raw) return null;
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
if (!Array.isArray(parsed.events)) return null;
|
|
119
|
+
return parsed;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
readBatch(key) {
|
|
125
|
+
const stored = this.readStored(key);
|
|
126
|
+
if (!stored) return null;
|
|
127
|
+
return {
|
|
128
|
+
id: key,
|
|
129
|
+
seq: stored.seq,
|
|
130
|
+
attempts: stored.attempts,
|
|
131
|
+
createdAtMs: stored.createdAtMs,
|
|
132
|
+
events: stored.events.filter((e) => e.trim().length > 0)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var InMemoryEventStore = class {
|
|
137
|
+
constructor() {
|
|
138
|
+
this.current = [];
|
|
139
|
+
this.sealed = [];
|
|
140
|
+
this.seq = 0;
|
|
141
|
+
this.currentByteCount = 0;
|
|
142
|
+
}
|
|
143
|
+
append(eventJson) {
|
|
144
|
+
this.current.push(eventJson);
|
|
145
|
+
this.currentByteCount += byteLength(eventJson);
|
|
146
|
+
}
|
|
147
|
+
currentCount() {
|
|
148
|
+
return this.current.length;
|
|
149
|
+
}
|
|
150
|
+
currentBytes() {
|
|
151
|
+
return this.currentByteCount;
|
|
152
|
+
}
|
|
153
|
+
sealCurrent() {
|
|
154
|
+
if (this.current.length === 0) return;
|
|
155
|
+
const s = this.seq++;
|
|
156
|
+
this.sealed.push({
|
|
157
|
+
id: `mem-${s}`,
|
|
158
|
+
seq: s,
|
|
159
|
+
attempts: 0,
|
|
160
|
+
createdAtMs: Date.now(),
|
|
161
|
+
events: [...this.current]
|
|
162
|
+
});
|
|
163
|
+
this.current = [];
|
|
164
|
+
this.currentByteCount = 0;
|
|
165
|
+
}
|
|
166
|
+
batches() {
|
|
167
|
+
return [...this.sealed].sort((a, b) => a.seq - b.seq);
|
|
168
|
+
}
|
|
169
|
+
delete(batch) {
|
|
170
|
+
this.sealed = this.sealed.filter((b) => b.id !== batch.id);
|
|
171
|
+
}
|
|
172
|
+
bumpAttempt(batch) {
|
|
173
|
+
const idx = this.sealed.findIndex((b) => b.id === batch.id);
|
|
174
|
+
if (idx >= 0) this.sealed[idx].attempts += 1;
|
|
175
|
+
}
|
|
176
|
+
enforceLimits(maxBytes, maxAgeMs, _debug) {
|
|
177
|
+
const now = Date.now();
|
|
178
|
+
this.sealed = this.sealed.filter((b) => now - b.createdAtMs <= maxAgeMs);
|
|
179
|
+
let total = this.sealed.reduce(
|
|
180
|
+
(n, b) => n + b.events.reduce((m, e) => m + byteLength(e), 0),
|
|
181
|
+
0
|
|
182
|
+
);
|
|
183
|
+
while (total > maxBytes && this.sealed.length > 0) {
|
|
184
|
+
const removed = this.sealed.shift();
|
|
185
|
+
total -= removed.events.reduce((m, e) => m + byteLength(e), 0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
function buildStore(apiKey, debug) {
|
|
190
|
+
try {
|
|
191
|
+
if (typeof localStorage === "undefined") throw new Error("no localStorage");
|
|
192
|
+
const probe = "moveo-one-segment:probe";
|
|
193
|
+
localStorage.setItem(probe, "1");
|
|
194
|
+
localStorage.removeItem(probe);
|
|
195
|
+
return new LocalStorageEventStore(apiKey);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (debug)
|
|
198
|
+
console.warn(
|
|
199
|
+
"[MoveoOne] localStorage unavailable \u2014 using in-memory store (events are NOT durable)",
|
|
200
|
+
e
|
|
201
|
+
);
|
|
202
|
+
return new InMemoryEventStore();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function pad(seq) {
|
|
206
|
+
return String(seq).padStart(12, "0");
|
|
207
|
+
}
|
|
208
|
+
function hash(s) {
|
|
209
|
+
let h = 0;
|
|
210
|
+
for (let i = 0; i < s.length; i++) {
|
|
211
|
+
h = Math.imul(31, h) + s.charCodeAt(i) | 0;
|
|
212
|
+
}
|
|
213
|
+
return (h >>> 0).toString(16);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/MoveoOneDestination.ts
|
|
217
|
+
var encoder2 = new TextEncoder();
|
|
218
|
+
var MAX_EVENT_BYTES = 32e3;
|
|
219
|
+
var MAX_REQUEST_BYTES = 475e3;
|
|
220
|
+
var BACKOFF_BASE_SEC = 1;
|
|
221
|
+
var BACKOFF_MAX_SEC = 300;
|
|
222
|
+
var BACKOFF_JITTER = 0.2;
|
|
223
|
+
var MoveoOneDestination = class {
|
|
224
|
+
constructor(options) {
|
|
225
|
+
this.name = "Moveo One";
|
|
226
|
+
this.type = "enrichment";
|
|
227
|
+
this.version = "1.0.0";
|
|
228
|
+
this.loaded = false;
|
|
229
|
+
// Retry/backoff state.
|
|
230
|
+
this.consecutiveFailures = 0;
|
|
231
|
+
this.nextAllowedSendAtMs = 0;
|
|
232
|
+
// Guard so overlapping flushes never run concurrently (the browser is
|
|
233
|
+
// single-threaded, but flush I/O is async), serializing all store + network
|
|
234
|
+
// access.
|
|
235
|
+
this.flushing = false;
|
|
236
|
+
this.flushTimer = null;
|
|
237
|
+
if (!options || !options.apiKey) {
|
|
238
|
+
throw new Error("[MoveoOne] `apiKey` is required");
|
|
239
|
+
}
|
|
240
|
+
this.options = {
|
|
241
|
+
apiKey: options.apiKey,
|
|
242
|
+
endpoint: options.endpoint ?? DEFAULTS.endpoint,
|
|
243
|
+
debug: options.debug ?? DEFAULTS.debug,
|
|
244
|
+
gzip: options.gzip ?? DEFAULTS.gzip,
|
|
245
|
+
batchSize: options.batchSize ?? DEFAULTS.batchSize,
|
|
246
|
+
flushIntervalMs: options.flushIntervalMs ?? DEFAULTS.flushIntervalMs,
|
|
247
|
+
maxQueueBytes: options.maxQueueBytes ?? DEFAULTS.maxQueueBytes,
|
|
248
|
+
maxQueueAgeMs: options.maxQueueAgeMs ?? DEFAULTS.maxQueueAgeMs,
|
|
249
|
+
maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
|
|
250
|
+
filter: options.filter
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
isLoaded() {
|
|
254
|
+
return this.loaded;
|
|
255
|
+
}
|
|
256
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
257
|
+
// Lifecycle — runs when the plugin is registered with analytics-next.
|
|
258
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
259
|
+
async load(_ctx, _instance) {
|
|
260
|
+
this.store = buildStore(this.options.apiKey, this.options.debug);
|
|
261
|
+
this.registerLifecycle();
|
|
262
|
+
void this.flush();
|
|
263
|
+
this.flushTimer = setInterval(() => void this.flush(), this.options.flushIntervalMs);
|
|
264
|
+
this.loaded = true;
|
|
265
|
+
}
|
|
266
|
+
async unload() {
|
|
267
|
+
if (this.flushTimer !== null) {
|
|
268
|
+
clearInterval(this.flushTimer);
|
|
269
|
+
this.flushTimer = null;
|
|
270
|
+
}
|
|
271
|
+
if (typeof document !== "undefined" && this.onVisibilityChange) {
|
|
272
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
273
|
+
}
|
|
274
|
+
if (typeof window !== "undefined" && this.onPageHide) {
|
|
275
|
+
window.removeEventListener("pagehide", this.onPageHide);
|
|
276
|
+
}
|
|
277
|
+
await this.flush();
|
|
278
|
+
this.loaded = false;
|
|
279
|
+
}
|
|
280
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
281
|
+
// Segment plugin overrides — intercept every event and forward a copy. The
|
|
282
|
+
// context is returned unchanged so the rest of the Segment pipeline is intact.
|
|
283
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
284
|
+
track(ctx) {
|
|
285
|
+
this.ship(ctx.event);
|
|
286
|
+
return ctx;
|
|
287
|
+
}
|
|
288
|
+
page(ctx) {
|
|
289
|
+
this.ship(ctx.event);
|
|
290
|
+
return ctx;
|
|
291
|
+
}
|
|
292
|
+
screen(ctx) {
|
|
293
|
+
this.ship(ctx.event);
|
|
294
|
+
return ctx;
|
|
295
|
+
}
|
|
296
|
+
identify(ctx) {
|
|
297
|
+
this.ship(ctx.event);
|
|
298
|
+
return ctx;
|
|
299
|
+
}
|
|
300
|
+
group(ctx) {
|
|
301
|
+
this.ship(ctx.event);
|
|
302
|
+
return ctx;
|
|
303
|
+
}
|
|
304
|
+
alias(ctx) {
|
|
305
|
+
this.ship(ctx.event);
|
|
306
|
+
return ctx;
|
|
307
|
+
}
|
|
308
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
309
|
+
// Public API
|
|
310
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
311
|
+
/**
|
|
312
|
+
* Immediately flushes the pending batch and any queued events.
|
|
313
|
+
* Called automatically on the interval timer and when the page is hidden.
|
|
314
|
+
*/
|
|
315
|
+
async flush() {
|
|
316
|
+
await this.doFlush();
|
|
317
|
+
}
|
|
318
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
319
|
+
// Private helpers
|
|
320
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
321
|
+
ship(event) {
|
|
322
|
+
if (!this.passesFilter(event)) return;
|
|
323
|
+
const body = JSON.stringify(this.buildPayload(event));
|
|
324
|
+
if (byteLength2(body) > MAX_EVENT_BYTES) {
|
|
325
|
+
if (this.options.debug)
|
|
326
|
+
console.warn(
|
|
327
|
+
`[MoveoOne] Event ${event.messageId} is too large (${body.length} chars) \u2014 skipped`
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
this.store.append(body);
|
|
333
|
+
if (this.store.currentCount() >= this.options.batchSize || this.store.currentBytes() >= MAX_REQUEST_BYTES) {
|
|
334
|
+
void this.flush();
|
|
335
|
+
}
|
|
336
|
+
} catch (e) {
|
|
337
|
+
if (this.options.debug) console.warn("[MoveoOne] Failed to enqueue event", e);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Seals the pending events into a batch and uploads everything that is due.
|
|
342
|
+
* Guarded so only one flush runs at a time.
|
|
343
|
+
*/
|
|
344
|
+
async doFlush() {
|
|
345
|
+
if (this.flushing) return;
|
|
346
|
+
this.flushing = true;
|
|
347
|
+
try {
|
|
348
|
+
try {
|
|
349
|
+
this.store.sealCurrent();
|
|
350
|
+
} catch (e) {
|
|
351
|
+
if (this.options.debug) console.warn("[MoveoOne] Seal failed", e);
|
|
352
|
+
}
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
if (now < this.nextAllowedSendAtMs) {
|
|
355
|
+
if (this.options.debug)
|
|
356
|
+
console.debug(
|
|
357
|
+
`[MoveoOne] Backing off \u2014 skipping uploads for ${this.nextAllowedSendAtMs - now}ms`
|
|
358
|
+
);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.store.enforceLimits(
|
|
362
|
+
this.options.maxQueueBytes,
|
|
363
|
+
this.options.maxQueueAgeMs,
|
|
364
|
+
this.options.debug
|
|
365
|
+
);
|
|
366
|
+
for (const batch of this.store.batches()) {
|
|
367
|
+
if (batch.events.length === 0) {
|
|
368
|
+
this.store.delete(batch);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const result = await this.postBatch(batch.events);
|
|
372
|
+
if (result.kind === "success") {
|
|
373
|
+
this.store.delete(batch);
|
|
374
|
+
this.onSendSuccess();
|
|
375
|
+
if (this.options.debug)
|
|
376
|
+
console.debug(`[MoveoOne] Sent batch ${batch.id} (${batch.events.length} events)`);
|
|
377
|
+
} else if (result.kind === "permanent") {
|
|
378
|
+
this.store.delete(batch);
|
|
379
|
+
if (this.options.debug)
|
|
380
|
+
console.warn(
|
|
381
|
+
`[MoveoOne] Dropped batch ${batch.id} \u2014 HTTP ${result.code} (non-retryable)`
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
const nextAttempt = batch.attempts + 1;
|
|
385
|
+
if (nextAttempt > this.options.maxRetries) {
|
|
386
|
+
this.store.delete(batch);
|
|
387
|
+
if (this.options.debug)
|
|
388
|
+
console.warn(
|
|
389
|
+
`[MoveoOne] Dropped batch ${batch.id} after ${this.options.maxRetries} failed attempts`
|
|
390
|
+
);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
this.store.bumpAttempt(batch);
|
|
394
|
+
this.onSendFailure(result.retryAfterSec);
|
|
395
|
+
if (this.options.debug)
|
|
396
|
+
console.warn(
|
|
397
|
+
`[MoveoOne] Batch ${batch.id} failed (attempt ${nextAttempt}) \u2014 backing off`
|
|
398
|
+
);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} finally {
|
|
403
|
+
this.flushing = false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
onSendSuccess() {
|
|
407
|
+
this.consecutiveFailures = 0;
|
|
408
|
+
this.nextAllowedSendAtMs = 0;
|
|
409
|
+
}
|
|
410
|
+
onSendFailure(retryAfterSec) {
|
|
411
|
+
this.consecutiveFailures++;
|
|
412
|
+
let delaySec;
|
|
413
|
+
if (retryAfterSec !== null && retryAfterSec > 0) {
|
|
414
|
+
delaySec = retryAfterSec;
|
|
415
|
+
} else {
|
|
416
|
+
const expo = BACKOFF_BASE_SEC * Math.pow(2, this.consecutiveFailures - 1);
|
|
417
|
+
const capped = Math.min(expo, BACKOFF_MAX_SEC);
|
|
418
|
+
delaySec = capped + capped * BACKOFF_JITTER * Math.random();
|
|
419
|
+
}
|
|
420
|
+
this.nextAllowedSendAtMs = Date.now() + Math.floor(delaySec * 1e3);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Returns true if the event should be forwarded to Moveo One.
|
|
424
|
+
* When no filter is set all events pass. When a filter is set, all specified
|
|
425
|
+
* key/value conditions must match against the event's properties or traits.
|
|
426
|
+
*/
|
|
427
|
+
passesFilter(event) {
|
|
428
|
+
const filter = this.options.filter;
|
|
429
|
+
if (!filter || Object.keys(filter).length === 0) return true;
|
|
430
|
+
let bag;
|
|
431
|
+
switch (event.type) {
|
|
432
|
+
case "track":
|
|
433
|
+
case "page":
|
|
434
|
+
case "screen":
|
|
435
|
+
bag = event.properties;
|
|
436
|
+
break;
|
|
437
|
+
case "identify":
|
|
438
|
+
case "group":
|
|
439
|
+
bag = event.traits;
|
|
440
|
+
break;
|
|
441
|
+
default:
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
const properties = bag ?? {};
|
|
445
|
+
return Object.entries(filter).every(([key, allowedValues]) => {
|
|
446
|
+
const value = properties[key];
|
|
447
|
+
if (value === null || value === void 0) return false;
|
|
448
|
+
if (typeof value === "object") return false;
|
|
449
|
+
return allowedValues.includes(String(value));
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Builds the JSON body sent to the Moveo One API.
|
|
454
|
+
* Follows the Segment event spec so the same fields are available on both sides.
|
|
455
|
+
*/
|
|
456
|
+
buildPayload(event) {
|
|
457
|
+
const out = {
|
|
458
|
+
type: String(event.type).toLowerCase(),
|
|
459
|
+
messageId: event.messageId,
|
|
460
|
+
anonymousId: event.anonymousId,
|
|
461
|
+
timestamp: normalizeTimestamp(event.timestamp),
|
|
462
|
+
originalTimestamp: normalizeTimestamp(event.timestamp)
|
|
463
|
+
};
|
|
464
|
+
if (typeof event.userId === "string" && event.userId.trim().length > 0) {
|
|
465
|
+
out["userId"] = event.userId;
|
|
466
|
+
}
|
|
467
|
+
out["context"] = event.context;
|
|
468
|
+
out["integrations"] = event.integrations;
|
|
469
|
+
switch (event.type) {
|
|
470
|
+
case "track":
|
|
471
|
+
out["event"] = event.event;
|
|
472
|
+
out["properties"] = event.properties;
|
|
473
|
+
break;
|
|
474
|
+
case "page":
|
|
475
|
+
case "screen":
|
|
476
|
+
out["name"] = event.name;
|
|
477
|
+
out["properties"] = event.properties;
|
|
478
|
+
break;
|
|
479
|
+
case "identify":
|
|
480
|
+
out["traits"] = event.traits;
|
|
481
|
+
break;
|
|
482
|
+
case "group":
|
|
483
|
+
out["groupId"] = event.groupId;
|
|
484
|
+
out["traits"] = event.traits;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
async postBatch(events) {
|
|
490
|
+
const sentAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
491
|
+
const wrapped = events.map((raw) => {
|
|
492
|
+
const obj = JSON.parse(raw);
|
|
493
|
+
if (Object.prototype.hasOwnProperty.call(obj, "sentAt")) return obj;
|
|
494
|
+
return { ...obj, sentAt };
|
|
495
|
+
});
|
|
496
|
+
const body = JSON.stringify({ events: wrapped });
|
|
497
|
+
return this.sendRequest(body);
|
|
498
|
+
}
|
|
499
|
+
async sendRequest(body) {
|
|
500
|
+
const headers = {
|
|
501
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
502
|
+
Authorization: this.options.apiKey
|
|
503
|
+
};
|
|
504
|
+
let payload = body;
|
|
505
|
+
if (this.options.gzip) {
|
|
506
|
+
const gzipped = await gzip(body);
|
|
507
|
+
if (gzipped) {
|
|
508
|
+
payload = gzipped;
|
|
509
|
+
headers["Content-Encoding"] = "gzip";
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (this.options.debug) {
|
|
513
|
+
console.debug("[MoveoOne] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
514
|
+
console.debug(
|
|
515
|
+
`[MoveoOne] \u2192 POST ${this.options.endpoint}${headers["Content-Encoding"] ? " (gzip)" : ""}`
|
|
516
|
+
);
|
|
517
|
+
console.debug(`[MoveoOne] \u2192 Authorization: ${this.options.apiKey.slice(0, 8)}\u2026`);
|
|
518
|
+
console.debug(`[MoveoOne] \u2192 Body: ${body}`);
|
|
519
|
+
}
|
|
520
|
+
let res;
|
|
521
|
+
try {
|
|
522
|
+
res = await fetch(this.options.endpoint, {
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers,
|
|
525
|
+
body: payload,
|
|
526
|
+
// keepalive lets in-flight requests complete during page unload.
|
|
527
|
+
keepalive: true
|
|
528
|
+
});
|
|
529
|
+
} catch (e) {
|
|
530
|
+
if (this.options.debug) console.warn("[MoveoOne] Network error while uploading", e);
|
|
531
|
+
return { kind: "retryable", retryAfterSec: null };
|
|
532
|
+
}
|
|
533
|
+
const code = res.status;
|
|
534
|
+
const retryAfter = parseRetryAfter(res.headers.get("Retry-After"));
|
|
535
|
+
if (this.options.debug) {
|
|
536
|
+
const responseBody = await res.text().catch(() => "(empty)");
|
|
537
|
+
console.debug(`[MoveoOne] \u2190 HTTP ${code}`);
|
|
538
|
+
console.debug(`[MoveoOne] \u2190 Response: ${responseBody || "(empty)"}`);
|
|
539
|
+
console.debug("[MoveoOne] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
540
|
+
}
|
|
541
|
+
if (code >= 200 && code <= 299) return { kind: "success" };
|
|
542
|
+
if (code === 429) return { kind: "retryable", retryAfterSec: retryAfter };
|
|
543
|
+
if (code >= 500 && code <= 599) return { kind: "retryable", retryAfterSec: retryAfter };
|
|
544
|
+
if (code >= 400 && code <= 499) return { kind: "permanent", code };
|
|
545
|
+
return { kind: "retryable", retryAfterSec: retryAfter };
|
|
546
|
+
}
|
|
547
|
+
registerLifecycle() {
|
|
548
|
+
if (typeof document !== "undefined") {
|
|
549
|
+
this.onVisibilityChange = () => {
|
|
550
|
+
if (document.visibilityState === "hidden") void this.flush();
|
|
551
|
+
};
|
|
552
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
553
|
+
}
|
|
554
|
+
if (typeof window !== "undefined") {
|
|
555
|
+
this.onPageHide = () => void this.flush();
|
|
556
|
+
window.addEventListener("pagehide", this.onPageHide);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
function byteLength2(s) {
|
|
561
|
+
return encoder2.encode(s).length;
|
|
562
|
+
}
|
|
563
|
+
function normalizeTimestamp(ts) {
|
|
564
|
+
if (ts === void 0 || ts === null) return void 0;
|
|
565
|
+
if (ts instanceof Date) return ts.toISOString();
|
|
566
|
+
return String(ts);
|
|
567
|
+
}
|
|
568
|
+
function parseRetryAfter(value) {
|
|
569
|
+
if (!value) return null;
|
|
570
|
+
const n = parseInt(value.trim(), 10);
|
|
571
|
+
return Number.isNaN(n) ? null : n;
|
|
572
|
+
}
|
|
573
|
+
async function gzip(s) {
|
|
574
|
+
if (typeof CompressionStream === "undefined") return null;
|
|
575
|
+
try {
|
|
576
|
+
const stream = new Blob([s]).stream().pipeThrough(new CompressionStream("gzip"));
|
|
577
|
+
return await new Response(stream).blob();
|
|
578
|
+
} catch {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/index.ts
|
|
584
|
+
function moveoOneDestination(options) {
|
|
585
|
+
return new MoveoOneDestination(options);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
exports.MoveoOneDestination = MoveoOneDestination;
|
|
589
|
+
exports.moveoOneDestination = moveoOneDestination;
|
|
590
|
+
//# sourceMappingURL=index.cjs.map
|
|
591
|
+
//# sourceMappingURL=index.cjs.map
|