• 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

93.53
/server/src/metrics/client_metrics.rs
1
use crate::metrics::client_impact_metrics::{
2
    ImpactMetricsKey, convert_to_impact_metrics_env, merge_impact_metrics,
3
};
4
use crate::metrics::metric_batching::{cut_into_sendable_batches, sendable, size_of_batch};
5
use crate::types::{BatchMetricsRequestBody, EdgeToken};
6
use actix_web::web::Data;
7
use chrono::{DateTime, Utc};
8
use dashmap::DashMap;
9
use itertools::Itertools;
10
use lazy_static::lazy_static;
11
use prometheus::{Histogram, IntCounterVec, register_histogram, register_int_counter_vec};
12
use serde::{Deserialize, Serialize};
13
use std::{
14
    collections::HashMap,
15
    hash::{Hash, Hasher},
16
};
17
use tracing::debug;
18
use unleash_types::client_metrics::SdkType::Backend;
19
use unleash_types::client_metrics::{
20
    ClientApplication, ClientMetrics, ClientMetricsEnv, ConnectVia, ImpactMetricEnv,
21
    MetricsMetadata,
22
};
23
use utoipa::ToSchema;
24

25
lazy_static! {
26
    pub static ref METRICS_SIZE_HISTOGRAM: Histogram = register_histogram!(
27
        "metrics_size_in_bytes",
28
        "Size of metrics when posting",
29
        vec![
30
            1000.0, 10000.0, 20000.0, 50000.0, 75000.0, 100000.0, 250000.0, 500000.0, 1000000.0
31
        ]
32
    )
33
    .unwrap();
34
    pub static ref FEATURE_TOGGLE_USAGE_TOTAL: IntCounterVec = register_int_counter_vec!(
35
        "feature_toggle_usage_total",
36
        "Number of times a feature flag has been used",
37
        &["appName", "toggle", "active"]
38
    )
39
    .unwrap();
40
}
41

42
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
43
pub(crate) struct ApplicationKey {
44
    pub app_name: String,
45
    pub instance_id: String,
46
}
47

48
impl From<ClientApplication> for ApplicationKey {
49
    fn from(value: ClientApplication) -> Self {
6,512✔
50
        Self {
6,512✔
51
            app_name: value.app_name,
6,512✔
52
            instance_id: value.instance_id.unwrap_or_else(|| "default".into()),
6,512✔
53
        }
6,512✔
54
    }
6,512✔
55
}
56

57
impl From<ClientMetricsEnv> for MetricsKey {
58
    fn from(value: ClientMetricsEnv) -> Self {
16,101✔
59
        Self {
16,101✔
60
            app_name: value.app_name,
16,101✔
61
            feature_name: value.feature_name,
16,101✔
62
            timestamp: value.timestamp,
16,101✔
63
            environment: value.environment,
16,101✔
64
        }
16,101✔
65
    }
16,101✔
66
}
67

68
#[derive(Debug, Clone, Eq, Deserialize, Serialize, ToSchema)]
×
69
pub struct MetricsKey {
70
    pub app_name: String,
71
    pub feature_name: String,
72
    pub environment: String,
73
    pub timestamp: DateTime<Utc>,
74
}
75

76
impl Hash for MetricsKey {
77
    fn hash<H: Hasher>(&self, state: &mut H) {
55,302✔
78
        self.app_name.hash(state);
55,302✔
79
        self.feature_name.hash(state);
55,302✔
80
        self.environment.hash(state);
55,302✔
81
        to_time_key(&self.timestamp).hash(state);
55,302✔
82
    }
55,302✔
83
}
84

85
fn to_time_key(timestamp: &DateTime<Utc>) -> String {
90,162✔
86
    format!("{}", timestamp.format("%Y-%m-%d %H"))
90,162✔
87
}
90,162✔
88

89
impl PartialEq for MetricsKey {
90
    fn eq(&self, other: &Self) -> bool {
17,430✔
91
        let other_hour_bin = to_time_key(&other.timestamp);
17,430✔
92
        let self_hour_bin = to_time_key(&self.timestamp);
17,430✔
93

17,430✔
94
        self.app_name == other.app_name
17,430✔
95
            && self.feature_name == other.feature_name
16,112✔
96
            && self.environment == other.environment
16,112✔
97
            && self_hour_bin == other_hour_bin
16,112✔
98
    }
17,430✔
99
}
100

101
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
102
pub struct MetricsBatch {
103
    pub applications: Vec<ClientApplication>,
104
    pub metrics: Vec<ClientMetricsEnv>,
105
    #[serde(
106
        default,
107
        skip_serializing_if = "Vec::is_empty",
108
        rename = "impactMetrics"
109
    )]
110
    pub impact_metrics: Vec<ImpactMetricEnv>,
111
}
112

113
#[derive(Default, Debug)]
114
pub struct MetricsCache {
115
    pub(crate) applications: DashMap<ApplicationKey, ClientApplication>,
116
    pub(crate) metrics: DashMap<MetricsKey, ClientMetricsEnv>,
117
    pub(crate) impact_metrics: DashMap<ImpactMetricsKey, Vec<ImpactMetricEnv>>,
118
}
119

120
pub(crate) fn register_client_application(
7✔
121
    edge_token: EdgeToken,
7✔
122
    connect_via: &ConnectVia,
7✔
123
    client_application: ClientApplication,
7✔
124
    metrics_cache: Data<MetricsCache>,
7✔
125
) {
7✔
126
    let updated_with_connection_info = client_application.connect_via(
7✔
127
        connect_via.app_name.as_str(),
7✔
128
        connect_via.instance_id.as_str(),
7✔
129
    );
7✔
130
    let to_write = ClientApplication {
7✔
131
        environment: edge_token.environment,
7✔
132
        projects: Some(edge_token.projects),
7✔
133
        metadata: MetricsMetadata {
7✔
134
            sdk_type: Some(Backend),
7✔
135
            ..updated_with_connection_info.metadata
7✔
136
        },
7✔
137
        ..updated_with_connection_info
7✔
138
    };
7✔
139
    metrics_cache.applications.insert(
7✔
140
        ApplicationKey {
7✔
141
            app_name: to_write.app_name.clone(),
7✔
142
            instance_id: to_write
7✔
143
                .instance_id
7✔
144
                .clone()
7✔
145
                .unwrap_or_else(|| ulid::Ulid::new().to_string()),
7✔
146
        },
7✔
147
        to_write,
7✔
148
    );
7✔
149
}
7✔
150

151
pub(crate) fn register_client_metrics(
4✔
152
    edge_token: EdgeToken,
4✔
153
    metrics: ClientMetrics,
4✔
154
    metrics_cache: Data<MetricsCache>,
4✔
155
) {
4✔
156
    let environment = edge_token
4✔
157
        .environment
4✔
158
        .clone()
4✔
159
        .unwrap_or_else(|| "development".into());
4✔
160

4✔
161
    let client_metrics_env = unleash_types::client_metrics::from_bucket_app_name_and_env(
4✔
162
        metrics.bucket,
4✔
163
        metrics.app_name.clone(),
4✔
164
        environment.clone(),
4✔
165
        metrics.metadata.clone(),
4✔
166
    );
4✔
167

168
    if let Some(impact_metrics) = metrics.impact_metrics {
4✔
169
        let impact_metrics_env =
1✔
170
            convert_to_impact_metrics_env(impact_metrics, metrics.app_name.clone(), environment);
1✔
171
        metrics_cache.sink_impact_metrics(impact_metrics_env);
1✔
172
    }
3✔
173

174
    metrics_cache.sink_metrics(&client_metrics_env);
4✔
175
}
4✔
176

177
/***
178
   Will filter out metrics that do not belong to the environment that edge_token has access to
179
*/
180
pub(crate) fn register_bulk_metrics(
3✔
181
    metrics_cache: &MetricsCache,
3✔
182
    connect_via: &ConnectVia,
3✔
183
    edge_token: &EdgeToken,
3✔
184
    metrics: BatchMetricsRequestBody,
3✔
185
) {
3✔
186
    let updated: BatchMetricsRequestBody = BatchMetricsRequestBody {
3✔
187
        applications: metrics.applications.clone(),
3✔
188
        metrics: metrics
3✔
189
            .metrics
3✔
190
            .iter()
3✔
191
            .filter(|m| {
3✔
192
                edge_token
3✔
193
                    .environment
3✔
194
                    .clone()
3✔
195
                    .map(|e| e == m.environment)
3✔
196
                    .unwrap_or(false)
3✔
197
            })
3✔
198
            .cloned()
3✔
199
            .collect(),
3✔
200
        impact_metrics: metrics.impact_metrics.clone(),
3✔
201
    };
3✔
202
    metrics_cache.sink_bulk_metrics(updated, connect_via);
3✔
203
}
3✔
204

205
impl MetricsCache {
206
    pub fn get_metrics_by_environment(&self) -> HashMap<String, MetricsBatch> {
2✔
207
        let mut batches_by_environment = HashMap::new();
2✔
208

2✔
209
        let applications = self
2✔
210
            .applications
2✔
211
            .iter()
2✔
212
            .map(|e| e.value().clone())
2✔
213
            .collect::<Vec<ClientApplication>>();
2✔
214

2✔
215
        let mut all_environments = std::collections::HashSet::new();
2✔
216

217
        for entry in self.metrics.iter() {
4✔
218
            all_environments.insert(entry.value().environment.clone());
4✔
219
        }
4✔
220

221
        for entry in self.impact_metrics.iter() {
2✔
222
            all_environments.insert(entry.key().environment.clone());
2✔
223
        }
2✔
224

225
        let data = self
2✔
226
            .metrics
2✔
227
            .iter()
2✔
228
            .map(|e| e.value().clone())
4✔
229
            .collect::<Vec<ClientMetricsEnv>>();
2✔
230
        let metrics_by_env: HashMap<String, Vec<ClientMetricsEnv>> = data
2✔
231
            .into_iter()
2✔
232
            .into_group_map_by(|metric| metric.environment.clone());
4✔
233

234
        for environment in all_environments {
7✔
235
            let metrics = metrics_by_env
5✔
236
                .get(&environment)
5✔
237
                .cloned()
5✔
238
                .unwrap_or_default();
5✔
239

5✔
240
            let mut all_impact_metrics = Vec::new();
5✔
241
            for entry in self.impact_metrics.iter() {
6✔
242
                let key = entry.key();
6✔
243
                if key.environment == environment {
6✔
244
                    all_impact_metrics.extend(entry.value().clone());
2✔
245
                }
4✔
246
            }
247

248
            let merged_impact_metrics = merge_impact_metrics(all_impact_metrics);
5✔
249

5✔
250
            let batch = MetricsBatch {
5✔
251
                applications: applications.clone(),
5✔
252
                metrics,
5✔
253
                impact_metrics: merged_impact_metrics,
5✔
254
            };
5✔
255
            batches_by_environment.insert(environment, batch);
5✔
256
        }
257
        batches_by_environment
2✔
258
    }
2✔
259

260
    pub fn get_appropriately_sized_env_batches(&self, batch: &MetricsBatch) -> Vec<MetricsBatch> {
×
261
        for app in batch.applications.clone() {
×
262
            self.applications.remove(&ApplicationKey::from(app.clone()));
×
263
        }
×
264

265
        for impact_metric in batch.impact_metrics.clone() {
×
NEW
266
            self.impact_metrics
×
NEW
267
                .remove(&ImpactMetricsKey::from(impact_metric.clone()));
×
268
        }
×
269

270
        for metric in batch.metrics.clone() {
×
271
            self.metrics.remove(&MetricsKey::from(metric.clone()));
×
272
        }
×
273
        METRICS_SIZE_HISTOGRAM.observe(size_of_batch(batch) as f64);
×
274
        if sendable(batch) {
×
275
            vec![batch.clone()]
×
276
        } else {
277
            debug!(
×
278
                "We have {} applications and {} metrics",
×
279
                batch.applications.len(),
×
280
                batch.metrics.len()
×
281
            );
282
            cut_into_sendable_batches(batch.clone())
×
283
        }
284
    }
×
285
    /// This is a destructive call. We'll remove all metrics that is due for posting
286
    /// Called from [crate::http::background_send_metrics::send_metrics_task] which will reinsert on 5xx server failures, but leave 413 and 400 failures on the floor
287
    pub fn get_appropriately_sized_batches(&self) -> Vec<MetricsBatch> {
6✔
288
        let impact_keys: Vec<ImpactMetricsKey> = self
6✔
289
            .impact_metrics
6✔
290
            .iter()
6✔
291
            .map(|e| e.key().clone())
6✔
292
            .collect();
6✔
293

6✔
294
        let mut all_impact_metrics = Vec::new();
6✔
295
        for entry in self.impact_metrics.iter() {
6✔
296
            all_impact_metrics.extend(entry.value().clone());
×
297
        }
×
298

299
        let merged_impact_metrics = merge_impact_metrics(all_impact_metrics);
6✔
300

6✔
301
        let batch = MetricsBatch {
6✔
302
            applications: self
6✔
303
                .applications
6✔
304
                .iter()
6✔
305
                .map(|e| e.value().clone())
6,511✔
306
                .collect(),
6✔
307
            metrics: self
6✔
308
                .metrics
6✔
309
                .iter()
6✔
310
                .map(|e| e.value().clone())
16,102✔
311
                .filter(|m| m.yes > 0 || m.no > 0) // Makes sure that we only return buckets that have values. We should have a test for this :P
16,102✔
312
                .collect(),
6✔
313
            impact_metrics: merged_impact_metrics,
6✔
314
        };
6✔
315
        for app in batch.applications.clone() {
6,511✔
316
            self.applications.remove(&ApplicationKey::from(app.clone()));
6,511✔
317
        }
6,511✔
318

319
        for key in &impact_keys {
6✔
320
            self.impact_metrics.remove(key);
×
321
        }
×
322

323
        for metric in batch.metrics.clone() {
16,101✔
324
            self.metrics.remove(&MetricsKey::from(metric.clone()));
16,101✔
325
        }
16,101✔
326
        METRICS_SIZE_HISTOGRAM.observe(size_of_batch(&batch) as f64);
6✔
327
        if sendable(&batch) {
6✔
328
            vec![batch]
2✔
329
        } else {
330
            debug!(
4✔
331
                "We have {} applications and {} metrics",
×
332
                batch.applications.len(),
×
333
                batch.metrics.len()
×
334
            );
335
            cut_into_sendable_batches(batch)
4✔
336
        }
337
    }
6✔
338

339
    pub fn reinsert_batch(&self, batch: MetricsBatch) {
×
340
        for application in batch.applications {
×
341
            self.register_application(application);
×
342
        }
×
343

344
        self.sink_impact_metrics(batch.impact_metrics.clone());
×
345

×
346
        self.sink_metrics(&batch.metrics);
×
347
    }
×
348

349
    pub fn sink_bulk_metrics(&self, metrics: BatchMetricsRequestBody, connect_via: &ConnectVia) {
3✔
350
        for application in metrics.applications {
4✔
351
            self.register_application(
1✔
352
                application.connect_via(&connect_via.app_name, &connect_via.instance_id),
1✔
353
            )
1✔
354
        }
355

356
        // TODO: sink impact metrics
357

358
        self.sink_metrics(&metrics.metrics)
3✔
359
    }
3✔
360

361
    pub fn reset_metrics(&self) {
1✔
362
        self.applications.clear();
1✔
363
        self.metrics.clear();
1✔
364
        self.impact_metrics.clear();
1✔
365
    }
1✔
366

367
    pub fn register_application(&self, application: ClientApplication) {
1✔
368
        self.applications
1✔
369
            .insert(ApplicationKey::from(application.clone()), application);
1✔
370
    }
1✔
371

372
    pub fn sink_metrics(&self, metrics: &[ClientMetricsEnv]) {
19✔
373
        debug!("Sinking {} metrics", metrics.len());
19✔
374
        for metric in metrics.iter() {
16,119✔
375
            FEATURE_TOGGLE_USAGE_TOTAL
16,119✔
376
                .with_label_values(&[
16,119✔
377
                    metric.app_name.clone(),
16,119✔
378
                    metric.feature_name.clone(),
16,119✔
379
                    "true".to_string(),
16,119✔
380
                ])
16,119✔
381
                .inc_by(metric.yes as u64);
16,119✔
382
            FEATURE_TOGGLE_USAGE_TOTAL
16,119✔
383
                .with_label_values(&[
16,119✔
384
                    metric.app_name.clone(),
16,119✔
385
                    metric.feature_name.clone(),
16,119✔
386
                    "false".to_string(),
16,119✔
387
                ])
16,119✔
388
                .inc_by(metric.no as u64);
16,119✔
389
            self.metrics
16,119✔
390
                .entry(MetricsKey {
16,119✔
391
                    app_name: metric.app_name.clone(),
16,119✔
392
                    feature_name: metric.feature_name.clone(),
16,119✔
393
                    timestamp: metric.timestamp,
16,119✔
394
                    environment: metric.environment.clone(),
16,119✔
395
                })
16,119✔
396
                .and_modify(|feature_stats| {
16,119✔
397
                    feature_stats.yes += metric.yes;
5✔
398
                    feature_stats.no += metric.no;
5✔
399
                    metric.variants.iter().for_each(|(k, added_count)| {
5✔
400
                        feature_stats
×
401
                            .variants
×
402
                            .entry(k.clone())
×
403
                            .and_modify(|count| {
×
404
                                *count += added_count;
×
405
                            })
×
406
                            .or_insert(*added_count);
×
407
                    });
5✔
408
                })
16,119✔
409
                .or_insert_with(|| metric.clone());
16,119✔
410
        }
16,119✔
411
    }
19✔
412
}
413

414
#[cfg(test)]
415
mod test {
416
    use super::*;
417
    use crate::types::{TokenType, TokenValidationStatus};
418
    use chrono::{DateTime, Utc};
419
    use std::collections::HashMap;
420
    use std::str::FromStr;
421
    use unleash_types::client_metrics::{
422
        ClientMetricsEnv, ConnectVia, ConnectViaBuilder, MetricsMetadata,
423
    };
424

425
    #[test]
426
    fn cache_aggregates_data_correctly() {
1✔
427
        let cache = MetricsCache::default();
1✔
428

1✔
429
        let base_metric = ClientMetricsEnv {
1✔
430
            app_name: "some-app".into(),
1✔
431
            feature_name: "some-feature".into(),
1✔
432
            environment: "development".into(),
1✔
433
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
434
                .unwrap()
1✔
435
                .with_timezone(&Utc),
1✔
436
            yes: 1,
1✔
437
            no: 0,
1✔
438
            variants: HashMap::new(),
1✔
439
            metadata: MetricsMetadata {
1✔
440
                platform_name: None,
1✔
441
                platform_version: None,
1✔
442
                sdk_version: None,
1✔
443
                sdk_type: None,
1✔
444
                yggdrasil_version: None,
1✔
445
            },
1✔
446
        };
1✔
447

1✔
448
        let metrics = vec![
1✔
449
            ClientMetricsEnv {
1✔
450
                ..base_metric.clone()
1✔
451
            },
1✔
452
            ClientMetricsEnv { ..base_metric },
1✔
453
        ];
1✔
454

1✔
455
        cache.sink_metrics(&metrics);
1✔
456

1✔
457
        let found_metric = cache
1✔
458
            .metrics
1✔
459
            .get(&MetricsKey {
1✔
460
                app_name: "some-app".into(),
1✔
461
                feature_name: "some-feature".into(),
1✔
462
                timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
463
                    .unwrap()
1✔
464
                    .with_timezone(&Utc),
1✔
465
                environment: "development".into(),
1✔
466
            })
1✔
467
            .unwrap();
1✔
468

1✔
469
        let expected = ClientMetricsEnv {
1✔
470
            app_name: "some-app".into(),
1✔
471
            feature_name: "some-feature".into(),
1✔
472
            environment: "development".into(),
1✔
473
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
474
                .unwrap()
1✔
475
                .with_timezone(&Utc),
1✔
476
            yes: 2,
1✔
477
            no: 0,
1✔
478
            variants: HashMap::new(),
1✔
479
            metadata: MetricsMetadata {
1✔
480
                platform_name: None,
1✔
481
                platform_version: None,
1✔
482
                sdk_version: None,
1✔
483
                sdk_type: None,
1✔
484
                yggdrasil_version: None,
1✔
485
            },
1✔
486
        };
1✔
487

1✔
488
        assert_eq!(found_metric.yes, expected.yes);
1✔
489
        assert_eq!(found_metric.yes, 2);
1✔
490
        assert_eq!(found_metric.no, 0);
1✔
491
        assert_eq!(found_metric.no, expected.no);
1✔
492
    }
1✔
493

494
    #[test]
495
    fn cache_aggregates_data_correctly_across_date_boundaries() {
1✔
496
        let cache = MetricsCache::default();
1✔
497
        let a_long_time_ago = DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
498
            .unwrap()
1✔
499
            .with_timezone(&Utc);
1✔
500
        let hundred_years_later = DateTime::parse_from_rfc3339("1967-11-07T12:00:00Z")
1✔
501
            .unwrap()
1✔
502
            .with_timezone(&Utc);
1✔
503

1✔
504
        let base_metric = ClientMetricsEnv {
1✔
505
            app_name: "some-app".into(),
1✔
506
            feature_name: "some-feature".into(),
1✔
507
            environment: "development".into(),
1✔
508
            timestamp: a_long_time_ago,
1✔
509
            yes: 1,
1✔
510
            no: 0,
1✔
511
            variants: HashMap::new(),
1✔
512
            metadata: MetricsMetadata {
1✔
513
                platform_name: None,
1✔
514
                platform_version: None,
1✔
515
                sdk_version: None,
1✔
516
                sdk_type: None,
1✔
517
                yggdrasil_version: None,
1✔
518
            },
1✔
519
        };
1✔
520

1✔
521
        let metrics = vec![
1✔
522
            ClientMetricsEnv {
1✔
523
                timestamp: hundred_years_later,
1✔
524
                ..base_metric.clone()
1✔
525
            },
1✔
526
            ClientMetricsEnv {
1✔
527
                ..base_metric.clone()
1✔
528
            },
1✔
529
            ClientMetricsEnv { ..base_metric },
1✔
530
        ];
1✔
531

1✔
532
        cache.sink_metrics(&metrics);
1✔
533

1✔
534
        let old_metric = cache
1✔
535
            .metrics
1✔
536
            .get(&MetricsKey {
1✔
537
                app_name: "some-app".into(),
1✔
538
                feature_name: "some-feature".into(),
1✔
539
                environment: "development".into(),
1✔
540
                timestamp: a_long_time_ago,
1✔
541
            })
1✔
542
            .unwrap();
1✔
543

1✔
544
        let old_expectation = ClientMetricsEnv {
1✔
545
            app_name: "some-app".into(),
1✔
546
            feature_name: "some-feature".into(),
1✔
547
            environment: "development".into(),
1✔
548
            timestamp: a_long_time_ago,
1✔
549
            yes: 2,
1✔
550
            no: 0,
1✔
551
            variants: HashMap::new(),
1✔
552
            metadata: MetricsMetadata {
1✔
553
                platform_name: None,
1✔
554
                platform_version: None,
1✔
555
                sdk_version: None,
1✔
556
                sdk_type: None,
1✔
557
                yggdrasil_version: None,
1✔
558
            },
1✔
559
        };
1✔
560

1✔
561
        let new_metric = cache
1✔
562
            .metrics
1✔
563
            .get(&MetricsKey {
1✔
564
                app_name: "some-app".into(),
1✔
565
                feature_name: "some-feature".into(),
1✔
566
                environment: "development".into(),
1✔
567
                timestamp: hundred_years_later,
1✔
568
            })
1✔
569
            .unwrap();
1✔
570

1✔
571
        let new_expectation = ClientMetricsEnv {
1✔
572
            app_name: "some-app".into(),
1✔
573
            feature_name: "some-feature".into(),
1✔
574
            environment: "development".into(),
1✔
575
            timestamp: hundred_years_later,
1✔
576
            yes: 1,
1✔
577
            no: 0,
1✔
578
            variants: HashMap::new(),
1✔
579
            metadata: MetricsMetadata {
1✔
580
                platform_name: None,
1✔
581
                platform_version: None,
1✔
582
                sdk_version: None,
1✔
583
                sdk_type: None,
1✔
584
                yggdrasil_version: None,
1✔
585
            },
1✔
586
        };
1✔
587

1✔
588
        assert_eq!(cache.metrics.len(), 2);
1✔
589

590
        assert_eq!(old_metric.yes, old_expectation.yes);
1✔
591
        assert_eq!(old_metric.yes, 2);
1✔
592
        assert_eq!(old_metric.no, 0);
1✔
593
        assert_eq!(old_metric.no, old_expectation.no);
1✔
594

595
        assert_eq!(new_metric.yes, new_expectation.yes);
1✔
596
        assert_eq!(new_metric.yes, 1);
1✔
597
        assert_eq!(new_metric.no, 0);
1✔
598
        assert_eq!(new_metric.no, new_expectation.no);
1✔
599
    }
1✔
600

601
    #[test]
602
    fn cache_clears_metrics_correctly() {
1✔
603
        let cache = MetricsCache::default();
1✔
604
        let time_stamp = DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
605
            .unwrap()
1✔
606
            .with_timezone(&Utc);
1✔
607

1✔
608
        let base_metric = ClientMetricsEnv {
1✔
609
            app_name: "some-app".into(),
1✔
610
            feature_name: "some-feature".into(),
1✔
611
            environment: "development".into(),
1✔
612
            timestamp: time_stamp,
1✔
613
            yes: 1,
1✔
614
            no: 0,
1✔
615
            variants: HashMap::new(),
1✔
616
            metadata: MetricsMetadata {
1✔
617
                platform_name: None,
1✔
618
                platform_version: None,
1✔
619
                sdk_version: None,
1✔
620
                sdk_type: None,
1✔
621
                yggdrasil_version: None,
1✔
622
            },
1✔
623
        };
1✔
624

1✔
625
        let metrics = vec![
1✔
626
            ClientMetricsEnv {
1✔
627
                ..base_metric.clone()
1✔
628
            },
1✔
629
            ClientMetricsEnv { ..base_metric },
1✔
630
        ];
1✔
631

1✔
632
        cache.sink_metrics(&metrics);
1✔
633
        assert!(!cache.metrics.is_empty());
1✔
634
        cache.reset_metrics();
1✔
635
        assert!(cache.metrics.is_empty());
1✔
636
    }
1✔
637

638
    #[test]
639
    fn adding_another_connection_link_works() {
1✔
640
        let client_application = ClientApplication {
1✔
641
            app_name: "tests_help".into(),
1✔
642
            connect_via: None,
1✔
643
            environment: Some("development".into()),
1✔
644
            projects: None,
1✔
645
            instance_id: Some("test".into()),
1✔
646
            connection_id: Some("test".into()),
1✔
647
            interval: 60,
1✔
648
            started: Default::default(),
1✔
649
            strategies: vec![],
1✔
650
            metadata: MetricsMetadata {
1✔
651
                platform_name: None,
1✔
652
                platform_version: None,
1✔
653
                sdk_version: None,
1✔
654
                sdk_type: None,
1✔
655
                yggdrasil_version: None,
1✔
656
            },
1✔
657
        };
1✔
658
        let connected_via_test_instance = client_application.connect_via("test", "instance");
1✔
659
        let connected_via_edge_as_well = connected_via_test_instance.connect_via("edge", "edgeid");
1✔
660
        assert_eq!(
1✔
661
            connected_via_test_instance.connect_via.unwrap(),
1✔
662
            vec![ConnectVia {
1✔
663
                app_name: "test".into(),
1✔
664
                instance_id: "instance".into()
1✔
665
            }]
1✔
666
        );
1✔
667
        assert_eq!(
1✔
668
            connected_via_edge_as_well.connect_via.unwrap(),
1✔
669
            vec![
1✔
670
                ConnectVia {
1✔
671
                    app_name: "test".into(),
1✔
672
                    instance_id: "instance".into()
1✔
673
                },
1✔
674
                ConnectVia {
1✔
675
                    app_name: "edge".into(),
1✔
676
                    instance_id: "edgeid".into()
1✔
677
                }
1✔
678
            ]
1✔
679
        )
1✔
680
    }
1✔
681

682
    #[test]
683
    fn getting_unsent_metrics_filters_out_metrics_with_no_counters() {
1✔
684
        let cache = MetricsCache::default();
1✔
685

1✔
686
        let base_metric = ClientMetricsEnv {
1✔
687
            app_name: "some-app".into(),
1✔
688
            feature_name: "some-feature".into(),
1✔
689
            environment: "development".into(),
1✔
690
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
691
                .unwrap()
1✔
692
                .with_timezone(&Utc),
1✔
693
            yes: 0,
1✔
694
            no: 0,
1✔
695
            variants: HashMap::new(),
1✔
696
            metadata: MetricsMetadata {
1✔
697
                platform_name: None,
1✔
698
                platform_version: None,
1✔
699
                sdk_version: None,
1✔
700
                sdk_type: None,
1✔
701
                yggdrasil_version: None,
1✔
702
            },
1✔
703
        };
1✔
704

1✔
705
        let metrics = vec![
1✔
706
            ClientMetricsEnv {
1✔
707
                ..base_metric.clone()
1✔
708
            },
1✔
709
            ClientMetricsEnv { ..base_metric },
1✔
710
        ];
1✔
711

1✔
712
        cache.sink_metrics(&metrics);
1✔
713
        let metrics_batch = cache.get_appropriately_sized_batches();
1✔
714
        assert_eq!(metrics_batch.len(), 1);
1✔
715
        assert!(metrics_batch.first().unwrap().metrics.is_empty());
1✔
716
    }
1✔
717

718
    #[test]
719
    pub fn register_bulk_metrics_filters_metrics_based_on_environment_in_token() {
1✔
720
        let metrics_cache = MetricsCache::default();
1✔
721
        let connect_via = ConnectViaBuilder::default()
1✔
722
            .app_name("edge_bulk_metrics".into())
1✔
723
            .instance_id("sometest".into())
1✔
724
            .build()
1✔
725
            .unwrap();
1✔
726
        let mut edge_token_with_development =
1✔
727
            EdgeToken::from_str("*:development.randomstring").unwrap();
1✔
728
        edge_token_with_development.status = TokenValidationStatus::Validated;
1✔
729
        edge_token_with_development.token_type = Some(TokenType::Client);
1✔
730
        let metrics = BatchMetricsRequestBody {
1✔
731
            applications: vec![],
1✔
732
            metrics: vec![
1✔
733
                ClientMetricsEnv {
1✔
734
                    feature_name: "feature_one".into(),
1✔
735
                    app_name: "my_app".into(),
1✔
736
                    environment: "development".into(),
1✔
737
                    timestamp: Utc::now(),
1✔
738
                    yes: 50,
1✔
739
                    no: 10,
1✔
740
                    variants: Default::default(),
1✔
741
                    metadata: MetricsMetadata {
1✔
742
                        platform_name: None,
1✔
743
                        platform_version: None,
1✔
744
                        sdk_version: None,
1✔
745
                        sdk_type: None,
1✔
746
                        yggdrasil_version: None,
1✔
747
                    },
1✔
748
                },
1✔
749
                ClientMetricsEnv {
1✔
750
                    feature_name: "feature_two".to_string(),
1✔
751
                    app_name: "other_app".to_string(),
1✔
752
                    environment: "production".to_string(),
1✔
753
                    timestamp: Default::default(),
1✔
754
                    yes: 50,
1✔
755
                    no: 10,
1✔
756
                    variants: Default::default(),
1✔
757
                    metadata: MetricsMetadata {
1✔
758
                        platform_name: None,
1✔
759
                        platform_version: None,
1✔
760
                        sdk_version: None,
1✔
761
                        sdk_type: None,
1✔
762
                        yggdrasil_version: None,
1✔
763
                    },
1✔
764
                },
1✔
765
            ],
1✔
766
            impact_metrics: None,
1✔
767
        };
1✔
768
        register_bulk_metrics(
1✔
769
            &metrics_cache,
1✔
770
            &connect_via,
1✔
771
            &edge_token_with_development,
1✔
772
            metrics,
1✔
773
        );
1✔
774
        assert_eq!(metrics_cache.metrics.len(), 1);
1✔
775
    }
1✔
776

777
    #[test]
778
    pub fn metrics_will_be_gathered_per_environment() {
1✔
779
        let metrics = vec![
1✔
780
            ClientMetricsEnv {
1✔
781
                feature_name: "feature_one".into(),
1✔
782
                app_name: "my_app".into(),
1✔
783
                environment: "development".into(),
1✔
784
                timestamp: Utc::now(),
1✔
785
                yes: 50,
1✔
786
                no: 10,
1✔
787
                variants: Default::default(),
1✔
788
                metadata: MetricsMetadata {
1✔
789
                    platform_name: None,
1✔
790
                    platform_version: None,
1✔
791
                    sdk_version: None,
1✔
792
                    sdk_type: None,
1✔
793
                    yggdrasil_version: None,
1✔
794
                },
1✔
795
            },
1✔
796
            ClientMetricsEnv {
1✔
797
                feature_name: "feature_two".to_string(),
1✔
798
                app_name: "other_app".to_string(),
1✔
799
                environment: "production".to_string(),
1✔
800
                timestamp: Default::default(),
1✔
801
                yes: 50,
1✔
802
                no: 10,
1✔
803
                variants: Default::default(),
1✔
804
                metadata: MetricsMetadata {
1✔
805
                    platform_name: None,
1✔
806
                    platform_version: None,
1✔
807
                    sdk_version: None,
1✔
808
                    sdk_type: None,
1✔
809
                    yggdrasil_version: None,
1✔
810
                },
1✔
811
            },
1✔
812
        ];
1✔
813
        let cache = MetricsCache::default();
1✔
814
        cache.sink_metrics(&metrics);
1✔
815
        let metrics_by_env_map = cache.get_metrics_by_environment();
1✔
816
        assert_eq!(metrics_by_env_map.len(), 2);
1✔
817
        assert!(metrics_by_env_map.contains_key("development"));
1✔
818
        assert!(metrics_by_env_map.contains_key("production"));
1✔
819
    }
1✔
820
}
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