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