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