// 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/. use inherent::inherent; use std::{collections::HashMap, marker::PhantomData}; use glean_core::traits; use crate::{ErrorType, RecordedEvent}; // We need to wrap the glean-core type: otherwise if we try to implement // the trait for the metric in `glean_core::metrics` we hit error[E0117]: // only traits defined in the current crate can be implemented for arbitrary // types. /// Developer-facing API for recording event metrics. /// /// Instances of this class type are automatically generated by the parsers /// at build time, allowing developers to record values that were previously /// registered in the metrics.yaml file. #[derive(Clone)] pub struct EventMetric { pub(crate) inner: glean_core::metrics::EventMetric, extra_keys: PhantomData, } impl EventMetric { /// The public constructor used by automatically generated metrics. pub fn new(meta: glean_core::CommonMetricData) -> Self { let allowed_extra_keys = K::ALLOWED_KEYS.iter().map(|s| s.to_string()).collect(); let inner = glean_core::metrics::EventMetric::new(meta, allowed_extra_keys); Self { inner, extra_keys: PhantomData, } } /// The public constructor used by runtime-defined metrics. pub fn with_runtime_extra_keys( meta: glean_core::CommonMetricData, allowed_extra_keys: Vec, ) -> Self { let inner = glean_core::metrics::EventMetric::new(meta, allowed_extra_keys); Self { inner, extra_keys: PhantomData, } } /// Record a new event with a provided timestamp. /// /// It's the caller's responsibility to ensure the timestamp comes from the same clock source. /// Use [`glean::get_timestamp_ms`](crate::get_timestamp_ms) to get a valid timestamp. pub fn record_with_time(&self, timestamp: u64, extra: HashMap) { self.inner.record_with_time(timestamp, extra); } } #[inherent] impl traits::Event for EventMetric { type Extra = K; pub fn record::Extra>>>(&self, extra: M) { let extra = extra .into() .map(|e| e.into_ffi_extra()) .unwrap_or_else(HashMap::new); self.inner.record(extra); } pub fn test_get_value<'a, S: Into>>( &self, ping_name: S, ) -> Option> { let ping_name = ping_name.into().map(|s| s.to_string()); self.inner.test_get_value(ping_name) } pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 { self.inner.test_get_num_recorded_errors(error) } } #[cfg(test)] mod test { use super::*; use crate::common_test::{lock_test, new_glean}; use crate::CommonMetricData; #[test] fn no_extra_keys() { let _lock = lock_test(); let _t = new_glean(None, true); let metric: EventMetric = EventMetric::new(CommonMetricData { name: "event".into(), category: "test".into(), send_in_pings: vec!["test1".into()], ..Default::default() }); metric.record(None); metric.record(None); let data = metric.test_get_value(None).expect("no event recorded"); assert_eq!(2, data.len()); assert!(data[0].timestamp <= data[1].timestamp); } #[test] fn with_extra_keys() { let _lock = lock_test(); let _t = new_glean(None, true); #[derive(Default, Debug, Clone, Hash, Eq, PartialEq)] struct SomeExtra { key1: Option, key2: Option, } impl glean_core::traits::ExtraKeys for SomeExtra { const ALLOWED_KEYS: &'static [&'static str] = &["key1", "key2"]; fn into_ffi_extra(self) -> HashMap { let mut map = HashMap::new(); self.key1.and_then(|key1| map.insert("key1".into(), key1)); self.key2.and_then(|key2| map.insert("key2".into(), key2)); map } } let metric: EventMetric = EventMetric::new(CommonMetricData { name: "event".into(), category: "test".into(), send_in_pings: vec!["test1".into()], ..Default::default() }); let map1 = SomeExtra { key1: Some("1".into()), ..Default::default() }; metric.record(map1); let map2 = SomeExtra { key1: Some("1".into()), key2: Some("2".into()), }; metric.record(map2); metric.record(None); let data = metric.test_get_value(None).expect("no event recorded"); assert_eq!(3, data.len()); assert!(data[0].timestamp <= data[1].timestamp); assert!(data[1].timestamp <= data[2].timestamp); let mut map = HashMap::new(); map.insert("key1".into(), "1".into()); assert_eq!(Some(map), data[0].extra); let mut map = HashMap::new(); map.insert("key1".into(), "1".into()); map.insert("key2".into(), "2".into()); assert_eq!(Some(map), data[1].extra); assert_eq!(None, data[2].extra); } #[test] fn with_runtime_extra_keys() { let _lock = lock_test(); let _t = new_glean(None, true); #[derive(Default, Debug, Clone, Hash, Eq, PartialEq)] struct RuntimeExtra {} impl glean_core::traits::ExtraKeys for RuntimeExtra { const ALLOWED_KEYS: &'static [&'static str] = &[]; fn into_ffi_extra(self) -> HashMap { HashMap::new() } } let metric: EventMetric = EventMetric::with_runtime_extra_keys( CommonMetricData { name: "event".into(), category: "test".into(), send_in_pings: vec!["test1".into()], ..Default::default() }, vec!["key1".into(), "key2".into()], ); let map1 = HashMap::from([("key1".into(), "1".into())]); metric.record_with_time(0, map1); let map2 = HashMap::from([("key1".into(), "1".into()), ("key2".into(), "2".into())]); metric.record_with_time(1, map2); metric.record_with_time(2, HashMap::new()); let data = metric.test_get_value(None).expect("no event recorded"); assert_eq!(3, data.len()); assert!(data[0].timestamp <= data[1].timestamp); assert!(data[1].timestamp <= data[2].timestamp); let mut map = HashMap::new(); map.insert("key1".into(), "1".into()); assert_eq!(Some(map), data[0].extra); let mut map = HashMap::new(); map.insert("key1".into(), "1".into()); map.insert("key2".into(), "2".into()); assert_eq!(Some(map), data[1].extra); assert_eq!(None, data[2].extra); } }