diff options
Diffstat (limited to 'third_party/rust/glean')
26 files changed, 4264 insertions, 0 deletions
diff --git a/third_party/rust/glean/.cargo-checksum.json b/third_party/rust/glean/.cargo-checksum.json new file mode 100644 index 0000000000..8c46cce558 --- /dev/null +++ b/third_party/rust/glean/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"bb7825ea1f64c8c1d61c3dd696263f14335902b25c99fd5ec53d87c360e535e7","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"5627cc81e6187ab6c2b4dff061af16d559edcab64ba786bac39daa69c703c595","src/common_test.rs":"de47b53dcca37985c0a2b8c02daecbf32309aa54f5a4dd9290719c2c1fd0fa55","src/configuration.rs":"883b327fdad366e414ece83f65ab6b0216ab819c7854b382acf91b702b5a9697","src/core_metrics.rs":"dd17b482613894af08b51a2cff6dc1e84a6dbd853c14a55566e6698348941ced","src/lib.rs":"7cc249fc8f674958b91e6259225e858dfcd2b9b9dbdaecfab4d0ca85ad44129a","src/net/http_uploader.rs":"43812a70d19a38e8d7a093c8076c2b6345372c3c861b0f3511428762700a65e0","src/net/mod.rs":"e36e170a8e53530f8705988eea694ed7c55f50bb0ce403c0facbfb75ce03ac7f","src/private/event.rs":"02bbebf545695812e5055741cc0b5f3c99eda2039e684e26fcdd5f087ed15fe3","src/private/mod.rs":"eb8fe4e588bb32a54617324db39319920c627e6fc23c23cf4da5c17c63e0afed","src/private/ping.rs":"cbdc57f41fc9d46e56b4dfff91ac683753d1f8b3ecd0aa9bc3419e3595b8b81b","src/system.rs":"6eae5b41c15eba9cad6dbd116abe3519ee3e1fe034e79bdd692b029829a8c384","src/test.rs":"68e046309f943aacc45af9e8bb0687c5b49da32f3a55050a3724f0be0a91c61c","tests/common/mod.rs":"37cd4c48e140c793b852ae09fb3e812da28a4412977295015bcbffd632fcf294","tests/init_fails.rs":"28fd7726e76ca1295eb0905eca0b2ec65b0accfa28432c9ff90ec8f92616fc79","tests/never_init.rs":"1f33b8ce7ca3514b57b48cc16d98408974c85cf8aa7d13257ffc2ad878ebb295","tests/no_time_to_init.rs":"e7df75b47897fbf2c860a2e1c1c225b57598b8d1a39125ca897fe8d825bf0338","tests/overflowing_preinit.rs":"7ad4b2274dd9240b53430859a4eb1d2597cf508a5a678333f3d3abbadd2ed4a7","tests/persist_ping_lifetime.rs":"81415dc1d74743f02269f0d0dfa524003147056853f080276972e64a0b761d3c","tests/persist_ping_lifetime_nopanic.rs":"18379d3ffbf4a2c8c684c04ff7a0660b86dfbbb447db2d24dfed6073cb7ddf8f","tests/schema.rs":"1b7b19aec54a24c2bdd4738cf33c16802c19c83504c4d0e6bcfc19142877acdb","tests/simple.rs":"b099034b0599bdf4650e0fa09991a8413fc5fbf397755fc06c8963d4c7c8dfa6","tests/test-shutdown-blocking.sh":"9b16a01c190c7062474dd92182298a3d9a27928c8fa990340fdd798e6cdb7ab2","tests/upload_timing.rs":"d044fce7c783133e385671ea37d674e5a1b4120cae7b07708dcd825addfa0ee3"},"package":"cb5fc2dc8615ab49bfa879d64a02565b459881b72023ff39aca75e5581825695"}
\ No newline at end of file diff --git a/third_party/rust/glean/Cargo.toml b/third_party/rust/glean/Cargo.toml new file mode 100644 index 0000000000..3b8825f17f --- /dev/null +++ b/third_party/rust/glean/Cargo.toml @@ -0,0 +1,102 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.62" +name = "glean" +version = "52.7.0" +authors = [ + "Jan-Erik Rediger <jrediger@mozilla.com>", + "The Glean Team <glean-team@mozilla.com>", +] +include = [ + "/README.md", + "/LICENSE", + "/src", + "/tests", + "/Cargo.toml", +] +description = "Glean SDK Rust language bindings" +readme = "README.md" +keywords = [ + "telemetry", + "glean", +] +license = "MPL-2.0" +repository = "https://github.com/mozilla/glean" +resolver = "1" + +[dependencies.chrono] +version = "0.4.10" +features = ["serde"] + +[dependencies.crossbeam-channel] +version = "0.5" + +[dependencies.glean-core] +version = "52.7.0" + +[dependencies.inherent] +version = "1" + +[dependencies.log] +version = "0.4.8" + +[dependencies.once_cell] +version = "1.2.0" + +[dependencies.serde] +version = "1.0.104" +features = ["derive"] + +[dependencies.serde_json] +version = "1.0.44" + +[dependencies.thiserror] +version = "1.0.4" + +[dependencies.time] +version = "0.1.40" + +[dependencies.uuid] +version = "1.0" +features = ["v4"] + +[dependencies.whatsys] +version = "0.3.0" + +[dev-dependencies.env_logger] +version = "0.10.0" +features = [ + "auto-color", + "humantime", +] +default-features = false + +[dev-dependencies.flate2] +version = "1.0.19" + +[dev-dependencies.jsonschema-valid] +version = "0.5.0" + +[dev-dependencies.tempfile] +version = "3.1.0" + +[features] +preinit_million_queue = ["glean-core/preinit_million_queue"] + +[badges.circle-ci] +branch = "main" +repository = "mozilla/glean" + +[badges.maintenance] +status = "actively-developed" diff --git a/third_party/rust/glean/LICENSE b/third_party/rust/glean/LICENSE new file mode 100644 index 0000000000..a612ad9813 --- /dev/null +++ b/third_party/rust/glean/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + 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 http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/third_party/rust/glean/README.md b/third_party/rust/glean/README.md new file mode 100644 index 0000000000..4ec6bba914 --- /dev/null +++ b/third_party/rust/glean/README.md @@ -0,0 +1,38 @@ +# Glean + +The `Glean SDK` is a modern approach for a Telemetry library and is part of the [Glean project](https://docs.telemetry.mozilla.org/concepts/glean/glean.html). + +## `glean` + +This library provides a Rust language bindings on top of `glean-core`, targeted to Rust consumers. + +## Documentation + +All documentation is available online: + +* [The Glean SDK Book][book] +* [API documentation][apidocs] + +[book]: https://mozilla.github.io/glean/ +[apidocs]: https://mozilla.github.io/glean/docs/glean/index.html + +## Example + +```rust,no_run +use glean::{ConfigurationBuilder, Error, metrics::*}; + +let cfg = ConfigurationBuilder::new(true, "/tmp/data", "org.mozilla.glean_core.example").build(); +glean::initialize(cfg)?; + +let prototype_ping = PingType::new("prototype", true, true, vec![]); + +glean::register_ping_type(&prototype_ping); + +prototype_ping.submit(None); +``` + +## License + + 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 http://mozilla.org/MPL/2.0/ 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"); + } +} diff --git a/third_party/rust/glean/tests/common/mod.rs b/third_party/rust/glean/tests/common/mod.rs new file mode 100644 index 0000000000..cc02946d2c --- /dev/null +++ b/third_party/rust/glean/tests/common/mod.rs @@ -0,0 +1,51 @@ +// 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(dead_code)] is required on this module as a workaround for +// https://github.com/rust-lang/rust/issues/46379 +#![allow(dead_code)] + +use std::{panic, process}; + +use glean::{ClientInfoMetrics, Configuration}; + +/// Initialize the env logger for a test environment. +/// +/// When testing we want all logs to go to stdout/stderr by default. +pub fn enable_test_logging() { + let _ = env_logger::builder().is_test(true).try_init(); +} + +/// Install a panic handler that exits the whole process when a panic occurs. +/// +/// This causes the process to exit even if a thread panics. +/// This is similar to the `panic=abort` configuration, but works in the default configuration +/// (as used by `cargo test`). +fn install_panic_handler() { + let orig_hook = panic::take_hook(); + panic::set_hook(Box::new(move |panic_info| { + // invoke the default handler and exit the process + orig_hook(panic_info); + process::exit(1); + })); +} + +/// Create a new instance of Glean. +pub fn initialize(cfg: Configuration) { + // Ensure panics in threads, such as the init thread or the dispatcher, cause the process to + // exit. + // + // Otherwise in case of a panic in a thread the integration test will just hang. + // CI will terminate it after a timeout, but why stick around if we know nothing is happening? + install_panic_handler(); + + // Use some default values to make our life easier a bit. + let client_info = ClientInfoMetrics { + app_build: "1.0.0".to_string(), + app_display_version: "1.0.0".to_string(), + channel: Some("testing".to_string()), + }; + + glean::initialize(cfg, client_info); +} diff --git a/third_party/rust/glean/tests/init_fails.rs b/third_party/rust/glean/tests/init_fails.rs new file mode 100644 index 0000000000..2269da89ff --- /dev/null +++ b/third_party/rust/glean/tests/init_fails.rs @@ -0,0 +1,77 @@ +// 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/. + +//! This integration test should model how the RLB is used when embedded in another Rust application +//! (e.g. FOG/Firefox Desktop). +//! +//! We write a single test scenario per file to avoid any state keeping across runs +//! (different files run as different processes). + +mod common; + +use std::{thread, time::Duration}; + +use glean::ConfigurationBuilder; + +/// Some user metrics. +mod metrics { + use glean::private::*; + use glean::{Lifetime, TimeUnit}; + use glean_core::CommonMetricData; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static initialization: Lazy<TimespanMetric> = Lazy::new(|| { + TimespanMetric::new( + CommonMetricData { + name: "initialization".into(), + category: "sample".into(), + send_in_pings: vec!["validation".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }, + TimeUnit::Nanosecond, + ) + }); +} + +mod pings { + use glean::private::PingType; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static validation: Lazy<PingType> = + Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![])); +} + +/// Test scenario: Glean initialization fails. +/// +/// App tries to initialize Glean, but that somehow fails. +#[test] +fn init_fails() { + common::enable_test_logging(); + + metrics::initialization.start(); + + // Create a custom configuration to use a validating uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, "") + .with_server_endpoint("invalid-test-host") + .build(); + common::initialize(cfg); + + metrics::initialization.stop(); + + pings::validation.submit(None); + + // We don't test for data here, as that would block on the dispatcher. + + // Give it a short amount of time to actually finish initialization. + thread::sleep(Duration::from_millis(500)); + + glean::shutdown(); +} diff --git a/third_party/rust/glean/tests/never_init.rs b/third_party/rust/glean/tests/never_init.rs new file mode 100644 index 0000000000..321662b327 --- /dev/null +++ b/third_party/rust/glean/tests/never_init.rs @@ -0,0 +1,66 @@ +// 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/. + +//! This integration test should model how the RLB is used when embedded in another Rust application +//! (e.g. FOG/Firefox Desktop). +//! +//! We write a single test scenario per file to avoid any state keeping across runs +//! (different files run as different processes). + +mod common; + +/// Some user metrics. +mod metrics { + use glean::private::*; + use glean::{Lifetime, TimeUnit}; + use glean_core::CommonMetricData; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static initialization: Lazy<TimespanMetric> = Lazy::new(|| { + TimespanMetric::new( + CommonMetricData { + name: "initialization".into(), + category: "sample".into(), + send_in_pings: vec!["validation".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }, + TimeUnit::Nanosecond, + ) + }); +} + +mod pings { + use glean::private::PingType; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static validation: Lazy<PingType> = + Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![])); +} + +/// Test scenario: Glean is never initialized. +/// +/// Glean is never initialized. +/// Some data is recorded early on. +/// And later the whole process is shutdown. +#[test] +fn never_initialize() { + common::enable_test_logging(); + + metrics::initialization.start(); + + // NOT calling `initialize` here. + // In apps this might happen for several reasons: + // 1. Process doesn't run long enough for Glean to be initialized. + // 2. Getting some early data used for initialize fails + + pings::validation.submit(None); + + // We can't test for data either, as that would panic because init was never called. + + glean::shutdown(); +} diff --git a/third_party/rust/glean/tests/no_time_to_init.rs b/third_party/rust/glean/tests/no_time_to_init.rs new file mode 100644 index 0000000000..7d51e514d6 --- /dev/null +++ b/third_party/rust/glean/tests/no_time_to_init.rs @@ -0,0 +1,74 @@ +// 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/. + +//! This integration test should model how the RLB is used when embedded in another Rust application +//! (e.g. FOG/Firefox Desktop). +//! +//! We write a single test scenario per file to avoid any state keeping across runs +//! (different files run as different processes). + +mod common; + +use glean::ConfigurationBuilder; + +/// Some user metrics. +mod metrics { + use glean::private::*; + use glean::{Lifetime, TimeUnit}; + use glean_core::CommonMetricData; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static initialization: Lazy<TimespanMetric> = Lazy::new(|| { + TimespanMetric::new( + CommonMetricData { + name: "initialization".into(), + category: "sample".into(), + send_in_pings: vec!["validation".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }, + TimeUnit::Nanosecond, + ) + }); +} + +mod pings { + use glean::private::PingType; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static validation: Lazy<PingType> = + Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![])); +} + +/// Test scenario: Glean initialization fails. +/// +/// The app tries to initializate Glean, but that somehow fails. +#[test] +fn init_fails() { + common::enable_test_logging(); + + metrics::initialization.start(); + + // Create a custom configuration to use a validating uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, "firefox-desktop") + .with_server_endpoint("invalid-test-host") + .build(); + common::initialize(cfg); + + metrics::initialization.stop(); + + pings::validation.submit(None); + + // We don't test for data here, as that would block on the dispatcher. + + // Shut it down immediately; this might not be enough time to initialize. + + glean::shutdown(); +} diff --git a/third_party/rust/glean/tests/overflowing_preinit.rs b/third_party/rust/glean/tests/overflowing_preinit.rs new file mode 100644 index 0000000000..6d4ec7f6ae --- /dev/null +++ b/third_party/rust/glean/tests/overflowing_preinit.rs @@ -0,0 +1,88 @@ +// 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/. + +//! This integration test should model how the RLB is used when embedded in another Rust application +//! (e.g. FOG/Firefox Desktop). +//! +//! We write a single test scenario per file to avoid any state keeping across runs +//! (different files run as different processes). + +mod common; + +use glean::ConfigurationBuilder; + +/// Some user metrics. +mod metrics { + use glean::private::*; + use glean::Lifetime; + use glean_core::CommonMetricData; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static rapid_counting: Lazy<CounterMetric> = Lazy::new(|| { + CounterMetric::new(CommonMetricData { + name: "rapid_counting".into(), + category: "sample".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }) + }); + + // This is a hack, but a good one: + // + // To avoid reaching into RLB internals we re-create the metric so we can look at it. + #[allow(non_upper_case_globals)] + pub static preinit_tasks_overflow: Lazy<CounterMetric> = Lazy::new(|| { + CounterMetric::new(CommonMetricData { + category: "glean.error".into(), + name: "preinit_tasks_overflow".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }) + }); +} + +/// Test scenario: Lots of metric recordings before init. +/// +/// The app starts recording metrics before Glean is initialized. +/// Once initialized the recordings are processed and data is persisted. +/// The pre-init dispatcher queue records how many recordings over the limit it saw. +/// +/// This is an integration test to avoid dealing with resetting the dispatcher. +#[test] +fn overflowing_the_task_queue_records_telemetry() { + common::enable_test_logging(); + + // Create a custom configuration to use a validating uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, "firefox-desktop") + .with_server_endpoint("invalid-test-host") + .build(); + + // Insert a bunch of tasks to overflow the queue. + for _ in 0..1010 { + metrics::rapid_counting.add(1); + } + + // Now initialize Glean + common::initialize(cfg); + + assert_eq!(Some(1000), metrics::rapid_counting.test_get_value(None)); + + // The metrics counts the total number of overflowing tasks, + // (and the count of tasks in the queue when we overflowed: bug 1764573) + // this might include Glean-internal tasks. + let val = metrics::preinit_tasks_overflow + .test_get_value(None) + .unwrap(); + assert!(val >= 10); + + glean::shutdown(); +} diff --git a/third_party/rust/glean/tests/persist_ping_lifetime.rs b/third_party/rust/glean/tests/persist_ping_lifetime.rs new file mode 100644 index 0000000000..f73673f46f --- /dev/null +++ b/third_party/rust/glean/tests/persist_ping_lifetime.rs @@ -0,0 +1,89 @@ +// 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/. + +//! This integration test should model how the RLB is used when embedded in another Rust application +//! (e.g. FOG/Firefox Desktop). +//! +//! We write a single test scenario per file to avoid any state keeping across runs +//! (different files run as different processes). + +mod common; + +use glean::{ClientInfoMetrics, Configuration, ConfigurationBuilder}; +use std::path::PathBuf; + +/// Some user metrics. +mod metrics { + use glean::private::*; + use glean::Lifetime; + use glean_core::CommonMetricData; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static boo: Lazy<BooleanMetric> = Lazy::new(|| { + BooleanMetric::new(CommonMetricData { + name: "boo".into(), + category: "sample".into(), + send_in_pings: vec!["validation".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }) + }); +} + +fn cfg_new(tmpname: PathBuf) -> Configuration { + ConfigurationBuilder::new(true, tmpname, "firefox-desktop") + .with_server_endpoint("invalid-test-host") + .with_delay_ping_lifetime_io(true) + .build() +} + +/// Test scenario: Are ping-lifetime data persisted on shutdown when delayed? +/// +/// delay_ping_lifetime_io: true has Glean put "ping"-lifetime data in-memory +/// instead of the db. Ensure that, on orderly shutdowns, we correctly persist +/// these in-memory data to the db. +#[test] +fn delayed_ping_data() { + common::enable_test_logging(); + + metrics::boo.set(true); + + // Create a custom configuration to delay ping-lifetime io + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + common::initialize(cfg_new(tmpname.clone())); + + assert!( + metrics::boo.test_get_value(None).unwrap(), + "Data should be present. Doesn't mean it's persisted, though." + ); + + glean::test_reset_glean( + cfg_new(tmpname.clone()), + ClientInfoMetrics::unknown(), + false, + ); + + assert_eq!( + None, + metrics::boo.test_get_value(None), + "Data should not have made it to disk on unclean shutdown." + ); + metrics::boo.set(true); // Let's try again + + // This time, let's shut down cleanly + glean::shutdown(); + + // Now when we init, we should get the persisted data + glean::test_reset_glean(cfg_new(tmpname), ClientInfoMetrics::unknown(), false); + assert!( + metrics::boo.test_get_value(None).unwrap(), + "Data must be persisted between clean shutdown and init!" + ); + + glean::shutdown(); // Cleanly shut down at the end of the test. +} diff --git a/third_party/rust/glean/tests/persist_ping_lifetime_nopanic.rs b/third_party/rust/glean/tests/persist_ping_lifetime_nopanic.rs new file mode 100644 index 0000000000..18d54e9033 --- /dev/null +++ b/third_party/rust/glean/tests/persist_ping_lifetime_nopanic.rs @@ -0,0 +1,37 @@ +// 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/. + +//! This integration test should model how the RLB is used when embedded in another Rust application +//! (e.g. FOG/Firefox Desktop). +//! +//! We write a single test scenario per file to avoid any state keeping across runs +//! (different files run as different processes). + +mod common; + +use glean::{Configuration, ConfigurationBuilder}; +use std::path::PathBuf; + +fn cfg_new(tmpname: PathBuf) -> Configuration { + ConfigurationBuilder::new(true, tmpname, "firefox-desktop") + .with_server_endpoint("invalid-test-host") + .with_delay_ping_lifetime_io(true) + .build() +} + +/// Test scenario: `persist_ping_lifetime_data` called after shutdown. +#[test] +fn delayed_ping_data() { + common::enable_test_logging(); + + // Create a custom configuration to delay ping-lifetime io + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + common::initialize(cfg_new(tmpname)); + glean::persist_ping_lifetime_data(); + + glean::shutdown(); + glean::persist_ping_lifetime_data(); +} diff --git a/third_party/rust/glean/tests/schema.rs b/third_party/rust/glean/tests/schema.rs new file mode 100644 index 0000000000..bdcfb84185 --- /dev/null +++ b/third_party/rust/glean/tests/schema.rs @@ -0,0 +1,211 @@ +// 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::collections::HashMap; +use std::io::Read; + +use flate2::read::GzDecoder; +use glean_core::TextMetric; +use jsonschema_valid::{self, schemas::Draft}; +use serde_json::Value; + +use glean::net::UploadResult; +use glean::private::*; +use glean::{ + traits, ClientInfoMetrics, CommonMetricData, ConfigurationBuilder, HistogramType, MemoryUnit, + TimeUnit, +}; + +const SCHEMA_JSON: &str = include_str!("../../../glean.1.schema.json"); + +fn load_schema() -> Value { + serde_json::from_str(SCHEMA_JSON).unwrap() +} + +const GLOBAL_APPLICATION_ID: &str = "org.mozilla.glean.test.app"; + +struct SomeExtras { + extra1: Option<String>, + extra2: Option<bool>, +} + +impl traits::ExtraKeys for SomeExtras { + const ALLOWED_KEYS: &'static [&'static str] = &["extra1", "extra2"]; + + fn into_ffi_extra(self) -> HashMap<String, String> { + let mut map = HashMap::new(); + + self.extra1 + .and_then(|val| map.insert("extra1".to_string(), val)); + self.extra2 + .and_then(|val| map.insert("extra2".to_string(), val.to_string())); + + map + } +} + +#[test] +fn validate_against_schema() { + let _ = env_logger::builder().try_init(); + + let schema = load_schema(); + + let (s, r) = crossbeam_channel::bounded::<Vec<u8>>(1); + + // Define a fake uploader that reports back the submitted payload + // using a crossbeam channel. + #[derive(Debug)] + pub struct ValidatingUploader { + sender: crossbeam_channel::Sender<Vec<u8>>, + } + impl glean::net::PingUploader for ValidatingUploader { + fn upload( + &self, + _url: String, + body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> UploadResult { + self.sender.send(body).unwrap(); + UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a validating uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(ValidatingUploader { sender: s }) + .build(); + + let client_info = ClientInfoMetrics { + app_build: env!("CARGO_PKG_VERSION").to_string(), + app_display_version: env!("CARGO_PKG_VERSION").to_string(), + channel: Some("testing".to_string()), + }; + + glean::initialize(cfg, client_info); + + const PING_NAME: &str = "test-ping"; + + let common = |name: &str| CommonMetricData { + category: "test".into(), + name: name.into(), + send_in_pings: vec![PING_NAME.into()], + ..Default::default() + }; + + // Test each of the metric types, just for basic smoke testing against the + // schema + + // TODO: 1695762 Test all of the metric types against the schema from Rust + + let counter_metric = CounterMetric::new(common("counter")); + counter_metric.add(42); + + let bool_metric = BooleanMetric::new(common("bool")); + bool_metric.set(true); + + let string_metric = StringMetric::new(common("string")); + string_metric.set("test".into()); + + let stringlist_metric = StringListMetric::new(common("stringlist")); + stringlist_metric.add("one".into()); + stringlist_metric.add("two".into()); + + // Let's make sure an empty array is accepted. + let stringlist_metric2 = StringListMetric::new(common("stringlist2")); + stringlist_metric2.set(vec![]); + + let timespan_metric = TimespanMetric::new(common("timespan"), TimeUnit::Nanosecond); + timespan_metric.start(); + timespan_metric.stop(); + + let timing_dist = TimingDistributionMetric::new(common("timing_dist"), TimeUnit::Nanosecond); + let id = timing_dist.start(); + timing_dist.stop_and_accumulate(id); + + let memory_dist = MemoryDistributionMetric::new(common("memory_dist"), MemoryUnit::Byte); + memory_dist.accumulate(100); + + let uuid_metric = UuidMetric::new(common("uuid")); + // chosen by fair dic roll (`uuidgen`) + uuid_metric.set("3ee4db5f-ee26-4557-9a66-bc7425d7893f".into()); + + // We can't test the URL metric, + // because the regex used in the schema uses a negative lookahead, + // which the regex crate doesn't handle. + // + //let url_metric = UrlMetric::new(common("url")); + //url_metric.set("https://mozilla.github.io/glean/"); + + let datetime_metric = DatetimeMetric::new(common("datetime"), TimeUnit::Day); + datetime_metric.set(None); + + let event_metric = EventMetric::<SomeExtras>::new(common("event")); + event_metric.record(None); + event_metric.record(SomeExtras { + extra1: Some("test".into()), + extra2: Some(false), + }); + + let custom_dist = + CustomDistributionMetric::new(common("custom_dist"), 1, 100, 100, HistogramType::Linear); + custom_dist.accumulate_samples(vec![50, 51]); + + let quantity_metric = QuantityMetric::new(common("quantity")); + quantity_metric.set(0); + + let rate_metric = RateMetric::new(common("rate")); + rate_metric.add_to_numerator(1); + rate_metric.add_to_denominator(1); + + let numerator_metric1 = NumeratorMetric::new(common("num1")); + let numerator_metric2 = NumeratorMetric::new(common("num2")); + let denominator_metric = + DenominatorMetric::new(common("den"), vec![common("num1"), common("num2")]); + + numerator_metric1.add_to_numerator(1); + numerator_metric2.add_to_numerator(2); + denominator_metric.add(3); + + let text_metric = TextMetric::new(common("text")); + text_metric.set("loooooong text".repeat(100)); + + // Define a new ping and submit it. + let custom_ping = glean::private::PingType::new(PING_NAME, true, true, vec![]); + custom_ping.submit(None); + + // Wait for the ping to arrive. + let raw_body = r.recv().unwrap(); + + // Decode the gzipped body. + let mut gzip_decoder = GzDecoder::new(&raw_body[..]); + let mut s = String::with_capacity(raw_body.len()); + + let data = gzip_decoder + .read_to_string(&mut s) + .ok() + .map(|_| &s[..]) + .or_else(|| std::str::from_utf8(&raw_body).ok()) + .and_then(|payload| serde_json::from_str(payload).ok()) + .unwrap(); + + // Now validate against the vendored schema + let cfg = jsonschema_valid::Config::from_schema(&schema, Some(Draft::Draft6)).unwrap(); + let validation = cfg.validate(&data); + match validation { + Ok(()) => {} + Err(errors) => { + let mut msg = format!("Data: {data:#?}\n Errors:\n"); + for (idx, error) in errors.enumerate() { + msg.push_str(&format!("Error {}: ", idx + 1)); + msg.push_str(&error.to_string()); + msg.push('\n'); + } + panic!("{}", msg); + } + } +} diff --git a/third_party/rust/glean/tests/simple.rs b/third_party/rust/glean/tests/simple.rs new file mode 100644 index 0000000000..efc8d9a0f8 --- /dev/null +++ b/third_party/rust/glean/tests/simple.rs @@ -0,0 +1,77 @@ +// 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/. + +//! This integration test should model how the RLB is used when embedded in another Rust application +//! (e.g. FOG/Firefox Desktop). +//! +//! We write a single test scenario per file to avoid any state keeping across runs +//! (different files run as different processes). + +mod common; + +use glean::ConfigurationBuilder; + +/// Some user metrics. +mod metrics { + use glean::private::*; + use glean::{Lifetime, TimeUnit}; + use glean_core::CommonMetricData; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static initialization: Lazy<TimespanMetric> = Lazy::new(|| { + TimespanMetric::new( + CommonMetricData { + name: "initialization".into(), + category: "sample".into(), + send_in_pings: vec!["validation".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }, + TimeUnit::Nanosecond, + ) + }); +} + +mod pings { + use glean::private::PingType; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static validation: Lazy<PingType> = + Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![])); +} + +/// Test scenario: A clean run +/// +/// The app is initialized, in turn Glean gets initialized without problems. +/// Some data is recorded (before and after initialization). +/// And later the whole process is shutdown. +#[test] +fn simple_lifecycle() { + common::enable_test_logging(); + + metrics::initialization.start(); + + // Create a custom configuration to use a validating uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, "firefox-desktop") + .with_server_endpoint("invalid-test-host") + .build(); + common::initialize(cfg); + + metrics::initialization.stop(); + + // This would never be called outside of tests, + // but it's the only way we can really test it's working right now. + assert!(metrics::initialization.test_get_value(None).is_some()); + + pings::validation.submit(None); + assert!(metrics::initialization.test_get_value(None).is_none()); + + glean::shutdown(); +} diff --git a/third_party/rust/glean/tests/test-shutdown-blocking.sh b/third_party/rust/glean/tests/test-shutdown-blocking.sh new file mode 100755 index 0000000000..2f5d82acf0 --- /dev/null +++ b/third_party/rust/glean/tests/test-shutdown-blocking.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Test harness for testing the RLB processes from the outside. +# +# Some behavior can only be observed when properly exiting the process running Glean, +# e.g. when an uploader runs in another thread. +# On exit the threads will be killed, regardless of their state. + +# Remove the temporary data path on all exit conditions +cleanup() { + if [ -n "$datapath" ]; then + rm -r "$datapath" + fi +} +trap cleanup INT ABRT TERM EXIT + +tmp="${TMPDIR:-/tmp}" +datapath=$(mktemp -d "${tmp}/glean_long_running.XXXX") + +cargo run --example long-running -- "$datapath" +count=$(ls -1q "$datapath/pending_pings" | wc -l) + +if [[ "$count" -eq 0 ]]; then + echo "test result: ok." + exit 0 +else + echo "test result: FAILED." + exit 101 +fi diff --git a/third_party/rust/glean/tests/upload_timing.rs b/third_party/rust/glean/tests/upload_timing.rs new file mode 100644 index 0000000000..1dd073bebb --- /dev/null +++ b/third_party/rust/glean/tests/upload_timing.rs @@ -0,0 +1,225 @@ +// 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/. + +//! This integration test should model how the RLB is used when embedded in another Rust application +//! (e.g. FOG/Firefox Desktop). +//! +//! We write a single test scenario per file to avoid any state keeping across runs +//! (different files run as different processes). + +mod common; + +use std::io::Read; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::thread; +use std::time; + +use crossbeam_channel::{bounded, Sender}; +use flate2::read::GzDecoder; +use serde_json::Value as JsonValue; + +use glean::net; +use glean::ConfigurationBuilder; + +pub mod metrics { + #![allow(non_upper_case_globals)] + + use glean::{ + private::BooleanMetric, private::TimingDistributionMetric, CommonMetricData, Lifetime, + TimeUnit, + }; + + pub static sample_boolean: once_cell::sync::Lazy<BooleanMetric> = + once_cell::sync::Lazy::new(|| { + BooleanMetric::new(CommonMetricData { + name: "sample_boolean".into(), + category: "test.metrics".into(), + send_in_pings: vec!["validation".into()], + disabled: false, + lifetime: Lifetime::Ping, + ..Default::default() + }) + }); + + // The following are duplicated from `glean-core/src/internal_metrics.rs` + // so we can use the test APIs to query them. + + pub static send_success: once_cell::sync::Lazy<TimingDistributionMetric> = + once_cell::sync::Lazy::new(|| { + TimingDistributionMetric::new( + CommonMetricData { + name: "send_success".into(), + category: "glean.upload".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Ping, + disabled: false, + dynamic_label: None, + }, + TimeUnit::Millisecond, + ) + }); + + pub static send_failure: once_cell::sync::Lazy<TimingDistributionMetric> = + once_cell::sync::Lazy::new(|| { + TimingDistributionMetric::new( + CommonMetricData { + name: "send_failure".into(), + category: "glean.upload".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Ping, + disabled: false, + dynamic_label: None, + }, + TimeUnit::Millisecond, + ) + }); + + pub static shutdown_wait: once_cell::sync::Lazy<TimingDistributionMetric> = + once_cell::sync::Lazy::new(|| { + TimingDistributionMetric::new( + CommonMetricData { + name: "shutdown_wait".into(), + category: "glean.validation".into(), + send_in_pings: vec!["metrics".into()], + lifetime: Lifetime::Ping, + disabled: false, + dynamic_label: None, + }, + TimeUnit::Millisecond, + ) + }); +} + +mod pings { + use glean::private::PingType; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static validation: Lazy<PingType> = + Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![])); +} + +// Define a fake uploader that sleeps. +#[derive(Debug)] +struct FakeUploader { + calls: AtomicUsize, + sender: Sender<JsonValue>, +} + +impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + let calls = self.calls.fetch_add(1, Ordering::SeqCst); + let decode = |body: Vec<u8>| { + let mut gzip_decoder = GzDecoder::new(&body[..]); + let mut s = String::with_capacity(body.len()); + + 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() + }; + + match calls { + // First goes through as is. + 0 => net::UploadResult::http_status(200), + // Second briefly sleeps + 1 => { + thread::sleep(time::Duration::from_millis(100)); + net::UploadResult::http_status(200) + } + // Third one fails + 2 => net::UploadResult::http_status(404), + // Fourth one fast again + 3 => { + self.sender.send(decode(body)).unwrap(); + net::UploadResult::http_status(200) + } + // Last one is the metrics ping, a-ok. + _ => { + self.sender.send(decode(body)).unwrap(); + net::UploadResult::http_status(200) + } + } + } +} + +/// Test scenario: Different timings for upload on success and failure. +/// +/// The app is initialized, in turn Glean gets initialized without problems. +/// A custom ping is submitted multiple times to trigger upload. +/// A metrics ping is submitted to get the upload timing data. +/// +/// And later the whole process is shutdown. +#[test] +fn upload_timings() { + common::enable_test_logging(); + + // Create a custom configuration to use a validating uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + let (tx, rx) = bounded(1); + + let cfg = ConfigurationBuilder::new(true, tmpname.clone(), "glean-upload-timing") + .with_server_endpoint("invalid-test-host") + .with_use_core_mps(false) + .with_uploader(FakeUploader { + calls: AtomicUsize::new(0), + sender: tx, + }) + .build(); + common::initialize(cfg); + + // Wait for init to finish, + // otherwise we might be to quick with calling `shutdown`. + let _ = metrics::sample_boolean.test_get_value(None); + + // fast + pings::validation.submit(None); + // slow + pings::validation.submit(None); + // failed + pings::validation.submit(None); + // fast + pings::validation.submit(None); + + // wait for the last ping + let _body = rx.recv().unwrap(); + + assert_eq!( + 3, + metrics::send_success.test_get_value(None).unwrap().count, + "Successful pings: two fast, one slow" + ); + assert_eq!( + 1, + metrics::send_failure.test_get_value(None).unwrap().count, + "One failed ping" + ); + + // This is awkward, but it's what gets us very close to just starting a new process with a + // fresh Glean. + // This also calls `glean::shutdown();` internally, waiting on the uploader. + let data_path = Some(tmpname.display().to_string()); + glean_core::glean_test_destroy_glean(false, data_path); + + let cfg = ConfigurationBuilder::new(true, tmpname, "glean-upload-timing") + .with_server_endpoint("invalid-test-host") + .with_use_core_mps(false) + .build(); + common::initialize(cfg); + + assert_eq!( + 1, + metrics::shutdown_wait.test_get_value(None).unwrap().count, + "Measured time waiting for shutdown exactly once" + ); +} |