Skip to main content

glean_core/event_database/
mod.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::cmp::Ordering;
6use std::collections::hash_map::Entry;
7use std::collections::HashMap;
8use std::fs::{create_dir_all, File, OpenOptions};
9use std::io::BufReader;
10use std::io::Write;
11use std::io::{self, BufRead};
12use std::path::{Path, PathBuf};
13use std::sync::{atomic, Arc, Mutex, RwLock};
14use std::{fs, mem};
15
16use chrono::{DateTime, FixedOffset, Utc};
17
18use malloc_size_of::MallocSizeOf;
19use malloc_size_of_derive::MallocSizeOf;
20use serde::{Deserialize, Serialize};
21use serde_json::{json, Value as JsonValue};
22
23use crate::common_metric_data::CommonMetricDataInternal;
24use crate::error_recording::{record_error, ErrorType};
25use crate::metrics::{DatetimeMetric, TimeUnit};
26use crate::session::{EventSessionContext, SessionMetadata};
27use crate::storage::INTERNAL_STORAGE;
28use crate::util::get_iso_time_string;
29use crate::Glean;
30use crate::Result;
31use crate::{CommonMetricData, CounterMetric, Lifetime};
32
33/// Represents the recorded data for a single event.
34#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, MallocSizeOf)]
35#[cfg_attr(test, derive(Default))]
36pub struct RecordedEvent {
37    /// The timestamp of when the event was recorded.
38    ///
39    /// This allows to order events from a single process run.
40    pub timestamp: u64,
41
42    /// The event's category.
43    ///
44    /// This is defined by users in the metrics file.
45    pub category: String,
46
47    /// The event's name.
48    ///
49    /// This is defined by users in the metrics file.
50    pub name: String,
51
52    /// A map of all extra data values.
53    ///
54    /// The set of allowed extra keys is defined by users in the metrics file.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub extra: Option<HashMap<String, String>>,
57
58    /// Session metadata attached to this event.
59    ///
60    /// `None` for out-of-session events and events recorded before
61    /// sessions were introduced (backwards compatibility).
62    #[serde(skip_serializing_if = "Option::is_none")]
63    #[serde(default)]
64    pub session: Option<SessionMetadata>,
65}
66
67/// Represents the stored data for a single event.
68#[derive(
69    Debug, Clone, Deserialize, Serialize, PartialEq, Eq, malloc_size_of_derive::MallocSizeOf,
70)]
71struct StoredEvent {
72    #[serde(flatten)]
73    event: RecordedEvent,
74
75    /// The monotonically-increasing execution counter.
76    ///
77    /// Included to allow sending of events across Glean restarts (bug 1716725).
78    /// Is i32 because it is stored in a CounterMetric.
79    #[serde(default)]
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub execution_counter: Option<i32>,
82}
83
84/// This struct handles the in-memory and on-disk storage logic for events.
85///
86/// So that the data survives shutting down of the application, events are stored
87/// in an append-only file on disk, in addition to the store in memory. Each line
88/// of this file records a single event in JSON, exactly as it will be sent in the
89/// ping. There is one file per store.
90///
91/// When restarting the application, these on-disk files are checked, and if any are
92/// found, they are loaded, and a `glean.restarted` event is added before any
93/// further events are collected. This is because the timestamps for these events
94/// may have come from a previous boot of the device, and therefore will not be
95/// compatible with any newly-collected events.
96///
97/// Normalizing all these timestamps happens on serialization for submission (see
98/// `serialize_as_json`) where the client time between restarts is calculated using
99/// data stored in the `glean.startup.date` extra of the `glean.restarted` event, plus
100/// the `execution_counter` stored in events on disk.
101///
102/// Neither `execution_counter` nor `glean.startup.date` is submitted in pings.
103/// The `glean.restarted` event is, though.
104/// (See [bug 1716725](https://bugzilla.mozilla.org/show_bug.cgi?id=1716725).)
105#[derive(Debug)]
106pub struct EventDatabase {
107    /// Path to directory of on-disk event files
108    pub path: PathBuf,
109    /// The in-memory list of events
110    event_stores: RwLock<HashMap<String, Vec<StoredEvent>>>,
111    event_store_files: RwLock<HashMap<String, Arc<File>>>,
112    /// A lock to be held when doing operations on the filesystem
113    file_lock: Mutex<()>,
114    /// How many "events" pings have been submitted,
115    /// as estimated from how often the "events" store is snapshotted and cleared.
116    events_pings_submitted: atomic::AtomicUsize,
117}
118
119impl MallocSizeOf for EventDatabase {
120    fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
121        let mut n = 0;
122        n += self.event_stores.read().unwrap().size_of(ops);
123
124        let map = self.event_store_files.read().unwrap();
125        for store_name in map.keys() {
126            n += store_name.size_of(ops);
127            // `File` doesn't allocate, but `Arc` puts it on the heap.
128            n += mem::size_of::<File>();
129        }
130        n
131    }
132}
133
134impl EventDatabase {
135    /// Creates a new event database.
136    ///
137    /// # Arguments
138    ///
139    /// * `data_path` - The directory to store events in. A new directory
140    /// * `events` - will be created inside of this directory.
141    pub fn new(data_path: &Path) -> Result<Self> {
142        let path = data_path.join("events");
143        create_dir_all(&path)?;
144
145        Ok(Self {
146            path,
147            event_stores: RwLock::new(HashMap::new()),
148            event_store_files: RwLock::new(HashMap::new()),
149            file_lock: Mutex::new(()),
150            events_pings_submitted: atomic::AtomicUsize::new(0),
151        })
152    }
153
154    /// Initializes events storage after Glean is fully initialized and ready to send pings.
155    ///
156    /// This must be called once on application startup, e.g. from
157    /// [Glean.initialize], but after we are ready to send pings, since this
158    /// could potentially collect and send the "events" ping.
159    ///
160    /// If there are any events queued on disk, it loads them into memory so
161    /// that the memory and disk representations are in sync.
162    ///
163    /// If event records for the "events" ping are present, they are assembled into
164    /// an "events" ping which is submitted immediately with reason "startup".
165    ///
166    /// If event records for custom pings are present, we increment the custom pings'
167    /// stores' `execution_counter` and record a `glean.restarted`
168    /// event with the current client clock in its `glean.startup.date` extra.
169    ///
170    /// # Arguments
171    ///
172    /// * `glean` - The Glean instance.
173    /// * `trim_data_to_registered_pings` - Whether we should trim the event storage of
174    ///   any events not belonging to pings previously registered via `register_ping_type`.
175    ///
176    /// # Returns
177    ///
178    /// Whether the "events" ping was submitted.
179    pub fn flush_pending_events_on_startup(
180        &self,
181        glean: &Glean,
182        trim_data_to_registered_pings: bool,
183    ) -> bool {
184        match self.load_events_from_disk(glean, trim_data_to_registered_pings) {
185            Ok(_) => {
186                let stores_with_events: Vec<String> = {
187                    self.event_stores
188                        .read()
189                        .unwrap()
190                        .keys()
191                        .map(|x| x.to_owned())
192                        .collect() // safe unwrap, only error case is poisoning
193                };
194                // We do not want to be holding the event stores lock when
195                // submitting a ping or recording new events.
196                let has_events_events = stores_with_events.contains(&"events".to_owned());
197                let glean_restarted_stores = if has_events_events {
198                    stores_with_events
199                        .into_iter()
200                        .filter(|store| store != "events")
201                        .collect()
202                } else {
203                    stores_with_events
204                };
205                if !glean_restarted_stores.is_empty() {
206                    for store_name in glean_restarted_stores.iter() {
207                        CounterMetric::new(CommonMetricData {
208                            name: "execution_counter".into(),
209                            category: store_name.into(),
210                            send_in_pings: vec![INTERNAL_STORAGE.into()],
211                            lifetime: Lifetime::Ping,
212                            ..Default::default()
213                        })
214                        .add_sync(glean, 1);
215                    }
216                    let glean_restarted = CommonMetricData {
217                        name: "restarted".into(),
218                        category: "glean".into(),
219                        send_in_pings: glean_restarted_stores,
220                        lifetime: Lifetime::Ping,
221                        ..Default::default()
222                    };
223                    let startup = get_iso_time_string(glean.start_time(), TimeUnit::Minute);
224                    let mut extra: HashMap<String, String> =
225                        [("glean.startup.date".into(), startup)].into();
226                    if glean.with_timestamps() {
227                        let now = Utc::now();
228                        let precise_timestamp = now.timestamp_millis() as u64;
229                        extra.insert("glean_timestamp".to_string(), precise_timestamp.to_string());
230                    }
231                    self.record(
232                        glean,
233                        &glean_restarted.into(),
234                        crate::get_timestamp_ms(),
235                        Some(extra),
236                        EventSessionContext::OutOfSession,
237                    );
238                }
239                if has_events_events && glean.submit_ping_by_name("events", Some("startup")) {
240                    self.events_pings_submitted
241                        .fetch_sub(1, atomic::Ordering::Relaxed);
242                    true
243                } else {
244                    false
245                }
246            }
247            Err(err) => {
248                log::warn!("Error loading events from disk: {}", err);
249                false
250            }
251        }
252    }
253
254    fn load_events_from_disk(
255        &self,
256        glean: &Glean,
257        trim_data_to_registered_pings: bool,
258    ) -> Result<()> {
259        // NOTE: The order of locks here is important.
260        // In other code parts we might acquire the `file_lock` when we already have acquired
261        // a lock on `event_stores`.
262        // This is a potential lock-order-inversion.
263        let mut db = self.event_stores.write().unwrap(); // safe unwrap, only error case is poisoning
264        let _lock = self.file_lock.lock().unwrap(); // safe unwrap, only error case is poisoning
265
266        for entry in fs::read_dir(&self.path)? {
267            let entry = entry?;
268            if entry.file_type()?.is_file() {
269                let store_name = entry.file_name().into_string()?;
270                log::info!("Loading events for {}", store_name);
271                if trim_data_to_registered_pings && glean.get_ping_by_name(&store_name).is_none() {
272                    log::warn!("Trimming {}'s events", store_name);
273                    if let Err(err) = fs::remove_file(entry.path()) {
274                        match err.kind() {
275                            std::io::ErrorKind::NotFound => {
276                                // silently drop this error, the file was already non-existing
277                            }
278                            _ => log::warn!("Error trimming events file '{}': {}", store_name, err),
279                        }
280                    }
281                    continue;
282                }
283                let file = BufReader::new(File::open(entry.path())?);
284                db.insert(
285                    store_name,
286                    file.lines()
287                        .map_while(Result::ok)
288                        .filter_map(|line| serde_json::from_str::<StoredEvent>(&line).ok())
289                        .collect(),
290                );
291            }
292        }
293        Ok(())
294    }
295
296    /// Records an event in the desired stores.
297    ///
298    /// # Arguments
299    ///
300    /// * `glean` - The Glean instance.
301    /// * `meta` - The metadata about the event metric. Used to get the category,
302    ///   name and stores for the metric.
303    /// * `timestamp` - The timestamp of the event, in milliseconds. Must use a
304    ///   monotonically increasing timer (this value is obtained on the
305    ///   platform-specific side).
306    /// * `extra` - Extra data values, mapping strings to strings.
307    /// * `ctx` - The event's session context, conveying both whether session
308    ///   metadata should be attached and what that metadata is.
309    ///
310    /// ## Returns
311    ///
312    /// `true` if a ping was submitted and should be uploaded.
313    /// `false` otherwise.
314    pub fn record(
315        &self,
316        glean: &Glean,
317        meta: &CommonMetricDataInternal,
318        timestamp: u64,
319        extra: Option<HashMap<String, String>>,
320        ctx: EventSessionContext,
321    ) -> bool {
322        // If upload is disabled we don't want to record.
323        if !glean.is_upload_enabled() {
324            return false;
325        }
326
327        // Convert the session context to the optional metadata stored on the event.
328        let session = match ctx {
329            EventSessionContext::OutOfSession => None,
330            EventSessionContext::InSession(session_meta) => Some(session_meta),
331        };
332
333        let mut submit_max_capacity_event_ping = false;
334        {
335            let mut db = self.event_stores.write().unwrap(); // safe unwrap, only error case is poisoning
336            for store_name in meta.inner.send_in_pings.iter() {
337                if !glean.is_ping_enabled(store_name) {
338                    continue;
339                }
340
341                let store = db.entry(store_name.to_string()).or_default();
342                let execution_counter = CounterMetric::new(CommonMetricData {
343                    name: "execution_counter".into(),
344                    category: store_name.into(),
345                    send_in_pings: vec![INTERNAL_STORAGE.into()],
346                    lifetime: Lifetime::Ping,
347                    ..Default::default()
348                })
349                .get_value(glean, INTERNAL_STORAGE);
350                // Create StoredEvent object, and its JSON form for serialization on disk.
351                let event = StoredEvent {
352                    event: RecordedEvent {
353                        timestamp,
354                        category: meta.inner.category.to_string(),
355                        name: meta.inner.name.to_string(),
356                        extra: extra.clone(),
357                        session: session.clone(),
358                    },
359                    execution_counter,
360                };
361                let event_json = serde_json::to_string(&event).unwrap(); // safe unwrap, event can always be serialized
362                store.push(event);
363                self.write_event_to_disk(store_name, &event_json);
364                if store_name == "events" {
365                    if store.len() == glean.get_max_events() {
366                        submit_max_capacity_event_ping = true;
367                    }
368                    let factor = glean.get_events_ping_acceleration_factor();
369                    let events_pings_submitted =
370                        self.events_pings_submitted.load(atomic::Ordering::Relaxed);
371                    if factor > events_pings_submitted {
372                        // The early "events" ping acceleration formula is y = ax^2.
373                        let a = glean.get_max_events() / factor.saturating_mul(factor);
374                        let x = events_pings_submitted + 1; // We want the y for the next ping.
375                        let y = a.saturating_mul(x.saturating_mul(x));
376                        // It is possible to apply an acceleration factor at runtime that would
377                        // decrease y below store.len(). Submit a ping on the next event in that case.
378                        if store.len() >= y {
379                            submit_max_capacity_event_ping = true;
380                        }
381                    }
382                }
383            }
384        }
385
386        if submit_max_capacity_event_ping {
387            glean.submit_ping_by_name("events", Some("max_capacity"));
388            true
389        } else {
390            false
391        }
392    }
393
394    fn get_event_store(&self, store_name: &str) -> Result<Arc<File>, io::Error> {
395        // safe unwrap, only error case is poisoning
396        let mut map = self.event_store_files.write().unwrap();
397        let entry = map.entry(store_name.to_string());
398
399        match entry {
400            Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
401            Entry::Vacant(entry) => {
402                let file = OpenOptions::new()
403                    .create(true)
404                    .append(true)
405                    .open(self.path.join(store_name))?;
406                let file = Arc::new(file);
407                let entry = entry.insert(file);
408                Ok(Arc::clone(entry))
409            }
410        }
411    }
412
413    /// Writes an event to a single store on disk.
414    ///
415    /// # Arguments
416    ///
417    /// * `store_name` - The name of the store.
418    /// * `event_json` - The event content, as a single-line JSON-encoded string.
419    fn write_event_to_disk(&self, store_name: &str, event_json: &str) {
420        let _lock = self.file_lock.lock().unwrap(); // safe unwrap, only error case is poisoning
421
422        let write_res = (|| {
423            let mut file = self.get_event_store(store_name)?;
424            file.write_all(event_json.as_bytes())?;
425            file.write_all(b"\n")?;
426            file.flush()?;
427            Ok::<(), std::io::Error>(())
428        })();
429
430        if let Err(err) = write_res {
431            log::warn!("IO error writing event to store '{}': {}", store_name, err);
432        }
433    }
434
435    /// Normalizes the store in-place.
436    ///
437    /// A store may be in any order and contain any number of `glean.restarted` events,
438    /// whose values must be taken into account, along with `execution_counter` values,
439    /// to come up with the correct events with correct `timestamp` values,
440    /// on which we then sort.
441    ///
442    /// 1. Sort by `execution_counter` and `timestamp`,
443    ///    breaking ties so that `glean.restarted` comes first.
444    /// 2. Remove all initial and final `glean.restarted` events
445    /// 3. For each group of events that share a `execution_counter`,
446    ///    i. calculate the initial `glean.restarted` event's `timestamp`s to be
447    ///       clamp(glean.startup.date - ping_info.start_time, biggest_timestamp_of_previous_group + 1)
448    ///    ii. normalize each non-`glean-restarted` event's `timestamp`
449    ///        relative to the `glean.restarted` event's uncalculated `timestamp`
450    /// 4. Remove `execution_counter` and `glean.startup.date` extra keys
451    /// 5. Sort by `timestamp`
452    ///
453    /// In the event that something goes awry, this will record an invalid_state on
454    /// glean.restarted if it is due to internal inconsistencies, or invalid_value
455    /// on client clock weirdness.
456    ///
457    /// # Arguments
458    ///
459    /// * `glean` - Used to report errors
460    /// * `store_name` - The name of the store we're normalizing.
461    /// * `store` - The store we're to normalize.
462    /// * `glean_start_time` - Used if the glean.startup.date or ping_info.start_time aren't available. Passed as a parameter to ease unit-testing.
463    fn normalize_store(
464        &self,
465        glean: &Glean,
466        store_name: &str,
467        store: &mut Vec<StoredEvent>,
468        glean_start_time: DateTime<FixedOffset>,
469    ) {
470        let is_glean_restarted =
471            |event: &RecordedEvent| event.category == "glean" && event.name == "restarted";
472        let glean_restarted_meta = |store_name: &str| CommonMetricData {
473            name: "restarted".into(),
474            category: "glean".into(),
475            send_in_pings: vec![store_name.into()],
476            lifetime: Lifetime::Ping,
477            ..Default::default()
478        };
479        // Step 1
480        store.sort_by(|a, b| {
481            a.execution_counter
482                .cmp(&b.execution_counter)
483                .then_with(|| a.event.timestamp.cmp(&b.event.timestamp))
484                .then_with(|| {
485                    if is_glean_restarted(&a.event) {
486                        Ordering::Less
487                    } else {
488                        Ordering::Greater
489                    }
490                })
491        });
492        // Step 2
493        // Find the index of the first and final non-`glean.restarted` events.
494        // Remove events before the first and after the final.
495        let final_event = match store
496            .iter()
497            .rposition(|event| !is_glean_restarted(&event.event))
498        {
499            Some(idx) => idx + 1,
500            _ => 0,
501        };
502        store.drain(final_event..);
503        let first_event = store
504            .iter()
505            .position(|event| !is_glean_restarted(&event.event))
506            .unwrap_or(store.len());
507        store.drain(..first_event);
508        if store.is_empty() {
509            // There was nothing but `glean.restarted` events. Job's done!
510            return;
511        }
512        // Step 3
513        // It is allowed that there might not be any `glean.restarted` event, nor
514        // `execution_counter` extra values. (This should always be the case for the
515        // "events" ping, for instance).
516        // Other inconsistencies are evidence of errors, and so are logged.
517        let mut cur_ec = 0;
518        // The offset within a group of events with the same `execution_counter`.
519        let mut intra_group_offset = store[0].event.timestamp;
520        // The offset between this group and ping_info.start_date.
521        let mut inter_group_offset = 0;
522        let mut highest_ts = 0;
523        for event in store.iter_mut() {
524            let execution_counter = event.execution_counter.take().unwrap_or(0);
525            if is_glean_restarted(&event.event) {
526                // We've entered the next "event group".
527                // We need a new epoch based on glean.startup.date - ping_info.start_date
528                cur_ec = execution_counter;
529                let glean_startup_date = event
530                    .event
531                    .extra
532                    .as_mut()
533                    .and_then(|extra| {
534                        extra.remove("glean.startup.date").and_then(|date_str| {
535                            DateTime::parse_from_str(&date_str, TimeUnit::Minute.format_pattern())
536                                .map_err(|_| {
537                                    record_error(
538                                        glean,
539                                        &glean_restarted_meta(store_name).into(),
540                                        ErrorType::InvalidState,
541                                        format!("Unparseable glean.startup.date '{}'", date_str),
542                                        None,
543                                    );
544                                })
545                                .ok()
546                        })
547                    })
548                    .unwrap_or(glean_start_time);
549                if event
550                    .event
551                    .extra
552                    .as_ref()
553                    .is_some_and(|extra| extra.is_empty())
554                {
555                    // Small optimization to save us sending empty dicts.
556                    event.event.extra = None;
557                }
558                let ping_start = DatetimeMetric::new(
559                    CommonMetricData {
560                        name: format!("{}#start", store_name),
561                        category: "".into(),
562                        send_in_pings: vec![INTERNAL_STORAGE.into()],
563                        lifetime: Lifetime::User,
564                        ..Default::default()
565                    },
566                    TimeUnit::Minute,
567                );
568                let ping_start = ping_start
569                    .get_value(glean, INTERNAL_STORAGE)
570                    .unwrap_or(glean_start_time);
571                let time_from_ping_start_to_glean_restarted =
572                    (glean_startup_date - ping_start).num_milliseconds();
573                intra_group_offset = event.event.timestamp;
574                inter_group_offset =
575                    u64::try_from(time_from_ping_start_to_glean_restarted).unwrap_or(0);
576                if inter_group_offset < highest_ts {
577                    record_error(
578                        glean,
579                        &glean_restarted_meta(store_name).into(),
580                        ErrorType::InvalidValue,
581                        format!("Time between restart and ping start {} indicates client clock weirdness.", time_from_ping_start_to_glean_restarted),
582                        None,
583                    );
584                    // The client's clock went backwards enough that this event group's
585                    // glean.restarted looks like it happened _before_ the final event of the previous group.
586                    // Or, it went ahead enough to overflow u64.
587                    // Adjust things so this group starts 1ms after the previous one.
588                    inter_group_offset = highest_ts + 1;
589                }
590            } else if cur_ec == 0 {
591                // bug 1811872 - cur_ec might need initialization.
592                cur_ec = execution_counter;
593            }
594            event.event.timestamp = event.event.timestamp - intra_group_offset + inter_group_offset;
595            if execution_counter != cur_ec {
596                record_error(
597                    glean,
598                    &glean_restarted_meta(store_name).into(),
599                    ErrorType::InvalidState,
600                    format!(
601                        "Inconsistent execution counter {} (expected {})",
602                        execution_counter, cur_ec
603                    ),
604                    None,
605                );
606                // Let's fix cur_ec up and hope this isn't a sign something big is broken.
607                cur_ec = execution_counter;
608            }
609
610            // event timestamp is a `u64`, but BigQuery uses `i64` (signed!) everywhere. Let's clamp the value to make
611            // sure we stay within bounds.
612            if event.event.timestamp > i64::MAX as u64 {
613                glean
614                    .additional_metrics
615                    .event_timestamp_clamped
616                    .add_sync(glean, 1);
617                log::warn!(
618                    "Calculated event timestamp was too high. Got: {}, max: {}",
619                    event.event.timestamp,
620                    i64::MAX,
621                );
622                event.event.timestamp = event.event.timestamp.clamp(0, i64::MAX as u64);
623            }
624
625            if highest_ts > event.event.timestamp {
626                // Even though we sorted everything, something in the
627                // execution_counter or glean.startup.date math went awry.
628                record_error(
629                    glean,
630                    &glean_restarted_meta(store_name).into(),
631                    ErrorType::InvalidState,
632                    format!(
633                        "Inconsistent previous highest timestamp {} (expected <= {})",
634                        highest_ts, event.event.timestamp
635                    ),
636                    None,
637                );
638                // Let the highest_ts regress to event.timestamp to hope this minimizes weirdness.
639            }
640            highest_ts = event.event.timestamp
641        }
642    }
643
644    /// Gets a snapshot of the stored event data as a JsonValue.
645    ///
646    /// # Arguments
647    ///
648    /// * `glean` - the Glean instance.
649    /// * `store_name` - The name of the desired store.
650    /// * `clear_store` - Whether to clear the store after snapshotting.
651    ///
652    /// # Returns
653    ///
654    /// A array of events, JSON encoded, if any. Otherwise `None`.
655    pub fn snapshot_as_json(
656        &self,
657        glean: &Glean,
658        store_name: &str,
659        clear_store: bool,
660    ) -> Option<JsonValue> {
661        let result = {
662            let mut db = self.event_stores.write().unwrap(); // safe unwrap, only error case is poisoning
663            db.get_mut(&store_name.to_string()).and_then(|store| {
664                if !store.is_empty() {
665                    // Normalization happens in-place, so if we're not clearing,
666                    // operate on a clone.
667                    let mut clone;
668                    let store = if clear_store {
669                        store
670                    } else {
671                        clone = store.clone();
672                        &mut clone
673                    };
674                    // We may need to normalize event timestamps across multiple restarts.
675                    self.normalize_store(glean, store_name, store, glean.start_time());
676                    Some(json!(store))
677                } else {
678                    log::warn!("Unexpectly got empty event store for '{}'", store_name);
679                    None
680                }
681            })
682        };
683
684        if clear_store {
685            self.event_stores
686                .write()
687                .unwrap() // safe unwrap, only error case is poisoning
688                .remove(&store_name.to_string());
689            self.event_store_files
690                .write()
691                .unwrap() // safe unwrap, only error case is poisoning
692                .remove(&store_name.to_string());
693
694            let _lock = self.file_lock.lock().unwrap(); // safe unwrap, only error case is poisoning
695            if let Err(err) = fs::remove_file(self.path.join(store_name)) {
696                match err.kind() {
697                    std::io::ErrorKind::NotFound => {
698                        // silently drop this error, the file was already non-existing
699                    }
700                    _ => log::warn!("Error removing events queue file '{}': {}", store_name, err),
701                }
702            }
703        }
704
705        if clear_store && store_name == "events" {
706            self.events_pings_submitted
707                .fetch_add(1, atomic::Ordering::Relaxed);
708        }
709
710        result
711    }
712
713    /// Clears all stored events, both in memory and on-disk.
714    pub fn clear_all(&self) -> Result<()> {
715        // safe unwrap, only error case is poisoning
716        self.event_stores.write().unwrap().clear();
717        self.event_store_files.write().unwrap().clear();
718
719        // safe unwrap, only error case is poisoning
720        let _lock = self.file_lock.lock().unwrap();
721        std::fs::remove_dir_all(&self.path)?;
722        create_dir_all(&self.path)?;
723
724        Ok(())
725    }
726
727    /// Clears all stored events, both in memory and on-disk.
728    pub fn clear_for_ping(&self, ping: &str) -> Result<()> {
729        log::debug!("Clearing event storage for ping {ping}");
730
731        // safe unwrap, only error case is poisoning
732        let _lock = self.file_lock.lock().unwrap();
733
734        // safe unwrap, only error case is poisoning
735        self.event_stores.write().unwrap().remove(ping);
736        if let Some(ping_file) = self.event_store_files.write().unwrap().remove(ping) {
737            // We're the last owner, dropping it here ensures we're closing it.
738            debug_assert_eq!(1, Arc::strong_count(&ping_file));
739            drop(ping_file);
740            let file_path = self.path.join(ping);
741            std::fs::remove_file(file_path)?;
742        }
743
744        Ok(())
745    }
746
747    /// **Test-only API (exported for FFI purposes).**
748    ///
749    /// Gets the vector of currently stored events for the given event metric in
750    /// the given store.
751    ///
752    /// This doesn't clear the stored value.
753    pub fn test_get_value<'a>(
754        &'a self,
755        meta: &'a CommonMetricDataInternal,
756        store_name: &str,
757    ) -> Option<Vec<RecordedEvent>> {
758        let value: Vec<RecordedEvent> = self
759            .event_stores
760            .read()
761            .unwrap() // safe unwrap, only error case is poisoning
762            .get(&store_name.to_string())
763            .into_iter()
764            .flatten()
765            .map(|stored_event| stored_event.event.clone())
766            .filter(|event| event.name == meta.inner.name && event.category == meta.inner.category)
767            .collect();
768        if !value.is_empty() {
769            Some(value)
770        } else {
771            None
772        }
773    }
774}
775
776#[cfg(test)]
777mod test {
778    use super::*;
779    use crate::metrics::RemoteSettingsConfig;
780    use crate::test_get_num_recorded_errors;
781    use crate::tests::new_glean;
782    use chrono::{TimeZone, Timelike};
783
784    #[test]
785    fn handle_truncated_events_on_disk() {
786        let (glean, t) = new_glean(None);
787
788        {
789            let db = EventDatabase::new(t.path()).unwrap();
790            db.write_event_to_disk("events", "{\"timestamp\": 500");
791            db.write_event_to_disk("events", "{\"timestamp\"");
792            db.write_event_to_disk(
793                "events",
794                "{\"timestamp\": 501, \"category\": \"ui\", \"name\": \"click\"}",
795            );
796        }
797
798        {
799            let db = EventDatabase::new(t.path()).unwrap();
800            db.load_events_from_disk(&glean, false).unwrap();
801            let events = &db.event_stores.read().unwrap()["events"];
802            assert_eq!(1, events.len());
803        }
804    }
805
806    #[test]
807    fn stable_serialization() {
808        let event_empty = RecordedEvent {
809            timestamp: 2,
810            category: "cat".to_string(),
811            name: "name".to_string(),
812            extra: None,
813            session: None,
814        };
815
816        let mut data = HashMap::new();
817        data.insert("a key".to_string(), "a value".to_string());
818        let event_data = RecordedEvent {
819            timestamp: 2,
820            category: "cat".to_string(),
821            name: "name".to_string(),
822            extra: Some(data),
823            session: None,
824        };
825
826        let event_empty_json = ::serde_json::to_string_pretty(&event_empty).unwrap();
827        let event_data_json = ::serde_json::to_string_pretty(&event_data).unwrap();
828
829        assert_eq!(
830            StoredEvent {
831                event: event_empty,
832                execution_counter: None
833            },
834            serde_json::from_str(&event_empty_json).unwrap()
835        );
836        assert_eq!(
837            StoredEvent {
838                event: event_data,
839                execution_counter: None
840            },
841            serde_json::from_str(&event_data_json).unwrap()
842        );
843    }
844
845    #[test]
846    fn deserialize_existing_data() {
847        let event_empty_json = r#"
848{
849  "timestamp": 2,
850  "category": "cat",
851  "name": "name"
852}
853            "#;
854
855        let event_data_json = r#"
856{
857  "timestamp": 2,
858  "category": "cat",
859  "name": "name",
860  "extra": {
861    "a key": "a value"
862  }
863}
864        "#;
865
866        let event_empty = RecordedEvent {
867            timestamp: 2,
868            category: "cat".to_string(),
869            name: "name".to_string(),
870            extra: None,
871            session: None,
872        };
873
874        let mut data = HashMap::new();
875        data.insert("a key".to_string(), "a value".to_string());
876        let event_data = RecordedEvent {
877            timestamp: 2,
878            category: "cat".to_string(),
879            name: "name".to_string(),
880            extra: Some(data),
881            session: None,
882        };
883
884        assert_eq!(
885            StoredEvent {
886                event: event_empty,
887                execution_counter: None
888            },
889            serde_json::from_str(event_empty_json).unwrap()
890        );
891        assert_eq!(
892            StoredEvent {
893                event: event_data,
894                execution_counter: None
895            },
896            serde_json::from_str(event_data_json).unwrap()
897        );
898    }
899
900    #[test]
901    fn doesnt_record_when_upload_is_disabled() {
902        let (mut glean, dir) = new_glean(None);
903        let db = EventDatabase::new(dir.path()).unwrap();
904
905        let test_storage = "store1";
906        let test_category = "category";
907        let test_name = "name";
908        let test_timestamp = 2;
909        let test_meta = CommonMetricDataInternal::new(test_category, test_name, test_storage);
910        let event_data = RecordedEvent {
911            timestamp: test_timestamp,
912            category: test_category.to_string(),
913            name: test_name.to_string(),
914            extra: None,
915            session: None,
916        };
917
918        // Upload is not yet disabled,
919        // so let's check that everything is getting recorded as expected.
920        db.record(
921            &glean,
922            &test_meta,
923            2,
924            None,
925            EventSessionContext::OutOfSession,
926        );
927        {
928            let event_stores = db.event_stores.read().unwrap();
929            assert_eq!(
930                &StoredEvent {
931                    event: event_data,
932                    execution_counter: None
933                },
934                &event_stores.get(test_storage).unwrap()[0]
935            );
936            assert_eq!(event_stores.get(test_storage).unwrap().len(), 1);
937        }
938
939        glean.set_upload_enabled(false);
940
941        // Now that upload is disabled, let's check nothing is recorded.
942        db.record(
943            &glean,
944            &test_meta,
945            2,
946            None,
947            EventSessionContext::OutOfSession,
948        );
949        {
950            let event_stores = db.event_stores.read().unwrap();
951            assert_eq!(event_stores.get(test_storage).unwrap().len(), 1);
952        }
953    }
954
955    #[test]
956    fn normalize_store_of_glean_restarted() {
957        // Make sure stores empty of anything but glean.restarted events normalize without issue.
958        let (glean, _dir) = new_glean(None);
959
960        let store_name = "store-name";
961        let glean_restarted = StoredEvent {
962            event: RecordedEvent {
963                timestamp: 2,
964                category: "glean".into(),
965                name: "restarted".into(),
966                extra: None,
967                session: None,
968            },
969            execution_counter: None,
970        };
971        let mut store = vec![glean_restarted.clone()];
972        let glean_start_time = glean.start_time();
973
974        glean
975            .event_storage()
976            .normalize_store(&glean, store_name, &mut store, glean_start_time);
977        assert!(store.is_empty());
978
979        let mut store = vec![glean_restarted.clone(), glean_restarted.clone()];
980        glean
981            .event_storage()
982            .normalize_store(&glean, store_name, &mut store, glean_start_time);
983        assert!(store.is_empty());
984
985        let mut store = vec![
986            glean_restarted.clone(),
987            glean_restarted.clone(),
988            glean_restarted,
989        ];
990        glean
991            .event_storage()
992            .normalize_store(&glean, store_name, &mut store, glean_start_time);
993        assert!(store.is_empty());
994    }
995
996    #[test]
997    fn normalize_store_of_glean_restarted_on_both_ends() {
998        // Make sure stores with non-glean.restarted events don't get drained too far.
999        let (glean, _dir) = new_glean(None);
1000
1001        let store_name = "store-name";
1002        let glean_restarted = StoredEvent {
1003            event: RecordedEvent {
1004                timestamp: 2,
1005                category: "glean".into(),
1006                name: "restarted".into(),
1007                extra: None,
1008                session: None,
1009            },
1010            execution_counter: None,
1011        };
1012        let not_glean_restarted = StoredEvent {
1013            event: RecordedEvent {
1014                timestamp: 20,
1015                category: "category".into(),
1016                name: "name".into(),
1017                extra: None,
1018                session: None,
1019            },
1020            execution_counter: None,
1021        };
1022        let mut store = vec![
1023            glean_restarted.clone(),
1024            not_glean_restarted.clone(),
1025            glean_restarted,
1026        ];
1027        let glean_start_time = glean.start_time();
1028
1029        glean
1030            .event_storage()
1031            .normalize_store(&glean, store_name, &mut store, glean_start_time);
1032        assert_eq!(1, store.len());
1033        assert_eq!(
1034            StoredEvent {
1035                event: RecordedEvent {
1036                    timestamp: 0,
1037                    ..not_glean_restarted.event
1038                },
1039                execution_counter: None
1040            },
1041            store[0]
1042        );
1043    }
1044
1045    #[test]
1046    fn normalize_store_single_run_timestamp_math() {
1047        // With a single run of events (no non-initial or non-terminal `glean.restarted`),
1048        // ensure the timestamp math works.
1049        // (( works = Initial event gets to be 0, subsequent events get normalized to that 0 ))
1050        let (glean, _dir) = new_glean(None);
1051
1052        let store_name = "store-name";
1053        let glean_restarted = StoredEvent {
1054            event: RecordedEvent {
1055                timestamp: 2,
1056                category: "glean".into(),
1057                name: "restarted".into(),
1058                extra: None,
1059                session: None,
1060            },
1061            execution_counter: None,
1062        };
1063        let timestamps = [20, 40, 200];
1064        let not_glean_restarted = StoredEvent {
1065            event: RecordedEvent {
1066                timestamp: timestamps[0],
1067                category: "category".into(),
1068                name: "name".into(),
1069                extra: None,
1070                session: None,
1071            },
1072            execution_counter: None,
1073        };
1074        let mut store = vec![
1075            glean_restarted.clone(),
1076            not_glean_restarted.clone(),
1077            StoredEvent {
1078                event: RecordedEvent {
1079                    timestamp: timestamps[1],
1080                    ..not_glean_restarted.event.clone()
1081                },
1082                execution_counter: None,
1083            },
1084            StoredEvent {
1085                event: RecordedEvent {
1086                    timestamp: timestamps[2],
1087                    ..not_glean_restarted.event.clone()
1088                },
1089                execution_counter: None,
1090            },
1091            glean_restarted,
1092        ];
1093
1094        glean
1095            .event_storage()
1096            .normalize_store(&glean, store_name, &mut store, glean.start_time());
1097        assert_eq!(3, store.len());
1098        for (timestamp, event) in timestamps.iter().zip(store.iter()) {
1099            assert_eq!(
1100                &StoredEvent {
1101                    event: RecordedEvent {
1102                        timestamp: timestamp - timestamps[0],
1103                        ..not_glean_restarted.clone().event
1104                    },
1105                    execution_counter: None
1106                },
1107                event
1108            );
1109        }
1110    }
1111
1112    #[test]
1113    fn normalize_store_multi_run_timestamp_math() {
1114        // With multiple runs of events (separated by `glean.restarted`),
1115        // ensure the timestamp math works.
1116        // (( works = Initial event gets to be 0, subsequent events get normalized to that 0.
1117        //            Subsequent runs figure it out via glean.restarted.date and ping_info.start_time ))
1118        let (glean, _dir) = new_glean(None);
1119
1120        let store_name = "store-name";
1121        let glean_restarted = StoredEvent {
1122            event: RecordedEvent {
1123                category: "glean".into(),
1124                name: "restarted".into(),
1125                ..Default::default()
1126            },
1127            execution_counter: None,
1128        };
1129        let not_glean_restarted = StoredEvent {
1130            event: RecordedEvent {
1131                category: "category".into(),
1132                name: "name".into(),
1133                ..Default::default()
1134            },
1135            execution_counter: None,
1136        };
1137
1138        // This scenario represents a run of three events followed by an hour between runs,
1139        // followed by one final event.
1140        let timestamps = [20, 40, 200, 12];
1141        let ecs = [0, 1];
1142        let some_hour = 16;
1143        let startup_date = FixedOffset::east_opt(0)
1144            .unwrap()
1145            .with_ymd_and_hms(2022, 11, 24, some_hour, 29, 0) // TimeUnit::Minute -- don't put seconds
1146            .unwrap();
1147        let glean_start_time = startup_date.with_hour(some_hour - 1);
1148        let restarted_ts = 2;
1149        let mut store = vec![
1150            StoredEvent {
1151                event: RecordedEvent {
1152                    timestamp: timestamps[0],
1153                    ..not_glean_restarted.event.clone()
1154                },
1155                execution_counter: Some(ecs[0]),
1156            },
1157            StoredEvent {
1158                event: RecordedEvent {
1159                    timestamp: timestamps[1],
1160                    ..not_glean_restarted.event.clone()
1161                },
1162                execution_counter: Some(ecs[0]),
1163            },
1164            StoredEvent {
1165                event: RecordedEvent {
1166                    timestamp: timestamps[2],
1167                    ..not_glean_restarted.event.clone()
1168                },
1169                execution_counter: Some(ecs[0]),
1170            },
1171            StoredEvent {
1172                event: RecordedEvent {
1173                    extra: Some(
1174                        [(
1175                            "glean.startup.date".into(),
1176                            get_iso_time_string(startup_date, TimeUnit::Minute),
1177                        )]
1178                        .into(),
1179                    ),
1180                    timestamp: restarted_ts,
1181                    ..glean_restarted.event.clone()
1182                },
1183                execution_counter: Some(ecs[1]),
1184            },
1185            StoredEvent {
1186                event: RecordedEvent {
1187                    timestamp: timestamps[3],
1188                    ..not_glean_restarted.event.clone()
1189                },
1190                execution_counter: Some(ecs[1]),
1191            },
1192        ];
1193
1194        glean.event_storage().normalize_store(
1195            &glean,
1196            store_name,
1197            &mut store,
1198            glean_start_time.unwrap(),
1199        );
1200        assert_eq!(5, store.len()); // 4 "real" events plus 1 `glean.restarted`
1201
1202        // Let's check the first three.
1203        for (timestamp, event) in timestamps[..timestamps.len() - 1].iter().zip(store.clone()) {
1204            assert_eq!(
1205                StoredEvent {
1206                    event: RecordedEvent {
1207                        timestamp: timestamp - timestamps[0],
1208                        ..not_glean_restarted.event.clone()
1209                    },
1210                    execution_counter: None,
1211                },
1212                event
1213            );
1214        }
1215        // The fourth should be a glean.restarted and have a realtime-based timestamp.
1216        let hour_in_millis = 3600000;
1217        assert_eq!(
1218            store[3],
1219            StoredEvent {
1220                event: RecordedEvent {
1221                    timestamp: hour_in_millis,
1222                    ..glean_restarted.event
1223                },
1224                execution_counter: None,
1225            }
1226        );
1227        // The fifth should have a timestamp based on the new origin.
1228        assert_eq!(
1229            store[4],
1230            StoredEvent {
1231                event: RecordedEvent {
1232                    timestamp: hour_in_millis + timestamps[3] - restarted_ts,
1233                    ..not_glean_restarted.event
1234                },
1235                execution_counter: None,
1236            }
1237        );
1238    }
1239
1240    #[test]
1241    fn normalize_store_multi_run_client_clocks() {
1242        // With multiple runs of events (separated by `glean.restarted`),
1243        // ensure the timestamp math works. Even when the client clock goes backwards.
1244        let (glean, _dir) = new_glean(None);
1245
1246        let store_name = "store-name";
1247        let glean_restarted = StoredEvent {
1248            event: RecordedEvent {
1249                category: "glean".into(),
1250                name: "restarted".into(),
1251                ..Default::default()
1252            },
1253            execution_counter: None,
1254        };
1255        let not_glean_restarted = StoredEvent {
1256            event: RecordedEvent {
1257                category: "category".into(),
1258                name: "name".into(),
1259                ..Default::default()
1260            },
1261            execution_counter: None,
1262        };
1263
1264        // This scenario represents a run of two events followed by negative one hours between runs,
1265        // followed by two more events.
1266        let timestamps = [20, 40, 12, 200];
1267        let ecs = [0, 1];
1268        let some_hour = 10;
1269        let startup_date = FixedOffset::east_opt(0)
1270            .unwrap()
1271            .with_ymd_and_hms(2022, 11, 25, some_hour, 37, 0) // TimeUnit::Minute -- don't put seconds
1272            .unwrap();
1273        let glean_start_time = startup_date.with_hour(some_hour + 1);
1274        let restarted_ts = 2;
1275        let mut store = vec![
1276            StoredEvent {
1277                event: RecordedEvent {
1278                    timestamp: timestamps[0],
1279                    ..not_glean_restarted.event.clone()
1280                },
1281                execution_counter: Some(ecs[0]),
1282            },
1283            StoredEvent {
1284                event: RecordedEvent {
1285                    timestamp: timestamps[1],
1286                    ..not_glean_restarted.event.clone()
1287                },
1288                execution_counter: Some(ecs[0]),
1289            },
1290            StoredEvent {
1291                event: RecordedEvent {
1292                    extra: Some(
1293                        [(
1294                            "glean.startup.date".into(),
1295                            get_iso_time_string(startup_date, TimeUnit::Minute),
1296                        )]
1297                        .into(),
1298                    ),
1299                    timestamp: restarted_ts,
1300                    ..glean_restarted.event.clone()
1301                },
1302                execution_counter: Some(ecs[1]),
1303            },
1304            StoredEvent {
1305                event: RecordedEvent {
1306                    timestamp: timestamps[2],
1307                    ..not_glean_restarted.event.clone()
1308                },
1309                execution_counter: Some(ecs[1]),
1310            },
1311            StoredEvent {
1312                event: RecordedEvent {
1313                    timestamp: timestamps[3],
1314                    ..not_glean_restarted.event.clone()
1315                },
1316                execution_counter: Some(ecs[1]),
1317            },
1318        ];
1319
1320        glean.event_storage().normalize_store(
1321            &glean,
1322            store_name,
1323            &mut store,
1324            glean_start_time.unwrap(),
1325        );
1326        assert_eq!(5, store.len()); // 4 "real" events plus 1 `glean.restarted`
1327
1328        // Let's check the first two.
1329        for (timestamp, event) in timestamps[..timestamps.len() - 2].iter().zip(store.clone()) {
1330            assert_eq!(
1331                StoredEvent {
1332                    event: RecordedEvent {
1333                        timestamp: timestamp - timestamps[0],
1334                        ..not_glean_restarted.event.clone()
1335                    },
1336                    execution_counter: None,
1337                },
1338                event
1339            );
1340        }
1341        // The third should be a glean.restarted. Its timestamp should be
1342        // one larger than the largest timestamp seen so far (because that's
1343        // how we ensure monotonic timestamps when client clocks go backwards).
1344        assert_eq!(
1345            store[2],
1346            StoredEvent {
1347                event: RecordedEvent {
1348                    timestamp: store[1].event.timestamp + 1,
1349                    ..glean_restarted.event
1350                },
1351                execution_counter: None,
1352            }
1353        );
1354        // The fifth should have a timestamp based on the new origin.
1355        assert_eq!(
1356            store[3],
1357            StoredEvent {
1358                event: RecordedEvent {
1359                    timestamp: timestamps[2] - restarted_ts + store[2].event.timestamp,
1360                    ..not_glean_restarted.event
1361                },
1362                execution_counter: None,
1363            }
1364        );
1365        // And we should have an InvalidValue on glean.restarted to show for it.
1366        assert_eq!(
1367            Ok(1),
1368            test_get_num_recorded_errors(
1369                &glean,
1370                &CommonMetricData {
1371                    name: "restarted".into(),
1372                    category: "glean".into(),
1373                    send_in_pings: vec![store_name.into()],
1374                    lifetime: Lifetime::Ping,
1375                    ..Default::default()
1376                }
1377                .into(),
1378                ErrorType::InvalidValue
1379            )
1380        );
1381    }
1382
1383    #[test]
1384    fn normalize_store_non_zero_ec() {
1385        // After the first run, execution_counter will likely be non-zero.
1386        // Ensure normalizing a store that begins with non-zero ec works.
1387        let (glean, _dir) = new_glean(None);
1388
1389        let store_name = "store-name";
1390        let glean_restarted = StoredEvent {
1391            event: RecordedEvent {
1392                timestamp: 2,
1393                category: "glean".into(),
1394                name: "restarted".into(),
1395                extra: None,
1396                session: None,
1397            },
1398            execution_counter: Some(2),
1399        };
1400        let not_glean_restarted = StoredEvent {
1401            event: RecordedEvent {
1402                timestamp: 20,
1403                category: "category".into(),
1404                name: "name".into(),
1405                extra: None,
1406                session: None,
1407            },
1408            execution_counter: Some(2),
1409        };
1410        let glean_restarted_2 = StoredEvent {
1411            event: RecordedEvent {
1412                timestamp: 2,
1413                category: "glean".into(),
1414                name: "restarted".into(),
1415                extra: None,
1416                session: None,
1417            },
1418            execution_counter: Some(3),
1419        };
1420        let mut store = vec![
1421            glean_restarted,
1422            not_glean_restarted.clone(),
1423            glean_restarted_2,
1424        ];
1425        let glean_start_time = glean.start_time();
1426
1427        glean
1428            .event_storage()
1429            .normalize_store(&glean, store_name, &mut store, glean_start_time);
1430
1431        assert_eq!(1, store.len());
1432        assert_eq!(
1433            StoredEvent {
1434                event: RecordedEvent {
1435                    timestamp: 0,
1436                    ..not_glean_restarted.event
1437                },
1438                execution_counter: None
1439            },
1440            store[0]
1441        );
1442        // And we should have no InvalidState errors on glean.restarted.
1443        assert!(test_get_num_recorded_errors(
1444            &glean,
1445            &CommonMetricData {
1446                name: "restarted".into(),
1447                category: "glean".into(),
1448                send_in_pings: vec![store_name.into()],
1449                lifetime: Lifetime::Ping,
1450                ..Default::default()
1451            }
1452            .into(),
1453            ErrorType::InvalidState
1454        )
1455        .is_err());
1456        // (and, just because we're here, double-check there are no InvalidValue either).
1457        assert!(test_get_num_recorded_errors(
1458            &glean,
1459            &CommonMetricData {
1460                name: "restarted".into(),
1461                category: "glean".into(),
1462                send_in_pings: vec![store_name.into()],
1463                lifetime: Lifetime::Ping,
1464                ..Default::default()
1465            }
1466            .into(),
1467            ErrorType::InvalidValue
1468        )
1469        .is_err());
1470    }
1471
1472    #[test]
1473    fn normalize_store_clamps_timestamp() {
1474        let (glean, _dir) = new_glean(None);
1475
1476        let store_name = "store-name";
1477        let event = RecordedEvent {
1478            category: "category".into(),
1479            name: "name".into(),
1480            ..Default::default()
1481        };
1482
1483        let timestamps = [
1484            0,
1485            (i64::MAX / 2) as u64,
1486            i64::MAX as _,
1487            (i64::MAX as u64) + 1,
1488        ];
1489        let mut store = timestamps
1490            .into_iter()
1491            .map(|timestamp| StoredEvent {
1492                event: RecordedEvent {
1493                    timestamp,
1494                    ..event.clone()
1495                },
1496                execution_counter: None,
1497            })
1498            .collect();
1499
1500        let glean_start_time = glean.start_time();
1501        glean
1502            .event_storage()
1503            .normalize_store(&glean, store_name, &mut store, glean_start_time);
1504        assert_eq!(4, store.len());
1505
1506        assert_eq!(0, store[0].event.timestamp);
1507        assert_eq!((i64::MAX / 2) as u64, store[1].event.timestamp);
1508        assert_eq!((i64::MAX as u64), store[2].event.timestamp);
1509        assert_eq!((i64::MAX as u64), store[3].event.timestamp);
1510    }
1511
1512    #[test]
1513    fn normalize_store_clamps_timestamp_metric_enabled() {
1514        let (glean, _dir) = new_glean(None);
1515
1516        let mut cfg = RemoteSettingsConfig::default();
1517        cfg.metrics_enabled
1518            .insert("glean.error.event_timestamp_clamped".to_string(), true);
1519        glean.apply_server_knobs_config(cfg);
1520
1521        let store_name = "store-name";
1522        let event = RecordedEvent {
1523            category: "category".into(),
1524            name: "name".into(),
1525            ..Default::default()
1526        };
1527
1528        let timestamps = [0, (i64::MAX as u64) + 1];
1529        let mut store = timestamps
1530            .into_iter()
1531            .map(|timestamp| StoredEvent {
1532                event: RecordedEvent {
1533                    timestamp,
1534                    ..event.clone()
1535                },
1536                execution_counter: None,
1537            })
1538            .collect();
1539
1540        let glean_start_time = glean.start_time();
1541        glean
1542            .event_storage()
1543            .normalize_store(&glean, store_name, &mut store, glean_start_time);
1544        assert_eq!(2, store.len());
1545
1546        assert_eq!(0, store[0].event.timestamp);
1547        assert_eq!((i64::MAX as u64), store[1].event.timestamp);
1548
1549        let error_count = glean
1550            .additional_metrics
1551            .event_timestamp_clamped
1552            .get_value(&glean, "health");
1553        assert_eq!(Some(1), error_count);
1554    }
1555}