@uniformdev/insights 20.31.1-alpha.184

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.
@@ -0,0 +1,634 @@
1
+ import {
2
+ BackendInsightsProxyHandler,
3
+ createBackendInsightsProxyHandler
4
+ } from "./chunk-FFLO523H.mjs";
5
+
6
+ // src/batching.ts
7
+ import pLimit from "p-limit";
8
+ var DEFAULT_BATCH_CONFIG = {
9
+ maxBatchSize: 50,
10
+ maxBatchDelayMs: 2e3,
11
+ maxPayloadSizeBytes: 1024 * 1024,
12
+ // 1MB
13
+ maxRequestsPerSecond: 1
14
+ };
15
+ var createEventHash = (action, pageViewId, payload) => {
16
+ const payloadStr = JSON.stringify(payload);
17
+ return `${action}:${pageViewId}:${payloadStr}`;
18
+ };
19
+ var isDuplicateEvent = (deduplicationMap, hash, deduplicationWindowMs = 5 * 60 * 1e3) => {
20
+ const now = Date.now();
21
+ const existing = deduplicationMap.get(hash);
22
+ if (existing && now - existing.timestamp < deduplicationWindowMs) {
23
+ return true;
24
+ }
25
+ for (const [key, entry] of deduplicationMap.entries()) {
26
+ if (now - entry.timestamp >= deduplicationWindowMs) {
27
+ deduplicationMap.delete(key);
28
+ }
29
+ }
30
+ deduplicationMap.set(hash, { timestamp: now, hash });
31
+ return false;
32
+ };
33
+ var getEventPriority = (action) => {
34
+ switch (action) {
35
+ case "session_start":
36
+ return 3;
37
+ // Highest priority, as session start has to be the first for correct attribution in Insights
38
+ case "page_hit":
39
+ return 2;
40
+ // Medium priority, has to come right after session start for correct attribution in Insights
41
+ default:
42
+ return 1;
43
+ }
44
+ };
45
+ var sortEventsByPriority = (events) => {
46
+ return events.sort((a, b) => {
47
+ if (b.priority !== a.priority) {
48
+ return b.priority - a.priority;
49
+ }
50
+ return a.timestamp - b.timestamp;
51
+ });
52
+ };
53
+ var adjustTimestampsForOrder = (events) => {
54
+ const sortedEvents = sortEventsByPriority(events);
55
+ const baseTime = Date.now();
56
+ const timeIncrement = 100;
57
+ return sortedEvents.map((event, index) => ({
58
+ ...event.message,
59
+ timestamp: new Date(baseTime + index * timeIncrement).toISOString()
60
+ }));
61
+ };
62
+ var buildEndpointUrl = (endpoint) => {
63
+ if (endpoint.type === "api") {
64
+ const url = new URL(endpoint.host);
65
+ url.pathname = `/v0/events`;
66
+ if (endpoint.host.includes("tinybird.co")) {
67
+ url.pathname = "/v0/events";
68
+ }
69
+ url.searchParams.set("name", "events");
70
+ return url.toString();
71
+ } else {
72
+ return endpoint.path;
73
+ }
74
+ };
75
+ var sendBatchToEndpoint = async (messages, endpoint) => {
76
+ const endpointUrl = buildEndpointUrl(endpoint);
77
+ const ndjson = messages.map((msg) => JSON.stringify(msg)).join("\n");
78
+ const headers = {
79
+ "Content-Type": "application/x-ndjson"
80
+ };
81
+ if (endpoint.type === "api" && endpoint.apiKey) {
82
+ headers.Authorization = `Bearer ${endpoint.apiKey}`;
83
+ }
84
+ const response = await fetch(endpointUrl, {
85
+ method: "POST",
86
+ headers,
87
+ body: ndjson
88
+ });
89
+ if (!response.ok) {
90
+ throw new Error(`Failed to send batch: ${response.statusText}`);
91
+ }
92
+ const json = await response.json();
93
+ return json;
94
+ };
95
+ var createBatchProcessor = (config, endpoint) => {
96
+ let eventQueue = [];
97
+ let batchTimeout = null;
98
+ const limit = pLimit(config.maxRequestsPerSecond);
99
+ const processBatch = async () => {
100
+ if (eventQueue.length === 0) return;
101
+ const messages = adjustTimestampsForOrder(eventQueue);
102
+ const batches = [];
103
+ let currentBatch = [];
104
+ let currentBatchSize = 0;
105
+ for (const message of messages) {
106
+ const messageSize = JSON.stringify(message).length;
107
+ if (currentBatch.length >= config.maxBatchSize || currentBatchSize + messageSize > config.maxPayloadSizeBytes) {
108
+ if (currentBatch.length > 0) {
109
+ batches.push(currentBatch);
110
+ }
111
+ currentBatch = [message];
112
+ currentBatchSize = messageSize;
113
+ } else {
114
+ currentBatch.push(message);
115
+ currentBatchSize += messageSize;
116
+ }
117
+ }
118
+ if (currentBatch.length > 0) {
119
+ batches.push(currentBatch);
120
+ }
121
+ const sendPromises = batches.map(
122
+ (batch) => limit(async () => {
123
+ try {
124
+ await sendBatchToEndpoint(batch, endpoint);
125
+ } catch (error) {
126
+ console.warn("Failed to send batch:", error);
127
+ }
128
+ })
129
+ );
130
+ await Promise.all(sendPromises);
131
+ eventQueue = [];
132
+ batchTimeout = null;
133
+ };
134
+ const scheduleBatch = () => {
135
+ if (batchTimeout) return;
136
+ batchTimeout = setTimeout(() => {
137
+ processBatch();
138
+ }, config.maxBatchDelayMs);
139
+ };
140
+ const addEvent = (message) => {
141
+ const priority = getEventPriority(message.action);
142
+ const timestamp = new Date(message.timestamp).getTime();
143
+ eventQueue.push({ message, priority, timestamp });
144
+ if (eventQueue.length >= config.maxBatchSize) {
145
+ if (batchTimeout) {
146
+ clearTimeout(batchTimeout);
147
+ batchTimeout = null;
148
+ }
149
+ processBatch();
150
+ } else {
151
+ scheduleBatch();
152
+ }
153
+ };
154
+ const flush = () => {
155
+ if (batchTimeout) {
156
+ clearTimeout(batchTimeout);
157
+ batchTimeout = null;
158
+ }
159
+ processBatch();
160
+ };
161
+ const clear = () => {
162
+ if (batchTimeout) {
163
+ clearTimeout(batchTimeout);
164
+ batchTimeout = null;
165
+ }
166
+ eventQueue = [];
167
+ };
168
+ return { addEvent, flush, clear };
169
+ };
170
+ var createDeduplicationManager = () => {
171
+ const deduplicationMap = /* @__PURE__ */ new Map();
172
+ return {
173
+ isDuplicate: (message) => {
174
+ const hash = createEventHash(message.action, message.page_view_id, message.payload);
175
+ return isDuplicateEvent(deduplicationMap, hash);
176
+ },
177
+ clear: () => {
178
+ deduplicationMap.clear();
179
+ }
180
+ };
181
+ };
182
+
183
+ // src/utils.ts
184
+ var getWebMetadata = () => {
185
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
186
+ const locale = navigator.languages && navigator.languages.length ? navigator.languages[0] : navigator.userLanguage || navigator.language || navigator.browserLanguage || "en";
187
+ return {
188
+ "user-agent": window.navigator.userAgent,
189
+ locale,
190
+ location: timeZone,
191
+ referrer: document.referrer,
192
+ pathname: window.location.pathname,
193
+ href: window.location.href
194
+ };
195
+ };
196
+ var generateVisitorId = async () => {
197
+ return `visitor_${generalRandomId()}`;
198
+ };
199
+ var generateSessionId = async () => {
200
+ return `session_${generalRandomId()}`;
201
+ };
202
+ var generatePageId = () => {
203
+ return `page_${generalRandomId()}`;
204
+ };
205
+ var generalRandomId = () => {
206
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
207
+ const id = crypto.randomUUID();
208
+ return id.replaceAll("-", "").toLowerCase();
209
+ }
210
+ return Math.random().toString(32).substring(2);
211
+ };
212
+
213
+ // src/events/goal.ts
214
+ var buildGoalConvertMessage = (sessionId, visitorId, pageId, projectId, goalId, compositionData) => ({
215
+ action: "goal_convert",
216
+ version: "2",
217
+ session_id: sessionId,
218
+ visitor_id: visitorId,
219
+ page_view_id: pageId,
220
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
221
+ project_id: projectId,
222
+ payload: {
223
+ goal_id: goalId
224
+ },
225
+ web_metadata: getWebMetadata(),
226
+ uniform: compositionData || {}
227
+ });
228
+
229
+ // src/events/page.ts
230
+ var buildPageHitMessage = (sessionId, visitorId, pageId, projectId, compositionData) => ({
231
+ action: "page_hit",
232
+ version: "2",
233
+ session_id: sessionId,
234
+ visitor_id: visitorId,
235
+ page_view_id: pageId,
236
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
237
+ project_id: projectId,
238
+ payload: {},
239
+ web_metadata: getWebMetadata(),
240
+ uniform: compositionData || {}
241
+ });
242
+
243
+ // src/events/personalization.ts
244
+ var buildPersonalizationResultMessage = (sessionId, visitorId, pageId, projectId, result, variant, compositionData) => ({
245
+ action: "personalization_result",
246
+ version: "2",
247
+ session_id: sessionId,
248
+ visitor_id: visitorId,
249
+ page_view_id: pageId,
250
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
251
+ project_id: projectId,
252
+ payload: {
253
+ name: result.name,
254
+ variantId: variant.id,
255
+ control: variant.control || result.control,
256
+ changed: result.changed
257
+ },
258
+ web_metadata: getWebMetadata(),
259
+ uniform: compositionData || {}
260
+ });
261
+
262
+ // src/events/session.ts
263
+ var buildSessionStartMessage = (sessionId, visitorId, pageId, projectId, previousSessionId, compositionData) => ({
264
+ action: "session_start",
265
+ version: "2",
266
+ session_id: sessionId,
267
+ visitor_id: visitorId,
268
+ page_view_id: pageId,
269
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
270
+ project_id: projectId,
271
+ payload: {
272
+ previous_session_id: previousSessionId
273
+ },
274
+ web_metadata: getWebMetadata(),
275
+ uniform: compositionData || {}
276
+ });
277
+
278
+ // src/events/test.ts
279
+ var buildTestResultMessage = (sessionId, visitorId, pageId, projectId, result, compositionData) => ({
280
+ action: "test_result",
281
+ version: "2",
282
+ session_id: sessionId,
283
+ visitor_id: visitorId,
284
+ page_view_id: pageId,
285
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
286
+ project_id: projectId,
287
+ payload: {
288
+ name: result.name,
289
+ variantId: result.variantId,
290
+ control: result.control,
291
+ variantAssigned: result.variantAssigned
292
+ },
293
+ web_metadata: getWebMetadata(),
294
+ uniform: compositionData || {}
295
+ });
296
+
297
+ // src/storage.ts
298
+ var createInsightsStorage = (customStorage) => {
299
+ if (customStorage) {
300
+ return customStorage;
301
+ }
302
+ const STORAGE_KEY = "ufin";
303
+ return {
304
+ get: () => {
305
+ if (typeof localStorage === "undefined") {
306
+ return void 0;
307
+ }
308
+ const data = localStorage.getItem(STORAGE_KEY);
309
+ if (!data) {
310
+ return void 0;
311
+ }
312
+ try {
313
+ return JSON.parse(data);
314
+ } catch (e) {
315
+ return void 0;
316
+ }
317
+ },
318
+ set: (data) => {
319
+ if (typeof localStorage === "undefined") {
320
+ return;
321
+ }
322
+ const toSet = {
323
+ ...data,
324
+ updated: Date.now()
325
+ };
326
+ try {
327
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(toSet));
328
+ } catch (e) {
329
+ }
330
+ },
331
+ clear: () => {
332
+ if (typeof localStorage === "undefined") {
333
+ return;
334
+ }
335
+ try {
336
+ localStorage.removeItem(STORAGE_KEY);
337
+ } catch (e) {
338
+ }
339
+ }
340
+ };
341
+ };
342
+ var createMemoryStorage = () => {
343
+ let data;
344
+ return {
345
+ get: () => data,
346
+ set: (newData) => {
347
+ data = {
348
+ ...newData,
349
+ updated: Date.now()
350
+ };
351
+ },
352
+ clear: () => {
353
+ data = void 0;
354
+ }
355
+ };
356
+ };
357
+
358
+ // src/plugin.ts
359
+ var createInsightsCore = (options) => {
360
+ const {
361
+ endpoint,
362
+ storage: customStorage,
363
+ batchConfig = DEFAULT_BATCH_CONFIG,
364
+ sessionDurationSeconds = 30 * 60,
365
+ getVisitorId = generateVisitorId,
366
+ getSessionId = generateSessionId
367
+ } = options;
368
+ const storage = createInsightsStorage(customStorage);
369
+ let storageData;
370
+ let pageId = generatePageId();
371
+ let previousUrl = void 0;
372
+ const batchProcessor = batchConfig ? createBatchProcessor({ ...DEFAULT_BATCH_CONFIG, ...batchConfig }, endpoint) : null;
373
+ const deduplicationManager = batchConfig ? createDeduplicationManager() : null;
374
+ const sendEventDirectly = async (message) => {
375
+ if (typeof window !== "undefined" && window.__UNIFORM_CONTEXTUAL_EDITING__) {
376
+ return;
377
+ }
378
+ try {
379
+ const endpointUrl = endpoint.type === "api" ? `${endpoint.host}/v0/events?name=events` : endpoint.path;
380
+ const headers = {
381
+ "Content-Type": "application/x-ndjson"
382
+ };
383
+ if (endpoint.type === "api" && endpoint.apiKey) {
384
+ headers.Authorization = `Bearer ${endpoint.apiKey}`;
385
+ }
386
+ await fetch(endpointUrl, {
387
+ method: "POST",
388
+ headers,
389
+ body: JSON.stringify(message)
390
+ });
391
+ } catch (error) {
392
+ console.warn("Failed to send event:", error);
393
+ }
394
+ };
395
+ const addEvent = (message) => {
396
+ if (typeof window !== "undefined" && window.__UNIFORM_CONTEXTUAL_EDITING__) {
397
+ return;
398
+ }
399
+ if (batchProcessor && deduplicationManager) {
400
+ if (!deduplicationManager.isDuplicate(message)) {
401
+ batchProcessor.addEvent(message);
402
+ }
403
+ } else {
404
+ sendEventDirectly(message);
405
+ }
406
+ };
407
+ return {
408
+ init: async (context) => {
409
+ storageData = storage.get();
410
+ if (!storageData || Date.now() - storageData.updated > sessionDurationSeconds * 1e3) {
411
+ const previousSessionId = storageData == null ? void 0 : storageData.sessionId;
412
+ let visitorId;
413
+ if (storageData == null ? void 0 : storageData.visitorId) {
414
+ visitorId = storageData.visitorId;
415
+ } else {
416
+ visitorId = await getVisitorId({
417
+ context,
418
+ previousVisitorId: storageData == null ? void 0 : storageData.visitorId,
419
+ previousSessionId
420
+ });
421
+ }
422
+ const sessionId = await getSessionId({ context, visitorId, previousSessionId });
423
+ const newStorageData = {
424
+ visitorId,
425
+ sessionId,
426
+ updated: Date.now()
427
+ };
428
+ storage.set(newStorageData);
429
+ storageData = newStorageData;
430
+ const message = buildSessionStartMessage(
431
+ storageData.sessionId,
432
+ storageData.visitorId,
433
+ pageId,
434
+ endpoint.projectId,
435
+ previousSessionId
436
+ );
437
+ addEvent(message);
438
+ } else if (storageData) {
439
+ storage.set(storageData);
440
+ }
441
+ },
442
+ pageHit: (compositionData) => {
443
+ if (!storageData) {
444
+ return;
445
+ }
446
+ if (typeof window !== "undefined" && previousUrl === window.location.href) {
447
+ return;
448
+ }
449
+ if (typeof window !== "undefined") {
450
+ previousUrl = window.location.href;
451
+ }
452
+ pageId = generatePageId();
453
+ const message = buildPageHitMessage(
454
+ storageData.sessionId,
455
+ storageData.visitorId,
456
+ pageId,
457
+ endpoint.projectId,
458
+ compositionData
459
+ );
460
+ addEvent(message);
461
+ },
462
+ testResult: (result, compositionData) => {
463
+ if (!storageData) {
464
+ return;
465
+ }
466
+ const message = buildTestResultMessage(
467
+ storageData.sessionId,
468
+ storageData.visitorId,
469
+ pageId,
470
+ endpoint.projectId,
471
+ result,
472
+ compositionData
473
+ );
474
+ addEvent(message);
475
+ },
476
+ personalizationResult: (result, compositionData) => {
477
+ if (!storageData) {
478
+ return;
479
+ }
480
+ result.variantIds.forEach((variant) => {
481
+ const message = buildPersonalizationResultMessage(
482
+ storageData.sessionId,
483
+ storageData.visitorId,
484
+ pageId,
485
+ endpoint.projectId,
486
+ result,
487
+ variant,
488
+ compositionData
489
+ );
490
+ addEvent(message);
491
+ });
492
+ },
493
+ goalConvert: (goalId, compositionData) => {
494
+ if (!storageData) {
495
+ return;
496
+ }
497
+ const message = buildGoalConvertMessage(
498
+ storageData.sessionId,
499
+ storageData.visitorId,
500
+ pageId,
501
+ endpoint.projectId,
502
+ goalId,
503
+ compositionData
504
+ );
505
+ addEvent(message);
506
+ },
507
+ forget: () => {
508
+ storage.clear();
509
+ storageData = void 0;
510
+ deduplicationManager == null ? void 0 : deduplicationManager.clear();
511
+ batchProcessor == null ? void 0 : batchProcessor.flush();
512
+ },
513
+ get sessionId() {
514
+ return storageData == null ? void 0 : storageData.sessionId;
515
+ }
516
+ };
517
+ };
518
+ var createInsightsPlugin = (options) => {
519
+ const insights = createInsightsCore(options);
520
+ let previousUrl = void 0;
521
+ let isInitialized = false;
522
+ let eventQueue = [];
523
+ const processQueuedEvents = () => {
524
+ if (isInitialized && eventQueue.length > 0) {
525
+ eventQueue.forEach((event) => event());
526
+ eventQueue = [];
527
+ }
528
+ };
529
+ const queueEvent = (eventFn) => {
530
+ if (isInitialized) {
531
+ eventFn();
532
+ } else {
533
+ eventQueue.push(eventFn);
534
+ }
535
+ };
536
+ return {
537
+ init: (context) => {
538
+ if (typeof window === "undefined") {
539
+ return () => {
540
+ };
541
+ }
542
+ const consentChanged = () => {
543
+ if (context.storage.data.consent) {
544
+ insights.init(context).then(() => {
545
+ isInitialized = true;
546
+ processQueuedEvents();
547
+ });
548
+ } else {
549
+ insights.forget();
550
+ isInitialized = false;
551
+ }
552
+ };
553
+ const handlePersonalizationResult = (data) => {
554
+ queueEvent(() => {
555
+ var _a, _b, _c;
556
+ insights.personalizationResult(data, {
557
+ composition_id: (_a = data.compositionMetadata) == null ? void 0 : _a.compositionId,
558
+ pm_node_path: (_b = data.compositionMetadata) == null ? void 0 : _b.matchedRoute,
559
+ dynamic_inputs: (_c = data.compositionMetadata) == null ? void 0 : _c.dynamicInputs
560
+ });
561
+ });
562
+ };
563
+ const handleTestResult = (result) => {
564
+ queueEvent(() => {
565
+ insights.testResult(
566
+ result,
567
+ result.compositionMetadata ? {
568
+ composition_id: result.compositionMetadata.compositionId,
569
+ pm_node_path: result.compositionMetadata.matchedRoute,
570
+ dynamic_inputs: result.compositionMetadata.dynamicInputs
571
+ } : void 0
572
+ );
573
+ });
574
+ };
575
+ const handleGoalConvert = (result) => {
576
+ const compositionMetadata = context.getCompositionMetadata();
577
+ queueEvent(() => {
578
+ insights.goalConvert(result.goalId, {
579
+ composition_id: compositionMetadata == null ? void 0 : compositionMetadata.compositionId,
580
+ pm_node_path: compositionMetadata == null ? void 0 : compositionMetadata.matchedRoute,
581
+ dynamic_inputs: compositionMetadata == null ? void 0 : compositionMetadata.dynamicInputs
582
+ });
583
+ });
584
+ };
585
+ const handleCanvasDataUpdated = (data) => {
586
+ if (data) {
587
+ queueEvent(() => {
588
+ insights.pageHit({
589
+ composition_id: data.compositionId,
590
+ pm_node_path: data.matchedRoute,
591
+ dynamic_inputs: data.dynamicInputs
592
+ });
593
+ });
594
+ }
595
+ };
596
+ context.storage.events.on("goalConverted", handleGoalConvert);
597
+ context.storage.events.on("consentUpdated", consentChanged);
598
+ context.events.on("personalizationResult", handlePersonalizationResult);
599
+ context.events.on("testResult", handleTestResult);
600
+ context.events.on("canvasDataUpdated", handleCanvasDataUpdated);
601
+ if (context.storage.data.consent) {
602
+ consentChanged();
603
+ }
604
+ return () => {
605
+ context.storage.events.off("consentUpdated", consentChanged);
606
+ context.storage.events.off("goalConverted", handleGoalConvert);
607
+ context.events.off("personalizationResult", handlePersonalizationResult);
608
+ context.events.off("testResult", handleTestResult);
609
+ context.events.off("canvasDataUpdated", handleCanvasDataUpdated);
610
+ };
611
+ },
612
+ update: (context) => {
613
+ if (context.url && context.url.toString() !== previousUrl) {
614
+ previousUrl = context.url.toString();
615
+ queueEvent(() => {
616
+ insights.pageHit();
617
+ });
618
+ }
619
+ },
620
+ forget: () => {
621
+ insights.forget();
622
+ isInitialized = false;
623
+ eventQueue = [];
624
+ }
625
+ };
626
+ };
627
+ export {
628
+ BackendInsightsProxyHandler,
629
+ createBackendInsightsProxyHandler,
630
+ createInsightsPlugin,
631
+ createInsightsStorage,
632
+ createMemoryStorage,
633
+ createInsightsPlugin as enableUniformInsights
634
+ };