Skip to main content

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