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

Unleash / unleash-edge / 15526356487

09 Jun 2025 03:51AM UTC coverage: 78.199% (-0.06%) from 78.263%
15526356487

Pull #979

github

web-flow
Merge 54ca84f50 into a99ba5cca
Pull Request #979: chore: release v19.11.1

10151 of 12981 relevant lines covered (78.2%)

158.1 hits per line

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

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

20
pub const UPSTREAM_MAX_BODY_SIZE: usize = 100 * 1024;
21
pub const BATCH_BODY_SIZE: usize = 95 * 1024;
22

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

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

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

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

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

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

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

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

17,481✔
92
        self.app_name == other.app_name
17,481✔
93
            && self.feature_name == other.feature_name
16,112✔
94
            && self.environment == other.environment
16,112✔
95
            && self_hour_bin == other_hour_bin
16,112✔
96
    }
17,481✔
97
}
98

99
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
100
pub struct MetricsBatch {
101
    pub applications: Vec<ClientApplication>,
102
    pub metrics: Vec<ClientMetricsEnv>,
103
}
104

105
#[derive(Default, Debug)]
106
pub struct MetricsCache {
107
    pub(crate) applications: DashMap<ApplicationKey, ClientApplication>,
108
    pub(crate) metrics: DashMap<MetricsKey, ClientMetricsEnv>,
109
}
110

111
pub(crate) fn size_of_batch(batch: &MetricsBatch) -> usize {
88✔
112
    serde_json::to_string(batch).map(|s| s.len()).unwrap_or(0)
88✔
113
}
88✔
114

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

146
pub(crate) fn register_client_metrics(
4✔
147
    edge_token: EdgeToken,
4✔
148
    metrics: ClientMetrics,
4✔
149
    metrics_cache: Data<MetricsCache>,
4✔
150
) {
4✔
151
    let metrics = unleash_types::client_metrics::from_bucket_app_name_and_env(
4✔
152
        metrics.bucket,
4✔
153
        metrics.app_name,
4✔
154
        edge_token
4✔
155
            .environment
4✔
156
            .unwrap_or_else(|| "development".into()),
4✔
157
        metrics.metadata.clone(),
4✔
158
    );
4✔
159

4✔
160
    metrics_cache.sink_metrics(&metrics);
4✔
161
}
4✔
162

163
/***
164
   Will filter out metrics that do not belong to the environment that edge_token has access to
165
*/
166
pub(crate) fn register_bulk_metrics(
3✔
167
    metrics_cache: &MetricsCache,
3✔
168
    connect_via: &ConnectVia,
3✔
169
    edge_token: &EdgeToken,
3✔
170
    metrics: BatchMetricsRequestBody,
3✔
171
) {
3✔
172
    let updated: BatchMetricsRequestBody = BatchMetricsRequestBody {
3✔
173
        applications: metrics.applications.clone(),
3✔
174
        metrics: metrics
3✔
175
            .metrics
3✔
176
            .iter()
3✔
177
            .filter(|m| {
3✔
178
                edge_token
3✔
179
                    .environment
3✔
180
                    .clone()
3✔
181
                    .map(|e| e == m.environment)
3✔
182
                    .unwrap_or(false)
3✔
183
            })
3✔
184
            .cloned()
3✔
185
            .collect(),
3✔
186
    };
3✔
187
    metrics_cache.sink_bulk_metrics(updated, connect_via);
3✔
188
}
3✔
189

190
pub(crate) fn sendable(batch: &MetricsBatch) -> bool {
78✔
191
    size_of_batch(batch) < UPSTREAM_MAX_BODY_SIZE
78✔
192
}
78✔
193

194
#[instrument(skip(batch))]
195
pub(crate) fn cut_into_sendable_batches(batch: MetricsBatch) -> Vec<MetricsBatch> {
4✔
196
    let batch_count = (size_of_batch(&batch) / BATCH_BODY_SIZE) + 1;
4✔
197
    let apps_count = batch.applications.len();
4✔
198
    let apps_per_batch = apps_count / batch_count;
4✔
199

4✔
200
    let metrics_count = batch.metrics.len();
4✔
201
    let metrics_per_batch = metrics_count / batch_count;
4✔
202

4✔
203
    debug!(
4✔
204
        "Batch count: {batch_count}. Apps per batch: {apps_per_batch}, Metrics per batch: {metrics_per_batch}"
×
205
    );
206
    (0..=batch_count)
4✔
207
        .map(|counter| {
72✔
208
            let apps_iter = batch.applications.iter();
72✔
209
            let metrics_iter = batch.metrics.iter();
72✔
210
            let apps_take = if apps_per_batch == 0 && counter == 0 {
72✔
211
                apps_count
1✔
212
            } else {
213
                apps_per_batch
71✔
214
            };
215
            let metrics_take = if metrics_per_batch == 0 && counter == 0 {
72✔
216
                metrics_count
1✔
217
            } else {
218
                metrics_per_batch
71✔
219
            };
220
            MetricsBatch {
72✔
221
                metrics: metrics_iter
72✔
222
                    .skip(counter * metrics_per_batch)
72✔
223
                    .take(metrics_take)
72✔
224
                    .cloned()
72✔
225
                    .collect(),
72✔
226
                applications: apps_iter
72✔
227
                    .skip(counter * apps_per_batch)
72✔
228
                    .take(apps_take)
72✔
229
                    .cloned()
72✔
230
                    .collect(),
72✔
231
            }
72✔
232
        })
72✔
233
        .filter(|b| !b.applications.is_empty() || !b.metrics.is_empty())
72✔
234
        .collect::<Vec<MetricsBatch>>()
4✔
235
}
4✔
236

237
impl MetricsCache {
238
    pub fn get_metrics_by_environment(&self) -> HashMap<String, MetricsBatch> {
1✔
239
        let mut batches_by_environment = HashMap::new();
1✔
240

1✔
241
        let applications = self
1✔
242
            .applications
1✔
243
            .iter()
1✔
244
            .map(|e| e.value().clone())
1✔
245
            .collect::<Vec<ClientApplication>>();
1✔
246
        let data = self
1✔
247
            .metrics
1✔
248
            .iter()
1✔
249
            .map(|e| e.value().clone())
2✔
250
            .collect::<Vec<ClientMetricsEnv>>();
1✔
251
        let map: HashMap<String, Vec<ClientMetricsEnv>> = data
1✔
252
            .into_iter()
1✔
253
            .into_group_map_by(|metric| metric.environment.clone());
2✔
254
        for (environment, metrics) in map {
3✔
255
            let batch = MetricsBatch {
2✔
256
                applications: applications.clone(),
2✔
257
                metrics,
2✔
258
            };
2✔
259
            batches_by_environment.insert(environment, batch);
2✔
260
        }
2✔
261
        batches_by_environment
1✔
262
    }
1✔
263

264
    pub fn get_appropriately_sized_env_batches(&self, batch: &MetricsBatch) -> Vec<MetricsBatch> {
×
265
        for app in batch.applications.clone() {
×
266
            self.applications.remove(&ApplicationKey::from(app.clone()));
×
267
        }
×
268
        for metric in batch.metrics.clone() {
×
269
            self.metrics.remove(&MetricsKey::from(metric.clone()));
×
270
        }
×
271
        METRICS_SIZE_HISTOGRAM.observe(size_of_batch(batch) as f64);
×
272
        if sendable(batch) {
×
273
            vec![batch.clone()]
×
274
        } else {
275
            debug!(
×
276
                "We have {} applications and {} metrics",
×
277
                batch.applications.len(),
×
278
                batch.metrics.len()
×
279
            );
280
            cut_into_sendable_batches(batch.clone())
×
281
        }
282
    }
×
283
    /// This is a destructive call. We'll remove all metrics that is due for posting
284
    /// 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
285
    pub fn get_appropriately_sized_batches(&self) -> Vec<MetricsBatch> {
6✔
286
        let batch = MetricsBatch {
6✔
287
            applications: self
6✔
288
                .applications
6✔
289
                .iter()
6✔
290
                .map(|e| e.value().clone())
6,511✔
291
                .collect(),
6✔
292
            metrics: self
6✔
293
                .metrics
6✔
294
                .iter()
6✔
295
                .map(|e| e.value().clone())
16,102✔
296
                .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✔
297
                .collect(),
6✔
298
        };
6✔
299
        for app in batch.applications.clone() {
6,511✔
300
            self.applications.remove(&ApplicationKey::from(app.clone()));
6,511✔
301
        }
6,511✔
302
        for metric in batch.metrics.clone() {
16,101✔
303
            self.metrics.remove(&MetricsKey::from(metric.clone()));
16,101✔
304
        }
16,101✔
305
        METRICS_SIZE_HISTOGRAM.observe(size_of_batch(&batch) as f64);
6✔
306
        if sendable(&batch) {
6✔
307
            vec![batch]
2✔
308
        } else {
309
            debug!(
4✔
310
                "We have {} applications and {} metrics",
×
311
                batch.applications.len(),
×
312
                batch.metrics.len()
×
313
            );
314
            cut_into_sendable_batches(batch)
4✔
315
        }
316
    }
6✔
317

318
    pub fn reinsert_batch(&self, batch: MetricsBatch) {
×
319
        for application in batch.applications {
×
320
            self.register_application(application);
×
321
        }
×
322
        self.sink_metrics(&batch.metrics);
×
323
    }
×
324

325
    pub fn sink_bulk_metrics(&self, metrics: BatchMetricsRequestBody, connect_via: &ConnectVia) {
3✔
326
        for application in metrics.applications {
4✔
327
            self.register_application(
1✔
328
                application.connect_via(&connect_via.app_name, &connect_via.instance_id),
1✔
329
            )
1✔
330
        }
331
        self.sink_metrics(&metrics.metrics)
3✔
332
    }
3✔
333

334
    pub fn reset_metrics(&self) {
1✔
335
        self.applications.clear();
1✔
336
        self.metrics.clear();
1✔
337
    }
1✔
338

339
    pub fn register_application(&self, application: ClientApplication) {
1✔
340
        self.applications
1✔
341
            .insert(ApplicationKey::from(application.clone()), application);
1✔
342
    }
1✔
343

344
    pub fn sink_metrics(&self, metrics: &[ClientMetricsEnv]) {
17✔
345
        debug!("Sinking {} metrics", metrics.len());
17✔
346
        for metric in metrics.iter() {
16,117✔
347
            FEATURE_TOGGLE_USAGE_TOTAL
16,117✔
348
                .with_label_values(&[
16,117✔
349
                    metric.app_name.clone(),
16,117✔
350
                    metric.feature_name.clone(),
16,117✔
351
                    "true".to_string(),
16,117✔
352
                ])
16,117✔
353
                .inc_by(metric.yes as u64);
16,117✔
354
            FEATURE_TOGGLE_USAGE_TOTAL
16,117✔
355
                .with_label_values(&[
16,117✔
356
                    metric.app_name.clone(),
16,117✔
357
                    metric.feature_name.clone(),
16,117✔
358
                    "false".to_string(),
16,117✔
359
                ])
16,117✔
360
                .inc_by(metric.no as u64);
16,117✔
361
            self.metrics
16,117✔
362
                .entry(MetricsKey {
16,117✔
363
                    app_name: metric.app_name.clone(),
16,117✔
364
                    feature_name: metric.feature_name.clone(),
16,117✔
365
                    timestamp: metric.timestamp,
16,117✔
366
                    environment: metric.environment.clone(),
16,117✔
367
                })
16,117✔
368
                .and_modify(|feature_stats| {
16,117✔
369
                    feature_stats.yes += metric.yes;
5✔
370
                    feature_stats.no += metric.no;
5✔
371
                    metric.variants.iter().for_each(|(k, added_count)| {
5✔
372
                        feature_stats
×
373
                            .variants
×
374
                            .entry(k.clone())
×
375
                            .and_modify(|count| {
×
376
                                *count += added_count;
×
377
                            })
×
378
                            .or_insert(*added_count);
×
379
                    });
5✔
380
                })
16,117✔
381
                .or_insert_with(|| metric.clone());
16,117✔
382
        }
16,117✔
383
    }
17✔
384
}
385

386
#[cfg(test)]
387
mod test {
388
    use super::*;
389
    use crate::types::{TokenType, TokenValidationStatus};
390
    use chrono::{DateTime, Utc};
391
    use std::collections::HashMap;
392
    use std::str::FromStr;
393
    use test_case::test_case;
394
    use unleash_types::client_metrics::SdkType::Backend;
395
    use unleash_types::client_metrics::{
396
        ClientMetricsEnv, ConnectVia, ConnectViaBuilder, MetricsMetadata,
397
    };
398

399
    #[test]
400
    fn cache_aggregates_data_correctly() {
1✔
401
        let cache = MetricsCache::default();
1✔
402

1✔
403
        let base_metric = ClientMetricsEnv {
1✔
404
            app_name: "some-app".into(),
1✔
405
            feature_name: "some-feature".into(),
1✔
406
            environment: "development".into(),
1✔
407
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
408
                .unwrap()
1✔
409
                .with_timezone(&Utc),
1✔
410
            yes: 1,
1✔
411
            no: 0,
1✔
412
            variants: HashMap::new(),
1✔
413
            metadata: MetricsMetadata {
1✔
414
                platform_name: None,
1✔
415
                platform_version: None,
1✔
416
                sdk_version: None,
1✔
417
                sdk_type: None,
1✔
418
                yggdrasil_version: None,
1✔
419
            },
1✔
420
        };
1✔
421

1✔
422
        let metrics = vec![
1✔
423
            ClientMetricsEnv {
1✔
424
                ..base_metric.clone()
1✔
425
            },
1✔
426
            ClientMetricsEnv { ..base_metric },
1✔
427
        ];
1✔
428

1✔
429
        cache.sink_metrics(&metrics);
1✔
430

1✔
431
        let found_metric = cache
1✔
432
            .metrics
1✔
433
            .get(&MetricsKey {
1✔
434
                app_name: "some-app".into(),
1✔
435
                feature_name: "some-feature".into(),
1✔
436
                timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
437
                    .unwrap()
1✔
438
                    .with_timezone(&Utc),
1✔
439
                environment: "development".into(),
1✔
440
            })
1✔
441
            .unwrap();
1✔
442

1✔
443
        let expected = ClientMetricsEnv {
1✔
444
            app_name: "some-app".into(),
1✔
445
            feature_name: "some-feature".into(),
1✔
446
            environment: "development".into(),
1✔
447
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
448
                .unwrap()
1✔
449
                .with_timezone(&Utc),
1✔
450
            yes: 2,
1✔
451
            no: 0,
1✔
452
            variants: HashMap::new(),
1✔
453
            metadata: MetricsMetadata {
1✔
454
                platform_name: None,
1✔
455
                platform_version: None,
1✔
456
                sdk_version: None,
1✔
457
                sdk_type: None,
1✔
458
                yggdrasil_version: None,
1✔
459
            },
1✔
460
        };
1✔
461

1✔
462
        assert_eq!(found_metric.yes, expected.yes);
1✔
463
        assert_eq!(found_metric.yes, 2);
1✔
464
        assert_eq!(found_metric.no, 0);
1✔
465
        assert_eq!(found_metric.no, expected.no);
1✔
466
    }
1✔
467

468
    #[test]
469
    fn cache_aggregates_data_correctly_across_date_boundaries() {
1✔
470
        let cache = MetricsCache::default();
1✔
471
        let a_long_time_ago = DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
472
            .unwrap()
1✔
473
            .with_timezone(&Utc);
1✔
474
        let hundred_years_later = DateTime::parse_from_rfc3339("1967-11-07T12:00:00Z")
1✔
475
            .unwrap()
1✔
476
            .with_timezone(&Utc);
1✔
477

1✔
478
        let base_metric = ClientMetricsEnv {
1✔
479
            app_name: "some-app".into(),
1✔
480
            feature_name: "some-feature".into(),
1✔
481
            environment: "development".into(),
1✔
482
            timestamp: a_long_time_ago,
1✔
483
            yes: 1,
1✔
484
            no: 0,
1✔
485
            variants: HashMap::new(),
1✔
486
            metadata: MetricsMetadata {
1✔
487
                platform_name: None,
1✔
488
                platform_version: None,
1✔
489
                sdk_version: None,
1✔
490
                sdk_type: None,
1✔
491
                yggdrasil_version: None,
1✔
492
            },
1✔
493
        };
1✔
494

1✔
495
        let metrics = vec![
1✔
496
            ClientMetricsEnv {
1✔
497
                timestamp: hundred_years_later,
1✔
498
                ..base_metric.clone()
1✔
499
            },
1✔
500
            ClientMetricsEnv {
1✔
501
                ..base_metric.clone()
1✔
502
            },
1✔
503
            ClientMetricsEnv { ..base_metric },
1✔
504
        ];
1✔
505

1✔
506
        cache.sink_metrics(&metrics);
1✔
507

1✔
508
        let old_metric = cache
1✔
509
            .metrics
1✔
510
            .get(&MetricsKey {
1✔
511
                app_name: "some-app".into(),
1✔
512
                feature_name: "some-feature".into(),
1✔
513
                environment: "development".into(),
1✔
514
                timestamp: a_long_time_ago,
1✔
515
            })
1✔
516
            .unwrap();
1✔
517

1✔
518
        let old_expectation = ClientMetricsEnv {
1✔
519
            app_name: "some-app".into(),
1✔
520
            feature_name: "some-feature".into(),
1✔
521
            environment: "development".into(),
1✔
522
            timestamp: a_long_time_ago,
1✔
523
            yes: 2,
1✔
524
            no: 0,
1✔
525
            variants: HashMap::new(),
1✔
526
            metadata: MetricsMetadata {
1✔
527
                platform_name: None,
1✔
528
                platform_version: None,
1✔
529
                sdk_version: None,
1✔
530
                sdk_type: None,
1✔
531
                yggdrasil_version: None,
1✔
532
            },
1✔
533
        };
1✔
534

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

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

1✔
562
        assert_eq!(cache.metrics.len(), 2);
1✔
563

564
        assert_eq!(old_metric.yes, old_expectation.yes);
1✔
565
        assert_eq!(old_metric.yes, 2);
1✔
566
        assert_eq!(old_metric.no, 0);
1✔
567
        assert_eq!(old_metric.no, old_expectation.no);
1✔
568

569
        assert_eq!(new_metric.yes, new_expectation.yes);
1✔
570
        assert_eq!(new_metric.yes, 1);
1✔
571
        assert_eq!(new_metric.no, 0);
1✔
572
        assert_eq!(new_metric.no, new_expectation.no);
1✔
573
    }
1✔
574

575
    #[test]
576
    fn cache_clears_metrics_correctly() {
1✔
577
        let cache = MetricsCache::default();
1✔
578
        let time_stamp = DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
579
            .unwrap()
1✔
580
            .with_timezone(&Utc);
1✔
581

1✔
582
        let base_metric = ClientMetricsEnv {
1✔
583
            app_name: "some-app".into(),
1✔
584
            feature_name: "some-feature".into(),
1✔
585
            environment: "development".into(),
1✔
586
            timestamp: time_stamp,
1✔
587
            yes: 1,
1✔
588
            no: 0,
1✔
589
            variants: HashMap::new(),
1✔
590
            metadata: MetricsMetadata {
1✔
591
                platform_name: None,
1✔
592
                platform_version: None,
1✔
593
                sdk_version: None,
1✔
594
                sdk_type: None,
1✔
595
                yggdrasil_version: None,
1✔
596
            },
1✔
597
        };
1✔
598

1✔
599
        let metrics = vec![
1✔
600
            ClientMetricsEnv {
1✔
601
                ..base_metric.clone()
1✔
602
            },
1✔
603
            ClientMetricsEnv { ..base_metric },
1✔
604
        ];
1✔
605

1✔
606
        cache.sink_metrics(&metrics);
1✔
607
        assert!(!cache.metrics.is_empty());
1✔
608
        cache.reset_metrics();
1✔
609
        assert!(cache.metrics.is_empty());
1✔
610
    }
1✔
611

612
    #[test]
613
    fn adding_another_connection_link_works() {
1✔
614
        let client_application = ClientApplication {
1✔
615
            app_name: "tests_help".into(),
1✔
616
            connect_via: None,
1✔
617
            environment: Some("development".into()),
1✔
618
            projects: None,
1✔
619
            instance_id: Some("test".into()),
1✔
620
            connection_id: Some("test".into()),
1✔
621
            interval: 60,
1✔
622
            started: Default::default(),
1✔
623
            strategies: vec![],
1✔
624
            metadata: MetricsMetadata {
1✔
625
                platform_name: None,
1✔
626
                platform_version: None,
1✔
627
                sdk_version: None,
1✔
628
                sdk_type: None,
1✔
629
                yggdrasil_version: None,
1✔
630
            },
1✔
631
        };
1✔
632
        let connected_via_test_instance = client_application.connect_via("test", "instance");
1✔
633
        let connected_via_edge_as_well = connected_via_test_instance.connect_via("edge", "edgeid");
1✔
634
        assert_eq!(
1✔
635
            connected_via_test_instance.connect_via.unwrap(),
1✔
636
            vec![ConnectVia {
1✔
637
                app_name: "test".into(),
1✔
638
                instance_id: "instance".into()
1✔
639
            }]
1✔
640
        );
1✔
641
        assert_eq!(
1✔
642
            connected_via_edge_as_well.connect_via.unwrap(),
1✔
643
            vec![
1✔
644
                ConnectVia {
1✔
645
                    app_name: "test".into(),
1✔
646
                    instance_id: "instance".into()
1✔
647
                },
1✔
648
                ConnectVia {
1✔
649
                    app_name: "edge".into(),
1✔
650
                    instance_id: "edgeid".into()
1✔
651
                }
1✔
652
            ]
1✔
653
        )
1✔
654
    }
1✔
655

656
    #[test_case(10, 100, 1; "10 apps 100 toggles. Will not be split")]
5✔
657
    #[test_case(1, 10000, 27; "1 app 10k toggles, will be split into 27 batches")]
658
    #[test_case(1000, 1000, 8; "1000 apps 1000 toggles, will be split into 8 batches")]
659
    #[test_case(500, 5000, 16; "500 apps 5000 toggles, will be split into 16 batches")]
660
    #[test_case(5000, 1, 20; "5000 apps 1 metric will be split")]
661
    fn splits_successfully_into_sendable_chunks(apps: u64, toggles: u64, batch_count: usize) {
5✔
662
        let apps: Vec<ClientApplication> = (1..=apps)
5✔
663
            .map(|app_id| ClientApplication {
6,511✔
664
                app_name: format!("app_name_{}", app_id),
6,511✔
665
                environment: Some("development".into()),
6,511✔
666
                projects: Some(vec![]),
6,511✔
667
                instance_id: Some(format!("instance-{}", app_id)),
6,511✔
668
                connection_id: Some(format!("connection-{}", app_id)),
6,511✔
669
                interval: 10,
6,511✔
670
                connect_via: Some(vec![ConnectVia {
6,511✔
671
                    app_name: "edge".into(),
6,511✔
672
                    instance_id: "some-instance-id".into(),
6,511✔
673
                }]),
6,511✔
674
                started: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
6,511✔
675
                    .unwrap()
6,511✔
676
                    .with_timezone(&Utc),
6,511✔
677
                strategies: vec![],
6,511✔
678
                metadata: MetricsMetadata {
6,511✔
679
                    platform_name: None,
6,511✔
680
                    platform_version: None,
6,511✔
681
                    sdk_version: Some("some-test-sdk".into()),
6,511✔
682
                    sdk_type: Some(Backend),
6,511✔
683
                    yggdrasil_version: None,
6,511✔
684
                },
6,511✔
685
            })
6,511✔
686
            .collect();
5✔
687

5✔
688
        let toggles: Vec<ClientMetricsEnv> = (1..=toggles)
5✔
689
            .map(|toggle_id| ClientMetricsEnv {
16,101✔
690
                app_name: format!("app_name_{}", toggle_id),
16,101✔
691
                feature_name: format!("toggle-{}", toggle_id),
16,101✔
692
                environment: "development".into(),
16,101✔
693
                timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
16,101✔
694
                    .unwrap()
16,101✔
695
                    .with_timezone(&Utc),
16,101✔
696
                yes: 1,
16,101✔
697
                no: 1,
16,101✔
698
                variants: HashMap::new(),
16,101✔
699
                metadata: MetricsMetadata {
16,101✔
700
                    platform_name: None,
16,101✔
701
                    platform_version: None,
16,101✔
702
                    sdk_version: None,
16,101✔
703
                    sdk_type: None,
16,101✔
704
                    yggdrasil_version: None,
16,101✔
705
                },
16,101✔
706
            })
16,101✔
707
            .collect();
5✔
708

5✔
709
        let cache = MetricsCache::default();
5✔
710
        for app in apps.clone() {
6,511✔
711
            cache.applications.insert(
6,511✔
712
                ApplicationKey {
6,511✔
713
                    app_name: app.app_name.clone(),
6,511✔
714
                    instance_id: app.instance_id.clone().unwrap_or_else(|| "unknown".into()),
6,511✔
715
                },
6,511✔
716
                app,
6,511✔
717
            );
6,511✔
718
        }
6,511✔
719
        cache.sink_metrics(&toggles);
5✔
720
        let batches = cache.get_appropriately_sized_batches();
5✔
721

5✔
722
        assert_eq!(batches.len(), batch_count);
5✔
723
        assert!(batches.iter().all(sendable));
5✔
724
        // Check that we have no duplicates
725
        let applications_sent_count = batches.iter().flat_map(|b| b.applications.clone()).count();
72✔
726

5✔
727
        assert_eq!(applications_sent_count, apps.len());
5✔
728

729
        let metrics_sent_count = batches.iter().flat_map(|b| b.metrics.clone()).count();
72✔
730
        assert_eq!(metrics_sent_count, toggles.len());
5✔
731
    }
5✔
732

733
    #[test]
734
    fn getting_unsent_metrics_filters_out_metrics_with_no_counters() {
1✔
735
        let cache = MetricsCache::default();
1✔
736

1✔
737
        let base_metric = ClientMetricsEnv {
1✔
738
            app_name: "some-app".into(),
1✔
739
            feature_name: "some-feature".into(),
1✔
740
            environment: "development".into(),
1✔
741
            timestamp: DateTime::parse_from_rfc3339("1867-11-07T12:00:00Z")
1✔
742
                .unwrap()
1✔
743
                .with_timezone(&Utc),
1✔
744
            yes: 0,
1✔
745
            no: 0,
1✔
746
            variants: HashMap::new(),
1✔
747
            metadata: MetricsMetadata {
1✔
748
                platform_name: None,
1✔
749
                platform_version: None,
1✔
750
                sdk_version: None,
1✔
751
                sdk_type: None,
1✔
752
                yggdrasil_version: None,
1✔
753
            },
1✔
754
        };
1✔
755

1✔
756
        let metrics = vec![
1✔
757
            ClientMetricsEnv {
1✔
758
                ..base_metric.clone()
1✔
759
            },
1✔
760
            ClientMetricsEnv { ..base_metric },
1✔
761
        ];
1✔
762

1✔
763
        cache.sink_metrics(&metrics);
1✔
764
        let metrics_batch = cache.get_appropriately_sized_batches();
1✔
765
        assert_eq!(metrics_batch.len(), 1);
1✔
766
        assert!(metrics_batch.first().unwrap().metrics.is_empty());
1✔
767
    }
1✔
768

769
    #[test]
770
    pub fn register_bulk_metrics_filters_metrics_based_on_environment_in_token() {
1✔
771
        let metrics_cache = MetricsCache::default();
1✔
772
        let connect_via = ConnectViaBuilder::default()
1✔
773
            .app_name("edge_bulk_metrics".into())
1✔
774
            .instance_id("sometest".into())
1✔
775
            .build()
1✔
776
            .unwrap();
1✔
777
        let mut edge_token_with_development =
1✔
778
            EdgeToken::from_str("*:development.randomstring").unwrap();
1✔
779
        edge_token_with_development.status = TokenValidationStatus::Validated;
1✔
780
        edge_token_with_development.token_type = Some(TokenType::Client);
1✔
781
        let metrics = BatchMetricsRequestBody {
1✔
782
            applications: vec![],
1✔
783
            metrics: vec![
1✔
784
                ClientMetricsEnv {
1✔
785
                    feature_name: "feature_one".into(),
1✔
786
                    app_name: "my_app".into(),
1✔
787
                    environment: "development".into(),
1✔
788
                    timestamp: Utc::now(),
1✔
789
                    yes: 50,
1✔
790
                    no: 10,
1✔
791
                    variants: Default::default(),
1✔
792
                    metadata: MetricsMetadata {
1✔
793
                        platform_name: None,
1✔
794
                        platform_version: None,
1✔
795
                        sdk_version: None,
1✔
796
                        sdk_type: None,
1✔
797
                        yggdrasil_version: None,
1✔
798
                    },
1✔
799
                },
1✔
800
                ClientMetricsEnv {
1✔
801
                    feature_name: "feature_two".to_string(),
1✔
802
                    app_name: "other_app".to_string(),
1✔
803
                    environment: "production".to_string(),
1✔
804
                    timestamp: Default::default(),
1✔
805
                    yes: 50,
1✔
806
                    no: 10,
1✔
807
                    variants: Default::default(),
1✔
808
                    metadata: MetricsMetadata {
1✔
809
                        platform_name: None,
1✔
810
                        platform_version: None,
1✔
811
                        sdk_version: None,
1✔
812
                        sdk_type: None,
1✔
813
                        yggdrasil_version: None,
1✔
814
                    },
1✔
815
                },
1✔
816
            ],
1✔
817
        };
1✔
818
        register_bulk_metrics(
1✔
819
            &metrics_cache,
1✔
820
            &connect_via,
1✔
821
            &edge_token_with_development,
1✔
822
            metrics,
1✔
823
        );
1✔
824
        assert_eq!(metrics_cache.metrics.len(), 1);
1✔
825
    }
1✔
826

827
    #[test]
828
    pub fn metrics_will_be_gathered_per_environment() {
1✔
829
        let metrics = vec![
1✔
830
            ClientMetricsEnv {
1✔
831
                feature_name: "feature_one".into(),
1✔
832
                app_name: "my_app".into(),
1✔
833
                environment: "development".into(),
1✔
834
                timestamp: Utc::now(),
1✔
835
                yes: 50,
1✔
836
                no: 10,
1✔
837
                variants: Default::default(),
1✔
838
                metadata: MetricsMetadata {
1✔
839
                    platform_name: None,
1✔
840
                    platform_version: None,
1✔
841
                    sdk_version: None,
1✔
842
                    sdk_type: None,
1✔
843
                    yggdrasil_version: None,
1✔
844
                },
1✔
845
            },
1✔
846
            ClientMetricsEnv {
1✔
847
                feature_name: "feature_two".to_string(),
1✔
848
                app_name: "other_app".to_string(),
1✔
849
                environment: "production".to_string(),
1✔
850
                timestamp: Default::default(),
1✔
851
                yes: 50,
1✔
852
                no: 10,
1✔
853
                variants: Default::default(),
1✔
854
                metadata: MetricsMetadata {
1✔
855
                    platform_name: None,
1✔
856
                    platform_version: None,
1✔
857
                    sdk_version: None,
1✔
858
                    sdk_type: None,
1✔
859
                    yggdrasil_version: None,
1✔
860
                },
1✔
861
            },
1✔
862
        ];
1✔
863
        let cache = MetricsCache::default();
1✔
864
        cache.sink_metrics(&metrics);
1✔
865
        let metrics_by_env_map = cache.get_metrics_by_environment();
1✔
866
        assert_eq!(metrics_by_env_map.len(), 2);
1✔
867
        assert!(metrics_by_env_map.contains_key("development"));
1✔
868
        assert!(metrics_by_env_map.contains_key("production"));
1✔
869
    }
1✔
870
}
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