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