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
13#[allow(clippy::type_complexity)]
16static ANALYTICS_DB: Lazy<Arc<RwLock<Option<Arc<dyn PillarUsageRecorder>>>>> =
17 Lazy::new(|| Arc::new(RwLock::new(None)));
18
19#[async_trait::async_trait]
22pub trait PillarUsageRecorder: Send + Sync {
23 async fn record(
25 &self,
26 event: PillarUsageEvent,
27 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
28}
29
30#[derive(Debug, Clone)]
32pub struct PillarUsageEvent {
33 pub workspace_id: Option<String>,
35 pub org_id: Option<String>,
37 pub pillar: Pillar,
39 pub metric_name: String,
41 pub metric_value: Value,
43 pub timestamp: chrono::DateTime<Utc>,
45}
46
47pub async fn init(recorder: Arc<dyn PillarUsageRecorder>) {
49 let mut db = ANALYTICS_DB.write().await;
50 *db = Some(recorder);
51}
52
53pub 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
69pub 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
85pub 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
101pub 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
117pub 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
133async 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 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 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}