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