1#![allow(non_upper_case_globals)]
6
7use std::collections::HashMap;
10
11use serde_json::{json, Value as JsonValue};
12
13use crate::coverage::record_coverage;
14use crate::database::Database;
15use crate::metrics::Metric;
16use crate::Lifetime;
17
18pub(crate) const INTERNAL_STORAGE: &str = "glean_internal_info";
20
21pub struct StorageManager;
23
24fn snapshot_labeled_metrics(
30 snapshot: &mut HashMap<String, HashMap<String, JsonValue>>,
31 metric_id: &str,
32 metric: &Metric,
33) {
34 let ping_section = format!("labeled_{}", metric.ping_section());
35 let map = snapshot.entry(ping_section).or_default();
36
37 let (metric_id, label) = metric_id.split_once('/').unwrap();
39
40 let obj = map.entry(metric_id.into()).or_insert_with(|| json!({}));
41 let obj = obj.as_object_mut().unwrap(); obj.insert(label.into(), metric.as_json());
43}
44
45impl StorageManager {
46 pub fn snapshot(
59 &self,
60 storage: &Database,
61 store_name: &str,
62 clear_store: bool,
63 ) -> Option<String> {
64 self.snapshot_as_json(storage, store_name, clear_store)
65 .map(|data| ::serde_json::to_string_pretty(&data).unwrap())
66 }
67
68 pub fn snapshot_as_json(
81 &self,
82 storage: &Database,
83 store_name: &str,
84 clear_store: bool,
85 ) -> Option<JsonValue> {
86 let mut snapshot: HashMap<String, HashMap<String, JsonValue>> = HashMap::new();
87
88 let mut snapshotter = |metric_id: &[u8], metric: &Metric| {
89 let metric_id = String::from_utf8_lossy(metric_id).into_owned();
90 if metric_id.contains('/') {
91 snapshot_labeled_metrics(&mut snapshot, &metric_id, metric);
92 } else {
93 let map = snapshot.entry(metric.ping_section().into()).or_default();
94 map.insert(metric_id, metric.as_json());
95 }
96 };
97
98 storage.iter_store_from(Lifetime::Ping, store_name, None, &mut snapshotter);
99 storage.iter_store_from(Lifetime::Application, store_name, None, &mut snapshotter);
100 storage.iter_store_from(Lifetime::User, store_name, None, &mut snapshotter);
101
102 if store_name != "glean_client_info" {
104 storage.iter_store_from(Lifetime::Application, "all-pings", None, snapshotter);
105 }
106
107 if clear_store {
108 if let Err(e) = storage.clear_ping_lifetime_storage(store_name) {
109 log::warn!("Failed to clear lifetime storage: {:?}", e);
110 }
111 }
112
113 if snapshot.is_empty() {
114 None
115 } else {
116 Some(json!(snapshot))
117 }
118 }
119
120 pub fn snapshot_metric(
132 &self,
133 storage: &Database,
134 store_name: &str,
135 metric_id: &str,
136 metric_lifetime: Lifetime,
137 ) -> Option<Metric> {
138 let mut snapshot: Option<Metric> = None;
139
140 let mut snapshotter = |id: &[u8], metric: &Metric| {
141 let id = String::from_utf8_lossy(id).into_owned();
142 if id == metric_id {
143 snapshot = Some(metric.clone())
144 }
145 };
146
147 storage.iter_store_from(metric_lifetime, store_name, None, &mut snapshotter);
148
149 snapshot
150 }
151
152 pub fn snapshot_metric_for_test(
167 &self,
168 storage: &Database,
169 store_name: &str,
170 metric_id: &str,
171 metric_lifetime: Lifetime,
172 ) -> Option<Metric> {
173 record_coverage(metric_id);
174 self.snapshot_metric(storage, store_name, metric_id, metric_lifetime)
175 }
176
177 pub fn snapshot_experiments_as_json(
202 &self,
203 storage: &Database,
204 store_name: &str,
205 ) -> Option<JsonValue> {
206 let mut snapshot: HashMap<String, JsonValue> = HashMap::new();
207
208 let mut snapshotter = |metric_id: &[u8], metric: &Metric| {
209 let metric_id = String::from_utf8_lossy(metric_id).into_owned();
210 if metric_id.ends_with("#experiment") {
211 let (name, _) = metric_id.split_once('#').unwrap(); snapshot.insert(name.to_string(), metric.as_json());
213 }
214 };
215
216 storage.iter_store_from(Lifetime::Application, store_name, None, &mut snapshotter);
217
218 if snapshot.is_empty() {
219 None
220 } else {
221 Some(json!(snapshot))
222 }
223 }
224}
225
226#[cfg(test)]
227mod test {
228 use super::*;
229 use crate::metrics::ExperimentMetric;
230 use crate::Glean;
231
232 #[test]
235 fn test_experiments_json_serialization() {
236 let t = tempfile::tempdir().unwrap();
237 let name = t.path().display().to_string();
238 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
239
240 let extra: HashMap<String, String> = [("test-key".into(), "test-value".into())]
241 .iter()
242 .cloned()
243 .collect();
244
245 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
246
247 metric.set_active_sync(&glean, "test-branch".to_string(), extra);
248 let snapshot = StorageManager
249 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
250 .unwrap();
251 assert_eq!(
252 json!({"some-experiment": {"branch": "test-branch", "extra": {"test-key": "test-value"}}}),
253 snapshot
254 );
255
256 metric.set_inactive_sync(&glean);
257
258 let empty_snapshot =
259 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
260 assert!(empty_snapshot.is_none());
261 }
262
263 #[test]
264 fn test_experiments_json_serialization_empty() {
265 let t = tempfile::tempdir().unwrap();
266 let name = t.path().display().to_string();
267 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
268
269 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
270
271 metric.set_active_sync(&glean, "test-branch".to_string(), HashMap::new());
272 let snapshot = StorageManager
273 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
274 .unwrap();
275 assert_eq!(
276 json!({"some-experiment": {"branch": "test-branch"}}),
277 snapshot
278 );
279
280 metric.set_inactive_sync(&glean);
281
282 let empty_snapshot =
283 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
284 assert!(empty_snapshot.is_none());
285 }
286}