@athena-tracker/tracker 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,175 +1,1236 @@
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
+ };
99
199
  }
100
- /**
101
- * Get current session ID
102
- */
103
- getSessionId() {
104
- const tracker = this.getTracker();
105
- if (tracker && tracker.getSessionId) {
106
- return tracker.getSessionId();
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
+ };
207
+ }
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
+ const response = await this.fetchWithTimeout(healthUrl, {
318
+ method: 'GET'
319
+ }, 10000); // Increased to 10s for mobile networks
320
+ return response.ok;
321
+ } catch (error) {
322
+ console.warn('[ATHENA] Server connectivity test failed:', error);
323
+ return false;
324
+ }
325
+ }
326
+ /**
327
+ * Update configuration
328
+ */
329
+ updateConfig(apiUrl, appToken, timeout) {
330
+ if (apiUrl) this.apiUrl = apiUrl;
331
+ if (appToken) this.appToken = appToken;
332
+ if (timeout) this.timeout = timeout;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * ATHENA Tracker - Main Class
338
+ *
339
+ * Unified interface for behavioral tracking with dual-mode ML inference
340
+ */
341
+ const DEFAULT_API_URL = 'https://tracker.pascal.cx';
342
+ const DEFAULT_ML_API_URL = 'https://pascal-ml-api-344769701859.us-central1.run.app';
343
+ const DEFAULT_MODEL_PATH = 'https://tracker.pascal.cx/models/base_model_int8.onnx';
344
+ const DEFAULT_BATCH_SIZE = 10;
345
+ const DEFAULT_BATCH_INTERVAL_MS = 10000; // 10 seconds
346
+ class AthenaTracker {
347
+ constructor() {
348
+ this.config = null;
349
+ this.state = {
350
+ initialized: false,
351
+ inferenceMode: null,
352
+ sessionId: null,
353
+ userId: null,
354
+ events: []
355
+ };
356
+ this.onDeviceInference = null;
357
+ this.serverInference = null;
358
+ this.batchIntervalId = null;
359
+ // Private constructor for singleton
360
+ }
361
+ /**
362
+ * Get singleton instance
363
+ */
364
+ static getInstance() {
365
+ if (!AthenaTracker.instance) {
366
+ AthenaTracker.instance = new AthenaTracker();
367
+ }
368
+ return AthenaTracker.instance;
369
+ }
370
+ /**
371
+ * Initialize tracker
372
+ */
373
+ static async init(config) {
374
+ const instance = AthenaTracker.getInstance();
375
+ if (instance.state.initialized) {
376
+ console.warn('[ATHENA] Already initialized');
377
+ return;
378
+ }
379
+ console.log('[ATHENA] Initializing tracker...');
380
+ instance.config = {
381
+ ...config,
382
+ apiUrl: config.apiUrl || DEFAULT_API_URL,
383
+ modelPath: config.modelPath || DEFAULT_MODEL_PATH,
384
+ serverInferenceUrl: config.serverInferenceUrl || `${DEFAULT_ML_API_URL}/v1/predict`,
385
+ batching: {
386
+ size: config.batching?.size || DEFAULT_BATCH_SIZE,
387
+ intervalMs: config.batching?.intervalMs || DEFAULT_BATCH_INTERVAL_MS
388
+ }
389
+ };
390
+ // Detect inference mode
391
+ const detectedMode = await detectInferenceMode(config.inferenceMode);
392
+ instance.state.inferenceMode = detectedMode;
393
+ // Initialize appropriate inference engine
394
+ if (detectedMode === 'on-device') {
395
+ await instance.initializeOnDevice();
396
+ } else {
397
+ await instance.initializeServer();
398
+ }
399
+ // Generate session ID
400
+ instance.state.sessionId = instance.generateSessionId();
401
+ // Start event batching
402
+ instance.startBatching();
403
+ instance.state.initialized = true;
404
+ console.log(`[ATHENA] Tracker initialized successfully (mode: ${detectedMode}, platform: ${getPlatform()})`);
405
+ }
406
+ /**
407
+ * Initialize on-device inference
408
+ */
409
+ async initializeOnDevice() {
410
+ try {
411
+ this.onDeviceInference = new OnDeviceInference();
412
+ await this.onDeviceInference.initialize(this.config.modelPath);
413
+ console.log('[ATHENA] On-device inference ready');
414
+ } catch (error) {
415
+ console.error('[ATHENA] On-device inference initialization failed:', error);
416
+ console.log('[ATHENA] Falling back to server-side inference');
417
+ this.state.inferenceMode = 'server';
418
+ await this.initializeServer();
419
+ }
420
+ }
421
+ /**
422
+ * Initialize server-side inference
423
+ */
424
+ async initializeServer() {
425
+ this.serverInference = new ServerInference(this.config.serverInferenceUrl, this.config.appToken);
426
+ // Test connectivity
427
+ const connected = await this.serverInference.testConnection();
428
+ if (!connected) {
429
+ console.warn('[ATHENA] Server connectivity test failed - predictions may fail');
430
+ } else {
431
+ console.log('[ATHENA] Server-side inference ready');
432
+ }
433
+ }
434
+ /**
435
+ * Identify a user
436
+ */
437
+ static identify(userId, traits) {
438
+ const instance = AthenaTracker.getInstance();
439
+ instance.state.userId = userId;
440
+ AthenaTracker.track('identify', {
441
+ user_id: userId,
442
+ ...traits
443
+ });
444
+ console.log(`[ATHENA] User identified: ${userId}`);
445
+ }
446
+ /**
447
+ * Track an event
448
+ */
449
+ static track(eventType, properties) {
450
+ const instance = AthenaTracker.getInstance();
451
+ if (!instance.state.initialized) {
452
+ console.warn('[ATHENA] Tracker not initialized. Call init() first.');
453
+ return;
454
+ }
455
+ const event = {
456
+ event_type: eventType,
457
+ timestamp: Date.now(),
458
+ properties: properties || {},
459
+ session_id: instance.state.sessionId || undefined,
460
+ user_id: instance.state.userId || undefined
461
+ };
462
+ instance.state.events.push(event);
463
+ if (instance.config?.debug) {
464
+ console.log('[ATHENA] Event tracked:', event);
465
+ }
466
+ // Trigger immediate inference if batch size reached
467
+ if (instance.state.events.length >= instance.config.batching.size) {
468
+ instance.processBatch();
469
+ }
470
+ }
471
+ /**
472
+ * Process event batch and run inference
473
+ */
474
+ async processBatch() {
475
+ if (this.state.events.length === 0) return;
476
+ const events = [...this.state.events];
477
+ this.state.events = [];
478
+ try {
479
+ const prediction = await this.runInference(events);
480
+ if (this.config?.webhook?.enabled && this.config.webhook.url) {
481
+ await this.sendWebhook(prediction);
482
+ }
483
+ if (this.config?.debug) {
484
+ console.log('[ATHENA] Prediction:', prediction);
485
+ }
486
+ } catch (error) {
487
+ console.error('[ATHENA] Failed to process batch:', error);
488
+ // Re-queue events on failure
489
+ this.state.events.unshift(...events);
490
+ }
491
+ }
492
+ /**
493
+ * Run inference (delegates to on-device or server)
494
+ */
495
+ async runInference(events) {
496
+ if (this.state.inferenceMode === 'on-device' && this.onDeviceInference) {
497
+ // Extract features from events (simplified - would need proper feature extraction)
498
+ const features = this.extractFeatures(events);
499
+ return await this.onDeviceInference.predict(features);
500
+ } else if (this.state.inferenceMode === 'server' && this.serverInference) {
501
+ return await this.serverInference.predict(events, this.state.sessionId || undefined, this.state.userId || undefined);
502
+ } else {
503
+ throw new Error('No inference engine available');
504
+ }
505
+ }
506
+ /**
507
+ * Extract features from events (placeholder)
508
+ * In production, this would use proper feature engineering
509
+ */
510
+ extractFeatures(events) {
511
+ // Simplified feature extraction - 20 features
512
+ const features = new Float32Array(20);
513
+ // Event count
514
+ features[0] = events.length;
515
+ // Unique event types
516
+ const uniqueTypes = new Set(events.map(e => e.event_type));
517
+ features[1] = uniqueTypes.size;
518
+ // Time span
519
+ if (events.length > 1) {
520
+ const timeSpan = events[events.length - 1].timestamp - events[0].timestamp;
521
+ features[2] = timeSpan / 1000; // seconds
522
+ }
523
+ // Fill remaining features with event type frequencies
524
+ const typeCounts = new Map();
525
+ events.forEach(e => {
526
+ typeCounts.set(e.event_type, (typeCounts.get(e.event_type) || 0) + 1);
527
+ });
528
+ let idx = 3;
529
+ ['click', 'scroll', 'page_view', 'cart_add', 'checkout_start'].forEach(type => {
530
+ features[idx++] = typeCounts.get(type) || 0;
531
+ });
532
+ return features;
533
+ }
534
+ /**
535
+ * Send prediction to webhook
536
+ */
537
+ async sendWebhook(prediction) {
538
+ if (!this.config?.webhook?.url) return;
539
+ const maxAttempts = this.config.webhook.retry?.maxAttempts || 3;
540
+ const backoffMs = this.config.webhook.retry?.backoffMs || 1000;
541
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
542
+ try {
543
+ const response = await fetch(this.config.webhook.url, {
544
+ method: 'POST',
545
+ headers: {
546
+ 'Content-Type': 'application/json',
547
+ 'X-App-Token': this.config.appToken
548
+ },
549
+ body: JSON.stringify({
550
+ ...prediction,
551
+ session_id: this.state.sessionId,
552
+ user_id: this.state.userId
553
+ })
554
+ });
555
+ if (response.ok) {
556
+ if (this.config.debug) {
557
+ console.log('[ATHENA] Webhook delivered successfully');
558
+ }
559
+ return;
107
560
  }
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();
561
+ throw new Error(`Webhook failed: ${response.status}`);
562
+ } catch (error) {
563
+ console.warn(`[ATHENA] Webhook attempt ${attempt}/${maxAttempts} failed:`, error);
564
+ if (attempt < maxAttempts) {
565
+ await this.sleep(backoffMs * Math.pow(2, attempt - 1));
117
566
  }
118
- return null;
567
+ }
568
+ }
569
+ console.error('[ATHENA] Webhook delivery failed after all retries');
570
+ }
571
+ /**
572
+ * Start event batching interval
573
+ */
574
+ startBatching() {
575
+ this.batchIntervalId = setInterval(() => {
576
+ this.processBatch();
577
+ }, this.config.batching.intervalMs);
578
+ }
579
+ /**
580
+ * Get inference mode
581
+ */
582
+ static getInferenceMode() {
583
+ return AthenaTracker.getInstance().state.inferenceMode;
584
+ }
585
+ /**
586
+ * Get session ID
587
+ */
588
+ static getSessionId() {
589
+ return AthenaTracker.getInstance().state.sessionId;
590
+ }
591
+ /**
592
+ * Cleanup resources
593
+ */
594
+ static async dispose() {
595
+ const instance = AthenaTracker.getInstance();
596
+ if (instance.batchIntervalId) {
597
+ clearInterval(instance.batchIntervalId);
598
+ instance.batchIntervalId = null;
119
599
  }
600
+ if (instance.onDeviceInference) {
601
+ await instance.onDeviceInference.dispose();
602
+ instance.onDeviceInference = null;
603
+ }
604
+ instance.serverInference = null;
605
+ instance.config = null;
606
+ instance.state = {
607
+ initialized: false,
608
+ inferenceMode: null,
609
+ sessionId: null,
610
+ userId: null,
611
+ events: []
612
+ };
613
+ console.log('[ATHENA] Tracker disposed');
614
+ }
615
+ /**
616
+ * Generate session ID
617
+ */
618
+ generateSessionId() {
619
+ return `sess_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
620
+ }
621
+ /**
622
+ * Sleep utility
623
+ */
624
+ sleep(ms) {
625
+ return new Promise(resolve => setTimeout(resolve, ms));
626
+ }
627
+ }
628
+ AthenaTracker.instance = null;
629
+
630
+ /**
631
+ * Webhook Utilities
632
+ *
633
+ * Production-ready webhook system with HMAC signature verification
634
+ * Per ATHENA Integration Plan Phase 4.1
635
+ */
636
+ // Cross-platform crypto support
637
+ let crypto;
638
+ try {
639
+ // Node.js environment
640
+ crypto = require('crypto');
641
+ } catch {
642
+ // Browser/React Native - will use Web Crypto API
643
+ crypto = null;
644
+ }
645
+ /**
646
+ * Generate HMAC-SHA256 signature for webhook payload (Node.js version)
647
+ *
648
+ * @param payload - Webhook payload object
649
+ * @param secret - Webhook secret key
650
+ * @returns HMAC signature (hex-encoded)
651
+ */
652
+ function generateWebhookSignatureNode(payload, secret) {
653
+ const payloadString = JSON.stringify(payload);
654
+ const hmac = crypto.createHmac('sha256', secret);
655
+ hmac.update(payloadString);
656
+ return hmac.digest('hex');
657
+ }
658
+ /**
659
+ * Generate HMAC-SHA256 signature for webhook payload (Web Crypto API version)
660
+ *
661
+ * @param payload - Webhook payload object
662
+ * @param secret - Webhook secret key
663
+ * @returns HMAC signature (hex-encoded)
664
+ */
665
+ async function generateWebhookSignatureBrowser(payload, secret) {
666
+ const payloadString = JSON.stringify(payload);
667
+ const encoder = new TextEncoder();
668
+ const keyData = encoder.encode(secret);
669
+ const messageData = encoder.encode(payloadString);
670
+ // Import key for HMAC
671
+ const key = await globalThis.crypto.subtle.importKey('raw', keyData, {
672
+ name: 'HMAC',
673
+ hash: 'SHA-256'
674
+ }, false, ['sign']);
675
+ // Generate signature
676
+ const signature = await globalThis.crypto.subtle.sign('HMAC', key, messageData);
677
+ // Convert to hex
678
+ return Array.from(new Uint8Array(signature)).map(b => b.toString(16).padStart(2, '0')).join('');
679
+ }
680
+ /**
681
+ * Generate HMAC-SHA256 signature for webhook payload (cross-platform)
682
+ *
683
+ * @param payload - Webhook payload object
684
+ * @param secret - Webhook secret key
685
+ * @returns HMAC signature (hex-encoded)
686
+ */
687
+ async function generateWebhookSignature(payload, secret) {
688
+ if (crypto) {
689
+ // Node.js environment
690
+ return generateWebhookSignatureNode(payload, secret);
691
+ } else if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
692
+ // Browser/Web Crypto API
693
+ return await generateWebhookSignatureBrowser(payload, secret);
694
+ } else {
695
+ // No crypto available - return empty signature
696
+ console.warn('[ATHENA] Crypto API not available - webhook signatures disabled');
697
+ return '';
698
+ }
120
699
  }
121
- // Singleton instance
122
- let trackerInstance = null;
123
700
  /**
124
- * Initialize ATHENA tracker
701
+ * Verify webhook signature (for use on receiving end)
702
+ *
703
+ * @param payload - Webhook payload object
704
+ * @param signature - Received signature (from X-ATHENA-Signature header)
705
+ * @param secret - Webhook secret key
706
+ * @returns True if signature is valid
125
707
  */
126
- function initTracker(config) {
127
- if (!trackerInstance) {
128
- trackerInstance = new AthenaTrackerSDK(config);
708
+ async function verifyWebhookSignature(payload, signature, secret) {
709
+ const expectedSignature = await generateWebhookSignature(payload, secret);
710
+ if (!expectedSignature) {
711
+ // Crypto not available - skip verification
712
+ return true;
713
+ }
714
+ // Use timing-safe comparison to prevent timing attacks (Node.js only)
715
+ if (crypto && crypto.timingSafeEqual) {
716
+ try {
717
+ return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
718
+ } catch (error) {
719
+ return false;
129
720
  }
130
- return trackerInstance;
721
+ }
722
+ // Browser fallback - simple comparison (not timing-safe)
723
+ return signature === expectedSignature;
131
724
  }
132
725
  /**
133
- * Get the tracker instance
726
+ * Create webhook payload for prediction update
134
727
  */
135
- function getTracker() {
136
- if (!trackerInstance) {
137
- console.warn('ATHENA Tracker: not initialized. Call initTracker() first.');
728
+ function createPredictionPayload(appId, userId, sessionId, prediction) {
729
+ return {
730
+ event: 'prediction_update',
731
+ appId,
732
+ timestamp: new Date().toISOString(),
733
+ data: {
734
+ user_id: userId,
735
+ session_id: sessionId,
736
+ ...prediction
138
737
  }
139
- return trackerInstance;
738
+ };
140
739
  }
141
740
  /**
142
- * Convenience methods (use singleton instance)
741
+ * Create webhook payload for segment update
143
742
  */
144
- function identify(userId, properties) {
145
- trackerInstance?.identify(userId, properties);
743
+ function createSegmentUpdatePayload(appId, segment, userCount, users) {
744
+ return {
745
+ event: 'segment_update',
746
+ appId,
747
+ timestamp: new Date().toISOString(),
748
+ data: {
749
+ segment,
750
+ user_count: userCount,
751
+ users
752
+ }
753
+ };
146
754
  }
147
- function track(eventName, properties) {
148
- trackerInstance?.track(eventName, properties);
755
+ /**
756
+ * Create webhook payload for friction detection
757
+ */
758
+ function createFrictionPayload(appId, frictionType, screen, selector, affectedUsers) {
759
+ return {
760
+ event: 'friction_detected',
761
+ appId,
762
+ timestamp: new Date().toISOString(),
763
+ data: {
764
+ type: frictionType,
765
+ screen,
766
+ selector,
767
+ affected_users: affectedUsers,
768
+ severity: affectedUsers > 100 ? 'high' : affectedUsers > 50 ? 'medium' : 'low'
769
+ }
770
+ };
771
+ }
772
+ /**
773
+ * Create webhook payload for conversion event
774
+ */
775
+ function createConversionPayload(appId, userId, sessionId, conversionType, value, metadata) {
776
+ return {
777
+ event: 'conversion',
778
+ appId,
779
+ timestamp: new Date().toISOString(),
780
+ data: {
781
+ user_id: userId,
782
+ session_id: sessionId,
783
+ conversion_type: conversionType,
784
+ value,
785
+ ...metadata
786
+ }
787
+ };
788
+ }
789
+ /**
790
+ * Webhook delivery with retry logic and exponential backoff
791
+ *
792
+ * @param url - Webhook URL
793
+ * @param payload - Webhook payload
794
+ * @param secret - Webhook secret for signature (optional)
795
+ * @param options - Delivery options
796
+ * @returns Delivery result
797
+ */
798
+ async function deliverWebhook(url, payload, secret, options = {}) {
799
+ const maxAttempts = options.maxAttempts || 3;
800
+ const backoffMs = options.backoffMs || 1000;
801
+ const timeoutMs = options.timeoutMs || 5000;
802
+ const signature = secret ? await generateWebhookSignature(payload, secret) : '';
803
+ const startTime = Date.now();
804
+ let lastError;
805
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
806
+ try {
807
+ const controller = new AbortController();
808
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
809
+ const response = await fetch(url, {
810
+ method: 'POST',
811
+ headers: {
812
+ 'Content-Type': 'application/json',
813
+ 'X-ATHENA-Signature': signature,
814
+ 'X-ATHENA-Event': payload.event,
815
+ 'X-ATHENA-Timestamp': payload.timestamp
816
+ },
817
+ body: JSON.stringify(payload),
818
+ signal: controller.signal
819
+ });
820
+ clearTimeout(timeoutId);
821
+ if (response.ok) {
822
+ const latencyMs = Date.now() - startTime;
823
+ return {
824
+ success: true,
825
+ attempts: attempt,
826
+ latencyMs
827
+ };
828
+ }
829
+ lastError = `HTTP ${response.status}: ${response.statusText}`;
830
+ } catch (error) {
831
+ lastError = error.name === 'AbortError' ? `Timeout after ${timeoutMs}ms` : error.message || 'Unknown error';
832
+ }
833
+ // Wait before retry (exponential backoff)
834
+ if (attempt < maxAttempts) {
835
+ const delayMs = backoffMs * Math.pow(2, attempt - 1);
836
+ await new Promise(resolve => setTimeout(resolve, delayMs));
837
+ }
838
+ }
839
+ const latencyMs = Date.now() - startTime;
840
+ return {
841
+ success: false,
842
+ attempts: maxAttempts,
843
+ latencyMs,
844
+ error: lastError
845
+ };
149
846
  }
150
- function page(pageName, properties) {
151
- trackerInstance?.page(pageName, properties);
847
+ /**
848
+ * Validate webhook payload structure
849
+ */
850
+ function isValidWebhookPayload(payload) {
851
+ if (typeof payload !== 'object' || payload === null) return false;
852
+ const validEvents = ['prediction_update', 'segment_update', 'friction_detected', 'conversion', 'session_end'];
853
+ return typeof payload.event === 'string' && validEvents.includes(payload.event) && typeof payload.appId === 'string' && typeof payload.timestamp === 'string' && typeof payload.data === 'object' && payload.data !== null;
152
854
  }
153
- function reset() {
154
- trackerInstance?.reset();
855
+
856
+ /**
857
+ * ATHENA OTA Wrapper
858
+ *
859
+ * Wraps the React Native app and forces a reload on first launch after OTA update
860
+ * This ensures the ATHENA tracker is loaded fresh after deployment
861
+ *
862
+ * Usage:
863
+ * import { AthenaOTAWrapper } from '@athena/tracker';
864
+ *
865
+ * export default function App() {
866
+ * return (
867
+ * <AthenaOTAWrapper>
868
+ * <YourAppContent />
869
+ * </AthenaOTAWrapper>
870
+ * );
871
+ * }
872
+ */
873
+ // Dynamic import of expo-updates (optional peer dependency)
874
+ let Updates = null;
875
+ async function loadExpoUpdates() {
876
+ try {
877
+ Updates = await import('expo-updates');
878
+ return true;
879
+ } catch (error) {
880
+ console.warn('[ATHENA] expo-updates not available - OTA updates disabled');
881
+ return false;
882
+ }
155
883
  }
156
- function getSessionId() {
157
- return trackerInstance?.getSessionId() || null;
884
+ function AthenaOTAWrapper({
885
+ children,
886
+ loadingMessage = 'Loading...',
887
+ updateMessage = 'Updating...'
888
+ }) {
889
+ const [isCheckingUpdate, setIsCheckingUpdate] = useState(true);
890
+ const [updateAvailable, setUpdateAvailable] = useState(false);
891
+ const [error, setError] = useState(null);
892
+ useEffect(() => {
893
+ checkForUpdates();
894
+ }, []);
895
+ async function checkForUpdates() {
896
+ try {
897
+ // Load expo-updates module
898
+ const hasUpdates = await loadExpoUpdates();
899
+ if (!hasUpdates) {
900
+ // expo-updates not available, continue without OTA check
901
+ setIsCheckingUpdate(false);
902
+ return;
903
+ }
904
+ // Check if running in development mode (skip update check)
905
+ if (__DEV__) {
906
+ console.log('[ATHENA] Development mode - skipping OTA update check');
907
+ setIsCheckingUpdate(false);
908
+ return;
909
+ }
910
+ console.log('[ATHENA] Checking for OTA updates...');
911
+ // Check for available updates
912
+ const update = await Updates.checkForUpdateAsync();
913
+ if (update.isAvailable) {
914
+ console.log('[ATHENA] Update available - fetching...');
915
+ setUpdateAvailable(true);
916
+ // Fetch the new update
917
+ await Updates.fetchUpdateAsync();
918
+ console.log('[ATHENA] Update fetched - reloading app...');
919
+ // Force reload to apply update
920
+ await Updates.reloadAsync();
921
+ // If reloadAsync fails, we'll never reach here
922
+ } else {
923
+ console.log('[ATHENA] No updates available - continuing with current version');
924
+ setIsCheckingUpdate(false);
925
+ }
926
+ } catch (error) {
927
+ // Don't block app if update check fails
928
+ console.warn('[ATHENA] Update check failed:', error.message);
929
+ setError(error.message);
930
+ setIsCheckingUpdate(false);
931
+ }
932
+ }
933
+ // Show loading screen while checking for updates
934
+ if (isCheckingUpdate || updateAvailable) {
935
+ return React.createElement(View, {
936
+ style: styles.container
937
+ }, React.createElement(ActivityIndicator, {
938
+ size: "large",
939
+ color: "#007AFF"
940
+ }), React.createElement(Text, {
941
+ style: styles.text
942
+ }, updateAvailable ? updateMessage : loadingMessage), error && React.createElement(Text, {
943
+ style: styles.errorText
944
+ }, "Update check failed: ", error));
945
+ }
946
+ // Render app content once update check is complete
947
+ return React.createElement(React.Fragment, null, children);
158
948
  }
159
- function getUserId() {
160
- return trackerInstance?.getUserId() || null;
949
+ const styles = StyleSheet.create({
950
+ container: {
951
+ flex: 1,
952
+ justifyContent: 'center',
953
+ alignItems: 'center',
954
+ backgroundColor: '#FFFFFF'
955
+ },
956
+ text: {
957
+ marginTop: 16,
958
+ color: '#666666',
959
+ fontSize: 14,
960
+ fontWeight: '500'
961
+ },
962
+ errorText: {
963
+ marginTop: 8,
964
+ color: '#FF3B30',
965
+ fontSize: 12,
966
+ textAlign: 'center',
967
+ paddingHorizontal: 32
968
+ }
969
+ });
970
+
971
+ /**
972
+ * React Native Event Capture
973
+ *
974
+ * Captures behavioral events from React Native apps
975
+ * - Touch events (Tap, Swipe, LongPress)
976
+ * - Navigation events (Screen changes)
977
+ * - App lifecycle events (Open, Background, Foreground, Close)
978
+ * - Form interactions
979
+ * - Network errors
980
+ */
981
+ class ReactNativeEventCapture {
982
+ constructor(config = {}) {
983
+ this.events = [];
984
+ this.appStateSubscription = null;
985
+ this.panResponder = null;
986
+ this.batchTimer = null;
987
+ this.currentScreen = 'Unknown';
988
+ this.sessionStartTime = Date.now();
989
+ this.config = {
990
+ captureTouch: config.captureTouch !== false,
991
+ captureNavigation: config.captureNavigation !== false,
992
+ captureLifecycle: config.captureLifecycle !== false,
993
+ captureNetworkErrors: config.captureNetworkErrors !== false,
994
+ batchSize: config.batchSize || 10,
995
+ batchIntervalMs: config.batchIntervalMs || 10000
996
+ };
997
+ }
998
+ /**
999
+ * Start capturing events
1000
+ */
1001
+ start() {
1002
+ console.log('[ATHENA] Starting React Native event capture');
1003
+ if (this.config.captureLifecycle) {
1004
+ this.setupLifecycleTracking();
1005
+ }
1006
+ if (this.config.captureTouch) {
1007
+ this.setupTouchTracking();
1008
+ }
1009
+ if (this.config.captureNetworkErrors) {
1010
+ this.setupNetworkErrorTracking();
1011
+ }
1012
+ // Start batch timer
1013
+ this.batchTimer = setInterval(() => {
1014
+ this.flushEvents();
1015
+ }, this.config.batchIntervalMs);
1016
+ // Capture initial AppOpen event
1017
+ this.captureEvent({
1018
+ event_type: 'AppOpen',
1019
+ timestamp: Date.now(),
1020
+ properties: {
1021
+ screen: this.currentScreen,
1022
+ session_start: true
1023
+ }
1024
+ });
1025
+ }
1026
+ /**
1027
+ * Stop capturing events
1028
+ */
1029
+ stop() {
1030
+ console.log('[ATHENA] Stopping React Native event capture');
1031
+ if (this.appStateSubscription) {
1032
+ this.appStateSubscription.remove();
1033
+ }
1034
+ if (this.batchTimer) {
1035
+ clearInterval(this.batchTimer);
1036
+ }
1037
+ // Flush any remaining events
1038
+ this.flushEvents();
1039
+ }
1040
+ /**
1041
+ * Setup app lifecycle tracking (Open, Background, Foreground, Close)
1042
+ */
1043
+ setupLifecycleTracking() {
1044
+ this.appStateSubscription = AppState.addEventListener('change', nextAppState => {
1045
+ if (nextAppState === 'active') {
1046
+ this.captureEvent({
1047
+ event_type: 'AppForeground',
1048
+ timestamp: Date.now(),
1049
+ properties: {
1050
+ screen: this.currentScreen
1051
+ }
1052
+ });
1053
+ } else if (nextAppState === 'background') {
1054
+ this.captureEvent({
1055
+ event_type: 'AppBackground',
1056
+ timestamp: Date.now(),
1057
+ properties: {
1058
+ screen: this.currentScreen,
1059
+ session_duration: Date.now() - this.sessionStartTime
1060
+ }
1061
+ });
1062
+ } else if (nextAppState === 'inactive') {
1063
+ this.captureEvent({
1064
+ event_type: 'AppInactive',
1065
+ timestamp: Date.now(),
1066
+ properties: {
1067
+ screen: this.currentScreen
1068
+ }
1069
+ });
1070
+ }
1071
+ });
1072
+ }
1073
+ /**
1074
+ * Setup touch event tracking
1075
+ */
1076
+ setupTouchTracking() {
1077
+ let touchStartTime = 0;
1078
+ this.panResponder = PanResponder.create({
1079
+ onStartShouldSetPanResponder: () => true,
1080
+ onMoveShouldSetPanResponder: () => true,
1081
+ onPanResponderGrant: (_evt, _gestureState) => {
1082
+ touchStartTime = Date.now();
1083
+ },
1084
+ onPanResponderRelease: (evt, gestureState) => {
1085
+ const touchDuration = Date.now() - touchStartTime;
1086
+ const deltaX = Math.abs(gestureState.dx);
1087
+ const deltaY = Math.abs(gestureState.dy);
1088
+ // Detect gesture type
1089
+ if (touchDuration > 500 && deltaX < 10 && deltaY < 10) {
1090
+ // Long press
1091
+ this.captureEvent({
1092
+ event_type: 'LongPress',
1093
+ timestamp: Date.now(),
1094
+ properties: {
1095
+ screen: this.currentScreen,
1096
+ x: evt.nativeEvent.pageX,
1097
+ y: evt.nativeEvent.pageY,
1098
+ duration: touchDuration
1099
+ }
1100
+ });
1101
+ } else if (deltaX > 50 || deltaY > 50) {
1102
+ // Swipe
1103
+ const direction = deltaX > deltaY ? gestureState.dx > 0 ? 'right' : 'left' : gestureState.dy > 0 ? 'down' : 'up';
1104
+ this.captureEvent({
1105
+ event_type: 'Swipe',
1106
+ timestamp: Date.now(),
1107
+ properties: {
1108
+ screen: this.currentScreen,
1109
+ direction,
1110
+ distance: Math.sqrt(deltaX * deltaX + deltaY * deltaY)
1111
+ }
1112
+ });
1113
+ } else {
1114
+ // Tap
1115
+ this.captureEvent({
1116
+ event_type: 'Tap',
1117
+ timestamp: Date.now(),
1118
+ properties: {
1119
+ screen: this.currentScreen,
1120
+ x: evt.nativeEvent.pageX,
1121
+ y: evt.nativeEvent.pageY,
1122
+ duration: touchDuration
1123
+ }
1124
+ });
1125
+ }
1126
+ }
1127
+ });
1128
+ }
1129
+ /**
1130
+ * Setup network error tracking
1131
+ */
1132
+ setupNetworkErrorTracking() {
1133
+ // Monkey-patch fetch to track network errors
1134
+ const originalFetch = global.fetch;
1135
+ global.fetch = async (input, init) => {
1136
+ const startTime = Date.now();
1137
+ const url = typeof input === 'string' ? input : input.url || String(input);
1138
+ try {
1139
+ const response = await originalFetch(input, init);
1140
+ if (!response.ok) {
1141
+ this.captureEvent({
1142
+ event_type: 'NetworkError',
1143
+ timestamp: Date.now(),
1144
+ properties: {
1145
+ screen: this.currentScreen,
1146
+ url,
1147
+ status: response.status,
1148
+ statusText: response.statusText,
1149
+ duration: Date.now() - startTime
1150
+ }
1151
+ });
1152
+ }
1153
+ return response;
1154
+ } catch (error) {
1155
+ this.captureEvent({
1156
+ event_type: 'NetworkError',
1157
+ timestamp: Date.now(),
1158
+ properties: {
1159
+ screen: this.currentScreen,
1160
+ url,
1161
+ error: error.message,
1162
+ duration: Date.now() - startTime
1163
+ }
1164
+ });
1165
+ throw error;
1166
+ }
1167
+ };
1168
+ }
1169
+ /**
1170
+ * Manually track screen navigation
1171
+ * Should be called by navigation library (React Navigation, etc.)
1172
+ */
1173
+ trackScreenView(screenName, params) {
1174
+ this.currentScreen = screenName;
1175
+ this.captureEvent({
1176
+ event_type: 'ScreenView',
1177
+ timestamp: Date.now(),
1178
+ properties: {
1179
+ screen: screenName,
1180
+ params: params || {},
1181
+ previous_screen: this.currentScreen
1182
+ }
1183
+ });
1184
+ }
1185
+ /**
1186
+ * Manually track custom event
1187
+ */
1188
+ track(eventType, properties) {
1189
+ this.captureEvent({
1190
+ event_type: eventType,
1191
+ timestamp: Date.now(),
1192
+ properties: {
1193
+ ...properties,
1194
+ screen: this.currentScreen
1195
+ }
1196
+ });
1197
+ }
1198
+ /**
1199
+ * Capture an event and add to batch
1200
+ */
1201
+ captureEvent(event) {
1202
+ this.events.push(event);
1203
+ // Flush immediately if batch size reached
1204
+ if (this.events.length >= this.config.batchSize) {
1205
+ this.flushEvents();
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Flush accumulated events (to be sent to server)
1210
+ */
1211
+ flushEvents() {
1212
+ if (this.events.length === 0) return;
1213
+ const batch = [...this.events];
1214
+ this.events = [];
1215
+ // Emit events for tracker to send
1216
+ this.onEventBatch?.(batch);
1217
+ }
1218
+ /**
1219
+ * Get PanResponder for manual integration
1220
+ * Usage: <View {...capture.getPanResponderProps()}>
1221
+ */
1222
+ getPanResponderProps() {
1223
+ return this.panResponder?.panHandlers || {};
1224
+ }
161
1225
  }
162
- // Default export
163
- var index = {
164
- initTracker,
165
- getTracker,
166
- identify,
167
- track,
168
- page,
169
- reset,
170
- getSessionId,
171
- getUserId,
172
- };
173
1226
 
174
- export { index as default, getSessionId, getTracker, getUserId, identify, initTracker, page, reset, track };
1227
+ /**
1228
+ * @athena/tracker
1229
+ *
1230
+ * Behavioral analytics tracker with edge AI
1231
+ */
1232
+ // Version
1233
+ const VERSION = '1.0.0';
1234
+
1235
+ export { AthenaOTAWrapper, AthenaTracker, ReactNativeEventCapture, VERSION, createConversionPayload, createFrictionPayload, createPredictionPayload, createSegmentUpdatePayload, AthenaTracker as default, deliverWebhook, detectInferenceMode, generateWebhookSignature, getPlatform, isBrowser, isReactNative, isValidWebhookPayload, verifyWebhookSignature };
175
1236
  //# sourceMappingURL=index.esm.js.map