diff --git a/.buildconfig.yml b/.buildconfig.yml index 88ddd21704..64ebbb1202 100644 --- a/.buildconfig.yml +++ b/.buildconfig.yml @@ -1,4 +1,4 @@ -libraryVersion: 35.0.0 +libraryVersion: 36.0.0 groupId: org.mozilla.telemetry projects: glean: diff --git a/.circleci/config.yml b/.circleci/config.yml index fc022be2a0..19365b5c45 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -60,12 +60,19 @@ commands: rust-version: <> - run: name: Test - command: cargo test --all --verbose -- --nocapture + command: GLEAN_TEST_COVERAGE=$(realpath glean_coverage.txt) cargo test --all --verbose -- --nocapture - run: name: Test Glean with rkv safe-mode command: | cd glean-core cargo test -p glean-core --features rkv-safe-mode -- --nocapture + - run: + name: Upload coverage report + command: | + sudo apt install python3-pip + pip3 install glean_parser + glean_parser coverage --allow-reserved -c glean_coverage.txt -f codecovio -o codecov.json glean-core/metrics.yaml + bash <(curl -s https://codecov.io/bash) -X yaml -f codecov.json install-rustup: steps: @@ -130,17 +137,13 @@ commands: name: Install Python development tools for host command: make python-setup - - run: - name: Build Windows glean_ffi.dll - command: - cargo build --target x86_64-pc-windows-gnu --release - run: name: Build Windows wheel command: | cd glean-core/python .venv3.8/bin/python3 setup.py bdist_wheel environment: - GLEAN_PYTHON_MINGW_X86_64_BUILD: 1 + GLEAN_BUILD_TARGET: x86_64-pc-windows-gnu GLEAN_BUILD_VARIANT: release build-windows-i686-wheel: @@ -153,17 +156,13 @@ commands: name: Install Python development tools for host command: make python-setup - - run: - name: Build Windows glean_ffi.dll - command: - RUSTFLAGS="-C panic=abort" cargo build --target i686-pc-windows-gnu --release - run: name: Build Windows wheel command: | cd glean-core/python .venv3.8/bin/python3 setup.py bdist_wheel environment: - GLEAN_PYTHON_MINGW_I686_BUILD: 1 + GLEAN_BUILD_TARGET: i686-pc-windows-gnu GLEAN_BUILD_VARIANT: release install-python-windows-deps: @@ -276,7 +275,7 @@ jobs: steps: - checkout - run: sudo pip install yamllint - - run: make yamllint + - run: make lint-yaml ########################################################################### # Rust / C / FFI @@ -556,7 +555,6 @@ jobs: name: Setup build environment command: | rustup target add aarch64-apple-ios x86_64-apple-ios - cargo install cargo-lipo # Bootstrap dependencies bin/bootstrap.sh @@ -644,12 +642,19 @@ jobs: name: Setup build environment command: | rustup target add aarch64-apple-ios x86_64-apple-ios - cargo install cargo-lipo # List available devices -- allows us to see what's there xcrun instruments -w list || true # See https://circleci.com/docs/2.0/testing-ios/#pre-starting-the-simulator xcrun instruments -w "iPhone 11 (14" || true + - run: + name: Install Rust Nightly toolchain + command: | + # Need a nightly toolchain and the source code for targetting the arm64 iOS simulator + # We don't need that build on CI, but its hard to exclude it from the build + # while still allowing it in developer builds. + rustup toolchain add nightly --profile minimal + rustup component add rust-src --toolchain nightly - run: name: Use current commit of Glean command: | @@ -719,7 +724,7 @@ jobs: - checkout - run: name: Python lints - command: make pythonlint + command: make lint-python Python 3_6 tests: docker: @@ -761,26 +766,26 @@ jobs: root: glean-core/python/ paths: .venv3.8 - Python 3_8 tests minimum dependencies: + Python 3_9 tests: docker: - - image: circleci/python:3.8.5 + - image: circleci/python:3.9.0 steps: - checkout - - skip-if-doc-only - - run: - command: | - echo "export GLEAN_PYDEPS=min" >> $BASH_ENV - test-python + - persist_to_workspace: + root: glean-core/python/ + paths: .venv3.9 - Python 3_9 tests: + Python 3_9 tests minimum dependencies: docker: - image: circleci/python:3.9.0 steps: - checkout + - skip-if-doc-only + - run: + command: | + echo "export GLEAN_PYDEPS=min" >> $BASH_ENV - test-python - - persist_to_workspace: - root: glean-core/python/ - paths: .venv3.9 Python 3_9 on Alpine tests: docker: @@ -929,17 +934,50 @@ jobs: name: Build and Test Python extension command: | make test-python + environment: + # setup.py would set it automatically, but we're building separately. + MACOSX_DEPLOYMENT_TARGET: 10.7 + GLEAN_BUILD_VARIANT: release + - run: + name: Build macOS wheel + command: | + cd glean-core/python + .venv3.9/bin/python3 setup.py bdist_wheel + # Requires that the TWINE_USERNAME and TWINE_PASSWORD environment + # variables are configured in CircleCI's environment variables. + .venv3.9/bin/python3 -m twine upload dist/* environment: GLEAN_BUILD_VARIANT: release + - install-ghr-darwin + - run: + name: Publish to Github + command: | + # Upload to GitHub + ./ghr -replace ${CIRCLE_TAG} glean-core/python/dist + + pypi-macos-arm64-release: + macos: + xcode: "12.3.0" + steps: + - install-rustup + - setup-rust-toolchain + - checkout + - run: + name: Install Python development tools for host + command: + make python-setup - run: name: Build macOS wheel command: | + rustup target add aarch64-apple-darwin cd glean-core/python .venv3.9/bin/python3 setup.py bdist_wheel # Requires that the TWINE_USERNAME and TWINE_PASSWORD environment # variables are configured in CircleCI's environment variables. .venv3.9/bin/python3 -m twine upload dist/* environment: + SDKROOT: /Library/Developer/CommandLineTools/SDKs/MacOSX11.1.sdk + GLEAN_BUILD_TARGET: aarch64-apple-darwin GLEAN_BUILD_VARIANT: release - install-ghr-darwin - run: @@ -1023,14 +1061,7 @@ jobs: - run: name: Check internal documentation links command: | - link-checker \ - build/docs/book \ - --disable-external true \ - --allow-hash-href true \ - --url-ignore ".*/swift/.*" \ - --url-ignore ".*/python/.*" \ - --url-ignore ".*/javadoc/.*" \ - --url-ignore ".*/docs/glean_.*" + make linkcheck-raw docs-spellcheck: docker: @@ -1117,8 +1148,8 @@ workflows: - Python 3_6 tests minimum dependencies - Python 3_7 tests - Python 3_8 tests - - Python 3_8 tests minimum dependencies - Python 3_9 tests + - Python 3_9 tests minimum dependencies - Python 3_9 on Alpine tests - Python Windows x86_64 tests - Python Windows i686 tests @@ -1181,6 +1212,10 @@ workflows: requires: - Python 3_8 tests filters: *release-filters + - pypi-macos-arm64-release: + requires: + - Python 3_8 tests + filters: *release-filters - pypi-windows-i686-release: requires: - Python 3_8 tests diff --git a/.dictionary b/.dictionary index 95593aa448..9eedaa12cf 100644 --- a/.dictionary +++ b/.dictionary @@ -1,7 +1,8 @@ -personal_ws-1.1 en 211 utf-8 +personal_ws-1.1 en 215 utf-8 AAR AARs ABI +API's APIs APK BUGFIX @@ -35,6 +36,7 @@ JNI JSON JUnit JWE +Javascript JetBrains Karlton Kotlin @@ -188,10 +190,12 @@ serializer setuptools stateful struct +subcommand subprocess subprocesses swiftlint tcsh +templating timespan timespan's timespans diff --git a/CHANGELOG.md b/CHANGELOG.md index d615aa2eb4..1e2e29908c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ # Unreleased changes -[Full changelog](https://github.com/mozilla/glean/compare/v35.0.0...main) +[Full changelog](https://github.com/mozilla/glean/compare/v36.0.0...main) + +# v36.0.0 (2021-03-16) + +[Full changelog](https://github.com/mozilla/glean/compare/v35.0.0...v36.0.0) + +* General + * Introduce a new API `Ping#test_before_next_submit` to run a callback right before a custom ping is submitted ([#1507](https://github.com/mozilla/glean/pull/1507)). + * The new API exists for all language bindings (Kotlin, Swift, Rust, Python). + * Updated `glean_parser` version to 2.5.0 + * Change the `fmt-` and `lint-` make commands for consistency ([#1526](https://github.com/mozilla/glean/pull/1526)) + * The Glean SDK can now produce testing coverage reports for your metrics ([#1482](https://github.com/mozilla/glean/pull/1482/files)). +* Python + * Update minimal required version of `cffi` dependency to 1.13.0 ([#1520](https://github.com/mozilla/glean/pull/1520)). + * Ship wheels for arm64 macOS ([#1534](https://github.com/mozilla/glean/pull/1534)). +* RLB + * Added `rate` metric type ([#1516](https://github.com/mozilla/glean/pull/1516)). + * Set `internal_metrics::os_version` for MacOS, Windows and Linux ([#1538](https://github.com/mozilla/glean/pull/1538)) + * Expose a function `get_timestamp_ms` to get a timestamp from a monotonic clock on all supported operating systems, to be used for event timestamps ([#1546](https://github.com/mozilla/glean/pull/1546)). + * Expose a function to record events with an externally provided timestamp. +* iOS + * **Breaking Change**: Event timestamps are now correctly recorded in milliseconds ([#1546](https://github.com/mozilla/glean/pull/1546)). + * Since the first release event timestamps were erroneously recorded with nanosecond precision ([#1549](https://github.com/mozilla/glean/pull/1549)). + This is now fixed and event timestamps are in milliseconds. + This is equivalent to how it works in all other language bindings. # v35.0.0 (2021-02-22) @@ -11,8 +35,8 @@ * The `testGetValue` APIs now include a message on the `NullPointerException` thrown when the value is missing. * **Breaking change:** `LEGACY_TAG_PINGS` is removed from `GleanDebugActivity` ([#1510](https://github.com/mozilla/glean/pull/1510)) * RLB - * **Breaking change:** `Configuration.data_path` is now a `std::path::PathBuf`([#1493](https://github.com/mozilla/glean/pull/1493)). - + * **Breaking change:** `Configuration.data_path` is now a `std::path::PathBuf`([#1493](https://github.com/mozilla/glean/pull/1493)). + # v34.1.0 (2021-02-04) [Full changelog](https://github.com/mozilla/glean/compare/v34.0.0...v34.1.0) diff --git a/Cargo.lock b/Cargo.lock index e010a1a259..aca82c480a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,9 +80,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "cc" -version = "1.0.62" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1770ced377336a88a67c473594ccc14eca6f4559217c34f64aac8f83d641b40" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfg-if" @@ -251,7 +251,7 @@ dependencies = [ [[package]] name = "glean" -version = "35.0.0" +version = "36.0.0" dependencies = [ "chrono", "crossbeam-channel", @@ -268,11 +268,12 @@ dependencies = [ "thiserror", "time", "uuid", + "whatsys", ] [[package]] name = "glean-core" -version = "35.0.0" +version = "36.0.0" dependencies = [ "bincode", "chrono", @@ -288,11 +289,12 @@ dependencies = [ "serde_json", "tempfile", "uuid", + "zeitstempel", ] [[package]] name = "glean-ffi" -version = "35.0.0" +version = "36.0.0" dependencies = [ "android_logger", "env_logger", @@ -422,9 +424,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" +checksum = "265d751d31d6780a3f956bb5b8022feba2d94eeee5a84ba64f4212eedca42213" [[package]] name = "lmdb-rkv" @@ -521,9 +523,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.4.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "ordered-float" @@ -736,9 +738,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.62" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ "itoa", "ryu", @@ -894,6 +896,17 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "whatsys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c24fff5aa1e0973964ba23a995e8b10fa2cdeae507e0cbbbd36f8403242a765d" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -924,3 +937,14 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zeitstempel" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2837f9ad7a7a8c88d1cc50cb0c3d202ce6e178aa6ebf3e49b29561896a61f1d" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "once_cell", +] diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 154e2b3523..cc944eef00 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -1287,12 +1287,12 @@ The following text applies to code linked from these dependencies: * [inherent 0.1.6]( https://github.com/dtolnay/inherent ) * [itoa 0.4.6]( https://github.com/dtolnay/itoa ) * [lazy_static 1.4.0]( https://github.com/rust-lang-nursery/lazy-static.rs ) -* [libc 0.2.86]( https://github.com/rust-lang/libc ) +* [libc 0.2.87]( https://github.com/rust-lang/libc ) * [log 0.4.13]( https://github.com/rust-lang/log ) * [num-integer 0.1.44]( https://github.com/rust-num/num-integer ) * [num-traits 0.2.14]( https://github.com/rust-num/num-traits ) * [num_cpus 1.13.0]( https://github.com/seanmonstar/num_cpus ) -* [once_cell 1.4.1]( https://github.com/matklad/once_cell ) +* [once_cell 1.7.2]( https://github.com/matklad/once_cell ) * [paste 0.1.18]( https://github.com/dtolnay/paste ) * [paste-impl 0.1.18]( https://github.com/dtolnay/paste ) * [percent-encoding 2.1.0]( https://github.com/servo/rust-url/ ) @@ -1301,7 +1301,7 @@ The following text applies to code linked from these dependencies: * [quote 1.0.8]( https://github.com/dtolnay/quote ) * [serde 1.0.121]( https://github.com/serde-rs/serde ) * [serde_derive 1.0.121]( https://github.com/serde-rs/serde ) -* [serde_json 1.0.62]( https://github.com/serde-rs/json ) +* [serde_json 1.0.64]( https://github.com/serde-rs/json ) * [syn 1.0.58]( https://github.com/dtolnay/syn ) * [thiserror 1.0.24]( https://github.com/dtolnay/thiserror ) * [thiserror-impl 1.0.24]( https://github.com/dtolnay/thiserror ) @@ -2157,115 +2157,6 @@ limitations under the License. The following text applies to code linked from these dependencies: -* [lmdb-rkv-sys 0.11.0]( https://github.com/mozilla/lmdb-rs.git ) -* [tinyvec 0.3.4]( https://github.com/Lokathor/tinyvec ) -* [winapi-x86_64-pc-windows-gnu 0.4.0]( https://github.com/retep998/winapi-rs ) - - -``` -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - - - "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - - - - "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - - - - "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - - - "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - - - - "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - - - - "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - - - - "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - - - - "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - - - - "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - - - - "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); - -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software - -distributed under the License is distributed on an "AS IS" BASIS, - -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and - -limitations under the License. -``` -## Apache License 2.0 - - -The following text applies to code linked from these dependencies: - * [android_log-sys 0.2.0]( https://github.com/nercury/android_log-sys-rs ) @@ -2690,7 +2581,6 @@ limitations under the License. The following text applies to code linked from these dependencies: -* [failure_derive 0.1.8]( https://github.com/rust-lang-nursery/failure ) * [lmdb-rkv-sys 0.11.0]( https://github.com/mozilla/lmdb-rs.git ) * [tinyvec 0.3.4]( https://github.com/Lokathor/tinyvec ) * [winapi-x86_64-pc-windows-gnu 0.4.0]( https://github.com/retep998/winapi-rs ) @@ -3323,15 +3213,48 @@ 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. +``` +## MIT License + + +The following text applies to code linked from these dependencies: + +* [whatsys 0.1.2]( https://github.com/badboy/whatsys ) + + +``` +The MIT License (MIT) + +Copyright (c) 2021 Jan-Erik Rediger + +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. + ``` ## Mozilla Public License 2.0 The following text applies to code linked from these dependencies: -* [glean 35.0.0]( https://github.com/mozilla/glean ) -* [glean-core 35.0.0]( https://github.com/mozilla/glean ) -* [glean-ffi 35.0.0]( https://github.com/mozilla/glean ) +* [glean 36.0.0]( https://github.com/mozilla/glean ) +* [glean-core 36.0.0]( https://github.com/mozilla/glean ) +* [glean-ffi 36.0.0]( https://github.com/mozilla/glean ) +* [zeitstempel 0.1.0]( https://github.com/badboy/whatsys ) ``` diff --git a/Makefile b/Makefile index 6610c5fc9a..cf2f453de6 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ $(GLEAN_PYENV)/bin/python3: python3 -m venv $(GLEAN_PYENV) $(GLEAN_PYENV)/bin/pip install --upgrade pip $(GLEAN_PYENV)/bin/pip install --use-feature=2020-resolver -r glean-core/python/requirements_dev.txt - sh -c "if [ \"$(GLEAN_PYDEPS)\" == \"min\" ]; then \ + sh -c "if [ \"$(GLEAN_PYDEPS)\" = \"min\" ]; then \ $(GLEAN_PYENV)/bin/pip install requirements-builder; \ $(GLEAN_PYENV)/bin/requirements-builder --level=min glean-core/python/setup.py > min_requirements.txt; \ $(GLEAN_PYENV)/bin/pip install --use-feature=2020-resolver -r min_requirements.txt; \ @@ -37,7 +37,7 @@ $(GLEAN_PYENV)/bin/python3: build: build-rust build-rust: ## Build all Rust code - cargo build --all $(GLEAN_BUILD_PROFILE) + cargo build --all $(GLEAN_BUILD_PROFILE) $(addprefix --target ,$(GLEAN_BUILD_TARGET)) build-kotlin: ## Build all Kotlin code ./gradlew build -x test @@ -61,10 +61,10 @@ build-csharp: ## Build the C# bindings test: test-rust test-rust: ## Run Rust tests for glean-core and glean-ffi - cargo test --all + cargo test --all $(addprefix --target ,$(GLEAN_BUILD_TARGET)) test-rust-with-logs: ## Run all Rust tests with debug logging and single-threaded - RUST_LOG=glean_core=debug cargo test --all -- --nocapture --test-threads=1 + RUST_LOG=glean_core=debug cargo test --all -- --nocapture --test-threads=1 $(addprefix --target ,$(GLEAN_BUILD_TARGET)) test-kotlin: ## Run all Kotlin tests ./gradlew :glean:testDebugUnitTest @@ -86,48 +86,44 @@ test-csharp: ## Run all C# tests # Benchmarks bench-rust: ## Run Rust benchmarks - cargo bench -p benchmark + cargo bench -p benchmark $(addprefix --target ,$(GLEAN_BUILD_TARGET)) .PHONY: bench-rust # Linting -lint: clippy - -clippy: ## Run cargo-clippy to lint Rust code +lint-rust: ## Run cargo-clippy to lint Rust code cargo clippy --all --all-targets --all-features -- -D warnings -ktlint: ## Run ktlint to lint Kotlin code +lint-kotlin: ## Run ktlint to lint Kotlin code ./gradlew ktlint detekt -swiftlint: ## Run swiftlint to lint Swift code +lint-swift: ## Run swiftlint to lint Swift code swiftlint --strict -yamllint: ## Run yamllint to lint YAML files +lint-yaml: ## Run yamllint to lint YAML files yamllint glean-core .circleci shellcheck: ## Run shellcheck against important shell scripts shellcheck glean-core/ios/sdk_generator.sh shellcheck bin/check-artifact.sh -pythonlint: python-setup ## Run flake8 and black to lint Python code +lint-python: python-setup ## Run flake8 and black to lint Python code $(GLEAN_PYENV)/bin/python3 -m flake8 glean-core/python/glean glean-core/python/tests $(GLEAN_PYENV)/bin/python3 -m black --check --exclude \(.venv\*\)\|\(.eggs\) glean-core/python $(GLEAN_PYENV)/bin/python3 -m mypy glean-core/python/glean -.PHONY: lint clippy ktlint swiftlint yamllint +.PHONY: lint-rust lint-kotlin lint-swift lint-yaml # Formatting -fmt: rustfmt - -rustfmt: ## Format all Rust code +fmt-rust: ## Format all Rust code cargo fmt --all -pythonfmt: python-setup ## Run black to format Python code +fmt-python: python-setup ## Run black to format Python code $(GLEAN_PYENV)/bin/python3 -m black glean-core/python/glean glean-core/python/tests -.PHONY: fmt rustfmt +.PHONY: fmt-rust fmt-python # Docs @@ -148,23 +144,29 @@ python-docs: build-python ## Build the Python documentation .PHONY: docs rust-docs kotlin-docs swift-docs metrics-docs: python-setup ## Build the internal metrics documentation - $(GLEAN_PYENV)/bin/pip install glean_parser==2.2.0 + $(GLEAN_PYENV)/bin/pip install glean_parser==2.5.0 $(GLEAN_PYENV)/bin/glean_parser translate --allow-reserved \ -f markdown \ -o ./docs/user/user/collected-metrics \ glean-core/metrics.yaml glean-core/pings.yaml glean-core/android/metrics.yaml -linkcheck: docs ## Run link-checker on the generated docs +linkcheck: docs linkcheck-raw ## Run link-checker on the generated docs + +linkcheck-raw: # Requires https://www.npmjs.com/package/link-checker link-checker \ - build/docs/book \ + build/docs \ --disable-external true \ --allow-hash-href true \ + --file-ignore "swift/.*" \ + --file-ignore "python/.*" \ + --file-ignore "javadoc/.*" \ + --file-ignore "docs/.*" \ --url-ignore ".*/swift/.*" \ --url-ignore ".*/python/.*" \ --url-ignore ".*/javadoc/.*" \ --url-ignore ".*/docs/glean_.*" -.PHONY: linkcheck +.PHONY: linkcheck linkcheck-raw spellcheck: ## Spellcheck the docs # Requires http://aspell.net/ @@ -188,8 +190,8 @@ rust-coverage: export RUSTUP_TOOLCHAIN=nightly rust-coverage: ## Generate code coverage information for Rust code # Expects a Rust nightly toolchain to be available. # Expects grcov and genhtml to be available in $PATH. - cargo build --verbose - cargo test --verbose + cargo build --verbose $(addprefix --target ,$(GLEAN_BUILD_TARGET)) + cargo test --verbose $(addprefix --target ,$(GLEAN_BUILD_TARGET)) zip -0 ccov.zip `find . \( -name "glean*.gc*" \) -print` grcov ccov.zip -s . -t lcov --llvm --branch --ignore-not-existing --ignore "/*" --ignore "glean-core/ffi/*" -o lcov.info genhtml -o report/ --show-details --highlight --ignore-errors source --legend lcov.info diff --git a/README.md b/README.md index 366aac0d4b..bbbab3b4a1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Glean SDK -![Glean logo](docs/glean.jpeg) +![Glean logo](docs/user/glean.jpeg) [![glean-core on crates.io](http://meritbadge.herokuapp.com/glean-core)](https://crates.io/crates/glean-core) [![License: MPL-2.0](https://img.shields.io/crates/l/glean-core)](https://github.com/mozilla/glean/blob/main/LICENSE) @@ -15,7 +15,7 @@ All documentation is available online: ## Overview -Refer to the documentation for [using and developing the Glean SDK][book]. +Refer to the documentation for [using][book] and [developing][devbook] the Glean SDK. For an overview of Glean beyond just the SDK, see the [section in the Firefox data docs](https://docs.telemetry.mozilla.org/concepts/glean/glean.html). @@ -52,5 +52,6 @@ It's licensed under MPL. [newbugzilla]: https://bugzilla.mozilla.org/enter_bug.cgi?product=Data+Platform+and+Tools&component=Glean%3A+SDK&priority=P3&status_whiteboard=%5Btelemetry%3Aglean-rs%3Am%3F%5D [book]: https://mozilla.github.io/glean/ +[devbook]: https://mozilla.github.io/glean/dev/ [rustdoc]: https://mozilla.github.io/glean/docs/index.html [ktdoc]: https://mozilla.github.io/glean/javadoc/glean/index.html diff --git a/build-scripts/xc-universal-binary.sh b/build-scripts/xc-universal-binary.sh index 5dcc85ea6e..6636d94c8f 100644 --- a/build-scripts/xc-universal-binary.sh +++ b/build-scripts/xc-universal-binary.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # This should be invoked from inside xcode, not manually -if [ "$#" -ne 2 ] +if [ "$#" -ne 3 ] then echo "Usage (note: only call inside xcode!):" echo "Args: $*" - echo "path/to/build-scripts/xc-universal-binary.sh " + echo "path/to/build-scripts/xc-universal-binary.sh " exit 1 fi @@ -13,29 +13,14 @@ fi FFI_TARGET=$1 # path to app services root GLEAN_ROOT=$2 +# buildvariant from our xcconfigs +BUILDVARIANT=$3 -if [ -d "$HOME/.cargo/bin" ]; then - export PATH="$HOME/.cargo/bin:$PATH" +RELFLAG= +if [[ "$BUILDVARIANT" != "debug" ]]; then + RELFLAG=--release fi -if ! command -v cargo-lipo 2>/dev/null >/dev/null; -then - echo "$(basename $0) failed." - echo "Requires cargo-lipo to build universal library." - echo "Install it with:" - echo - echo " cargo install cargo-lipo" - exit 1 -fi - -# Ease testing of this script by assuming something about the environment. -if [ -z "$ACTION" ]; then - export ACTION=build -fi - -# Always build both architectures on x86_64. -export ARCHS="arm64 x86_64" - set -euvx if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then @@ -46,9 +31,31 @@ if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then export LIBRARY_PATH="${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}" fi -# Force correct target for dependencies compiled with `cc`. -# Required for M1 MacBooks (Arm target). -# Without this some dependencies might be compiled for the wrong target. -export CFLAGS_x86_64_apple_ios="-target x86_64-apple-ios" +IS_SIMULATOR=0 +if [ "${LLVM_TARGET_TRIPLE_SUFFIX-}" = "-simulator" ]; then + IS_SIMULATOR=1 +fi -cargo lipo --xcode-integ --manifest-path "$GLEAN_ROOT/Cargo.toml" --package "$FFI_TARGET" +for arch in $ARCHS; do + case "$arch" in + x86_64) + if [ $IS_SIMULATOR -eq 0 ]; then + echo "Building for x86_64, but not a simulator build. What's going on?" >&2 + exit 2 + fi + + # Intel iOS simulator + export CFLAGS_x86_64_apple_ios="-target x86_64-apple-ios" + $HOME/.cargo/bin/cargo build -p $FFI_TARGET --lib $RELFLAG --target x86_64-apple-ios + ;; + + arm64) + if [ $IS_SIMULATOR -eq 0 ]; then + # Hardware iOS targets + $HOME/.cargo/bin/cargo build -p $FFI_TARGET --lib $RELFLAG --target aarch64-apple-ios + else + # M1 iOS simulator -- currently in Nightly only and requires to build `libstd` + $HOME/.cargo/bin/cargo +nightly build -Z build-std -p $FFI_TARGET --lib $RELFLAG --target aarch64-apple-ios-sim + fi + esac +done diff --git a/docs/dev/README.md b/docs/dev/README.md index a85e73a2af..297daad896 100644 --- a/docs/dev/README.md +++ b/docs/dev/README.md @@ -5,6 +5,7 @@ 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). To contact us you can: + - Find us in the [#glean channel on chat.mozilla.org](https://chat.mozilla.org/#/room/#glean:mozilla.org). - To report issues or request changes, file a bug in [Bugzilla in Data Platform & Tools :: Glean: SDK](https://bugzilla.mozilla.org/enter_bug.cgi?product=Data+Platform+and+Tools&component=Glean%3A+SDK&priority=P3&status_whiteboard=%5Btelemetry%3Aglean-rs%3Am%3F%5D). - Send an email to *glean-team@mozilla.com*. @@ -12,25 +13,14 @@ To contact us you can: The source code is available [on GitHub](https://github.com/mozilla/glean/). -## Using this book +## About this book -This book is specifically about **contributing to and developing** the `Glean SDK`. +This book is meant for developers of the `Glean SDK`. -For documentation on **using** the `Glean SDK`, refer to [the Glean Book](../book). +For user documentation on the Glean SDK, refer to [the Glean Book](../book/index.html). For documentation about the broader end-to-end Glean project, refer to the [Mozilla Data Documentation](https://docs.telemetry.mozilla.org/concepts/glean/glean.html). -This book is divided into 2 main parts: - -### [Developing the Glean SDK](testing.md) - -This chapter describes how to develop the Glean SDK and its various implementations. -This is relevant if you plan to contribute to the Glean SDK code base. - -### [API Reference Documentation](api/index.md) - -Reference documentation for the API in its various language bindings. - ## License The Glean SDK Source Code is subject to the terms of the Mozilla Public License v2.0. diff --git a/docs/dev/SUMMARY.md b/docs/dev/SUMMARY.md index dd126c8d61..a54b0a4ba6 100644 --- a/docs/dev/SUMMARY.md +++ b/docs/dev/SUMMARY.md @@ -25,6 +25,8 @@ - [Kotlin](core/new-metric-type/kotlin.md) - [Swift](core/new-metric-type/swift.md) - [Python](core/new-metric-type/python.md) + - [Rust](core/new-metric-type/rust.md) + - [Platform](core/new-metric-type/platform.md) - [FFI Layer](ffi/index.md) - [When/How FFI](ffi/when-to-use-what-in-the-ffi.md) - [Internal implementation details](core/internal/index.md) diff --git a/docs/dev/android/setup-android-build-environment.md b/docs/dev/android/setup-android-build-environment.md index 16db7818a7..0fe408cb27 100644 --- a/docs/dev/android/setup-android-build-environment.md +++ b/docs/dev/android/setup-android-build-environment.md @@ -4,7 +4,7 @@ This document describes how to make local builds of the Android bindings in this repository. Most consumers of these bindings *do not* need to follow this process, -but will instead [use pre-built bindings](../../user/adding-glean-to-your-project.html). +but will instead [use pre-built bindings](../../book/user/adding-glean-to-your-project.html). ## Prepare your build environment diff --git a/docs/dev/code_coverage.md b/docs/dev/code_coverage.md index 161ea2f8b4..e9c55d0710 100644 --- a/docs/dev/code_coverage.md +++ b/docs/dev/code_coverage.md @@ -5,6 +5,8 @@ > which suggests it has a lower chance of containing undetected software bugs compared to a program with low test coverage. > ([Wikipedia](https://en.wikipedia.org/wiki/Code_coverage)) +This chapter describes how to generate a traditional code coverage report over the Kotlin, Rust, Swift and Python code in the Glean SDK repository. To learn how to generate a coverage report about what metrics your project is testing, see the user documentation on [generating testing coverage reports](https://mozilla.github.io/glean/book/user/testing-metrics.html#generating-testing-coverage-reports). + ## Generating Kotlin reports locally Locally you can generate a coverage report with the following command: diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index de7e27c31a..0303977369 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -34,7 +34,7 @@ make test-rust ``` If you plan to work on the Android component bindings, you should also review -the instructions for [setting up an Android build environment](dev/android/setup-android-build-environment.md). +the instructions for [setting up an Android build environment](android/setup-android-build-environment.md). To run all Kotlin tests: @@ -92,4 +92,4 @@ Reviewers can also ask for additional approval from other reviewers. ## Release -See the [Release process](dev/cut-a-new-release.md) on how to release a new version of the Glean SDK. +See the [Release process](cut-a-new-release.md) on how to release a new version of the Glean SDK. diff --git a/docs/dev/core/internal/debug-pings.md b/docs/dev/core/internal/debug-pings.md index f9f9c9a660..438bb72d87 100644 --- a/docs/dev/core/internal/debug-pings.md +++ b/docs/dev/core/internal/debug-pings.md @@ -10,7 +10,7 @@ X-Debug-ID: `` is a alphanumeric string with a maximum length of 20 characters, used to identify pings in the Debug Ping Viewer. -See [Debugging products using the Glean SDK](../../../user/debugging/index.md) for detailed information how to use this mechanism in applications. +See [Debugging products using the Glean SDK](../../../book/user/debugging/index.md) for detailed information how to use this mechanism in applications. [debug-ping-viewer]: https://debug-ping-preview.firebaseapp.com/ diff --git a/docs/dev/core/internal/payload.md b/docs/dev/core/internal/payload.md index a0a748523e..b904799596 100644 --- a/docs/dev/core/internal/payload.md +++ b/docs/dev/core/internal/payload.md @@ -1,6 +1,6 @@ # Payload format -The main sections of a Glean ping are described in [Ping Sections](../../../user/pings/index.md#ping-sections). +The main sections of a Glean ping are described in [Ping Sections](../../../book/user/pings/index.md#ping-sections). This **Payload format** chapter describes details of the ping payload that are relevant for decoding Glean pings in the pipeline. > NOTE: The payload format is an implementation detail of the Glean SDK and subject to change at any time. @@ -16,7 +16,7 @@ It is written as a set of [templates](https://github.com/mozilla-services/mozill ### Boolean -A [Boolean](../../../user/metrics/boolean.md) is represented by its boolean value. +A [Boolean](../../../book/user/metrics/boolean.md) is represented by its boolean value. #### Example @@ -27,7 +27,7 @@ true ### Counter -A [Counter](../../../user/metrics/counter.md) is represented by its integer value. +A [Counter](../../../book/user/metrics/counter.md) is represented by its integer value. #### Example @@ -37,7 +37,7 @@ A [Counter](../../../user/metrics/counter.md) is represented by its integer valu ### Quantity -A [Quantity](../../../user/metrics/quantity.md) is represented by its integer value. +A [Quantity](../../../book/user/metrics/quantity.md) is represented by its integer value. #### Example @@ -47,7 +47,7 @@ A [Quantity](../../../user/metrics/quantity.md) is represented by its integer va ### String -A [String](../../../user/metrics/string.md) is represented by its string value. +A [String](../../../book/user/metrics/string.md) is represented by its string value. #### Example @@ -57,7 +57,7 @@ A [String](../../../user/metrics/string.md) is represented by its string value. ### JWE -A [JWE](../../../user/metrics/jwe.md) is represented by its [compact representation](https://tools.ietf.org/html/rfc7516#appendix-A.2.7). +A [JWE](../../../book/user/metrics/jwe.md) is represented by its [compact representation](https://tools.ietf.org/html/rfc7516#appendix-A.2.7). #### Example @@ -67,7 +67,7 @@ A [JWE](../../../user/metrics/jwe.md) is represented by its [compact representat ### String list -A [String List](../../../user/metrics/string_list.md) is represented as an array of strings. +A [String List](../../../book/user/metrics/string_list.md) is represented as an array of strings. ```json ["sample string", "another one"] @@ -75,12 +75,12 @@ A [String List](../../../user/metrics/string_list.md) is represented as an array ### Timespan -A [Timespan](../../../user/metrics/timespan.md) is represented as an object of their duration as an integer and the time unit. +A [Timespan](../../../book/user/metrics/timespan.md) is represented as an object of their duration as an integer and the time unit. | Field name | Type | Description | |---|---|---| | `value` | Integer | The value in the marked time unit. | -| `time_unit` | String | The time unit, see the [timespan's configuration](../../../user/metrics/timespan.md#configuration) for valid values. | +| `time_unit` | String | The time unit, see the [timespan's configuration](../../../book/user/metrics/timespan.md#configuration) for valid values. | #### Example @@ -93,7 +93,7 @@ A [Timespan](../../../user/metrics/timespan.md) is represented as an object of t ### Timing Distribution -A [Timing distribution](../../../user/metrics/timing_distribution.md) is represented as an object with the following fields. +A [Timing distribution](../../../book/user/metrics/timing_distribution.md) is represented as an object with the following fields. | Field name | Type | Description | |---|---|---| @@ -128,7 +128,7 @@ sent: 1024: 2, 1116: 1, 1217: 0, 1327: 0, 1448: 1, 1579: 0 ### Memory Distribution -A [Memory distribution](../../../user/metrics/memory_distribution.md) is represented as an object with the following fields. +A [Memory distribution](../../../book/user/metrics/memory_distribution.md) is represented as an object with the following fields. | Field name | Type | Description | |---|---|---| @@ -152,7 +152,7 @@ See [timing distribution](#timing-distribution) for more details. ### UUID -A [UUID](../../../user/metrics/uuid.md) is represented by the string representation of the UUID. +A [UUID](../../../book/user/metrics/uuid.md) is represented by the string representation of the UUID. #### Example @@ -162,7 +162,7 @@ A [UUID](../../../user/metrics/uuid.md) is represented by the string representat ### Datetime -A [Datetime](../../../user/metrics/datetime.md) is represented by its ISO8601 string representation, truncated to the metric's time unit. +A [Datetime](../../../book/user/metrics/datetime.md) is represented by its ISO8601 string representation, truncated to the metric's time unit. It always includes the timezone offset. #### Example @@ -173,7 +173,7 @@ It always includes the timezone offset. ### Event -[Events](../../../user/metrics/event.md) are represented as an array of objects, with one object for each event. +[Events](../../../book/user/metrics/event.md) are represented as an array of objects, with one object for each event. Each event object has the following keys: | Field name | Type | Description | @@ -208,12 +208,12 @@ Also see [the JSON schema for events](https://github.com/mozilla-services/mozill To avoid losing events when the application is killed by the operating system, events are queued on disk as they are recorded. When the application starts up again, there is no good way to determine if the device has rebooted since the last run and therefore any timestamps recorded in the new run could not be guaranteed to be consistent with those recorded in the previous run. To get around this, on application startup, any queued events are immediately collected into pings and then cleared. -These "startup-triggered pings" are likely to have a very short duration, as recorded in `ping_info.start_time` and `ping_info.end_time` (see [the `ping_info` section](../../../user/pings/index.md#the-ping_info-section)). +These "startup-triggered pings" are likely to have a very short duration, as recorded in `ping_info.start_time` and `ping_info.end_time` (see [the `ping_info` section](../../../book/user/pings/index.md#the-ping_info-section)). The maximum timestamp of the events in these pings are quite likely to exceed the duration of the ping, but this is to be expected. ### Custom Distribution -A [Custom distribution](../../../user/metrics/custom_distribution.md) is represented as an object with the following fields. +A [Custom distribution](../../../book/user/metrics/custom_distribution.md) is represented as an object with the following fields. | Field name | Type | Description | |---|---|---| @@ -258,8 +258,8 @@ sent: 10: 0, 12: 2, 14: 0, 17: 0, 19: 0, 22: 1, 24: 0 Currently several labeled metrics are supported: -* [Labeled Counters](../../../user/metrics/labeled_counters.md). -* [Labeled Strings](../../../user/metrics/labeled_strings.md). +* [Labeled Counters](../../../book/user/metrics/labeled_counters.md). +* [Labeled Strings](../../../book/user/metrics/labeled_strings.md). All are on the top-level represented in the same way, as an object mapping the label to the metric's value. See the individual metric types for details on the value payload: @@ -275,3 +275,16 @@ See the individual metric types for details on the value payload: "label2": 17 } ``` + +### Rate + +A [Rate](../../../book/user/metrics/rate.md) is represented by its `numerator` and `denominator`. + +#### Example + +```json +{ + "numerator": 22, + "denominator": 7, +} +``` diff --git a/docs/dev/core/new-metric-type.md b/docs/dev/core/new-metric-type.md index 570b800615..08732dcc27 100644 --- a/docs/dev/core/new-metric-type.md +++ b/docs/dev/core/new-metric-type.md @@ -1,13 +1,27 @@ # Adding a new metric type Data in the Glean SDK is stored in so-called metrics. -You can find the full list of implemented metric types [in the user overview](../../user/metrics/index.md). +You can find the full list of implemented metric types [in the user overview](../../book/user/metrics/index.md). Adding a new metric type involves defining the metric type's API, its persisted and in-memory storage as well as its serialization into the ping payload. +## `glean_parser` + +In order for your metric to be usable, you must add it to +[`glean_parser`](https://github.com/mozilla/glean_parser) +so that instances of your new metric can be instantiated and available to our users. + +The documentation for how to do this should live in the `glean_parser` repository, +but in short: +* Your metric type must be added to the metrics schema. +* Your metric type must be added as a type in the object model +* Any new parameters outside of the common metric data must also be added to the schema, + and be stored in the object model. +* You must add tests. + ## The metric type's API -A metric type implementation is defined in its own file under `glean-core/src/metrics/`, e.g. `glean-core/src/metrics/counter.rs` for a [Counter](../../user/metrics/counter.md). +A metric type implementation is defined in its own file under `glean-core/src/metrics/`, e.g. `glean-core/src/metrics/counter.rs` for a [Counter](../../book/user/metrics/counter.md). Start by defining a structure to hold the metric's metadata: @@ -128,6 +142,27 @@ For example, the `DateTime` serializer has the following entry, where `get_iso_t Metric::Datetime(d, time_unit) => json!(get_iso_time_string(*d, *time_unit)), ``` +## Documentation + +Documentation for the new metric type must be added to the +[user book](https://mozilla.github.io/glean/book/index.html). + +* Add a new file for your new metric in `docs/user/user/metrics/`. + Its contents should follow the form and content of the other examples in that folder. +* Reference that file in `docs/user/SUMMARY.md` so it will be included in the build. +* Follow the [Documentation Contribution Guide](../docs.html). + +You must also update the +[payload documentation](internal/payload.md) +with how the metric looks in the payload. + +## Tests + +Tests are written in the Language Bindings and tend to just cover basic functionality: +* The metric returns the correct value when it has no value +* The metric correctly reports errors +* The metric returns the correct value when it has value + --- In the next step we will create the FFI wrapper and platform-specific wrappers. diff --git a/docs/dev/core/new-metric-type/platform.md b/docs/dev/core/new-metric-type/platform.md new file mode 100644 index 0000000000..c8bf374fec --- /dev/null +++ b/docs/dev/core/new-metric-type/platform.md @@ -0,0 +1,56 @@ +# Adding a new metric type - data platform + +The data platform technically exists outside of the Glean SDK. However, the Glean-specific steps for adding a new Glean metric type to the data platform are documented here for convenience. + +## Adding a new metric type to `mozilla-pipeline-schemas` + +The [`mozilla-pipeline-schemas`](https://github.com/mozilla-services/mozilla-pipeline-schemas) contains JSON schemas that are used to validate the Glean ping payload when reaching the ingestion server. +These schemas are written using a simple custom templating system to give more structure and organization to the schemas. + +Each individual metric type has its own file in `templates/include/glean`. For example, here is the schema for the Counter metric type in `counter.1.schema.json`: + +```json +{ + "type": "integer" +} +``` + +Add a new file for your new metric type in that directory, containing the JSON schema snippet to validate it. A good resource for learning about JSON schema and the validation keywords that are available is [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/). + +A reference to this template needs to be added to the main Glean schema in [templates/include/glean/glean.1.schema.json](https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/04043f16b319c2a38b1cfd773ccbcf8ec4d73ac3/templates/include/glean/glean.1.schema.json#L133). For example, the snippet to include the template for the counter metric type is: + +```json + "counter": { + @GLEAN_BASE_OBJECT_1_JSON@, + "additionalProperties": @GLEAN_COUNTER_1_JSON@ + }, +``` + +If adding a labeled metric type as well, the same template from the "core" metric type can be reused: + +```json + "labeled_counter": { + @GLEAN_BASE_OBJECT_1_JSON@, + "additionalProperties": { + @GLEAN_LABELED_GROUP_1_JSON@, + "additionalProperties": @GLEAN_COUNTER_1_JSON@ + } + }, +``` + +After updating the templates, you need to regenerate the fully-qualified schema using [these instructions](https://github.com/mozilla-services/mozilla-pipeline-schemas#build). + +The fully-qualified Glean schema is also used by the Glean SDK's unit test suite to make sure that ping payloads validate against the schema. Therefore, whenever the Glean JSON schema is updated, it should also be copied and checked in to the [Glean SDK repository](https://github.com/mozilla/glean). Specifically, copy the generated schema in `mozilla-pipeline-schemas/schemas/glean/glean.1.schema.json` to the root of the Glean SDK repository. + +## Adding a new metric type to `mozilla-schema-generator` + +Each new metric type also needs an entry in the Glean configuration in [`mozilla-schema-generator`](https://github.com/mozilla/mozilla-schema-generator). The config file for Glean is in [`glean.yaml`](https://github.com/mozilla/mozilla-schema-generator/blob/7276cfb3b14440f8cb93e57d9f167d7588092dae/mozilla_schema_generator/configs/glean.yaml#L1). Each entry in that file just needs some boilerplate for each metric type. For example, the snippet for the Counter metric type is: + +```yaml + counter: + match: + send_in_pings: + not: + - glean_ping_info + type: counter +``` diff --git a/docs/dev/core/new-metric-type/rust.md b/docs/dev/core/new-metric-type/rust.md new file mode 100644 index 0000000000..eae1523708 --- /dev/null +++ b/docs/dev/core/new-metric-type/rust.md @@ -0,0 +1,64 @@ +# Adding a new metric type - Rust + +## Trait + +To ensure the API is stable across Rust consumers and re-exporters (like FOG), +you must define a Trait for the new metric in `glean-core/src/traits`. +First, add your metric type to `mod.rs`: + +```rust +mod counter; +... +pub use self::counter::Counter; +``` + +Then add the trait in e.g. +[`counter.rs`](https://github.com/mozilla/glean/blob/HEAD/glean-core/src/traits/counter.rs). + +The trait includes `test_get_num_recorded_errors` +and any metric operation included in the metric type's API +(except `new`). +The idea here is to only include operations that make sense for Rust consumers. +If there are internal-only or language-specific APIs on the underlying metric, +feel free to not include them on the trait. + +Spend some time on the comments. +These will be the dev-facing API docs for Rust consumers. + +## Rust Language Binding (RLB) Type + +The Rust Language Binding supplies the implementation of the trait +(mostly by delegating to the glean-core implementation) +and adds a layer of ordering and safety using the dispatcher. +You can find the RLB metric implementations in +`glean-core/rlb/src/private`. + +First, add your metric type to `mod.rs`: + +```rust +mod counter; +... +pub use counter::Counter; +``` + +Then add the trait in e.g. +[`counter.rs`](https://github.com/mozilla/glean/blob/HEAD/glean-core/rlb/src/private/counter.rs). + +Note that in `counter.rs` the (internal) `new` and `add_sync` are on the `struct`'s `impl`, not the trait's. + +Note there are no API comments on the trait's `impl`. + +If your metric type has locked internal mutability, +(like `TimingDistributionMetric`'s `RwLock`) +you must always take the metric lock and Glean in the same order. + +## Tests + +Add at least a "smoke test" (a simple confirmation of the API's behavior) +to the RLB implementation. + +## Documentation + +Don't forget to document the new Rust API in the Book's page on the Metric Type +(e.g. [Counter](../../../book/user/metrics/counter.html)). +Add a tab for Rust, and mimic any other language's example. diff --git a/docs/dev/cut-a-new-release.md b/docs/dev/cut-a-new-release.md index 913aea1b6d..5e25e9e0cb 100644 --- a/docs/dev/cut-a-new-release.md +++ b/docs/dev/cut-a-new-release.md @@ -3,7 +3,7 @@ The Glean SDK consists of multiple libraries for different platforms and targets. The main supported libraries are released as one. Development happens on the main repository . -See [Contributing](../contributing.md) for how to contribute changes to the Glean SDK. +See [Contributing](contributing.md) for how to contribute changes to the Glean SDK. The development & release process roughly follows the [GitFlow model](https://nvie.com/posts/a-successful-git-branching-model/). diff --git a/docs/dev/python/setting-up-python-build-environment.md b/docs/dev/python/setting-up-python-build-environment.md index df22dab75b..5e48d11921 100644 --- a/docs/dev/python/setting-up-python-build-environment.md +++ b/docs/dev/python/setting-up-python-build-environment.md @@ -2,7 +2,7 @@ This document describes how to set up an environment for the development of the Glean Python bindings. -Instructions for installing a copy of the Glean Python bindings into your own environment for use in your project are described in [adding Glean to your project](../../user/adding-glean-to-your-project.html). +Instructions for installing a copy of the Glean Python bindings into your own environment for use in your project are described in [adding Glean to your project](../../book/user/adding-glean-to-your-project.html). ## Prerequisites @@ -174,7 +174,7 @@ The Python API docs are built using [pdoc3](https://pdoc3.github.io/pdoc/). ## Building wheels for Linux -Building on Linux using the above instructions will create Linux binaries that dynamically link against the version of `libc` installed on your machine. +Building on Linux using the above instructions will create Linux binaries that dynamically link against the version of `libc` installed on your machine. This generally will not be portable to other Linux distributions, and PyPI will not even allow it to be uploaded. In order to create wheels that can be installed on the broadest range of Linux distributions, the Python Packaging Authority's [manylinux](https://github.com/pypa/manylinux) project maintains a Docker image for building compatible Linux wheels. diff --git a/docs/user/README.md b/docs/user/README.md index 4d8d7a1f86..045afd561d 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -1,44 +1,91 @@ -# Glean SDK +# Introduction + +Glean 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 logo](glean.jpeg) -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). +There are two implementations of Glean, with support for 5 different programming languages in total. +Both implementations strive to contain the same features with similar, but idiomatic APIs. -To contact us you can: -- Find us in the [#glean channel on chat.mozilla.org](https://chat.mozilla.org/#/room/#glean:mozilla.org). -- To report issues or request changes, file a bug in [Bugzilla in Data Platform & Tools :: Glean: SDK](https://bugzilla.mozilla.org/enter_bug.cgi?product=Data+Platform+and+Tools&component=Glean%3A+SDK&priority=P3&status_whiteboard=%5Btelemetry%3Aglean-rs%3Am%3F%5D). -- Send an email to *glean-team@mozilla.com*. -- The Glean SDK team is: *:janerik*, *:dexter*, *:travis*, *:mdroettboom*, *:gfritzsche*, *:chutten*, *:brizental*. +Unless clearly stated otherwise, regard the text in this book as valid for both clients +and all the supported programming languages and environments. + +### [The Glean SDK](https://github.com/mozilla/glean) + +The Glean SDK is an implementation of Glean in Rust, with language bindings for **Kotlin**, +**Python**, **Rust** and **Swift**. + +For development documentation on the `Glean SDK`, +refer to [the Glean SDK development book](../dev/index.html). + +To report issues or request changes on the Glean SDK, +file a bug in [Bugzilla in Data Platform & Tools :: Glean: SDK](https://bugzilla.mozilla.org/enter_bug.cgi?product=Data+Platform+and+Tools&component=Glean%3A+SDK&priority=P3&status_whiteboard=%5Btelemetry%3Aglean-rs%3Am%3F%5D). + +### [Glean.js](https://github.com/mozilla/glean.js) + +Glean.js is an implementation of Glean in **Javascript**. Currently, it only has support +for usage in web extensions. + +For development documentation on `Glean.js`, +refer to [the Glean.js development documentation](https://github.com/mozilla/glean.js/tree/main/docs). -The source code is available [on GitHub](https://github.com/mozilla/glean/). +To report issues or request changes on Glean.js, +file a bug in [Bugzilla in Data Platform & Tools :: Glean.js][gleanjs-bugs]. -## Using this book +> **Note** Glean.js is still in development and does not provide all the features the Glean SDK does. +> Feature parity will be worked on after initial validation. Do not hesitate to [file a bug][gleanjs-bugs] +> if you want to use Glean.js and is missing some key Glean feature. +## Sections -This book is specifically about **using** the `Glean SDK`. +### [Using Glean](./user/index.html) -For documentation on **contributing to and developing** the `Glean SDK`, refer to [the Glean SDK development book](../dev/). +In this section we describe how to use Glean in your own libraries and applications. +It explains the first steps of integrating Glean into your project, choosing the right metric type for you, +debugging products that use Glean and Glean's built-in error reporting mechanism. +If you want to start using Glean to report data, this is the section you should read. -For documentation about the broader end-to-end Glean project, refer to the [Mozilla Data Documentation](https://docs.telemetry.mozilla.org/concepts/glean/glean.html). +### [Metric Types](./user/metrics/index.html) -This book is divided into 3 main chapters: +This sections lists all the metric types provided by Glean, with examples on how to define them +and record data using them. Before diving into Glean's metric types details, don't forget to +read the [Choosing a metric type](https://mozilla.github.io/glean/book/user/adding-new-metrics.html#choosing-a-metric-type) page. -### [Using the Glean SDK](user/index.html) +### [Pings](./user/pings/index.html) -If you want to use the Glean SDK to report data then this is the section you should read. -It explains the first steps from integrating Glean into your project, -contains details about all available metric types -and how to do send your own custom pings. +This section goes through what is a ping and how to define custom pings. A Glean client may provide +off-the-shelf pings, such as the [`metrics`](https://mozilla.github.io/glean/book/user/pings/metrics.html) +or [`baseline`](https://mozilla.github.io/glean/book/user/pings/baseline.html) pings. In this section, +you will also find the descriptions and the schedules of each of these pings. -### [Metrics collected by the Glean SDK](user/collected-metrics/metrics.md) +### Appendix -This chapter lists all metrics collected by the Glean SDK itself. +#### [Glossary](./appendix/glossary.html) -### [This Week in Glean](appendix/twig.md) +In this book we use a lot of Glean specific terminology. In the glossary, we go through +many of the terms used throughout this book and describe exactly what we mean when we use them. -“This Week in Glean” is a series of blog posts that the Glean Team at Mozilla is using to try to communicate better about our work. -They could be release notes, documentation, hopes, dreams, or whatever: so long as it is inspired by Glean. +#### [Changelog](./appendix/changelog.html) + +This section contains detailed notes about changes in Glean, per release. + +#### [This Week in Glean](./appendix/twig.html) + +“This Week in Glean” is a series of blog posts that the Glean Team at Mozilla is using to try +to communicate better about our work. They could be release notes, documentation, hopes, dreams, +or whatever: so long as it is inspired by Glean. + +## Contact + +To contact the Glean team you can: + +- Find us in the [#glean channel on chat.mozilla.org](https://chat.mozilla.org/#/room/#glean:mozilla.org). +- Send an email to *glean-team@mozilla.com*. +- The Glean SDK team is: *:janerik*, *:dexter*, *:travis*, *:mdroettboom*, *:gfritzsche*, *:chutten*, *:brizental*. ## License -The Glean SDK Source Code is subject to the terms of the Mozilla Public License v2.0. +Glean.js and the Glean SDK Source Code is subject to the terms of the Mozilla Public License v2.0. You can obtain a copy of the MPL at . + +[gleanjs-bugs]: https://bugzilla.mozilla.org/enter_bug.cgi?product=Data+Platform+and+Tools&component=Glean.js&priority=P4&status_whiteboard=%5Btelemetry%3Aglean-js%3Am%3F%5D diff --git a/docs/user/SUMMARY.md b/docs/user/SUMMARY.md index 6592ff5555..0f99dd92b0 100644 --- a/docs/user/SUMMARY.md +++ b/docs/user/SUMMARY.md @@ -30,6 +30,7 @@ - [Event](user/metrics/event.md) - [Custom Distribution](user/metrics/custom_distribution.md) - [Quantity](user/metrics/quantity.md) + - [Rate](user/metrics/rate.md) - [Pings](user/pings/index.md) - [Ping schedules and timings overview](user/pings/ping-schedules-and-timings.md) - [Baseline Ping](user/pings/baseline.md) diff --git a/docs/user/appendix/twig.md b/docs/user/appendix/twig.md index d45db7e1ea..6a547649c5 100644 --- a/docs/user/appendix/twig.md +++ b/docs/user/appendix/twig.md @@ -42,3 +42,4 @@ They could be release notes, documentation, hopes, dreams, or whatever: so long * 2020-12-18: [Glean in 2021](https://fnordig.de/2020/12/18/glean-in-2021/) * 2021-01-15: [Proposals for Asynchronous Design](https://blog.mozilla.org/data/2021/01/15/this-week-in-glean-proposals-for-asynchronous-design/) * 2021-01-28: [The Glean Dictionary](https://blog.mozilla.org/data/2021/01/27/this-week-in-glean-the-glean-dictionary/) +* 2021-02-24: [Boring Monitoring](https://blog.mozilla.org/data/2021/02/24/this-week-in-glean-boring-monitoring/) diff --git a/docs/user/user/adding-new-metrics.md b/docs/user/user/adding-new-metrics.md index 82a47235fc..c87513c8be 100644 --- a/docs/user/user/adding-new-metrics.md +++ b/docs/user/user/adding-new-metrics.md @@ -38,6 +38,9 @@ For all of the metric types in this section that measure single values, it is es If you want to know how many times something happened, use a [counter metric](metrics/counter.html). If you are counting a group of related things, or you don't know what all of the things to count are at build time, use a [labeled counter metric](metrics/labeled_counters.html). +If you need to know how many times something happened relative to the number of times something else happened, +use a [rate metric](metrics/rate.html). + If you need to know when the things being counted happened relative to other things, consider using an [event](metrics/event.html). ### Are you measuring time? diff --git a/docs/user/user/metrics/counter.md b/docs/user/user/metrics/counter.md index cbcf1be84c..84d6f352da 100644 --- a/docs/user/user/metrics/counter.md +++ b/docs/user/user/metrics/counter.md @@ -7,6 +7,8 @@ Unless incremented by a positive value, a counter will not be reported in pings. > **IMPORTANT:** When using a counter metric, it is important to let the Glean metric do the counting. Using your own variable for counting and setting the counter yourself could be problematic because it will be difficult to reset the value at the exact moment that the value is sent in a ping. Instead, just use `counter.add` to increment the value and let Glean handle resetting the counter. +If you find that you need to control the actual value sent in the ping, you may be measuring something, not just counting something, and a [Quantity metric](quantity.html) may be a better choice. + ## Configuration Say you're adding a new counter for how often the refresh button is pressed. First you need to add an entry for the counter to the `metrics.yaml` file: diff --git a/docs/user/user/metrics/index.md b/docs/user/user/metrics/index.md index bcb05dc146..aeb71c37a2 100644 --- a/docs/user/user/metrics/index.md +++ b/docs/user/user/metrics/index.md @@ -6,6 +6,8 @@ There are different metrics to choose from, depending on what you want to achiev * [Boolean](boolean.md): Records a single truth value, for example "is a11y enabled?" +* [Labeled boolean](labeled_booleans.md): Records truth values for a set of labels, for example "which a11y features are enabled?" + * [Counter](counter.md): Used to count how often something happens, for example, how often a certain button was pressed. * [Labeled counter](labeled_counters.md): Used to count how often something happens, for example which kind of crash occurred (`"uncaught_exception"` or `"native_code_crash"`). @@ -32,6 +34,9 @@ There are different metrics to choose from, depending on what you want to achiev * [Quantity](quantity.md): Used to record a single non-negative integer value. For example, the width of the display in pixels. +* [Rate](rate.md): Used to record the rate something happens relative to some other thing. + For example, the number of HTTP connections that experienced an error relative to the number of total HTTP connections made. + * [Custom Distribution](custom_distribution.md): Used to record the distribution of a value that needs fine-grained control of how the histogram buckets are computed. **Custom distributions are only available for values that come from Gecko.** ## Labeled metrics diff --git a/docs/user/user/metrics/rate.md b/docs/user/user/metrics/rate.md new file mode 100644 index 0000000000..17cb75f086 --- /dev/null +++ b/docs/user/user/metrics/rate.md @@ -0,0 +1,139 @@ +# Rate + +Used to count how often something happens relative to how often something else happens. +Like how many documents use a particular CSS Property, +or how many HTTP connections had an error. +You can think of it like a fraction, with a numerator and a denominator. + +All rates start without a value. +A rate with a numerator of 0 is valid and will be sent to ensure we capture the +"no errors happened" or "no use counted" cases. + +> **IMPORTANT:** When using a rate metric, it is important to let the Glean metric do the counting. + Using your own variable for counting and setting the metric yourself could be problematic: + ping scheduling will make it difficult to ensure the metric is at the correct value at the correct time. + Instead, count to the numerator and denominator as you go. + +## Configuration + +Say you're adding a new rate for how often HTTP connections have errors. +First you need to add an entry for the rate to the `metrics.yaml` file: + +```YAML +network: + http_connection_error: + type: rate + description: > + How many HTTP connections error out out of the total connections made. + ... +``` + +### External Denominators + +If several rates share the same denominator +(from our example above, maybe there are multiple rates per total connections made) +then the denominator should be defined as a `counter` and shared between +`rates` using the `denominator_metric` property: + +```YAML +network: + http_connections: + type: counter + description: > + Total number of http connections made. + ... + + http_connection_error: + type: rate + description: > + How many HTTP connections error out out of the total connections made. + denominator_metric: network.http_connections + ... + + http_connection_slow: + type: rate + description: > + How many HTTP connections were slow, out of the total connections made. + denominator_metric: network.http_connections + ... +``` + +## API + +{{#include ../../../shared/tab_header.md}} + +
+ +Since a rate is two numbers, you add to each one individually: + +```rust +use glean_metrics::*; + +if connection_had_error { + network::http_connection_error.add_to_numerator(1); +} + +network::http_connection_error.add_to_denominator(1); +``` + +If the rate uses an external denominator, +adding to the denominator must be done through the denominator's +`counter` API: + +```rust +use glean_metrics; + +if connection_had_error { + network::http_connection_error.add_to_numerator(1); +} +if connection_was_slow { + network::http_connection_slow.add_to_numerator(1); +} + +// network::http_connection_error has no `add_to_denominator` method. +network::http_connections.add(1); +``` + +There are test APIs available too. +Whether the rate has an external denominator or not, +you can use this API to get the current value: + +```rust +use glean::ErrorType; + +use glean_metrics; + +// Was anything recorded? +assert!(network::http_connection_error.test_get_value(None).is_some()); +// Does it contain counter have the expected values? +assert_eq!((1, 1), network::http_connection_error.test_get_value(None).unwrap()); +// Did the numerator or denominator ever have a negative value added? +assert_eq!( + 0, + network::http_connection_error.test_get_num_recorded_errors( + ErrorType::InvalidValue + ) +); +``` + +
+ +{{#include ../../../shared/tab_footer.md}} + +## Limits + +* Numerator and Denominator only increment. +* Numerator and Denominator saturate at the largest value that can be represented as a 32-bit signed integer (`2147483647`). + +## Examples + +* How often did an HTTP connection error? +* How many documents used a given CSS Property? + +## Recorded errors + +* `invalid_value`: If either numerator or denominator is incremented by a negative value. + +## Reference + +* [Rust API docs](../../../docs/glean/private/rate/struct.RateMetric.html) diff --git a/docs/user/user/testing-metrics.md b/docs/user/user/testing-metrics.md index 56d95a9771..e81a3c1eac 100644 --- a/docs/user/user/testing-metrics.md +++ b/docs/user/user/testing-metrics.md @@ -6,6 +6,8 @@ These functions expose a way to inspect and validate recorded metric values with (`@VisibleForTesting(otherwise = VisibleForTesting.NONE)` for Kotlin, `internal` methods for Swift). (Outside of a testing context, Glean APIs are otherwise write-only so that it can enforce semantics and constraints about data). +To encourage using the testing API, it is also possible to [generate testing coverage reports](#generating-testing-coverage-reports) to show which metrics in your project are tested. + ## General test API method semantics {{#include ../../shared/tab_header.md}} @@ -266,3 +268,53 @@ TODO. To be implemented in [bug 1648448](https://bugzilla.mozilla.org/show_bug.c {{#include ../../shared/tab_footer.md}} + +## Generating testing coverage reports + +Glean can generate coverage reports to track which metrics are tested in your unit test suite. + +There are three steps to integrate it into your continuous integration workflow: recording coverage, post-processing the results, and uploading the results. + +### Recording coverage + +Glean testing coverage is enabled by setting the `GLEAN_TEST_COVERAGE` environment variable to the name of a file to store results. +It is good practice to set it to the absolute path to a file, since some testing harnesses (such as `cargo test`) may change the current working directory. + +```bash +GLEAN_TEST_COVERAGE=$(realpath glean_coverage.txt) make test +``` + +### Post-processing the results + +A post-processing step is required to convert the raw output in the file specified by `GLEAN_TEST_COVERAGE` into usable output for coverage reporting tools. Currently, the only coverage reporting tool supported is [codecov.io](https://codecov.io). + +This post-processor is available in the `coverage` subcommand in the [`glean_parser`](https://github.com/mozilla/glean_parser) tool. + +For some build systems, `glean_parser` is already installed for you by the build system integration at the following locations: + +- On Android/Gradle, `$GRADLE_HOME/glean/bootstrap-4.5.11/Miniconda3/bin/glean_parser` +- On iOS/Carthage, `$PROJECT_ROOT/.venv/bin/glean_parser` +- For other systems, install `glean_parser` using `pip install glean_parser` + +The `glean_parser coverage` command requires the following parameters: + + - `-f`: The output format to produce, for example `codecovio` to produce [codecov.io](https://codecov.io)'s custom format. + - `-o`: The path to the output file, for example `codecov.json`. + - `-c`: The input raw coverage file. `glean_coverage.txt` in the example above. + - A list of the `metrics.yaml` files in your repository. + +For example, to produce output for [codecov.io](https://codecov.io): + +```bash +glean_parser coverage -f codecovio -o glean_coverage.json -c glean_coverage.txt app/metrics.yaml +``` + +In this example, the `glean_coverage.json` file is now ready for uploading to codecov.io. + +### Uploading coverage + +If using `codecov.io`, the uploader doesn't send coverage results for YAML files by default. Pass the `-X yaml` option to the uploader to make sure they are included: + +```bash +bash <(curl -s https://codecov.io/bash) -X yaml +``` diff --git a/glean-core/Cargo.toml b/glean-core/Cargo.toml index 3f6eb1be94..3ddf0cb54e 100644 --- a/glean-core/Cargo.toml +++ b/glean-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "glean-core" -version = "35.0.0" +version = "36.0.0" authors = ["Jan-Erik Rediger ", "The Glean Team "] description = "A modern Telemetry library" repository = "https://github.com/mozilla/glean" @@ -18,7 +18,7 @@ include = [ ] [package.metadata.glean] -glean-parser = "2.2.0" +glean-parser = "2.5.0" [badges] circle-ci = { repository = "mozilla/glean", branch = "main" } @@ -35,6 +35,7 @@ ffi-support = "0.4.0" chrono = { version = "0.4.10", features = ["serde"] } once_cell = "1.4.1" flate2 = "1.0.19" +zeitstempel = "0.1.0" [dev-dependencies] env_logger = { version = "0.7.1", default-features = false, features = ["termcolor", "atty", "humantime"] } diff --git a/glean-core/android/dependency-licenses.xml b/glean-core/android/dependency-licenses.xml index 1f8538fc50..0e7968f3e6 100644 --- a/glean-core/android/dependency-licenses.xml +++ b/glean-core/android/dependency-licenses.xml @@ -212,6 +212,14 @@ the details of which are reproduced below. Apache License 2.0: uuid https://github.com/uuid-rs/uuid + + Apache License 2.0: android_log-sys + https://github.com/nercury/android_log-sys-rs + + + Apache License 2.0: android_logger + https://github.com/Nercury/android_logger-rs + Apache License 2.0: lmdb-rkv-sys https://github.com/mozilla/lmdb-rs.git @@ -268,6 +276,10 @@ the details of which are reproduced below. MIT License: winapi-util https://github.com/BurntSushi/winapi-util + + MIT License: whatsys + https://github.com/badboy/whatsys + Mozilla Public License 2.0: glean https://github.com/mozilla/glean @@ -280,5 +292,9 @@ the details of which are reproduced below. Mozilla Public License 2.0: glean-ffi https://github.com/mozilla/glean + + Mozilla Public License 2.0: zeitstempel + https://github.com/badboy/whatsys + diff --git a/glean-core/android/src/main/java/mozilla/telemetry/glean/private/EventMetricType.kt b/glean-core/android/src/main/java/mozilla/telemetry/glean/private/EventMetricType.kt index f15d3bd9fd..6582565e6c 100644 --- a/glean-core/android/src/main/java/mozilla/telemetry/glean/private/EventMetricType.kt +++ b/glean-core/android/src/main/java/mozilla/telemetry/glean/private/EventMetricType.kt @@ -4,7 +4,6 @@ package mozilla.telemetry.glean.private -import android.os.SystemClock import androidx.annotation.VisibleForTesting import com.sun.jna.StringArray import mozilla.telemetry.glean.Dispatchers @@ -112,7 +111,7 @@ class EventMetricType> internal constructor( // We capture the event time now, since we don't know when the async code below // might get executed. - val timestamp = SystemClock.elapsedRealtime() + val timestamp = LibGleanFFI.INSTANCE.glean_get_timestamp_ms() @Suppress("EXPERIMENTAL_API_USAGE") Dispatchers.API.launch { diff --git a/glean-core/android/src/main/java/mozilla/telemetry/glean/private/PingType.kt b/glean-core/android/src/main/java/mozilla/telemetry/glean/private/PingType.kt index c41f8d6d0e..6af4a455f7 100644 --- a/glean-core/android/src/main/java/mozilla/telemetry/glean/private/PingType.kt +++ b/glean-core/android/src/main/java/mozilla/telemetry/glean/private/PingType.kt @@ -5,7 +5,9 @@ package mozilla.telemetry.glean.private import com.sun.jna.StringArray +import androidx.annotation.VisibleForTesting import mozilla.telemetry.glean.Glean +import mozilla.telemetry.glean.Dispatchers import mozilla.telemetry.glean.rust.LibGleanFFI import mozilla.telemetry.glean.rust.toByte @@ -47,6 +49,7 @@ class PingType> ( sendIfEmpty: Boolean, val reasonCodes: List ) : PingTypeBase(name) { + private var testCallback: ((ReasonCodesEnum?) -> Unit)? = null init { val ffiReasonList = StringArray(reasonCodes.toTypedArray(), "utf-8") @@ -61,6 +64,24 @@ class PingType> ( Glean.registerPingType(this) } + /** + * **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`). + */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + @Synchronized + fun testBeforeNextSubmit(cb: (ReasonCodesEnum?) -> Unit) { + @Suppress("EXPERIMENTAL_API_USAGE") + Dispatchers.API.assertInTestingMode() + this.testCallback = cb + } + /** * Collect and submit the ping for eventual upload. * @@ -74,6 +95,11 @@ class PingType> ( */ @JvmOverloads fun submit(reason: ReasonCodesEnum? = null) { + this.testCallback?.let { + it(reason) + } + this.testCallback = null + val reasonString = reason?.let { this.reasonCodes[it.ordinal] } Glean.submitPing(this, reasonString) } diff --git a/glean-core/android/src/main/java/mozilla/telemetry/glean/rust/LibGleanFFI.kt b/glean-core/android/src/main/java/mozilla/telemetry/glean/rust/LibGleanFFI.kt index 6a9943cb99..9682f51153 100644 --- a/glean-core/android/src/main/java/mozilla/telemetry/glean/rust/LibGleanFFI.kt +++ b/glean-core/android/src/main/java/mozilla/telemetry/glean/rust/LibGleanFFI.kt @@ -610,5 +610,7 @@ internal interface LibGleanFFI : Library { // Misc + fun glean_get_timestamp_ms(): Long + fun glean_str_free(ptr: Pointer) } diff --git a/glean-core/android/src/test/java/mozilla/telemetry/glean/private/EventMetricTypeTest.kt b/glean-core/android/src/test/java/mozilla/telemetry/glean/private/EventMetricTypeTest.kt index e5e5c8fbc6..89b3deb320 100644 --- a/glean-core/android/src/test/java/mozilla/telemetry/glean/private/EventMetricTypeTest.kt +++ b/glean-core/android/src/test/java/mozilla/telemetry/glean/private/EventMetricTypeTest.kt @@ -94,8 +94,9 @@ class EventMetricTypeTest { assertEquals("click", secondEvent.name) assertEquals("bar", secondEvent.extra?.get("other")) - assertTrue("The sequence of the events must be preserved", - firstEvent.timestamp < secondEvent.timestamp) + assertTrue("The sequence of the events must be preserved" + + ", first: ${firstEvent.timestamp}, second: ${secondEvent.timestamp}", + firstEvent.timestamp <= secondEvent.timestamp) } @Test @@ -129,8 +130,9 @@ class EventMetricTypeTest { val secondEvent = snapshot.single { e -> e.extra?.get("object_id") == "buttonB" } assertEquals("click", secondEvent.name) - assertTrue("The sequence of the events must be preserved", - firstEvent.timestamp < secondEvent.timestamp) + assertTrue("The sequence of the events must be preserved" + + ", first: ${firstEvent.timestamp}, second: ${secondEvent.timestamp}", + firstEvent.timestamp <= secondEvent.timestamp) } @Test @@ -198,8 +200,9 @@ class EventMetricTypeTest { assertEquals("ui", secondEvent.category) assertEquals("click", secondEvent.name) - assertTrue("The sequence of the events must be preserved", - firstEvent.timestamp < secondEvent.timestamp) + assertTrue("The sequence of the events must be preserved" + + ", first: ${firstEvent.timestamp}, second: ${secondEvent.timestamp}", + firstEvent.timestamp <= secondEvent.timestamp) } @Test diff --git a/glean-core/android/src/test/java/mozilla/telemetry/glean/private/PingTypeTest.kt b/glean-core/android/src/test/java/mozilla/telemetry/glean/private/PingTypeTest.kt index 6b223d1a5e..e6ab703bd7 100644 --- a/glean-core/android/src/test/java/mozilla/telemetry/glean/private/PingTypeTest.kt +++ b/glean-core/android/src/test/java/mozilla/telemetry/glean/private/PingTypeTest.kt @@ -62,7 +62,16 @@ class PingTypeTest { counter.add() assertTrue(counter.testHasValue()) + var callbackWasCalled = false + customPing.testBeforeNextSubmit { reason -> + assertNull(reason) + assertEquals(1, counter.testGetValue()) + callbackWasCalled = true + } + customPing.submit() + assertTrue(callbackWasCalled) + // Trigger worker task to upload the pings in the background triggerWorkManager(context) diff --git a/glean-core/csharp/Glean/Glean.csproj b/glean-core/csharp/Glean/Glean.csproj index 3eb70d1ae7..e020162959 100644 --- a/glean-core/csharp/Glean/Glean.csproj +++ b/glean-core/csharp/Glean/Glean.csproj @@ -10,7 +10,7 @@ While we're still testing, mark this as a pre-release package. See https://docs.microsoft.com/en-us/nuget/concepts/package-versioning#pre-release-versions --> - 35.0.0 + 36.0.0 Mozilla.Glean Mozilla.Telemetry.Glean https://github.com/mozilla/glean/ diff --git a/glean-core/csharp/Glean/GleanParser.cs b/glean-core/csharp/Glean/GleanParser.cs index ab44123c2a..648fd064bc 100644 --- a/glean-core/csharp/Glean/GleanParser.cs +++ b/glean-core/csharp/Glean/GleanParser.cs @@ -18,7 +18,7 @@ public class GleanParser : ToolTask private const string DefaultVirtualEnvDir = ".venv"; // The glean_parser pypi package version - private const string GleanParserVersion = "2.2.0"; + private const string GleanParserVersion = "2.5.0"; // This script runs a given Python module as a "main" module, like // `python -m module`. However, it first checks that the installed diff --git a/glean-core/ffi/Cargo.toml b/glean-core/ffi/Cargo.toml index c3f76bfde7..9a0bc4a0b4 100644 --- a/glean-core/ffi/Cargo.toml +++ b/glean-core/ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "glean-ffi" -version = "35.0.0" +version = "36.0.0" authors = ["Jan-Erik Rediger ", "The Glean Team "] description = "FFI layer for Glean, a modern Telemetry library" repository = "https://github.com/mozilla/glean" @@ -36,7 +36,7 @@ once_cell = "1.2.0" [dependencies.glean-core] path = ".." -version = "35.0.0" +version = "36.0.0" [target.'cfg(target_os = "android")'.dependencies] android_logger = { version = "0.9.0", default-features = false } diff --git a/glean-core/ffi/glean.h b/glean-core/ffi/glean.h index 71db64920c..3992d1616b 100644 --- a/glean-core/ffi/glean.h +++ b/glean-core/ffi/glean.h @@ -412,6 +412,8 @@ void glean_set_log_pings(uint8_t value); uint8_t glean_set_source_tags(RawStringArray raw_tags, int32_t tags_count); +uint64_t glean_get_timestamp_ms(void); + /** * Public destructor for strings managed by the other side of the FFI. * diff --git a/glean-core/ffi/src/lib.rs b/glean-core/ffi/src/lib.rs index 5396efcc85..f1f4df4e59 100644 --- a/glean-core/ffi/src/lib.rs +++ b/glean-core/ffi/src/lib.rs @@ -530,4 +530,9 @@ pub extern "C" fn glean_set_source_tags(raw_tags: RawStringArray, tags_count: i3 }) } +#[no_mangle] +pub extern "C" fn glean_get_timestamp_ms() -> u64 { + glean_core::get_timestamp_ms() +} + define_string_destructor!(glean_str_free); diff --git a/glean-core/ios/Glean.xcodeproj/project.pbxproj b/glean-core/ios/Glean.xcodeproj/project.pbxproj index 1352df2853..8c50559073 100644 --- a/glean-core/ios/Glean.xcodeproj/project.pbxproj +++ b/glean-core/ios/Glean.xcodeproj/project.pbxproj @@ -523,7 +523,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "bash $PWD/../../build-scripts/xc-universal-binary.sh glean-ffi $PWD/../..\n"; + shellScript = "bash $PWD/../../build-scripts/xc-universal-binary.sh glean-ffi $PWD/../.. $buildvariant\n"; }; BFB59A9723429FC000F40CA8 /* Run Glean SDK generator */ = { isa = PBXShellScriptBuildPhase; @@ -766,7 +766,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Glean/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -775,6 +774,9 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "../../target/aarch64-apple-ios/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "../../target/aarch64-apple-ios-sim/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "../../target/x86_64-apple-ios/debug"; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.Glean; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; @@ -800,7 +802,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Glean/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -809,6 +810,9 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "../../target/aarch64-apple-ios/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "../../target/aarch64-apple-ios-sim/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "../../target/x86_64-apple-ios/release"; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.Glean; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; diff --git a/glean-core/ios/Glean/GleanFfi.h b/glean-core/ios/Glean/GleanFfi.h index 71db64920c..3992d1616b 100644 --- a/glean-core/ios/Glean/GleanFfi.h +++ b/glean-core/ios/Glean/GleanFfi.h @@ -412,6 +412,8 @@ void glean_set_log_pings(uint8_t value); uint8_t glean_set_source_tags(RawStringArray raw_tags, int32_t tags_count); +uint64_t glean_get_timestamp_ms(void); + /** * Public destructor for strings managed by the other side of the FFI. * diff --git a/glean-core/ios/Glean/Metrics/EventMetric.swift b/glean-core/ios/Glean/Metrics/EventMetric.swift index dd306b8794..f8c93d18bb 100644 --- a/glean-core/ios/Glean/Metrics/EventMetric.swift +++ b/glean-core/ios/Glean/Metrics/EventMetric.swift @@ -101,7 +101,7 @@ public class EventMetricType { // We capture the event time now, since we don't know when the async code below // might get executed. - let timestamp = timestampNanos() + let timestamp = glean_get_timestamp_ms() Dispatchers.shared.launchAPI { // The map is sent over FFI as a pair of arrays, one containing the diff --git a/glean-core/ios/Glean/Metrics/Ping.swift b/glean-core/ios/Glean/Metrics/Ping.swift index b9a8b419a0..84e9af4baf 100644 --- a/glean-core/ios/Glean/Metrics/Ping.swift +++ b/glean-core/ios/Glean/Metrics/Ping.swift @@ -45,6 +45,7 @@ public class PingBase { public class Ping: PingBase { let includeClientId: Bool let reasonCodes: [String] + var testCallback: ((ReasonCodesEnum?) throws -> Void)? /// The public constructor used by automatically generated metrics. public init(name: String, includeClientId: Bool, sendIfEmpty: Bool, reasonCodes: [String]) { @@ -71,6 +72,19 @@ public class Ping: PingBase { } } + /// **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`). + public func testBeforeNextSubmit(cb: @escaping (ReasonCodesEnum?) throws -> Void) { + Dispatchers.shared.assertInTestingMode() + self.testCallback = cb + } + /// Collect and submit the ping for eventual uploading. /// /// While the collection of metrics into pings happens synchronously, the @@ -82,6 +96,15 @@ public class Ping: PingBase { /// - parameters: /// * reason: The reason the ping is being submitted. public func submit(reason: ReasonCodesEnum? = nil) { + if let cb = self.testCallback { + do { + try cb(reason) + } catch { + assert(false, "Callback threw before submitting ping \(name).") + } + self.testCallback = nil + } + var reasonString: String? if reason != nil { reasonString = self.reasonCodes[reason!.index()] diff --git a/glean-core/ios/Glean/Utils/Utils.swift b/glean-core/ios/Glean/Utils/Utils.swift index 7fd2af9c73..70cf586db2 100644 --- a/glean-core/ios/Glean/Utils/Utils.swift +++ b/glean-core/ios/Glean/Utils/Utils.swift @@ -21,7 +21,7 @@ extension UInt8 { /// Turn a string into an error, so that it can be thrown as an exception. /// /// This should only be used in tests. -extension String: Error { +extension String: Swift.Error { /// The string itself is the error description. public var errorDescription: String? { return self } } diff --git a/glean-core/ios/GleanTests/Metrics/EventMetricTests.swift b/glean-core/ios/GleanTests/Metrics/EventMetricTests.swift index 05557fce8d..1ca51c85b0 100644 --- a/glean-core/ios/GleanTests/Metrics/EventMetricTests.swift +++ b/glean-core/ios/GleanTests/Metrics/EventMetricTests.swift @@ -98,7 +98,7 @@ class EventMetricTypeTests: XCTestCase { XCTAssertEqual("buttonB", events[1].extra?["object_id"]) XCTAssertEqual("bar", events[1].extra?["other"]) - XCTAssert(events[0].timestamp < events[1].timestamp, "The sequence of events must be preserved") + XCTAssertLessThanOrEqual(events[0].timestamp, events[1].timestamp, "The sequence of events must be preserved") } func testEventRecordedWithEmptyCategory() { @@ -125,7 +125,8 @@ class EventMetricTypeTests: XCTestCase { XCTAssertEqual("click", events[0].identifier) XCTAssertEqual("click", events[1].identifier) - XCTAssert(events[0].timestamp < events[1].timestamp, "The sequence of events must be preserved") + + XCTAssertLessThanOrEqual(events[0].timestamp, events[1].timestamp, "The sequence of events must be preserved") } func testEventNotRecordedWhenDisabled() { @@ -186,7 +187,7 @@ class EventMetricTypeTests: XCTestCase { XCTAssertEqual("ui", events[1].category) XCTAssertEqual("click", events[1].name) - XCTAssert(events[0].timestamp < events[1].timestamp, "The sequence of events must be preserved") + XCTAssertLessThanOrEqual(events[0].timestamp, events[1].timestamp, "The sequence of events must be preserved") } func testEventNotRecordWhenUploadDisabled() { diff --git a/glean-core/ios/GleanTests/Metrics/PingTests.swift b/glean-core/ios/GleanTests/Metrics/PingTests.swift index a1c768915f..34d69d61d3 100644 --- a/glean-core/ios/GleanTests/Metrics/PingTests.swift +++ b/glean-core/ios/GleanTests/Metrics/PingTests.swift @@ -57,7 +57,15 @@ class PingTests: XCTestCase { counter.add() XCTAssert(counter.testHasValue()) + var callbackWasCalled = false + customPing.testBeforeNextSubmit { reason in + XCTAssertNil(reason, "Unexpected reason for custom ping submitted") + XCTAssertEqual(1, try counter.testGetValue(), "Unexpected value for counter in custom ping") + callbackWasCalled = true + } + customPing.submit() + XCTAssert(callbackWasCalled, "Expected callback to be called by now.") waitForExpectations(timeout: 5.0) { error in XCTAssertNil(error, "Test timed out waiting for upload: \(error!)") diff --git a/glean-core/ios/base.xcconfig b/glean-core/ios/base.xcconfig index 1de87f564a..54ed25f70a 100644 --- a/glean-core/ios/base.xcconfig +++ b/glean-core/ios/base.xcconfig @@ -1,4 +1,3 @@ #include "../../xcconfig/common.xcconfig" INFOPLIST_FILE = Glean/Info.plist -LIBRARY_SEARCH_PATHS = "../../target/universal/$(buildvariant)" diff --git a/glean-core/ios/sdk_generator.sh b/glean-core/ios/sdk_generator.sh index ab6d68ab9f..aed5b05f44 100755 --- a/glean-core/ios/sdk_generator.sh +++ b/glean-core/ios/sdk_generator.sh @@ -25,7 +25,7 @@ set -e -GLEAN_PARSER_VERSION=2.2.0 +GLEAN_PARSER_VERSION=2.5.0 # CMDNAME is used in the usage text below. # shellcheck disable=SC2034 diff --git a/glean-core/python/glean/__init__.py b/glean-core/python/glean/__init__.py index 7bc5eba2fb..6798de54cd 100644 --- a/glean-core/python/glean/__init__.py +++ b/glean-core/python/glean/__init__.py @@ -30,7 +30,7 @@ __email__ = "glean-team@mozilla.com" -GLEAN_PARSER_VERSION = "2.2.0" +GLEAN_PARSER_VERSION = "2.5.0" if glean_parser.__version__ != GLEAN_PARSER_VERSION: diff --git a/glean-core/python/glean/_util.py b/glean-core/python/glean/_util.py index 90aa301fbb..4ee6861b59 100644 --- a/glean-core/python/glean/_util.py +++ b/glean-core/python/glean/_util.py @@ -38,23 +38,11 @@ def get_locale_tag() -> str: if sys.version_info >= (3, 7): - def time_ms() -> int: - """ - Get time from a monotonic timer in milliseconds. - """ - return int(time.monotonic_ns() / 1000000.0) - time_ns = time.monotonic_ns else: - def time_ms() -> int: - """ - Get time from a monotonic timer in milliseconds. - """ - return int(time.monotonic() * 1000.0) - def time_ns() -> int: """ Get time from a monotonic timer in nanoseconds. diff --git a/glean-core/python/glean/metrics/event.py b/glean-core/python/glean/metrics/event.py index 662ea24f8b..3d2069c65d 100644 --- a/glean-core/python/glean/metrics/event.py +++ b/glean-core/python/glean/metrics/event.py @@ -10,7 +10,6 @@ from .. import _ffi from .._dispatcher import Dispatcher from ..testing import ErrorType -from .. import _util from .lifetime import Lifetime @@ -125,7 +124,7 @@ def record(self, extra: Optional[Dict[int, str]] = None) -> None: if self._disabled: return - timestamp = _util.time_ms() + timestamp = _ffi.lib.glean_get_timestamp_ms() @Dispatcher.launch def record(): diff --git a/glean-core/python/glean/metrics/ping.py b/glean-core/python/glean/metrics/ping.py index 3638dbe7df..48c8e66b9c 100644 --- a/glean-core/python/glean/metrics/ping.py +++ b/glean-core/python/glean/metrics/ping.py @@ -3,10 +3,11 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. -from typing import List, Optional +from typing import Callable, List, Optional from ..glean import Glean +from .._dispatcher import Dispatcher from .. import _ffi @@ -33,6 +34,7 @@ def __init__( _ffi.ffi_encode_vec_string(reason_codes), len(reason_codes), ) + self._test_callback = None # type: Optional[Callable[[Optional[str]], None]] Glean.register_ping_type(self) def __del__(self): @@ -46,6 +48,20 @@ def name(self) -> str: """ return self._name + def test_before_next_submit(self, cb: Callable[[Optional[str]], None]): + """ + **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`). + """ + assert Dispatcher._testing_mode is True + self._test_callback = cb + def submit(self, reason: Optional[int] = None) -> None: """ Collect and submit the ping for eventual uploading. @@ -60,4 +76,9 @@ def submit(self, reason: Optional[int] = None) -> None: reason_string = self._reason_codes[reason] else: reason_string = None + + if self._test_callback is not None: + self._test_callback(reason_string) + self._test_callback = None + Glean._submit_ping(self, reason_string) diff --git a/glean-core/python/requirements_dev.txt b/glean-core/python/requirements_dev.txt index c0c8fee24c..af981a4167 100644 --- a/glean-core/python/requirements_dev.txt +++ b/glean-core/python/requirements_dev.txt @@ -1,9 +1,9 @@ auditwheel==3.3.1 black==20.8b1 cffi==1.14.5 -coverage==5.4 -flake8==3.8.4 -flake8-bugbear==20.11.1 +coverage==5.5 +flake8==3.9.0 +flake8-bugbear==21.3.2 jsonschema==3.2.0 mypy==0.812 pdoc3==0.9.2 @@ -12,5 +12,5 @@ pytest-localserver==0.5.0 pytest-runner==5.3.0 pytest==6.2.2 setuptools-git==1.2 -twine==3.3.0 +twine==3.4.0 wheel==0.36.1 diff --git a/glean-core/python/setup.py b/glean-core/python/setup.py index 989fb58c13..a6213339f2 100644 --- a/glean-core/python/setup.py +++ b/glean-core/python/setup.py @@ -34,18 +34,6 @@ sys.dont_write_bytecode = True -platform = sys.platform - -if os.environ.get("GLEAN_PYTHON_MINGW_I686_BUILD"): - mingw_arch = "i686" -elif os.environ.get("GLEAN_PYTHON_MINGW_X86_64_BUILD"): - mingw_arch = "x86_64" -else: - mingw_arch = None - -if mingw_arch is not None: - platform = "windows" - if sys.version_info < (3, 6): print("glean requires at least Python 3.6", file=sys.stderr) sys.exit(1) @@ -68,37 +56,19 @@ history = history_file.read() # glean version. Automatically updated by the bin/prepare_release.sh script -version = "35.0.0" +version = "36.0.0" requirements = [ - "cffi>=1", - "glean_parser==2.2.0", + "cffi>=1.13.0", + "glean_parser==2.5.0", "iso8601>=0.1.10; python_version<='3.6'", ] -setup_requirements = ["cffi>=1.0.0"] +setup_requirements = ["cffi>=1.13.0"] # The environment variable `GLEAN_BUILD_VARIANT` can be set to `debug` or `release` buildvariant = os.environ.get("GLEAN_BUILD_VARIANT", "debug") -if mingw_arch == "i686": - shared_object_build_dir = SRC_ROOT / "target" / "i686-pc-windows-gnu" -elif mingw_arch == "x86_64": - shared_object_build_dir = SRC_ROOT / "target" / "x86_64-pc-windows-gnu" -else: - shared_object_build_dir = SRC_ROOT / "target" - - -if platform == "darwin": - shared_object = "libglean_ffi.dylib" -elif platform.startswith("win"): - # `platform` can be both "windows" (if running within MinGW) or "win32" - # if running in a standard Python environment. Account for both. - shared_object = "glean_ffi.dll" -else: - # Anything else must be an ELF platform - Linux, *BSD, Solaris/illumos - shared_object = "libglean_ffi.so" - class BinaryDistribution(Distribution): def is_pure(self): @@ -108,6 +78,12 @@ def has_ext_modules(self): return True +def macos_compat(target): + if target.startswith("aarch64-"): + return "11.0" + return "10.7" + + # The logic for specifying wheel tags in setuptools/wheel is very complex, hard # to override, and is really meant for extensions that are compiled against # libpython.so, not this case where we have a fairly portable Rust-compiled @@ -116,20 +92,26 @@ def has_ext_modules(self): # simple that only handles the cases we need. class bdist_wheel(wheel.bdist_wheel.bdist_wheel): def get_tag(self): - if platform == "linux": - return ("cp36", "abi3", "linux_x86_64") - elif platform == "darwin": - return ("cp36", "abi3", "macosx_10_7_x86_64") - elif platform == "windows": - if mingw_arch == "i686": - return ("py3", "none", "win32") - elif mingw_arch == "x86_64": - return ("py3", "none", "win_amd64") + cpu, _, __ = target.partition("-") + impl, abi_tag = "cp36", "abi3" + if "-linux" in target: + plat_name = f"linux_{cpu}" + elif "-darwin" in target: + compat = macos_compat(target).replace(".", "_") + plat_name = f"macosx_{compat}_{cpu}" + elif "-windows" in target: + impl, abi_tag = "py3", "none" + if cpu == "i686": + plat_name = "win32" + elif cpu == "x86_64": + plat_name = "win_amd64" else: raise ValueError("Unsupported Windows platform") else: # Keep local wheel build on BSD/etc. working - return super().get_tag() + _, __, plat_name = super().get_tag() + + return (impl, abi_tag, plat_name) class InstallPlatlib(install): @@ -139,17 +121,17 @@ def finalize_options(self): self.install_lib = self.install_platlib -def get_rustc_config(): +def get_rustc_info(): """ - Get the rustc configuration values from `rustc --print cfg`, parsed into a + Get the rustc info from `rustc --version --verbose`, parsed into a dictionary. """ - regex = re.compile(r"(?P[^=]+)(=\"(?P\S+?)\")?") + regex = re.compile(r"(?P[^:]+)(: *(?P\S+))") - output = subprocess.check_output(["rustc", "--print", "cfg"]).decode("utf-8") + output = subprocess.check_output(["rustc", "--version", "--verbose"]) data = {} - for line in output.splitlines(): + for line in output.decode("utf-8").splitlines(): match = regex.match(line) if match: d = match.groupdict() @@ -158,6 +140,20 @@ def get_rustc_config(): return data +target = os.environ.get("GLEAN_BUILD_TARGET") +if not target: + target = get_rustc_info()["host"] + + +if "-darwin" in target: + shared_object = "libglean_ffi.dylib" +elif "-windows" in target: + shared_object = "glean_ffi.dll" +else: + # Anything else must be an ELF platform - Linux, *BSD, Solaris/illumos + shared_object = "libglean_ffi.so" + + class build(_build): def run(self): try: @@ -171,22 +167,26 @@ def run(self): sys.exit(1) env = os.environ.copy() - config = get_rustc_config() # For `musl`-based targets (e.g. Alpine Linux), we need to set a flag # to produce a shared object Python extension. - if config.get("target_env") == "musl": + if "-musl" in target: env["RUSTFLAGS"] = ( env.get("RUSTFLAGS", "") + " -C target-feature=-crt-static" ) + if target == "i686-pc-windows-gnu": + env["RUSTFLAGS"] = env.get("RUSTFLAGS", "") + " -C panic=abort" - command = ["cargo", "build", "--package", "glean-ffi"] + command = ["cargo", "build", "--package", "glean-ffi", "--target", target] if buildvariant != "debug": command.append(f"--{buildvariant}") + if "-darwin" in target: + env["MACOSX_DEPLOYMENT_TARGET"] = macos_compat(target) + subprocess.run(command, cwd=SRC_ROOT, env=env) shutil.copyfile( - shared_object_build_dir / buildvariant / shared_object, + SRC_ROOT / "target" / target / buildvariant / shared_object, PYTHON_ROOT / "glean" / shared_object, ) diff --git a/glean-core/python/tests/test_glean.py b/glean-core/python/tests/test_glean.py index e7faa27227..6a4d1893bf 100644 --- a/glean-core/python/tests/test_glean.py +++ b/glean-core/python/tests/test_glean.py @@ -877,3 +877,37 @@ def test_client_activity_api(tmpdir, monkeypatch): assert "baseline" == url_path.split("/")[3] assert payload["ping_info"]["reason"] == "active" assert "timespan" not in payload["metrics"] + + +def test_sending_of_custom_pings(safe_httpserver): + safe_httpserver.serve_content(b"", code=200) + Glean._configuration.server_endpoint = safe_httpserver.url + + counter_metric = CounterMetricType( + disabled=False, + category="telemetry", + lifetime=Lifetime.APPLICATION, + name="counter_metric", + send_in_pings=["store1"], + ) + + custom_ping = PingType( + name="store1", include_client_id=True, send_if_empty=False, reason_codes=[] + ) + + counter_metric.add() + + # Need a mutable object and plain booleans are not. + callback_was_called = [False] + + def check_custom_ping(reason): + assert reason is None + assert 1 == counter_metric.test_get_value() + callback_was_called[0] = True + + custom_ping.test_before_next_submit(check_custom_ping) + custom_ping.submit() + + assert callback_was_called[0] + + assert 1 == len(safe_httpserver.requests) diff --git a/glean-core/rlb/Cargo.toml b/glean-core/rlb/Cargo.toml index 79f203ddd7..8980238466 100644 --- a/glean-core/rlb/Cargo.toml +++ b/glean-core/rlb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "glean" -version = "35.0.0" +version = "36.0.0" authors = ["Jan-Erik Rediger ", "The Glean Team "] description = "Glean SDK Rust language bindings" repository = "https://github.com/mozilla/glean" @@ -22,7 +22,7 @@ maintenance = { status = "actively-developed" } [dependencies.glean-core] path = ".." -version = "35.0.0" +version = "36.0.0" [dependencies] crossbeam-channel = "0.5" @@ -35,6 +35,7 @@ serde = { version = "1.0.104", features = ["derive"] } uuid = { version = "0.8.1", features = ["v4"] } chrono = { version = "0.4.10", features = ["serde"] } time = "0.1.40" +whatsys = "0.1.2" [dev-dependencies] env_logger = { version = "0.7.1", default-features = false, features = ["termcolor", "atty", "humantime"] } diff --git a/glean-core/rlb/src/lib.rs b/glean-core/rlb/src/lib.rs index baeaf773a9..dabb45c1d3 100644 --- a/glean-core/rlb/src/lib.rs +++ b/glean-core/rlb/src/lib.rs @@ -434,7 +434,7 @@ fn initialize_core_metrics( if let Some(app_channel) = channel { core_metrics::internal_metrics::app_channel.set_sync(glean, app_channel); } - core_metrics::internal_metrics::os_version.set_sync(glean, "unknown".to_string()); + core_metrics::internal_metrics::os_version.set_sync(glean, system::get_os_version()); core_metrics::internal_metrics::architecture.set_sync(glean, system::ARCH.to_string()); core_metrics::internal_metrics::device_manufacturer.set_sync(glean, "unknown".to_string()); core_metrics::internal_metrics::device_model.set_sync(glean, "unknown".to_string()); @@ -760,5 +760,10 @@ pub fn set_source_tags(tags: Vec) -> bool { } } +/// Returns a timestamp corresponding to "now" with millisecond precision. +pub fn get_timestamp_ms() -> u64 { + glean_core::get_timestamp_ms() +} + #[cfg(test)] mod test; diff --git a/glean-core/rlb/src/private/denominator.rs b/glean-core/rlb/src/private/denominator.rs new file mode 100644 index 0000000000..2f6c385648 --- /dev/null +++ b/glean-core/rlb/src/private/denominator.rs @@ -0,0 +1,64 @@ +// 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::sync::Arc; + +use glean_core::metrics::MetricType; +use glean_core::CommonMetricData; +use glean_core::ErrorType; + +// 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 counter metrics that are acting as +/// external denominators for rate 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 DenominatorMetric(pub(crate) Arc); + +impl DenominatorMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: CommonMetricData, numerators: Vec) -> Self { + Self(Arc::new(glean_core::metrics::DenominatorMetric::new( + meta, numerators, + ))) + } +} + +#[inherent(pub)] +impl glean_core::traits::Counter for DenominatorMetric { + fn add(&self, amount: i32) { + let metric = Arc::clone(&self.0); + crate::launch_with_glean(move |glean| metric.add(glean, amount)); + } + + fn test_get_value<'a, S: Into>>(&self, ping_name: S) -> Option { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} diff --git a/glean-core/rlb/src/private/event.rs b/glean-core/rlb/src/private/event.rs index 946ec5b1d7..c7621ad357 100644 --- a/glean-core/rlb/src/private/event.rs +++ b/glean-core/rlb/src/private/event.rs @@ -41,6 +41,15 @@ impl EventMetric { 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) { + let metric = Arc::clone(&self.inner); + crate::launch_with_glean(move |glean| metric.record(glean, timestamp, Some(extra))); + } } #[inherent(pub)] @@ -48,8 +57,7 @@ impl traits::Event for EventMetric { type Extra = K; fn record::Extra, String>>>>(&self, extra: M) { - const NANOS_PER_MILLI: u64 = 1_000_000; - let now = time::precise_time_ns() / NANOS_PER_MILLI; + let now = crate::get_timestamp_ms(); // Translate from [ExtraKey -> String] to a [Int -> String] map let extra = extra diff --git a/glean-core/rlb/src/private/mod.rs b/glean-core/rlb/src/private/mod.rs index c4b692072b..05e4057048 100644 --- a/glean-core/rlb/src/private/mod.rs +++ b/glean-core/rlb/src/private/mod.rs @@ -8,11 +8,14 @@ mod boolean; mod counter; mod custom_distribution; mod datetime; +mod denominator; mod event; mod labeled; mod memory_distribution; +mod numerator; mod ping; mod quantity; +mod rate; mod recorded_experiment_data; mod string; mod string_list; @@ -25,11 +28,14 @@ pub use boolean::BooleanMetric; pub use counter::CounterMetric; pub use custom_distribution::CustomDistributionMetric; pub use datetime::{Datetime, DatetimeMetric}; +pub use denominator::DenominatorMetric; pub use event::EventMetric; pub use labeled::{AllowLabeled, LabeledMetric}; pub use memory_distribution::MemoryDistributionMetric; +pub use numerator::NumeratorMetric; pub use ping::PingType; pub use quantity::QuantityMetric; +pub use rate::RateMetric; pub use recorded_experiment_data::RecordedExperimentData; pub use string::StringMetric; pub use string_list::StringListMetric; diff --git a/glean-core/rlb/src/private/numerator.rs b/glean-core/rlb/src/private/numerator.rs new file mode 100644 index 0000000000..2835549622 --- /dev/null +++ b/glean-core/rlb/src/private/numerator.rs @@ -0,0 +1,108 @@ +// 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::sync::Arc; + +use glean_core::metrics::MetricType; +use glean_core::ErrorType; + +use crate::dispatcher; + +// 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 rate metrics with external denominators. +/// +/// 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 NumeratorMetric(pub(crate) Arc); + +impl NumeratorMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + Self(Arc::new(glean_core::metrics::RateMetric::new(meta))) + } +} + +#[inherent(pub)] +impl glean_core::traits::Numerator for NumeratorMetric { + fn add_to_numerator(&self, amount: i32) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || { + crate::with_glean(|glean| metric.add_to_numerator(glean, amount)) + }); + } + + fn test_get_value<'a, S: Into>>(&self, ping_name: S) -> Option<(i32, i32)> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common_test::{lock_test, new_glean}; + use crate::CommonMetricData; + + #[test] + fn numerator_smoke() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + let metric: NumeratorMetric = NumeratorMetric::new(CommonMetricData { + name: "rate".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }); + + // Adding 0 doesn't error. + metric.add_to_numerator(0); + + assert_eq!( + metric.test_get_num_recorded_errors(ErrorType::InvalidValue, None), + 0 + ); + + // Adding a negative value errors. + metric.add_to_numerator(-1); + + assert_eq!( + metric.test_get_num_recorded_errors(ErrorType::InvalidValue, None), + 1 + ); + + // Getting the value returns 0s if that's all we have. + assert_eq!(metric.test_get_value(None), Some((0, 0))); + + // And normal values of course work. + metric.add_to_numerator(22); + + assert_eq!(metric.test_get_value(None), Some((22, 0))); + } +} diff --git a/glean-core/rlb/src/private/ping.rs b/glean-core/rlb/src/private/ping.rs index 88559cda40..f136be6ae9 100644 --- a/glean-core/rlb/src/private/ping.rs +++ b/glean-core/rlb/src/private/ping.rs @@ -2,15 +2,24 @@ // 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}; + use inherent::inherent; +type BoxedCallback = Box) + 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, Debug)] +#[derive(Clone)] pub struct PingType { pub(crate) name: String, pub(crate) ping_type: glean_core::metrics::PingType, + + /// **Test-only API** + /// + /// A function to be called right before a ping is submitted. + test_callback: Arc>>, } impl PingType { @@ -36,15 +45,40 @@ impl PingType { reason_codes, ); - let me = Self { name, ping_type }; + let me = Self { + name, + ping_type, + test_callback: Arc::new(Default::default()), + }; crate::register_ping_type(&me); me } + + /// **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); + } } #[inherent(pub)] impl glean_core::traits::Ping for PingType { 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) + } + crate::submit_ping(self, reason) } } diff --git a/glean-core/rlb/src/private/rate.rs b/glean-core/rlb/src/private/rate.rs new file mode 100644 index 0000000000..3e8b694e76 --- /dev/null +++ b/glean-core/rlb/src/private/rate.rs @@ -0,0 +1,118 @@ +// 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::sync::Arc; + +use glean_core::metrics::MetricType; +use glean_core::ErrorType; + +use crate::dispatcher; + +// 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 rate 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 RateMetric(pub(crate) Arc); + +impl RateMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + Self(Arc::new(glean_core::metrics::RateMetric::new(meta))) + } +} + +#[inherent(pub)] +impl glean_core::traits::Rate for RateMetric { + fn add_to_numerator(&self, amount: i32) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || { + crate::with_glean(|glean| metric.add_to_numerator(glean, amount)) + }); + } + + fn add_to_denominator(&self, amount: i32) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || { + crate::with_glean(|glean| metric.add_to_denominator(glean, amount)) + }); + } + + fn test_get_value<'a, S: Into>>(&self, ping_name: S) -> Option<(i32, i32)> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common_test::{lock_test, new_glean}; + use crate::CommonMetricData; + + #[test] + fn rate_smoke() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + let metric: RateMetric = RateMetric::new(CommonMetricData { + name: "rate".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }); + + // Adding 0 doesn't error. + metric.add_to_numerator(0); + metric.add_to_denominator(0); + + assert_eq!( + metric.test_get_num_recorded_errors(ErrorType::InvalidValue, None), + 0 + ); + + // Adding a negative value errors. + metric.add_to_numerator(-1); + metric.add_to_denominator(-1); + + assert_eq!( + metric.test_get_num_recorded_errors(ErrorType::InvalidValue, None), + 2 + ); + + // Getting the value returns 0s if that's all we have. + assert_eq!(metric.test_get_value(None), Some((0, 0))); + + // And normal values of course work. + metric.add_to_numerator(22); + metric.add_to_denominator(7); + + assert_eq!(metric.test_get_value(None), Some((22, 7))); + } +} diff --git a/glean-core/rlb/src/system.rs b/glean-core/rlb/src/system.rs index 5bb7d3c34a..b6b678d062 100644 --- a/glean-core/rlb/src/system.rs +++ b/glean-core/rlb/src/system.rs @@ -53,3 +53,37 @@ pub const ARCH: &str = "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 . 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 = "linux")] +fn parse_linux_os_string(os_str: String) -> String { + os_str.split('.').take(2).collect::>().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/glean-core/rlb/src/test.rs b/glean-core/rlb/src/test.rs index 5f5ccf3ac4..acfccdbe75 100644 --- a/glean-core/rlb/src/test.rs +++ b/glean-core/rlb/src/test.rs @@ -977,3 +977,73 @@ fn registering_pings_before_init_must_work() { 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::(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::HttpStatus(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, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + }; + + 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, + }); + + crate::block_on_dispatcher(); + + 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")); +} diff --git a/glean-core/rlb/tests/schema.rs b/glean-core/rlb/tests/schema.rs index c8cfe9f2c6..a2cf617644 100644 --- a/glean-core/rlb/tests/schema.rs +++ b/glean-core/rlb/tests/schema.rs @@ -8,7 +8,8 @@ use flate2::read::GzDecoder; use jsonschema_valid::{self, schemas::Draft}; use serde_json::Value; -use glean::{ClientInfoMetrics, Configuration}; +use glean::private::{DenominatorMetric, NumeratorMetric, RateMetric}; +use glean::{ClientInfoMetrics, CommonMetricData, Configuration}; const SCHEMA_JSON: &str = include_str!("../../../glean.1.schema.json"); @@ -88,8 +89,62 @@ fn validate_against_schema() { }; let _ = new_glean(Some(cfg)); - // Define a new ping and submit it. const PING_NAME: &str = "test-ping"; + + // 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 rate_metric: RateMetric = RateMetric::new(CommonMetricData { + category: "test".into(), + name: "rate".into(), + send_in_pings: vec![PING_NAME.into()], + ..Default::default() + }); + rate_metric.add_to_numerator(1); + rate_metric.add_to_denominator(1); + + let numerator_metric1: NumeratorMetric = NumeratorMetric::new(CommonMetricData { + category: "test".into(), + name: "num1".into(), + send_in_pings: vec![PING_NAME.into()], + ..Default::default() + }); + let numerator_metric2: NumeratorMetric = NumeratorMetric::new(CommonMetricData { + category: "test".into(), + name: "num2".into(), + send_in_pings: vec![PING_NAME.into()], + ..Default::default() + }); + let denominator_metric: DenominatorMetric = DenominatorMetric::new( + CommonMetricData { + category: "test".into(), + name: "den".into(), + send_in_pings: vec![PING_NAME.into()], + ..Default::default() + }, + vec![ + CommonMetricData { + category: "test".into(), + name: "num1".into(), + send_in_pings: vec![PING_NAME.into()], + ..Default::default() + }, + CommonMetricData { + category: "test".into(), + name: "num2".into(), + send_in_pings: vec![PING_NAME.into()], + ..Default::default() + }, + ], + ); + + numerator_metric1.add_to_numerator(1); + numerator_metric2.add_to_numerator(2); + denominator_metric.add(3); + + // Define a new ping and submit it. let custom_ping = glean::private::PingType::new(PING_NAME, true, true, vec![]); custom_ping.submit(None); diff --git a/glean-core/src/coverage.rs b/glean-core/src/coverage.rs new file mode 100644 index 0000000000..426e6295c8 --- /dev/null +++ b/glean-core/src/coverage.rs @@ -0,0 +1,47 @@ +// 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/. + +//! Utilities for recording when testing APIs have been called on specific +//! metrics. +//! +//! Testing coverage is enabled by setting the GLEAN_TEST_COVERAGE environment +//! variable to the name of an output file. This output file must run through a +//! post-processor (in glean_parser's `coverage` command) to convert to a format +//! understood by third-party coverage reporting tools. +//! +//! While running a unit test suite, Glean records which database keys were +//! accessed by the testing APIs, with one entry per line. Database keys are +//! usually, but not always, the same as metric identifiers, but it is the +//! responsibility of the post-processor to resolve that difference. +//! +//! This functionality has no runtime overhead unless the testing API is used. + +use std::env; +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::sync::Mutex; + +use once_cell::sync::Lazy; + +static COVERAGE_FILE: Lazy>> = Lazy::new(|| { + if let Some(filename) = env::var_os("GLEAN_TEST_COVERAGE") { + match OpenOptions::new().append(true).create(true).open(filename) { + Ok(file) => { + return Some(Mutex::new(file)); + } + Err(err) => { + log::error!("Couldn't open file for coverage results: {:?}", err); + } + } + } + None +}); + +pub(crate) fn record_coverage(metric_id: &str) { + if let Some(file_mutex) = &*COVERAGE_FILE { + let mut file = file_mutex.lock().unwrap(); + writeln!(&mut file, "{}", metric_id).ok(); + file.flush().ok(); + } +} diff --git a/glean-core/src/event_database/mod.rs b/glean-core/src/event_database/mod.rs index 23ff9ca6f1..41d92eb7e0 100644 --- a/glean-core/src/event_database/mod.rs +++ b/glean-core/src/event_database/mod.rs @@ -15,6 +15,7 @@ use std::sync::RwLock; use serde::{Deserialize, Serialize}; use serde_json::{json, Value as JsonValue}; +use crate::coverage::record_coverage; use crate::CommonMetricData; use crate::Glean; use crate::Result; @@ -326,6 +327,8 @@ impl EventDatabase { /// /// This doesn't clear the stored value. pub fn test_has_value<'a>(&'a self, meta: &'a CommonMetricData, store_name: &str) -> bool { + record_coverage(&meta.base_identifier()); + self.event_stores .read() .unwrap() // safe unwrap, only error case is poisoning @@ -346,6 +349,8 @@ impl EventDatabase { meta: &'a CommonMetricData, store_name: &str, ) -> Option> { + record_coverage(&meta.base_identifier()); + let value: Vec = self .event_stores .read() diff --git a/glean-core/src/lib.rs b/glean-core/src/lib.rs index e3e367b305..551b9b267f 100644 --- a/glean-core/src/lib.rs +++ b/glean-core/src/lib.rs @@ -27,6 +27,7 @@ use uuid::Uuid; mod macros; mod common_metric_data; +mod coverage; mod database; mod debug; mod error; @@ -970,8 +971,13 @@ impl Glean { } } +/// Returns a timestamp corresponding to "now" with millisecond precision. +pub fn get_timestamp_ms() -> u64 { + const NANOS_PER_MILLI: u64 = 1_000_000; + zeitstempel::now() / NANOS_PER_MILLI +} + // Split unit tests to a separate file, to reduce the file of this one. #[cfg(test)] -#[cfg(test)] #[path = "lib_unit_tests.rs"] mod tests; diff --git a/glean-core/src/lib_unit_tests.rs b/glean-core/src/lib_unit_tests.rs index 9c741508c8..363dbbd09a 100644 --- a/glean-core/src/lib_unit_tests.rs +++ b/glean-core/src/lib_unit_tests.rs @@ -411,6 +411,7 @@ fn correct_order() { TimingDistribution(Histogram::functional(2.0, 8.0)), MemoryDistribution(Histogram::functional(2.0, 8.0)), Jwe("eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ".into()), + Rate(0, 0), ]; for metric in all_metrics { @@ -436,6 +437,7 @@ fn correct_order() { TimingDistribution(..) => assert_eq!(11, disc), MemoryDistribution(..) => assert_eq!(12, disc), Jwe(..) => assert_eq!(13, disc), + Rate(..) => assert_eq!(14, disc), } } } diff --git a/glean-core/src/metrics/boolean.rs b/glean-core/src/metrics/boolean.rs index b434594781..3d9f358e31 100644 --- a/glean-core/src/metrics/boolean.rs +++ b/glean-core/src/metrics/boolean.rs @@ -57,7 +57,7 @@ impl BooleanMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/counter.rs b/glean-core/src/metrics/counter.rs index af762071be..fa5d5e5b7c 100644 --- a/glean-core/src/metrics/counter.rs +++ b/glean-core/src/metrics/counter.rs @@ -80,7 +80,7 @@ impl CounterMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/custom_distribution.rs b/glean-core/src/metrics/custom_distribution.rs index f015d1cd75..5cc53f24a0 100644 --- a/glean-core/src/metrics/custom_distribution.rs +++ b/glean-core/src/metrics/custom_distribution.rs @@ -158,7 +158,7 @@ impl CustomDistributionMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/datetime.rs b/glean-core/src/metrics/datetime.rs index 7020f9d99e..f054de78c2 100644 --- a/glean-core/src/metrics/datetime.rs +++ b/glean-core/src/metrics/datetime.rs @@ -159,7 +159,7 @@ impl DatetimeMetric { /// /// The stored value or `None` if nothing stored. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), @@ -211,7 +211,7 @@ impl DatetimeMetric { /// /// This doesn't clear the stored value. pub fn test_get_value_as_string(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/denominator.rs b/glean-core/src/metrics/denominator.rs new file mode 100644 index 0000000000..6844147e94 --- /dev/null +++ b/glean-core/src/metrics/denominator.rs @@ -0,0 +1,99 @@ +// 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::error_recording::{record_error, ErrorType}; +use crate::metrics::CounterMetric; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::metrics::RateMetric; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// A Denominator metric (a kind of count shared among Rate metrics). +/// +/// Used to count things. +/// The value can only be incremented, not decremented. +#[derive(Clone, Debug)] +pub struct DenominatorMetric { + counter: CounterMetric, + numerators: Vec, +} + +impl MetricType for DenominatorMetric { + fn meta(&self) -> &CommonMetricData { + self.counter.meta() + } + + fn meta_mut(&mut self) -> &mut CommonMetricData { + self.counter.meta_mut() + } +} + +impl DenominatorMetric { + /// Creates a new denominator metric. + pub fn new(meta: CommonMetricData, numerators: Vec) -> Self { + Self { + counter: CounterMetric::new(meta), + numerators: numerators.into_iter().map(RateMetric::new).collect(), + } + } + + /// Increases the denominator by `amount`. + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + /// * `amount` - The amount to increase by. Should be positive. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is 0 or negative. + pub fn add(&self, glean: &Glean, amount: i32) { + if !self.should_record(glean) { + return; + } + + if amount <= 0 { + record_error( + glean, + self.meta(), + ErrorType::InvalidValue, + format!("Added negative or zero value {}", amount), + None, + ); + return; + } + + for num in &self.numerators { + num.add_to_denominator(glean, amount); + } + + glean + .storage() + .record_with(glean, self.counter.meta(), |old_value| match old_value { + Some(Metric::Counter(old_value)) => { + Metric::Counter(old_value.saturating_add(amount)) + } + _ => Metric::Counter(amount), + }) + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as an integer. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { + match StorageManager.snapshot_metric_for_test( + glean.storage(), + storage_name, + &self.meta().identifier(glean), + self.meta().lifetime, + ) { + Some(Metric::Counter(i)) => Some(i), + _ => None, + } + } +} diff --git a/glean-core/src/metrics/experiment.rs b/glean-core/src/metrics/experiment.rs index 5cf2139b05..382d3ed80b 100644 --- a/glean-core/src/metrics/experiment.rs +++ b/glean-core/src/metrics/experiment.rs @@ -221,7 +221,7 @@ impl ExperimentMetric { /// /// This doesn't clear the stored value. pub fn test_get_value_as_json_string(&self, glean: &Glean) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), INTERNAL_STORAGE, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/jwe.rs b/glean-core/src/metrics/jwe.rs index f054275a59..921ded521c 100644 --- a/glean-core/src/metrics/jwe.rs +++ b/glean-core/src/metrics/jwe.rs @@ -290,7 +290,7 @@ impl JweMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/memory_distribution.rs b/glean-core/src/metrics/memory_distribution.rs index 40687e7dc3..f54aa80166 100644 --- a/glean-core/src/metrics/memory_distribution.rs +++ b/glean-core/src/metrics/memory_distribution.rs @@ -186,7 +186,7 @@ impl MemoryDistributionMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/mod.rs b/glean-core/src/metrics/mod.rs index ca3acac514..00d56bb7e4 100644 --- a/glean-core/src/metrics/mod.rs +++ b/glean-core/src/metrics/mod.rs @@ -14,6 +14,7 @@ mod boolean; mod counter; mod custom_distribution; mod datetime; +mod denominator; mod event; mod experiment; mod jwe; @@ -22,6 +23,7 @@ mod memory_distribution; mod memory_unit; mod ping; mod quantity; +mod rate; mod string; mod string_list; mod time_unit; @@ -40,6 +42,7 @@ pub use self::boolean::BooleanMetric; pub use self::counter::CounterMetric; pub use self::custom_distribution::CustomDistributionMetric; pub use self::datetime::DatetimeMetric; +pub use self::denominator::DenominatorMetric; pub use self::event::EventMetric; pub(crate) use self::experiment::ExperimentMetric; pub use crate::histogram::HistogramType; @@ -55,6 +58,7 @@ pub use self::memory_distribution::MemoryDistributionMetric; pub use self::memory_unit::MemoryUnit; pub use self::ping::PingType; pub use self::quantity::QuantityMetric; +pub use self::rate::RateMetric; pub use self::string::StringMetric; pub use self::string_list::StringListMetric; pub use self::time_unit::TimeUnit; @@ -117,6 +121,8 @@ pub enum Metric { MemoryDistribution(Histogram), /// A JWE metric. See [`JweMetric`] for more information. Jwe(String), + /// A rate metric. See [`RateMetric`] for more information. + Rate(i32, i32), } /// A [`MetricType`] describes common behavior across all metrics. @@ -151,6 +157,7 @@ impl Metric { Metric::Datetime(_, _) => "datetime", Metric::Experiment(_) => panic!("Experiments should not be serialized through this"), Metric::Quantity(_) => "quantity", + Metric::Rate(..) => "rate", Metric::String(_) => "string", Metric::StringList(_) => "string_list", Metric::Timespan(..) => "timespan", @@ -173,6 +180,9 @@ impl Metric { Metric::Datetime(d, time_unit) => json!(get_iso_time_string(*d, *time_unit)), Metric::Experiment(e) => e.as_json(), Metric::Quantity(q) => json!(q), + Metric::Rate(num, den) => { + json!({"numerator": num, "denominator": den}) + } Metric::String(s) => json!(s), Metric::StringList(v) => json!(v), Metric::Timespan(time, time_unit) => { diff --git a/glean-core/src/metrics/quantity.rs b/glean-core/src/metrics/quantity.rs index 128761d4c6..d8e58abae0 100644 --- a/glean-core/src/metrics/quantity.rs +++ b/glean-core/src/metrics/quantity.rs @@ -74,7 +74,7 @@ impl QuantityMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/rate.rs b/glean-core/src/metrics/rate.rs new file mode 100644 index 0000000000..5925bd292d --- /dev/null +++ b/glean-core/src/metrics/rate.rs @@ -0,0 +1,128 @@ +// 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::error_recording::{record_error, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// A rate metric. +/// +/// Used to determine the proportion of things via two counts: +/// * A numerator defining the amount of times something happened, +/// * A denominator counting the amount of times someting could have happened. +/// +/// Both numerator and denominator can only be incremented, not decremented. +#[derive(Clone, Debug)] +pub struct RateMetric { + meta: CommonMetricData, +} + +impl MetricType for RateMetric { + fn meta(&self) -> &CommonMetricData { + &self.meta + } + + fn meta_mut(&mut self) -> &mut CommonMetricData { + &mut self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl RateMetric { + /// Creates a new rate metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { meta } + } + + /// Increases the numerator by `amount`. + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + /// * `amount` - The amount to increase by. Should be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is negative. + pub fn add_to_numerator(&self, glean: &Glean, amount: i32) { + if !self.should_record(glean) { + return; + } + + if amount < 0 { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + format!("Added negative value {} to numerator", amount), + None, + ); + return; + } + + glean + .storage() + .record_with(glean, &self.meta, |old_value| match old_value { + Some(Metric::Rate(num, den)) => Metric::Rate(num.saturating_add(amount), den), + _ => Metric::Rate(amount, 0), // Denominator will show up eventually. Probably. + }); + } + + /// Increases the denominator by `amount`. + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + /// * `amount` - The amount to increase by. Should be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is negative. + pub fn add_to_denominator(&self, glean: &Glean, amount: i32) { + if !self.should_record(glean) { + return; + } + + if amount < 0 { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + format!("Added negative value {} to denominator", amount), + None, + ); + return; + } + + glean + .storage() + .record_with(glean, &self.meta, |old_value| match old_value { + Some(Metric::Rate(num, den)) => Metric::Rate(num, den.saturating_add(amount)), + _ => Metric::Rate(0, amount), + }); + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as a pair of integers. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option<(i32, i32)> { + match StorageManager.snapshot_metric_for_test( + glean.storage(), + storage_name, + &self.meta.identifier(glean), + self.meta.lifetime, + ) { + Some(Metric::Rate(n, d)) => Some((n, d)), + _ => None, + } + } +} diff --git a/glean-core/src/metrics/string.rs b/glean-core/src/metrics/string.rs index e280d08c32..cb8dbc8b12 100644 --- a/glean-core/src/metrics/string.rs +++ b/glean-core/src/metrics/string.rs @@ -67,7 +67,7 @@ impl StringMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/string_list.rs b/glean-core/src/metrics/string_list.rs index e61183c018..a483df12c2 100644 --- a/glean-core/src/metrics/string_list.rs +++ b/glean-core/src/metrics/string_list.rs @@ -133,7 +133,7 @@ impl StringListMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option> { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/timespan.rs b/glean-core/src/metrics/timespan.rs index ef2c329467..c4768cc44b 100644 --- a/glean-core/src/metrics/timespan.rs +++ b/glean-core/src/metrics/timespan.rs @@ -179,7 +179,7 @@ impl TimespanMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/metrics/timing_distribution.rs b/glean-core/src/metrics/timing_distribution.rs index c227150e5d..00e07969df 100644 --- a/glean-core/src/metrics/timing_distribution.rs +++ b/glean-core/src/metrics/timing_distribution.rs @@ -320,7 +320,7 @@ impl TimingDistributionMetric { /// /// This doesn't clear the stored value. pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option { - match StorageManager.snapshot_metric( + match StorageManager.snapshot_metric_for_test( glean.storage(), storage_name, &self.meta.identifier(glean), diff --git a/glean-core/src/storage/mod.rs b/glean-core/src/storage/mod.rs index 144b37ff7b..7c79e9c6c0 100644 --- a/glean-core/src/storage/mod.rs +++ b/glean-core/src/storage/mod.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; use serde_json::{json, Value as JsonValue}; +use crate::coverage::record_coverage; use crate::database::Database; use crate::metrics::Metric; use crate::Lifetime; @@ -113,8 +114,6 @@ impl StorageManager { /// Gets the current value of a single metric identified by name. /// - /// This look for a value in stores for all lifetimes. - /// /// # Arguments /// /// * `storage` - The database to get data from. @@ -145,6 +144,31 @@ impl StorageManager { snapshot } + /// Gets the current value of a single metric identified by name. + /// + /// Use this API, rather than `snapshot_metric` within the testing API, so + /// that the usage will be reported in coverage, if enabled. + /// + /// # Arguments + /// + /// * `storage` - The database to get data from. + /// * `store_name` - The store name to look into. + /// * `metric_id` - The full metric identifier. + /// + /// # Returns + /// + /// The decoded metric or `None` if no data is found. + pub fn snapshot_metric_for_test( + &self, + storage: &Database, + store_name: &str, + metric_id: &str, + metric_lifetime: Lifetime, + ) -> Option { + record_coverage(metric_id); + self.snapshot_metric(storage, store_name, metric_id, metric_lifetime) + } + /// Snapshots the experiments. /// /// # Arguments diff --git a/glean-core/src/traits/mod.rs b/glean-core/src/traits/mod.rs index 53c68e56e4..23f7df2fad 100644 --- a/glean-core/src/traits/mod.rs +++ b/glean-core/src/traits/mod.rs @@ -15,8 +15,10 @@ mod event; mod jwe; mod labeled; mod memory_distribution; +mod numerator; mod ping; mod quantity; +mod rate; mod string; mod string_list; mod timespan; @@ -34,8 +36,10 @@ pub use self::event::NoExtraKeys; pub use self::jwe::Jwe; pub use self::labeled::Labeled; pub use self::memory_distribution::MemoryDistribution; +pub use self::numerator::Numerator; pub use self::ping::Ping; pub use self::quantity::Quantity; +pub use self::rate::Rate; pub use self::string::String; pub use self::string_list::StringList; pub use self::timespan::Timespan; diff --git a/glean-core/src/traits/numerator.rs b/glean-core/src/traits/numerator.rs new file mode 100644 index 0000000000..fec8219d05 --- /dev/null +++ b/glean-core/src/traits/numerator.rs @@ -0,0 +1,52 @@ +// 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::ErrorType; + +// When changing this trait, ensure all operations are implemented in the +// related type in `../metrics`. (Except test_get_num_errors) +/// A description for the `NumeratorMetric` subtype of the [`RateMetric`](crate::metrics::RateMetric) type. +pub trait Numerator { + /// Increases the numerator by `amount`. + /// + /// # Arguments + /// + /// * `amount` - The amount to increase by. Should be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is negative. + fn add_to_numerator(&self, amount: i32); + + /// **Exported for test purposes.** + /// + /// Gets the currently stored value as a pair of integers. + /// + /// # Arguments + /// + /// * `ping_name` - the optional name of the ping to retrieve the metric + /// for. Defaults to the first value in `send_in_pings`. + /// + /// This doesn't clear the stored value. + fn test_get_value<'a, S: Into>>(&self, ping_name: S) -> Option<(i32, i32)>; + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - the optional name of the ping to retrieve the metric + /// for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + fn test_get_num_recorded_errors<'a, S: Into>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32; +} diff --git a/glean-core/src/traits/rate.rs b/glean-core/src/traits/rate.rs new file mode 100644 index 0000000000..fc6b5d92fd --- /dev/null +++ b/glean-core/src/traits/rate.rs @@ -0,0 +1,63 @@ +// 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::ErrorType; + +// When changing this trait, ensure all operations are implemented in the +// related type in `../metrics`. (Except test_get_num_errors) +/// A description for the [`RateMetric`](crate::metrics::RateMetric) type. +pub trait Rate { + /// Increases the numerator by `amount`. + /// + /// # Arguments + /// + /// * `amount` - The amount to increase by. Should be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is negative. + fn add_to_numerator(&self, amount: i32); + + /// Increases the denominator by `amount`. + /// + /// # Arguments + /// + /// * `amount` - The amount to increase by. Should be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is negative. + fn add_to_denominator(&self, amount: i32); + + /// **Exported for test purposes.** + /// + /// Gets the currently stored value as a pair of integers. + /// + /// # Arguments + /// + /// * `ping_name` - the optional name of the ping to retrieve the metric + /// for. Defaults to the first value in `send_in_pings`. + /// + /// This doesn't clear the stored value. + fn test_get_value<'a, S: Into>>(&self, ping_name: S) -> Option<(i32, i32)>; + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - the optional name of the ping to retrieve the metric + /// for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + fn test_get_num_recorded_errors<'a, S: Into>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32; +} diff --git a/glean.1.schema.json b/glean.1.schema.json index c12c1c0d70..180ffa574f 100644 --- a/glean.1.schema.json +++ b/glean.1.schema.json @@ -14,44 +14,57 @@ "additionalProperties": false, "properties": { "android_sdk_version": { + "description": "The optional Android specific SDK version of the software running on this hardware device.", "type": "string" }, "app_build": { + "description": "The build identifier generated by the CI system (e.g. \"1234/A\"). For language bindings that provide automatic detection for this value, (e.g. Android/Kotlin), in the unlikely event that the build identifier can not be retrieved from the OS, it is set to \"inaccessible\". For other language bindings, if the value was not provided through configuration, this metric gets set to `Unknown`.", "type": "string" }, "app_channel": { + "description": "The channel the application is being distributed on.", "type": "string" }, "app_display_version": { + "description": "The user visible version string (e.g. \"1.0.3\"). In the unlikely event that the display version can not be retrieved, it is set to \"inaccessible\".", "type": "string" }, "architecture": { + "description": "The architecture of the device, (e.g. \"arm\", \"x86\").", "type": "string" }, "client_id": { + "description": "A UUID uniquely identifying the client.", "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", "type": "string" }, "device_manufacturer": { + "description": "The manufacturer of the device the application is running on. Not set if the device manufacturer can't be determined (e.g. on Desktop).", "type": "string" }, "device_model": { + "description": "The model of the device the application is running on. On Android, this is Build.MODEL, the user-visible marketing name, like \"Pixel 2 XL\". Not set if the device model can't be determined (e.g. on Desktop).", "type": "string" }, "first_run_date": { + "description": "The date of the first run of the application.", "format": "datetime", "type": "string" }, "locale": { + "description": "The locale of the application during initialization (e.g. \"es-ES\"). If the locale can't be determined on the system, the value is [\"und\"](https://unicode.org/reports/tr35/#Unknown_or_Invalid_Identifiers), to indicate \"undetermined\".", "type": "string" }, "os": { + "description": "The name of the operating system. Possible values: Android, iOS, Linux, Darwin, Windows, FreeBSD, NetBSD, OpenBSD, Solaris, unknown", "type": "string" }, "os_version": { + "description": "The user-visible version of the operating system (e.g. \"1.2.3\"). If the version detection fails, this metric gets set to `Unknown`.", "type": "string" }, "telemetry_sdk_build": { + "description": "The version of the Glean SDK", "type": "string" } }, @@ -136,7 +149,7 @@ "type": "integer" }, "propertyNames": { - "pattern": "[0-9]+" + "pattern": "^[0-9]+$" }, "type": "object" } @@ -166,7 +179,7 @@ }, "type": "object" }, - "enumeration": { + "jwe": { "additionalProperties": { "type": "string" }, @@ -215,179 +228,23 @@ }, "type": "object" }, - "labeled_datetime": { - "additionalProperties": { - "additionalProperties": { - "format": "datetime", - "type": "string" - }, - "propertyNames": { - "comment": "This must be at least the length of 'category.name' metric names to support error reporting", - "maxLength": 61, - "type": "string" - }, - "type": "object" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, - "labeled_enumeration": { - "additionalProperties": { - "additionalProperties": { - "type": "string" - }, - "propertyNames": { - "comment": "This must be at least the length of 'category.name' metric names to support error reporting", - "maxLength": 61, - "type": "string" - }, - "type": "object" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, - "labeled_number": { - "additionalProperties": { - "additionalProperties": { - "type": "number" - }, - "propertyNames": { - "comment": "This must be at least the length of 'category.name' metric names to support error reporting", - "maxLength": 61, - "type": "string" - }, - "type": "object" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, "labeled_rate": { "additionalProperties": { "additionalProperties": { - "type": "integer" - }, - "propertyNames": { - "comment": "This must be at least the length of 'category.name' metric names to support error reporting", - "maxLength": 61, - "type": "string" - }, - "type": "object" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, - "labeled_string": { - "additionalProperties": { - "additionalProperties": { - "type": "string" - }, - "propertyNames": { - "comment": "This must be at least the length of 'category.name' metric names to support error reporting", - "maxLength": 61, - "type": "string" - }, - "type": "object" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, - "labeled_string_list": { - "additionalProperties": { - "additionalProperties": { - "items": { - "type": "string" - }, - "type": "array" - }, - "propertyNames": { - "comment": "This must be at least the length of 'category.name' metric names to support error reporting", - "maxLength": 61, - "type": "string" - }, - "type": "object" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, - "labeled_timing_distribution": { - "additionalProperties": { - "additionalProperties": { + "additionalProperties": false, "properties": { - "bucket_count": { - "type": "integer" - }, - "histogram_type": { - "enum": [ - "linear", - "exponential" - ], - "type": "string" - }, - "overflow": { - "type": "integer" - }, - "range": { - "items": { - "type": "number" - }, - "maxItems": 2, - "minItems": 2, - "type": "array" - }, - "sum": { + "denominator": { + "minimum": 0, "type": "integer" }, - "time_unit": { - "enum": [ - "nanosecond", - "microsecond", - "millisecond", - "second", - "minute", - "hour", - "day" - ], - "type": "string" - }, - "underflow": { + "numerator": { + "minimum": 0, "type": "integer" - }, - "values": { - "additionalProperties": { - "type": "integer" - }, - "propertyNames": { - "pattern": "[0-9]+" - }, - "type": "object" } }, "required": [ - "values" + "numerator", + "denominator" ], "type": "object" }, @@ -405,74 +262,9 @@ }, "type": "object" }, - "labeled_usage": { - "additionalProperties": { - "additionalProperties": { - "type": "boolean" - }, - "propertyNames": { - "comment": "This must be at least the length of 'category.name' metric names to support error reporting", - "maxLength": 61, - "type": "string" - }, - "type": "object" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, - "labeled_use_counter": { - "additionalProperties": { - "additionalProperties": { - "properties": { - "denominator": { - "properties": { - "name": { - "maxLength": 30, - "pattern": "^[a-z_][a-z0-9_]*$", - "type": "string" - }, - "value": { - "type": "integer" - } - }, - "type": "object" - }, - "values": { - "additionalProperties": { - "type": "integer" - }, - "propertyNames": { - "maxLength": 30, - "pattern": "^[a-z_][a-z0-9_]*$", - "type": "string" - }, - "type": "object" - } - }, - "type": "object" - }, - "propertyNames": { - "comment": "This must be at least the length of 'category.name' metric names to support error reporting", - "maxLength": 61, - "type": "string" - }, - "type": "object" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, - "labeled_uuid": { + "labeled_string": { "additionalProperties": { "additionalProperties": { - "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", "type": "string" }, "propertyNames": { @@ -500,7 +292,7 @@ "type": "integer" }, "propertyNames": { - "pattern": "[0-9]+" + "pattern": "^[0-9]+$" }, "type": "object" } @@ -517,17 +309,6 @@ }, "type": "object" }, - "number": { - "additionalProperties": { - "type": "number" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, "quantity": { "additionalProperties": { "type": "integer" @@ -541,7 +322,22 @@ }, "rate": { "additionalProperties": { - "type": "integer" + "additionalProperties": false, + "properties": { + "denominator": { + "minimum": 0, + "type": "integer" + }, + "numerator": { + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "numerator", + "denominator" + ], + "type": "object" }, "propertyNames": { "maxLength": 61, @@ -654,7 +450,7 @@ "type": "integer" }, "propertyNames": { - "pattern": "[0-9]+" + "pattern": "^[0-9]+$" }, "type": "object" } @@ -671,54 +467,6 @@ }, "type": "object" }, - "usage": { - "additionalProperties": { - "type": "boolean" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, - "use_counter": { - "additionalProperties": { - "properties": { - "denominator": { - "properties": { - "name": { - "maxLength": 30, - "pattern": "^[a-z_][a-z0-9_]*$", - "type": "string" - }, - "value": { - "type": "integer" - } - }, - "type": "object" - }, - "values": { - "additionalProperties": { - "type": "integer" - }, - "propertyNames": { - "maxLength": 30, - "pattern": "^[a-z_][a-z0-9_]*$", - "type": "string" - }, - "type": "object" - } - }, - "type": "object" - }, - "propertyNames": { - "maxLength": 61, - "pattern": "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})+$", - "type": "string" - }, - "type": "object" - }, "uuid": { "additionalProperties": { "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", diff --git a/gradle-plugin/src/main/groovy/mozilla/telemetry/glean-gradle-plugin/GleanGradlePlugin.groovy b/gradle-plugin/src/main/groovy/mozilla/telemetry/glean-gradle-plugin/GleanGradlePlugin.groovy index 4787bf036e..42a1ea7c3b 100644 --- a/gradle-plugin/src/main/groovy/mozilla/telemetry/glean-gradle-plugin/GleanGradlePlugin.groovy +++ b/gradle-plugin/src/main/groovy/mozilla/telemetry/glean-gradle-plugin/GleanGradlePlugin.groovy @@ -36,7 +36,7 @@ class GleanMetricsYamlTransform extends ArtifactTransform { @SuppressWarnings("GrPackage") class GleanPlugin implements Plugin { // The version of glean_parser to install from PyPI. - private String GLEAN_PARSER_VERSION = "2.2.0" + private String GLEAN_PARSER_VERSION = "2.5.0" // The version of Miniconda is explicitly specified. // Miniconda3-4.5.12 is known to not work on Windows. private String MINICONDA_VERSION = "4.5.11" @@ -473,7 +473,7 @@ except: void apply(Project project) { isOffline = project.gradle.startParameter.offline - project.ext.glean_version = "35.0.0" + project.ext.glean_version = "36.0.0" // Print the required glean_parser version to the console. This is // offline builds, and is mentioned in the documentation for offline diff --git a/samples/ios/app/glean-sample-app.xcodeproj/project.pbxproj b/samples/ios/app/glean-sample-app.xcodeproj/project.pbxproj index 1c678f9273..fbeb78f75c 100644 --- a/samples/ios/app/glean-sample-app.xcodeproj/project.pbxproj +++ b/samples/ios/app/glean-sample-app.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ BFEA24D025CC3A4F00110728 /* Gzip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEA24BB25CC39BF00110728 /* Gzip.xcframework */; }; BFEA24D325CC3A5200110728 /* Swifter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEA24C025CC39CB00110728 /* Swifter.xcframework */; }; BFED6B2C238C1638006E2BC4 /* DeletionRequestPingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFED6B2B238C1638006E2BC4 /* DeletionRequestPingTest.swift */; }; + CD99D84625F131D600BD5339 /* EventPingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD99D84525F131D600BD5339 /* EventPingTest.swift */; }; CDDD7A9D25D1635F00837394 /* Gzip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEA24BB25CC39BF00110728 /* Gzip.xcframework */; }; CDDD7A9E25D1635F00837394 /* Gzip.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFEA24BB25CC39BF00110728 /* Gzip.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CDDD7AA125D1636C00837394 /* Swifter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEA24C025CC39CB00110728 /* Swifter.xcframework */; }; @@ -89,6 +90,7 @@ BFEA24BB25CC39BF00110728 /* Gzip.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Gzip.xcframework; path = Carthage/Build/Gzip.xcframework; sourceTree = ""; }; BFEA24C025CC39CB00110728 /* Swifter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Swifter.xcframework; path = Carthage/Build/Swifter.xcframework; sourceTree = ""; }; BFED6B2B238C1638006E2BC4 /* DeletionRequestPingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletionRequestPingTest.swift; sourceTree = ""; }; + CD99D84525F131D600BD5339 /* EventPingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventPingTest.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -184,6 +186,7 @@ children = ( BFD3AB4D224D475E00AD9255 /* BaselinePingTest.swift */, BFED6B2B238C1638006E2BC4 /* DeletionRequestPingTest.swift */, + CD99D84525F131D600BD5339 /* EventPingTest.swift */, BFDA7ACF2371CE4900575C7B /* ViewControllerTest.swift */, BFD3AB4F224D475E00AD9255 /* Info.plist */, BFDA7AD12371D2BA00575C7B /* MockServer.swift */, @@ -381,6 +384,7 @@ BFDA7AD22371D2BA00575C7B /* MockServer.swift in Sources */, BFDA7AD02371CE4900575C7B /* ViewControllerTest.swift in Sources */, BFD3AB4E224D475E00AD9255 /* BaselinePingTest.swift in Sources */, + CD99D84625F131D600BD5339 /* EventPingTest.swift in Sources */, BFED6B2C238C1638006E2BC4 /* DeletionRequestPingTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -543,7 +547,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = G9EAK7RA89; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; FRAMEWORK_SEARCH_PATHS = ""; INFOPLIST_FILE = "glean-sample-app/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( @@ -564,7 +567,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = G9EAK7RA89; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; FRAMEWORK_SEARCH_PATHS = ""; INFOPLIST_FILE = "glean-sample-app/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/samples/ios/app/glean-sample-appUITests/BaselinePingTest.swift b/samples/ios/app/glean-sample-appUITests/BaselinePingTest.swift index bdb72af9bf..02ecfbef64 100644 --- a/samples/ios/app/glean-sample-appUITests/BaselinePingTest.swift +++ b/samples/ios/app/glean-sample-appUITests/BaselinePingTest.swift @@ -29,9 +29,14 @@ class BaselinePingTest: XCTestCase { func setupServer(expectPingType: String) -> HttpServer { return mockServer(expectPingType: expectPingType) { json in - self.lastPingJson = json - // Fulfill test's expectation once we parsed the incoming data. - self.expectation?.fulfill() + let pingInfo = json!["ping_info"] as! [String: Any] + let reason = pingInfo["reason"] as! String + + if reason != "dirty_startup" { + self.lastPingJson = json + // Fulfill test's expectation once we parsed the incoming data. + self.expectation?.fulfill() + } } } diff --git a/samples/ios/app/glean-sample-appUITests/EventPingTest.swift b/samples/ios/app/glean-sample-appUITests/EventPingTest.swift new file mode 100644 index 0000000000..622bc5a9bc --- /dev/null +++ b/samples/ios/app/glean-sample-appUITests/EventPingTest.swift @@ -0,0 +1,96 @@ +/* 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/. */ + +import Glean +import Swifter +import XCTest + +// swiftlint:disable force_cast +// REASON: Used in below test cases to cause errors if data is missing +class EventPingTest: XCTestCase { + var app: XCUIApplication! + var expectation: XCTestExpectation? + var lastPingJson: [String: Any]? + + override func setUp() { + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // UI tests must launch the application that they test. + // Doing this in setup will make sure it happens for each test method. + app = XCUIApplication() + } + + override func tearDown() { + self.lastPingJson = nil + self.expectation = nil + } + + func setupServer(expectPingType: String) -> HttpServer { + return mockServer(expectPingType: expectPingType) { json in + self.lastPingJson = json + // Fulfill test's expectation once we parsed the incoming data. + self.expectation?.fulfill() + } + } + + // We launch the app, tap the record button a couple of times, + // then restart the app, which should trigger an event ping. + func testValidateEventPing() { + let server = setupServer(expectPingType: "events") + let port = try! server.port() + expectation = expectation(description: "Completed upload (event ping)") + + app.launchArguments = ["USE_MOCK_SERVER", "\(port)"] + app.launch() + + let recordButton = app.buttons["Record"] + + // 3 events, quickly + recordButton.tap() + recordButton.tap() + recordButton.tap() + + // one event after a while + sleep(1) + recordButton.tap() + + // We need to send the ping to clear out old values. + let sendButton = app.buttons["Send"] + sendButton.tap() + + // Trigger the event ping by putting app into the background + XCUIDevice.shared.press(XCUIDevice.Button.home) + + waitForExpectations(timeout: 5.0) { error in + XCTAssertNil(error, "Test timed out waiting for upload: \(error!)") + } + + let pingInfo = lastPingJson!["ping_info"] as! [String: Any] + let reason = pingInfo["reason"] as! String + XCTAssertEqual("inactive", reason, "Should have gotten a inactive events ping") + + let events = lastPingJson!["events"] as! [[String: Any]] + XCTAssertEqual(4, events.count, "Events ping should have all button-tap events") + + let firstEvent = events[0] + XCTAssertEqual(0, firstEvent["timestamp"] as! Int, "First event should be at timestamp 0") + + for i in 1...3 { + let earlier = events[i-1]["timestamp"] as! Int + let this = events[i]["timestamp"] as! Int + XCTAssert(earlier <= this, "Events should be ordered monotonically non-decreasing") + } + + let notLast = events[2]["timestamp"] as! Int + let last = events[3]["timestamp"] as! Int + let diff = last - notLast + // Sleeping and tapping the button has a delay of ~600ms, + // so we account for a tiny bit more here. + XCTAssert(diff >= 1000 && diff <= 2000, + "Last event should be a second after the second-to-last event (actual diff: \(diff)") + + server.stop() + } +} diff --git a/taskcluster/docker/linux/Dockerfile b/taskcluster/docker/linux/Dockerfile index 11f60c97fc..ab4792d6f6 100644 --- a/taskcluster/docker/linux/Dockerfile +++ b/taskcluster/docker/linux/Dockerfile @@ -71,6 +71,8 @@ RUN apt-get update -qq \ unzip \ # Required by tooltool to extract tar.xz archives. xz-utils \ + # Required to unpack compiler + zstd \ # Required to build libs/. make \ # Required to build sqlcipher. diff --git a/taskcluster/scripts/cross-compile-setup.sh b/taskcluster/scripts/cross-compile-setup.sh index 0b068a1d52..3e5abf532c 100755 --- a/taskcluster/scripts/cross-compile-setup.sh +++ b/taskcluster/scripts/cross-compile-setup.sh @@ -2,12 +2,12 @@ export PATH=$PATH:/tmp/clang/bin export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_CC=/tmp/clang/bin/clang export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_TOOLCHAIN_PREFIX=/tmp/cctools/bin -export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_AR=/tmp/cctools/bin/x86_64-darwin11-ar -export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_RANLIB=/tmp/cctools/bin/x86_64-darwin11-ranlib +export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_AR=/tmp/cctools/bin/x86_64-apple-darwin-ar +export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_RANLIB=/tmp/cctools/bin/x86_64-apple-darwin-ranlib export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_LD_LIBRARY_PATH=/tmp/clang/lib -export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS="-C linker=/tmp/clang/bin/clang -C link-arg=-B -C link-arg=/tmp/cctools/bin -C link-arg=-target -C link-arg=x86_64-darwin11 -C link-arg=-isysroot -C link-arg=/tmp/MacOSX10.11.sdk -C link-arg=-Wl,-syslibroot,/tmp/MacOSX10.11.sdk -C link-arg=-Wl,-dead_strip" +export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS="-C linker=/tmp/clang/bin/clang -C link-arg=-B -C link-arg=/tmp/cctools/bin -C link-arg=-target -C link-arg=x86_64-apple-darwin -C link-arg=-isysroot -C link-arg=/tmp/MacOSX10.12.sdk -C link-arg=-Wl,-syslibroot,/tmp/MacOSX10.12.sdk -C link-arg=-Wl,-dead_strip" # For ring's use of `cc`. -export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_CFLAGS_x86_64_apple_darwin="-B /tmp/cctools/bin -target x86_64-darwin11 -isysroot /tmp/MacOSX10.11.sdk -Wl,-syslibroot,/tmp/MacOSX10.11.sdk -Wl,-dead_strip" +export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_APPLE_DARWIN_CFLAGS_x86_64_apple_darwin="-B /tmp/cctools/bin -target x86_64-apple-darwin -isysroot /tmp/MacOSX10.12.sdk -Wl,-syslibroot,/tmp/MacOSX10.12.sdk -Wl,-dead_strip" # The wrong linker gets used otherwise: https://github.com/rust-lang/rust/issues/33465. export ORG_GRADLE_PROJECT_RUST_ANDROID_GRADLE_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C linker=x86_64-w64-mingw32-gcc" @@ -26,6 +26,14 @@ tooltool.py \ --manifest="/builds/worker/checkouts/src/taskcluster/scripts/macos-cc-tools.manifest" \ fetch +curl -sfSL --retry 5 --retry-delay 10 https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/NcaljhkvQOSKxGlBSWJl_w/runs/0/artifacts/public/build/cctools.tar.xz > cctools.tar.xz +tar -xf cctools.tar.xz +ls -l /tmp/cctools +curl -sfSL --retry 5 --retry-delay 10 https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/eJJotfnPSt2weVG0sZlfeQ/runs/0/artifacts/public/build/clang.tar.zst > clang.tar.zst +tar -I zstd -xf clang.tar.zst +ls -l /tmp/clang +ls -l /tmp/clang/bin + rustup target add x86_64-apple-darwin rustup target add x86_64-pc-windows-gnu diff --git a/taskcluster/scripts/macos-cc-tools.manifest b/taskcluster/scripts/macos-cc-tools.manifest index 2407beec4c..fb9b3d005e 100644 --- a/taskcluster/scripts/macos-cc-tools.manifest +++ b/taskcluster/scripts/macos-cc-tools.manifest @@ -1,26 +1,10 @@ [ { - "size": 34094283, + "size": 31991917, "visibility": "internal", - "digest": "8811050fe375bcc566c8b85173d86b8a87aa2148edfed93023735c2de44b66a5a28cbaa1cfd396032447fd803e03f308ed941a200c0e2a1ad9fbe16b5606ee7c", + "digest": "c5c0be09972b56b5980dc9d06b61ff49cf58c4572913437256a79b202e19e936af3c0ab0924df72b9f648d518c257597f84800a84bb80e68af4eabdaf1df5f24", "algorithm": "sha512", "unpack": true, - "filename": "MacOSX10.11.sdk.tar.xz" - }, - { - "size": 1549588, - "visibility": "public", - "digest": "3653886e5ea471f6089e526c8ae63063a7e032872c051d85f91389e40baef887e7452c93f0386e23c72875853cb9c2f46f282eec8015c53c6ab4c24e964e8620", - "algorithm": "sha512", - "unpack": true, - "filename": "cctools.tar.xz" - }, - { - "size": 160611724, - "visibility": "public", - "digest": "82d300433526a0a008214a2b704f8de63617751001126b62a91239ce0f002e6504200988f42e25fff273d7228daa4de835d6fe82ef7e6ff8a4f667d8636abb99", - "algorithm": "sha512", - "unpack": true, - "filename": "clang.tar.xz" + "filename": "MacOSX10.12.sdk.tar.xz" } ]