@builtonveya/analytics-web 4.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.
@@ -0,0 +1,1114 @@
1
+ /*! @builtonveya/analytics-web v4.0.0 — https://github.com/adam-3owl/veya-analytics-api */
2
+ "use strict";
3
+ var VeyaAnalyticsSDK = (() => {
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
23
+
24
+ // index.ts
25
+ var index_exports = {};
26
+ __export(index_exports, {
27
+ VeyaAnalyticsCore: () => VeyaAnalyticsCore,
28
+ VeyaAnalyticsWeb: () => VeyaAnalyticsWeb,
29
+ default: () => VeyaAnalyticsWeb_default,
30
+ generateUUID: () => generateUUID,
31
+ getTimestamp: () => getTimestamp
32
+ });
33
+
34
+ // ../core/VeyaAnalyticsCore.ts
35
+ var DEFAULT_CONFIG = {
36
+ tenantSlug: "",
37
+ tenantId: "",
38
+ storeId: "",
39
+ storeName: "",
40
+ endpoint: "https://analytics.veya.io/v1/events",
41
+ aiEndpoint: "https://analytics.veya.io/v1/classify",
42
+ batchSize: 10,
43
+ flushInterval: 5e3,
44
+ sessionTimeout: 30 * 60 * 1e3,
45
+ debug: false,
46
+ autoDetect: true,
47
+ autoTrackProducts: true,
48
+ autoTrackCart: true,
49
+ interceptOloApi: true,
50
+ useAiClassification: true,
51
+ confidenceThreshold: 0.7,
52
+ platform: "web"
53
+ };
54
+ function generateUUID() {
55
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
56
+ const r = Math.random() * 16 | 0;
57
+ const v = c === "x" ? r : r & 3 | 8;
58
+ return v.toString(16);
59
+ });
60
+ }
61
+ function getTimestamp() {
62
+ return (/* @__PURE__ */ new Date()).toISOString();
63
+ }
64
+ var VeyaAnalyticsCore = class {
65
+ constructor() {
66
+ __publicField(this, "config");
67
+ __publicField(this, "initialized", false);
68
+ __publicField(this, "eventQueue", []);
69
+ __publicField(this, "flushTimer", null);
70
+ // Identifiers
71
+ __publicField(this, "sessionId", "");
72
+ __publicField(this, "visitorId", "");
73
+ __publicField(this, "customerId", null);
74
+ // State
75
+ __publicField(this, "sessionData", {
76
+ is_logged_in: false,
77
+ login_method: null,
78
+ logged_in_at: null,
79
+ login_funnel_step: null,
80
+ experiment_assignments: {}
81
+ });
82
+ __publicField(this, "cartState", {
83
+ items: [],
84
+ itemCount: 0,
85
+ total: 0
86
+ });
87
+ __publicField(this, "deviceInfo", null);
88
+ __publicField(this, "currentScreenName", "");
89
+ __publicField(this, "currentPageType", "");
90
+ __publicField(this, "currentFunnelStep", null);
91
+ // Storage adapter - must be set by platform-specific implementation
92
+ __publicField(this, "storage", null);
93
+ this.config = { ...DEFAULT_CONFIG };
94
+ }
95
+ // ============================================================================
96
+ // INITIALIZATION
97
+ // ============================================================================
98
+ async init(options) {
99
+ if (this.initialized) {
100
+ this.log("warn", "SDK already initialized");
101
+ return this;
102
+ }
103
+ if (!options.tenantSlug && !options.tenantId) {
104
+ throw new Error("VeyaAnalytics: tenantSlug is required");
105
+ }
106
+ this.config = { ...DEFAULT_CONFIG, ...options };
107
+ if (!this.config.tenantSlug) {
108
+ this.config.tenantSlug = this.config.tenantId;
109
+ }
110
+ await this.initVisitor();
111
+ await this.initSession();
112
+ this.deviceInfo = await this.getDeviceInfo();
113
+ this.startFlushTimer();
114
+ this.trackSessionStart();
115
+ this.initialized = true;
116
+ this.log("info", "VeyaAnalytics initialized", {
117
+ tenantSlug: this.config.tenantSlug,
118
+ visitorId: this.visitorId,
119
+ sessionId: this.sessionId,
120
+ platform: this.config.platform
121
+ });
122
+ return this;
123
+ }
124
+ async initVisitor() {
125
+ if (!this.storage) {
126
+ this.visitorId = generateUUID();
127
+ return;
128
+ }
129
+ const stored = await this.storage.getItem("veya_visitor_id");
130
+ if (stored) {
131
+ this.visitorId = stored;
132
+ } else {
133
+ this.visitorId = generateUUID();
134
+ await this.storage.setItem("veya_visitor_id", this.visitorId);
135
+ }
136
+ }
137
+ async initSession() {
138
+ if (!this.storage) {
139
+ this.sessionId = generateUUID();
140
+ return;
141
+ }
142
+ const stored = await this.storage.getItem("veya_session");
143
+ const now = Date.now();
144
+ if (stored) {
145
+ try {
146
+ const sessionData = JSON.parse(stored);
147
+ if (now - sessionData.lastActivity < this.config.sessionTimeout) {
148
+ this.sessionId = sessionData.sessionId;
149
+ this.sessionData = { ...this.sessionData, ...sessionData.data };
150
+ await this.saveSession();
151
+ return;
152
+ }
153
+ } catch (e) {
154
+ }
155
+ }
156
+ this.sessionId = generateUUID();
157
+ await this.saveSession();
158
+ }
159
+ async saveSession() {
160
+ if (!this.storage) return;
161
+ await this.storage.setItem("veya_session", JSON.stringify({
162
+ sessionId: this.sessionId,
163
+ lastActivity: Date.now(),
164
+ data: this.sessionData
165
+ }));
166
+ }
167
+ // Override in platform-specific implementations
168
+ async getDeviceInfo() {
169
+ return {
170
+ device_type: "unknown",
171
+ platform: this.config.platform || "unknown",
172
+ os: "unknown",
173
+ screen_width: 0,
174
+ screen_height: 0,
175
+ language: "en",
176
+ timezone: "UTC"
177
+ };
178
+ }
179
+ // ============================================================================
180
+ // CORE TRACKING
181
+ // ============================================================================
182
+ track(eventName, properties = {}) {
183
+ if (!this.initialized) {
184
+ this.log("warn", "SDK not initialized");
185
+ return this;
186
+ }
187
+ const event = this.buildEvent(eventName, properties);
188
+ this.queueEvent(event);
189
+ this.log("debug", "Event tracked", event);
190
+ return this;
191
+ }
192
+ buildEvent(eventName, properties) {
193
+ var _a, _b;
194
+ return {
195
+ event_id: generateUUID(),
196
+ event_name: eventName,
197
+ event_category: properties.category || this.inferCategory(eventName),
198
+ event_timestamp: getTimestamp(),
199
+ // Identifiers
200
+ tenant_id: this.config.tenantSlug || this.config.tenantId || "",
201
+ tenant_slug: this.config.tenantSlug || this.config.tenantId || "",
202
+ session_id: this.sessionId,
203
+ visitor_id: this.visitorId,
204
+ customer_id: this.customerId || void 0,
205
+ store_id: this.config.storeId || properties.store_id,
206
+ store_name: this.config.storeName || properties.store_name,
207
+ // Context
208
+ page_url: properties.page_url,
209
+ page_title: properties.page_title,
210
+ page_type: properties.page_type || this.currentPageType,
211
+ screen_name: properties.screen_name || this.currentScreenName,
212
+ referrer: properties.referrer,
213
+ // Device info
214
+ ...this.deviceInfo,
215
+ // Session state
216
+ is_logged_in: this.sessionData.is_logged_in,
217
+ login_method: this.sessionData.login_method || void 0,
218
+ // Funnel
219
+ funnel_step: properties.funnel_step || ((_a = this.currentFunnelStep) == null ? void 0 : _a.name),
220
+ funnel_step_number: properties.funnel_step_number || ((_b = this.currentFunnelStep) == null ? void 0 : _b.number),
221
+ // Cart
222
+ cart_value: this.cartState.total,
223
+ cart_item_count: this.cartState.itemCount,
224
+ // Experiments
225
+ experiment_assignments: this.sessionData.experiment_assignments,
226
+ // Custom properties
227
+ properties: this.sanitizeProperties(properties)
228
+ };
229
+ }
230
+ inferCategory(eventName) {
231
+ if (eventName.includes("cart")) return "cart";
232
+ if (eventName.includes("checkout") || eventName.includes("order")) return "checkout";
233
+ if (eventName.includes("product") || eventName.includes("menu")) return "catalog";
234
+ if (eventName.includes("login") || eventName.includes("auth")) return "auth";
235
+ if (eventName.includes("error")) return "error";
236
+ if (eventName.includes("screen") || eventName.includes("page") || eventName.includes("session")) return "navigation";
237
+ return "engagement";
238
+ }
239
+ sanitizeProperties(properties) {
240
+ const sanitized = { ...properties };
241
+ delete sanitized.category;
242
+ delete sanitized.page_type;
243
+ delete sanitized.store_id;
244
+ delete sanitized.page_url;
245
+ delete sanitized.page_title;
246
+ delete sanitized.screen_name;
247
+ delete sanitized.referrer;
248
+ delete sanitized.funnel_step;
249
+ delete sanitized.funnel_step_number;
250
+ const piiFields = ["email", "phone", "password", "ssn", "credit_card", "cvv"];
251
+ piiFields.forEach((field) => delete sanitized[field]);
252
+ return sanitized;
253
+ }
254
+ // ============================================================================
255
+ // SESSION TRACKING
256
+ // ============================================================================
257
+ trackSessionStart() {
258
+ this.track("session_start", {
259
+ platform: this.config.platform
260
+ });
261
+ }
262
+ trackSessionEnd() {
263
+ this.track("session_end", {});
264
+ this.flush(true);
265
+ }
266
+ // ============================================================================
267
+ // SCREEN / PAGE TRACKING
268
+ // ============================================================================
269
+ screenView(screenName, properties = {}) {
270
+ this.currentScreenName = screenName;
271
+ this.currentPageType = this.inferPageType(screenName);
272
+ this.updateFunnelStep(this.currentPageType);
273
+ return this.track("screen_view", {
274
+ screen_name: screenName,
275
+ page_type: this.currentPageType,
276
+ ...properties
277
+ });
278
+ }
279
+ pageView(properties = {}) {
280
+ const pageType = properties.pageType || properties.page_type || "unknown";
281
+ this.currentPageType = pageType;
282
+ this.updateFunnelStep(pageType);
283
+ return this.track("page_view", {
284
+ page_type: pageType,
285
+ ...properties
286
+ });
287
+ }
288
+ inferPageType(screenName) {
289
+ const name = screenName.toLowerCase();
290
+ if (name.includes("menu") || name.includes("order") || name.includes("food")) return "menu";
291
+ if (name.includes("cart") || name.includes("basket") || name.includes("bag")) return "cart";
292
+ if (name.includes("checkout") || name.includes("payment")) return "checkout";
293
+ if (name.includes("confirm") || name.includes("success") || name.includes("thank")) return "confirmation";
294
+ if (name.includes("product") || name.includes("item") || name.includes("detail")) return "product";
295
+ if (name.includes("location") || name.includes("store")) return "location";
296
+ if (name.includes("account") || name.includes("profile") || name.includes("login")) return "account";
297
+ if (name.includes("reward") || name.includes("loyalty") || name.includes("point")) return "rewards";
298
+ if (name.includes("home") || name.includes("landing")) return "home";
299
+ return "other";
300
+ }
301
+ updateFunnelStep(pageType) {
302
+ const funnelMap = {
303
+ home: { name: "landing", number: 1 },
304
+ location: { name: "location", number: 1 },
305
+ menu: { name: "menu", number: 2 },
306
+ product: { name: "menu", number: 2 },
307
+ cart: { name: "cart", number: 3 },
308
+ checkout: { name: "checkout", number: 4 },
309
+ confirmation: { name: "confirmation", number: 5 }
310
+ };
311
+ this.currentFunnelStep = funnelMap[pageType] || null;
312
+ }
313
+ // ============================================================================
314
+ // PRODUCT TRACKING
315
+ // ============================================================================
316
+ productView(product, properties = {}) {
317
+ return this.track("product_view", {
318
+ product_id: product.id || product.product_id,
319
+ product_name: product.name || product.product_name,
320
+ product_category: product.category,
321
+ product_price: product.price,
322
+ ...properties
323
+ });
324
+ }
325
+ productClick(product, properties = {}) {
326
+ return this.track("product_click", {
327
+ product_id: product.id || product.product_id,
328
+ product_name: product.name || product.product_name,
329
+ product_category: product.category,
330
+ product_price: product.price,
331
+ ...properties
332
+ });
333
+ }
334
+ // ============================================================================
335
+ // CART TRACKING
336
+ // ============================================================================
337
+ addToCart(product, quantity = 1, properties = {}) {
338
+ this.cartState.itemCount += quantity;
339
+ this.cartState.total += (product.price || 0) * quantity;
340
+ this.cartState.items.push({ ...product, quantity });
341
+ return this.track("add_to_cart", {
342
+ product_id: product.id || product.product_id,
343
+ product_name: product.name || product.product_name,
344
+ product_category: product.category,
345
+ product_price: product.price,
346
+ quantity,
347
+ ...properties
348
+ });
349
+ }
350
+ removeFromCart(product, quantity = 1, properties = {}) {
351
+ this.cartState.itemCount = Math.max(0, this.cartState.itemCount - quantity);
352
+ this.cartState.total = Math.max(0, this.cartState.total - (product.price || 0) * quantity);
353
+ return this.track("remove_from_cart", {
354
+ product_id: product.id || product.product_id,
355
+ product_name: product.name || product.product_name,
356
+ product_category: product.category,
357
+ product_price: product.price,
358
+ quantity,
359
+ ...properties
360
+ });
361
+ }
362
+ updateCartQuantity(product, oldQuantity, newQuantity) {
363
+ const diff = newQuantity - oldQuantity;
364
+ this.cartState.itemCount += diff;
365
+ this.cartState.total += (product.price || 0) * diff;
366
+ return this.track("update_cart_quantity", {
367
+ product_id: product.id || product.product_id,
368
+ product_name: product.name || product.product_name,
369
+ old_quantity: oldQuantity,
370
+ new_quantity: newQuantity
371
+ });
372
+ }
373
+ setCart(cart) {
374
+ this.cartState = {
375
+ items: cart.items || [],
376
+ itemCount: cart.itemCount || 0,
377
+ total: cart.total || 0
378
+ };
379
+ return this;
380
+ }
381
+ viewCart(properties = {}) {
382
+ return this.track("cart_view", {
383
+ cart_value: this.cartState.total,
384
+ cart_item_count: this.cartState.itemCount,
385
+ ...properties
386
+ });
387
+ }
388
+ // ============================================================================
389
+ // CHECKOUT TRACKING
390
+ // ============================================================================
391
+ checkoutStart(properties = {}) {
392
+ this.updateFunnelStep("checkout");
393
+ return this.track("checkout_start", {
394
+ cart_value: this.cartState.total,
395
+ cart_item_count: this.cartState.itemCount,
396
+ ...properties
397
+ });
398
+ }
399
+ checkoutStep(step, properties = {}) {
400
+ return this.track("checkout_step", {
401
+ checkout_step: step,
402
+ ...properties
403
+ });
404
+ }
405
+ checkoutComplete(order, properties = {}) {
406
+ this.updateFunnelStep("confirmation");
407
+ const event = this.track("checkout_complete", {
408
+ order_id: order.id || order.order_id,
409
+ olo_order_id: order.olo_order_id,
410
+ order_total: order.total,
411
+ subtotal: order.subtotal,
412
+ tax: order.tax,
413
+ tip: order.tip,
414
+ item_count: order.itemCount || order.item_count,
415
+ order_type: order.orderType || order.order_type,
416
+ payment_method: order.paymentMethod || order.payment_method,
417
+ ...properties
418
+ });
419
+ this.cartState = { items: [], itemCount: 0, total: 0 };
420
+ return event;
421
+ }
422
+ // ============================================================================
423
+ // AUTH TRACKING
424
+ // ============================================================================
425
+ loginAttempt(method, properties = {}) {
426
+ var _a;
427
+ return this.track("login_attempt", {
428
+ login_method: method,
429
+ funnel_step: (_a = this.currentFunnelStep) == null ? void 0 : _a.name,
430
+ ...properties
431
+ });
432
+ }
433
+ loginSuccess(method, customerId, properties = {}) {
434
+ var _a, _b;
435
+ this.sessionData.is_logged_in = true;
436
+ this.sessionData.login_method = method;
437
+ this.sessionData.logged_in_at = getTimestamp();
438
+ this.sessionData.login_funnel_step = ((_a = this.currentFunnelStep) == null ? void 0 : _a.name) || null;
439
+ if (customerId) {
440
+ this.customerId = customerId;
441
+ }
442
+ this.saveSession();
443
+ return this.track("login_success", {
444
+ login_method: method,
445
+ customer_id: customerId,
446
+ funnel_step: (_b = this.currentFunnelStep) == null ? void 0 : _b.name,
447
+ ...properties
448
+ });
449
+ }
450
+ loginFailure(method, error, properties = {}) {
451
+ return this.track("login_failure", {
452
+ login_method: method,
453
+ error_message: error,
454
+ ...properties
455
+ });
456
+ }
457
+ logout(properties = {}) {
458
+ const previousCustomerId = this.customerId;
459
+ this.sessionData.is_logged_in = false;
460
+ this.customerId = null;
461
+ this.saveSession();
462
+ return this.track("logout", {
463
+ previous_customer_id: previousCustomerId,
464
+ ...properties
465
+ });
466
+ }
467
+ identify(customerId, traits = {}) {
468
+ this.customerId = customerId;
469
+ this.sessionData.is_logged_in = true;
470
+ this.saveSession();
471
+ return this.track("identify", {
472
+ customer_id: customerId,
473
+ traits: this.sanitizeProperties(traits)
474
+ });
475
+ }
476
+ // ============================================================================
477
+ // STORE SELECTION
478
+ // ============================================================================
479
+ storeSelect(store, properties = {}) {
480
+ const storeId = store.id || store.store_id;
481
+ const storeName = store.name || store.store_name;
482
+ if (storeId) {
483
+ this.config.storeId = storeId;
484
+ }
485
+ if (storeName) {
486
+ this.config.storeName = storeName;
487
+ }
488
+ return this.track("store_select", {
489
+ store_id: storeId,
490
+ store_name: storeName,
491
+ ...properties
492
+ });
493
+ }
494
+ setStore(storeId, storeName) {
495
+ this.config.storeId = storeId;
496
+ if (storeName) {
497
+ this.config.storeName = storeName;
498
+ }
499
+ return this;
500
+ }
501
+ // ============================================================================
502
+ // SEARCH
503
+ // ============================================================================
504
+ search(query, resultCount, properties = {}) {
505
+ return this.track("search", {
506
+ search_query: query,
507
+ result_count: resultCount,
508
+ ...properties
509
+ });
510
+ }
511
+ // ============================================================================
512
+ // LOYALTY / REWARDS
513
+ // ============================================================================
514
+ loyaltyLogin(customer, properties = {}) {
515
+ const customerId = customer.id || customer.customer_id;
516
+ if (customerId) {
517
+ this.customerId = customerId;
518
+ }
519
+ return this.track("loyalty_login", {
520
+ customer_id: customerId,
521
+ loyalty_tier: customer.tier,
522
+ points_balance: customer.points,
523
+ ...properties
524
+ });
525
+ }
526
+ rewardView(reward, properties = {}) {
527
+ return this.track("reward_view", {
528
+ reward_id: reward.id,
529
+ reward_name: reward.name,
530
+ reward_value: reward.value,
531
+ points_required: reward.points_required,
532
+ ...properties
533
+ });
534
+ }
535
+ rewardApply(reward, properties = {}) {
536
+ return this.track("reward_apply", {
537
+ reward_id: reward.id,
538
+ reward_name: reward.name,
539
+ reward_value: reward.value,
540
+ points_used: reward.points_used,
541
+ ...properties
542
+ });
543
+ }
544
+ rewardRemove(reward, properties = {}) {
545
+ return this.track("reward_remove", {
546
+ reward_id: reward.id,
547
+ reward_name: reward.name,
548
+ ...properties
549
+ });
550
+ }
551
+ // ============================================================================
552
+ // PROMO CODES
553
+ // ============================================================================
554
+ promoApply(promo, properties = {}) {
555
+ return this.track("promo_apply", {
556
+ promo_code: promo.code,
557
+ discount_amount: promo.discount,
558
+ success: promo.success,
559
+ error_message: promo.error,
560
+ ...properties
561
+ });
562
+ }
563
+ promoRemove(promoCode, properties = {}) {
564
+ return this.track("promo_remove", {
565
+ promo_code: promoCode,
566
+ ...properties
567
+ });
568
+ }
569
+ // ============================================================================
570
+ // EXPERIMENTS
571
+ // ============================================================================
572
+ setExperiment(experimentKey, variantKey) {
573
+ this.sessionData.experiment_assignments[experimentKey] = variantKey;
574
+ this.saveSession();
575
+ return this.track("experiment_assigned", {
576
+ experiment_key: experimentKey,
577
+ variant_key: variantKey
578
+ });
579
+ }
580
+ // ============================================================================
581
+ // ERRORS
582
+ // ============================================================================
583
+ error(errorType, errorMessage, context = {}) {
584
+ return this.track("error", {
585
+ error_type: errorType,
586
+ error_message: errorMessage,
587
+ ...context
588
+ });
589
+ }
590
+ // ============================================================================
591
+ // QUEUE & FLUSH
592
+ // ============================================================================
593
+ queueEvent(event) {
594
+ this.eventQueue.push(event);
595
+ if (this.eventQueue.length >= this.config.batchSize) {
596
+ this.flush();
597
+ }
598
+ }
599
+ startFlushTimer() {
600
+ this.flushTimer = setInterval(() => {
601
+ if (this.eventQueue.length > 0) {
602
+ this.flush();
603
+ }
604
+ }, this.config.flushInterval);
605
+ }
606
+ async flush(sync = false) {
607
+ if (this.eventQueue.length === 0) return;
608
+ const events = [...this.eventQueue];
609
+ this.eventQueue = [];
610
+ const payload = {
611
+ batch_id: generateUUID(),
612
+ tenant_slug: this.config.tenantSlug || this.config.tenantId,
613
+ events,
614
+ sent_at: getTimestamp()
615
+ };
616
+ try {
617
+ await fetch(this.config.endpoint, {
618
+ method: "POST",
619
+ headers: { "Content-Type": "application/json" },
620
+ body: JSON.stringify(payload)
621
+ });
622
+ this.log("debug", "Flushed events", { count: events.length });
623
+ } catch (error) {
624
+ this.log("error", "Failed to send events", error);
625
+ this.eventQueue = [...events, ...this.eventQueue];
626
+ }
627
+ }
628
+ // ============================================================================
629
+ // UTILITIES
630
+ // ============================================================================
631
+ getSessionId() {
632
+ return this.sessionId;
633
+ }
634
+ getVisitorId() {
635
+ return this.visitorId;
636
+ }
637
+ getCustomerId() {
638
+ return this.customerId;
639
+ }
640
+ isLoggedIn() {
641
+ return this.sessionData.is_logged_in;
642
+ }
643
+ log(level, message, data) {
644
+ if (!this.config.debug && level === "debug") return;
645
+ const prefix = "[VeyaAnalytics]";
646
+ if (level === "error") {
647
+ console.error(prefix, message, data || "");
648
+ } else if (level === "warn") {
649
+ console.warn(prefix, message, data || "");
650
+ } else {
651
+ console.log(prefix, message, data || "");
652
+ }
653
+ }
654
+ destroy() {
655
+ if (this.flushTimer) {
656
+ clearInterval(this.flushTimer);
657
+ }
658
+ this.flush(true);
659
+ }
660
+ };
661
+
662
+ // VeyaAnalyticsWeb.ts
663
+ var LocalStorageAdapter = class {
664
+ async getItem(key) {
665
+ try {
666
+ return localStorage.getItem(key);
667
+ } catch (e) {
668
+ return null;
669
+ }
670
+ }
671
+ async setItem(key, value) {
672
+ try {
673
+ localStorage.setItem(key, value);
674
+ } catch (e) {
675
+ }
676
+ }
677
+ async removeItem(key) {
678
+ try {
679
+ localStorage.removeItem(key);
680
+ } catch (e) {
681
+ }
682
+ }
683
+ };
684
+ var PAGE_PATTERNS = {
685
+ menu: {
686
+ urlPatterns: [/\/menu/i, /\/order/i, /\/food/i, /\/products/i],
687
+ domSignals: ["[data-product]", ".menu-item", ".product-card", ".food-item"],
688
+ titlePatterns: [/menu/i, /order/i, /food/i]
689
+ },
690
+ cart: {
691
+ urlPatterns: [/\/cart/i, /\/basket/i, /\/bag/i],
692
+ domSignals: [".cart-item", ".basket-item", "[data-cart]", ".cart-summary"],
693
+ titlePatterns: [/cart/i, /basket/i, /your order/i]
694
+ },
695
+ checkout: {
696
+ urlPatterns: [/\/checkout/i, /\/payment/i, /\/pay\b/i],
697
+ domSignals: ["[data-checkout]", ".checkout-form", ".payment-form"],
698
+ titlePatterns: [/checkout/i, /payment/i]
699
+ },
700
+ confirmation: {
701
+ urlPatterns: [/\/confirm/i, /\/thank/i, /\/success/i],
702
+ domSignals: [".confirmation", ".order-confirmed", ".thank-you"],
703
+ titlePatterns: [/thank/i, /confirm/i, /success/i]
704
+ },
705
+ product: {
706
+ urlPatterns: [/\/product\//i, /\/item\//i],
707
+ domSignals: [".product-detail", ".item-detail"],
708
+ titlePatterns: []
709
+ },
710
+ location: {
711
+ urlPatterns: [/\/location/i, /\/store/i, /\/find/i],
712
+ domSignals: [".store-locator", ".location-picker"],
713
+ titlePatterns: [/location/i, /find.*store/i]
714
+ },
715
+ account: {
716
+ urlPatterns: [/\/account/i, /\/profile/i, /\/login/i, /\/signin/i],
717
+ domSignals: [".login-form", ".account-page"],
718
+ titlePatterns: [/account/i, /sign in/i, /log in/i]
719
+ },
720
+ rewards: {
721
+ urlPatterns: [/\/reward/i, /\/loyalty/i, /\/points/i],
722
+ domSignals: [".rewards", ".loyalty", ".points-balance"],
723
+ titlePatterns: [/reward/i, /loyalty/i]
724
+ },
725
+ home: {
726
+ urlPatterns: [/^\/$/, /\/home/i],
727
+ domSignals: [".hero", ".home-banner"],
728
+ titlePatterns: []
729
+ }
730
+ };
731
+ var VeyaAnalyticsWeb = class extends VeyaAnalyticsCore {
732
+ constructor() {
733
+ super();
734
+ this.pageDetectionCache = /* @__PURE__ */ new Map();
735
+ this.productTracker = null;
736
+ this.cartTracker = null;
737
+ this.networkInterceptor = null;
738
+ this.storage = new LocalStorageAdapter();
739
+ }
740
+ async init(options) {
741
+ options.platform = "web";
742
+ await super.init(options);
743
+ if (this.config.autoDetect) {
744
+ this.setupPageChangeDetection();
745
+ }
746
+ if (this.config.autoTrackProducts) {
747
+ this.productTracker = new ProductTracker(this);
748
+ this.productTracker.init();
749
+ }
750
+ if (this.config.autoTrackCart) {
751
+ this.cartTracker = new CartTracker(this);
752
+ this.cartTracker.init();
753
+ }
754
+ if (this.config.interceptOloApi) {
755
+ this.networkInterceptor = new NetworkInterceptor(this, this.config);
756
+ this.networkInterceptor.init();
757
+ }
758
+ this.setupBrowserListeners();
759
+ this.trackCurrentPage();
760
+ return this;
761
+ }
762
+ async getDeviceInfo() {
763
+ const ua = navigator.userAgent;
764
+ return {
765
+ device_type: this.getDeviceType(),
766
+ platform: "web",
767
+ os: this.getOS(),
768
+ os_version: this.getOSVersion(),
769
+ screen_width: window.screen.width,
770
+ screen_height: window.screen.height,
771
+ language: navigator.language,
772
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
773
+ };
774
+ }
775
+ getDeviceType() {
776
+ const ua = navigator.userAgent.toLowerCase();
777
+ const width = window.innerWidth;
778
+ if (/ipad|tablet|playbook|silk/i.test(ua) || width >= 768 && width < 1024) {
779
+ return "tablet";
780
+ }
781
+ if (/mobile|iphone|ipod|android|blackberry/i.test(ua) || width < 768) {
782
+ return "mobile";
783
+ }
784
+ return "desktop";
785
+ }
786
+ getOS() {
787
+ const ua = navigator.userAgent;
788
+ if (/Windows/.test(ua)) return "Windows";
789
+ if (/Mac OS X/.test(ua)) return "macOS";
790
+ if (/Android/.test(ua)) return "Android";
791
+ if (/iOS|iPhone|iPad/.test(ua)) return "iOS";
792
+ if (/Linux/.test(ua)) return "Linux";
793
+ return "unknown";
794
+ }
795
+ getOSVersion() {
796
+ const ua = navigator.userAgent;
797
+ const match = ua.match(/(?:Windows NT|Mac OS X|Android|iOS)\s*([\d._]+)/i);
798
+ return match ? match[1].replace(/_/g, ".") : "unknown";
799
+ }
800
+ // ============================================================================
801
+ // PAGE DETECTION
802
+ // ============================================================================
803
+ setupPageChangeDetection() {
804
+ let lastUrl = window.location.href;
805
+ const handleUrlChange = () => {
806
+ if (window.location.href !== lastUrl) {
807
+ lastUrl = window.location.href;
808
+ this.pageDetectionCache.clear();
809
+ this.trackCurrentPage();
810
+ if (this.productTracker) {
811
+ this.productTracker.reset();
812
+ }
813
+ }
814
+ };
815
+ window.addEventListener("popstate", handleUrlChange);
816
+ const originalPushState = history.pushState;
817
+ const originalReplaceState = history.replaceState;
818
+ history.pushState = function() {
819
+ originalPushState.apply(this, arguments);
820
+ handleUrlChange();
821
+ };
822
+ history.replaceState = function() {
823
+ originalReplaceState.apply(this, arguments);
824
+ handleUrlChange();
825
+ };
826
+ window.addEventListener("hashchange", handleUrlChange);
827
+ }
828
+ detectPageType() {
829
+ const cacheKey = window.location.pathname + document.title;
830
+ if (this.pageDetectionCache.has(cacheKey)) {
831
+ return this.pageDetectionCache.get(cacheKey);
832
+ }
833
+ const url = window.location.href.toLowerCase();
834
+ const path = window.location.pathname.toLowerCase();
835
+ const title = document.title.toLowerCase();
836
+ const scores = {};
837
+ for (const [pageType, patterns] of Object.entries(PAGE_PATTERNS)) {
838
+ let score = 0;
839
+ for (const pattern of patterns.urlPatterns) {
840
+ if (pattern.test(path)) {
841
+ score += 40;
842
+ break;
843
+ }
844
+ }
845
+ let domMatches = 0;
846
+ for (const selector of patterns.domSignals) {
847
+ try {
848
+ if (document.querySelector(selector)) domMatches++;
849
+ } catch (e) {
850
+ }
851
+ }
852
+ if (patterns.domSignals.length > 0) {
853
+ score += domMatches / patterns.domSignals.length * 40;
854
+ }
855
+ for (const pattern of patterns.titlePatterns) {
856
+ if (pattern.test(title)) {
857
+ score += 20;
858
+ break;
859
+ }
860
+ }
861
+ scores[pageType] = score / 100;
862
+ }
863
+ let bestType = "unknown";
864
+ let bestScore = 0;
865
+ for (const [type, score] of Object.entries(scores)) {
866
+ if (score > bestScore) {
867
+ bestScore = score;
868
+ bestType = type;
869
+ }
870
+ }
871
+ const result = {
872
+ type: bestScore >= (this.config.confidenceThreshold || 0.7) ? bestType : "unknown",
873
+ confidence: bestScore
874
+ };
875
+ this.pageDetectionCache.set(cacheKey, result);
876
+ this.currentPageType = result.type;
877
+ return result;
878
+ }
879
+ trackCurrentPage() {
880
+ const detection = this.detectPageType();
881
+ this.pageView({
882
+ page_url: window.location.href,
883
+ page_title: document.title,
884
+ page_type: detection.type,
885
+ page_type_confidence: detection.confidence,
886
+ referrer: document.referrer,
887
+ ...this.getUTMParams()
888
+ });
889
+ }
890
+ getUTMParams() {
891
+ const params = new URLSearchParams(window.location.search);
892
+ return {
893
+ utm_source: params.get("utm_source"),
894
+ utm_medium: params.get("utm_medium"),
895
+ utm_campaign: params.get("utm_campaign"),
896
+ utm_term: params.get("utm_term"),
897
+ utm_content: params.get("utm_content")
898
+ };
899
+ }
900
+ // ============================================================================
901
+ // BROWSER LISTENERS
902
+ // ============================================================================
903
+ setupBrowserListeners() {
904
+ document.addEventListener("visibilitychange", () => {
905
+ if (document.visibilityState === "hidden") {
906
+ this.flush(true);
907
+ }
908
+ });
909
+ window.addEventListener("beforeunload", () => {
910
+ this.trackSessionEnd();
911
+ this.flush(true);
912
+ });
913
+ window.addEventListener("error", (e) => {
914
+ this.error("javascript", e.message, {
915
+ filename: e.filename,
916
+ lineno: e.lineno,
917
+ colno: e.colno
918
+ });
919
+ });
920
+ window.addEventListener("unhandledrejection", (e) => {
921
+ var _a;
922
+ this.error("promise_rejection", ((_a = e.reason) == null ? void 0 : _a.message) || String(e.reason));
923
+ });
924
+ }
925
+ // ============================================================================
926
+ // FLUSH OVERRIDE FOR WEB
927
+ // ============================================================================
928
+ async flush(sync = false) {
929
+ if (this.eventQueue.length === 0) return;
930
+ const events = [...this.eventQueue];
931
+ this.eventQueue = [];
932
+ const payload = {
933
+ batch_id: generateUUID(),
934
+ tenant_slug: this.config.tenantSlug || this.config.tenantId,
935
+ events,
936
+ sent_at: getTimestamp()
937
+ };
938
+ if (sync && navigator.sendBeacon) {
939
+ navigator.sendBeacon(this.config.endpoint, JSON.stringify(payload));
940
+ } else {
941
+ try {
942
+ await fetch(this.config.endpoint, {
943
+ method: "POST",
944
+ headers: { "Content-Type": "application/json" },
945
+ body: JSON.stringify(payload),
946
+ keepalive: true
947
+ });
948
+ } catch (error) {
949
+ this.eventQueue = [...events, ...this.eventQueue];
950
+ }
951
+ }
952
+ }
953
+ destroy() {
954
+ if (this.productTracker) this.productTracker.destroy();
955
+ if (this.cartTracker) this.cartTracker.destroy();
956
+ if (this.networkInterceptor) this.networkInterceptor.destroy();
957
+ super.destroy();
958
+ }
959
+ };
960
+ var ProductTracker = class {
961
+ constructor(sdk) {
962
+ this.viewedProducts = /* @__PURE__ */ new Set();
963
+ this.observer = null;
964
+ this.intersectionObserver = null;
965
+ this.sdk = sdk;
966
+ }
967
+ init() {
968
+ this.intersectionObserver = new IntersectionObserver(
969
+ this.handleIntersection.bind(this),
970
+ { threshold: 0.5 }
971
+ );
972
+ this.scanForProducts();
973
+ this.observer = new MutationObserver(() => {
974
+ setTimeout(() => this.scanForProducts(), 500);
975
+ });
976
+ this.observer.observe(document.body, {
977
+ childList: true,
978
+ subtree: true
979
+ });
980
+ }
981
+ scanForProducts() {
982
+ const selectors = [
983
+ "[data-product-id]",
984
+ "[data-product]",
985
+ ".product-card",
986
+ ".menu-item",
987
+ ".food-item"
988
+ ];
989
+ for (const selector of selectors) {
990
+ try {
991
+ document.querySelectorAll(selector).forEach((el) => {
992
+ var _a;
993
+ if (!el.dataset.veyaTracked) {
994
+ el.dataset.veyaTracked = "true";
995
+ (_a = this.intersectionObserver) == null ? void 0 : _a.observe(el);
996
+ }
997
+ });
998
+ } catch (e) {
999
+ }
1000
+ }
1001
+ }
1002
+ handleIntersection(entries) {
1003
+ entries.forEach((entry) => {
1004
+ if (entry.isIntersecting) {
1005
+ const product = this.extractProductData(entry.target);
1006
+ if (product.name && !this.viewedProducts.has(product.id || product.name)) {
1007
+ this.viewedProducts.add(product.id || product.name);
1008
+ this.sdk.productView(product, { auto: true });
1009
+ }
1010
+ }
1011
+ });
1012
+ }
1013
+ extractProductData(element) {
1014
+ var _a, _b;
1015
+ return {
1016
+ id: element.dataset.productId || element.dataset.id || element.id,
1017
+ name: element.dataset.productName || element.dataset.name || ((_b = (_a = element.querySelector(".product-name, .item-name, h2, h3")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim()),
1018
+ category: element.dataset.category,
1019
+ price: parseFloat(element.dataset.price || "0") || null
1020
+ };
1021
+ }
1022
+ reset() {
1023
+ this.viewedProducts.clear();
1024
+ this.scanForProducts();
1025
+ }
1026
+ destroy() {
1027
+ var _a, _b;
1028
+ (_a = this.observer) == null ? void 0 : _a.disconnect();
1029
+ (_b = this.intersectionObserver) == null ? void 0 : _b.disconnect();
1030
+ }
1031
+ };
1032
+ var CartTracker = class {
1033
+ constructor(sdk) {
1034
+ this.observer = null;
1035
+ this.sdk = sdk;
1036
+ }
1037
+ init() {
1038
+ const cartSelectors = [".cart", ".basket", "#cart", "[data-cart]"];
1039
+ this.observer = new MutationObserver(() => {
1040
+ });
1041
+ for (const selector of cartSelectors) {
1042
+ const cartEl = document.querySelector(selector);
1043
+ if (cartEl) {
1044
+ this.observer.observe(cartEl, {
1045
+ childList: true,
1046
+ subtree: true
1047
+ });
1048
+ break;
1049
+ }
1050
+ }
1051
+ }
1052
+ destroy() {
1053
+ var _a;
1054
+ (_a = this.observer) == null ? void 0 : _a.disconnect();
1055
+ }
1056
+ };
1057
+ var NetworkInterceptor = class {
1058
+ constructor(sdk, config) {
1059
+ this.originalFetch = null;
1060
+ this.sdk = sdk;
1061
+ this.config = config;
1062
+ }
1063
+ init() {
1064
+ this.originalFetch = window.fetch;
1065
+ const self = this;
1066
+ window.fetch = function(input, init) {
1067
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
1068
+ return self.originalFetch.apply(window, [input, init]).then((response) => {
1069
+ self.handleResponse(url, response.clone());
1070
+ return response;
1071
+ });
1072
+ };
1073
+ }
1074
+ async handleResponse(url, response) {
1075
+ if (!this.isOloApi(url)) return;
1076
+ try {
1077
+ const data = await response.json();
1078
+ if (url.includes("order") && data.id) {
1079
+ this.sdk.checkoutComplete({
1080
+ id: data.id,
1081
+ olo_order_id: data.id,
1082
+ total: data.total,
1083
+ orderType: data.deliveryMode || data.type
1084
+ }, { auto: true });
1085
+ }
1086
+ if (url.includes("basket") && data.products) {
1087
+ this.sdk.setCart({
1088
+ items: data.products,
1089
+ total: data.total || data.subtotal,
1090
+ itemCount: data.products.reduce((sum, p) => sum + (p.quantity || 1), 0)
1091
+ });
1092
+ }
1093
+ } catch (e) {
1094
+ }
1095
+ }
1096
+ isOloApi(url) {
1097
+ const patterns = [/\/basket/i, /\/order/i, /olo\.com/i];
1098
+ return patterns.some((p) => p.test(url));
1099
+ }
1100
+ destroy() {
1101
+ if (this.originalFetch) {
1102
+ window.fetch = this.originalFetch;
1103
+ }
1104
+ }
1105
+ };
1106
+ var veya = new VeyaAnalyticsWeb();
1107
+ if (typeof window !== "undefined") {
1108
+ window.VeyaAnalytics = veya;
1109
+ window.veya = veya;
1110
+ }
1111
+ var VeyaAnalyticsWeb_default = veya;
1112
+ return __toCommonJS(index_exports);
1113
+ })();
1114
+ //# sourceMappingURL=veya-analytics.js.map