diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/glean/src | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/glean/src')
-rw-r--r-- | third_party/rust/glean/src/common_test.rs | 50 | ||||
-rw-r--r-- | third_party/rust/glean/src/configuration.rs | 152 | ||||
-rw-r--r-- | third_party/rust/glean/src/core_metrics.rs | 41 | ||||
-rw-r--r-- | third_party/rust/glean/src/lib.rs | 284 | ||||
-rw-r--r-- | third_party/rust/glean/src/net/http_uploader.rs | 24 | ||||
-rw-r--r-- | third_party/rust/glean/src/net/mod.rs | 221 | ||||
-rw-r--r-- | third_party/rust/glean/src/private/event.rs | 223 | ||||
-rw-r--r-- | third_party/rust/glean/src/private/mod.rs | 35 | ||||
-rw-r--r-- | third_party/rust/glean/src/private/ping.rs | 86 | ||||
-rw-r--r-- | third_party/rust/glean/src/system.rs | 106 | ||||
-rw-r--r-- | third_party/rust/glean/src/test.rs | 1504 |
11 files changed, 2726 insertions, 0 deletions
diff --git a/third_party/rust/glean/src/common_test.rs b/third_party/rust/glean/src/common_test.rs new file mode 100644 index 0000000000..fdb7cfadbf --- /dev/null +++ b/third_party/rust/glean/src/common_test.rs @@ -0,0 +1,50 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::ClientInfoMetrics; +use crate::{Configuration, ConfigurationBuilder}; +use std::sync::{Mutex, MutexGuard}; + +use once_cell::sync::Lazy; + +pub(crate) const GLOBAL_APPLICATION_ID: &str = "org.mozilla.rlb.test"; + +// Because Glean uses a global-singleton, we need to run the tests one-by-one to +// avoid different tests stomping over each other. +// This is only an issue because we're resetting Glean, this cannot happen in normal +// use of the RLB. +// +// We use a global lock to force synchronization of all tests, even if run multi-threaded. +// This allows us to run without `--test-threads 1`.` +pub(crate) fn lock_test() -> MutexGuard<'static, ()> { + static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(())); + + // This is going to be called from all the tests: make sure + // to enable logging. + env_logger::try_init().ok(); + + let lock = GLOBAL_LOCK.lock().unwrap(); + + lock +} + +// Create a new instance of Glean with a temporary directory. +// We need to keep the `TempDir` alive, so that it's not deleted before we stop using it. +pub(crate) fn new_glean( + configuration: Option<Configuration>, + clear_stores: bool, +) -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = match configuration { + Some(c) => c, + None => ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(), + }; + + crate::test_reset_glean(cfg, ClientInfoMetrics::unknown(), clear_stores); + dir +} diff --git a/third_party/rust/glean/src/configuration.rs b/third_party/rust/glean/src/configuration.rs new file mode 100644 index 0000000000..145f1a5732 --- /dev/null +++ b/third_party/rust/glean/src/configuration.rs @@ -0,0 +1,152 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use log::LevelFilter; + +use crate::net::PingUploader; + +use std::path::PathBuf; + +/// The default server pings are sent to. +pub(crate) const DEFAULT_GLEAN_ENDPOINT: &str = "https://incoming.telemetry.mozilla.org"; + +/// The Glean configuration. +/// +/// Optional values will be filled in with default values. +#[derive(Debug)] +pub struct Configuration { + /// Whether upload should be enabled. + pub upload_enabled: bool, + /// Path to a directory to store all data in. + pub data_path: PathBuf, + /// The application ID (will be sanitized during initialization). + pub application_id: String, + /// The maximum number of events to store before sending a ping containing events. + pub max_events: Option<usize>, + /// Whether Glean should delay persistence of data from metrics with ping lifetime. + pub delay_ping_lifetime_io: bool, + /// The server pings are sent to. + pub server_endpoint: Option<String>, + /// The instance of the uploader used to send pings. + pub uploader: Option<Box<dyn PingUploader + 'static>>, + /// Whether Glean should schedule "metrics" pings for you. + pub use_core_mps: bool, + /// Whether Glean should limit its storage to only that of registered pings. + /// Unless you know that all your and your libraries' pings are appropriately registered + /// _before_ init, you shouldn't use this. + pub trim_data_to_registered_pings: bool, + /// The internal logging level. + pub log_level: Option<LevelFilter>, +} + +/// Configuration builder. +/// +/// Let's you build a configuration from the required fields +/// and let you set optional fields individually. +#[derive(Debug)] +pub struct Builder { + /// Required: Whether upload should be enabled. + pub upload_enabled: bool, + /// Required: Path to a directory to store all data in. + pub data_path: PathBuf, + /// Required: The application ID (will be sanitized during initialization). + pub application_id: String, + /// Optional: The maximum number of events to store before sending a ping containing events. + /// Default: `None` + pub max_events: Option<usize>, + /// Optional: Whether Glean should delay persistence of data from metrics with ping lifetime. + /// Default: `false` + pub delay_ping_lifetime_io: bool, + /// Optional: The server pings are sent to. + /// Default: `None` + pub server_endpoint: Option<String>, + /// Optional: The instance of the uploader used to send pings. + /// Default: `None` + pub uploader: Option<Box<dyn PingUploader + 'static>>, + /// Optional: Whether Glean should schedule "metrics" pings for you. + /// Default: `false` + pub use_core_mps: bool, + /// Optional: Whether Glean should limit its storage to only that of registered pings. + /// Unless you know that all your and your libraries' pings are appropriately registered + /// _before_ init, you shouldn't use this. + /// Default: `false` + pub trim_data_to_registered_pings: bool, + /// Optional: The internal logging level. + /// Default: `None` + pub log_level: Option<LevelFilter>, +} + +impl Builder { + /// A new configuration builder. + pub fn new<P: Into<PathBuf>, S: Into<String>>( + upload_enabled: bool, + data_path: P, + application_id: S, + ) -> Self { + Self { + upload_enabled, + data_path: data_path.into(), + application_id: application_id.into(), + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: None, + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + } + } + + /// Generate the full configuration. + pub fn build(self) -> Configuration { + Configuration { + upload_enabled: self.upload_enabled, + data_path: self.data_path, + application_id: self.application_id, + max_events: self.max_events, + delay_ping_lifetime_io: self.delay_ping_lifetime_io, + server_endpoint: self.server_endpoint, + uploader: self.uploader, + use_core_mps: self.use_core_mps, + trim_data_to_registered_pings: self.trim_data_to_registered_pings, + log_level: self.log_level, + } + } + + /// Set the maximum number of events to store before sending a ping containing events. + pub fn with_max_events(mut self, max_events: usize) -> Self { + self.max_events = Some(max_events); + self + } + + /// Set whether Glean should delay persistence of data from metrics with ping lifetime. + pub fn with_delay_ping_lifetime_io(mut self, value: bool) -> Self { + self.delay_ping_lifetime_io = value; + self + } + + /// Set the server pings are sent to. + pub fn with_server_endpoint<S: Into<String>>(mut self, server_endpoint: S) -> Self { + self.server_endpoint = Some(server_endpoint.into()); + self + } + + /// Set the instance of the uploader used to send pings. + pub fn with_uploader<U: PingUploader + 'static>(mut self, uploader: U) -> Self { + self.uploader = Some(Box::new(uploader)); + self + } + + /// Set whether Glean should schedule "metrics" pings for you. + pub fn with_use_core_mps(mut self, value: bool) -> Self { + self.use_core_mps = value; + self + } + + /// Set whether Glean should limit its storage to only that of registered pings. + pub fn with_trim_data_to_registered_pings(mut self, value: bool) -> Self { + self.trim_data_to_registered_pings = value; + self + } +} diff --git a/third_party/rust/glean/src/core_metrics.rs b/third_party/rust/glean/src/core_metrics.rs new file mode 100644 index 0000000000..fd3c11f2a1 --- /dev/null +++ b/third_party/rust/glean/src/core_metrics.rs @@ -0,0 +1,41 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::system; + +/// Metrics included in every ping as `client_info`. +#[derive(Debug)] +pub struct ClientInfoMetrics { + /// The build identifier generated by the CI system (e.g. "1234/A"). + pub app_build: String, + /// The user visible version string (e.g. "1.0.3"). + pub app_display_version: String, + /// The product-provided release channel (e.g. "beta"). + pub channel: Option<String>, +} + +impl ClientInfoMetrics { + /// Creates the client info with dummy values for all. + pub fn unknown() -> Self { + ClientInfoMetrics { + app_build: "Unknown".to_string(), + app_display_version: "Unknown".to_string(), + channel: None, + } + } +} + +impl From<ClientInfoMetrics> for glean_core::ClientInfoMetrics { + fn from(metrics: ClientInfoMetrics) -> Self { + glean_core::ClientInfoMetrics { + app_build: metrics.app_build, + app_display_version: metrics.app_display_version, + channel: metrics.channel, + os_version: system::get_os_version(), + windows_build_number: system::get_windows_build_number(), + architecture: system::ARCH.to_string(), + ..Default::default() + } + } +} diff --git a/third_party/rust/glean/src/lib.rs b/third_party/rust/glean/src/lib.rs new file mode 100644 index 0000000000..d6ad16bdc1 --- /dev/null +++ b/third_party/rust/glean/src/lib.rs @@ -0,0 +1,284 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![allow(clippy::uninlined_format_args)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(missing_docs)] + +//! Glean is a modern approach for recording and sending Telemetry data. +//! +//! It's in use at Mozilla. +//! +//! All documentation can be found online: +//! +//! ## [The Glean SDK Book](https://mozilla.github.io/glean) +//! +//! ## Example +//! +//! Initialize Glean, register a ping and then send it. +//! +//! ```rust,no_run +//! # use glean::{ConfigurationBuilder, ClientInfoMetrics, Error, private::*}; +//! let cfg = ConfigurationBuilder::new(true, "/tmp/data", "org.mozilla.glean_core.example").build(); +//! glean::initialize(cfg, ClientInfoMetrics::unknown()); +//! +//! let prototype_ping = PingType::new("prototype", true, true, vec!()); +//! +//! prototype_ping.submit(None); +//! ``` + +use std::collections::HashMap; +use std::path::Path; + +use configuration::DEFAULT_GLEAN_ENDPOINT; +pub use configuration::{Builder as ConfigurationBuilder, Configuration}; +pub use core_metrics::ClientInfoMetrics; +pub use glean_core::{ + metrics::{Datetime, DistributionData, MemoryUnit, Rate, RecordedEvent, TimeUnit, TimerId}, + traits, CommonMetricData, Error, ErrorType, Glean, HistogramType, Lifetime, RecordedExperiment, + Result, +}; + +mod configuration; +mod core_metrics; +pub mod net; +pub mod private; +mod system; + +#[cfg(test)] +mod common_test; + +const LANGUAGE_BINDING_NAME: &str = "Rust"; + +/// Creates and initializes a new Glean object. +/// +/// See [`glean_core::Glean::new`] for more information. +/// +/// # Arguments +/// +/// * `cfg` - the [`Configuration`] options to initialize with. +/// * `client_info` - the [`ClientInfoMetrics`] values used to set Glean +/// core metrics. +pub fn initialize(cfg: Configuration, client_info: ClientInfoMetrics) { + initialize_internal(cfg, client_info); +} + +struct GleanEvents { + /// An instance of the upload manager + upload_manager: net::UploadManager, +} + +impl glean_core::OnGleanEvents for GleanEvents { + fn initialize_finished(&self) { + // intentionally left empty + } + + fn trigger_upload(&self) -> Result<(), glean_core::CallbackError> { + self.upload_manager.trigger_upload(); + Ok(()) + } + + fn start_metrics_ping_scheduler(&self) -> bool { + // We rely on the glean-core MPS. + // We always trigger an upload as it might have submitted a ping. + true + } + + fn cancel_uploads(&self) -> Result<(), glean_core::CallbackError> { + // intentionally left empty + Ok(()) + } + + fn shutdown(&self) -> Result<(), glean_core::CallbackError> { + self.upload_manager.shutdown(); + Ok(()) + } +} + +fn initialize_internal(cfg: Configuration, client_info: ClientInfoMetrics) -> Option<()> { + // Initialize the ping uploader. + let upload_manager = net::UploadManager::new( + cfg.server_endpoint + .unwrap_or_else(|| DEFAULT_GLEAN_ENDPOINT.to_string()), + cfg.uploader + .unwrap_or_else(|| Box::new(net::HttpUploader) as Box<dyn net::PingUploader>), + ); + + // Now make this the global object available to others. + let callbacks = Box::new(GleanEvents { upload_manager }); + + let core_cfg = glean_core::InternalConfiguration { + upload_enabled: cfg.upload_enabled, + data_path: cfg.data_path.display().to_string(), + application_id: cfg.application_id.clone(), + language_binding_name: LANGUAGE_BINDING_NAME.into(), + max_events: cfg.max_events.map(|m| m as u32), + delay_ping_lifetime_io: cfg.delay_ping_lifetime_io, + app_build: client_info.app_build.clone(), + use_core_mps: cfg.use_core_mps, + trim_data_to_registered_pings: cfg.trim_data_to_registered_pings, + log_level: cfg.log_level, + }; + + glean_core::glean_initialize(core_cfg, client_info.into(), callbacks); + Some(()) +} + +/// Shuts down Glean in an orderly fashion. +pub fn shutdown() { + glean_core::shutdown() +} + +/// Sets whether upload is enabled or not. +/// +/// See [`glean_core::Glean::set_upload_enabled`]. +pub fn set_upload_enabled(enabled: bool) { + glean_core::glean_set_upload_enabled(enabled) +} + +/// Collects and submits a ping for eventual uploading by name. +/// +/// Note that this needs to be public in order for RLB consumers to +/// use Glean debugging facilities. +/// +/// See [`glean_core::Glean.submit_ping_by_name`]. +pub fn submit_ping_by_name(ping: &str, reason: Option<&str>) { + let ping = ping.to_string(); + let reason = reason.map(|s| s.to_string()); + glean_core::glean_submit_ping_by_name(ping, reason) +} + +/// Indicate that an experiment is running. Glean will then add an +/// experiment annotation to the environment which is sent with pings. This +/// infomration is not persisted between runs. +/// +/// See [`glean_core::Glean::set_experiment_active`]. +pub fn set_experiment_active( + experiment_id: String, + branch: String, + extra: Option<HashMap<String, String>>, +) { + glean_core::glean_set_experiment_active(experiment_id, branch, extra.unwrap_or_default()) +} + +/// Indicate that an experiment is no longer running. +/// +/// See [`glean_core::Glean::set_experiment_inactive`]. +pub fn set_experiment_inactive(experiment_id: String) { + glean_core::glean_set_experiment_inactive(experiment_id) +} + +/// Set the remote configuration values for the metrics' disabled property +/// +/// See [`glean_core::Glean::set_metrics_enabled_config`]. +pub fn glean_set_metrics_enabled_config(json: String) { + glean_core::glean_set_metrics_enabled_config(json) +} + +/// Performs the collection/cleanup operations required by becoming active. +/// +/// This functions generates a baseline ping with reason `active` +/// and then sets the dirty bit. +/// This should be called whenever the consuming product becomes active (e.g. +/// getting to foreground). +pub fn handle_client_active() { + glean_core::glean_handle_client_active() +} + +/// Performs the collection/cleanup operations required by becoming inactive. +/// +/// This functions generates a baseline and an events ping with reason +/// `inactive` and then clears the dirty bit. +/// This should be called whenever the consuming product becomes inactive (e.g. +/// getting to background). +pub fn handle_client_inactive() { + glean_core::glean_handle_client_inactive() +} + +/// TEST ONLY FUNCTION. +/// Checks if an experiment is currently active. +pub fn test_is_experiment_active(experiment_id: String) -> bool { + glean_core::glean_test_get_experiment_data(experiment_id).is_some() +} + +/// TEST ONLY FUNCTION. +/// Returns the [`RecordedExperiment`] for the given `experiment_id` or panics if +/// the id isn't found. +pub fn test_get_experiment_data(experiment_id: String) -> Option<RecordedExperiment> { + glean_core::glean_test_get_experiment_data(experiment_id) +} + +/// Destroy the global Glean state. +pub(crate) fn destroy_glean(clear_stores: bool, data_path: &Path) { + let data_path = data_path.display().to_string(); + glean_core::glean_test_destroy_glean(clear_stores, Some(data_path)) +} + +/// TEST ONLY FUNCTION. +/// Resets the Glean state and triggers init again. +pub fn test_reset_glean(cfg: Configuration, client_info: ClientInfoMetrics, clear_stores: bool) { + destroy_glean(clear_stores, &cfg.data_path); + initialize_internal(cfg, client_info); + glean_core::join_init(); +} + +/// Sets a debug view tag. +/// +/// When the debug view tag is set, pings are sent with a `X-Debug-ID` header with the +/// value of the tag and are sent to the ["Ping Debug Viewer"](https://mozilla.github.io/glean/book/dev/core/internal/debug-pings.html). +/// +/// # Arguments +/// +/// * `tag` - A valid HTTP header value. Must match the regex: "[a-zA-Z0-9-]{1,20}". +/// +/// # Returns +/// +/// This will return `false` in case `tag` is not a valid tag and `true` otherwise. +/// If called before Glean is initialized it will always return `true`. +pub fn set_debug_view_tag(tag: &str) -> bool { + glean_core::glean_set_debug_view_tag(tag.to_string()) +} + +/// Sets the log pings debug option. +/// +/// When the log pings debug option is `true`, +/// we log the payload of all succesfully assembled pings. +/// +/// # Arguments +/// +/// * `value` - The value of the log pings option +pub fn set_log_pings(value: bool) { + glean_core::glean_set_log_pings(value) +} + +/// Sets source tags. +/// +/// Overrides any existing source tags. +/// Source tags will show in the destination datasets, after ingestion. +/// +/// **Note** If one or more tags are invalid, all tags are ignored. +/// +/// # Arguments +/// +/// * `tags` - A vector of at most 5 valid HTTP header values. Individual +/// tags must match the regex: "[a-zA-Z0-9-]{1,20}". +pub fn set_source_tags(tags: Vec<String>) { + glean_core::glean_set_source_tags(tags); +} + +/// Returns a timestamp corresponding to "now" with millisecond precision. +pub fn get_timestamp_ms() -> u64 { + glean_core::get_timestamp_ms() +} + +/// Asks the database to persist ping-lifetime data to disk. Probably expensive to call. +/// Only has effect when Glean is configured with `delay_ping_lifetime_io: true`. +/// If Glean hasn't been initialized this will dispatch and return Ok(()), +/// otherwise it will block until the persist is done and return its Result. +pub fn persist_ping_lifetime_data() { + glean_core::persist_ping_lifetime_data(); +} + +#[cfg(test)] +mod test; diff --git a/third_party/rust/glean/src/net/http_uploader.rs b/third_party/rust/glean/src/net/http_uploader.rs new file mode 100644 index 0000000000..4646fe61b4 --- /dev/null +++ b/third_party/rust/glean/src/net/http_uploader.rs @@ -0,0 +1,24 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::net::{PingUploader, UploadResult}; + +/// A simple mechanism to upload pings over HTTPS. +#[derive(Debug)] +pub struct HttpUploader; + +impl PingUploader for HttpUploader { + /// Uploads a ping to a server. + /// + /// # Arguments + /// + /// * `url` - the URL path to upload the data to. + /// * `body` - the serialized text data to send. + /// * `headers` - a vector of tuples containing the headers to send with + /// the request, i.e. (Name, Value). + fn upload(&self, url: String, _body: Vec<u8>, _headers: Vec<(String, String)>) -> UploadResult { + log::debug!("TODO bug 1675468: submitting to {:?}", url); + UploadResult::http_status(200) + } +} diff --git a/third_party/rust/glean/src/net/mod.rs b/third_party/rust/glean/src/net/mod.rs new file mode 100644 index 0000000000..cc1e14f3d6 --- /dev/null +++ b/third_party/rust/glean/src/net/mod.rs @@ -0,0 +1,221 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Handling the Glean upload logic. +//! +//! This doesn't perform the actual upload but rather handles +//! retries, upload limitations and error tracking. + +use std::sync::{atomic::Ordering, Arc, Mutex}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +use glean_core::upload::PingUploadTask; +pub use glean_core::upload::{PingRequest, UploadResult, UploadTaskAction}; + +pub use http_uploader::*; +use thread_state::{AtomicState, State}; + +mod http_uploader; + +/// A description of a component used to upload pings. +pub trait PingUploader: std::fmt::Debug + Send + Sync { + /// Uploads a ping to a server. + /// + /// # Arguments + /// + /// * `url` - the URL path to upload the data to. + /// * `body` - the serialized text data to send. + /// * `headers` - a vector of tuples containing the headers to send with + /// the request, i.e. (Name, Value). + fn upload(&self, url: String, body: Vec<u8>, headers: Vec<(String, String)>) -> UploadResult; +} + +/// The logic for uploading pings: this leaves the actual upload mechanism as +/// a detail of the user-provided object implementing [`PingUploader`]. +#[derive(Debug)] +pub(crate) struct UploadManager { + inner: Arc<Inner>, +} + +#[derive(Debug)] +struct Inner { + server_endpoint: String, + uploader: Box<dyn PingUploader + 'static>, + thread_running: AtomicState, + handle: Mutex<Option<JoinHandle<()>>>, +} + +impl UploadManager { + /// Create a new instance of the upload manager. + /// + /// # Arguments + /// + /// * `server_endpoint` - the server pings are sent to. + /// * `new_uploader` - the instance of the uploader used to send pings. + pub(crate) fn new( + server_endpoint: String, + new_uploader: Box<dyn PingUploader + 'static>, + ) -> Self { + Self { + inner: Arc::new(Inner { + server_endpoint, + uploader: new_uploader, + thread_running: AtomicState::new(State::Stopped), + handle: Mutex::new(None), + }), + } + } + + /// Signals Glean to upload pings at the next best opportunity. + pub(crate) fn trigger_upload(&self) { + // If no other upload proces is running, we're the one starting it. + // Need atomic compare/exchange to avoid any further races + // or we can end up with 2+ uploader threads. + if self + .inner + .thread_running + .compare_exchange( + State::Stopped, + State::Running, + Ordering::SeqCst, + Ordering::SeqCst, + ) + .is_err() + { + return; + } + + let inner = Arc::clone(&self.inner); + + // Need to lock before we start so that noone thinks we're not running. + let mut handle = self.inner.handle.lock().unwrap(); + let thread = thread::Builder::new() + .name("glean.upload".into()) + .spawn(move || { + log::trace!("Started glean.upload thread"); + loop { + let incoming_task = glean_core::glean_get_upload_task(); + + match incoming_task { + PingUploadTask::Upload { request } => { + log::trace!("Received upload task with request {:?}", request); + let doc_id = request.document_id.clone(); + let upload_url = format!("{}{}", inner.server_endpoint, request.path); + let headers: Vec<(String, String)> = + request.headers.into_iter().collect(); + let result = inner.uploader.upload(upload_url, request.body, headers); + // Process the upload response. + match glean_core::glean_process_ping_upload_response(doc_id, result) { + UploadTaskAction::Next => (), + UploadTaskAction::End => break, + } + + let status = inner.thread_running.load(Ordering::SeqCst); + // asked to shut down. let's do it. + if status == State::ShuttingDown { + break; + } + } + PingUploadTask::Wait { time } => { + log::trace!("Instructed to wait for {:?}ms", time); + thread::sleep(Duration::from_millis(time)); + } + PingUploadTask::Done { .. } => { + log::trace!("Received PingUploadTask::Done. Exiting."); + // Nothing to do here, break out of the loop. + break; + } + } + } + + // Clear the running flag to signal that this thread is done, + // but only if there's no shutdown thread. + let _ = inner.thread_running.compare_exchange( + State::Running, + State::Stopped, + Ordering::SeqCst, + Ordering::SeqCst, + ); + }) + .expect("Failed to spawn Glean's uploader thread"); + *handle = Some(thread); + } + + pub(crate) fn shutdown(&self) { + // mark as shutting down. + self.inner + .thread_running + .store(State::ShuttingDown, Ordering::SeqCst); + + // take the thread handle out. + let mut handle = self.inner.handle.lock().unwrap(); + let thread = handle.take(); + + if let Some(thread) = thread { + thread + .join() + .expect("couldn't join on the uploader thread."); + } + } +} + +mod thread_state { + use std::sync::atomic::{AtomicU8, Ordering}; + + #[derive(Debug, PartialEq)] + #[repr(u8)] + pub enum State { + Stopped = 0, + Running = 1, + ShuttingDown = 2, + } + + #[derive(Debug)] + pub struct AtomicState(AtomicU8); + + impl AtomicState { + const fn to_u8(val: State) -> u8 { + val as u8 + } + + fn from_u8(val: u8) -> State { + #![allow(non_upper_case_globals)] + const U8_Stopped: u8 = State::Stopped as u8; + const U8_Running: u8 = State::Running as u8; + const U8_ShuttingDown: u8 = State::ShuttingDown as u8; + match val { + U8_Stopped => State::Stopped, + U8_Running => State::Running, + U8_ShuttingDown => State::ShuttingDown, + _ => panic!("Invalid enum discriminant"), + } + } + + pub const fn new(v: State) -> AtomicState { + AtomicState(AtomicU8::new(Self::to_u8(v))) + } + + pub fn load(&self, order: Ordering) -> State { + Self::from_u8(self.0.load(order)) + } + + pub fn store(&self, val: State, order: Ordering) { + self.0.store(Self::to_u8(val), order) + } + + pub fn compare_exchange( + &self, + current: State, + new: State, + success: Ordering, + failure: Ordering, + ) -> Result<State, State> { + self.0 + .compare_exchange(Self::to_u8(current), Self::to_u8(new), success, failure) + .map(Self::from_u8) + .map_err(Self::from_u8) + } + } +} diff --git a/third_party/rust/glean/src/private/event.rs b/third_party/rust/glean/src/private/event.rs new file mode 100644 index 0000000000..d646ec3eb6 --- /dev/null +++ b/third_party/rust/glean/src/private/event.rs @@ -0,0 +1,223 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::{collections::HashMap, marker::PhantomData}; + +use glean_core::traits; + +use crate::{ErrorType, RecordedEvent}; + +pub use glean_core::traits::NoExtraKeys; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// Developer-facing API for recording event metrics. +/// +/// Instances of this class type are automatically generated by the parsers +/// at build time, allowing developers to record values that were previously +/// registered in the metrics.yaml file. +#[derive(Clone)] +pub struct EventMetric<K> { + pub(crate) inner: glean_core::metrics::EventMetric, + extra_keys: PhantomData<K>, +} + +impl<K: traits::ExtraKeys> EventMetric<K> { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + let allowed_extra_keys = K::ALLOWED_KEYS.iter().map(|s| s.to_string()).collect(); + let inner = glean_core::metrics::EventMetric::new(meta, allowed_extra_keys); + Self { + inner, + extra_keys: PhantomData, + } + } + + /// The public constructor used by runtime-defined metrics. + pub fn with_runtime_extra_keys( + meta: glean_core::CommonMetricData, + allowed_extra_keys: Vec<String>, + ) -> Self { + let inner = glean_core::metrics::EventMetric::new(meta, allowed_extra_keys); + Self { + inner, + extra_keys: PhantomData, + } + } + + /// Record a new event with a provided timestamp. + /// + /// It's the caller's responsibility to ensure the timestamp comes from the same clock source. + /// Use [`glean::get_timestamp_ms`](crate::get_timestamp_ms) to get a valid timestamp. + pub fn record_with_time(&self, timestamp: u64, extra: HashMap<String, String>) { + self.inner.record_with_time(timestamp, extra); + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common_test::{lock_test, new_glean}; + use crate::CommonMetricData; + + #[test] + fn no_extra_keys() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + let metric: EventMetric<NoExtraKeys> = EventMetric::new(CommonMetricData { + name: "event".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }); + + metric.record(None); + metric.record(None); + + let data = metric.test_get_value(None).expect("no event recorded"); + assert_eq!(2, data.len()); + assert!(data[0].timestamp <= data[1].timestamp); + } + + #[test] + fn with_extra_keys() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + #[derive(Default, Debug, Clone, Hash, Eq, PartialEq)] + struct SomeExtra { + key1: Option<String>, + key2: Option<String>, + } + + impl glean_core::traits::ExtraKeys for SomeExtra { + const ALLOWED_KEYS: &'static [&'static str] = &["key1", "key2"]; + + fn into_ffi_extra(self) -> HashMap<String, String> { + let mut map = HashMap::new(); + self.key1.and_then(|key1| map.insert("key1".into(), key1)); + self.key2.and_then(|key2| map.insert("key2".into(), key2)); + map + } + } + + let metric: EventMetric<SomeExtra> = EventMetric::new(CommonMetricData { + name: "event".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }); + + let map1 = SomeExtra { + key1: Some("1".into()), + ..Default::default() + }; + metric.record(map1); + + let map2 = SomeExtra { + key1: Some("1".into()), + key2: Some("2".into()), + }; + metric.record(map2); + + metric.record(None); + + let data = metric.test_get_value(None).expect("no event recorded"); + assert_eq!(3, data.len()); + assert!(data[0].timestamp <= data[1].timestamp); + assert!(data[1].timestamp <= data[2].timestamp); + + let mut map = HashMap::new(); + map.insert("key1".into(), "1".into()); + assert_eq!(Some(map), data[0].extra); + + let mut map = HashMap::new(); + map.insert("key1".into(), "1".into()); + map.insert("key2".into(), "2".into()); + assert_eq!(Some(map), data[1].extra); + + assert_eq!(None, data[2].extra); + } + + #[test] + fn with_runtime_extra_keys() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + #[derive(Default, Debug, Clone, Hash, Eq, PartialEq)] + struct RuntimeExtra {} + + impl glean_core::traits::ExtraKeys for RuntimeExtra { + const ALLOWED_KEYS: &'static [&'static str] = &[]; + + fn into_ffi_extra(self) -> HashMap<String, String> { + HashMap::new() + } + } + + let metric: EventMetric<RuntimeExtra> = EventMetric::with_runtime_extra_keys( + CommonMetricData { + name: "event".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + vec!["key1".into(), "key2".into()], + ); + + let map1 = HashMap::from([("key1".into(), "1".into())]); + metric.record_with_time(0, map1); + + let map2 = HashMap::from([("key1".into(), "1".into()), ("key2".into(), "2".into())]); + metric.record_with_time(1, map2); + + metric.record_with_time(2, HashMap::new()); + + let data = metric.test_get_value(None).expect("no event recorded"); + assert_eq!(3, data.len()); + assert!(data[0].timestamp <= data[1].timestamp); + assert!(data[1].timestamp <= data[2].timestamp); + + let mut map = HashMap::new(); + map.insert("key1".into(), "1".into()); + assert_eq!(Some(map), data[0].extra); + + let mut map = HashMap::new(); + map.insert("key1".into(), "1".into()); + map.insert("key2".into(), "2".into()); + assert_eq!(Some(map), data[1].extra); + + assert_eq!(None, data[2].extra); + } +} + +#[inherent] +impl<K: traits::ExtraKeys> traits::Event for EventMetric<K> { + type Extra = K; + + pub fn record<M: Into<Option<<Self as traits::Event>::Extra>>>(&self, extra: M) { + let extra = extra + .into() + .map(|e| e.into_ffi_extra()) + .unwrap_or_else(HashMap::new); + self.inner.record(extra); + } + + pub fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<Vec<RecordedEvent>> { + let ping_name = ping_name.into().map(|s| s.to_string()); + self.inner.test_get_value(ping_name) + } + + pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 { + self.inner.test_get_num_recorded_errors(error) + } +} diff --git a/third_party/rust/glean/src/private/mod.rs b/third_party/rust/glean/src/private/mod.rs new file mode 100644 index 0000000000..8a5c304193 --- /dev/null +++ b/third_party/rust/glean/src/private/mod.rs @@ -0,0 +1,35 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! The different metric types supported by the Glean SDK to handle data. + +mod event; +mod ping; + +pub use event::EventMetric; +pub use glean_core::BooleanMetric; +pub use glean_core::CounterMetric; +pub use glean_core::CustomDistributionMetric; +pub use glean_core::DenominatorMetric; +pub use glean_core::MemoryDistributionMetric; +pub use glean_core::NumeratorMetric; +pub use glean_core::QuantityMetric; +pub use glean_core::RateMetric; +pub use glean_core::RecordedExperiment; +pub use glean_core::StringListMetric; +pub use glean_core::StringMetric; +pub use glean_core::TextMetric; +pub use glean_core::TimespanMetric; +pub use glean_core::TimingDistributionMetric; +pub use glean_core::UrlMetric; +pub use glean_core::UuidMetric; +pub use glean_core::{AllowLabeled, LabeledMetric}; +pub use glean_core::{Datetime, DatetimeMetric}; +pub use ping::PingType; + +// Re-export types that are used by the glean_parser-generated code. +#[doc(hidden)] +pub mod __export { + pub use once_cell::sync::Lazy; +} diff --git a/third_party/rust/glean/src/private/ping.rs b/third_party/rust/glean/src/private/ping.rs new file mode 100644 index 0000000000..85f8bef58b --- /dev/null +++ b/third_party/rust/glean/src/private/ping.rs @@ -0,0 +1,86 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::sync::{Arc, Mutex}; + +type BoxedCallback = Box<dyn FnOnce(Option<&str>) + Send + 'static>; + +/// A ping is a bundle of related metrics, gathered in a payload to be transmitted. +/// +/// The ping payload will be encoded in JSON format and contains shared information data. +#[derive(Clone)] +pub struct PingType { + pub(crate) inner: glean_core::metrics::PingType, + + /// **Test-only API** + /// + /// A function to be called right before a ping is submitted. + test_callback: Arc<Mutex<Option<BoxedCallback>>>, +} + +impl PingType { + /// Creates a new ping type. + /// + /// # Arguments + /// + /// * `name` - The name of the ping. + /// * `include_client_id` - Whether to include the client ID in the assembled ping when. + /// * `send_if_empty` - Whether the ping should be sent empty or not. + /// * `reason_codes` - The valid reason codes for this ping. + pub fn new<A: Into<String>>( + name: A, + include_client_id: bool, + send_if_empty: bool, + reason_codes: Vec<String>, + ) -> Self { + let inner = glean_core::metrics::PingType::new( + name.into(), + include_client_id, + send_if_empty, + reason_codes, + ); + + Self { + inner, + test_callback: Arc::new(Default::default()), + } + } + + /// Submits the ping for eventual uploading. + /// + /// The ping content is assembled as soon as possible, but upload is not + /// guaranteed to happen immediately, as that depends on the upload policies. + /// + /// If the ping currently contains no content, it will not be sent, + /// unless it is configured to be sent if empty. + /// + /// # Arguments + /// + /// * `reason` - the reason the ping was triggered. Included in the + /// `ping_info.reason` part of the payload. + pub fn submit(&self, reason: Option<&str>) { + let mut cb = self.test_callback.lock().unwrap(); + let cb = cb.take(); + if let Some(cb) = cb { + cb(reason) + } + + self.inner.submit(reason.map(|s| s.to_string())) + } + + /// **Test-only API** + /// + /// Attach a callback to be called right before a new ping is submitted. + /// The provided function is called exactly once before submitting a ping. + /// + /// Note: The callback will be called on any call to submit. + /// A ping might not be sent afterwards, e.g. if the ping is otherwise empty (and + /// `send_if_empty` is `false`). + pub fn test_before_next_submit(&self, cb: impl FnOnce(Option<&str>) + Send + 'static) { + let mut test_callback = self.test_callback.lock().unwrap(); + + let cb = Box::new(cb); + *test_callback = Some(cb); + } +} diff --git a/third_party/rust/glean/src/system.rs b/third_party/rust/glean/src/system.rs new file mode 100644 index 0000000000..4816f2552a --- /dev/null +++ b/third_party/rust/glean/src/system.rs @@ -0,0 +1,106 @@ +// Copyright (c) 2017 The Rust Project Developers +// Copyright (c) 2018-2020 The Rust Secure Code Working Group +// Licensed under the MIT License. +// Original license: +// https://github.com/rustsec/rustsec/blob/2a080f173ad9d8ac7fa260f0a3a6aebf0000de06/platforms/LICENSE-MIT +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Detect and expose `target_arch` as a constant + +#[cfg(target_arch = "aarch64")] +/// `target_arch` when building this crate: `aarch64` +pub const ARCH: &str = "aarch64"; + +#[cfg(target_arch = "arm")] +/// `target_arch` when building this crate: `arm` +pub const ARCH: &str = "arm"; + +#[cfg(target_arch = "x86")] +/// `target_arch` when building this crate: `x86` +pub const ARCH: &str = "x86"; + +#[cfg(target_arch = "x86_64")] +/// `target_arch` when building this crate: `x86_64` +pub const ARCH: &str = "x86_64"; + +#[cfg(not(any( + target_arch = "aarch64", + target_arch = "arm", + target_arch = "x86", + target_arch = "x86_64" +)))] +/// `target_arch` when building this crate: unknown! +pub const ARCH: &str = "Unknown"; + +#[cfg(any(target_os = "macos", target_os = "windows"))] +/// Returns Darwin kernel version for MacOS, or NT Kernel version for Windows +pub fn get_os_version() -> String { + whatsys::kernel_version().unwrap_or_else(|| "Unknown".to_owned()) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +/// Returns "Unknown" for platforms other than Linux, MacOS or Windows +pub fn get_os_version() -> String { + "Unknown".to_owned() +} + +#[cfg(target_os = "linux")] +/// Returns Linux kernel version, in the format of <Major>.<Minor> e.g. 5.8 +pub fn get_os_version() -> String { + parse_linux_os_string(whatsys::kernel_version().unwrap_or_else(|| "Unknown".to_owned())) +} + +#[cfg(target_os = "windows")] +/// Returns the Windows build number, e.g. 22000 +pub fn get_windows_build_number() -> Option<i64> { + match whatsys::windows_build_number() { + // Cast to i64 to work with QuantityMetric type + Some(i) => Some(i as i64), + _ => None, + } +} + +#[cfg(not(target_os = "windows"))] +/// Returns None, for non-Windows operating systems +pub fn get_windows_build_number() -> Option<i64> { + None +} + +#[cfg(target_os = "linux")] +fn parse_linux_os_string(os_str: String) -> String { + os_str.split('.').take(2).collect::<Vec<&str>>().join(".") +} + +#[test] +#[cfg(target_os = "linux")] +fn parse_fixed_linux_os_string() { + let alpine_os_string = "4.12.0-rc6-g48ec1f0-dirty".to_owned(); + assert_eq!(parse_linux_os_string(alpine_os_string), "4.12"); + let centos_os_string = "3.10.0-514.16.1.el7.x86_64".to_owned(); + assert_eq!(parse_linux_os_string(centos_os_string), "3.10"); + let ubuntu_os_string = "5.8.0-44-generic".to_owned(); + assert_eq!(parse_linux_os_string(ubuntu_os_string), "5.8"); +} diff --git a/third_party/rust/glean/src/test.rs b/third_party/rust/glean/src/test.rs new file mode 100644 index 0000000000..bca1993d0b --- /dev/null +++ b/third_party/rust/glean/src/test.rs @@ -0,0 +1,1504 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::io::Read; +use std::sync::{Arc, Barrier, Mutex}; +use std::thread::{self, ThreadId}; + +use flate2::read::GzDecoder; +use serde_json::Value as JsonValue; + +use crate::private::PingType; +use crate::private::{BooleanMetric, CounterMetric, EventMetric, StringMetric, TextMetric}; + +use super::*; +use crate::common_test::{lock_test, new_glean, GLOBAL_APPLICATION_ID}; + +#[test] +fn send_a_ping() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Define a new ping and submit it. + const PING_NAME: &str = "test-ping"; + let custom_ping = private::PingType::new(PING_NAME, true, true, vec![]); + custom_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains(PING_NAME)); +} + +#[test] +fn disabling_upload_disables_metrics_recording() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + let metric = BooleanMetric::new(CommonMetricData { + name: "bool_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + crate::set_upload_enabled(false); + + assert!(metric.test_get_value(Some("store1".into())).is_none()) +} + +#[test] +fn test_experiments_recording() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + set_experiment_active("experiment_test".to_string(), "branch_a".to_string(), None); + let mut extra = HashMap::new(); + extra.insert("test_key".to_string(), "value".to_string()); + set_experiment_active( + "experiment_api".to_string(), + "branch_b".to_string(), + Some(extra), + ); + assert!(test_is_experiment_active("experiment_test".to_string())); + assert!(test_is_experiment_active("experiment_api".to_string())); + set_experiment_inactive("experiment_test".to_string()); + assert!(!test_is_experiment_active("experiment_test".to_string())); + assert!(test_is_experiment_active("experiment_api".to_string())); + let stored_data = test_get_experiment_data("experiment_api".to_string()).unwrap(); + assert_eq!("branch_b", stored_data.branch); + assert_eq!("value", stored_data.extra.unwrap()["test_key"]); +} + +#[test] +fn test_experiments_recording_before_glean_inits() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + destroy_glean(true, &tmpname); + + set_experiment_active( + "experiment_set_preinit".to_string(), + "branch_a".to_string(), + None, + ); + set_experiment_active( + "experiment_preinit_disabled".to_string(), + "branch_a".to_string(), + None, + ); + set_experiment_inactive("experiment_preinit_disabled".to_string()); + + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + false, + ); + + assert!(test_is_experiment_active( + "experiment_set_preinit".to_string() + )); + assert!(!test_is_experiment_active( + "experiment_preinit_disabled".to_string() + )); +} + +#[test] +fn sending_of_foreground_background_pings() { + let _lock = lock_test(); + + let click: EventMetric<traits::NoExtraKeys> = private::EventMetric::new(CommonMetricData { + name: "click".into(), + category: "ui".into(), + send_in_pings: vec!["events".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<String>(3); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Simulate becoming active. + handle_client_active(); + + // We expect a baseline ping to be generated here (reason: 'active'). + let url = r.recv().unwrap(); + assert!(url.contains("baseline")); + + // Recording an event so that an "events" ping will contain data. + click.record(None); + + // Simulate becoming inactive + handle_client_inactive(); + + // Wait for the pings to arrive. + let mut expected_pings = vec!["baseline", "events"]; + for _ in 0..2 { + let url = r.recv().unwrap(); + // If the url contains the expected reason, remove it from the list. + expected_pings.retain(|&name| !url.contains(name)); + } + // We received all the expected pings. + assert_eq!(0, expected_pings.len()); + + // Simulate becoming active again. + handle_client_active(); + + // We expect a baseline ping to be generated here (reason: 'active'). + let url = r.recv().unwrap(); + assert!(url.contains("baseline")); +} + +#[test] +fn sending_of_startup_baseline_ping() { + let _lock = lock_test(); + + // Create an instance of Glean and then flip the dirty + // bit to true. + let data_dir = new_glean(None, true); + + glean_core::glean_set_dirty_flag(true); + + // Restart glean and wait for a baseline ping to be generated. + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let tmpname = data_dir.path().to_path_buf(); + + // Now reset Glean: it should still send a baseline ping with reason + // dirty_startup when starting, because of the dirty bit being set. + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + false, + ); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("baseline")); +} + +#[test] +fn no_dirty_baseline_on_clean_shutdowns() { + let _lock = lock_test(); + + // Create an instance of Glean, wait for init and then flip the dirty + // bit to true. + let data_dir = new_glean(None, true); + + glean_core::glean_set_dirty_flag(true); + + crate::shutdown(); + + // Restart glean and wait for a baseline ping to be generated. + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let tmpname = data_dir.path().to_path_buf(); + + // Now reset Glean: it should not send a baseline ping, because + // we cleared the dirty bit. + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + false, + ); + + // We don't expect a startup ping. + assert_eq!(r.try_recv(), Err(crossbeam_channel::TryRecvError::Empty)); +} + +#[test] +fn initialize_must_not_crash_if_data_dir_is_messed_up() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpdirname = dir.path(); + // Create a file in the temporary dir and use that as the + // name of the Glean data dir. + let file_path = tmpdirname.to_path_buf().join("notadir"); + std::fs::write(file_path.clone(), "test").expect("The test Glean dir file must be created"); + + let cfg = Configuration { + data_path: file_path, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + test_reset_glean(cfg, ClientInfoMetrics::unknown(), false); + + // We don't need to sleep here. + // The `test_reset_glean` already waited on the initialize task. +} + +#[test] +fn queued_recorded_metrics_correctly_record_during_init() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + destroy_glean(true, &tmpname); + + let metric = CounterMetric::new(CommonMetricData { + name: "counter_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + // This will queue 3 tasks that will add to the metric value once Glean is initialized + for _ in 0..3 { + metric.add(1); + } + + // TODO: To be fixed in bug 1677150. + // Ensure that no value has been stored yet since the tasks have only been queued + // and not executed yet + + // Calling `new_glean` here will cause Glean to be initialized and should cause the queued + // tasks recording metrics to execute + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + let _t = new_glean(Some(cfg), false); + + // Verify that the callback was executed by testing for the correct value + assert!(metric.test_get_value(None).is_some(), "Value must exist"); + assert_eq!(3, metric.test_get_value(None).unwrap(), "Value must match"); +} + +#[test] +fn initializing_twice_is_a_noop() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + test_reset_glean( + Configuration { + data_path: tmpname.clone(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + true, + ); + + // Glean was initialized and it waited for a full initialization to finish. + // We now just want to try to initialize again. + // This will bail out early. + + crate::initialize( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + ); + + // We don't need to sleep here. + // The `test_reset_glean` already waited on the initialize task, + // and the 2nd initialize will bail out early. + // + // All we tested is that this didn't crash. +} + +#[test] +fn dont_handle_events_when_uninitialized() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + test_reset_glean( + Configuration { + data_path: tmpname.clone(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + true, + ); + + // Ensure there's at least one event recorded, + // otherwise the ping is not sent. + let click: EventMetric<traits::NoExtraKeys> = private::EventMetric::new(CommonMetricData { + name: "click".into(), + category: "ui".into(), + send_in_pings: vec!["events".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + click.record(None); + // Wait for the dispatcher. + assert_ne!(None, click.test_get_value(None)); + + // Now destroy Glean. We test submission when not initialized. + destroy_glean(false, &tmpname); + + // We reach into `glean_core` to test this, + // only there we can synchronously submit and get a return value. + assert!(!glean_core::glean_submit_ping_by_name_sync( + "events".to_string(), + None + )); +} + +// TODO: Should probably move into glean-core. +#[test] +fn the_app_channel_must_be_correctly_set_if_requested() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + // Internal metric, replicated here for testing. + let app_channel = StringMetric::new(CommonMetricData { + name: "app_channel".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }); + + // No app_channel reported. + let client_info = ClientInfoMetrics { + channel: None, + ..ClientInfoMetrics::unknown() + }; + test_reset_glean( + Configuration { + data_path: tmpname.clone(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + client_info, + true, + ); + assert!(app_channel.test_get_value(None).is_none()); + + // Custom app_channel reported. + let client_info = ClientInfoMetrics { + channel: Some("testing".into()), + ..ClientInfoMetrics::unknown() + }; + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + client_info, + true, + ); + assert_eq!("testing", app_channel.test_get_value(None).unwrap()); +} + +#[test] +fn ping_collection_must_happen_after_concurrently_scheduled_metrics_recordings() { + // Given the following block of code: + // + // Metric.A.set("SomeTestValue") + // Glean.submitPings(listOf("custom-ping-1")) + // + // This test ensures that "custom-ping-1" contains "metric.a" with a value of "SomeTestValue" + // when the ping is collected. + + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<(String, JsonValue)>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + // Decode the gzipped body. + let mut gzip_decoder = GzDecoder::new(&body[..]); + let mut s = String::with_capacity(body.len()); + + let data = gzip_decoder + .read_to_string(&mut s) + .ok() + .map(|_| &s[..]) + .or_else(|| std::str::from_utf8(&body).ok()) + .and_then(|payload| serde_json::from_str(payload).ok()) + .unwrap(); + self.sender.send((url, data)).unwrap(); + net::UploadResult::http_status(200) + } + } + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + true, + ); + + let ping_name = "custom_ping_1"; + let ping = private::PingType::new(ping_name, true, false, vec![]); + let metric = private::StringMetric::new(CommonMetricData { + name: "string_metric".into(), + category: "telemetry".into(), + send_in_pings: vec![ping_name.into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + let test_value = "SomeTestValue"; + metric.set(test_value.to_string()); + ping.submit(None); + + // Wait for the ping to arrive. + let (url, body) = r.recv().unwrap(); + assert!(url.contains(ping_name)); + + assert_eq!( + test_value, + body["metrics"]["string"]["telemetry.string_metric"] + ); +} + +#[test] +fn basic_metrics_should_be_cleared_when_disabling_uploading() { + let _lock = lock_test(); + + let _t = new_glean(None, false); + + let metric = private::StringMetric::new(CommonMetricData { + name: "string_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["default".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + assert!(metric.test_get_value(None).is_none()); + + metric.set("TEST VALUE".into()); + assert!(metric.test_get_value(None).is_some()); + + set_upload_enabled(false); + assert!(metric.test_get_value(None).is_none()); + metric.set("TEST VALUE".into()); + assert!(metric.test_get_value(None).is_none()); + + set_upload_enabled(true); + assert!(metric.test_get_value(None).is_none()); + metric.set("TEST VALUE".into()); + assert_eq!("TEST VALUE", metric.test_get_value(None).unwrap()); +} + +// TODO: Should probably move into glean-core. +#[test] +fn core_metrics_should_be_cleared_and_restored_when_disabling_and_enabling_uploading() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + // No app_channel reported. + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + true, + ); + + // Internal metric, replicated here for testing. + let os_version = StringMetric::new(CommonMetricData { + name: "os_version".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }); + + assert!(os_version.test_get_value(None).is_some()); + + set_upload_enabled(false); + assert!(os_version.test_get_value(None).is_none()); + + set_upload_enabled(true); + assert!(os_version.test_get_value(None).is_some()); +} + +#[test] +fn sending_deletion_ping_if_disabled_outside_of_run() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = Configuration { + data_path: tmpname.clone(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Now reset Glean and disable upload: it should still send a deletion request + // ping even though we're just starting. + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: false, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + false, + ); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("deletion-request")); +} + +#[test] +fn no_sending_of_deletion_ping_if_unchanged_outside_of_run() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = Configuration { + data_path: tmpname.clone(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Now reset Glean and keep upload enabled: no deletion-request + // should be sent. + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + false, + ); + + assert_eq!(0, r.len()); +} + +#[test] +fn test_sending_of_startup_baseline_ping_with_application_lifetime_metric() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<(String, JsonValue)>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + // Decode the gzipped body. + let mut gzip_decoder = GzDecoder::new(&body[..]); + let mut s = String::with_capacity(body.len()); + + let data = gzip_decoder + .read_to_string(&mut s) + .ok() + .map(|_| &s[..]) + .or_else(|| std::str::from_utf8(&body).ok()) + .and_then(|payload| serde_json::from_str(payload).ok()) + .unwrap(); + self.sender.send((url, data)).unwrap(); + net::UploadResult::http_status(200) + } + } + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + test_reset_glean( + Configuration { + data_path: tmpname.clone(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + true, + ); + + // Reaching into the core. + glean_core::glean_set_dirty_flag(true); + + let metric = private::StringMetric::new(CommonMetricData { + name: "app_lifetime".into(), + category: "telemetry".into(), + send_in_pings: vec!["baseline".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }); + let test_value = "HELLOOOOO!"; + metric.set(test_value.into()); + assert_eq!(test_value, metric.test_get_value(None).unwrap()); + + // Restart glean and don't clear the stores. + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }, + ClientInfoMetrics::unknown(), + false, + ); + + let (url, body) = r.recv().unwrap(); + assert!(url.contains("/baseline/")); + + // We set the dirty bit above. + assert_eq!("dirty_startup", body["ping_info"]["reason"]); + assert_eq!( + test_value, + body["metrics"]["string"]["telemetry.app_lifetime"] + ); +} + +#[test] +fn setting_debug_view_tag_before_initialization_should_not_crash() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + destroy_glean(true, &tmpname); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<Vec<(String, String)>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(headers).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Attempt to set a debug view tag before Glean is initialized. + set_debug_view_tag("valid-tag"); + + // Create a custom configuration to use a fake uploader. + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Submit a baseline ping. + submit_ping_by_name("baseline", Some("inactive")); + + // Wait for the ping to arrive. + let headers = r.recv().unwrap(); + assert_eq!( + "valid-tag", + headers.iter().find(|&kv| kv.0 == "X-Debug-ID").unwrap().1 + ); +} + +#[test] +fn setting_source_tags_before_initialization_should_not_crash() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + destroy_glean(true, &tmpname); + //assert!(!was_initialize_called()); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<Vec<(String, String)>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(headers).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Attempt to set source tags before Glean is initialized. + set_source_tags(vec!["valid-tag1".to_string(), "valid-tag2".to_string()]); + + // Create a custom configuration to use a fake uploader. + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Submit a baseline ping. + submit_ping_by_name("baseline", Some("inactive")); + + // Wait for the ping to arrive. + let headers = r.recv().unwrap(); + assert_eq!( + "valid-tag1,valid-tag2", + headers + .iter() + .find(|&kv| kv.0 == "X-Source-Tags") + .unwrap() + .1 + ); +} + +#[test] +fn setting_source_tags_after_initialization_should_not_crash() { + let _lock = lock_test(); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<Vec<(String, String)>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(headers).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Attempt to set source tags after `Glean.initialize` is called, + // but before Glean is fully initialized. + //assert!(was_initialize_called()); + set_source_tags(vec!["valid-tag1".to_string(), "valid-tag2".to_string()]); + + // Submit a baseline ping. + submit_ping_by_name("baseline", Some("inactive")); + + // Wait for the ping to arrive. + let headers = r.recv().unwrap(); + assert_eq!( + "valid-tag1,valid-tag2", + headers + .iter() + .find(|&kv| kv.0 == "X-Source-Tags") + .unwrap() + .1 + ); +} + +#[test] +fn flipping_upload_enabled_respects_order_of_events() { + // NOTES(janerik): + // I'm reasonably sure this test is excercising the right code paths + // and from the log output it does the right thing: + // + // * It fully initializes with the assumption uploadEnabled=true + // * It then disables upload + // * Then it submits the custom ping, which rightfully is ignored because uploadEnabled=false. + // + // The test passes. + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + // We create a ping and a metric before we initialize Glean + let sample_ping = PingType::new("sample-ping-1", true, false, vec![]); + let metric = private::StringMetric::new(CommonMetricData { + name: "string_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["sample-ping-1".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + let _t = new_glean(Some(cfg), true); + + // Glean might still be initializing. Disable upload. + set_upload_enabled(false); + + // Set data and try to submit a custom ping. + metric.set("some-test-value".into()); + sample_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("deletion-request")); +} + +#[test] +fn registering_pings_before_init_must_work() { + let _lock = lock_test(); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<String>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom ping and attempt its registration. + let sample_ping = PingType::new("pre-register", true, true, vec![]); + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Submit a baseline ping. + sample_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("pre-register")); +} + +#[test] +fn test_a_ping_before_submission() { + let _lock = lock_test(); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<String>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Create a custom ping and register it. + let sample_ping = PingType::new("custom1", true, true, vec![]); + + let metric = CounterMetric::new(CommonMetricData { + name: "counter_metric".into(), + category: "test".into(), + send_in_pings: vec!["custom1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + metric.add(1); + + sample_ping.test_before_next_submit(move |reason| { + assert_eq!(None, reason); + assert_eq!(1, metric.test_get_value(None).unwrap()); + }); + + // Submit a baseline ping. + sample_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("custom1")); +} + +#[test] +fn test_boolean_get_num_errors() { + let _lock = lock_test(); + + let _t = new_glean(None, false); + + let metric = BooleanMetric::new(CommonMetricData { + name: "counter_metric".into(), + category: "test".into(), + send_in_pings: vec!["custom1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: Some(str::to_string("asdf")), + }); + + // Check specifically for an invalid label + let result = metric.test_get_num_recorded_errors(ErrorType::InvalidLabel); + + assert_eq!(result, 0); +} + +#[test] +fn test_text_can_hold_long_string() { + let _lock = lock_test(); + + let _t = new_glean(None, false); + + let metric = TextMetric::new(CommonMetricData { + name: "text_metric".into(), + category: "test".into(), + send_in_pings: vec!["custom1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: Some(str::to_string("text")), + }); + + // 216 characters, which would overflow StringMetric + metric.set("I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain".into()); + + let result = metric.test_get_num_recorded_errors(ErrorType::InvalidValue); + assert_eq!(result, 0); + + let result = metric.test_get_num_recorded_errors(ErrorType::InvalidOverflow); + assert_eq!(result, 0); +} + +#[test] +fn signaling_done() { + let _lock = lock_test(); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + barrier: Arc<Barrier>, + counter: Arc<Mutex<HashMap<ThreadId, u32>>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + let mut map = self.counter.lock().unwrap(); + *map.entry(thread::current().id()).or_insert(0) += 1; + + // Wait for the sync. + self.barrier.wait(); + + // Signal that this uploader thread is done. + net::UploadResult::done() + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + // We use a barrier to sync this test thread with the uploader thread. + let barrier = Arc::new(Barrier::new(2)); + // We count how many times `upload` was invoked per thread. + let call_count = Arc::new(Mutex::default()); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { + barrier: Arc::clone(&barrier), + counter: Arc::clone(&call_count), + })), + use_core_mps: false, + trim_data_to_registered_pings: false, + log_level: None, + }; + + let _t = new_glean(Some(cfg), true); + + // Define a new ping and submit it. + const PING_NAME: &str = "test-ping"; + let custom_ping = private::PingType::new(PING_NAME, true, true, vec![]); + custom_ping.submit(None); + custom_ping.submit(None); + + // Sync up with the upload thread. + barrier.wait(); + + // Submit another ping and wait for it to do work. + custom_ping.submit(None); + + // Sync up with the upload thread again. + // This will not be the same thread as the one before (hopefully). + barrier.wait(); + + // No one's ever gonna wait for the uploader thread (the RLB doesn't store the handle to it), + // so all we can do is hope it finishes within time. + std::thread::sleep(std::time::Duration::from_millis(100)); + + let map = call_count.lock().unwrap(); + assert_eq!(2, map.len(), "should have launched 2 uploader threads"); + for &count in map.values() { + assert_eq!(1, count, "each thread should call upload only once"); + } +} |