• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Unleash / unleash-edge / 16214554656

11 Jul 2025 07:37AM UTC coverage: 78.859%. First build
16214554656

Pull #1035

github

web-flow
Merge 6ff5f05fe into a927a96d0
Pull Request #1035: chore: apply formatter and enforce in the build

168 of 180 new or added lines in 4 files covered. (93.33%)

10661 of 13519 relevant lines covered (78.86%)

5563.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.82
/server/src/metrics/client_impact_metrics.rs
1
use crate::metrics::client_metrics::MetricsCache;
2
use itertools::Itertools;
3
use serde::{Deserialize, Serialize};
4
use std::collections::HashMap;
5
use std::hash::Hash;
6
use unleash_types::Merge;
7
use unleash_types::client_metrics::{ImpactMetric, ImpactMetricEnv};
8
use utoipa::ToSchema;
9

10
#[derive(Debug, Clone, Eq, Deserialize, Serialize, ToSchema, Hash, PartialEq)]
×
11
pub struct ImpactMetricsKey {
12
    pub app_name: String,
13
    pub environment: String,
14
}
15

16
impl From<ImpactMetricEnv> for ImpactMetricsKey {
17
    fn from(value: ImpactMetricEnv) -> Self {
5✔
18
        Self {
5✔
19
            app_name: value.app_name,
5✔
20
            environment: value.environment,
5✔
21
        }
5✔
22
    }
5✔
23
}
24

25
pub fn convert_to_impact_metrics_env(
5✔
26
    metrics: Vec<ImpactMetric>,
5✔
27
    app_name: String,
5✔
28
    environment: String,
5✔
29
) -> Vec<ImpactMetricEnv> {
5✔
30
    metrics
5✔
31
        .into_iter()
5✔
32
        .map(|metric| ImpactMetricEnv::new(metric, app_name.clone(), environment.clone()))
5✔
33
        .collect()
5✔
34
}
5✔
35

36
fn group_by_key(
5✔
37
    impact_metrics: Vec<ImpactMetricEnv>,
5✔
38
) -> HashMap<ImpactMetricsKey, Vec<ImpactMetricEnv>> {
5✔
39
    impact_metrics
5✔
40
        .into_iter()
5✔
41
        .chunk_by(|m| ImpactMetricsKey::from(m.clone()))
5✔
42
        .into_iter()
5✔
43
        .map(|(k, group)| (k, group.collect()))
5✔
44
        .collect()
5✔
45
}
5✔
46

47
fn index_by_name(metrics: Vec<ImpactMetricEnv>) -> HashMap<String, ImpactMetricEnv> {
5✔
48
    metrics
5✔
49
        .into_iter()
5✔
50
        .map(|metric| (metric.impact_metric.name.clone(), metric))
5✔
51
        .collect()
5✔
52
}
5✔
53

54
fn reduce_metrics_samples(metric: ImpactMetricEnv) -> ImpactMetricEnv {
5✔
55
    let empty_metric = ImpactMetricEnv::new(
5✔
56
        ImpactMetric {
5✔
57
            name: metric.impact_metric.name.clone(),
5✔
58
            help: metric.impact_metric.help.clone(),
5✔
59
            r#type: metric.impact_metric.r#type.clone(),
5✔
60
            samples: vec![],
5✔
61
        },
5✔
62
        metric.app_name.clone(),
5✔
63
        metric.environment.clone(),
5✔
64
    );
5✔
65

5✔
66
    empty_metric.merge(metric)
5✔
67
}
5✔
68

69
pub fn merge_impact_metrics(metrics: Vec<ImpactMetricEnv>) -> Vec<ImpactMetricEnv> {
12✔
70
    if metrics.is_empty() {
12✔
71
        return Vec::new();
9✔
72
    }
3✔
73

3✔
74
    let mut merged_metrics: HashMap<String, ImpactMetricEnv> =
3✔
75
        HashMap::with_capacity(metrics.len());
3✔
76

77
    for metric in metrics {
7✔
78
        let metric_name = metric.impact_metric.name.clone();
4✔
79

80
        if let Some(existing_metric) = merged_metrics.get_mut(&metric_name) {
4✔
81
            let merged = existing_metric.clone().merge(metric);
1✔
82
            *existing_metric = merged;
1✔
83
        } else {
3✔
84
            merged_metrics.insert(metric_name, metric);
3✔
85
        }
3✔
86
    }
87

88
    merged_metrics.into_values().collect()
3✔
89
}
12✔
90

91
impl MetricsCache {
92
    pub fn sink_impact_metrics(&self, impact_metrics: Vec<ImpactMetricEnv>) {
5✔
93
        let metrics_by_key = group_by_key(impact_metrics);
5✔
94

95
        for (key, metrics) in metrics_by_key {
10✔
96
            let existing_metrics = self
5✔
97
                .impact_metrics
5✔
98
                .get(&key)
5✔
99
                .map(|m| m.value().clone())
5✔
100
                .unwrap_or_default();
5✔
101

5✔
102
            let mut aggregated_metrics = index_by_name(existing_metrics);
5✔
103

104
            for metric in metrics {
10✔
105
                let reduced_metric = reduce_metrics_samples(metric);
5✔
106

NEW
107
                if let Some(existing_metric) =
×
108
                    aggregated_metrics.get_mut(&reduced_metric.impact_metric.name)
5✔
NEW
109
                {
×
110
                    let merged = existing_metric.clone().merge(reduced_metric);
×
111
                    *existing_metric = merged;
×
112
                } else {
5✔
113
                    aggregated_metrics
5✔
114
                        .insert(reduced_metric.impact_metric.name.clone(), reduced_metric);
5✔
115
                }
5✔
116
            }
117

118
            self.impact_metrics
5✔
119
                .insert(key, aggregated_metrics.into_values().collect());
5✔
120
        }
121
    }
5✔
122
}
123

124
#[cfg(test)]
125
mod test {
126
    use super::*;
127
    use crate::metrics::client_metrics::MetricsCache;
128
    use chrono::Utc;
129
    use std::collections::HashMap;
130
    use unleash_types::client_metrics::{ClientMetricsEnv, MetricsMetadata};
131

132
    fn create_sample(
9✔
133
        value: f64,
9✔
134
        labels: HashMap<String, String>,
9✔
135
    ) -> unleash_types::client_metrics::MetricSample {
9✔
136
        unleash_types::client_metrics::MetricSample {
9✔
137
            value,
9✔
138
            labels: Some(labels),
9✔
139
        }
9✔
140
    }
9✔
141

142
    fn create_impact_metric(
6✔
143
        name: &str,
6✔
144
        r#type: &str,
6✔
145
        samples: Vec<unleash_types::client_metrics::MetricSample>,
6✔
146
    ) -> ImpactMetric {
6✔
147
        ImpactMetric {
6✔
148
            name: name.into(),
6✔
149
            help: format!("Test {} metric", r#type).into(),
6✔
150
            r#type: r#type.into(),
6✔
151
            samples,
6✔
152
        }
6✔
153
    }
6✔
154

155
    fn create_test_labels(key: &str, value: &str) -> HashMap<String, String> {
2✔
156
        HashMap::from([(key.into(), value.into())])
2✔
157
    }
2✔
158

159
    fn create_and_sink_impact_metrics(
2✔
160
        cache: &MetricsCache,
2✔
161
        app_name: &str,
2✔
162
        env: &str,
2✔
163
        metric_name: &str,
2✔
164
        metric_type: &str,
2✔
165
        value: f64,
2✔
166
    ) {
2✔
167
        let labels = HashMap::from([("env".into(), env.into())]);
2✔
168
        let impact_metrics = vec![ImpactMetricEnv::new(
2✔
169
            create_impact_metric(metric_name, metric_type, vec![create_sample(value, labels)]),
2✔
170
            app_name.into(),
2✔
171
            env.into(),
2✔
172
        )];
2✔
173
        cache.sink_impact_metrics(impact_metrics);
2✔
174
    }
2✔
175

176
    fn create_client_metrics(
2✔
177
        app_name: &str,
2✔
178
        feature_name: &str,
2✔
179
        environment: &str,
2✔
180
        yes: u32,
2✔
181
        no: u32,
2✔
182
    ) -> ClientMetricsEnv {
2✔
183
        ClientMetricsEnv {
2✔
184
            app_name: app_name.into(),
2✔
185
            feature_name: feature_name.into(),
2✔
186
            environment: environment.into(),
2✔
187
            timestamp: Utc::now(),
2✔
188
            yes,
2✔
189
            no,
2✔
190
            variants: HashMap::new(),
2✔
191
            metadata: MetricsMetadata {
2✔
192
                platform_name: None,
2✔
193
                platform_version: None,
2✔
194
                sdk_version: None,
2✔
195
                sdk_type: None,
2✔
196
                yggdrasil_version: None,
2✔
197
            },
2✔
198
        }
2✔
199
    }
2✔
200

201
    #[test]
202
    pub fn sink_impact_metrics_aggregates_correctly() {
1✔
203
        // Setup
1✔
204
        let cache = MetricsCache::default();
1✔
205
        let app = "test_app";
1✔
206
        let env = "test_env";
1✔
207
        let test_key = ImpactMetricsKey {
1✔
208
            app_name: app.into(),
1✔
209
            environment: env.into(),
1✔
210
        };
1✔
211

1✔
212
        let labels1 = create_test_labels("label1", "value1");
1✔
213
        let labels2 = create_test_labels("label1", "different");
1✔
214

1✔
215
        let counter_metrics = vec![create_impact_metric(
1✔
216
            "test_counter",
1✔
217
            "counter",
1✔
218
            vec![
1✔
219
                create_sample(1.0, labels1.clone()),
1✔
220
                create_sample(2.0, labels1.clone()),
1✔
221
                create_sample(3.0, labels2.clone()),
1✔
222
            ],
1✔
223
        )];
1✔
224

1✔
225
        let gauge_metrics = vec![create_impact_metric(
1✔
226
            "test_gauge",
1✔
227
            "gauge",
1✔
228
            vec![
1✔
229
                create_sample(1.0, labels1.clone()),
1✔
230
                create_sample(2.0, labels1.clone()),
1✔
231
            ],
1✔
232
        )];
1✔
233

1✔
234
        cache.sink_impact_metrics(convert_to_impact_metrics_env(
1✔
235
            counter_metrics,
1✔
236
            app.into(),
1✔
237
            env.into(),
1✔
238
        ));
1✔
239
        cache.sink_impact_metrics(convert_to_impact_metrics_env(
1✔
240
            gauge_metrics,
1✔
241
            app.into(),
1✔
242
            env.into(),
1✔
243
        ));
1✔
244

1✔
245
        let aggregated_metrics = cache.impact_metrics.get(&test_key).unwrap();
1✔
246
        let counter = aggregated_metrics
1✔
247
            .value()
1✔
248
            .iter()
1✔
249
            .find(|m| m.impact_metric.name == "test_counter")
1✔
250
            .unwrap();
1✔
251

1✔
252
        let value1_sample = counter
1✔
253
            .impact_metric
1✔
254
            .samples
1✔
255
            .iter()
1✔
256
            .find(|s| s.labels.as_ref().unwrap().get("label1") == Some(&"value1".into()))
2✔
257
            .unwrap();
1✔
258
        assert_eq!(value1_sample.value, 3.0, "Counter values should be summed");
1✔
259

260
        let gauge = aggregated_metrics
1✔
261
            .value()
1✔
262
            .iter()
1✔
263
            .find(|m| m.impact_metric.name == "test_gauge")
2✔
264
            .unwrap();
1✔
265
        assert_eq!(
1✔
266
            gauge.impact_metric.samples[0].value, 2.0,
1✔
NEW
267
            "Gauge should have the last value"
×
268
        );
269
    }
1✔
270

271
    #[test]
272
    pub fn merge_impact_metrics_from_different_apps() {
1✔
273
        let cache = MetricsCache::default();
1✔
274
        let env = "default";
1✔
275

1✔
276
        let app1 = "app1";
1✔
277
        let app1_key = ImpactMetricsKey {
1✔
278
            app_name: app1.into(),
1✔
279
            environment: env.into(),
1✔
280
        };
1✔
281
        let app1_labels = HashMap::from([("appName".into(), "my-application-1".into())]);
1✔
282
        let app1_metrics = vec![create_impact_metric(
1✔
283
            "test",
1✔
284
            "counter",
1✔
285
            vec![create_sample(10.0, app1_labels)],
1✔
286
        )];
1✔
287

1✔
288
        let app2 = "app2";
1✔
289
        let app2_key = ImpactMetricsKey {
1✔
290
            app_name: app2.into(),
1✔
291
            environment: env.into(),
1✔
292
        };
1✔
293
        let app2_labels = HashMap::from([("appName".into(), "my-application-2".into())]);
1✔
294
        let app2_metrics = vec![create_impact_metric(
1✔
295
            "test",
1✔
296
            "counter",
1✔
297
            vec![create_sample(1.0, app2_labels)],
1✔
298
        )];
1✔
299

1✔
300
        cache.impact_metrics.insert(
1✔
301
            app1_key,
1✔
302
            convert_to_impact_metrics_env(app1_metrics, app1.into(), env.into()),
1✔
303
        );
1✔
304
        cache.impact_metrics.insert(
1✔
305
            app2_key,
1✔
306
            convert_to_impact_metrics_env(app2_metrics, app2.into(), env.into()),
1✔
307
        );
1✔
308

1✔
309
        let mut all_impact_metrics = Vec::new();
1✔
310
        for entry in cache.impact_metrics.iter() {
2✔
311
            all_impact_metrics.extend(entry.value().clone());
2✔
312
        }
2✔
313
        let merged_impact_metrics = merge_impact_metrics(all_impact_metrics);
1✔
314

1✔
315
        assert_eq!(
1✔
316
            merged_impact_metrics.len(),
1✔
317
            1,
NEW
318
            "Should have one merged metric"
×
319
        );
320
        let test_metric = &merged_impact_metrics[0];
1✔
321

1✔
322
        let app1_value = test_metric
1✔
323
            .impact_metric
1✔
324
            .samples
1✔
325
            .iter()
1✔
326
            .find(|s| s.labels.as_ref().unwrap().get("appName") == Some(&"my-application-1".into()))
1✔
327
            .unwrap()
1✔
328
            .value;
1✔
329
        let app2_value = test_metric
1✔
330
            .impact_metric
1✔
331
            .samples
1✔
332
            .iter()
1✔
333
            .find(|s| s.labels.as_ref().unwrap().get("appName") == Some(&"my-application-2".into()))
2✔
334
            .unwrap()
1✔
335
            .value;
1✔
336

1✔
337
        assert_eq!(app1_value, 10.0, "App1 sample value should be preserved");
1✔
338
        assert_eq!(app2_value, 1.0, "App2 sample value should be preserved");
1✔
339
    }
1✔
340

341
    #[test]
342
    pub fn get_metrics_by_environment_handles_metrics_and_impact_metrics_independently() {
1✔
343
        let cache = MetricsCache::default();
1✔
344

1✔
345
        // 1. Development: only regular metrics
1✔
346
        let dev_env = "development";
1✔
347
        let dev_app = "dev-app";
1✔
348
        cache.sink_metrics(&[create_client_metrics(dev_app, "feature", dev_env, 5, 2)]);
1✔
349

1✔
350
        // 2. Production: only impact metrics
1✔
351
        let prod_env = "production";
1✔
352
        let prod_app = "prod-app";
1✔
353
        create_and_sink_impact_metrics(&cache, prod_app, prod_env, "counter", "counter", 10.0);
1✔
354

1✔
355
        // 3. Staging: both regular and impact metrics
1✔
356
        let staging_env = "staging";
1✔
357
        let staging_app = "staging-app";
1✔
358
        cache.sink_metrics(&[create_client_metrics(
1✔
359
            staging_app,
1✔
360
            "feature",
1✔
361
            staging_env,
1✔
362
            3,
1✔
363
            1,
1✔
364
        )]);
1✔
365
        create_and_sink_impact_metrics(&cache, staging_app, staging_env, "gauge", "gauge", 42.0);
1✔
366

1✔
367
        let batches = cache.get_metrics_by_environment();
1✔
368

1✔
369
        let dev_batch = &batches[dev_env];
1✔
370
        assert_eq!(dev_batch.metrics.len(), 1);
1✔
371
        assert_eq!(dev_batch.impact_metrics.len(), 0);
1✔
372

373
        let prod_batch = &batches[prod_env];
1✔
374
        assert_eq!(prod_batch.metrics.len(), 0);
1✔
375
        assert_eq!(prod_batch.impact_metrics[0].impact_metric.name, "counter");
1✔
376

377
        let staging_batch = &batches[staging_env];
1✔
378
        assert_eq!(staging_batch.metrics.len(), 1);
1✔
379
        assert_eq!(staging_batch.impact_metrics[0].impact_metric.name, "gauge");
1✔
380
    }
1✔
381
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc