glean_core/metrics/
event.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use std::collections::HashMap;
6
7use crate::common_metric_data::CommonMetricDataInternal;
8use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
9use crate::event_database::RecordedEvent;
10use crate::metrics::MetricType;
11use crate::util::truncate_string_at_boundary_with_error;
12use crate::Glean;
13use crate::{CommonMetricData, TestGetValue};
14
15use chrono::Utc;
16
17const MAX_LENGTH_EXTRA_KEY_VALUE: usize = 500;
18
19/// An event metric.
20///
21/// Events allow recording of e.g. individual occurences of user actions, say
22/// every time a view was open and from where. Each time you record an event, it
23/// records a timestamp, the event's name and a set of custom values.
24#[derive(Clone, Debug)]
25pub struct EventMetric {
26    meta: CommonMetricDataInternal,
27    allowed_extra_keys: Vec<String>,
28}
29
30impl MetricType for EventMetric {
31    fn meta(&self) -> &CommonMetricDataInternal {
32        &self.meta
33    }
34}
35
36// IMPORTANT:
37//
38// When changing this implementation, make sure all the operations are
39// also declared in the related trait in `../traits/`.
40impl EventMetric {
41    /// Creates a new event metric.
42    pub fn new(meta: CommonMetricData, allowed_extra_keys: Vec<String>) -> Self {
43        Self {
44            meta: meta.into(),
45            allowed_extra_keys,
46        }
47    }
48
49    /// Records an event.
50    ///
51    /// # Arguments
52    ///
53    /// * `extra` - A [`HashMap`] of `(key, value)` pairs.
54    ///             Keys must be one of the allowed extra keys.
55    ///             If any key is not allowed, an error is reported and no event is recorded.
56    pub fn record(&self, extra: HashMap<String, String>) {
57        let timestamp = crate::get_timestamp_ms();
58        self.record_with_time(timestamp, extra);
59    }
60
61    /// Record a new event with a provided timestamp.
62    ///
63    /// It's the caller's responsibility to ensure the timestamp comes from the same clock source.
64    ///
65    /// # Arguments
66    ///
67    /// * `timestamp` - The event timestamp, in milliseconds.
68    /// * `extra` - A [`HashMap`] of `(key, value)` pairs.
69    ///             Keys must be one of the allowed extra keys.
70    ///             If any key is not allowed, an error is reported and no event is recorded.
71    pub fn record_with_time(&self, timestamp: u64, extra: HashMap<String, String>) {
72        let metric = self.clone();
73
74        // Precise timestamp based on wallclock. Will be used if `enable_event_timestamps` is true.
75        let now = Utc::now();
76        let precise_timestamp = now.timestamp_millis() as u64;
77
78        crate::launch_with_glean(move |glean| {
79            let sent = metric.record_sync(glean, timestamp, extra, precise_timestamp);
80            if sent {
81                let state = crate::global_state().lock().unwrap();
82                if let Err(e) = state.callbacks.trigger_upload() {
83                    log::error!("Triggering upload failed. Error: {}", e);
84                }
85            }
86        });
87
88        let id = self.meta().base_identifier();
89        crate::launch_with_glean(move |_| {
90            let event_listeners = crate::event_listeners().lock().unwrap();
91            event_listeners
92                .iter()
93                .for_each(|(_, listener)| listener.on_event_recorded(id.clone()));
94        });
95    }
96
97    /// Validate that extras are empty or all extra keys are allowed.
98    ///
99    /// If at least one key is not allowed, record an error and fail.
100    fn validate_extra(
101        &self,
102        glean: &Glean,
103        extra: HashMap<String, String>,
104    ) -> Result<Option<HashMap<String, String>>, ()> {
105        if extra.is_empty() {
106            return Ok(None);
107        }
108
109        let mut extra_strings = HashMap::new();
110        for (k, v) in extra.into_iter() {
111            if !self.allowed_extra_keys.contains(&k) {
112                let msg = format!("Invalid key index {}", k);
113                record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
114                return Err(());
115            }
116
117            let value = truncate_string_at_boundary_with_error(
118                glean,
119                &self.meta,
120                v,
121                MAX_LENGTH_EXTRA_KEY_VALUE,
122            );
123            extra_strings.insert(k, value);
124        }
125
126        Ok(Some(extra_strings))
127    }
128
129    /// Records an event.
130    ///
131    /// ## Returns
132    ///
133    /// `true` if a ping was submitted and should be uploaded.
134    /// `false` otherwise.
135    #[doc(hidden)]
136    pub fn record_sync(
137        &self,
138        glean: &Glean,
139        timestamp: u64,
140        extra: HashMap<String, String>,
141        precise_timestamp: u64,
142    ) -> bool {
143        if !self.should_record(glean) {
144            return false;
145        }
146
147        let mut extra_strings = match self.validate_extra(glean, extra) {
148            Ok(extra) => extra,
149            Err(()) => return false,
150        };
151
152        if glean.with_timestamps() {
153            if extra_strings.is_none() {
154                extra_strings.replace(Default::default());
155            }
156            let map = extra_strings.get_or_insert(Default::default());
157            map.insert("glean_timestamp".to_string(), precise_timestamp.to_string());
158        }
159
160        glean
161            .event_storage()
162            .record(glean, &self.meta, timestamp, extra_strings)
163    }
164
165    /// **Test-only API (exported for FFI purposes).**
166    ///
167    /// Get the vector of currently stored events for this event metric.
168    #[doc(hidden)]
169    pub fn get_value<'a, S: Into<Option<&'a str>>>(
170        &self,
171        glean: &Glean,
172        ping_name: S,
173    ) -> Option<Vec<RecordedEvent>> {
174        let queried_ping_name = ping_name
175            .into()
176            .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
177
178        let events = glean
179            .event_storage()
180            .test_get_value(&self.meta, queried_ping_name);
181
182        events.map(|mut evts| {
183            for ev in &mut evts {
184                let Some(extra) = &mut ev.extra else { continue };
185                extra.remove("glean_timestamp");
186                if extra.is_empty() {
187                    ev.extra = None;
188                }
189            }
190
191            evts
192        })
193    }
194
195    /// **Exported for test purposes.**
196    ///
197    /// Gets the number of recorded errors for the given metric and error type.
198    ///
199    /// # Arguments
200    ///
201    /// * `error` - The type of error
202    ///
203    /// # Returns
204    ///
205    /// The number of errors reported.
206    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
207        crate::block_on_dispatcher();
208
209        crate::core::with_glean(|glean| {
210            test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
211        })
212    }
213}
214
215impl TestGetValue<Vec<RecordedEvent>> for EventMetric {
216    /// **Test-only API (exported for FFI purposes).**
217    ///
218    /// Get the vector of currently stored events for this event metric.
219    ///
220    /// This doesn't clear the stored value.
221    ///
222    /// # Arguments
223    ///
224    /// * `ping_name` - the optional name of the ping to retrieve the metric
225    ///                 for. Defaults to the first value in `send_in_pings`.
226    fn test_get_value(&self, ping_name: Option<String>) -> Option<Vec<RecordedEvent>> {
227        crate::block_on_dispatcher();
228        crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
229    }
230}