Revision control
Copy as Markdown
// 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
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<K> {
pub(crate) inner: glean_core::metrics::EventMetric,
extra_keys: PhantomData<K>,
}
impl<K: traits::ExtraKeys> EventMetric<K> {
/// 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<String>,
) -> 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<String, String>) {
self.inner.record_with_time(timestamp, extra);
}
}
#[inherent]
impl<K: traits::ExtraKeys> traits::Event for EventMetric<K> {
type Extra = K;
pub fn record<M: Into<Option<<Self as traits::Event>::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<Option<&'a str>>>(
&self,
ping_name: S,
) -> Option<Vec<RecordedEvent>> {
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<traits::NoExtraKeys> = EventMetric::new(CommonMetricData {
name: "event".into(),
category: "test".into(),
send_in_pings: vec!["store1".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<String>,
key2: Option<String>,
}
impl glean_core::traits::ExtraKeys for SomeExtra {
const ALLOWED_KEYS: &'static [&'static str] = &["key1", "key2"];
fn into_ffi_extra(self) -> HashMap<String, String> {
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<SomeExtra> = EventMetric::new(CommonMetricData {
name: "event".into(),
category: "test".into(),
send_in_pings: vec!["store1".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<String, String> {
HashMap::new()
}
}
let metric: EventMetric<RuntimeExtra> = EventMetric::with_runtime_extra_keys(
CommonMetricData {
name: "event".into(),
category: "test".into(),
send_in_pings: vec!["store1".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);
}
}