@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,679 @@
1
+ "use strict";
2
+ /**
3
+ * Veya Analytics SDK - Core
4
+ * Platform-agnostic core that works on Web and React Native
5
+ *
6
+ * @version 4.0.0
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.VeyaAnalyticsCore = void 0;
10
+ exports.generateUUID = generateUUID;
11
+ exports.getTimestamp = getTimestamp;
12
+ // ============================================================================
13
+ // DEFAULT CONFIG
14
+ // ============================================================================
15
+ const DEFAULT_CONFIG = {
16
+ tenantSlug: '',
17
+ tenantId: '',
18
+ storeId: '',
19
+ storeName: '',
20
+ endpoint: 'https://analytics.veya.io/v1/events',
21
+ aiEndpoint: 'https://analytics.veya.io/v1/classify',
22
+ batchSize: 10,
23
+ flushInterval: 5000,
24
+ sessionTimeout: 30 * 60 * 1000,
25
+ debug: false,
26
+ autoDetect: true,
27
+ autoTrackProducts: true,
28
+ autoTrackCart: true,
29
+ interceptOloApi: true,
30
+ useAiClassification: true,
31
+ confidenceThreshold: 0.7,
32
+ platform: 'web'
33
+ };
34
+ // ============================================================================
35
+ // UTILITIES
36
+ // ============================================================================
37
+ function generateUUID() {
38
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
39
+ const r = Math.random() * 16 | 0;
40
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
41
+ return v.toString(16);
42
+ });
43
+ }
44
+ function getTimestamp() {
45
+ return new Date().toISOString();
46
+ }
47
+ // ============================================================================
48
+ // CORE ANALYTICS CLASS
49
+ // ============================================================================
50
+ class VeyaAnalyticsCore {
51
+ constructor() {
52
+ this.initialized = false;
53
+ this.eventQueue = [];
54
+ this.flushTimer = null;
55
+ // Identifiers
56
+ this.sessionId = '';
57
+ this.visitorId = '';
58
+ this.customerId = null;
59
+ // State
60
+ this.sessionData = {
61
+ is_logged_in: false,
62
+ login_method: null,
63
+ logged_in_at: null,
64
+ login_funnel_step: null,
65
+ experiment_assignments: {}
66
+ };
67
+ this.cartState = {
68
+ items: [],
69
+ itemCount: 0,
70
+ total: 0
71
+ };
72
+ this.deviceInfo = null;
73
+ this.currentScreenName = '';
74
+ this.currentPageType = '';
75
+ this.currentFunnelStep = null;
76
+ // Storage adapter - must be set by platform-specific implementation
77
+ this.storage = null;
78
+ this.config = { ...DEFAULT_CONFIG };
79
+ }
80
+ // ============================================================================
81
+ // INITIALIZATION
82
+ // ============================================================================
83
+ async init(options) {
84
+ if (this.initialized) {
85
+ this.log('warn', 'SDK already initialized');
86
+ return this;
87
+ }
88
+ if (!options.tenantSlug && !options.tenantId) {
89
+ throw new Error('VeyaAnalytics: tenantSlug is required');
90
+ }
91
+ this.config = { ...DEFAULT_CONFIG, ...options };
92
+ // Sync: tenantSlug takes precedence, fall back to tenantId
93
+ if (!this.config.tenantSlug) {
94
+ this.config.tenantSlug = this.config.tenantId;
95
+ }
96
+ // Initialize visitor and session
97
+ await this.initVisitor();
98
+ await this.initSession();
99
+ // Get device info (implemented by platform-specific class)
100
+ this.deviceInfo = await this.getDeviceInfo();
101
+ // Start flush timer
102
+ this.startFlushTimer();
103
+ // Track session start
104
+ this.trackSessionStart();
105
+ this.initialized = true;
106
+ this.log('info', 'VeyaAnalytics initialized', {
107
+ tenantSlug: this.config.tenantSlug,
108
+ visitorId: this.visitorId,
109
+ sessionId: this.sessionId,
110
+ platform: this.config.platform
111
+ });
112
+ return this;
113
+ }
114
+ async initVisitor() {
115
+ if (!this.storage) {
116
+ this.visitorId = generateUUID();
117
+ return;
118
+ }
119
+ const stored = await this.storage.getItem('veya_visitor_id');
120
+ if (stored) {
121
+ this.visitorId = stored;
122
+ }
123
+ else {
124
+ this.visitorId = generateUUID();
125
+ await this.storage.setItem('veya_visitor_id', this.visitorId);
126
+ }
127
+ }
128
+ async initSession() {
129
+ if (!this.storage) {
130
+ this.sessionId = generateUUID();
131
+ return;
132
+ }
133
+ const stored = await this.storage.getItem('veya_session');
134
+ const now = Date.now();
135
+ if (stored) {
136
+ try {
137
+ const sessionData = JSON.parse(stored);
138
+ if ((now - sessionData.lastActivity) < this.config.sessionTimeout) {
139
+ this.sessionId = sessionData.sessionId;
140
+ this.sessionData = { ...this.sessionData, ...sessionData.data };
141
+ await this.saveSession();
142
+ return;
143
+ }
144
+ }
145
+ catch (e) { }
146
+ }
147
+ // New session
148
+ this.sessionId = generateUUID();
149
+ await this.saveSession();
150
+ }
151
+ async saveSession() {
152
+ if (!this.storage)
153
+ return;
154
+ await this.storage.setItem('veya_session', JSON.stringify({
155
+ sessionId: this.sessionId,
156
+ lastActivity: Date.now(),
157
+ data: this.sessionData
158
+ }));
159
+ }
160
+ // Override in platform-specific implementations
161
+ async getDeviceInfo() {
162
+ return {
163
+ device_type: 'unknown',
164
+ platform: this.config.platform || 'unknown',
165
+ os: 'unknown',
166
+ screen_width: 0,
167
+ screen_height: 0,
168
+ language: 'en',
169
+ timezone: 'UTC'
170
+ };
171
+ }
172
+ // ============================================================================
173
+ // CORE TRACKING
174
+ // ============================================================================
175
+ track(eventName, properties = {}) {
176
+ if (!this.initialized) {
177
+ this.log('warn', 'SDK not initialized');
178
+ return this;
179
+ }
180
+ const event = this.buildEvent(eventName, properties);
181
+ this.queueEvent(event);
182
+ this.log('debug', 'Event tracked', event);
183
+ return this;
184
+ }
185
+ buildEvent(eventName, properties) {
186
+ return {
187
+ event_id: generateUUID(),
188
+ event_name: eventName,
189
+ event_category: properties.category || this.inferCategory(eventName),
190
+ event_timestamp: getTimestamp(),
191
+ // Identifiers
192
+ tenant_id: this.config.tenantSlug || this.config.tenantId || '',
193
+ tenant_slug: this.config.tenantSlug || this.config.tenantId || '',
194
+ session_id: this.sessionId,
195
+ visitor_id: this.visitorId,
196
+ customer_id: this.customerId || undefined,
197
+ store_id: this.config.storeId || properties.store_id,
198
+ store_name: this.config.storeName || properties.store_name,
199
+ // Context
200
+ page_url: properties.page_url,
201
+ page_title: properties.page_title,
202
+ page_type: properties.page_type || this.currentPageType,
203
+ screen_name: properties.screen_name || this.currentScreenName,
204
+ referrer: properties.referrer,
205
+ // Device info
206
+ ...this.deviceInfo,
207
+ // Session state
208
+ is_logged_in: this.sessionData.is_logged_in,
209
+ login_method: this.sessionData.login_method || undefined,
210
+ // Funnel
211
+ funnel_step: properties.funnel_step || this.currentFunnelStep?.name,
212
+ funnel_step_number: properties.funnel_step_number || this.currentFunnelStep?.number,
213
+ // Cart
214
+ cart_value: this.cartState.total,
215
+ cart_item_count: this.cartState.itemCount,
216
+ // Experiments
217
+ experiment_assignments: this.sessionData.experiment_assignments,
218
+ // Custom properties
219
+ properties: this.sanitizeProperties(properties)
220
+ };
221
+ }
222
+ inferCategory(eventName) {
223
+ if (eventName.includes('cart'))
224
+ return 'cart';
225
+ if (eventName.includes('checkout') || eventName.includes('order'))
226
+ return 'checkout';
227
+ if (eventName.includes('product') || eventName.includes('menu'))
228
+ return 'catalog';
229
+ if (eventName.includes('login') || eventName.includes('auth'))
230
+ return 'auth';
231
+ if (eventName.includes('error'))
232
+ return 'error';
233
+ if (eventName.includes('screen') || eventName.includes('page') || eventName.includes('session'))
234
+ return 'navigation';
235
+ return 'engagement';
236
+ }
237
+ sanitizeProperties(properties) {
238
+ const sanitized = { ...properties };
239
+ // Remove internal properties
240
+ delete sanitized.category;
241
+ delete sanitized.page_type;
242
+ delete sanitized.store_id;
243
+ delete sanitized.page_url;
244
+ delete sanitized.page_title;
245
+ delete sanitized.screen_name;
246
+ delete sanitized.referrer;
247
+ delete sanitized.funnel_step;
248
+ delete sanitized.funnel_step_number;
249
+ // Remove PII
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'))
291
+ return 'menu';
292
+ if (name.includes('cart') || name.includes('basket') || name.includes('bag'))
293
+ return 'cart';
294
+ if (name.includes('checkout') || name.includes('payment'))
295
+ return 'checkout';
296
+ if (name.includes('confirm') || name.includes('success') || name.includes('thank'))
297
+ return 'confirmation';
298
+ if (name.includes('product') || name.includes('item') || name.includes('detail'))
299
+ return 'product';
300
+ if (name.includes('location') || name.includes('store'))
301
+ return 'location';
302
+ if (name.includes('account') || name.includes('profile') || name.includes('login'))
303
+ return 'account';
304
+ if (name.includes('reward') || name.includes('loyalty') || name.includes('point'))
305
+ return 'rewards';
306
+ if (name.includes('home') || name.includes('landing'))
307
+ return 'home';
308
+ return 'other';
309
+ }
310
+ updateFunnelStep(pageType) {
311
+ const funnelMap = {
312
+ home: { name: 'landing', number: 1 },
313
+ location: { name: 'location', number: 1 },
314
+ menu: { name: 'menu', number: 2 },
315
+ product: { name: 'menu', number: 2 },
316
+ cart: { name: 'cart', number: 3 },
317
+ checkout: { name: 'checkout', number: 4 },
318
+ confirmation: { name: 'confirmation', number: 5 }
319
+ };
320
+ this.currentFunnelStep = funnelMap[pageType] || null;
321
+ }
322
+ // ============================================================================
323
+ // PRODUCT TRACKING
324
+ // ============================================================================
325
+ productView(product, properties = {}) {
326
+ return this.track('product_view', {
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
+ productClick(product, properties = {}) {
335
+ return this.track('product_click', {
336
+ product_id: product.id || product.product_id,
337
+ product_name: product.name || product.product_name,
338
+ product_category: product.category,
339
+ product_price: product.price,
340
+ ...properties
341
+ });
342
+ }
343
+ // ============================================================================
344
+ // CART TRACKING
345
+ // ============================================================================
346
+ addToCart(product, quantity = 1, properties = {}) {
347
+ this.cartState.itemCount += quantity;
348
+ this.cartState.total += (product.price || 0) * quantity;
349
+ this.cartState.items.push({ ...product, quantity });
350
+ return this.track('add_to_cart', {
351
+ product_id: product.id || product.product_id,
352
+ product_name: product.name || product.product_name,
353
+ product_category: product.category,
354
+ product_price: product.price,
355
+ quantity,
356
+ ...properties
357
+ });
358
+ }
359
+ removeFromCart(product, quantity = 1, properties = {}) {
360
+ this.cartState.itemCount = Math.max(0, this.cartState.itemCount - quantity);
361
+ this.cartState.total = Math.max(0, this.cartState.total - (product.price || 0) * quantity);
362
+ return this.track('remove_from_cart', {
363
+ product_id: product.id || product.product_id,
364
+ product_name: product.name || product.product_name,
365
+ product_category: product.category,
366
+ product_price: product.price,
367
+ quantity,
368
+ ...properties
369
+ });
370
+ }
371
+ updateCartQuantity(product, oldQuantity, newQuantity) {
372
+ const diff = newQuantity - oldQuantity;
373
+ this.cartState.itemCount += diff;
374
+ this.cartState.total += (product.price || 0) * diff;
375
+ return this.track('update_cart_quantity', {
376
+ product_id: product.id || product.product_id,
377
+ product_name: product.name || product.product_name,
378
+ old_quantity: oldQuantity,
379
+ new_quantity: newQuantity
380
+ });
381
+ }
382
+ setCart(cart) {
383
+ this.cartState = {
384
+ items: cart.items || [],
385
+ itemCount: cart.itemCount || 0,
386
+ total: cart.total || 0
387
+ };
388
+ return this;
389
+ }
390
+ viewCart(properties = {}) {
391
+ return this.track('cart_view', {
392
+ cart_value: this.cartState.total,
393
+ cart_item_count: this.cartState.itemCount,
394
+ ...properties
395
+ });
396
+ }
397
+ // ============================================================================
398
+ // CHECKOUT TRACKING
399
+ // ============================================================================
400
+ checkoutStart(properties = {}) {
401
+ this.updateFunnelStep('checkout');
402
+ return this.track('checkout_start', {
403
+ cart_value: this.cartState.total,
404
+ cart_item_count: this.cartState.itemCount,
405
+ ...properties
406
+ });
407
+ }
408
+ checkoutStep(step, properties = {}) {
409
+ return this.track('checkout_step', {
410
+ checkout_step: step,
411
+ ...properties
412
+ });
413
+ }
414
+ checkoutComplete(order, properties = {}) {
415
+ this.updateFunnelStep('confirmation');
416
+ const event = this.track('checkout_complete', {
417
+ order_id: order.id || order.order_id,
418
+ olo_order_id: order.olo_order_id,
419
+ order_total: order.total,
420
+ subtotal: order.subtotal,
421
+ tax: order.tax,
422
+ tip: order.tip,
423
+ item_count: order.itemCount || order.item_count,
424
+ order_type: order.orderType || order.order_type,
425
+ payment_method: order.paymentMethod || order.payment_method,
426
+ ...properties
427
+ });
428
+ // Clear cart
429
+ this.cartState = { items: [], itemCount: 0, total: 0 };
430
+ return event;
431
+ }
432
+ // ============================================================================
433
+ // AUTH TRACKING
434
+ // ============================================================================
435
+ loginAttempt(method, properties = {}) {
436
+ return this.track('login_attempt', {
437
+ login_method: method,
438
+ funnel_step: this.currentFunnelStep?.name,
439
+ ...properties
440
+ });
441
+ }
442
+ loginSuccess(method, customerId, properties = {}) {
443
+ this.sessionData.is_logged_in = true;
444
+ this.sessionData.login_method = method;
445
+ this.sessionData.logged_in_at = getTimestamp();
446
+ this.sessionData.login_funnel_step = this.currentFunnelStep?.name || null;
447
+ if (customerId) {
448
+ this.customerId = customerId;
449
+ }
450
+ this.saveSession();
451
+ return this.track('login_success', {
452
+ login_method: method,
453
+ customer_id: customerId,
454
+ funnel_step: this.currentFunnelStep?.name,
455
+ ...properties
456
+ });
457
+ }
458
+ loginFailure(method, error, properties = {}) {
459
+ return this.track('login_failure', {
460
+ login_method: method,
461
+ error_message: error,
462
+ ...properties
463
+ });
464
+ }
465
+ logout(properties = {}) {
466
+ const previousCustomerId = this.customerId;
467
+ this.sessionData.is_logged_in = false;
468
+ this.customerId = null;
469
+ this.saveSession();
470
+ return this.track('logout', {
471
+ previous_customer_id: previousCustomerId,
472
+ ...properties
473
+ });
474
+ }
475
+ identify(customerId, traits = {}) {
476
+ this.customerId = customerId;
477
+ this.sessionData.is_logged_in = true;
478
+ this.saveSession();
479
+ return this.track('identify', {
480
+ customer_id: customerId,
481
+ traits: this.sanitizeProperties(traits)
482
+ });
483
+ }
484
+ // ============================================================================
485
+ // STORE SELECTION
486
+ // ============================================================================
487
+ storeSelect(store, properties = {}) {
488
+ const storeId = store.id || store.store_id;
489
+ const storeName = store.name || store.store_name;
490
+ if (storeId) {
491
+ this.config.storeId = storeId;
492
+ }
493
+ if (storeName) {
494
+ // Persist so every subsequent event carries the name (the warehouse
495
+ // resolves location names via ANY_VALUE(store_name) per store_id).
496
+ this.config.storeName = storeName;
497
+ }
498
+ return this.track('store_select', {
499
+ store_id: storeId,
500
+ store_name: storeName,
501
+ ...properties
502
+ });
503
+ }
504
+ setStore(storeId, storeName) {
505
+ this.config.storeId = storeId;
506
+ if (storeName) {
507
+ this.config.storeName = storeName;
508
+ }
509
+ return this;
510
+ }
511
+ // ============================================================================
512
+ // SEARCH
513
+ // ============================================================================
514
+ search(query, resultCount, properties = {}) {
515
+ return this.track('search', {
516
+ search_query: query,
517
+ result_count: resultCount,
518
+ ...properties
519
+ });
520
+ }
521
+ // ============================================================================
522
+ // LOYALTY / REWARDS
523
+ // ============================================================================
524
+ loyaltyLogin(customer, properties = {}) {
525
+ const customerId = customer.id || customer.customer_id;
526
+ if (customerId) {
527
+ this.customerId = customerId;
528
+ }
529
+ return this.track('loyalty_login', {
530
+ customer_id: customerId,
531
+ loyalty_tier: customer.tier,
532
+ points_balance: customer.points,
533
+ ...properties
534
+ });
535
+ }
536
+ rewardView(reward, properties = {}) {
537
+ return this.track('reward_view', {
538
+ reward_id: reward.id,
539
+ reward_name: reward.name,
540
+ reward_value: reward.value,
541
+ points_required: reward.points_required,
542
+ ...properties
543
+ });
544
+ }
545
+ rewardApply(reward, properties = {}) {
546
+ return this.track('reward_apply', {
547
+ reward_id: reward.id,
548
+ reward_name: reward.name,
549
+ reward_value: reward.value,
550
+ points_used: reward.points_used,
551
+ ...properties
552
+ });
553
+ }
554
+ rewardRemove(reward, properties = {}) {
555
+ return this.track('reward_remove', {
556
+ reward_id: reward.id,
557
+ reward_name: reward.name,
558
+ ...properties
559
+ });
560
+ }
561
+ // ============================================================================
562
+ // PROMO CODES
563
+ // ============================================================================
564
+ promoApply(promo, properties = {}) {
565
+ return this.track('promo_apply', {
566
+ promo_code: promo.code,
567
+ discount_amount: promo.discount,
568
+ success: promo.success,
569
+ error_message: promo.error,
570
+ ...properties
571
+ });
572
+ }
573
+ promoRemove(promoCode, properties = {}) {
574
+ return this.track('promo_remove', {
575
+ promo_code: promoCode,
576
+ ...properties
577
+ });
578
+ }
579
+ // ============================================================================
580
+ // EXPERIMENTS
581
+ // ============================================================================
582
+ setExperiment(experimentKey, variantKey) {
583
+ this.sessionData.experiment_assignments[experimentKey] = variantKey;
584
+ this.saveSession();
585
+ return this.track('experiment_assigned', {
586
+ experiment_key: experimentKey,
587
+ variant_key: variantKey
588
+ });
589
+ }
590
+ // ============================================================================
591
+ // ERRORS
592
+ // ============================================================================
593
+ error(errorType, errorMessage, context = {}) {
594
+ return this.track('error', {
595
+ error_type: errorType,
596
+ error_message: errorMessage,
597
+ ...context
598
+ });
599
+ }
600
+ // ============================================================================
601
+ // QUEUE & FLUSH
602
+ // ============================================================================
603
+ queueEvent(event) {
604
+ this.eventQueue.push(event);
605
+ if (this.eventQueue.length >= this.config.batchSize) {
606
+ this.flush();
607
+ }
608
+ }
609
+ startFlushTimer() {
610
+ this.flushTimer = setInterval(() => {
611
+ if (this.eventQueue.length > 0) {
612
+ this.flush();
613
+ }
614
+ }, this.config.flushInterval);
615
+ }
616
+ async flush(sync = false) {
617
+ if (this.eventQueue.length === 0)
618
+ return;
619
+ const events = [...this.eventQueue];
620
+ this.eventQueue = [];
621
+ const payload = {
622
+ batch_id: generateUUID(),
623
+ tenant_slug: this.config.tenantSlug || this.config.tenantId,
624
+ events,
625
+ sent_at: getTimestamp()
626
+ };
627
+ try {
628
+ await fetch(this.config.endpoint, {
629
+ method: 'POST',
630
+ headers: { 'Content-Type': 'application/json' },
631
+ body: JSON.stringify(payload)
632
+ });
633
+ this.log('debug', 'Flushed events', { count: events.length });
634
+ }
635
+ catch (error) {
636
+ this.log('error', 'Failed to send events', error);
637
+ // Re-queue events
638
+ this.eventQueue = [...events, ...this.eventQueue];
639
+ }
640
+ }
641
+ // ============================================================================
642
+ // UTILITIES
643
+ // ============================================================================
644
+ getSessionId() {
645
+ return this.sessionId;
646
+ }
647
+ getVisitorId() {
648
+ return this.visitorId;
649
+ }
650
+ getCustomerId() {
651
+ return this.customerId;
652
+ }
653
+ isLoggedIn() {
654
+ return this.sessionData.is_logged_in;
655
+ }
656
+ log(level, message, data) {
657
+ if (!this.config.debug && level === 'debug')
658
+ return;
659
+ const prefix = '[VeyaAnalytics]';
660
+ if (level === 'error') {
661
+ console.error(prefix, message, data || '');
662
+ }
663
+ else if (level === 'warn') {
664
+ console.warn(prefix, message, data || '');
665
+ }
666
+ else {
667
+ console.log(prefix, message, data || '');
668
+ }
669
+ }
670
+ destroy() {
671
+ if (this.flushTimer) {
672
+ clearInterval(this.flushTimer);
673
+ }
674
+ this.flush(true);
675
+ }
676
+ }
677
+ exports.VeyaAnalyticsCore = VeyaAnalyticsCore;
678
+ exports.default = VeyaAnalyticsCore;
679
+ //# sourceMappingURL=VeyaAnalyticsCore.js.map