// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. mod common; use crate::common::*; use serde_json::json; use glean_core::metrics::*; use glean_core::storage::StorageManager; use glean_core::{test_get_num_recorded_errors, ErrorType}; use glean_core::{CommonMetricData, Lifetime}; #[test] fn can_create_dual_labeled_counter_metric() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, Some(vec!["key1".into()]), Some(vec!["category1".into()]), ); let metric = dual_labeled_counter.get("key1", "category1"); metric.add_sync(&glean, 1); let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", true) .unwrap(); assert_eq!( json!({ "dual_labeled_counter": { "telemetry.dual_labeled_counter": { "key1": { "category1": 1 } } } }), snapshot ); } #[test] fn can_use_multiple_labels() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, None, None, ); let metric = dual_labeled_counter.get("key1", "category1"); metric.add_sync(&glean, 1); let metric = dual_labeled_counter.get("key1", "category2"); metric.add_sync(&glean, 2); let metric = dual_labeled_counter.get("key2", "category1"); metric.add_sync(&glean, 3); let metric = dual_labeled_counter.get("key2", "category3"); metric.add_sync(&glean, 4); let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", true) .unwrap(); assert_eq!( json!({ "dual_labeled_counter": { "telemetry.dual_labeled_counter": { "key1": { "category1": 1, "category2": 2 }, "key2": { "category1": 3, "category3": 4 } } } }), snapshot ); } #[test] fn can_record_error_for_submetric() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, Some(vec!["key1".into()]), Some(vec!["category1".into()]), ); let metric = dual_labeled_counter.get("key1", "category1"); metric.add_sync(&glean, -1); // Make sure that the errors have been recorded assert_eq!( Ok(1), test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue) ); } #[test] fn labels_are_checked_against_static_list() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, Some(vec!["key1".into(), "key2".into()]), Some(vec!["category1".into(), "category2".into()]), ); let metric = dual_labeled_counter.get("key1", "category1"); metric.add_sync(&glean, 1); let metric = dual_labeled_counter.get("key1", "category2"); metric.add_sync(&glean, 2); let metric = dual_labeled_counter.get("key2", "category1"); metric.add_sync(&glean, 3); let metric = dual_labeled_counter.get("key2", "category2"); metric.add_sync(&glean, 4); // All non-registed labels get mapped to the `other` label let metric = dual_labeled_counter.get("key3", "category1"); metric.add_sync(&glean, 5); let metric = dual_labeled_counter.get("key1", "category3"); metric.add_sync(&glean, 6); let metric = dual_labeled_counter.get("key3", "category3"); metric.add_sync(&glean, 7); let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", true) .unwrap(); assert_eq!( json!({ "dual_labeled_counter": { "telemetry.dual_labeled_counter": { "key1": { "category1": 1, "category2": 2, "__other__": 6 }, "key2": { "category1": 3, "category2": 4 }, "__other__": { "category1": 5, "__other__": 7 } } } }), snapshot ); } #[test] fn dynamic_labels_too_long() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, None, None, ); let metric = dual_labeled_counter.get("1".repeat(112), "2".repeat(112)); metric.add_sync(&glean, 1); let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", true) .unwrap(); // We end up with 2x InvalidLabel errors, one each for key and category which are both too long. assert_eq!( json!({ "dual_labeled_counter": { "telemetry.dual_labeled_counter": { "__other__": { "__other__": 1 } } }, "labeled_counter": { "glean.error.invalid_label": { "telemetry.dual_labeled_counter": 2 } } }), snapshot ); } #[test] fn dynamic_labels_regex_allowed() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, None, None, ); let labels_validating = vec![ ("key.is.fine", "cat.is.fine"), ("key_is_fine_too", "cat_is_fine_too"), ("key.is_still_fine", "cat.is_still_fine"), ("keyisfine", "catisfine"), ("_.key-is_fine", "_.cat-is_fine"), ("key.is-fine", "cat.is-fine"), ("key-is-fine", "cat-is-fine"), ]; for (key, category) in &labels_validating { dual_labeled_counter.get(key, category).add_sync(&glean, 1); } let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", true) .unwrap(); assert_eq!( json!({ "dual_labeled_counter": { "telemetry.dual_labeled_counter": { "key.is.fine": { "cat.is.fine": 1 }, "key_is_fine_too": { "cat_is_fine_too": 1 }, "key.is_still_fine": { "cat.is_still_fine": 1 }, "keyisfine": { "catisfine": 1 }, "_.key-is_fine": { "_.cat-is_fine": 1 }, "key.is-fine": { "cat.is-fine": 1 }, "key-is-fine": { "cat-is-fine": 1 } } } }), snapshot ); } #[test] fn seen_labels_get_reloaded_from_disk() { let (mut tempdir, _) = tempdir(); let (glean, dir) = new_glean(Some(tempdir)); tempdir = dir; let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, None, None, ); // Store some data into labeled metrics { // Set the maximum number of labels for i in 1..=16 { let key = format!("key{i}"); let category = format!("category{i}"); dual_labeled_counter.get(key, category).add_sync(&glean, i); } let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", false) .unwrap(); // Check that the data is there for i in 1..=16 { let key = format!("key{i}"); let category = format!("category{i}"); assert_eq!( i, snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"][&key][&category] ); } drop(glean); } // Force a reload { let (glean, _t) = new_glean(Some(tempdir)); // Try to store another label dual_labeled_counter .get("new_key", "new_category") .add_sync(&glean, 40); let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", false) .unwrap(); // Check that the old data is still there for i in 1..=16 { let key = format!("key{i}"); let category = format!("category{i}"); assert_eq!( i, snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"][&key][&category] ); } // The new label lands in the __other__ bucket, due to too many labels assert_eq!( 40, snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"]["__other__"] ["__other__"] ); } } #[test] fn caching_metrics_with_dynamic_labels() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, None, None, ); // Create multiple metric instances and cache them for later use. let metrics = (1..=20) .map(|i| { let key = format!("key{i}"); let category = format!("category{i}"); dual_labeled_counter.get(key, category) }) .collect::>(); // Only now use them. for metric in metrics { metric.add_sync(&glean, 1); } // The maximum number of labels we store is 16. // So we should have put 4 metrics in the __other__ bucket. let other = dual_labeled_counter.get("__other__", "__other__"); assert_eq!(Some(4), other.get_value(&glean, Some("store1"))); } #[test] fn metrics_with_static_keys_and_dynamic_categories() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, Some(vec![ std::borrow::Cow::Borrowed("key1"), std::borrow::Cow::Borrowed("key2"), std::borrow::Cow::Borrowed("key3"), std::borrow::Cow::Borrowed("key4"), ]), None, ); // Create multiple metric instances and cache them for later use. // We also purposefully create a single key that should fail static // validation and be replaced with `__other__` let mut metrics: Vec> = Vec::new(); for i in 1..=5 { let key = format!("key{i}"); for j in 1..=20 { let category = format!("category{j}"); metrics.push(dual_labeled_counter.get(&key, &category)); } } // Only now use them. for metric in metrics { metric.add_sync(&glean, 1); } // The maximum number of categories we store is 16. // So we should have put 4 metrics in the __other__ bucket for every key. for i in 1..=5 { let other = dual_labeled_counter.get(format!("key{i}"), "__other__".into()); let recorded_value = other.get_value(&glean, Some("store1")); assert_eq!( Some(4), recorded_value, "Other value should be 4, found {recorded_value:?} for key{i}" ); } // The above would have asked for "key5", but since that doesn't exist as a static key it should have fallen into // `__other__` as a key, the way `get()` works the above assert wouldn't fail but the actual key would have been // renamed. Let's check for that explicitly, just to verify. let other = dual_labeled_counter.get("__other__".to_string(), "__other__".to_string()); let recorded_value = other.get_value(&glean, Some("store1")); assert_eq!( Some(4), recorded_value, "Other value should be 4, found {recorded_value:?}" ); } #[test] fn metrics_with_dynamic_keys_and_static_categories() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, None, Some(vec![ std::borrow::Cow::Borrowed("category1"), std::borrow::Cow::Borrowed("category2"), std::borrow::Cow::Borrowed("category3"), std::borrow::Cow::Borrowed("category4"), ]), ); // Create multiple metric instances and cache them for later use. // We also purposefully create a single key that should fail static // validation and be replaced with `__other__` let mut metrics: Vec> = Vec::new(); for i in 1..=20 { let key = format!("key{i}"); for j in 1..=5 { let category = format!("category{j}"); metrics.push(dual_labeled_counter.get(&key, &category)); } } // Only now use them. for metric in metrics { metric.add_sync(&glean, 1); } for i in 1..=16 { for j in 1..=4 { let metric = dual_labeled_counter.get(format!("key{i}"), format!("category{j}")); let recorded_value = metric.get_value(&glean, Some("store1")); // The maximum number of keys we store is 16. // The first 16 keys are valid, but we also used an invalid static category ("category5"), so each of the // valid keys should have 1 count in the `_other_` category too, we will check that explicitly later in this test. assert_eq!( Some(1), recorded_value, "Value should be 1, found {recorded_value:?} for key{i}/category{j}" ); } // Now we test for the invalid static category ("category5") that we used above went into the `__other__` category. let metric = dual_labeled_counter.get(format!("key{i}"), "__other__".to_string()); let recorded_value = metric.get_value(&glean, Some("store1")); assert_eq!( Some(1), recorded_value, "Value should be 1, found {recorded_value:?} for key{i}/__other__" ); } // Now lets check that key17 through key20 landed in the `__other__` key, and have counts in the proper categories. Each // category should have 4 counts since we had 4 keys that resulted in `__other__` for i in 1..=4 { let metric = dual_labeled_counter.get("__other__".into(), format!("category{i}")); let recorded_value = metric.get_value(&glean, Some("store1")); // The maximum number of keys we store is 16. // The first 16 keys are valid, but we also used an invalid static category ("category5"), so each of the // valid keys should have 1 count in the `_other_` category too, we will check that explicitly later in this test. assert_eq!( Some(4), recorded_value, "Value should be 4, found {recorded_value:?} for __other__/category{i}" ); } // Finally, 4 keys each should have had one category that was invalid so we should have 4 counts in `__other__`/`__other__` let metric = dual_labeled_counter.get("__other__", "__other__"); let recorded_value = metric.get_value(&glean, Some("store1")); assert_eq!( Some(4), recorded_value, "Value should be 4, found {recorded_value:?} for __other__/__other__" ); } #[test] fn caching_metrics_with_dynamic_labels_across_pings() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, None, None, ); // Create multiple metric instances and cache them for later use. let metrics = (1..=20) .map(|i| { let key = format!("key{i}"); let category = format!("category{i}"); dual_labeled_counter.get(key, category) }) .collect::>(); // Only now use them. for metric in &metrics { metric.add_sync(&glean, 1); } // The maximum number of keys/categories we store is 16. // So we should have put 4 metrics in the __other__ bucket. let other = dual_labeled_counter.get("__other__", "__other__"); assert_eq!(Some(4), other.get_value(&glean, Some("store1"))); // Snapshot (so we can inspect the JSON) // and clear out storage (the same way submitting a ping would) let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", true) .unwrap(); // We didn't send the 20th key/category assert_eq!( json!(null), snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"]["key20"]["category20"] ); // We now set the ones that ended up in `__other__` before. // Note: indexing is zero-based, // but we later check the names, so let's offset it by 1. metrics[16].add_sync(&glean, 17); metrics[17].add_sync(&glean, 18); metrics[18].add_sync(&glean, 19); metrics[19].add_sync(&glean, 20); assert_eq!(Some(17), metrics[16].get_value(&glean, Some("store1"))); assert_eq!(Some(18), metrics[17].get_value(&glean, Some("store1"))); assert_eq!(Some(19), metrics[18].get_value(&glean, Some("store1"))); assert_eq!(Some(20), metrics[19].get_value(&glean, Some("store1"))); assert_eq!(None, other.get_value(&glean, Some("store1"))); let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", true) .unwrap(); let cached_labels = &snapshot["dual_labeled_counter"]["telemetry.dual_labeled_counter"]; assert_eq!(json!(17), cached_labels["key17"]["category17"]); assert_eq!(json!(18), cached_labels["key18"]["category18"]); assert_eq!(json!(19), cached_labels["key19"]["category19"]); assert_eq!(json!(20), cached_labels["key20"]["category20"]); assert_eq!(json!(null), cached_labels["__other__"]["__other__"]); } #[test] fn labels_containing_a_record_separator_record_an_error() { let (glean, _t) = new_glean(None); let dual_labeled_counter = DualLabeledCounterMetric::new( CommonMetricData { name: "dual_labeled_counter".into(), category: "telemetry".into(), send_in_pings: vec!["store1".into()], disabled: false, lifetime: Lifetime::Ping, ..Default::default() }, None, None, ); let metric = dual_labeled_counter.get("record\x1eseparator", "category1"); metric.add_sync(&glean, 1); let snapshot = StorageManager .snapshot_as_json(glean.storage(), "store1", true) .unwrap(); // We end up with 1x InvalidLabel error because the key contains an ASCII record separator, but both // labels end up as other because the use of the record separator prevents us from knowing which one // contained it (or both) at this point in exection. assert_eq!( json!({ "dual_labeled_counter": { "telemetry.dual_labeled_counter": { "__other__": { "__other__": 1 } } }, "labeled_counter": { "glean.error.invalid_label": { "telemetry.dual_labeled_counter": 1 } } }), snapshot ); }