@athena-tracker/tracker 1.0.2 → 1.2.1

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/dist/index.js CHANGED
@@ -2,186 +2,1385 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var React = require('react');
6
+ var reactNative = require('react-native');
7
+
5
8
  /**
6
- * @athena-tracker/tracker
7
- * ATHENA Analytics tracker SDK wrapper
8
- */
9
- class AthenaTrackerSDK {
10
- constructor(config) {
11
- this.scriptLoaded = false;
12
- this.loadPromise = null;
13
- this.config = {
14
- apiUrl: 'https://tracker.pascal.cx',
15
- sampleRate: 1.0,
16
- ...config,
17
- };
9
+ * Auto-Detection Logic
10
+ *
11
+ * Automatically detects whether to use on-device or server-side inference
12
+ * based on availability of onnxruntime-react-native
13
+ */
14
+ async function detectInferenceMode(configMode) {
15
+ // Respect explicit configuration
16
+ if (configMode === 'on-device') {
17
+ console.log('[ATHENA] Forcing on-device inference mode');
18
+ return 'on-device';
19
+ }
20
+ if (configMode === 'server') {
21
+ console.log('[ATHENA] Forcing server-side inference mode');
22
+ return 'server';
23
+ }
24
+ // Auto-detect based on onnxruntime-react-native availability
25
+ try {
26
+ // Try to dynamically import onnxruntime-react-native
27
+ await import('onnxruntime-react-native');
28
+ console.log('[ATHENA] On-device inference available (onnxruntime-react-native detected)');
29
+ return 'on-device';
30
+ } catch (error) {
31
+ console.log('[ATHENA] Falling back to server-side inference (onnxruntime-react-native not found)');
32
+ return 'server';
33
+ }
34
+ }
35
+ /**
36
+ * Check if running in React Native environment
37
+ */
38
+ function isReactNative() {
39
+ return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
40
+ }
41
+ /**
42
+ * Check if running in browser environment
43
+ */
44
+ function isBrowser() {
45
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
46
+ }
47
+ /**
48
+ * Get platform identifier
49
+ */
50
+ function getPlatform() {
51
+ if (isReactNative()) {
52
+ return 'react-native';
53
+ } else if (isBrowser()) {
54
+ return 'web';
55
+ }
56
+ return 'unknown';
57
+ }
58
+
59
+ /**
60
+ * On-Device Inference Module
61
+ *
62
+ * Uses onnxruntime-react-native for local ML inference
63
+ * Target latency: <10ms P95
64
+ */
65
+ let InferenceSession;
66
+ let Tensor;
67
+ // Dynamically import ONNX Runtime (only available in React Native)
68
+ async function loadOnnxRuntime() {
69
+ try {
70
+ const onnx = await import('onnxruntime-react-native');
71
+ InferenceSession = onnx.InferenceSession;
72
+ Tensor = onnx.Tensor;
73
+ return true;
74
+ } catch (error) {
75
+ console.error('[ATHENA] Failed to load onnxruntime-react-native:', error);
76
+ return false;
77
+ }
78
+ }
79
+ class OnDeviceInference {
80
+ constructor() {
81
+ this.session = null;
82
+ this.modelLoaded = false;
83
+ }
84
+ /**
85
+ * Initialize ONNX session with model file
86
+ */
87
+ async initialize(modelPath) {
88
+ console.log('[ATHENA] Loading ONNX model for on-device inference...');
89
+ // Load ONNX Runtime
90
+ const loaded = await loadOnnxRuntime();
91
+ if (!loaded) {
92
+ throw new Error('onnxruntime-react-native not available');
18
93
  }
19
- /**
20
- * Load the ATHENA tracker script dynamically
21
- */
22
- async load() {
23
- if (this.scriptLoaded)
24
- return;
25
- if (this.loadPromise)
26
- return this.loadPromise;
27
- this.loadPromise = new Promise((resolve, reject) => {
28
- const script = document.createElement('script');
29
- script.src = `${this.config.apiUrl}/v1/tracker.min.js`;
30
- script.async = true;
31
- script.onload = () => {
32
- this.scriptLoaded = true;
33
- this.initialize();
34
- resolve();
35
- };
36
- script.onerror = () => {
37
- reject(new Error('Failed to load ATHENA tracker script'));
38
- };
39
- document.head.appendChild(script);
40
- });
41
- return this.loadPromise;
42
- }
43
- /**
44
- * Initialize the tracker with configuration
45
- */
46
- initialize() {
47
- if (typeof window !== 'undefined' && window.PascalTracker) {
48
- window.athenaTracker = new window.PascalTracker(this.config);
49
- }
94
+ try {
95
+ const startTime = performance.now();
96
+ this.session = await InferenceSession.create(modelPath);
97
+ const loadTime = performance.now() - startTime;
98
+ this.modelLoaded = true;
99
+ console.log(`[ATHENA] ONNX model loaded successfully (${loadTime.toFixed(0)}ms)`);
100
+ } catch (error) {
101
+ console.error('[ATHENA] Failed to load ONNX model:', error);
102
+ throw new Error(`Model loading failed: ${error}`);
50
103
  }
51
- /**
52
- * Get the underlying tracker instance
53
- */
54
- getTracker() {
55
- if (typeof window === 'undefined') {
56
- console.warn('ATHENA Tracker: window is not defined (SSR environment)');
57
- return null;
58
- }
59
- if (!window.athenaTracker) {
60
- console.warn('ATHENA Tracker: not initialized. Call load() first.');
61
- return null;
62
- }
63
- return window.athenaTracker;
64
- }
65
- /**
66
- * Identify a user
67
- */
68
- identify(userId, properties) {
69
- const tracker = this.getTracker();
70
- if (tracker && tracker.identify) {
71
- tracker.identify({
72
- userId,
73
- ...properties,
74
- });
75
- }
104
+ }
105
+ /**
106
+ * Run inference on feature vector
107
+ */
108
+ async predict(features) {
109
+ if (!this.modelLoaded || !this.session) {
110
+ throw new Error('ONNX model not initialized. Call initialize() first.');
76
111
  }
77
- /**
78
- * Track a custom event
79
- */
80
- track(eventName, properties) {
81
- const tracker = this.getTracker();
82
- if (tracker && tracker.track) {
83
- tracker.track(eventName, properties);
84
- }
112
+ const startTime = performance.now();
113
+ try {
114
+ // Create input tensor
115
+ const inputTensor = new Tensor('float32', features, [1, features.length]);
116
+ const feeds = {
117
+ input: inputTensor
118
+ };
119
+ // Run inference
120
+ const results = await this.session.run(feeds);
121
+ const inferenceTime = performance.now() - startTime;
122
+ // Parse output
123
+ const prediction = this.parseOutput(results.output);
124
+ console.log(`[ATHENA] On-device inference complete: ${inferenceTime.toFixed(2)}ms ` + `(class: ${prediction.predicted_class}, confidence: ${prediction.confidence.toFixed(2)})`);
125
+ return {
126
+ ...prediction,
127
+ inference_time_ms: Math.round(inferenceTime * 100) / 100,
128
+ inference_location: 'on-device',
129
+ timestamp: new Date().toISOString()
130
+ };
131
+ } catch (error) {
132
+ console.error('[ATHENA] On-device inference failed:', error);
133
+ throw new Error(`Inference failed: ${error}`);
85
134
  }
86
- /**
87
- * Track a page view
88
- */
89
- page(pageName, properties) {
90
- const tracker = this.getTracker();
91
- if (tracker && tracker.page) {
92
- tracker.page(pageName, properties);
93
- }
135
+ }
136
+ /**
137
+ * Parse ONNX output tensor to prediction result
138
+ */
139
+ parseOutput(outputTensor) {
140
+ const data = outputTensor.data;
141
+ // Extract predictions from model output
142
+ // Model outputs: [predicted_class_idx, confidence, purchase_intent, cart_abandon_risk, checkout_abandon_risk]
143
+ const predictedClassIdx = data[0];
144
+ const confidence = data[1];
145
+ const purchaseIntent = data[2];
146
+ const cartAbandonRisk = data[3] || 0;
147
+ const checkoutAbandonRisk = data[4] || 0;
148
+ // Map class index to label
149
+ const classLabels = ['engaged_explorer', 'high_intent_buyer', 'cart_abandoner', 'checkout_abandoner', 'bounce_risk'];
150
+ const predictedClass = classLabels[predictedClassIdx] || 'unknown';
151
+ // Determine archetype based on purchase intent
152
+ let archetype;
153
+ if (purchaseIntent >= 0.85) {
154
+ archetype = 'fast_mover';
155
+ } else if (purchaseIntent >= 0.60) {
156
+ archetype = 'on_track';
157
+ } else if (purchaseIntent >= 0.40) {
158
+ archetype = 'slow_adopter';
159
+ } else if (purchaseIntent >= 0.20) {
160
+ archetype = 'at_risk';
161
+ } else {
162
+ archetype = 'different_path';
94
163
  }
95
- /**
96
- * Reset user identity (logout)
97
- */
98
- reset() {
99
- const tracker = this.getTracker();
100
- if (tracker && tracker.reset) {
101
- tracker.reset();
102
- }
164
+ // Generate recommendations
165
+ const recommendation = this.generateRecommendation(predictedClass, purchaseIntent, cartAbandonRisk, checkoutAbandonRisk);
166
+ return {
167
+ predicted_class: predictedClass,
168
+ confidence,
169
+ archetype,
170
+ purchase_intent: purchaseIntent,
171
+ cart_abandonment_risk: cartAbandonRisk,
172
+ checkout_abandonment_risk: checkoutAbandonRisk,
173
+ ...recommendation
174
+ };
175
+ }
176
+ /**
177
+ * Generate action recommendations based on prediction
178
+ */
179
+ generateRecommendation(predictedClass, purchaseIntent, cartAbandonRisk, checkoutAbandonRisk) {
180
+ // High cart abandonment risk
181
+ if (cartAbandonRisk > 0.7) {
182
+ return {
183
+ recommended_action: 'Show cart abandonment discount (10-15% off)',
184
+ urgency: 'high',
185
+ trigger_reason: `High cart abandonment risk (${(cartAbandonRisk * 100).toFixed(0)}%)`
186
+ };
187
+ }
188
+ // High checkout abandonment risk
189
+ if (checkoutAbandonRisk > 0.7) {
190
+ return {
191
+ recommended_action: 'Simplify checkout flow or offer free shipping',
192
+ urgency: 'critical',
193
+ trigger_reason: `High checkout abandonment risk (${(checkoutAbandonRisk * 100).toFixed(0)}%)`
194
+ };
195
+ }
196
+ // High purchase intent
197
+ if (purchaseIntent > 0.8) {
198
+ return {
199
+ recommended_action: 'Show product recommendations or upsell',
200
+ urgency: 'medium',
201
+ trigger_reason: `High purchase intent (${(purchaseIntent * 100).toFixed(0)}%)`
202
+ };
203
+ }
204
+ // Low purchase intent
205
+ if (purchaseIntent < 0.3) {
206
+ return {
207
+ recommended_action: 'Show value proposition or testimonials',
208
+ urgency: 'low',
209
+ trigger_reason: `Low purchase intent (${(purchaseIntent * 100).toFixed(0)}%)`
210
+ };
103
211
  }
104
- /**
105
- * Get current session ID
106
- */
107
- getSessionId() {
108
- const tracker = this.getTracker();
109
- if (tracker && tracker.getSessionId) {
110
- return tracker.getSessionId();
212
+ return {
213
+ recommended_action: 'Continue monitoring',
214
+ urgency: 'low'
215
+ };
216
+ }
217
+ /**
218
+ * Check if model is loaded
219
+ */
220
+ isReady() {
221
+ return this.modelLoaded;
222
+ }
223
+ /**
224
+ * Cleanup resources
225
+ */
226
+ async dispose() {
227
+ if (this.session) {
228
+ try {
229
+ // ONNX Runtime sessions should be disposed
230
+ await this.session.release?.();
231
+ console.log('[ATHENA] On-device inference session disposed');
232
+ } catch (error) {
233
+ console.warn('[ATHENA] Failed to dispose session:', error);
234
+ }
235
+ this.session = null;
236
+ this.modelLoaded = false;
237
+ }
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Server-Side Inference Module
243
+ *
244
+ * Fallback inference via HTTP API when on-device inference is not available
245
+ * Target latency: <100ms P95
246
+ */
247
+ class ServerInference {
248
+ constructor(apiUrl, appToken, timeout = 5000) {
249
+ this.apiUrl = apiUrl;
250
+ this.appToken = appToken;
251
+ this.timeout = timeout;
252
+ }
253
+ /**
254
+ * Make prediction request to server
255
+ */
256
+ async predict(events, sessionId, userId) {
257
+ const startTime = Date.now();
258
+ try {
259
+ const requestBody = {
260
+ app_token: this.appToken,
261
+ events,
262
+ session_id: sessionId,
263
+ user_id: userId
264
+ };
265
+ console.log(`[ATHENA] Sending ${events.length} events to server for inference...`);
266
+ const response = await this.fetchWithTimeout(this.apiUrl, {
267
+ method: 'POST',
268
+ headers: {
269
+ 'Content-Type': 'application/json',
270
+ 'X-App-Token': this.appToken
271
+ },
272
+ body: JSON.stringify(requestBody)
273
+ }, this.timeout);
274
+ if (!response.ok) {
275
+ const errorText = await response.text();
276
+ throw new Error(`Server inference failed: ${response.status} ${errorText}`);
277
+ }
278
+ const result = await response.json();
279
+ const latency = Date.now() - startTime;
280
+ console.log(`[ATHENA] Server-side inference complete: ${latency}ms ` + `(class: ${result.predicted_class}, confidence: ${result.confidence.toFixed(2)})`);
281
+ return {
282
+ ...result,
283
+ inference_time_ms: latency,
284
+ inference_location: 'server',
285
+ timestamp: result.timestamp || new Date().toISOString()
286
+ };
287
+ } catch (error) {
288
+ const latency = Date.now() - startTime;
289
+ console.error(`[ATHENA] Server inference failed after ${latency}ms:`, error.message);
290
+ throw new Error(`Server inference failed: ${error.message}`);
291
+ }
292
+ }
293
+ /**
294
+ * Fetch with timeout
295
+ */
296
+ async fetchWithTimeout(url, options, timeout) {
297
+ const controller = new AbortController();
298
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
299
+ try {
300
+ const response = await fetch(url, {
301
+ ...options,
302
+ signal: controller.signal
303
+ });
304
+ clearTimeout(timeoutId);
305
+ return response;
306
+ } catch (error) {
307
+ clearTimeout(timeoutId);
308
+ if (error.name === 'AbortError') {
309
+ throw new Error(`Request timeout after ${timeout}ms`);
310
+ }
311
+ throw error;
312
+ }
313
+ }
314
+ /**
315
+ * Test server connectivity
316
+ */
317
+ async testConnection() {
318
+ try {
319
+ // Replace /v1/predict with /health for health check
320
+ const healthUrl = this.apiUrl.replace('/v1/predict', '/health');
321
+ console.log(`[ATHENA] Testing server connectivity: ${healthUrl}`);
322
+ const response = await this.fetchWithTimeout(healthUrl, {
323
+ method: 'GET'
324
+ }, Math.min(this.timeout, 5000)); // Use 5s max for health check
325
+ if (response.ok) {
326
+ console.log('[ATHENA] Server connectivity test passed');
327
+ }
328
+ return response.ok;
329
+ } catch (error) {
330
+ console.warn('[ATHENA] Server connectivity test failed:', error.message || error);
331
+ console.warn('[ATHENA] URL attempted:', this.apiUrl.replace('/v1/predict', '/health'));
332
+ return false;
333
+ }
334
+ }
335
+ /**
336
+ * Update configuration
337
+ */
338
+ updateConfig(apiUrl, appToken, timeout) {
339
+ if (apiUrl) this.apiUrl = apiUrl;
340
+ if (appToken) this.appToken = appToken;
341
+ if (timeout) this.timeout = timeout;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * ATHENA Tracker - Main Class
347
+ *
348
+ * Unified interface for behavioral tracking with dual-mode ML inference
349
+ */
350
+ const DEFAULT_API_URL = 'https://tracker.pascal.cx';
351
+ const DEFAULT_ML_API_URL = 'https://pascal-ml-api-344769701859.us-central1.run.app';
352
+ const DEFAULT_MODEL_PATH = 'https://tracker.pascal.cx/models/base_model_int8.onnx';
353
+ const DEFAULT_BATCH_SIZE = 10;
354
+ const DEFAULT_BATCH_INTERVAL_MS = 10000; // 10 seconds
355
+ class AthenaTracker {
356
+ constructor() {
357
+ this.config = null;
358
+ this.state = {
359
+ initialized: false,
360
+ inferenceMode: null,
361
+ sessionId: null,
362
+ userId: null,
363
+ events: []
364
+ };
365
+ this.onDeviceInference = null;
366
+ this.serverInference = null;
367
+ this.batchIntervalId = null;
368
+ this.failureCount = 0;
369
+ this.maxFailures = 3;
370
+ // External IDs for custom audience building (Meta, Google)
371
+ this.externalIds = {};
372
+ // Private constructor for singleton
373
+ }
374
+ /**
375
+ * Get singleton instance
376
+ */
377
+ static getInstance() {
378
+ if (!AthenaTracker.instance) {
379
+ AthenaTracker.instance = new AthenaTracker();
380
+ }
381
+ return AthenaTracker.instance;
382
+ }
383
+ /**
384
+ * Initialize tracker
385
+ */
386
+ static async init(config) {
387
+ const instance = AthenaTracker.getInstance();
388
+ if (instance.state.initialized) {
389
+ console.warn('[ATHENA] Already initialized');
390
+ return;
391
+ }
392
+ console.log('[ATHENA] Initializing tracker...');
393
+ instance.config = {
394
+ ...config,
395
+ apiUrl: config.apiUrl || DEFAULT_API_URL,
396
+ modelPath: config.modelPath || DEFAULT_MODEL_PATH,
397
+ serverInferenceUrl: config.serverInferenceUrl || `${DEFAULT_ML_API_URL}/v1/predict`,
398
+ batching: {
399
+ size: config.batching?.size || DEFAULT_BATCH_SIZE,
400
+ intervalMs: config.batching?.intervalMs || DEFAULT_BATCH_INTERVAL_MS
401
+ }
402
+ };
403
+ // Detect inference mode
404
+ const detectedMode = await detectInferenceMode(config.inferenceMode);
405
+ instance.state.inferenceMode = detectedMode;
406
+ // Initialize appropriate inference engine
407
+ if (detectedMode === 'on-device') {
408
+ await instance.initializeOnDevice();
409
+ } else {
410
+ await instance.initializeServer();
411
+ }
412
+ // Generate session ID
413
+ instance.state.sessionId = instance.generateSessionId();
414
+ // Generate anonymous user ID (will be replaced on identify())
415
+ instance.state.userId = instance.generateAnonymousId();
416
+ // Capture external IDs for custom audience building (browser only)
417
+ instance.captureExternalIds();
418
+ // Start event batching
419
+ instance.startBatching();
420
+ instance.state.initialized = true;
421
+ console.log(`[ATHENA] Tracker initialized successfully (mode: ${detectedMode}, platform: ${getPlatform()})`);
422
+ }
423
+ /**
424
+ * Initialize on-device inference
425
+ */
426
+ async initializeOnDevice() {
427
+ try {
428
+ this.onDeviceInference = new OnDeviceInference();
429
+ await this.onDeviceInference.initialize(this.config.modelPath);
430
+ console.log('[ATHENA] On-device inference ready');
431
+ } catch (error) {
432
+ console.error('[ATHENA] On-device inference initialization failed:', error);
433
+ console.log('[ATHENA] Falling back to server-side inference');
434
+ this.state.inferenceMode = 'server';
435
+ await this.initializeServer();
436
+ }
437
+ }
438
+ /**
439
+ * Initialize server-side inference
440
+ */
441
+ async initializeServer() {
442
+ this.serverInference = new ServerInference(this.config.serverInferenceUrl, this.config.appToken, this.config.timeout || 5000 // Use configured timeout or default to 5s
443
+ );
444
+ // Test connectivity
445
+ const connected = await this.serverInference.testConnection();
446
+ if (!connected) {
447
+ console.warn('[ATHENA] Server connectivity test failed - predictions may fail');
448
+ } else {
449
+ console.log('[ATHENA] Server-side inference ready');
450
+ }
451
+ }
452
+ /**
453
+ * Identify a user
454
+ * Supports email/phone hashing for Meta/Google custom audiences
455
+ *
456
+ * @param userId - User identifier
457
+ * @param traits - User traits (email, phone, name, etc.)
458
+ * @param options - Optional configuration
459
+ */
460
+ static async identify(userId, traits, options) {
461
+ const instance = AthenaTracker.getInstance();
462
+ const previousUserId = instance.state.userId; // Store anonymous ID for mapping
463
+ instance.state.userId = userId;
464
+ // Hash PII for custom audience building if requested
465
+ const hashedTraits = {
466
+ ...traits
467
+ };
468
+ if (options?.hashPII && traits) {
469
+ if (traits.email) {
470
+ hashedTraits.email_sha256 = await instance.hashPII(traits.email);
471
+ }
472
+ if (traits.phone) {
473
+ hashedTraits.phone_sha256 = await instance.hashPII(traits.phone);
474
+ }
475
+ }
476
+ AthenaTracker.track('identify', {
477
+ user_id: userId,
478
+ anonymous_id: previousUserId,
479
+ // Map anonymous → identified user
480
+ ...hashedTraits,
481
+ // Include external IDs for audience sync
482
+ external_ids: Object.keys(instance.externalIds).length > 0 ? instance.externalIds : undefined
483
+ });
484
+ console.log(`[ATHENA] User identified: ${userId}` + (previousUserId?.startsWith('anon_') ? ` (was ${previousUserId})` : ''));
485
+ }
486
+ /**
487
+ * Track an event
488
+ */
489
+ static track(eventType, properties) {
490
+ const instance = AthenaTracker.getInstance();
491
+ if (!instance.state.initialized) {
492
+ console.warn('[ATHENA] Tracker not initialized. Call init() first.');
493
+ return;
494
+ }
495
+ const event = {
496
+ event_type: eventType,
497
+ timestamp: Date.now(),
498
+ properties: properties || {},
499
+ session_id: instance.state.sessionId || undefined,
500
+ user_id: instance.state.userId || undefined
501
+ };
502
+ instance.state.events.push(event);
503
+ if (instance.config?.debug) {
504
+ console.log('[ATHENA] Event tracked:', event);
505
+ }
506
+ // Trigger immediate inference if batch size reached
507
+ if (instance.state.events.length >= instance.config.batching.size) {
508
+ instance.processBatch();
509
+ }
510
+ }
511
+ /**
512
+ * Process event batch and run inference
513
+ */
514
+ async processBatch() {
515
+ if (this.state.events.length === 0) return;
516
+ // Skip inference if too many consecutive failures
517
+ if (this.failureCount >= this.maxFailures) {
518
+ if (this.config?.debug) {
519
+ console.warn(`[ATHENA] Skipping inference after ${this.failureCount} consecutive failures. Will retry after ${this.maxFailures} successful events.`);
520
+ }
521
+ // Reset failure count after clearing queue
522
+ this.failureCount = Math.max(0, this.failureCount - 1);
523
+ this.state.events = []; // Drop events to prevent infinite buildup
524
+ return;
525
+ }
526
+ const events = [...this.state.events];
527
+ this.state.events = [];
528
+ try {
529
+ const prediction = await this.runInference(events);
530
+ this.failureCount = 0; // Reset on success
531
+ if (this.config?.webhook?.enabled && this.config.webhook.url) {
532
+ await this.sendWebhook(prediction);
533
+ }
534
+ if (this.config?.debug) {
535
+ console.log('[ATHENA] Prediction:', prediction);
536
+ }
537
+ } catch (error) {
538
+ this.failureCount++;
539
+ console.error(`[ATHENA] Failed to process batch (${this.failureCount}/${this.maxFailures}):`, error.message || error);
540
+ // Only re-queue if below failure threshold
541
+ if (this.failureCount < this.maxFailures) {
542
+ this.state.events.unshift(...events);
543
+ } else {
544
+ console.warn('[ATHENA] Max failures reached. Dropping events and disabling inference temporarily.');
545
+ }
546
+ }
547
+ }
548
+ /**
549
+ * Run inference (delegates to on-device or server)
550
+ */
551
+ async runInference(events) {
552
+ if (this.state.inferenceMode === 'on-device' && this.onDeviceInference) {
553
+ // Extract features from events (simplified - would need proper feature extraction)
554
+ const features = this.extractFeatures(events);
555
+ return await this.onDeviceInference.predict(features);
556
+ } else if (this.state.inferenceMode === 'server' && this.serverInference) {
557
+ return await this.serverInference.predict(events, this.state.sessionId || undefined, this.state.userId || undefined);
558
+ } else {
559
+ throw new Error('No inference engine available');
560
+ }
561
+ }
562
+ /**
563
+ * Extract features from events (placeholder)
564
+ * In production, this would use proper feature engineering
565
+ */
566
+ extractFeatures(events) {
567
+ // Simplified feature extraction - 20 features
568
+ const features = new Float32Array(20);
569
+ // Event count
570
+ features[0] = events.length;
571
+ // Unique event types
572
+ const uniqueTypes = new Set(events.map(e => e.event_type));
573
+ features[1] = uniqueTypes.size;
574
+ // Time span
575
+ if (events.length > 1) {
576
+ const timeSpan = events[events.length - 1].timestamp - events[0].timestamp;
577
+ features[2] = timeSpan / 1000; // seconds
578
+ }
579
+ // Fill remaining features with event type frequencies
580
+ const typeCounts = new Map();
581
+ events.forEach(e => {
582
+ typeCounts.set(e.event_type, (typeCounts.get(e.event_type) || 0) + 1);
583
+ });
584
+ let idx = 3;
585
+ ['click', 'scroll', 'page_view', 'cart_add', 'checkout_start'].forEach(type => {
586
+ features[idx++] = typeCounts.get(type) || 0;
587
+ });
588
+ return features;
589
+ }
590
+ /**
591
+ * Send prediction to webhook
592
+ */
593
+ async sendWebhook(prediction) {
594
+ if (!this.config?.webhook?.url) return;
595
+ const maxAttempts = this.config.webhook.retry?.maxAttempts || 3;
596
+ const backoffMs = this.config.webhook.retry?.backoffMs || 1000;
597
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
598
+ try {
599
+ const response = await fetch(this.config.webhook.url, {
600
+ method: 'POST',
601
+ headers: {
602
+ 'Content-Type': 'application/json',
603
+ 'X-App-Token': this.config.appToken
604
+ },
605
+ body: JSON.stringify({
606
+ ...prediction,
607
+ session_id: this.state.sessionId,
608
+ user_id: this.state.userId,
609
+ // Include external IDs for custom audience building
610
+ external_ids: Object.keys(this.externalIds).length > 0 ? this.externalIds : undefined
611
+ })
612
+ });
613
+ if (response.ok) {
614
+ if (this.config.debug) {
615
+ console.log('[ATHENA] Webhook delivered successfully');
616
+ }
617
+ return;
111
618
  }
112
- return null;
113
- }
114
- /**
115
- * Get current user ID
116
- */
117
- getUserId() {
118
- const tracker = this.getTracker();
119
- if (tracker && tracker.getUserId) {
120
- return tracker.getUserId();
619
+ throw new Error(`Webhook failed: ${response.status}`);
620
+ } catch (error) {
621
+ console.warn(`[ATHENA] Webhook attempt ${attempt}/${maxAttempts} failed:`, error);
622
+ if (attempt < maxAttempts) {
623
+ await this.sleep(backoffMs * Math.pow(2, attempt - 1));
121
624
  }
122
- return null;
625
+ }
626
+ }
627
+ console.error('[ATHENA] Webhook delivery failed after all retries');
628
+ }
629
+ /**
630
+ * Start event batching interval
631
+ */
632
+ startBatching() {
633
+ this.batchIntervalId = setInterval(() => {
634
+ this.processBatch();
635
+ }, this.config.batching.intervalMs);
636
+ }
637
+ /**
638
+ * Get inference mode
639
+ */
640
+ static getInferenceMode() {
641
+ return AthenaTracker.getInstance().state.inferenceMode;
642
+ }
643
+ /**
644
+ * Get session ID
645
+ */
646
+ static getSessionId() {
647
+ return AthenaTracker.getInstance().state.sessionId;
648
+ }
649
+ /**
650
+ * Cleanup resources
651
+ */
652
+ static async dispose() {
653
+ const instance = AthenaTracker.getInstance();
654
+ if (instance.batchIntervalId) {
655
+ clearInterval(instance.batchIntervalId);
656
+ instance.batchIntervalId = null;
657
+ }
658
+ if (instance.onDeviceInference) {
659
+ await instance.onDeviceInference.dispose();
660
+ instance.onDeviceInference = null;
123
661
  }
662
+ instance.serverInference = null;
663
+ instance.config = null;
664
+ instance.state = {
665
+ initialized: false,
666
+ inferenceMode: null,
667
+ sessionId: null,
668
+ userId: null,
669
+ events: []
670
+ };
671
+ console.log('[ATHENA] Tracker disposed');
672
+ }
673
+ /**
674
+ * Generate session ID
675
+ */
676
+ generateSessionId() {
677
+ return `sess_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
678
+ }
679
+ /**
680
+ * Generate anonymous user ID compatible with Meta/Google standards
681
+ * Format: anon_{timestamp}_{random} for Pascal internal use
682
+ * Can be mapped to hashed PII for custom audience building
683
+ */
684
+ generateAnonymousId() {
685
+ const timestamp = Date.now();
686
+ const random = Math.random().toString(36).substring(2, 11);
687
+ return `anon_${timestamp}_${random}`;
688
+ }
689
+ /**
690
+ * Capture external IDs from Meta and Google for custom audience building
691
+ * Only works in browser environment with cookies
692
+ */
693
+ captureExternalIds() {
694
+ // Skip if not in browser environment
695
+ if (typeof document === 'undefined' || typeof document.cookie === 'undefined') {
696
+ return;
697
+ }
698
+ try {
699
+ const cookies = document.cookie.split(';').reduce((acc, cookie) => {
700
+ const [key, value] = cookie.trim().split('=');
701
+ acc[key] = value;
702
+ return acc;
703
+ }, {});
704
+ // Meta Pixel ID (_fbp) - Format: fb.{version}.{timestamp}.{random}
705
+ if (cookies._fbp) {
706
+ this.externalIds.fbp = cookies._fbp;
707
+ }
708
+ // Meta Click ID (_fbc) - Format: fb.{version}.{timestamp}.{click_id}
709
+ if (cookies._fbc) {
710
+ this.externalIds.fbc = cookies._fbc;
711
+ }
712
+ // Google Analytics Client ID (_ga) - Format: GA1.{version}.{random}.{timestamp}
713
+ if (cookies._ga) {
714
+ this.externalIds.ga = cookies._ga;
715
+ }
716
+ // Google Click ID (gcl_aw) - For Google Ads conversions
717
+ if (cookies.gcl_aw) {
718
+ this.externalIds.gclid = cookies.gcl_aw;
719
+ }
720
+ if (this.config?.debug && Object.keys(this.externalIds).length > 0) {
721
+ console.log('[ATHENA] External IDs captured for audience building:', this.externalIds);
722
+ }
723
+ } catch (error) {
724
+ console.warn('[ATHENA] Failed to capture external IDs:', error);
725
+ }
726
+ }
727
+ /**
728
+ * Hash PII for custom audience matching (SHA-256)
729
+ * Use this when user identifies with email/phone
730
+ */
731
+ async hashPII(value) {
732
+ // Normalize: lowercase, trim whitespace
733
+ const normalized = value.toLowerCase().trim();
734
+ // Use Web Crypto API for hashing (browser/modern Node)
735
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
736
+ const encoder = new TextEncoder();
737
+ const data = encoder.encode(normalized);
738
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
739
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
740
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
741
+ }
742
+ // Try expo-crypto for React Native (Expo)
743
+ try {
744
+ // @ts-expect-error - expo-crypto is an optional peer dependency
745
+ const Crypto = await import('expo-crypto');
746
+ const hash = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, normalized);
747
+ return hash;
748
+ } catch (error) {
749
+ // expo-crypto not available
750
+ }
751
+ // Fallback: return unhashed (server should hash it)
752
+ console.warn('[ATHENA] SHA-256 not available - install expo-crypto for React Native: npx expo install expo-crypto');
753
+ return normalized;
754
+ }
755
+ /**
756
+ * Sleep utility
757
+ */
758
+ sleep(ms) {
759
+ return new Promise(resolve => setTimeout(resolve, ms));
760
+ }
761
+ }
762
+ AthenaTracker.instance = null;
763
+
764
+ /**
765
+ * Webhook Utilities
766
+ *
767
+ * Production-ready webhook system with HMAC signature verification
768
+ * Per ATHENA Integration Plan Phase 4.1
769
+ */
770
+ // Cross-platform crypto support
771
+ let crypto$1;
772
+ try {
773
+ // Node.js environment
774
+ crypto$1 = require('crypto');
775
+ } catch {
776
+ // Browser/React Native - will use Web Crypto API
777
+ crypto$1 = null;
778
+ }
779
+ /**
780
+ * Generate HMAC-SHA256 signature for webhook payload (Node.js version)
781
+ *
782
+ * @param payload - Webhook payload object
783
+ * @param secret - Webhook secret key
784
+ * @returns HMAC signature (hex-encoded)
785
+ */
786
+ function generateWebhookSignatureNode(payload, secret) {
787
+ const payloadString = JSON.stringify(payload);
788
+ const hmac = crypto$1.createHmac('sha256', secret);
789
+ hmac.update(payloadString);
790
+ return hmac.digest('hex');
791
+ }
792
+ /**
793
+ * Generate HMAC-SHA256 signature for webhook payload (Web Crypto API version)
794
+ *
795
+ * @param payload - Webhook payload object
796
+ * @param secret - Webhook secret key
797
+ * @returns HMAC signature (hex-encoded)
798
+ */
799
+ async function generateWebhookSignatureBrowser(payload, secret) {
800
+ const payloadString = JSON.stringify(payload);
801
+ const encoder = new TextEncoder();
802
+ const keyData = encoder.encode(secret);
803
+ const messageData = encoder.encode(payloadString);
804
+ // Import key for HMAC
805
+ const key = await globalThis.crypto.subtle.importKey('raw', keyData, {
806
+ name: 'HMAC',
807
+ hash: 'SHA-256'
808
+ }, false, ['sign']);
809
+ // Generate signature
810
+ const signature = await globalThis.crypto.subtle.sign('HMAC', key, messageData);
811
+ // Convert to hex
812
+ return Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join('');
124
813
  }
125
- // Singleton instance
126
- let trackerInstance = null;
127
814
  /**
128
- * Initialize ATHENA tracker
815
+ * Generate HMAC-SHA256 signature for webhook payload (cross-platform)
816
+ *
817
+ * @param payload - Webhook payload object
818
+ * @param secret - Webhook secret key
819
+ * @returns HMAC signature (hex-encoded)
129
820
  */
130
- function initTracker(config) {
131
- if (!trackerInstance) {
132
- trackerInstance = new AthenaTrackerSDK(config);
821
+ async function generateWebhookSignature(payload, secret) {
822
+ if (crypto$1) {
823
+ // Node.js environment
824
+ return generateWebhookSignatureNode(payload, secret);
825
+ } else if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
826
+ // Browser/Web Crypto API
827
+ return await generateWebhookSignatureBrowser(payload, secret);
828
+ } else {
829
+ // No crypto available - return empty signature
830
+ console.warn('[ATHENA] Crypto API not available - webhook signatures disabled');
831
+ return '';
832
+ }
833
+ }
834
+ /**
835
+ * Verify webhook signature (for use on receiving end)
836
+ *
837
+ * @param payload - Webhook payload object
838
+ * @param signature - Received signature (from X-ATHENA-Signature header)
839
+ * @param secret - Webhook secret key
840
+ * @returns True if signature is valid
841
+ */
842
+ async function verifyWebhookSignature(payload, signature, secret) {
843
+ const expectedSignature = await generateWebhookSignature(payload, secret);
844
+ if (!expectedSignature) {
845
+ // Crypto not available - skip verification
846
+ return true;
847
+ }
848
+ // Use timing-safe comparison to prevent timing attacks (Node.js only)
849
+ if (crypto$1 && crypto$1.timingSafeEqual) {
850
+ try {
851
+ return crypto$1.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
852
+ } catch (error) {
853
+ return false;
133
854
  }
134
- return trackerInstance;
855
+ }
856
+ // Browser fallback - simple comparison (not timing-safe)
857
+ return signature === expectedSignature;
135
858
  }
136
859
  /**
137
- * Get the tracker instance
860
+ * Create webhook payload for prediction update
138
861
  */
139
- function getTracker() {
140
- if (!trackerInstance) {
141
- console.warn('ATHENA Tracker: not initialized. Call initTracker() first.');
862
+ function createPredictionPayload(appId, userId, sessionId, prediction) {
863
+ return {
864
+ event: 'prediction_update',
865
+ appId,
866
+ timestamp: new Date().toISOString(),
867
+ data: {
868
+ user_id: userId,
869
+ session_id: sessionId,
870
+ ...prediction
142
871
  }
143
- return trackerInstance;
872
+ };
144
873
  }
145
874
  /**
146
- * Convenience methods (use singleton instance)
875
+ * Create webhook payload for segment update
147
876
  */
148
- function identify(userId, properties) {
149
- trackerInstance?.identify(userId, properties);
877
+ function createSegmentUpdatePayload(appId, segment, userCount, users) {
878
+ return {
879
+ event: 'segment_update',
880
+ appId,
881
+ timestamp: new Date().toISOString(),
882
+ data: {
883
+ segment,
884
+ user_count: userCount,
885
+ users
886
+ }
887
+ };
150
888
  }
151
- function track(eventName, properties) {
152
- trackerInstance?.track(eventName, properties);
889
+ /**
890
+ * Create webhook payload for friction detection
891
+ */
892
+ function createFrictionPayload(appId, frictionType, screen, selector, affectedUsers) {
893
+ return {
894
+ event: 'friction_detected',
895
+ appId,
896
+ timestamp: new Date().toISOString(),
897
+ data: {
898
+ type: frictionType,
899
+ screen,
900
+ selector,
901
+ affected_users: affectedUsers,
902
+ severity: affectedUsers > 100 ? 'high' : affectedUsers > 50 ? 'medium' : 'low'
903
+ }
904
+ };
153
905
  }
154
- function page(pageName, properties) {
155
- trackerInstance?.page(pageName, properties);
906
+ /**
907
+ * Create webhook payload for conversion event
908
+ */
909
+ function createConversionPayload(appId, userId, sessionId, conversionType, value, metadata) {
910
+ return {
911
+ event: 'conversion',
912
+ appId,
913
+ timestamp: new Date().toISOString(),
914
+ data: {
915
+ user_id: userId,
916
+ session_id: sessionId,
917
+ conversion_type: conversionType,
918
+ value,
919
+ ...metadata
920
+ }
921
+ };
156
922
  }
157
- function reset() {
158
- trackerInstance?.reset();
923
+ /**
924
+ * Webhook delivery with retry logic and exponential backoff
925
+ *
926
+ * @param url - Webhook URL
927
+ * @param payload - Webhook payload
928
+ * @param secret - Webhook secret for signature (optional)
929
+ * @param options - Delivery options
930
+ * @returns Delivery result
931
+ */
932
+ async function deliverWebhook(url, payload, secret, options = {}) {
933
+ const maxAttempts = options.maxAttempts || 3;
934
+ const backoffMs = options.backoffMs || 1000;
935
+ const timeoutMs = options.timeoutMs || 5000;
936
+ const signature = secret ? await generateWebhookSignature(payload, secret) : '';
937
+ const startTime = Date.now();
938
+ let lastError;
939
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
940
+ try {
941
+ const controller = new AbortController();
942
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
943
+ const response = await fetch(url, {
944
+ method: 'POST',
945
+ headers: {
946
+ 'Content-Type': 'application/json',
947
+ 'X-ATHENA-Signature': signature,
948
+ 'X-ATHENA-Event': payload.event,
949
+ 'X-ATHENA-Timestamp': payload.timestamp
950
+ },
951
+ body: JSON.stringify(payload),
952
+ signal: controller.signal
953
+ });
954
+ clearTimeout(timeoutId);
955
+ if (response.ok) {
956
+ const latencyMs = Date.now() - startTime;
957
+ return {
958
+ success: true,
959
+ attempts: attempt,
960
+ latencyMs
961
+ };
962
+ }
963
+ lastError = `HTTP ${response.status}: ${response.statusText}`;
964
+ } catch (error) {
965
+ lastError = error.name === 'AbortError' ? `Timeout after ${timeoutMs}ms` : error.message || 'Unknown error';
966
+ }
967
+ // Wait before retry (exponential backoff)
968
+ if (attempt < maxAttempts) {
969
+ const delayMs = backoffMs * Math.pow(2, attempt - 1);
970
+ await new Promise(resolve => setTimeout(resolve, delayMs));
971
+ }
972
+ }
973
+ const latencyMs = Date.now() - startTime;
974
+ return {
975
+ success: false,
976
+ attempts: maxAttempts,
977
+ latencyMs,
978
+ error: lastError
979
+ };
980
+ }
981
+ /**
982
+ * Validate webhook payload structure
983
+ */
984
+ function isValidWebhookPayload(payload) {
985
+ if (typeof payload !== 'object' || payload === null) return false;
986
+ const validEvents = ['prediction_update', 'segment_update', 'friction_detected', 'conversion', 'session_end'];
987
+ return typeof payload.event === 'string' && validEvents.includes(payload.event) && typeof payload.appId === 'string' && typeof payload.timestamp === 'string' && typeof payload.data === 'object' && payload.data !== null;
159
988
  }
160
- function getSessionId() {
161
- return trackerInstance?.getSessionId() || null;
989
+
990
+ /**
991
+ * ATHENA OTA Wrapper
992
+ *
993
+ * Wraps the React Native app and forces a reload on first launch after OTA update
994
+ * This ensures the ATHENA tracker is loaded fresh after deployment
995
+ *
996
+ * Usage:
997
+ * import { AthenaOTAWrapper } from '@athena/tracker';
998
+ *
999
+ * export default function App() {
1000
+ * return (
1001
+ * <AthenaOTAWrapper>
1002
+ * <YourAppContent />
1003
+ * </AthenaOTAWrapper>
1004
+ * );
1005
+ * }
1006
+ */
1007
+ // Dynamic import of expo-updates (optional peer dependency)
1008
+ let Updates = null;
1009
+ async function loadExpoUpdates() {
1010
+ try {
1011
+ Updates = await import('expo-updates');
1012
+ return true;
1013
+ } catch (error) {
1014
+ console.warn('[ATHENA] expo-updates not available - OTA updates disabled');
1015
+ return false;
1016
+ }
162
1017
  }
163
- function getUserId() {
164
- return trackerInstance?.getUserId() || null;
1018
+ function AthenaOTAWrapper({
1019
+ children,
1020
+ loadingMessage = 'Loading...',
1021
+ updateMessage = 'Updating...'
1022
+ }) {
1023
+ const [isCheckingUpdate, setIsCheckingUpdate] = React.useState(true);
1024
+ const [updateAvailable, setUpdateAvailable] = React.useState(false);
1025
+ const [error, setError] = React.useState(null);
1026
+ React.useEffect(() => {
1027
+ checkForUpdates();
1028
+ }, []);
1029
+ async function checkForUpdates() {
1030
+ try {
1031
+ // Load expo-updates module
1032
+ const hasUpdates = await loadExpoUpdates();
1033
+ if (!hasUpdates) {
1034
+ // expo-updates not available, continue without OTA check
1035
+ setIsCheckingUpdate(false);
1036
+ return;
1037
+ }
1038
+ // Check if running in development mode (skip update check)
1039
+ if (__DEV__) {
1040
+ console.log('[ATHENA] Development mode - skipping OTA update check');
1041
+ setIsCheckingUpdate(false);
1042
+ return;
1043
+ }
1044
+ console.log('[ATHENA] Checking for OTA updates...');
1045
+ // Check for available updates
1046
+ const update = await Updates.checkForUpdateAsync();
1047
+ if (update.isAvailable) {
1048
+ console.log('[ATHENA] Update available - fetching...');
1049
+ setUpdateAvailable(true);
1050
+ // Fetch the new update
1051
+ await Updates.fetchUpdateAsync();
1052
+ console.log('[ATHENA] Update fetched - reloading app...');
1053
+ // Force reload to apply update
1054
+ await Updates.reloadAsync();
1055
+ // If reloadAsync fails, we'll never reach here
1056
+ } else {
1057
+ console.log('[ATHENA] No updates available - continuing with current version');
1058
+ setIsCheckingUpdate(false);
1059
+ }
1060
+ } catch (error) {
1061
+ // Don't block app if update check fails
1062
+ console.warn('[ATHENA] Update check failed:', error.message);
1063
+ setError(error.message);
1064
+ setIsCheckingUpdate(false);
1065
+ }
1066
+ }
1067
+ // Show loading screen while checking for updates
1068
+ if (isCheckingUpdate || updateAvailable) {
1069
+ return React.createElement(reactNative.View, {
1070
+ style: styles.container
1071
+ }, React.createElement(reactNative.ActivityIndicator, {
1072
+ size: "large",
1073
+ color: "#007AFF"
1074
+ }), React.createElement(reactNative.Text, {
1075
+ style: styles.text
1076
+ }, updateAvailable ? updateMessage : loadingMessage), error && React.createElement(reactNative.Text, {
1077
+ style: styles.errorText
1078
+ }, "Update check failed: ", error));
1079
+ }
1080
+ // Render app content once update check is complete
1081
+ return React.createElement(React.Fragment, null, children);
165
1082
  }
166
- // Default export
167
- var index = {
168
- initTracker,
169
- getTracker,
170
- identify,
171
- track,
172
- page,
173
- reset,
174
- getSessionId,
175
- getUserId,
176
- };
1083
+ const styles = reactNative.StyleSheet.create({
1084
+ container: {
1085
+ flex: 1,
1086
+ justifyContent: 'center',
1087
+ alignItems: 'center',
1088
+ backgroundColor: '#FFFFFF'
1089
+ },
1090
+ text: {
1091
+ marginTop: 16,
1092
+ color: '#666666',
1093
+ fontSize: 14,
1094
+ fontWeight: '500'
1095
+ },
1096
+ errorText: {
1097
+ marginTop: 8,
1098
+ color: '#FF3B30',
1099
+ fontSize: 12,
1100
+ textAlign: 'center',
1101
+ paddingHorizontal: 32
1102
+ }
1103
+ });
1104
+
1105
+ /**
1106
+ * React Native Event Capture
1107
+ *
1108
+ * Captures behavioral events from React Native apps
1109
+ * - Touch events (Tap, Swipe, LongPress)
1110
+ * - Navigation events (Screen changes)
1111
+ * - App lifecycle events (Open, Background, Foreground, Close)
1112
+ * - Form interactions
1113
+ * - Network errors
1114
+ */
1115
+ class ReactNativeEventCapture {
1116
+ constructor(config = {}) {
1117
+ this.events = [];
1118
+ this.appStateSubscription = null;
1119
+ this.panResponder = null;
1120
+ this.batchTimer = null;
1121
+ this.currentScreen = 'Unknown';
1122
+ this.sessionStartTime = Date.now();
1123
+ this.config = {
1124
+ captureTouch: config.captureTouch !== false,
1125
+ captureNavigation: config.captureNavigation !== false,
1126
+ captureLifecycle: config.captureLifecycle !== false,
1127
+ captureNetworkErrors: config.captureNetworkErrors !== false,
1128
+ batchSize: config.batchSize || 10,
1129
+ batchIntervalMs: config.batchIntervalMs || 10000
1130
+ };
1131
+ }
1132
+ /**
1133
+ * Start capturing events
1134
+ */
1135
+ start() {
1136
+ console.log('[ATHENA] Starting React Native event capture');
1137
+ if (this.config.captureLifecycle) {
1138
+ this.setupLifecycleTracking();
1139
+ }
1140
+ if (this.config.captureTouch) {
1141
+ this.setupTouchTracking();
1142
+ }
1143
+ if (this.config.captureNetworkErrors) {
1144
+ this.setupNetworkErrorTracking();
1145
+ }
1146
+ // Start batch timer
1147
+ this.batchTimer = setInterval(() => {
1148
+ this.flushEvents();
1149
+ }, this.config.batchIntervalMs);
1150
+ // Capture initial AppOpen event
1151
+ this.captureEvent({
1152
+ event_type: 'AppOpen',
1153
+ timestamp: Date.now(),
1154
+ properties: {
1155
+ screen: this.currentScreen,
1156
+ session_start: true
1157
+ }
1158
+ });
1159
+ }
1160
+ /**
1161
+ * Stop capturing events
1162
+ */
1163
+ stop() {
1164
+ console.log('[ATHENA] Stopping React Native event capture');
1165
+ if (this.appStateSubscription) {
1166
+ this.appStateSubscription.remove();
1167
+ }
1168
+ if (this.batchTimer) {
1169
+ clearInterval(this.batchTimer);
1170
+ }
1171
+ // Flush any remaining events
1172
+ this.flushEvents();
1173
+ }
1174
+ /**
1175
+ * Setup app lifecycle tracking (Open, Background, Foreground, Close)
1176
+ */
1177
+ setupLifecycleTracking() {
1178
+ this.appStateSubscription = reactNative.AppState.addEventListener('change', nextAppState => {
1179
+ if (nextAppState === 'active') {
1180
+ this.captureEvent({
1181
+ event_type: 'AppForeground',
1182
+ timestamp: Date.now(),
1183
+ properties: {
1184
+ screen: this.currentScreen
1185
+ }
1186
+ });
1187
+ } else if (nextAppState === 'background') {
1188
+ this.captureEvent({
1189
+ event_type: 'AppBackground',
1190
+ timestamp: Date.now(),
1191
+ properties: {
1192
+ screen: this.currentScreen,
1193
+ session_duration: Date.now() - this.sessionStartTime
1194
+ }
1195
+ });
1196
+ } else if (nextAppState === 'inactive') {
1197
+ this.captureEvent({
1198
+ event_type: 'AppInactive',
1199
+ timestamp: Date.now(),
1200
+ properties: {
1201
+ screen: this.currentScreen
1202
+ }
1203
+ });
1204
+ }
1205
+ });
1206
+ }
1207
+ /**
1208
+ * Setup touch event tracking
1209
+ */
1210
+ setupTouchTracking() {
1211
+ let touchStartTime = 0;
1212
+ this.panResponder = reactNative.PanResponder.create({
1213
+ onStartShouldSetPanResponder: () => true,
1214
+ onMoveShouldSetPanResponder: () => true,
1215
+ onPanResponderGrant: (_evt, _gestureState) => {
1216
+ touchStartTime = Date.now();
1217
+ },
1218
+ onPanResponderRelease: (evt, gestureState) => {
1219
+ const touchDuration = Date.now() - touchStartTime;
1220
+ const deltaX = Math.abs(gestureState.dx);
1221
+ const deltaY = Math.abs(gestureState.dy);
1222
+ // Detect gesture type
1223
+ if (touchDuration > 500 && deltaX < 10 && deltaY < 10) {
1224
+ // Long press
1225
+ this.captureEvent({
1226
+ event_type: 'LongPress',
1227
+ timestamp: Date.now(),
1228
+ properties: {
1229
+ screen: this.currentScreen,
1230
+ x: evt.nativeEvent.pageX,
1231
+ y: evt.nativeEvent.pageY,
1232
+ duration: touchDuration
1233
+ }
1234
+ });
1235
+ } else if (deltaX > 50 || deltaY > 50) {
1236
+ // Swipe
1237
+ const direction = deltaX > deltaY ? gestureState.dx > 0 ? 'right' : 'left' : gestureState.dy > 0 ? 'down' : 'up';
1238
+ this.captureEvent({
1239
+ event_type: 'Swipe',
1240
+ timestamp: Date.now(),
1241
+ properties: {
1242
+ screen: this.currentScreen,
1243
+ direction,
1244
+ distance: Math.sqrt(deltaX * deltaX + deltaY * deltaY)
1245
+ }
1246
+ });
1247
+ } else {
1248
+ // Tap
1249
+ this.captureEvent({
1250
+ event_type: 'Tap',
1251
+ timestamp: Date.now(),
1252
+ properties: {
1253
+ screen: this.currentScreen,
1254
+ x: evt.nativeEvent.pageX,
1255
+ y: evt.nativeEvent.pageY,
1256
+ duration: touchDuration
1257
+ }
1258
+ });
1259
+ }
1260
+ }
1261
+ });
1262
+ }
1263
+ /**
1264
+ * Setup network error tracking
1265
+ */
1266
+ setupNetworkErrorTracking() {
1267
+ // Monkey-patch fetch to track network errors
1268
+ const originalFetch = global.fetch;
1269
+ global.fetch = async (input, init) => {
1270
+ const startTime = Date.now();
1271
+ const url = typeof input === 'string' ? input : input.url || String(input);
1272
+ try {
1273
+ const response = await originalFetch(input, init);
1274
+ if (!response.ok) {
1275
+ this.captureEvent({
1276
+ event_type: 'NetworkError',
1277
+ timestamp: Date.now(),
1278
+ properties: {
1279
+ screen: this.currentScreen,
1280
+ url,
1281
+ status: response.status,
1282
+ statusText: response.statusText,
1283
+ duration: Date.now() - startTime
1284
+ }
1285
+ });
1286
+ }
1287
+ return response;
1288
+ } catch (error) {
1289
+ this.captureEvent({
1290
+ event_type: 'NetworkError',
1291
+ timestamp: Date.now(),
1292
+ properties: {
1293
+ screen: this.currentScreen,
1294
+ url,
1295
+ error: error.message,
1296
+ duration: Date.now() - startTime
1297
+ }
1298
+ });
1299
+ throw error;
1300
+ }
1301
+ };
1302
+ }
1303
+ /**
1304
+ * Manually track screen navigation
1305
+ * Should be called by navigation library (React Navigation, etc.)
1306
+ */
1307
+ trackScreenView(screenName, params) {
1308
+ this.currentScreen = screenName;
1309
+ this.captureEvent({
1310
+ event_type: 'ScreenView',
1311
+ timestamp: Date.now(),
1312
+ properties: {
1313
+ screen: screenName,
1314
+ params: params || {},
1315
+ previous_screen: this.currentScreen
1316
+ }
1317
+ });
1318
+ }
1319
+ /**
1320
+ * Manually track custom event
1321
+ */
1322
+ track(eventType, properties) {
1323
+ this.captureEvent({
1324
+ event_type: eventType,
1325
+ timestamp: Date.now(),
1326
+ properties: {
1327
+ ...properties,
1328
+ screen: this.currentScreen
1329
+ }
1330
+ });
1331
+ }
1332
+ /**
1333
+ * Capture an event and add to batch
1334
+ */
1335
+ captureEvent(event) {
1336
+ this.events.push(event);
1337
+ // Flush immediately if batch size reached
1338
+ if (this.events.length >= this.config.batchSize) {
1339
+ this.flushEvents();
1340
+ }
1341
+ }
1342
+ /**
1343
+ * Flush accumulated events (to be sent to server)
1344
+ */
1345
+ flushEvents() {
1346
+ if (this.events.length === 0) return;
1347
+ const batch = [...this.events];
1348
+ this.events = [];
1349
+ // Emit events for tracker to send
1350
+ this.onEventBatch?.(batch);
1351
+ }
1352
+ /**
1353
+ * Get PanResponder for manual integration
1354
+ * Usage: <View {...capture.getPanResponderProps()}>
1355
+ */
1356
+ getPanResponderProps() {
1357
+ return this.panResponder?.panHandlers || {};
1358
+ }
1359
+ }
1360
+
1361
+ /**
1362
+ * @athena/tracker
1363
+ *
1364
+ * Behavioral analytics tracker with edge AI
1365
+ */
1366
+ // Version
1367
+ const VERSION = '1.0.0';
177
1368
 
178
- exports.default = index;
179
- exports.getSessionId = getSessionId;
180
- exports.getTracker = getTracker;
181
- exports.getUserId = getUserId;
182
- exports.identify = identify;
183
- exports.initTracker = initTracker;
184
- exports.page = page;
185
- exports.reset = reset;
186
- exports.track = track;
1369
+ exports.AthenaOTAWrapper = AthenaOTAWrapper;
1370
+ exports.AthenaTracker = AthenaTracker;
1371
+ exports.ReactNativeEventCapture = ReactNativeEventCapture;
1372
+ exports.VERSION = VERSION;
1373
+ exports.createConversionPayload = createConversionPayload;
1374
+ exports.createFrictionPayload = createFrictionPayload;
1375
+ exports.createPredictionPayload = createPredictionPayload;
1376
+ exports.createSegmentUpdatePayload = createSegmentUpdatePayload;
1377
+ exports.default = AthenaTracker;
1378
+ exports.deliverWebhook = deliverWebhook;
1379
+ exports.detectInferenceMode = detectInferenceMode;
1380
+ exports.generateWebhookSignature = generateWebhookSignature;
1381
+ exports.getPlatform = getPlatform;
1382
+ exports.isBrowser = isBrowser;
1383
+ exports.isReactNative = isReactNative;
1384
+ exports.isValidWebhookPayload = isValidWebhookPayload;
1385
+ exports.verifyWebhookSignature = verifyWebhookSignature;
187
1386
  //# sourceMappingURL=index.js.map