Skip to main content

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