gestura_core_analytics/
analytics.rs

1//! Usage analytics and insights for Gestura.app
2//! Collects and analyzes user behavior patterns while respecting privacy
3
4use chrono::Timelike;
5#[allow(unused_imports)]
6use gestura_core_foundation::error::AppError;
7use std::collections::{BTreeMap, HashMap};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11/// Usage event types
12#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
13pub enum EventType {
14    AppLaunch,
15    AppClose,
16    VoiceCommand,
17    GesturePerformed,
18    RingConnected,
19    RingDisconnected,
20    SettingsChanged,
21    ErrorOccurred,
22    FeatureUsed(String),
23    Custom(String),
24}
25
26/// Usage event
27#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
28pub struct UsageEvent {
29    pub event_id: String,
30    pub event_type: EventType,
31    pub timestamp: chrono::DateTime<chrono::Utc>,
32    pub user_id: Option<String>,
33    pub session_id: String,
34    pub properties: HashMap<String, serde_json::Value>,
35    pub duration_ms: Option<u64>,
36}
37
38/// Analytics insights
39#[derive(Debug, Clone, serde::Serialize)]
40pub struct AnalyticsInsights {
41    pub total_events: usize,
42    pub unique_users: usize,
43    pub active_sessions: usize,
44    pub most_used_features: Vec<(String, usize)>,
45    pub usage_patterns: UsagePatterns,
46    pub performance_metrics: PerformanceMetrics,
47    pub error_analysis: ErrorAnalysis,
48    pub time_period: TimePeriod,
49}
50
51/// Usage patterns analysis
52#[derive(Debug, Clone, serde::Serialize)]
53pub struct UsagePatterns {
54    pub peak_usage_hours: Vec<u8>, // Hours of day (0-23)
55    pub average_session_duration_minutes: f64,
56    pub most_common_gestures: Vec<(String, usize)>,
57    pub voice_command_frequency: f64, // Commands per session
58    pub feature_adoption_rate: HashMap<String, f64>, // Feature -> adoption rate
59}
60
61/// Performance metrics
62#[derive(Debug, Clone, serde::Serialize)]
63pub struct PerformanceMetrics {
64    pub average_response_time_ms: f64,
65    pub gesture_recognition_accuracy: f64,
66    pub voice_recognition_accuracy: f64,
67    pub system_stability_score: f64, // 0-1 scale
68}
69
70/// Error analysis
71#[derive(Debug, Clone, serde::Serialize)]
72pub struct ErrorAnalysis {
73    pub total_errors: usize,
74    pub error_rate: f64, // Errors per session
75    pub most_common_errors: Vec<(String, usize)>,
76    pub error_trends: Vec<(chrono::DateTime<chrono::Utc>, usize)>,
77}
78
79/// Time period for analysis
80#[derive(Debug, Clone, serde::Serialize)]
81pub struct TimePeriod {
82    pub start: chrono::DateTime<chrono::Utc>,
83    pub end: chrono::DateTime<chrono::Utc>,
84    pub duration_days: i64,
85}
86
87/// Analytics configuration
88#[derive(Debug, Clone)]
89pub struct AnalyticsConfig {
90    pub enable_collection: bool,
91    pub anonymize_data: bool,
92    pub retention_days: i64,
93    pub batch_size: usize,
94    pub flush_interval_seconds: u64,
95    pub privacy_mode: PrivacyMode,
96}
97
98/// Privacy modes for analytics
99#[derive(Debug, Clone, PartialEq)]
100pub enum PrivacyMode {
101    Full,      // Collect all data
102    Limited,   // Collect only essential metrics
103    Anonymous, // Collect data without user identification
104    Disabled,  // No data collection
105}
106
107impl Default for AnalyticsConfig {
108    fn default() -> Self {
109        Self {
110            enable_collection: true,
111            anonymize_data: true,
112            retention_days: 30,
113            batch_size: 100,
114            flush_interval_seconds: 300, // 5 minutes
115            privacy_mode: PrivacyMode::Anonymous,
116        }
117    }
118}
119
120/// Usage analytics system
121pub struct UsageAnalytics {
122    events: Arc<RwLock<Vec<UsageEvent>>>,
123    config: Arc<RwLock<AnalyticsConfig>>,
124    session_cache: Arc<RwLock<HashMap<String, SessionInfo>>>,
125    insights_cache: Arc<RwLock<Option<AnalyticsInsights>>>,
126    last_flush: Arc<RwLock<chrono::DateTime<chrono::Utc>>>,
127}
128
129/// Session information
130#[derive(Debug, Clone)]
131struct SessionInfo {
132    #[allow(dead_code)]
133    session_id: String,
134    #[allow(dead_code)]
135    user_id: Option<String>,
136    start_time: chrono::DateTime<chrono::Utc>,
137    last_activity: chrono::DateTime<chrono::Utc>,
138    event_count: usize,
139}
140
141impl UsageAnalytics {
142    /// Create a new usage analytics system
143    pub fn new(config: AnalyticsConfig) -> Self {
144        Self {
145            events: Arc::new(RwLock::new(Vec::new())),
146            config: Arc::new(RwLock::new(config)),
147            session_cache: Arc::new(RwLock::new(HashMap::new())),
148            insights_cache: Arc::new(RwLock::new(None)),
149            last_flush: Arc::new(RwLock::new(chrono::Utc::now())),
150        }
151    }
152
153    /// Track a usage event
154    pub async fn track_event(&self, mut event: UsageEvent) -> Result<(), AppError> {
155        let config = self.config.read().await;
156
157        if !config.enable_collection || config.privacy_mode == PrivacyMode::Disabled {
158            return Ok(());
159        }
160
161        // Apply privacy settings
162        if config.anonymize_data || config.privacy_mode == PrivacyMode::Anonymous {
163            event.user_id = None;
164            // Remove potentially identifying properties
165            event.properties.remove("device_id");
166            event.properties.remove("ip_address");
167            event.properties.remove("user_agent");
168        }
169
170        // Update session info
171        self.update_session_info(&event).await;
172
173        // Store event
174        let mut events = self.events.write().await;
175        events.push(event);
176
177        // Check if we need to flush
178        if events.len() >= config.batch_size {
179            drop(events);
180            drop(config);
181            self.flush_events().await?;
182        }
183
184        Ok(())
185    }
186
187    /// Track app launch
188    pub async fn track_app_launch(
189        &self,
190        session_id: String,
191        user_id: Option<String>,
192    ) -> Result<(), AppError> {
193        let event = UsageEvent {
194            event_id: uuid::Uuid::new_v4().to_string(),
195            event_type: EventType::AppLaunch,
196            timestamp: chrono::Utc::now(),
197            user_id,
198            session_id,
199            properties: HashMap::from([
200                (
201                    "version".to_string(),
202                    serde_json::Value::String("1.0.0".to_string()),
203                ),
204                (
205                    "platform".to_string(),
206                    serde_json::Value::String(std::env::consts::OS.to_string()),
207                ),
208            ]),
209            duration_ms: None,
210        };
211
212        self.track_event(event).await
213    }
214
215    /// Track voice command
216    pub async fn track_voice_command(
217        &self,
218        session_id: String,
219        user_id: Option<String>,
220        command: &str,
221        confidence: f32,
222        processing_time_ms: u64,
223    ) -> Result<(), AppError> {
224        let event = UsageEvent {
225            event_id: uuid::Uuid::new_v4().to_string(),
226            event_type: EventType::VoiceCommand,
227            timestamp: chrono::Utc::now(),
228            user_id,
229            session_id,
230            properties: HashMap::from([
231                (
232                    "command_length".to_string(),
233                    serde_json::Value::Number(serde_json::Number::from(command.len())),
234                ),
235                (
236                    "confidence".to_string(),
237                    serde_json::Value::Number(
238                        serde_json::Number::from_f64(confidence as f64).unwrap(),
239                    ),
240                ),
241                (
242                    "processing_time_ms".to_string(),
243                    serde_json::Value::Number(serde_json::Number::from(processing_time_ms)),
244                ),
245            ]),
246            duration_ms: Some(processing_time_ms),
247        };
248
249        self.track_event(event).await
250    }
251
252    /// Track gesture
253    pub async fn track_gesture(
254        &self,
255        session_id: String,
256        user_id: Option<String>,
257        gesture_type: &str,
258        confidence: f32,
259    ) -> Result<(), AppError> {
260        let event = UsageEvent {
261            event_id: uuid::Uuid::new_v4().to_string(),
262            event_type: EventType::GesturePerformed,
263            timestamp: chrono::Utc::now(),
264            user_id,
265            session_id,
266            properties: HashMap::from([
267                (
268                    "gesture_type".to_string(),
269                    serde_json::Value::String(gesture_type.to_string()),
270                ),
271                (
272                    "confidence".to_string(),
273                    serde_json::Value::Number(
274                        serde_json::Number::from_f64(confidence as f64).unwrap(),
275                    ),
276                ),
277            ]),
278            duration_ms: None,
279        };
280
281        self.track_event(event).await
282    }
283
284    /// Track error
285    pub async fn track_error(
286        &self,
287        session_id: String,
288        user_id: Option<String>,
289        error_type: &str,
290        error_message: &str,
291    ) -> Result<(), AppError> {
292        let event = UsageEvent {
293            event_id: uuid::Uuid::new_v4().to_string(),
294            event_type: EventType::ErrorOccurred,
295            timestamp: chrono::Utc::now(),
296            user_id,
297            session_id,
298            properties: HashMap::from([
299                (
300                    "error_type".to_string(),
301                    serde_json::Value::String(error_type.to_string()),
302                ),
303                (
304                    "error_message".to_string(),
305                    serde_json::Value::String(error_message.to_string()),
306                ),
307            ]),
308            duration_ms: None,
309        };
310
311        self.track_event(event).await
312    }
313
314    /// Generate analytics insights
315    pub async fn generate_insights(
316        &self,
317        days_back: Option<i64>,
318    ) -> Result<AnalyticsInsights, AppError> {
319        let days = days_back.unwrap_or(7);
320        let start_time = chrono::Utc::now() - chrono::Duration::days(days);
321        let end_time = chrono::Utc::now();
322
323        let events = self.events.read().await;
324        let filtered_events: Vec<&UsageEvent> = events
325            .iter()
326            .filter(|e| e.timestamp >= start_time && e.timestamp <= end_time)
327            .collect();
328
329        if filtered_events.is_empty() {
330            return Ok(AnalyticsInsights {
331                total_events: 0,
332                unique_users: 0,
333                active_sessions: 0,
334                most_used_features: Vec::new(),
335                usage_patterns: UsagePatterns {
336                    peak_usage_hours: Vec::new(),
337                    average_session_duration_minutes: 0.0,
338                    most_common_gestures: Vec::new(),
339                    voice_command_frequency: 0.0,
340                    feature_adoption_rate: HashMap::new(),
341                },
342                performance_metrics: PerformanceMetrics {
343                    average_response_time_ms: 0.0,
344                    gesture_recognition_accuracy: 0.0,
345                    voice_recognition_accuracy: 0.0,
346                    system_stability_score: 1.0,
347                },
348                error_analysis: ErrorAnalysis {
349                    total_errors: 0,
350                    error_rate: 0.0,
351                    most_common_errors: Vec::new(),
352                    error_trends: Vec::new(),
353                },
354                time_period: TimePeriod {
355                    start: start_time,
356                    end: end_time,
357                    duration_days: days,
358                },
359            });
360        }
361
362        // Basic metrics
363        let total_events = filtered_events.len();
364        let unique_users = filtered_events
365            .iter()
366            .filter_map(|e| e.user_id.as_ref())
367            .collect::<std::collections::HashSet<_>>()
368            .len();
369        let unique_sessions = filtered_events
370            .iter()
371            .map(|e| &e.session_id)
372            .collect::<std::collections::HashSet<_>>()
373            .len();
374
375        // Feature usage analysis
376        let mut feature_counts = HashMap::new();
377        for event in &filtered_events {
378            let feature_name = match &event.event_type {
379                EventType::VoiceCommand => "voice_commands",
380                EventType::GesturePerformed => "gestures",
381                EventType::RingConnected => "ring_connection",
382                EventType::SettingsChanged => "settings",
383                EventType::FeatureUsed(name) => name,
384                _ => "other",
385            };
386            *feature_counts.entry(feature_name.to_string()).or_insert(0) += 1;
387        }
388
389        let mut most_used_features: Vec<(String, usize)> = feature_counts.into_iter().collect();
390        most_used_features.sort_by(|a, b| b.1.cmp(&a.1));
391        most_used_features.truncate(10);
392
393        // Usage patterns
394        let usage_patterns = self.analyze_usage_patterns(&filtered_events).await;
395
396        // Performance metrics
397        let performance_metrics = self.analyze_performance(&filtered_events).await;
398
399        // Error analysis
400        let error_analysis = self.analyze_errors(&filtered_events).await;
401
402        let insights = AnalyticsInsights {
403            total_events,
404            unique_users,
405            active_sessions: unique_sessions,
406            most_used_features,
407            usage_patterns,
408            performance_metrics,
409            error_analysis,
410            time_period: TimePeriod {
411                start: start_time,
412                end: end_time,
413                duration_days: days,
414            },
415        };
416
417        // Cache insights
418        let mut cache = self.insights_cache.write().await;
419        *cache = Some(insights.clone());
420
421        Ok(insights)
422    }
423
424    /// Analyze usage patterns
425    async fn analyze_usage_patterns(&self, events: &[&UsageEvent]) -> UsagePatterns {
426        // Peak usage hours
427        let mut hour_counts = BTreeMap::new();
428        for event in events {
429            let hour = event.timestamp.hour() as u8;
430            *hour_counts.entry(hour).or_insert(0) += 1;
431        }
432
433        let mut peak_hours: Vec<(u8, usize)> = hour_counts.into_iter().collect();
434        peak_hours.sort_by(|a, b| b.1.cmp(&a.1));
435        let peak_usage_hours = peak_hours.into_iter().take(3).map(|(h, _)| h).collect();
436
437        // Session duration analysis
438        let sessions = self.session_cache.read().await;
439        let avg_duration = if !sessions.is_empty() {
440            let total_duration: i64 = sessions
441                .values()
442                .map(|s| (s.last_activity - s.start_time).num_minutes())
443                .sum();
444            total_duration as f64 / sessions.len() as f64
445        } else {
446            0.0
447        };
448
449        // Gesture analysis
450        let mut gesture_counts = HashMap::new();
451        for event in events {
452            if let EventType::GesturePerformed = event.event_type
453                && let Some(gesture_type) = event.properties.get("gesture_type")
454                && let Some(gesture_str) = gesture_type.as_str()
455            {
456                *gesture_counts.entry(gesture_str.to_string()).or_insert(0) += 1;
457            }
458        }
459
460        let mut most_common_gestures: Vec<(String, usize)> = gesture_counts.into_iter().collect();
461        most_common_gestures.sort_by(|a, b| b.1.cmp(&a.1));
462        most_common_gestures.truncate(5);
463
464        // Voice command frequency
465        let voice_commands = events
466            .iter()
467            .filter(|e| matches!(e.event_type, EventType::VoiceCommand))
468            .count();
469        let unique_sessions = events
470            .iter()
471            .map(|e| &e.session_id)
472            .collect::<std::collections::HashSet<_>>()
473            .len();
474        let voice_command_frequency = if unique_sessions > 0 {
475            voice_commands as f64 / unique_sessions as f64
476        } else {
477            0.0
478        };
479
480        UsagePatterns {
481            peak_usage_hours,
482            average_session_duration_minutes: avg_duration,
483            most_common_gestures,
484            voice_command_frequency,
485            feature_adoption_rate: HashMap::new(), // Would be calculated based on user cohorts
486        }
487    }
488
489    /// Analyze performance metrics
490    async fn analyze_performance(&self, events: &[&UsageEvent]) -> PerformanceMetrics {
491        let mut response_times = Vec::new();
492        let mut gesture_confidences = Vec::new();
493        let mut voice_confidences = Vec::new();
494
495        for event in events {
496            if let Some(duration) = event.duration_ms {
497                response_times.push(duration as f64);
498            }
499
500            if let Some(confidence) = event.properties.get("confidence")
501                && let Some(conf_val) = confidence.as_f64()
502            {
503                match event.event_type {
504                    EventType::GesturePerformed => gesture_confidences.push(conf_val),
505                    EventType::VoiceCommand => voice_confidences.push(conf_val),
506                    _ => {}
507                }
508            }
509        }
510
511        let avg_response_time = if !response_times.is_empty() {
512            response_times.iter().sum::<f64>() / response_times.len() as f64
513        } else {
514            0.0
515        };
516
517        let gesture_accuracy = if !gesture_confidences.is_empty() {
518            gesture_confidences.iter().sum::<f64>() / gesture_confidences.len() as f64
519        } else {
520            0.0
521        };
522
523        let voice_accuracy = if !voice_confidences.is_empty() {
524            voice_confidences.iter().sum::<f64>() / voice_confidences.len() as f64
525        } else {
526            0.0
527        };
528
529        // System stability (1 - error_rate)
530        let error_count = events
531            .iter()
532            .filter(|e| matches!(e.event_type, EventType::ErrorOccurred))
533            .count();
534        let stability_score = if !events.is_empty() {
535            1.0 - (error_count as f64 / events.len() as f64)
536        } else {
537            1.0
538        };
539
540        PerformanceMetrics {
541            average_response_time_ms: avg_response_time,
542            gesture_recognition_accuracy: gesture_accuracy,
543            voice_recognition_accuracy: voice_accuracy,
544            system_stability_score: stability_score,
545        }
546    }
547
548    /// Analyze errors
549    async fn analyze_errors(&self, events: &[&UsageEvent]) -> ErrorAnalysis {
550        let error_events: Vec<&UsageEvent> = events
551            .iter()
552            .filter(|e| matches!(e.event_type, EventType::ErrorOccurred))
553            .cloned()
554            .collect();
555
556        let total_errors = error_events.len();
557        let unique_sessions = events
558            .iter()
559            .map(|e| &e.session_id)
560            .collect::<std::collections::HashSet<_>>()
561            .len();
562        let error_rate = if unique_sessions > 0 {
563            total_errors as f64 / unique_sessions as f64
564        } else {
565            0.0
566        };
567
568        // Most common errors
569        let mut error_counts = HashMap::new();
570        for event in &error_events {
571            if let Some(error_type) = event.properties.get("error_type")
572                && let Some(error_str) = error_type.as_str()
573            {
574                *error_counts.entry(error_str.to_string()).or_insert(0) += 1;
575            }
576        }
577
578        let mut most_common_errors: Vec<(String, usize)> = error_counts.into_iter().collect();
579        most_common_errors.sort_by(|a, b| b.1.cmp(&a.1));
580        most_common_errors.truncate(5);
581
582        // Error trends (daily)
583        let mut daily_errors = BTreeMap::new();
584        for event in &error_events {
585            let date = event.timestamp.date_naive();
586            *daily_errors.entry(date).or_insert(0) += 1;
587        }
588
589        let error_trends: Vec<(chrono::DateTime<chrono::Utc>, usize)> = daily_errors
590            .into_iter()
591            .map(|(date, count)| (date.and_hms_opt(0, 0, 0).unwrap().and_utc(), count))
592            .collect();
593
594        ErrorAnalysis {
595            total_errors,
596            error_rate,
597            most_common_errors,
598            error_trends,
599        }
600    }
601
602    /// Update session information
603    async fn update_session_info(&self, event: &UsageEvent) {
604        let mut sessions = self.session_cache.write().await;
605
606        let session_info =
607            sessions
608                .entry(event.session_id.clone())
609                .or_insert_with(|| SessionInfo {
610                    session_id: event.session_id.clone(),
611                    user_id: event.user_id.clone(),
612                    start_time: event.timestamp,
613                    last_activity: event.timestamp,
614                    event_count: 0,
615                });
616
617        session_info.last_activity = event.timestamp;
618        session_info.event_count += 1;
619    }
620
621    /// Flush events to persistent storage
622    async fn flush_events(&self) -> Result<(), AppError> {
623        let mut events = self.events.write().await;
624        let _config = self.config.read().await;
625
626        if events.is_empty() {
627            return Ok(());
628        }
629
630        // In a real implementation, this would write to a database or file
631        tracing::info!("Flushing {} analytics events", events.len());
632
633        // Clear events after flushing
634        events.clear();
635
636        let mut last_flush = self.last_flush.write().await;
637        *last_flush = chrono::Utc::now();
638
639        Ok(())
640    }
641
642    /// Clean up old data
643    pub async fn cleanup_old_data(&self) -> Result<usize, AppError> {
644        let config = self.config.read().await;
645        let cutoff_date = chrono::Utc::now() - chrono::Duration::days(config.retention_days);
646
647        let mut events = self.events.write().await;
648        let initial_count = events.len();
649
650        events.retain(|event| event.timestamp > cutoff_date);
651
652        let removed_count = initial_count - events.len();
653        if removed_count > 0 {
654            tracing::info!("Cleaned up {} old analytics events", removed_count);
655        }
656
657        Ok(removed_count)
658    }
659
660    /// Update configuration
661    pub async fn update_config(&self, new_config: AnalyticsConfig) {
662        let mut config = self.config.write().await;
663        *config = new_config;
664    }
665
666    /// Get current configuration
667    pub async fn get_config(&self) -> AnalyticsConfig {
668        let config = self.config.read().await;
669        config.clone()
670    }
671
672    /// Get cached insights
673    pub async fn get_cached_insights(&self) -> Option<AnalyticsInsights> {
674        let cache = self.insights_cache.read().await;
675        cache.clone()
676    }
677}
678
679/// Global usage analytics instance
680static USAGE_ANALYTICS: tokio::sync::OnceCell<UsageAnalytics> = tokio::sync::OnceCell::const_new();
681
682/// Get the global usage analytics
683pub async fn get_usage_analytics() -> &'static UsageAnalytics {
684    USAGE_ANALYTICS
685        .get_or_init(|| async { UsageAnalytics::new(AnalyticsConfig::default()) })
686        .await
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692
693    #[tokio::test]
694    async fn test_event_tracking() {
695        // Use Full privacy mode to preserve user_id for testing
696        let config = AnalyticsConfig {
697            enable_collection: true,
698            anonymize_data: false,
699            privacy_mode: PrivacyMode::Full,
700            ..Default::default()
701        };
702        let analytics = UsageAnalytics::new(config);
703
704        analytics
705            .track_app_launch("session1".to_string(), Some("user1".to_string()))
706            .await
707            .unwrap();
708        analytics
709            .track_voice_command(
710                "session1".to_string(),
711                Some("user1".to_string()),
712                "test command",
713                0.9,
714                100,
715            )
716            .await
717            .unwrap();
718
719        let insights = analytics.generate_insights(Some(1)).await.unwrap();
720        assert_eq!(insights.total_events, 2);
721        assert_eq!(insights.unique_users, 1);
722    }
723
724    #[tokio::test]
725    async fn test_privacy_mode() {
726        let config = AnalyticsConfig {
727            privacy_mode: PrivacyMode::Anonymous,
728            ..Default::default()
729        };
730
731        let analytics = UsageAnalytics::new(config);
732
733        let event = UsageEvent {
734            event_id: "test".to_string(),
735            event_type: EventType::VoiceCommand,
736            timestamp: chrono::Utc::now(),
737            user_id: Some("user123".to_string()),
738            session_id: "session1".to_string(),
739            properties: HashMap::new(),
740            duration_ms: None,
741        };
742
743        analytics.track_event(event.clone()).await.unwrap();
744
745        // User ID should be anonymized
746        let events = analytics.events.read().await;
747        assert!(events[0].user_id.is_none());
748    }
749
750    #[tokio::test]
751    async fn test_insights_generation() {
752        let analytics = UsageAnalytics::new(AnalyticsConfig::default());
753
754        // Add some test events
755        for i in 0..10 {
756            analytics
757                .track_gesture(
758                    "session1".to_string(),
759                    Some("user1".to_string()),
760                    "tap",
761                    0.8 + (i as f32 * 0.01),
762                )
763                .await
764                .unwrap();
765        }
766
767        let insights = analytics.generate_insights(Some(1)).await.unwrap();
768        assert_eq!(insights.total_events, 10);
769        assert!(insights.performance_metrics.gesture_recognition_accuracy > 0.8);
770    }
771}