mockforge_core/
pillar_tracking.rs1use crate::pillars::Pillar;
7use chrono::Utc;
8use once_cell::sync::Lazy;
9use serde_json::Value;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13static ANALYTICS_DB: Lazy<Arc<RwLock<Option<Arc<dyn PillarUsageRecorder>>>>> =
16 Lazy::new(|| Arc::new(RwLock::new(None)));
17
18#[async_trait::async_trait]
21pub trait PillarUsageRecorder: Send + Sync {
22 async fn record(
24 &self,
25 event: PillarUsageEvent,
26 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
27}
28
29#[derive(Debug, Clone)]
31pub struct PillarUsageEvent {
32 pub workspace_id: Option<String>,
34 pub org_id: Option<String>,
36 pub pillar: Pillar,
38 pub metric_name: String,
40 pub metric_value: Value,
42 pub timestamp: chrono::DateTime<Utc>,
44}
45
46pub async fn init(recorder: Arc<dyn PillarUsageRecorder>) {
48 let mut db = ANALYTICS_DB.write().await;
49 *db = Some(recorder);
50}
51
52pub 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
68pub 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
84pub 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
100pub 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
116pub 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
132async 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 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 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}