Skip to main content

glean_core/core/
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::collections::HashMap;
6use std::fs::{self, File};
7use std::io::{self, Write};
8use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicU8, Ordering};
10use std::sync::{Arc, Mutex};
11use std::time::Duration;
12
13use chrono::{DateTime, FixedOffset, SecondsFormat, Utc};
14use malloc_size_of_derive::MallocSizeOf;
15use once_cell::sync::OnceCell;
16use uuid::Uuid;
17
18#[cfg(feature = "sqlite")]
19use crate::database::sqlite::MigrationResult;
20use crate::database::Database;
21use crate::debug::DebugOptions;
22use crate::error::ClientIdFileError;
23use crate::event_database::EventDatabase;
24use crate::internal_metrics::{
25    AdditionalMetrics, CoreMetrics, DatabaseMetrics, ExceptionState, HealthMetrics,
26};
27use crate::internal_pings::InternalPings;
28use crate::metrics::{
29    self, ExperimentMetric, Metric, MetricType, PingType, RecordedExperiment, RemoteSettingsConfig,
30};
31use crate::ping::PingMaker;
32use crate::session::{self, EventSessionContext, SessionManager, SessionMode, SessionState};
33use crate::storage::{StorageManager, INTERNAL_STORAGE};
34use crate::upload::{PingUploadManager, PingUploadTask, UploadResult, UploadTaskAction};
35#[cfg(feature = "sqlite")]
36use crate::util::truncate_string_at_boundary;
37use crate::util::{local_now_with_offset, sanitize_application_id};
38use crate::{
39    scheduler, system, AttributionMetrics, CommonMetricData, DistributionMetrics, ErrorKind,
40    InternalConfiguration, Lifetime, PingRateLimit, Result, DEFAULT_MAX_EVENTS,
41    GLEAN_SCHEMA_VERSION, GLEAN_VERSION, KNOWN_CLIENT_ID,
42};
43
44const CLIENT_ID_PLAIN_FILENAME: &str = "client_id.txt";
45static GLEAN: OnceCell<Mutex<Glean>> = OnceCell::new();
46
47/// Rate limiting defaults
48/// 15 pings every 60 seconds.
49pub const DEFAULT_SECONDS_PER_INTERVAL: u64 = 60;
50pub const DEFAULT_PINGS_PER_INTERVAL: u32 = 15;
51
52pub fn global_glean() -> Option<&'static Mutex<Glean>> {
53    GLEAN.get()
54}
55
56/// Sets or replaces the global Glean object.
57pub fn setup_glean(glean: Glean) -> Result<()> {
58    // The `OnceCell` type wrapping our Glean is thread-safe and can only be set once.
59    // Therefore even if our check for it being empty succeeds, setting it could fail if a
60    // concurrent thread is quicker in setting it.
61    // However this will not cause a bigger problem, as the second `set` operation will just fail.
62    // We can log it and move on.
63    //
64    // For all wrappers this is not a problem, as the Glean object is intialized exactly once on
65    // calling `initialize` on the global singleton and further operations check that it has been
66    // initialized.
67    if GLEAN.get().is_none() {
68        if GLEAN.set(Mutex::new(glean)).is_err() {
69            log::warn!(
70                "Global Glean object is initialized already. This probably happened concurrently."
71            )
72        }
73    } else {
74        // We allow overriding the global Glean object to support test mode.
75        // In test mode the Glean object is fully destroyed and recreated.
76        // This all happens behind a mutex and is therefore also thread-safe..
77        let mut lock = GLEAN.get().unwrap().lock().unwrap();
78        *lock = glean;
79    }
80    Ok(())
81}
82
83/// Execute `f` passing the global Glean object.
84///
85/// Panics if the global Glean object has not been set.
86pub fn with_glean<F, R>(f: F) -> R
87where
88    F: FnOnce(&Glean) -> R,
89{
90    let glean = global_glean().expect("Global Glean object not initialized");
91    let lock = glean.lock().unwrap();
92    f(&lock)
93}
94
95/// Execute `f` passing the global Glean object mutable.
96///
97/// Panics if the global Glean object has not been set.
98pub fn with_glean_mut<F, R>(f: F) -> R
99where
100    F: FnOnce(&mut Glean) -> R,
101{
102    let glean = global_glean().expect("Global Glean object not initialized");
103    let mut lock = glean.lock().unwrap();
104    f(&mut lock)
105}
106
107/// Execute `f` passing the global Glean object if it has been set.
108///
109/// Returns `None` if the global Glean object has not been set.
110/// Returns `Some(T)` otherwise.
111pub fn with_opt_glean<F, R>(f: F) -> Option<R>
112where
113    F: FnOnce(&Glean) -> R,
114{
115    let glean = global_glean()?;
116    let lock = glean.lock().unwrap();
117    Some(f(&lock))
118}
119
120/// The object holding meta information about a Glean instance.
121///
122/// ## Example
123///
124/// Create a new Glean instance, register a ping, record a simple counter and then send the final
125/// ping.
126///
127/// ```rust,no_run
128/// # use glean_core::{Glean, InternalConfiguration, CommonMetricData, metrics::*};
129/// let cfg = InternalConfiguration {
130///     data_path: "/tmp/glean".into(),
131///     application_id: "glean.sample.app".into(),
132///     language_binding_name: "Rust".into(),
133///     upload_enabled: true,
134///     max_events: None,
135///     delay_ping_lifetime_io: false,
136///     app_build: "".into(),
137///     use_core_mps: false,
138///     trim_data_to_registered_pings: false,
139///     log_level: None,
140///     rate_limit: None,
141///     enable_event_timestamps: true,
142///     experimentation_id: None,
143///     enable_internal_pings: true,
144///     ping_schedule: Default::default(),
145///     ping_lifetime_threshold: 1000,
146///     ping_lifetime_max_time: 2000,
147///     max_pending_pings_count: None,
148///     max_pending_pings_directory_size: None,
149///     session_mode: glean_core::SessionMode::Auto,
150///     session_sample_rate: 1.0,
151///     session_inactivity_timeout_ms: 1_800_000,
152///     events_ping_acceleration_factor: None,
153///     enable_store_submitted_pings: false,
154/// };
155/// let mut glean = Glean::new(cfg).unwrap();
156/// let ping = PingType::new("sample", true, false, true, true, true, vec![], vec![], true, vec![]);
157/// glean.register_ping_type(&ping);
158///
159/// let call_counter: CounterMetric = CounterMetric::new(CommonMetricData {
160///     name: "calls".into(),
161///     category: "local".into(),
162///     send_in_pings: vec!["sample".into()],
163///     ..Default::default()
164/// });
165///
166/// call_counter.add_sync(&glean, 1);
167///
168/// ping.submit_sync(&glean, None);
169/// ```
170///
171/// ## Note
172///
173/// In specific language bindings, this is usually wrapped in a singleton and all metric recording goes to a single instance of this object.
174/// In the Rust core, it is possible to create multiple instances, which is used in testing.
175#[derive(Debug, MallocSizeOf)]
176pub struct Glean {
177    upload_enabled: bool,
178    pub(crate) data_store: Option<Database>,
179    event_data_store: EventDatabase,
180    pub(crate) core_metrics: CoreMetrics,
181    pub(crate) additional_metrics: AdditionalMetrics,
182    pub(crate) database_metrics: DatabaseMetrics,
183    pub(crate) health_metrics: HealthMetrics,
184    pub(crate) internal_pings: InternalPings,
185    data_path: PathBuf,
186    application_id: String,
187    ping_registry: HashMap<String, PingType>,
188    #[ignore_malloc_size_of = "external non-allocating type"]
189    start_time: DateTime<FixedOffset>,
190    max_events: u32,
191    is_first_run: bool,
192    pub(crate) upload_manager: PingUploadManager,
193    debug: DebugOptions,
194    pub(crate) app_build: String,
195    pub(crate) schedule_metrics_pings: bool,
196    pub(crate) remote_settings_epoch: AtomicU8,
197    #[ignore_malloc_size_of = "TODO: Expose Glean's inner memory allocations (bug 1960592)"]
198    pub(crate) remote_settings_config: Arc<Mutex<RemoteSettingsConfig>>,
199    pub(crate) with_timestamps: bool,
200    pub(crate) ping_schedule: HashMap<String, Vec<String>>,
201    #[ignore_malloc_size_of = "TODO: Expose session memory allocations (bug 2043355)"]
202    pub(crate) session_manager: SessionManager,
203    events_ping_acceleration_factor: Option<usize>,
204    pub(crate) store_submitted_pings_enabled: bool,
205}
206
207impl Glean {
208    /// Creates and initializes a new Glean object for use in a subprocess.
209    ///
210    /// Importantly, this will not send any pings at startup, since that
211    /// sort of management should only happen in the main process.
212    pub fn new_for_subprocess(cfg: &InternalConfiguration, scan_directories: bool) -> Result<Self> {
213        log::info!("Creating new Glean v{}", GLEAN_VERSION);
214
215        let application_id = sanitize_application_id(&cfg.application_id);
216        if application_id.is_empty() {
217            return Err(ErrorKind::InvalidConfig.into());
218        }
219
220        let data_path = Path::new(&cfg.data_path);
221        let event_data_store = EventDatabase::new(data_path)?;
222
223        // Create an upload manager with rate limiting of 15 pings every 60 seconds.
224        let mut upload_manager = PingUploadManager::new(&cfg.data_path, &cfg.language_binding_name);
225        let rate_limit = cfg.rate_limit.as_ref().unwrap_or(&PingRateLimit {
226            seconds_per_interval: DEFAULT_SECONDS_PER_INTERVAL,
227            pings_per_interval: DEFAULT_PINGS_PER_INTERVAL,
228        });
229        upload_manager.set_rate_limiter(
230            rate_limit.seconds_per_interval,
231            rate_limit.pings_per_interval,
232        );
233        if let Some(n) = cfg.max_pending_pings_count {
234            upload_manager.set_max_pending_pings_count(n);
235        }
236        if let Some(n) = cfg.max_pending_pings_directory_size {
237            upload_manager.set_max_pending_pings_directory_size(n);
238        }
239
240        // We only scan the pending ping directories when calling this from a subprocess,
241        // when calling this from ::new we need to scan the directories after dealing with the upload state.
242        if scan_directories {
243            let _scanning_thread = upload_manager.scan_pending_pings_directories(false);
244        }
245
246        let start_time = local_now_with_offset();
247        let mut this = Self {
248            upload_enabled: cfg.upload_enabled,
249            // In the subprocess, we want to avoid accessing the database entirely.
250            // The easiest way to ensure that is to just not initialize it.
251            data_store: None,
252            event_data_store,
253            core_metrics: CoreMetrics::new(),
254            additional_metrics: AdditionalMetrics::new(),
255            database_metrics: DatabaseMetrics::new(),
256            health_metrics: HealthMetrics::new(),
257            internal_pings: InternalPings::new(cfg.enable_internal_pings),
258            upload_manager,
259            data_path: PathBuf::from(&cfg.data_path),
260            application_id,
261            ping_registry: HashMap::new(),
262            start_time,
263            max_events: cfg.max_events.unwrap_or(DEFAULT_MAX_EVENTS),
264            is_first_run: false,
265            debug: DebugOptions::new(),
266            app_build: cfg.app_build.to_string(),
267            // Subprocess doesn't use "metrics" pings so has no need for a scheduler.
268            schedule_metrics_pings: false,
269            remote_settings_epoch: AtomicU8::new(0),
270            remote_settings_config: Arc::new(Mutex::new(RemoteSettingsConfig::new())),
271            with_timestamps: cfg.enable_event_timestamps,
272            ping_schedule: cfg.ping_schedule.clone(),
273            // The SessionManager is deliberately left in its default (hollow)
274            // state for subprocesses. `restore_session_state_from_storage()`
275            // is only called in `Glean::new()`, not here, so the subprocess
276            // never loads or mutates the main process's persisted session
277            // state. This prevents subprocesses from interfering with the
278            // main process's session lifecycle (seq counters, dirty flags,
279            // boundary events, etc.).
280            session_manager: SessionManager::new(
281                cfg.session_mode,
282                cfg.session_sample_rate,
283                std::time::Duration::from_millis(cfg.session_inactivity_timeout_ms),
284            ),
285            events_ping_acceleration_factor: cfg
286                .events_ping_acceleration_factor
287                .map(|x| x as usize),
288            store_submitted_pings_enabled: cfg.enable_store_submitted_pings,
289        };
290
291        // Ensuring these pings are registered.
292        let pings = this.internal_pings.clone();
293        this.register_ping_type(&pings.baseline);
294        this.register_ping_type(&pings.metrics);
295        this.register_ping_type(&pings.events);
296        this.register_ping_type(&pings.health);
297        this.register_ping_type(&pings.deletion_request);
298
299        Ok(this)
300    }
301
302    /// Creates and initializes a new Glean object.
303    ///
304    /// This will create the necessary directories and files in
305    /// [`cfg.data_path`](InternalConfiguration::data_path). This will also initialize
306    /// the core metrics.
307    pub fn new(cfg: InternalConfiguration) -> Result<Self> {
308        let mut glean = Self::new_for_subprocess(&cfg, false)?;
309
310        // Creating the data store creates the necessary path as well.
311        // If that fails we bail out and don't initialize further.
312        let data_path = Path::new(&cfg.data_path);
313        let ping_lifetime_threshold = cfg.ping_lifetime_threshold as usize;
314        let ping_lifetime_max_time = Duration::from_millis(cfg.ping_lifetime_max_time);
315        glean.data_store = Some(Database::new(
316            data_path,
317            cfg.delay_ping_lifetime_io,
318            ping_lifetime_threshold,
319            ping_lifetime_max_time,
320        )?);
321
322        #[cfg(feature = "sqlite")]
323        if let Some(state) = glean.data_store.as_mut().unwrap().migration_state.take() {
324            glean
325                .database_metrics
326                .migrated_metrics
327                .add_sync(&glean, state.migrated_metrics);
328            glean
329                .database_metrics
330                .metrics_in_sqlite
331                .add_sync(&glean, state.metrics_in_sql);
332            glean
333                .database_metrics
334                .failed_metrics
335                .add_sync(&glean, state.failed_metrics);
336
337            let duration_ns = state.duration.as_nanos().try_into().unwrap_or(u64::MAX);
338            glean
339                .database_metrics
340                .migration_duration
341                .accumulate_raw_samples_nanos_sync(&glean, &[duration_ns]);
342        }
343
344        #[cfg(feature = "sqlite")]
345        if glean.data_store.as_mut().unwrap().migration_error == MigrationResult::Error {
346            glean.database_metrics.migration_error.add_sync(&glean, 1);
347        }
348
349        glean.restore_session_state_from_storage();
350
351        // This code references different states from the "Client ID recovery" flowchart.
352        // See https://mozilla.github.io/glean/dev/core/internal/client_id_recovery.html for details.
353
354        // We don't have the database yet when we first encounter the error,
355        // so we store it and apply it later.
356        // state (a)
357        let stored_client_id = match glean.client_id_from_file() {
358            Ok(id) if id == *KNOWN_CLIENT_ID => {
359                glean
360                    .health_metrics
361                    .file_read_error
362                    .get("c0ffee-in-file")
363                    .add_sync(&glean, 1);
364                None
365            }
366            Ok(id) => Some(id),
367            Err(ClientIdFileError::NotFound) => {
368                // That's ok, the file might just not exist yet.
369                glean
370                    .health_metrics
371                    .file_read_error
372                    .get("file-not-found")
373                    .add_sync(&glean, 1);
374                None
375            }
376            Err(ClientIdFileError::PermissionDenied) => {
377                // state (b)
378                // Uhm ... who removed our permission?
379                glean
380                    .health_metrics
381                    .file_read_error
382                    .get("permission-denied")
383                    .add_sync(&glean, 1);
384                None
385            }
386            Err(ClientIdFileError::ParseError(e)) => {
387                // state (b)
388                log::trace!("reading cliend_id.txt. Could not parse into UUID: {e}");
389                glean
390                    .health_metrics
391                    .file_read_error
392                    .get("parse")
393                    .add_sync(&glean, 1);
394                None
395            }
396            Err(ClientIdFileError::IoError(e)) => {
397                // state (b)
398                // We can't handle other IO errors (most couldn't occur on this operation anyway)
399                log::trace!("reading client_id.txt. Unexpected io error: {e}");
400                glean
401                    .health_metrics
402                    .file_read_error
403                    .get("io")
404                    .add_sync(&glean, 1);
405                None
406            }
407        };
408
409        {
410            let data_store = glean.data_store.as_ref().unwrap();
411            let file_size = data_store.file_size().map(|n| n.get()).unwrap_or(0);
412
413            // If we have a client ID on disk, we check the database
414            if let Some(stored_client_id) = stored_client_id {
415                // state (c)
416                if file_size == 0 {
417                    log::trace!("no database. database size={file_size}. stored_client_id={stored_client_id}");
418                    // state (d)
419                    glean
420                        .health_metrics
421                        .recovered_client_id
422                        .set_from_uuid_sync(&glean, stored_client_id);
423                    glean
424                        .health_metrics
425                        .exception_state
426                        .set_sync(&glean, ExceptionState::EmptyDb);
427
428                    // state (e) -- mitigation: store recovered client ID in DB
429                    glean
430                        .core_metrics
431                        .client_id
432                        .set_from_uuid_sync(&glean, stored_client_id);
433                } else {
434                    let db_client_id = glean
435                        .core_metrics
436                        .client_id
437                        .get_value(&glean, Some("glean_client_info"));
438
439                    match db_client_id {
440                        None => {
441                            // state (f)
442                            log::trace!("no client_id in DB. stored_client_id={stored_client_id}");
443                            glean
444                                .health_metrics
445                                .exception_state
446                                .set_sync(&glean, ExceptionState::RegenDb);
447
448                            // state (e) -- mitigation: store recovered client ID in DB
449                            glean
450                                .core_metrics
451                                .client_id
452                                .set_from_uuid_sync(&glean, stored_client_id);
453                        }
454                        Some(db_client_id) if db_client_id == *KNOWN_CLIENT_ID => {
455                            // state (i)
456                            log::trace!(
457                                "c0ffee client_id in DB, stored_client_id={stored_client_id}"
458                            );
459                            glean
460                                .health_metrics
461                                .recovered_client_id
462                                .set_from_uuid_sync(&glean, stored_client_id);
463                            glean
464                                .health_metrics
465                                .exception_state
466                                .set_sync(&glean, ExceptionState::C0ffeeInDb);
467
468                            // If we have a recovered client ID we also overwrite the database.
469                            // state (e)
470                            glean
471                                .core_metrics
472                                .client_id
473                                .set_from_uuid_sync(&glean, stored_client_id);
474                        }
475                        Some(db_client_id) if db_client_id == stored_client_id => {
476                            // all valid. nothing to do
477                            log::trace!("database consistent. db_client_id == stored_client_id: {db_client_id}");
478                        }
479                        Some(db_client_id) => {
480                            // state (g)
481                            log::trace!(
482                                "client_id mismatch. db_client_id{db_client_id}, stored_client_id={stored_client_id}. Overwriting file with db's client_id."
483                            );
484                            glean
485                                .health_metrics
486                                .recovered_client_id
487                                .set_from_uuid_sync(&glean, stored_client_id);
488                            glean
489                                .health_metrics
490                                .exception_state
491                                .set_sync(&glean, ExceptionState::ClientIdMismatch);
492
493                            // state (h)
494                            glean.store_client_id_with_reporting(
495                                db_client_id,
496                                "client_id mismatch will re-occur.",
497                            );
498                        }
499                    }
500                }
501            } else {
502                log::trace!("No stored client ID. Database might have it.");
503
504                let db_client_id = glean
505                    .core_metrics
506                    .client_id
507                    .get_value(&glean, Some("glean_client_info"));
508                if let Some(db_client_id) = db_client_id {
509                    // state (h)
510                    glean.store_client_id_with_reporting(
511                        db_client_id,
512                        "Might happen on next init then.",
513                    );
514                } else {
515                    log::trace!("Database has no client ID either. We might be fresh!");
516                }
517            }
518        }
519
520        // Set experimentation identifier (if any)
521        if let Some(experimentation_id) = &cfg.experimentation_id {
522            glean
523                .additional_metrics
524                .experimentation_id
525                .set_sync(&glean, experimentation_id.to_string());
526        }
527
528        // The upload enabled flag may have changed since the last run, for
529        // example by the changing of a config file.
530        if cfg.upload_enabled {
531            // If upload is enabled, just follow the normal code path to
532            // instantiate the core metrics.
533            glean.on_upload_enabled();
534        } else {
535            // If upload is disabled, then clear the metrics
536            // but do not send a deletion request ping.
537            // If we have run before, and we have an old client_id,
538            // do the full upload disabled operations to clear metrics
539            // and send a deletion request ping.
540            match glean
541                .core_metrics
542                .client_id
543                .get_value(&glean, Some("glean_client_info"))
544            {
545                None => glean.clear_metrics(),
546                Some(uuid) => {
547                    if let Err(e) = glean.remove_stored_client_id() {
548                        log::error!("Couldn't remove client ID on disk. This might lead to a resurrection of this client ID later. Error: {e}");
549                    }
550                    if uuid == *KNOWN_CLIENT_ID {
551                        // Previously Glean kept the KNOWN_CLIENT_ID stored.
552                        // Let's ensure we erase it now.
553                        if let Some(data) = glean.data_store.as_ref() {
554                            _ = data.remove_single_metric(
555                                Lifetime::User,
556                                "glean_client_info",
557                                "client_id",
558                            );
559                        }
560                    } else {
561                        // Temporarily enable uploading so we can submit a
562                        // deletion request ping.
563                        glean.upload_enabled = true;
564                        glean.on_upload_disabled(true);
565                    }
566                }
567            }
568        }
569
570        // We set this only for non-subprocess situations.
571        // If internal pings are disabled, we don't set up the MPS either,
572        // it wouldn't send any data anyway.
573        glean.schedule_metrics_pings = cfg.enable_internal_pings && cfg.use_core_mps;
574
575        // We only scan the pendings pings directories **after** dealing with the upload state.
576        // If upload is disabled, we delete all pending pings files
577        // and we need to do that **before** scanning the pending pings folder
578        // to ensure we don't enqueue pings before their files are deleted.
579        let _scanning_thread = glean.upload_manager.scan_pending_pings_directories(true);
580
581        Ok(glean)
582    }
583
584    /// For tests make it easy to create a Glean object using only the required configuration.
585    #[cfg(test)]
586    pub(crate) fn with_options(
587        data_path: &str,
588        application_id: &str,
589        upload_enabled: bool,
590        enable_internal_pings: bool,
591    ) -> Self {
592        let cfg = InternalConfiguration {
593            data_path: data_path.into(),
594            application_id: application_id.into(),
595            language_binding_name: "Rust".into(),
596            upload_enabled,
597            max_events: None,
598            delay_ping_lifetime_io: false,
599            app_build: "Unknown".into(),
600            use_core_mps: false,
601            trim_data_to_registered_pings: false,
602            log_level: None,
603            rate_limit: None,
604            enable_event_timestamps: true,
605            experimentation_id: None,
606            enable_internal_pings,
607            ping_schedule: Default::default(),
608            ping_lifetime_threshold: 0,
609            ping_lifetime_max_time: 0,
610            max_pending_pings_count: None,
611            max_pending_pings_directory_size: None,
612            session_mode: SessionMode::Auto,
613            session_sample_rate: 1.0,
614            session_inactivity_timeout_ms: 1_800_000,
615            events_ping_acceleration_factor: None,
616            enable_store_submitted_pings: false,
617        };
618
619        let mut glean = Self::new(cfg).unwrap();
620
621        // Disable all upload manager policies for testing
622        glean.upload_manager = PingUploadManager::no_policy(data_path);
623
624        glean
625    }
626
627    /// Close the database connection.
628    ///
629    /// After this Glean needs to be reinitialized.
630    pub fn close_db(&mut self) {
631        self.data_store = None;
632    }
633
634    fn client_id_file_path(&self) -> PathBuf {
635        self.data_path.join(CLIENT_ID_PLAIN_FILENAME)
636    }
637
638    /// Write the client ID to a separate plain file on disk
639    ///
640    /// Use `store_client_id_with_reporting` to handle the error cases.
641    fn store_client_id(&self, client_id: Uuid) -> Result<(), ClientIdFileError> {
642        let mut fp = File::create(self.client_id_file_path())?;
643
644        let mut buffer = Uuid::encode_buffer();
645        let uuid_str = client_id.hyphenated().encode_lower(&mut buffer);
646        fp.write_all(uuid_str.as_bytes())?;
647        fp.sync_all()?;
648
649        Ok(())
650    }
651
652    /// Write the client ID to a separate plain file on disk
653    ///
654    /// When an error occurs an error message is logged and the error is counted in a metric.
655    fn store_client_id_with_reporting(&self, client_id: Uuid, msg: &str) {
656        if let Err(err) = self.store_client_id(client_id) {
657            log::error!(
658                "Could not write {client_id} to state file. {} Error: {err}",
659                msg
660            );
661            match err {
662                ClientIdFileError::NotFound => {
663                    self.health_metrics
664                        .file_write_error
665                        .get("not-found")
666                        .add_sync(self, 1);
667                }
668                ClientIdFileError::PermissionDenied => {
669                    self.health_metrics
670                        .file_write_error
671                        .get("permission-denied")
672                        .add_sync(self, 1);
673                }
674                ClientIdFileError::IoError(..) => {
675                    self.health_metrics
676                        .file_write_error
677                        .get("io")
678                        .add_sync(self, 1);
679                }
680                ClientIdFileError::ParseError(..) => {
681                    log::error!("Parse error encountered on file write. This is impossible.");
682                }
683            }
684        }
685    }
686
687    /// Try to load a client ID from the plain file on disk.
688    fn client_id_from_file(&self) -> Result<Uuid, ClientIdFileError> {
689        let uuid_str = fs::read_to_string(self.client_id_file_path())?;
690        // We don't write a newline, but we still trim it. Who knows who else touches that file by accident.
691        // We're also a bit more lenient in what we accept here:
692        // uppercase, lowercase, with or without dashes, urn, braced (and whatever else `Uuid`
693        // parses by default).
694        let uuid = Uuid::try_parse(uuid_str.trim_end())?;
695        Ok(uuid)
696    }
697
698    /// Remove the stored client ID from disk.
699    /// Should only be called when the client ID is also removed from the database.
700    fn remove_stored_client_id(&self) -> Result<(), ClientIdFileError> {
701        match fs::remove_file(self.client_id_file_path()) {
702            Ok(()) => Ok(()),
703            Err(e) if e.kind() == io::ErrorKind::NotFound => {
704                // File was already missing. No need to report that.
705                Ok(())
706            }
707            Err(e) => Err(e.into()),
708        }
709    }
710
711    /// Initializes the core metrics managed by Glean's Rust core.
712    fn initialize_core_metrics(&mut self) {
713        let need_new_client_id = match self
714            .core_metrics
715            .client_id
716            .get_value(self, Some("glean_client_info"))
717        {
718            None => true,
719            Some(uuid) => uuid == *KNOWN_CLIENT_ID,
720        };
721        if need_new_client_id {
722            let new_clientid = self.core_metrics.client_id.generate_and_set_sync(self);
723            self.store_client_id_with_reporting(new_clientid, "New client in database only.");
724        }
725
726        if self
727            .core_metrics
728            .first_run_date
729            .get_value(self, "glean_client_info")
730            .is_none()
731        {
732            self.core_metrics.first_run_date.set_sync(self, None);
733            // The `first_run_date` field is generated on the very first run
734            // and persisted across upload toggling. We can assume that, the only
735            // time it is set, that's indeed our "first run".
736            self.is_first_run = true;
737        }
738
739        self.set_application_lifetime_core_metrics();
740    }
741
742    /// Initializes the database metrics managed by Glean's Rust core.
743    fn initialize_database_metrics(&mut self) {
744        log::trace!("Initializing database metrics");
745
746        if let Some(size) = self
747            .data_store
748            .as_ref()
749            .and_then(|database| database.file_size())
750        {
751            log::trace!("Database file size: {}", size.get());
752            self.database_metrics
753                .size
754                .accumulate_sync(self, size.get() as i64)
755        }
756
757        #[cfg(feature = "sqlite")]
758        if let Some(load_state) = self
759            .data_store
760            .as_ref()
761            .and_then(|database| database.load_state())
762        {
763            use crate::metrics::string::MAX_LENGTH_VALUE;
764            let load_state = truncate_string_at_boundary(load_state, MAX_LENGTH_VALUE);
765            self.database_metrics.load_error.set_sync(self, load_state)
766        }
767
768        #[cfg(not(feature = "sqlite"))]
769        if let Some(rkv_load_state) = self
770            .data_store
771            .as_ref()
772            .and_then(|database| database.rkv_load_state())
773        {
774            self.database_metrics
775                .rkv_load_error
776                .set_sync(self, rkv_load_state);
777        }
778    }
779
780    /// Signals that the environment is ready to submit pings.
781    ///
782    /// Should be called when Glean is initialized to the point where it can correctly assemble pings.
783    /// Usually called from the language binding after all of the core metrics have been set
784    /// and the ping types have been registered.
785    ///
786    /// # Arguments
787    ///
788    /// * `trim_data_to_registered_pings` - Whether we should limit to storing data only for
789    ///   data belonging to pings previously registered via `register_ping_type`.
790    ///
791    /// # Returns
792    ///
793    /// Whether the "events" ping was submitted.
794    pub fn on_ready_to_submit_pings(&mut self, trim_data_to_registered_pings: bool) -> bool {
795        // When upload is disabled on init we already clear out metrics.
796        // However at that point not all pings are registered and so we keep that data around.
797        // By the time we would be ready to submit we try again cleaning out metrics from
798        // now-known pings.
799        if !self.upload_enabled {
800            log::debug!("on_ready_to_submit_pings. let's clear pings once again.");
801            self.clear_metrics();
802        }
803
804        self.event_data_store
805            .flush_pending_events_on_startup(self, trim_data_to_registered_pings)
806    }
807
808    /// Sets whether upload is enabled or not.
809    ///
810    /// When uploading is disabled, metrics aren't recorded at all and no
811    /// data is uploaded.
812    ///
813    /// When disabling, all pending metrics, events and queued pings are cleared.
814    ///
815    /// When enabling, the core Glean metrics are recreated.
816    ///
817    /// If the value of this flag is not actually changed, this is a no-op.
818    ///
819    /// # Arguments
820    ///
821    /// * `flag` - When true, enable metric collection.
822    ///
823    /// # Returns
824    ///
825    /// Whether the flag was different from the current value,
826    /// and actual work was done to clear or reinstate metrics.
827    pub fn set_upload_enabled(&mut self, flag: bool) -> bool {
828        log::info!("Upload enabled: {:?}", flag);
829
830        if self.upload_enabled != flag {
831            if flag {
832                self.on_upload_enabled();
833            } else {
834                self.on_upload_disabled(false);
835            }
836            true
837        } else {
838            false
839        }
840    }
841
842    /// Sets whether storing submitted pings is enabled or not.
843    ///
844    /// # Arguments
845    ///
846    /// * `enabled` - When true, enables storing submitted pings.
847    ///
848    pub fn set_store_submitted_pings_enabled(&mut self, enabled: bool) {
849        self.store_submitted_pings_enabled = enabled;
850    }
851
852    /// Enable or disable a ping.
853    ///
854    /// Disabling a ping causes all data for that ping to be removed from storage
855    /// and all pending pings of that type to be deleted.
856    ///
857    /// **Note**: Do not use directly. Call `PingType::set_enabled` instead.
858    #[doc(hidden)]
859    pub fn set_ping_enabled(&mut self, ping: &PingType, enabled: bool) {
860        ping.store_enabled(enabled);
861        if !enabled {
862            if let Some(data) = self.data_store.as_ref() {
863                _ = data.clear_ping_lifetime_storage(ping.name());
864                _ = data.clear_lifetime_storage(Lifetime::User, ping.name());
865                _ = data.clear_lifetime_storage(Lifetime::Application, ping.name());
866            }
867            if let Err(err) = self.event_storage().clear_for_ping(ping.name()) {
868                log::warn!("Error clearing event storage for ping: {}", err);
869            }
870
871            let ping_maker = PingMaker::new();
872            let disabled_pings = &[ping.name()][..];
873            if let Err(err) = ping_maker.clear_pending_pings(self.get_data_path(), disabled_pings) {
874                log::warn!("Error clearing pending pings: {}", err);
875            }
876        }
877    }
878
879    /// Determines whether upload is enabled.
880    ///
881    /// When upload is disabled, no data will be recorded.
882    pub fn is_upload_enabled(&self) -> bool {
883        self.upload_enabled
884    }
885
886    /// Check if a ping is enabled.
887    ///
888    /// Note that some internal "ping" names are considered to be always enabled.
889    ///
890    /// If a ping is not known to Glean ("unregistered") it is always considered disabled.
891    /// If a ping is known, it can be enabled/disabled at any point.
892    /// Only data for enabled pings is recorded.
893    /// Disabled pings are never submitted.
894    pub fn is_ping_enabled(&self, ping: &str) -> bool {
895        // We "abuse" pings/storage names for internal data.
896        const DEFAULT_ENABLED: &[&str] = &[
897            "glean_client_info",
898            "glean_internal_info",
899            // for `experimentation_id`.
900            // That should probably have gone into `glean_internal_info` instead.
901            "all-pings",
902        ];
903
904        // `client_info`-like stuff is always enabled.
905        if DEFAULT_ENABLED.contains(&ping) {
906            return true;
907        }
908
909        let Some(ping) = self.ping_registry.get(ping) else {
910            log::trace!("Unknown ping {ping}. Assuming disabled.");
911            return false;
912        };
913
914        ping.enabled(self)
915    }
916
917    /// Handles the changing of state from upload disabled to enabled.
918    ///
919    /// Should only be called when the state actually changes.
920    ///
921    /// The `upload_enabled` flag is set to true and the core Glean metrics are
922    /// recreated.
923    fn on_upload_enabled(&mut self) {
924        self.upload_enabled = true;
925        self.initialize_core_metrics();
926        self.initialize_database_metrics();
927    }
928
929    /// Handles the changing of state from upload enabled to disabled.
930    ///
931    /// Should only be called when the state actually changes.
932    ///
933    /// A deletion_request ping is sent, all pending metrics, events and queued
934    /// pings are cleared, and the client_id is set to KNOWN_CLIENT_ID.
935    /// Afterward, the upload_enabled flag is set to false.
936    fn on_upload_disabled(&mut self, during_init: bool) {
937        // The upload_enabled flag should be true here, or the deletion ping
938        // won't be submitted.
939        let reason = if during_init {
940            Some("at_init")
941        } else {
942            Some("set_upload_enabled")
943        };
944        if !self
945            .internal_pings
946            .deletion_request
947            .submit_sync(self, reason)
948        {
949            log::error!("Failed to submit deletion-request ping on optout.");
950        }
951        self.clear_metrics();
952        self.upload_enabled = false;
953    }
954
955    /// Clear any pending metrics when telemetry is disabled.
956    fn clear_metrics(&mut self) {
957        // Clear the pending pings queue and acquire the lock
958        // so that it can't be accessed until this function is done.
959        let _lock = self.upload_manager.clear_ping_queue();
960
961        // Clear any pending pings that follow `collection_enabled`.
962        let ping_maker = PingMaker::new();
963        let disabled_pings = self
964            .ping_registry
965            .iter()
966            .filter(|&(_ping_name, ping)| ping.follows_collection_enabled())
967            .map(|(ping_name, _ping)| &ping_name[..])
968            .collect::<Vec<_>>();
969        if let Err(err) = ping_maker.clear_pending_pings(self.get_data_path(), &disabled_pings) {
970            log::warn!("Error clearing pending pings: {}", err);
971        }
972
973        if let Err(e) = self.remove_stored_client_id() {
974            log::error!("Couldn't remove client ID on disk. This might lead to a resurrection of this client ID later. Error: {e}");
975        }
976
977        // Delete all stored metrics.
978        // Note that this also includes the ping sequence numbers, so it has
979        // the effect of resetting those to their initial values.
980        if let Some(data) = self.data_store.as_ref() {
981            let warn_on_error = |result, msg| {
982                if let Err(e) = result {
983                    log::warn!("{msg}: {e}");
984                }
985            };
986
987            warn_on_error(
988                data.clear_lifetime_storage(Lifetime::User, INTERNAL_STORAGE),
989                "failed to clear internal storage",
990            );
991            warn_on_error(
992                data.remove_single_metric(Lifetime::User, "glean_client_info", "client_id"),
993                "failed to clear internal client info storage",
994            );
995            for (ping_name, ping) in &self.ping_registry {
996                if ping.follows_collection_enabled() {
997                    warn_on_error(
998                        data.clear_ping_lifetime_storage(ping_name),
999                        "failed to clear ping lifetime storage",
1000                    );
1001                    warn_on_error(
1002                        data.clear_lifetime_storage(Lifetime::User, ping_name),
1003                        "failed to clear user lifetime storage",
1004                    );
1005                    warn_on_error(
1006                        data.clear_lifetime_storage(Lifetime::Application, ping_name),
1007                        "failed to clear application lifetime storage",
1008                    );
1009                }
1010            }
1011        }
1012        if let Err(err) = self.event_data_store.clear_all() {
1013            log::warn!("Error clearing pending events: {}", err);
1014        }
1015
1016        // The persisted `session#*` keys were cleared with internal storage above.
1017        // Reset the in-memory session state to match, so a later opt-in can't
1018        // continue the pre-opt-out session. No `session_end` is recorded, since
1019        // upload is being disabled.
1020        self.session_manager = SessionManager::new(
1021            self.session_manager.mode,
1022            self.session_manager.configured_sample_rate,
1023            self.session_manager.inactivity_timeout,
1024        );
1025
1026        // This does not clear the experiments store (which isn't managed by the
1027        // StorageEngineManager), since doing so would mean we would have to have the
1028        // application tell us again which experiments are active if telemetry is
1029        // re-enabled.
1030    }
1031
1032    /// Gets the application ID as specified on instantiation.
1033    pub fn get_application_id(&self) -> &str {
1034        &self.application_id
1035    }
1036
1037    /// Gets the data path of this instance.
1038    pub fn get_data_path(&self) -> &Path {
1039        &self.data_path
1040    }
1041
1042    /// Gets a handle to the database.
1043    #[track_caller] // If this fails we're interested in the caller.
1044    pub fn storage(&self) -> &Database {
1045        self.data_store.as_ref().expect("No database found")
1046    }
1047
1048    /// Gets an optional handle to the database.
1049    pub fn storage_opt(&self) -> Option<&Database> {
1050        self.data_store.as_ref()
1051    }
1052
1053    /// Gets a handle to the event database.
1054    pub fn event_storage(&self) -> &EventDatabase {
1055        &self.event_data_store
1056    }
1057
1058    /// Gets a reference to the session manager.
1059    pub fn session_manager(&self) -> &SessionManager {
1060        &self.session_manager
1061    }
1062
1063    pub(crate) fn with_timestamps(&self) -> bool {
1064        self.with_timestamps
1065    }
1066
1067    /// Gets the maximum number of events to store before sending a ping.
1068    pub fn get_max_events(&self) -> usize {
1069        let remote_settings_config = self.remote_settings_config.lock().unwrap();
1070
1071        if let Some(max_events) = remote_settings_config.event_threshold {
1072            max_events as usize
1073        } else {
1074            self.max_events as usize
1075        }
1076    }
1077
1078    /// Gets the number of "events" pings to accelerate each session, plus one.
1079    pub fn get_events_ping_acceleration_factor(&self) -> usize {
1080        let remote_settings_config = self.remote_settings_config.lock().unwrap();
1081
1082        if let Some(factor) = remote_settings_config.events_ping_acceleration_factor {
1083            factor
1084        } else {
1085            self.events_ping_acceleration_factor.unwrap_or(1)
1086        }
1087    }
1088
1089    /// Gets the next task for an uploader.
1090    ///
1091    /// This can be one of:
1092    ///
1093    /// * [`Wait`](PingUploadTask::Wait) - which means the requester should ask
1094    ///   again later;
1095    /// * [`Upload(PingRequest)`](PingUploadTask::Upload) - which means there is
1096    ///   a ping to upload. This wraps the actual request object;
1097    /// * [`Done`](PingUploadTask::Done) - which means requester should stop
1098    ///   asking for now.
1099    ///
1100    /// # Returns
1101    ///
1102    /// A [`PingUploadTask`] representing the next task.
1103    pub fn get_upload_task(&self) -> PingUploadTask {
1104        self.upload_manager.get_upload_task(self, self.log_pings())
1105    }
1106
1107    /// Processes the response from an attempt to upload a ping.
1108    ///
1109    /// # Arguments
1110    ///
1111    /// * `uuid` - The UUID of the ping in question.
1112    /// * `status` - The upload result.
1113    pub fn process_ping_upload_response(
1114        &self,
1115        uuid: &str,
1116        status: UploadResult,
1117    ) -> UploadTaskAction {
1118        self.upload_manager
1119            .process_ping_upload_response(self, uuid, status)
1120    }
1121
1122    /// Takes a snapshot for the given store and optionally clear it.
1123    ///
1124    /// # Arguments
1125    ///
1126    /// * `store_name` - The store to snapshot.
1127    /// * `clear_store` - Whether to clear the store after snapshotting.
1128    ///
1129    /// # Returns
1130    ///
1131    /// The snapshot in a string encoded as JSON. If the snapshot is empty, returns an empty string.
1132    pub fn snapshot(&mut self, store_name: &str, clear_store: bool) -> String {
1133        StorageManager
1134            .snapshot(self.storage(), store_name, clear_store)
1135            .unwrap_or_else(|| String::from(""))
1136    }
1137
1138    pub(crate) fn make_path(&self, ping_name: &str, doc_id: &str) -> String {
1139        format!(
1140            "/submit/{}/{}/{}/{}",
1141            self.get_application_id(),
1142            ping_name,
1143            GLEAN_SCHEMA_VERSION,
1144            doc_id
1145        )
1146    }
1147
1148    /// Collects and submits a ping by name for eventual uploading.
1149    ///
1150    /// The ping content is assembled as soon as possible, but upload is not
1151    /// guaranteed to happen immediately, as that depends on the upload policies.
1152    ///
1153    /// If the ping currently contains no content, it will not be sent,
1154    /// unless it is configured to be sent if empty.
1155    ///
1156    /// # Arguments
1157    ///
1158    /// * `ping_name` - The name of the ping to submit
1159    /// * `reason` - A reason code to include in the ping
1160    ///
1161    /// # Returns
1162    ///
1163    /// Whether the ping was succesfully assembled and queued.
1164    ///
1165    /// # Errors
1166    ///
1167    /// If collecting or writing the ping to disk failed.
1168    pub fn submit_ping_by_name(&self, ping_name: &str, reason: Option<&str>) -> bool {
1169        match self.get_ping_by_name(ping_name) {
1170            None => {
1171                log::error!("Attempted to submit unknown ping '{}'", ping_name);
1172                false
1173            }
1174            Some(ping) => ping.submit_sync(self, reason),
1175        }
1176    }
1177
1178    /// Gets a [`PingType`] by name.
1179    ///
1180    /// # Returns
1181    ///
1182    /// The [`PingType`] of a ping if the given name was registered before, [`None`]
1183    /// otherwise.
1184    pub fn get_ping_by_name(&self, ping_name: &str) -> Option<&PingType> {
1185        self.ping_registry.get(ping_name)
1186    }
1187
1188    /// Register a new [`PingType`](metrics/struct.PingType.html).
1189    pub fn register_ping_type(&mut self, ping: &PingType) {
1190        if self.ping_registry.contains_key(ping.name()) {
1191            log::debug!("Duplicate ping named '{}'", ping.name())
1192        }
1193
1194        self.ping_registry
1195            .insert(ping.name().to_string(), ping.clone());
1196    }
1197
1198    /// Gets a list of currently registered ping names.
1199    ///
1200    /// # Returns
1201    ///
1202    /// The list of ping names that are currently registered.
1203    pub fn get_registered_ping_names(&self) -> Vec<&str> {
1204        self.ping_registry.keys().map(String::as_str).collect()
1205    }
1206
1207    /// Get create time of the Glean object.
1208    pub(crate) fn start_time(&self) -> DateTime<FixedOffset> {
1209        self.start_time
1210    }
1211
1212    /// Indicates that an experiment is running.
1213    ///
1214    /// Glean will then add an experiment annotation to the environment
1215    /// which is sent with pings. This information is not persisted between runs.
1216    ///
1217    /// # Arguments
1218    ///
1219    /// * `experiment_id` - The id of the active experiment (maximum 30 bytes).
1220    /// * `branch` - The experiment branch (maximum 30 bytes).
1221    /// * `extra` - Optional metadata to output with the ping.
1222    pub fn set_experiment_active(
1223        &self,
1224        experiment_id: String,
1225        branch: String,
1226        extra: HashMap<String, String>,
1227    ) {
1228        let metric = ExperimentMetric::new(self, experiment_id);
1229        metric.set_active_sync(self, branch, extra);
1230    }
1231
1232    /// Indicates that an experiment is no longer running.
1233    ///
1234    /// # Arguments
1235    ///
1236    /// * `experiment_id` - The id of the active experiment to deactivate (maximum 30 bytes).
1237    pub fn set_experiment_inactive(&self, experiment_id: String) {
1238        let metric = ExperimentMetric::new(self, experiment_id);
1239        metric.set_inactive_sync(self);
1240    }
1241
1242    /// **Test-only API (exported for FFI purposes).**
1243    ///
1244    /// Gets stored data for the requested experiment.
1245    ///
1246    /// # Arguments
1247    ///
1248    /// * `experiment_id` - The id of the active experiment (maximum 30 bytes).
1249    pub fn test_get_experiment_data(&self, experiment_id: String) -> Option<RecordedExperiment> {
1250        let metric = ExperimentMetric::new(self, experiment_id);
1251        metric.test_get_value(self)
1252    }
1253
1254    /// **Test-only API (exported for FFI purposes).**
1255    ///
1256    /// Gets stored experimentation id annotation.
1257    pub fn test_get_experimentation_id(&self) -> Option<String> {
1258        self.additional_metrics
1259            .experimentation_id
1260            .get_value(self, None)
1261    }
1262
1263    /// Set configuration to override the default state, typically initiated from a
1264    /// remote_settings experiment or rollout
1265    ///
1266    /// # Arguments
1267    ///
1268    /// * `cfg` - The stringified JSON representation of a `RemoteSettingsConfig` object
1269    pub fn apply_server_knobs_config(&self, cfg: RemoteSettingsConfig) {
1270        let config_value = {
1271            // Hold the lock while merging config and serializing, then release
1272            // before performing IO in set_sync.
1273            let mut remote_settings_config = self.remote_settings_config.lock().unwrap();
1274
1275            // Merge the exising metrics configuration with the supplied one
1276            remote_settings_config
1277                .metrics_enabled
1278                .extend(cfg.metrics_enabled);
1279
1280            // Merge the exising ping configuration with the supplied one
1281            remote_settings_config
1282                .pings_enabled
1283                .extend(cfg.pings_enabled);
1284
1285            remote_settings_config.event_threshold = cfg.event_threshold;
1286
1287            // Clamp to [0.0, 1.0] so callers can't accidentally set an invalid rate.
1288            //
1289            // NOTE: `session_sample_rate` is intentionally NOT applied to any
1290            // currently-active session.  The override is picked up at the next
1291            // `session_start()` call.  This "sticky per session" design means:
1292            //   - A mid-session RS rollout does not change sampling mid-flight,
1293            //     which would otherwise cause partial session data.
1294            //   - To clear the override and revert to the configured rate, set
1295            //     `session_sample_rate` to `null` in the RS payload.  The next
1296            //     session will use `configured_sample_rate` as the fallback.
1297            //
1298            // This override is intentionally NOT persisted to storage.  Remote
1299            // Settings configuration is refreshed on every app startup, so the
1300            // override will be re-applied before the next session begins.
1301            // Persisting it would risk making a stale value sticky if the RS
1302            // payload changes or is removed between restarts.
1303            remote_settings_config.session_sample_rate = cfg.session_sample_rate.map(|r| {
1304                let clamped = r.clamp(0.0, 1.0);
1305                if clamped != r {
1306                    log::warn!(
1307                        "session_sample_rate {} out of range, clamped to {}",
1308                        r,
1309                        clamped
1310                    );
1311                }
1312                clamped
1313            });
1314
1315            remote_settings_config.events_ping_acceleration_factor =
1316                cfg.events_ping_acceleration_factor;
1317
1318            // Store the Server Knobs configuration as an ObjectMetric
1319            // Since RemoteSettingsConfig only contains maps with string keys and primitives,
1320            // serialization via the derived Serialize impl cannot fail so it is safe to unwrap.
1321            serde_json::to_value(&*remote_settings_config).unwrap()
1322        };
1323
1324        self.additional_metrics
1325            .server_knobs_config
1326            .set_sync(self, config_value);
1327
1328        // Update remote_settings epoch
1329        self.remote_settings_epoch.fetch_add(1, Ordering::SeqCst);
1330    }
1331
1332    /// Persists [`Lifetime::Ping`] data that might be in memory in case
1333    /// [`delay_ping_lifetime_io`](InternalConfiguration::delay_ping_lifetime_io) is set
1334    /// or was set at a previous time.
1335    ///
1336    /// If there is no data to persist, this function does nothing.
1337    pub fn persist_ping_lifetime_data(&self) -> Result<()> {
1338        if let Some(data) = self.data_store.as_ref() {
1339            return data.persist_ping_lifetime_data();
1340        }
1341
1342        Ok(())
1343    }
1344
1345    /// Sets internally-handled application lifetime metrics.
1346    fn set_application_lifetime_core_metrics(&self) {
1347        self.core_metrics.os.set_sync(self, system::OS);
1348    }
1349
1350    /// **This is not meant to be used directly.**
1351    ///
1352    /// Clears all the metrics that have [`Lifetime::Application`].
1353    pub fn clear_application_lifetime_metrics(&self) {
1354        log::trace!("Clearing Lifetime::Application metrics");
1355        if let Some(data) = self.data_store.as_ref() {
1356            data.clear_lifetime(Lifetime::Application);
1357        }
1358
1359        // Set internally handled app lifetime metrics again.
1360        self.set_application_lifetime_core_metrics();
1361    }
1362
1363    /// Whether or not this is the first run on this profile.
1364    pub fn is_first_run(&self) -> bool {
1365        self.is_first_run
1366    }
1367
1368    /// Sets a debug view tag.
1369    ///
1370    /// This will return `false` in case `value` is not a valid tag.
1371    ///
1372    /// When the debug view tag is set, pings are sent with a `X-Debug-ID` header with the value of the tag
1373    /// and are sent to the ["Ping Debug Viewer"](https://mozilla.github.io/glean/book/dev/core/internal/debug-pings.html).
1374    ///
1375    /// # Arguments
1376    ///
1377    /// * `value` - A valid HTTP header value. Must match the regex: "[a-zA-Z0-9-]{1,20}".
1378    pub fn set_debug_view_tag(&mut self, value: &str) -> bool {
1379        self.debug.debug_view_tag.set(value.into())
1380    }
1381
1382    /// Return the value for the debug view tag or [`None`] if it hasn't been set.
1383    ///
1384    /// The `debug_view_tag` may be set from an environment variable
1385    /// (`GLEAN_DEBUG_VIEW_TAG`) or through the [`set_debug_view_tag`](Glean::set_debug_view_tag) function.
1386    pub fn debug_view_tag(&self) -> Option<&String> {
1387        self.debug.debug_view_tag.get()
1388    }
1389
1390    /// Sets source tags.
1391    ///
1392    /// This will return `false` in case `value` contains invalid tags.
1393    ///
1394    /// Ping tags will show in the destination datasets, after ingestion.
1395    ///
1396    /// **Note** If one or more tags are invalid, all tags are ignored.
1397    ///
1398    /// # Arguments
1399    ///
1400    /// * `value` - A vector of at most 5 valid HTTP header values. Individual tags must match the regex: "[a-zA-Z0-9-]{1,20}".
1401    pub fn set_source_tags(&mut self, value: Vec<String>) -> bool {
1402        self.debug.source_tags.set(value)
1403    }
1404
1405    /// Return the value for the source tags or [`None`] if it hasn't been set.
1406    ///
1407    /// The `source_tags` may be set from an environment variable (`GLEAN_SOURCE_TAGS`)
1408    /// or through the [`set_source_tags`](Glean::set_source_tags) function.
1409    pub(crate) fn source_tags(&self) -> Option<&Vec<String>> {
1410        self.debug.source_tags.get()
1411    }
1412
1413    /// Sets the log pings debug option.
1414    ///
1415    /// This will return `false` in case we are unable to set the option.
1416    ///
1417    /// When the log pings debug option is `true`,
1418    /// we log the payload of all succesfully assembled pings.
1419    ///
1420    /// # Arguments
1421    ///
1422    /// * `value` - The value of the log pings option
1423    pub fn set_log_pings(&mut self, value: bool) -> bool {
1424        self.debug.log_pings.set(value)
1425    }
1426
1427    /// Return the value for the log pings debug option or `false` if it hasn't been set.
1428    ///
1429    /// The `log_pings` option may be set from an environment variable (`GLEAN_LOG_PINGS`)
1430    /// or through the `set_log_pings` function.
1431    pub fn log_pings(&self) -> bool {
1432        self.debug.log_pings.get().copied().unwrap_or(false)
1433    }
1434
1435    fn get_dirty_bit_metric(&self) -> metrics::BooleanMetric {
1436        metrics::BooleanMetric::new(CommonMetricData {
1437            name: "dirtybit".into(),
1438            // We don't need a category, the name is already unique
1439            category: "".into(),
1440            send_in_pings: vec![INTERNAL_STORAGE.into()],
1441            lifetime: Lifetime::User,
1442            ..Default::default()
1443        })
1444    }
1445
1446    /// **This is not meant to be used directly.**
1447    ///
1448    /// Sets the value of a "dirty flag" in the permanent storage.
1449    ///
1450    /// The "dirty flag" is meant to have the following behaviour, implemented
1451    /// by the consumers of the FFI layer:
1452    ///
1453    /// - on mobile: set to `false` when going to background or shutting down,
1454    ///   set to `true` at startup and when going to foreground.
1455    /// - on non-mobile platforms: set to `true` at startup and `false` at
1456    ///   shutdown.
1457    ///
1458    /// At startup, before setting its new value, if the "dirty flag" value is
1459    /// `true`, then Glean knows it did not exit cleanly and can implement
1460    /// coping mechanisms (e.g. sending a `baseline` ping).
1461    pub fn set_dirty_flag(&self, new_value: bool) {
1462        self.get_dirty_bit_metric().set_sync(self, new_value);
1463    }
1464
1465    /// **This is not meant to be used directly.**
1466    ///
1467    /// Checks the stored value of the "dirty flag".
1468    pub fn is_dirty_flag_set(&self) -> bool {
1469        let dirty_bit_metric = self.get_dirty_bit_metric();
1470        match self.storage().get_metric(
1471            #[cfg(not(feature = "sqlite"))]
1472            self,
1473            dirty_bit_metric.meta(),
1474            INTERNAL_STORAGE,
1475        ) {
1476            Some(Metric::Boolean(b)) => b,
1477            _ => false,
1478        }
1479    }
1480
1481    // -----------------------------------------------------------------------
1482    // Session lifecycle methods
1483    // -----------------------------------------------------------------------
1484
1485    /// Restores session state from persistent storage at startup.
1486    ///
1487    /// Must be called after `data_store` is initialized (i.e. after
1488    /// `Database::new` succeeds) so that the storage reads are valid.
1489    ///
1490    /// **Sequence counter**: `session_seq` is always restored so it is
1491    /// monotonically increasing across restarts.  Note that if a crash occurs
1492    /// between `store_session_seq` and `persist_session_id` inside
1493    /// `session_start`, the sequence number will have been incremented but no
1494    /// session ID will be persisted.  On the next restart this method will
1495    /// restore the incremented seq and the next session will be assigned
1496    /// seq+1, leaving a one-element gap.  This is acceptable — downstream
1497    /// analysts should treat sequence numbers as monotonically non-decreasing,
1498    /// not strictly contiguous.
1499    ///
1500    /// **AUTO mode resumption**: requires both a persisted `session_id` **and**
1501    /// an `inactive_since` timestamp.  If either is absent the previous session
1502    /// is considered abandoned and the next `handle_client_active` call will
1503    /// start a fresh session via `session_start()`.  On a crash restart,
1504    /// `recover_session_on_dirty_flag()` overwrites whatever this method
1505    /// restores, so the dirty-flag path is always authoritative.
1506    fn restore_session_state_from_storage(&mut self) {
1507        // Always restore seq so new sessions increment from the last known value.
1508        self.session_manager.session_seq = session::read_session_seq(self);
1509
1510        // Check for an orphaned session from a previous build that used a
1511        // different SessionMode.  If the current mode would not restore the
1512        // persisted session, emit a synthetic session_end("abandoned") and
1513        // clear all persisted session state so it doesn't leak across builds.
1514        if self.session_manager.mode != SessionMode::Auto {
1515            if let Some(id_str) = session::read_session_id(self) {
1516                log::info!(
1517                    "Orphaned session {} found from a previous Auto-mode build; \
1518                     emitting session_end(\"abandoned\") and clearing storage",
1519                    id_str
1520                );
1521                let seq = self.session_manager.session_seq;
1522                self.record_session_end_event(&id_str, seq, Some("abandoned"));
1523                session::clear(self);
1524            }
1525            return;
1526        }
1527
1528        // AUTO mode: restore inactive session state so inactivity timeout
1529        // evaluation can happen lazily on the next handle_client_active call.
1530        if let Some(inactive_since) = session::read_inactive_since(self) {
1531            if let Some(id_str) = session::read_session_id(self) {
1532                if let Ok(id) = Uuid::parse_str(&id_str) {
1533                    // Recompute sampled_in deterministically from the UUID so
1534                    // the sampling decision is consistent across the resumed session.
1535                    let sampled_in = session::uuid_to_sample_value(&id)
1536                        < self.session_manager.configured_sample_rate;
1537                    self.session_manager.session_id = Some(id);
1538                    self.session_manager.inactive_since = Some(inactive_since);
1539                    self.session_manager.sampled_in = sampled_in;
1540                    self.session_manager.session_start_time =
1541                        session::read_session_start_time(self);
1542                    if self.session_manager.session_start_time.is_none() {
1543                        log::warn!(
1544                            "Resumed session {} has no persisted session_start_time; \
1545                             events in this session will carry session_start_time: null",
1546                            id
1547                        );
1548                    }
1549                    // Restore event_seq so the resumed session issues
1550                    // monotonically increasing sequence numbers even across
1551                    // a clean restart.
1552                    self.session_manager
1553                        .event_seq
1554                        .store(session::read_session_event_seq(self), Ordering::Relaxed);
1555                    self.session_manager.state = SessionState::Inactive;
1556                }
1557            }
1558        }
1559    }
1560
1561    /// Injects a `glean_timestamp` key into `extra` when event timestamps are enabled.
1562    ///
1563    /// Uses the wall clock, matching `EventMetric::record`. The event's main
1564    /// `timestamp` stays on the monotonic clock.
1565    fn maybe_inject_glean_timestamp(&self, extra: &mut std::collections::HashMap<String, String>) {
1566        if self.with_timestamps {
1567            let precise_timestamp = Utc::now().timestamp_millis() as u64;
1568            extra.insert("glean_timestamp".to_string(), precise_timestamp.to_string());
1569        }
1570    }
1571
1572    /// Records a `glean.session_start` boundary event (always, regardless of sampling).
1573    fn record_session_start_event(
1574        &self,
1575        session_id: &str,
1576        seq: u64,
1577        start_time: DateTime<FixedOffset>,
1578        sampled_in: bool,
1579    ) {
1580        let meta = CommonMetricData {
1581            name: "session_start".into(),
1582            category: "glean".into(),
1583            send_in_pings: vec!["events".into()],
1584            lifetime: Lifetime::Ping,
1585            ..Default::default()
1586        };
1587        let timestamp = crate::get_timestamp_ms();
1588        let mut extra = std::collections::HashMap::new();
1589        extra.insert("session_id".to_string(), session_id.to_string());
1590        extra.insert("session_seq".to_string(), seq.to_string());
1591        extra.insert(
1592            "session_start_time".to_string(),
1593            start_time.to_rfc3339_opts(SecondsFormat::Millis, true),
1594        );
1595        extra.insert("sampled_in".to_string(), sampled_in.to_string());
1596        self.maybe_inject_glean_timestamp(&mut extra);
1597        self.event_data_store.record(
1598            self,
1599            &meta.into(),
1600            timestamp,
1601            Some(extra),
1602            EventSessionContext::OutOfSession,
1603        );
1604    }
1605
1606    /// Records a `glean.session_end` boundary event (always, regardless of sampling).
1607    fn record_session_end_event(&self, session_id: &str, seq: u64, reason: Option<&str>) {
1608        let meta = CommonMetricData {
1609            name: "session_end".into(),
1610            category: "glean".into(),
1611            send_in_pings: vec!["events".into()],
1612            lifetime: Lifetime::Ping,
1613            ..Default::default()
1614        };
1615        let timestamp = crate::get_timestamp_ms();
1616        let mut extra = std::collections::HashMap::new();
1617        extra.insert("session_id".to_string(), session_id.to_string());
1618        extra.insert("session_seq".to_string(), seq.to_string());
1619        if let Some(r) = reason {
1620            extra.insert("reason".to_string(), r.to_string());
1621        }
1622        self.maybe_inject_glean_timestamp(&mut extra);
1623        self.event_data_store.record(
1624            self,
1625            &meta.into(),
1626            timestamp,
1627            Some(extra),
1628            EventSessionContext::OutOfSession,
1629        );
1630    }
1631
1632    /// Starts a new session, persists state, and records a boundary event.
1633    ///
1634    /// If a session is already active it is ended cleanly before the new one
1635    /// starts, preventing orphaned sessions with no corresponding `session_end`.
1636    pub fn session_start(&mut self) {
1637        // End any already-active session so we never orphan a session_end event.
1638        if self.session_manager.is_active() {
1639            self.session_end(Some("replaced"));
1640        }
1641
1642        // 1. Compute new seq from in-memory value (authoritative after init).
1643        let new_seq = self.session_manager.session_seq + 1;
1644
1645        // 2. Generate new session_id and compute sampling.
1646        //    Prefer a remote-settings override if one has been set, falling back
1647        //    to the immutable configured_sample_rate (never the last effective
1648        //    rate) so RS overrides can be fully cleared without residual effects.
1649        //    The rate is sampled once here and is sticky for the entire session;
1650        //    any RS update received mid-session takes effect at the next session_start.
1651        let session_id = uuid::Uuid::new_v4();
1652        let sample_rate = {
1653            let remote = self.remote_settings_config.lock().unwrap();
1654            remote
1655                .session_sample_rate
1656                .unwrap_or(self.session_manager.configured_sample_rate)
1657        };
1658        let sampled_in = session::uuid_to_sample_value(&session_id) < sample_rate;
1659
1660        // 3. Update in-memory state.
1661        self.session_manager.sample_rate = sample_rate;
1662        // Truncate to millisecond precision so that in-memory and persisted
1663        // (RFC 3339 millis) representations are identical after a round-trip.
1664        let start_time = {
1665            let now = local_now_with_offset();
1666            let millis = now.timestamp_millis();
1667            DateTime::from_timestamp_millis(millis)
1668                .expect("valid timestamp")
1669                .with_timezone(now.offset())
1670        };
1671        self.session_manager.session_start_time = Some(start_time);
1672        self.session_manager.session_id = Some(session_id);
1673        self.session_manager.session_seq = new_seq;
1674        self.session_manager.event_seq.store(0, Ordering::Relaxed);
1675        self.session_manager.sampled_in = sampled_in;
1676        self.session_manager.state = SessionState::Active;
1677        self.session_manager.inactive_since = None;
1678
1679        // 4. Persist to storage.
1680        session::store_session_seq(self, new_seq);
1681        session::persist_session_id(self, &session_id.to_string());
1682        session::persist_session_start_time(self, start_time);
1683        session::clear_inactive_since(self);
1684
1685        // 5. Increment diagnostic counter.
1686        self.additional_metrics.sessions_seen.add_sync(self, 1);
1687
1688        // 6. Record boundary event.
1689        self.record_session_start_event(&session_id.to_string(), new_seq, start_time, sampled_in);
1690    }
1691
1692    /// Ends the current session, persists state, and records a boundary event.
1693    ///
1694    /// Returns the ended session's metadata, or `None` if no session was active.
1695    pub fn session_end(&mut self, reason: Option<&str>) -> Option<crate::session::SessionMetadata> {
1696        if self.session_manager.state != SessionState::Active {
1697            return None;
1698        }
1699
1700        let session_id = self.session_manager.session_id?;
1701        let seq = self.session_manager.session_seq;
1702        let event_seq = self.session_manager.event_seq.load(Ordering::Relaxed);
1703        let sample_rate = self.session_manager.sample_rate;
1704        let start_time = self.session_manager.session_start_time;
1705
1706        // Clear persistence.
1707        session::clear(self);
1708
1709        // Reset in-memory state so the next session_start gets a clean slate.
1710        self.session_manager.reset_state();
1711
1712        // Record boundary event.
1713        self.record_session_end_event(&session_id.to_string(), seq, reason);
1714
1715        Some(crate::session::SessionMetadata {
1716            session_id: session_id.to_string(),
1717            session_seq: seq,
1718            event_seq,
1719            session_sample_rate: sample_rate,
1720            session_start_time: start_time.map(|t| t.to_rfc3339_opts(SecondsFormat::Millis, true)),
1721        })
1722    }
1723
1724    /// Transitions the current session to inactive (AUTO mode).
1725    ///
1726    /// Records the `inactive_since` timestamp for timeout evaluation on next activation.
1727    /// Does NOT end the session — that happens lazily on next `handle_client_active`.
1728    pub(crate) fn session_transition_to_inactive(&mut self) {
1729        if self.session_manager.state != SessionState::Active {
1730            return;
1731        }
1732
1733        let now = local_now_with_offset();
1734        // Snapshot event_seq before changing state so the value is stable.
1735        let event_seq = self.session_manager.event_seq.load(Ordering::Relaxed);
1736        self.session_manager.state = SessionState::Inactive;
1737        self.session_manager.inactive_since = Some(now);
1738
1739        // Persist for crash recovery and clean-restart resumption.
1740        // event_seq is persisted here (rather than on every increment) because
1741        // this is the only point where events stop being recorded mid-session;
1742        // if the app crashes before the next activation, the recovered session
1743        // will at least have the correct seq baseline from the last inactive
1744        // transition.
1745        session::persist_inactive_since(self, now);
1746        session::store_session_event_seq(self, event_seq);
1747    }
1748
1749    /// Handles transitioning from inactive to active (AUTO mode).
1750    ///
1751    /// Evaluates the inactivity timeout:
1752    /// - If the timeout has NOT expired: resume the existing session.
1753    /// - If the timeout HAS expired: end the old session and start a new one.
1754    ///
1755    /// Returns `true` if a new session was started.
1756    pub(crate) fn session_transition_to_active(&mut self) -> bool {
1757        match self.session_manager.inactive_since {
1758            None => {
1759                // No inactive_since recorded: treat as a cold activation and start
1760                // a fresh session.  The call site in handle_client_active guards
1761                // with `inactive_since.is_some()` so this is normally unreachable,
1762                // but we handle it safely rather than leaving state inconsistent.
1763                self.session_start();
1764                true
1765            }
1766            Some(inactive_since) => {
1767                let now = local_now_with_offset();
1768                let elapsed = (now - inactive_since).to_std().unwrap_or_default();
1769
1770                // A timeout of zero means "never time out" (session always resumes).
1771                if !self.session_manager.inactivity_timeout.is_zero()
1772                    && elapsed >= self.session_manager.inactivity_timeout
1773                {
1774                    // Timeout expired → end old session (emits boundary event), start new one.
1775                    // The session state was set to Inactive by session_transition_to_inactive(),
1776                    // but session_id is still set. Restore Active so session_end() can proceed.
1777                    self.session_manager.state = SessionState::Active;
1778                    self.session_end(Some("timeout"));
1779                    self.session_start();
1780                    true
1781                } else {
1782                    // Timeout has NOT expired → resume existing session.
1783                    self.session_manager.state = SessionState::Active;
1784                    self.session_manager.inactive_since = None;
1785                    session::clear_inactive_since(self);
1786                    false
1787                }
1788            }
1789        }
1790    }
1791
1792    /// Called during initialization to recover an abnormally terminated session.
1793    ///
1794    /// If the dirty flag was set and a session ID is persisted, emits a synthetic
1795    /// `session_end` event with reason "abnormal" and clears session state.
1796    pub(crate) fn recover_session_on_dirty_flag(&mut self) {
1797        let persisted_id = match session::read_session_id(self) {
1798            Some(id) => id,
1799            None => return, // No previous session to recover.
1800        };
1801
1802        let persisted_seq = self.session_manager.session_seq;
1803        let inactive_since = session::read_inactive_since(self);
1804
1805        // Determine if the session ended while inactive (timeout may have expired).
1806        let reason = if inactive_since.is_some() {
1807            "abnormal_inactive"
1808        } else {
1809            "abnormal"
1810        };
1811
1812        log::info!(
1813            "Recovering abnormally terminated session: {} (seq={})",
1814            persisted_id,
1815            persisted_seq
1816        );
1817
1818        // Emit synthetic session_end.
1819        self.record_session_end_event(&persisted_id, persisted_seq, Some(reason));
1820
1821        // Clear persisted session state so the recovered session won't be replayed.
1822        session::clear(self);
1823
1824        // Reset in-memory state so the next session_start gets a clean slate.
1825        self.session_manager.reset_state();
1826    }
1827
1828    // -----------------------------------------------------------------------
1829    // Client lifecycle methods
1830    // -----------------------------------------------------------------------
1831
1832    /// Performs the collection/cleanup operations required by becoming active.
1833    ///
1834    /// This functions generates a baseline ping with reason `active`
1835    /// and then sets the dirty bit.
1836    pub fn handle_client_active(&mut self) {
1837        match self.session_manager.mode {
1838            SessionMode::Auto => {
1839                if !self.session_manager.is_active() {
1840                    if self.session_manager.inactive_since.is_some() {
1841                        // Was inactive — evaluate timeout.
1842                        self.session_transition_to_active();
1843                    } else {
1844                        // First activation — start initial session.
1845                        self.session_start();
1846                    }
1847                }
1848            }
1849            SessionMode::Lifecycle => {
1850                // Only start a session on the first activation following an inactive
1851                // transition. Guard against duplicate handle_client_active calls which
1852                // are not a real lifecycle transition.
1853                if !self.session_manager.is_active() {
1854                    self.session_start();
1855                }
1856            }
1857            SessionMode::Manual => {
1858                // No automatic session management.
1859            }
1860        }
1861
1862        if !self
1863            .internal_pings
1864            .baseline
1865            .submit_sync(self, Some("active"))
1866        {
1867            log::info!("baseline ping not submitted on active");
1868        }
1869
1870        self.set_dirty_flag(true);
1871    }
1872
1873    /// Performs the collection/cleanup operations required by becoming inactive.
1874    ///
1875    /// This functions generates a baseline and an events ping with reason
1876    /// `inactive` and then clears the dirty bit.
1877    pub fn handle_client_inactive(&mut self) {
1878        match self.session_manager.mode {
1879            SessionMode::Auto => {
1880                // In AUTO mode, don't end the session immediately. Instead record
1881                // inactive_since for lazy timeout evaluation on next activation.
1882                self.session_transition_to_inactive();
1883            }
1884            SessionMode::Lifecycle => {
1885                // End session immediately on going inactive.
1886                self.session_end(Some("inactive"));
1887            }
1888            SessionMode::Manual => {
1889                // No automatic session management.
1890            }
1891        }
1892
1893        if !self
1894            .internal_pings
1895            .baseline
1896            .submit_sync(self, Some("inactive"))
1897        {
1898            log::info!("baseline ping not submitted on inactive");
1899        }
1900
1901        if !self
1902            .internal_pings
1903            .events
1904            .submit_sync(self, Some("inactive"))
1905        {
1906            log::info!("events ping not submitted on inactive");
1907        }
1908
1909        self.set_dirty_flag(false);
1910    }
1911
1912    /// **Test-only API (exported for FFI purposes).**
1913    ///
1914    /// Deletes all stored metrics.
1915    ///
1916    /// Note that this also includes the ping sequence numbers, so it has
1917    /// the effect of resetting those to their initial values.
1918    pub fn test_clear_all_stores(&self) {
1919        if let Some(data) = self.data_store.as_ref() {
1920            data.clear_all()
1921        }
1922        // We don't care about this failing, maybe the data does just not exist.
1923        let _ = self.event_data_store.clear_all();
1924    }
1925
1926    /// Instructs the Metrics Ping Scheduler's thread to exit cleanly.
1927    /// If Glean was configured with `use_core_mps: false`, this has no effect.
1928    pub fn cancel_metrics_ping_scheduler(&self) {
1929        if self.schedule_metrics_pings {
1930            scheduler::cancel();
1931        }
1932    }
1933
1934    /// Instructs the Metrics Ping Scheduler to being scheduling metrics pings.
1935    /// If Glean wsa configured with `use_core_mps: false`, this has no effect.
1936    pub fn start_metrics_ping_scheduler(&self) {
1937        if self.schedule_metrics_pings {
1938            scheduler::schedule(self);
1939        }
1940    }
1941
1942    /// Clears the core attribution data.
1943    /// Does not clear glean.attribution.ext.
1944    pub fn clear_attribution(&self) {
1945        if let Some(data) = self.data_store.as_ref() {
1946            [
1947                &self.core_metrics.attribution_source,
1948                &self.core_metrics.attribution_medium,
1949                &self.core_metrics.attribution_campaign,
1950                &self.core_metrics.attribution_term,
1951                &self.core_metrics.attribution_content,
1952            ]
1953            .iter()
1954            .for_each(|metric| {
1955                let meta = metric.meta();
1956                _ = data.remove_single_metric(
1957                    meta.inner.lifetime,
1958                    &meta.storage_names()[0],
1959                    &meta.base_identifier(),
1960                );
1961            });
1962        }
1963    }
1964
1965    /// Updates attribution fields with new values.
1966    /// AttributionMetrics fields with `None` values will not overwrite older values.
1967    pub fn update_attribution(&self, attribution: AttributionMetrics) {
1968        if let Some(source) = attribution.source {
1969            self.core_metrics.attribution_source.set_sync(self, source);
1970        }
1971        if let Some(medium) = attribution.medium {
1972            self.core_metrics.attribution_medium.set_sync(self, medium);
1973        }
1974        if let Some(campaign) = attribution.campaign {
1975            self.core_metrics
1976                .attribution_campaign
1977                .set_sync(self, campaign);
1978        }
1979        if let Some(term) = attribution.term {
1980            self.core_metrics.attribution_term.set_sync(self, term);
1981        }
1982        if let Some(content) = attribution.content {
1983            self.core_metrics
1984                .attribution_content
1985                .set_sync(self, content);
1986        }
1987    }
1988
1989    /// **TEST-ONLY Method**
1990    ///
1991    /// Returns the current attribution metrics.
1992    pub fn test_get_attribution(&self) -> AttributionMetrics {
1993        AttributionMetrics {
1994            source: self
1995                .core_metrics
1996                .attribution_source
1997                .get_value(self, Some("glean_client_info")),
1998            medium: self
1999                .core_metrics
2000                .attribution_medium
2001                .get_value(self, Some("glean_client_info")),
2002            campaign: self
2003                .core_metrics
2004                .attribution_campaign
2005                .get_value(self, Some("glean_client_info")),
2006            term: self
2007                .core_metrics
2008                .attribution_term
2009                .get_value(self, Some("glean_client_info")),
2010            content: self
2011                .core_metrics
2012                .attribution_content
2013                .get_value(self, Some("glean_client_info")),
2014        }
2015    }
2016
2017    /// Clears the core distribution data.
2018    /// Does not clear glean.distribution.ext.
2019    pub fn clear_distribution(&self) {
2020        if let Some(data) = self.data_store.as_ref() {
2021            let meta = self.core_metrics.distribution_name.meta();
2022            _ = data.remove_single_metric(
2023                meta.inner.lifetime,
2024                &meta.storage_names()[0],
2025                &meta.base_identifier(),
2026            );
2027        }
2028    }
2029
2030    /// Updates distribution fields with new values.
2031    /// DistributionMetrics fields with `None` values will not overwrite older values.
2032    pub fn update_distribution(&self, distribution: DistributionMetrics) {
2033        if let Some(name) = distribution.name {
2034            self.core_metrics.distribution_name.set_sync(self, name);
2035        }
2036    }
2037
2038    /// **TEST-ONLY Method**
2039    ///
2040    /// Returns the current distribution metrics.
2041    pub fn test_get_distribution(&self) -> DistributionMetrics {
2042        DistributionMetrics {
2043            name: self
2044                .core_metrics
2045                .distribution_name
2046                .get_value(self, Some("glean_client_info")),
2047        }
2048    }
2049}