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