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