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