@athena-tracker/tracker 1.2.0 → 1.3.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 +374 -33
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +374 -33
- package/dist/index.js.map +1 -1
- package/dist/inference/auto-detect.d.ts +1 -1
- package/dist/inference/auto-detect.d.ts.map +1 -1
- package/dist/inference/on-device-web.d.ts +36 -0
- package/dist/inference/on-device-web.d.ts.map +1 -0
- package/dist/inference/server.d.ts.map +1 -1
- package/dist/tracker.d.ts +27 -1
- package/dist/tracker.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -1
package/dist/index.esm.js
CHANGED
|
@@ -5,7 +5,7 @@ import { StyleSheet, View, ActivityIndicator, Text, AppState, PanResponder } fro
|
|
|
5
5
|
* Auto-Detection Logic
|
|
6
6
|
*
|
|
7
7
|
* Automatically detects whether to use on-device or server-side inference
|
|
8
|
-
* based on availability of onnxruntime-react-native
|
|
8
|
+
* based on availability of onnxruntime-web (browser) or onnxruntime-react-native (RN)
|
|
9
9
|
*/
|
|
10
10
|
async function detectInferenceMode(configMode) {
|
|
11
11
|
// Respect explicit configuration
|
|
@@ -17,14 +17,31 @@ async function detectInferenceMode(configMode) {
|
|
|
17
17
|
console.log('[ATHENA] Forcing server-side inference mode');
|
|
18
18
|
return 'server';
|
|
19
19
|
}
|
|
20
|
-
// Auto-detect based on
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
// Auto-detect based on platform and ONNX runtime availability
|
|
21
|
+
const platform = getPlatform();
|
|
22
|
+
if (platform === 'web') {
|
|
23
|
+
// Try to load onnxruntime-web for browsers
|
|
24
|
+
try {
|
|
25
|
+
await import('onnxruntime-web');
|
|
26
|
+
console.log('[ATHENA] On-device inference available (onnxruntime-web detected)');
|
|
27
|
+
return 'on-device';
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.log('[ATHENA] Falling back to server-side inference (onnxruntime-web not found)');
|
|
30
|
+
return 'server';
|
|
31
|
+
}
|
|
32
|
+
} else if (platform === 'react-native') {
|
|
33
|
+
// Try to load onnxruntime-react-native for React Native
|
|
34
|
+
try {
|
|
35
|
+
await import('onnxruntime-react-native');
|
|
36
|
+
console.log('[ATHENA] On-device inference available (onnxruntime-react-native detected)');
|
|
37
|
+
return 'on-device';
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.log('[ATHENA] Falling back to server-side inference (onnxruntime-react-native not found)');
|
|
40
|
+
return 'server';
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// Unknown platform - use server-side inference
|
|
44
|
+
console.log('[ATHENA] Falling back to server-side inference (unknown platform)');
|
|
28
45
|
return 'server';
|
|
29
46
|
}
|
|
30
47
|
}
|
|
@@ -58,14 +75,14 @@ function getPlatform() {
|
|
|
58
75
|
* Uses onnxruntime-react-native for local ML inference
|
|
59
76
|
* Target latency: <10ms P95
|
|
60
77
|
*/
|
|
61
|
-
let InferenceSession;
|
|
62
|
-
let Tensor;
|
|
78
|
+
let InferenceSession$1;
|
|
79
|
+
let Tensor$1;
|
|
63
80
|
// Dynamically import ONNX Runtime (only available in React Native)
|
|
64
81
|
async function loadOnnxRuntime() {
|
|
65
82
|
try {
|
|
66
83
|
const onnx = await import('onnxruntime-react-native');
|
|
67
|
-
InferenceSession = onnx.InferenceSession;
|
|
68
|
-
Tensor = onnx.Tensor;
|
|
84
|
+
InferenceSession$1 = onnx.InferenceSession;
|
|
85
|
+
Tensor$1 = onnx.Tensor;
|
|
69
86
|
return true;
|
|
70
87
|
} catch (error) {
|
|
71
88
|
console.error('[ATHENA] Failed to load onnxruntime-react-native:', error);
|
|
@@ -89,7 +106,7 @@ class OnDeviceInference {
|
|
|
89
106
|
}
|
|
90
107
|
try {
|
|
91
108
|
const startTime = performance.now();
|
|
92
|
-
this.session = await InferenceSession.create(modelPath);
|
|
109
|
+
this.session = await InferenceSession$1.create(modelPath);
|
|
93
110
|
const loadTime = performance.now() - startTime;
|
|
94
111
|
this.modelLoaded = true;
|
|
95
112
|
console.log(`[ATHENA] ONNX model loaded successfully (${loadTime.toFixed(0)}ms)`);
|
|
@@ -108,7 +125,7 @@ class OnDeviceInference {
|
|
|
108
125
|
const startTime = performance.now();
|
|
109
126
|
try {
|
|
110
127
|
// Create input tensor
|
|
111
|
-
const inputTensor = new Tensor('float32', features, [1, features.length]);
|
|
128
|
+
const inputTensor = new Tensor$1('float32', features, [1, features.length]);
|
|
112
129
|
const feeds = {
|
|
113
130
|
input: inputTensor
|
|
114
131
|
};
|
|
@@ -234,6 +251,192 @@ class OnDeviceInference {
|
|
|
234
251
|
}
|
|
235
252
|
}
|
|
236
253
|
|
|
254
|
+
/**
|
|
255
|
+
* On-Device Inference Module (Web Browser)
|
|
256
|
+
*
|
|
257
|
+
* Uses onnxruntime-web for browser-based ML inference
|
|
258
|
+
* Target latency: <3ms P95
|
|
259
|
+
*/
|
|
260
|
+
let InferenceSession;
|
|
261
|
+
let Tensor;
|
|
262
|
+
// Dynamically import ONNX Runtime Web (only available in browsers)
|
|
263
|
+
async function loadOnnxRuntimeWeb() {
|
|
264
|
+
try {
|
|
265
|
+
const onnx = await import('onnxruntime-web');
|
|
266
|
+
InferenceSession = onnx.InferenceSession;
|
|
267
|
+
Tensor = onnx.Tensor;
|
|
268
|
+
return true;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error('[ATHENA] Failed to load onnxruntime-web:', error);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
class OnDeviceInferenceWeb {
|
|
275
|
+
constructor() {
|
|
276
|
+
this.session = null;
|
|
277
|
+
this.modelLoaded = false;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Initialize ONNX session with model file
|
|
281
|
+
*/
|
|
282
|
+
async initialize(modelPath) {
|
|
283
|
+
console.log('[ATHENA] Loading ONNX model for browser inference...');
|
|
284
|
+
// Load ONNX Runtime Web
|
|
285
|
+
const loaded = await loadOnnxRuntimeWeb();
|
|
286
|
+
if (!loaded) {
|
|
287
|
+
throw new Error('onnxruntime-web not available');
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const startTime = performance.now();
|
|
291
|
+
this.session = await InferenceSession.create(modelPath, {
|
|
292
|
+
executionProviders: ['wasm'],
|
|
293
|
+
// Use WebAssembly backend
|
|
294
|
+
graphOptimizationLevel: 'all'
|
|
295
|
+
});
|
|
296
|
+
const loadTime = performance.now() - startTime;
|
|
297
|
+
this.modelLoaded = true;
|
|
298
|
+
console.log(`[ATHENA] ONNX model loaded successfully in browser (${loadTime.toFixed(0)}ms)`);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('[ATHENA] Failed to load ONNX model:', error);
|
|
301
|
+
throw new Error(`Model loading failed: ${error}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Run inference on feature vector
|
|
306
|
+
*/
|
|
307
|
+
async predict(features) {
|
|
308
|
+
if (!this.modelLoaded || !this.session) {
|
|
309
|
+
throw new Error('ONNX model not initialized. Call initialize() first.');
|
|
310
|
+
}
|
|
311
|
+
const startTime = performance.now();
|
|
312
|
+
try {
|
|
313
|
+
// Create input tensor
|
|
314
|
+
const inputTensor = new Tensor('float32', features, [1, features.length]);
|
|
315
|
+
const feeds = {
|
|
316
|
+
input: inputTensor
|
|
317
|
+
};
|
|
318
|
+
// Run inference
|
|
319
|
+
const results = await this.session.run(feeds);
|
|
320
|
+
const inferenceTime = performance.now() - startTime;
|
|
321
|
+
// Parse output
|
|
322
|
+
const prediction = this.parseOutput(results.output);
|
|
323
|
+
console.log(`[ATHENA] Browser inference complete: ${inferenceTime.toFixed(2)}ms ` + `(class: ${prediction.predicted_class}, confidence: ${prediction.confidence.toFixed(2)})`);
|
|
324
|
+
return {
|
|
325
|
+
...prediction,
|
|
326
|
+
inference_time_ms: Math.round(inferenceTime * 100) / 100,
|
|
327
|
+
inference_location: 'on-device',
|
|
328
|
+
timestamp: new Date().toISOString()
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('[ATHENA] Browser inference failed:', error);
|
|
332
|
+
throw new Error(`Inference failed: ${error}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Parse ONNX output tensor to prediction result
|
|
337
|
+
*/
|
|
338
|
+
parseOutput(outputTensor) {
|
|
339
|
+
const data = outputTensor.data;
|
|
340
|
+
// Extract predictions from model output
|
|
341
|
+
// Model outputs: [predicted_class_idx, confidence, purchase_intent, cart_abandon_risk, checkout_abandon_risk]
|
|
342
|
+
const predictedClassIdx = data[0];
|
|
343
|
+
const confidence = data[1];
|
|
344
|
+
const purchaseIntent = data[2];
|
|
345
|
+
const cartAbandonRisk = data[3] || 0;
|
|
346
|
+
const checkoutAbandonRisk = data[4] || 0;
|
|
347
|
+
// Map class index to label
|
|
348
|
+
const classLabels = ['engaged_explorer', 'high_intent_buyer', 'cart_abandoner', 'checkout_abandoner', 'bounce_risk'];
|
|
349
|
+
const predictedClass = classLabels[predictedClassIdx] || 'unknown';
|
|
350
|
+
// Determine archetype based on purchase intent
|
|
351
|
+
let archetype;
|
|
352
|
+
if (purchaseIntent >= 0.85) {
|
|
353
|
+
archetype = 'fast_mover';
|
|
354
|
+
} else if (purchaseIntent >= 0.60) {
|
|
355
|
+
archetype = 'on_track';
|
|
356
|
+
} else if (purchaseIntent >= 0.40) {
|
|
357
|
+
archetype = 'slow_adopter';
|
|
358
|
+
} else if (purchaseIntent >= 0.20) {
|
|
359
|
+
archetype = 'at_risk';
|
|
360
|
+
} else {
|
|
361
|
+
archetype = 'different_path';
|
|
362
|
+
}
|
|
363
|
+
// Generate recommendations
|
|
364
|
+
const recommendation = this.generateRecommendation(predictedClass, purchaseIntent, cartAbandonRisk, checkoutAbandonRisk);
|
|
365
|
+
return {
|
|
366
|
+
predicted_class: predictedClass,
|
|
367
|
+
confidence,
|
|
368
|
+
archetype,
|
|
369
|
+
purchase_intent: purchaseIntent,
|
|
370
|
+
cart_abandonment_risk: cartAbandonRisk,
|
|
371
|
+
checkout_abandonment_risk: checkoutAbandonRisk,
|
|
372
|
+
...recommendation
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Generate action recommendations based on prediction
|
|
377
|
+
*/
|
|
378
|
+
generateRecommendation(predictedClass, purchaseIntent, cartAbandonRisk, checkoutAbandonRisk) {
|
|
379
|
+
// High cart abandonment risk
|
|
380
|
+
if (cartAbandonRisk > 0.7) {
|
|
381
|
+
return {
|
|
382
|
+
recommended_action: 'Show cart abandonment discount (10-15% off)',
|
|
383
|
+
urgency: 'high',
|
|
384
|
+
trigger_reason: `High cart abandonment risk (${(cartAbandonRisk * 100).toFixed(0)}%)`
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
// High checkout abandonment risk
|
|
388
|
+
if (checkoutAbandonRisk > 0.7) {
|
|
389
|
+
return {
|
|
390
|
+
recommended_action: 'Simplify checkout flow or offer free shipping',
|
|
391
|
+
urgency: 'critical',
|
|
392
|
+
trigger_reason: `High checkout abandonment risk (${(checkoutAbandonRisk * 100).toFixed(0)}%)`
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
// High purchase intent
|
|
396
|
+
if (purchaseIntent > 0.8) {
|
|
397
|
+
return {
|
|
398
|
+
recommended_action: 'Show product recommendations or upsell',
|
|
399
|
+
urgency: 'medium',
|
|
400
|
+
trigger_reason: `High purchase intent (${(purchaseIntent * 100).toFixed(0)}%)`
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
// Low purchase intent
|
|
404
|
+
if (purchaseIntent < 0.3) {
|
|
405
|
+
return {
|
|
406
|
+
recommended_action: 'Show value proposition or testimonials',
|
|
407
|
+
urgency: 'low',
|
|
408
|
+
trigger_reason: `Low purchase intent (${(purchaseIntent * 100).toFixed(0)}%)`
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
recommended_action: 'Continue monitoring',
|
|
413
|
+
urgency: 'low'
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Check if model is loaded
|
|
418
|
+
*/
|
|
419
|
+
isReady() {
|
|
420
|
+
return this.modelLoaded;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Cleanup resources
|
|
424
|
+
*/
|
|
425
|
+
async dispose() {
|
|
426
|
+
if (this.session) {
|
|
427
|
+
try {
|
|
428
|
+
// ONNX Runtime sessions should be disposed
|
|
429
|
+
await this.session.release?.();
|
|
430
|
+
console.log('[ATHENA] Browser inference session disposed');
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.warn('[ATHENA] Failed to dispose session:', error);
|
|
433
|
+
}
|
|
434
|
+
this.session = null;
|
|
435
|
+
this.modelLoaded = false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
237
440
|
/**
|
|
238
441
|
* Server-Side Inference Module
|
|
239
442
|
*
|
|
@@ -314,12 +517,17 @@ class ServerInference {
|
|
|
314
517
|
try {
|
|
315
518
|
// Replace /v1/predict with /health for health check
|
|
316
519
|
const healthUrl = this.apiUrl.replace('/v1/predict', '/health');
|
|
520
|
+
console.log(`[ATHENA] Testing server connectivity: ${healthUrl}`);
|
|
317
521
|
const response = await this.fetchWithTimeout(healthUrl, {
|
|
318
522
|
method: 'GET'
|
|
319
|
-
},
|
|
523
|
+
}, Math.min(this.timeout, 5000)); // Use 5s max for health check
|
|
524
|
+
if (response.ok) {
|
|
525
|
+
console.log('[ATHENA] Server connectivity test passed');
|
|
526
|
+
}
|
|
320
527
|
return response.ok;
|
|
321
528
|
} catch (error) {
|
|
322
|
-
console.warn('[ATHENA] Server connectivity test failed:', error);
|
|
529
|
+
console.warn('[ATHENA] Server connectivity test failed:', error.message || error);
|
|
530
|
+
console.warn('[ATHENA] URL attempted:', this.apiUrl.replace('/v1/predict', '/health'));
|
|
323
531
|
return false;
|
|
324
532
|
}
|
|
325
533
|
}
|
|
@@ -356,6 +564,10 @@ class AthenaTracker {
|
|
|
356
564
|
this.onDeviceInference = null;
|
|
357
565
|
this.serverInference = null;
|
|
358
566
|
this.batchIntervalId = null;
|
|
567
|
+
this.failureCount = 0;
|
|
568
|
+
this.maxFailures = 3;
|
|
569
|
+
// External IDs for custom audience building (Meta, Google)
|
|
570
|
+
this.externalIds = {};
|
|
359
571
|
// Private constructor for singleton
|
|
360
572
|
}
|
|
361
573
|
/**
|
|
@@ -398,6 +610,10 @@ class AthenaTracker {
|
|
|
398
610
|
}
|
|
399
611
|
// Generate session ID
|
|
400
612
|
instance.state.sessionId = instance.generateSessionId();
|
|
613
|
+
// Generate anonymous user ID (will be replaced on identify())
|
|
614
|
+
instance.state.userId = instance.generateAnonymousId();
|
|
615
|
+
// Capture external IDs for custom audience building (browser only)
|
|
616
|
+
instance.captureExternalIds();
|
|
401
617
|
// Start event batching
|
|
402
618
|
instance.startBatching();
|
|
403
619
|
instance.state.initialized = true;
|
|
@@ -408,7 +624,15 @@ class AthenaTracker {
|
|
|
408
624
|
*/
|
|
409
625
|
async initializeOnDevice() {
|
|
410
626
|
try {
|
|
411
|
-
|
|
627
|
+
const platform = getPlatform();
|
|
628
|
+
// Use platform-specific ONNX runtime
|
|
629
|
+
if (platform === 'web') {
|
|
630
|
+
this.onDeviceInference = new OnDeviceInferenceWeb();
|
|
631
|
+
} else if (platform === 'react-native') {
|
|
632
|
+
this.onDeviceInference = new OnDeviceInference();
|
|
633
|
+
} else {
|
|
634
|
+
throw new Error('Unsupported platform for on-device inference');
|
|
635
|
+
}
|
|
412
636
|
await this.onDeviceInference.initialize(this.config.modelPath);
|
|
413
637
|
console.log('[ATHENA] On-device inference ready');
|
|
414
638
|
} catch (error) {
|
|
@@ -422,7 +646,8 @@ class AthenaTracker {
|
|
|
422
646
|
* Initialize server-side inference
|
|
423
647
|
*/
|
|
424
648
|
async initializeServer() {
|
|
425
|
-
this.serverInference = new ServerInference(this.config.serverInferenceUrl, this.config.appToken
|
|
649
|
+
this.serverInference = new ServerInference(this.config.serverInferenceUrl, this.config.appToken, this.config.timeout || 5000 // Use configured timeout or default to 5s
|
|
650
|
+
);
|
|
426
651
|
// Test connectivity
|
|
427
652
|
const connected = await this.serverInference.testConnection();
|
|
428
653
|
if (!connected) {
|
|
@@ -433,15 +658,37 @@ class AthenaTracker {
|
|
|
433
658
|
}
|
|
434
659
|
/**
|
|
435
660
|
* Identify a user
|
|
661
|
+
* Supports email/phone hashing for Meta/Google custom audiences
|
|
662
|
+
*
|
|
663
|
+
* @param userId - User identifier
|
|
664
|
+
* @param traits - User traits (email, phone, name, etc.)
|
|
665
|
+
* @param options - Optional configuration
|
|
436
666
|
*/
|
|
437
|
-
static identify(userId, traits) {
|
|
667
|
+
static async identify(userId, traits, options) {
|
|
438
668
|
const instance = AthenaTracker.getInstance();
|
|
669
|
+
const previousUserId = instance.state.userId; // Store anonymous ID for mapping
|
|
439
670
|
instance.state.userId = userId;
|
|
671
|
+
// Hash PII for custom audience building if requested
|
|
672
|
+
const hashedTraits = {
|
|
673
|
+
...traits
|
|
674
|
+
};
|
|
675
|
+
if (options?.hashPII && traits) {
|
|
676
|
+
if (traits.email) {
|
|
677
|
+
hashedTraits.email_sha256 = await instance.hashPII(traits.email);
|
|
678
|
+
}
|
|
679
|
+
if (traits.phone) {
|
|
680
|
+
hashedTraits.phone_sha256 = await instance.hashPII(traits.phone);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
440
683
|
AthenaTracker.track('identify', {
|
|
441
684
|
user_id: userId,
|
|
442
|
-
|
|
685
|
+
anonymous_id: previousUserId,
|
|
686
|
+
// Map anonymous → identified user
|
|
687
|
+
...hashedTraits,
|
|
688
|
+
// Include external IDs for audience sync
|
|
689
|
+
external_ids: Object.keys(instance.externalIds).length > 0 ? instance.externalIds : undefined
|
|
443
690
|
});
|
|
444
|
-
console.log(`[ATHENA] User identified: ${userId}`);
|
|
691
|
+
console.log(`[ATHENA] User identified: ${userId}` + (previousUserId?.startsWith('anon_') ? ` (was ${previousUserId})` : ''));
|
|
445
692
|
}
|
|
446
693
|
/**
|
|
447
694
|
* Track an event
|
|
@@ -473,10 +720,21 @@ class AthenaTracker {
|
|
|
473
720
|
*/
|
|
474
721
|
async processBatch() {
|
|
475
722
|
if (this.state.events.length === 0) return;
|
|
723
|
+
// Skip inference if too many consecutive failures
|
|
724
|
+
if (this.failureCount >= this.maxFailures) {
|
|
725
|
+
if (this.config?.debug) {
|
|
726
|
+
console.warn(`[ATHENA] Skipping inference after ${this.failureCount} consecutive failures. Will retry after ${this.maxFailures} successful events.`);
|
|
727
|
+
}
|
|
728
|
+
// Reset failure count after clearing queue
|
|
729
|
+
this.failureCount = Math.max(0, this.failureCount - 1);
|
|
730
|
+
this.state.events = []; // Drop events to prevent infinite buildup
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
476
733
|
const events = [...this.state.events];
|
|
477
734
|
this.state.events = [];
|
|
478
735
|
try {
|
|
479
736
|
const prediction = await this.runInference(events);
|
|
737
|
+
this.failureCount = 0; // Reset on success
|
|
480
738
|
if (this.config?.webhook?.enabled && this.config.webhook.url) {
|
|
481
739
|
await this.sendWebhook(prediction);
|
|
482
740
|
}
|
|
@@ -484,9 +742,14 @@ class AthenaTracker {
|
|
|
484
742
|
console.log('[ATHENA] Prediction:', prediction);
|
|
485
743
|
}
|
|
486
744
|
} catch (error) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
745
|
+
this.failureCount++;
|
|
746
|
+
console.error(`[ATHENA] Failed to process batch (${this.failureCount}/${this.maxFailures}):`, error.message || error);
|
|
747
|
+
// Only re-queue if below failure threshold
|
|
748
|
+
if (this.failureCount < this.maxFailures) {
|
|
749
|
+
this.state.events.unshift(...events);
|
|
750
|
+
} else {
|
|
751
|
+
console.warn('[ATHENA] Max failures reached. Dropping events and disabling inference temporarily.');
|
|
752
|
+
}
|
|
490
753
|
}
|
|
491
754
|
}
|
|
492
755
|
/**
|
|
@@ -549,7 +812,9 @@ class AthenaTracker {
|
|
|
549
812
|
body: JSON.stringify({
|
|
550
813
|
...prediction,
|
|
551
814
|
session_id: this.state.sessionId,
|
|
552
|
-
user_id: this.state.userId
|
|
815
|
+
user_id: this.state.userId,
|
|
816
|
+
// Include external IDs for custom audience building
|
|
817
|
+
external_ids: Object.keys(this.externalIds).length > 0 ? this.externalIds : undefined
|
|
553
818
|
})
|
|
554
819
|
});
|
|
555
820
|
if (response.ok) {
|
|
@@ -618,6 +883,82 @@ class AthenaTracker {
|
|
|
618
883
|
generateSessionId() {
|
|
619
884
|
return `sess_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
620
885
|
}
|
|
886
|
+
/**
|
|
887
|
+
* Generate anonymous user ID compatible with Meta/Google standards
|
|
888
|
+
* Format: anon_{timestamp}_{random} for Pascal internal use
|
|
889
|
+
* Can be mapped to hashed PII for custom audience building
|
|
890
|
+
*/
|
|
891
|
+
generateAnonymousId() {
|
|
892
|
+
const timestamp = Date.now();
|
|
893
|
+
const random = Math.random().toString(36).substring(2, 11);
|
|
894
|
+
return `anon_${timestamp}_${random}`;
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Capture external IDs from Meta and Google for custom audience building
|
|
898
|
+
* Only works in browser environment with cookies
|
|
899
|
+
*/
|
|
900
|
+
captureExternalIds() {
|
|
901
|
+
// Skip if not in browser environment
|
|
902
|
+
if (typeof document === 'undefined' || typeof document.cookie === 'undefined') {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
const cookies = document.cookie.split(';').reduce((acc, cookie) => {
|
|
907
|
+
const [key, value] = cookie.trim().split('=');
|
|
908
|
+
acc[key] = value;
|
|
909
|
+
return acc;
|
|
910
|
+
}, {});
|
|
911
|
+
// Meta Pixel ID (_fbp) - Format: fb.{version}.{timestamp}.{random}
|
|
912
|
+
if (cookies._fbp) {
|
|
913
|
+
this.externalIds.fbp = cookies._fbp;
|
|
914
|
+
}
|
|
915
|
+
// Meta Click ID (_fbc) - Format: fb.{version}.{timestamp}.{click_id}
|
|
916
|
+
if (cookies._fbc) {
|
|
917
|
+
this.externalIds.fbc = cookies._fbc;
|
|
918
|
+
}
|
|
919
|
+
// Google Analytics Client ID (_ga) - Format: GA1.{version}.{random}.{timestamp}
|
|
920
|
+
if (cookies._ga) {
|
|
921
|
+
this.externalIds.ga = cookies._ga;
|
|
922
|
+
}
|
|
923
|
+
// Google Click ID (gcl_aw) - For Google Ads conversions
|
|
924
|
+
if (cookies.gcl_aw) {
|
|
925
|
+
this.externalIds.gclid = cookies.gcl_aw;
|
|
926
|
+
}
|
|
927
|
+
if (this.config?.debug && Object.keys(this.externalIds).length > 0) {
|
|
928
|
+
console.log('[ATHENA] External IDs captured for audience building:', this.externalIds);
|
|
929
|
+
}
|
|
930
|
+
} catch (error) {
|
|
931
|
+
console.warn('[ATHENA] Failed to capture external IDs:', error);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Hash PII for custom audience matching (SHA-256)
|
|
936
|
+
* Use this when user identifies with email/phone
|
|
937
|
+
*/
|
|
938
|
+
async hashPII(value) {
|
|
939
|
+
// Normalize: lowercase, trim whitespace
|
|
940
|
+
const normalized = value.toLowerCase().trim();
|
|
941
|
+
// Use Web Crypto API for hashing (browser/modern Node)
|
|
942
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
943
|
+
const encoder = new TextEncoder();
|
|
944
|
+
const data = encoder.encode(normalized);
|
|
945
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
946
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
947
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
948
|
+
}
|
|
949
|
+
// Try expo-crypto for React Native (Expo)
|
|
950
|
+
try {
|
|
951
|
+
// @ts-expect-error - expo-crypto is an optional peer dependency
|
|
952
|
+
const Crypto = await import('expo-crypto');
|
|
953
|
+
const hash = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, normalized);
|
|
954
|
+
return hash;
|
|
955
|
+
} catch (error) {
|
|
956
|
+
// expo-crypto not available
|
|
957
|
+
}
|
|
958
|
+
// Fallback: return unhashed (server should hash it)
|
|
959
|
+
console.warn('[ATHENA] SHA-256 not available - install expo-crypto for React Native: npx expo install expo-crypto');
|
|
960
|
+
return normalized;
|
|
961
|
+
}
|
|
621
962
|
/**
|
|
622
963
|
* Sleep utility
|
|
623
964
|
*/
|
|
@@ -634,13 +975,13 @@ AthenaTracker.instance = null;
|
|
|
634
975
|
* Per ATHENA Integration Plan Phase 4.1
|
|
635
976
|
*/
|
|
636
977
|
// Cross-platform crypto support
|
|
637
|
-
let crypto;
|
|
978
|
+
let crypto$1;
|
|
638
979
|
try {
|
|
639
980
|
// Node.js environment
|
|
640
|
-
crypto = require('crypto');
|
|
981
|
+
crypto$1 = require('crypto');
|
|
641
982
|
} catch {
|
|
642
983
|
// Browser/React Native - will use Web Crypto API
|
|
643
|
-
crypto = null;
|
|
984
|
+
crypto$1 = null;
|
|
644
985
|
}
|
|
645
986
|
/**
|
|
646
987
|
* Generate HMAC-SHA256 signature for webhook payload (Node.js version)
|
|
@@ -651,7 +992,7 @@ try {
|
|
|
651
992
|
*/
|
|
652
993
|
function generateWebhookSignatureNode(payload, secret) {
|
|
653
994
|
const payloadString = JSON.stringify(payload);
|
|
654
|
-
const hmac = crypto.createHmac('sha256', secret);
|
|
995
|
+
const hmac = crypto$1.createHmac('sha256', secret);
|
|
655
996
|
hmac.update(payloadString);
|
|
656
997
|
return hmac.digest('hex');
|
|
657
998
|
}
|
|
@@ -685,7 +1026,7 @@ async function generateWebhookSignatureBrowser(payload, secret) {
|
|
|
685
1026
|
* @returns HMAC signature (hex-encoded)
|
|
686
1027
|
*/
|
|
687
1028
|
async function generateWebhookSignature(payload, secret) {
|
|
688
|
-
if (crypto) {
|
|
1029
|
+
if (crypto$1) {
|
|
689
1030
|
// Node.js environment
|
|
690
1031
|
return generateWebhookSignatureNode(payload, secret);
|
|
691
1032
|
} else if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
|
|
@@ -712,9 +1053,9 @@ async function verifyWebhookSignature(payload, signature, secret) {
|
|
|
712
1053
|
return true;
|
|
713
1054
|
}
|
|
714
1055
|
// Use timing-safe comparison to prevent timing attacks (Node.js only)
|
|
715
|
-
if (crypto && crypto.timingSafeEqual) {
|
|
1056
|
+
if (crypto$1 && crypto$1.timingSafeEqual) {
|
|
716
1057
|
try {
|
|
717
|
-
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
|
|
1058
|
+
return crypto$1.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
|
|
718
1059
|
} catch (error) {
|
|
719
1060
|
return false;
|
|
720
1061
|
}
|