mockforge_core/
pillar_tracking.rs

1//! Pillar usage tracking utilities
2//!
3//! Provides helper functions for recording pillar usage events throughout the codebase.
4//! These events are used for analytics and understanding which pillars are most used.
5
6use crate::pillars::Pillar;
7use chrono::Utc;
8use once_cell::sync::Lazy;
9use serde_json::Value;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13/// Optional analytics database for recording pillar usage
14/// This is set globally and can be None if analytics is not enabled
15static ANALYTICS_DB: Lazy<Arc<RwLock<Option<Arc<dyn PillarUsageRecorder>>>>> =
16    Lazy::new(|| Arc::new(RwLock::new(None)));
17
18/// Trait for recording pillar usage events
19/// This allows different implementations (analytics DB, API endpoint, etc.)
20#[async_trait::async_trait]
21pub trait PillarUsageRecorder: Send + Sync {
22    /// Record a pillar usage event
23    async fn record(
24        &self,
25        event: PillarUsageEvent,
26    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
27}
28
29/// Pillar usage event (simplified version for internal use)
30#[derive(Debug, Clone)]
31pub struct PillarUsageEvent {
32    /// Workspace ID where the event occurred
33    pub workspace_id: Option<String>,
34    /// Organization ID (if applicable)
35    pub org_id: Option<String>,
36    /// The pillar this event relates to
37    pub pillar: Pillar,
38    /// Name of the metric being recorded
39    pub metric_name: String,
40    /// Value of the metric (JSON)
41    pub metric_value: Value,
42    /// Timestamp when the event occurred
43    pub timestamp: chrono::DateTime<Utc>,
44}
45
46/// Initialize the pillar usage tracker with a recorder
47pub async fn init(recorder: Arc<dyn PillarUsageRecorder>) {
48    let mut db = ANALYTICS_DB.write().await;
49    *db = Some(recorder);
50}
51
52/// Record a reality pillar usage event
53///
54/// This should be called when:
55/// - Reality continuum blend ratio is used
56/// - Smart personas are activated
57/// - Chaos is enabled/used
58/// - Reality level changes
59pub async fn record_reality_usage(
60    workspace_id: Option<String>,
61    org_id: Option<String>,
62    metric_name: &str,
63    metric_value: Value,
64) {
65    record_pillar_usage(workspace_id, org_id, Pillar::Reality, metric_name, metric_value).await;
66}
67
68/// Record a contracts pillar usage event
69///
70/// This should be called when:
71/// - Contract validation is performed
72/// - Drift detection occurs
73/// - Contract sync happens
74/// - Validation mode changes
75pub async fn record_contracts_usage(
76    workspace_id: Option<String>,
77    org_id: Option<String>,
78    metric_name: &str,
79    metric_value: Value,
80) {
81    record_pillar_usage(workspace_id, org_id, Pillar::Contracts, metric_name, metric_value).await;
82}
83
84/// Record a DevX pillar usage event
85///
86/// This should be called when:
87/// - SDK is installed/used
88/// - Client code is generated
89/// - Playground session starts
90/// - CLI command is executed
91pub async fn record_devx_usage(
92    workspace_id: Option<String>,
93    org_id: Option<String>,
94    metric_name: &str,
95    metric_value: Value,
96) {
97    record_pillar_usage(workspace_id, org_id, Pillar::DevX, metric_name, metric_value).await;
98}
99
100/// Record a cloud pillar usage event
101///
102/// This should be called when:
103/// - Scenario is shared
104/// - Marketplace download occurs
105/// - Workspace is created/shared
106/// - Organization template is used
107pub async fn record_cloud_usage(
108    workspace_id: Option<String>,
109    org_id: Option<String>,
110    metric_name: &str,
111    metric_value: Value,
112) {
113    record_pillar_usage(workspace_id, org_id, Pillar::Cloud, metric_name, metric_value).await;
114}
115
116/// Record an AI pillar usage event
117///
118/// This should be called when:
119/// - AI mock generation occurs
120/// - AI contract diff is performed
121/// - Voice command is executed
122/// - LLM-assisted operation happens
123pub async fn record_ai_usage(
124    workspace_id: Option<String>,
125    org_id: Option<String>,
126    metric_name: &str,
127    metric_value: Value,
128) {
129    record_pillar_usage(workspace_id, org_id, Pillar::Ai, metric_name, metric_value).await;
130}
131
132/// Record a pillar usage event (internal helper)
133async fn record_pillar_usage(
134    workspace_id: Option<String>,
135    org_id: Option<String>,
136    pillar: Pillar,
137    metric_name: &str,
138    metric_value: Value,
139) {
140    let db = ANALYTICS_DB.read().await;
141    if let Some(recorder) = db.as_ref() {
142        let event = PillarUsageEvent {
143            workspace_id,
144            org_id,
145            pillar,
146            metric_name: metric_name.to_string(),
147            metric_value,
148            timestamp: Utc::now(),
149        };
150
151        // Record asynchronously without blocking
152        let recorder = recorder.clone();
153        tokio::spawn(async move {
154            if let Err(e) = recorder.record(event).await {
155                tracing::warn!("Failed to record pillar usage event: {}", e);
156            }
157        });
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use serde_json::json;
165
166    struct TestRecorder {
167        events: Arc<RwLock<Vec<PillarUsageEvent>>>,
168    }
169
170    #[async_trait::async_trait]
171    impl PillarUsageRecorder for TestRecorder {
172        async fn record(
173            &self,
174            event: PillarUsageEvent,
175        ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
176            let mut events = self.events.write().await;
177            events.push(event);
178            Ok(())
179        }
180    }
181
182    #[tokio::test]
183    async fn test_record_reality_usage() {
184        let events = Arc::new(RwLock::new(Vec::new()));
185        let recorder = Arc::new(TestRecorder {
186            events: events.clone(),
187        });
188        init(recorder).await;
189
190        record_reality_usage(
191            Some("workspace-1".to_string()),
192            None,
193            "blended_reality_ratio",
194            json!({"ratio": 0.5}),
195        )
196        .await;
197
198        // Give async task time to complete
199        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
200
201        let recorded = events.read().await;
202        assert_eq!(recorded.len(), 1);
203        assert_eq!(recorded[0].pillar, Pillar::Reality);
204        assert_eq!(recorded[0].metric_name, "blended_reality_ratio");
205    }
206}