summaryrefslogtreecommitdiffstats
path: root/third_party/python/glean_parser
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/glean_parser')
-rw-r--r--third_party/python/glean_parser/glean_parser-11.0.1.dist-info/AUTHORS.md17
-rw-r--r--third_party/python/glean_parser/glean_parser-11.0.1.dist-info/LICENSE373
-rw-r--r--third_party/python/glean_parser/glean_parser-11.0.1.dist-info/METADATA769
-rw-r--r--third_party/python/glean_parser/glean_parser-11.0.1.dist-info/RECORD44
-rw-r--r--third_party/python/glean_parser/glean_parser-11.0.1.dist-info/WHEEL5
-rw-r--r--third_party/python/glean_parser/glean_parser-11.0.1.dist-info/entry_points.txt2
-rw-r--r--third_party/python/glean_parser/glean_parser-11.0.1.dist-info/top_level.txt1
-rw-r--r--third_party/python/glean_parser/glean_parser/__init__.py18
-rw-r--r--third_party/python/glean_parser/glean_parser/__main__.py351
-rw-r--r--third_party/python/glean_parser/glean_parser/coverage.py140
-rw-r--r--third_party/python/glean_parser/glean_parser/data_review.py79
-rw-r--r--third_party/python/glean_parser/glean_parser/javascript.py322
-rw-r--r--third_party/python/glean_parser/glean_parser/javascript_server.py204
-rw-r--r--third_party/python/glean_parser/glean_parser/kotlin.py356
-rw-r--r--third_party/python/glean_parser/glean_parser/lint.py653
-rw-r--r--third_party/python/glean_parser/glean_parser/markdown.py273
-rw-r--r--third_party/python/glean_parser/glean_parser/metrics.py437
-rw-r--r--third_party/python/glean_parser/glean_parser/parser.py446
-rw-r--r--third_party/python/glean_parser/glean_parser/pings.py99
-rw-r--r--third_party/python/glean_parser/glean_parser/ruby_server.py128
-rw-r--r--third_party/python/glean_parser/glean_parser/rust.py218
-rw-r--r--third_party/python/glean_parser/glean_parser/schemas/metrics.1-0-0.schema.yaml605
-rw-r--r--third_party/python/glean_parser/glean_parser/schemas/metrics.2-0-0.schema.yaml770
-rw-r--r--third_party/python/glean_parser/glean_parser/schemas/pings.1-0-0.schema.yaml157
-rw-r--r--third_party/python/glean_parser/glean_parser/schemas/pings.2-0-0.schema.yaml178
-rw-r--r--third_party/python/glean_parser/glean_parser/schemas/tags.1-0-0.schema.yaml51
-rw-r--r--third_party/python/glean_parser/glean_parser/swift.py260
-rw-r--r--third_party/python/glean_parser/glean_parser/tags.py49
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/data_review.jinja282
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/javascript.buildinfo.jinja211
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/javascript.jinja273
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/javascript_server.jinja2285
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/kotlin.buildinfo.jinja231
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/kotlin.geckoview.jinja2124
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/kotlin.jinja2133
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/markdown.jinja298
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/qmldir.jinja24
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/ruby_server.jinja2180
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/rust.jinja289
-rw-r--r--third_party/python/glean_parser/glean_parser/templates/swift.jinja2139
-rw-r--r--third_party/python/glean_parser/glean_parser/translate.py232
-rw-r--r--third_party/python/glean_parser/glean_parser/translation_options.py54
-rw-r--r--third_party/python/glean_parser/glean_parser/util.py533
-rw-r--r--third_party/python/glean_parser/glean_parser/validate_ping.py74
44 files changed, 9147 insertions, 0 deletions
diff --git a/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/AUTHORS.md b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/AUTHORS.md
new file mode 100644
index 0000000000..525116ee7e
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/AUTHORS.md
@@ -0,0 +1,17 @@
+# Credits
+
+## Development Lead
+
+- Jan-Erik Rediger <jrediger@mozilla.com>
+- Alessio Placitelli <aplacitelli@mozilla.com>
+
+## Contributors
+
+See [the full list of contributors](https://github.com/mozilla/glean_parser/graphs/contributors).
+
+## Acknowledgements
+
+This package was created with
+[Cookiecutter](https://github.com/audreyr/cookiecutter) and the
+[audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage)
+project template.
diff --git a/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/LICENSE b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/LICENSE
new file mode 100644
index 0000000000..a612ad9813
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/METADATA b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/METADATA
new file mode 100644
index 0000000000..201d8bb48b
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/METADATA
@@ -0,0 +1,769 @@
+Metadata-Version: 2.1
+Name: glean-parser
+Version: 11.0.1
+Summary: Parser tools for Mozilla's Glean telemetry
+Home-page: https://github.com/mozilla/glean_parser
+Author: The Glean Team
+Author-email: glean-team@mozilla.com
+Keywords: glean_parser
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Natural Language :: English
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Description-Content-Type: text/markdown
+License-File: LICENSE
+License-File: AUTHORS.md
+Requires-Dist: appdirs >=1.4
+Requires-Dist: Click >=7
+Requires-Dist: diskcache >=4
+Requires-Dist: Jinja2 >=2.10.1
+Requires-Dist: jsonschema >=3.0.2
+Requires-Dist: PyYAML >=5.3.1
+
+# Glean Parser
+
+Parser tools for Mozilla's Glean telemetry.
+
+## Features
+
+Contains various utilities for handling `metrics.yaml` and `pings.yaml` for [the
+Glean SDKs](https://mozilla.github.io/glean). This includes producing generated
+code for various integrations, linting and coverage testing.
+
+## Documentation
+
+- [How to Contribute](https://github.com/mozilla/glean_parser/blob/main/CONTRIBUTING.md). Please file bugs in [bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?assigned_to=nobody%40mozilla.org&bug_ignored=0&bug_severity=normal&bug_status=NEW&cf_fission_milestone=---&cf_fx_iteration=---&cf_fx_points=---&cf_status_firefox65=---&cf_status_firefox66=---&cf_status_firefox67=---&cf_status_firefox_esr60=---&cf_status_thunderbird_esr60=---&cf_tracking_firefox65=---&cf_tracking_firefox66=---&cf_tracking_firefox67=---&cf_tracking_firefox_esr60=---&cf_tracking_firefox_relnote=---&cf_tracking_thunderbird_esr60=---&product=Data%20Platform%20and%20Tools&component=Glean%3A%20SDK&contenttypemethod=list&contenttypeselection=text%2Fplain&defined_groups=1&flag_type-203=X&flag_type-37=X&flag_type-41=X&flag_type-607=X&flag_type-721=X&flag_type-737=X&flag_type-787=X&flag_type-799=X&flag_type-800=X&flag_type-803=X&flag_type-835=X&flag_type-846=X&flag_type-855=X&flag_type-864=X&flag_type-916=X&flag_type-929=X&flag_type-930=X&flag_type-935=X&flag_type-936=X&flag_type-937=X&form_name=enter_bug&maketemplate=Remember%20values%20as%20bookmarkable%20template&op_sys=Unspecified&priority=P3&&rep_platform=Unspecified&status_whiteboard=%5Btelemetry%3Aglean-rs%3Am%3F%5D&target_milestone=---&version=unspecified).
+- [User documentation for Glean](https://mozilla.github.io/glean/).
+- [`glean_parser` developer documentation](https://mozilla.github.io/glean_parser/).
+
+## Requirements
+
+- Python 3.8 (or later)
+
+The following library requirements are installed automatically when
+`glean_parser` is installed by `pip`.
+
+- appdirs
+- Click
+- diskcache
+- Jinja2
+- jsonschema
+- PyYAML
+
+## Usage
+
+```sh
+$ glean_parser --help
+```
+
+Read in `metrics.yaml`, translate to Kotlin format, and
+output to `output_dir`:
+
+```sh
+$ glean_parser translate -o output_dir -f kotlin metrics.yaml
+```
+
+Check a Glean ping against the ping schema:
+
+```sh
+$ glean_parser check < ping.json
+```
+
+
+# Changelog
+
+## Unreleased
+
+## 11.0.1
+
+- Fix javascript_server template to include non-event metric parameters in #record call for event metrics ([#643](https://github.com/mozilla/glean_parser/pull/643))
+- events: Increase extra key limit to 50 ([Bug 1869429](https://bugzilla.mozilla.org/show_bug.cgi?id=1869429))
+
+## 11.0.0
+
+- Add updated logging logic for Ruby Server ([#642](https://github.com/mozilla/glean_parser/pull/642))
+- Add support for event metric type in server-side JavaScript outputter ([DENG-1736](https://mozilla-hub.atlassian.net/browse/DENG-1736))
+- BREAKING CHANGE: Dropped support for Python 3.7 ([#638](https://github.com/mozilla/glean_parser/pull/638))
+- Add official support for Python 3.11+ ([#638](https://github.com/mozilla/glean_parser/pull/638))
+
+## 10.0.3
+
+- Warn about empty or TODO-tagged data reviews in the list ([#634](https://github.com/mozilla/glean_parser/pull/634))
+- Allow `unit` field on all metrics, but warn for all but quantity and custom distribution ([#636](https://github.com/mozilla/glean_parser/pull/636))
+
+## 10.0.2
+
+- Allow `unit` field for string again, but warn about it in the linter ([#634](https://github.com/mozilla/glean_parser/pull/634))
+
+## 10.0.1
+
+- Allow `unit` field for custom distribution again ([#633](https://github.com/mozilla/glean_parser/pull/633))
+
+## 10.0.0
+
+- Add Ruby log outputter (`ruby_server`) ([#620](https://github.com/mozilla/glean_parser/pull/620))
+- BREAKING CHANE: `ping` lifetime metrics on the events ping are now disallowed ([#625](https://github.com/mozilla/glean_parser/pull/625))
+- Disallow `unit` field for anything but quantity ([#630](https://github.com/mozilla/glean_parser/pull/630)).
+ Note that this was already considered the case, now the code enforces it.
+
+## 9.0.0
+
+- BREAKING CHANGE: Dropped support for Python 3.6 ([#615](https://github.com/mozilla/glean_parser/issues/615))
+- Allow metadata to configure precise timestamps in pings ([#592](https://github.com/mozilla/glean_parser/pull/592))
+
+## 8.1.1
+
+- Small updates to the `javascript_server` tempalte to address lint warnings ([#598](https://github.com/mozilla/glean_parser/pull/598))
+
+## 8.1.0
+
+- Increased the maximum metric name length in version 2.0.0 schema ([#596](https://github.com/mozilla/glean_parser/pull/596))
+
+## 8.0.0
+
+- BREAKING CHANGE: Remove exposed `lint_yaml_files` function ([#580](https://github.com/mozilla/glean_parser/pull/580))
+- Rust: Removed `__glean_metric_maps` from the Rust Jinja template. This functionality is better placed downstream ([Bug 1816526](https://bugzilla.mozilla.org/show_bug.cgi?id=1816526))
+- New lint: check that all referenced pings are known ([#584](https://github.com/mozilla/glean_parser/pull/584))
+- Add experimental server-side JavaScript outputter ([FXA-7922](https://mozilla-hub.atlassian.net/browse/FXA-7922))
+
+## 7.2.1
+
+- Unbreak last minor release ([#579](https://github.com/mozilla/glean_parser/pull/579))
+
+## 7.2.0
+
+- Remove yamllint integration ([#578](https://github.com/mozilla/glean_parser/pull/578))
+
+## 7.1.0
+
+- ENHANCEMENT: Labels in `labels:` fields may now contain any printable ASCII characters ([bug 1672273](https://bugzilla.mozilla.org/show_bug.cgi?id=1672273))
+- BUGFIX: Enforce ordering of generation of Pings, Metrics and Tags such that order is deterministic ([bug 1820334](https://bugzilla.mozilla.org/show_bug.cgi?id=1820334))
+
+## 7.0.0
+
+- BUGFIX: Remove internal-only fields from serialized metrics data ([#550](https://github.com/mozilla/glean_parser/pull/550))
+- FEATURE: New subcommand: `dump` to dump the metrics data as JSON ([#550](https://github.com/mozilla/glean_parser/pull/550))
+- BUGFIX: Kotlin: Generate enums with the right generic bound for ping reason codes ([#551](https://github.com/mozilla/glean_parser/pull/551)).
+- **BREAKING CHANGE:** Fully remove support for the old events API ([#549](https://github.com/mozilla/glean_parser/pull/549))
+ Adds a new lint `OLD_EVENT_API` to warn about missing `type` attributes on event extra keys.
+ Note that the Glean SDK already dropped support for the old events API.
+
+## 6.4.0
+
+- BUGFIX: Correct code generation for labeled metrics in Rust ([#533](https://github.com/mozilla/glean_parser/pull/533))
+- BUGFIX: Correctly serialize `Rates` for Rust code ([#530](https://github.com/mozilla/glean_parser/pull/530))
+- Feature: Wrap labeled metric's static labels list as CoW strings (requires updated Glean support) ([#534](https://github.com/mozilla/glean_parser/pull/534))
+
+## 6.3.0
+
+- events: Increase extras limit to 15 ([bug 1798713](https://bugzilla.mozilla.org/show_bug.cgi?id=1798713))
+
+## 6.2.1
+
+- Add support for Rate, Denominator and Numerator metrics for JavaScript. ([bug 1793777](https://bugzilla.mozilla.org/show_bug.cgi?id=1793777))
+
+## 6.2.0
+
+- [data-review] Use a template to generate the Data Review Request template ([bug 1772605](https://bugzilla.mozilla.org/show_bug.cgi?id=1772605))
+- Make tag and no\_lint order deterministic ([#518](https://github.com/mozilla/glean_parser/pull/518))
+
+## 6.1.2
+
+- Swift: Add a conditional `import Foundation` to support generating metrics when Glean is delivered via the AppServices iOS megazord
+
+## 6.1.1
+
+- Rust: Use correct name for a ping in generated code.
+
+## 6.1.0
+
+- [data-review] Include extra keys' names and descriptions in data review template ([bug 1767027](https://bugzilla.mozilla.org/show_bug.cgi?id=1767027))
+- Raise limit on number of statically-defined labels to 4096. ([bug 1772163](https://bugzilla.mozilla.org/show_bug.cgi?id=1772163))
+- Fix Rust code generation for new UniFFI interface ([#491](https://github.com/mozilla/glean_parser/pull/491), [#494](https://github.com/mozilla/glean_parser/pull/494), [#495](https://github.com/mozilla/glean_parser/pull/495))
+
+## 6.0.1
+
+- Relax version requirement for MarkupSafe.
+ Now works with MarkupSafe v1.1.1 to v2.0.1 inclusive again.
+
+## 6.0.0
+
+- BUGFIX: Add missing `extra_args` to Rust constructor generation ([bug 1765855](https://bugzilla.mozilla.org/show_bug.cgi?id=1765855))
+- **Breaking change:** `glean_parser` now generates metrics compatible with the UniFFI-powered Glean SDK.
+ This is not backwards-compatible with previous versions.
+- Generate Rate, Denominator and Numerator metrics for Kotlin and Swift
+- Explicitly skip Rate, Denominator and Numerator metrics for JavaScript.
+ These will cause a build failure by default, but can be turned into warnings on request.
+ Use `-s fail_rates=false` to enable warning-only mode.
+
+## 5.1.2
+
+- BUGFIX: Revert changes made on v5.1.1.
+ - The issues addressed by those changes, were non-issues and result of misuse of the APIs.
+
+## 5.1.1
+
+- BUGFIX: Fix issues with Swift templates ([bug 1749494](https://bugzilla.mozilla.org/show_bug.cgi?id=1749494))
+ - Make metrics and pings all `public`
+ - Make pings `static`
+
+## 5.1.0
+
+- Add support for build info generation for JavaScript and Typescript targets ([bug 1749494](https://bugzilla.mozilla.org/show_bug.cgi?id=1749494))
+
+## 5.0.1
+
+- Fix the logic for the metric expiration by version ([bug 1753194](https://bugzilla.mozilla.org/show_bug.cgi?id=1753194))
+
+## 5.0.0
+
+- Remove C# support ([#436](https://github.com/mozilla/glean_parser/pull/436)).
+- Add support for Rust code generation ([bug 1677434](https://bugzilla.mozilla.org/show_bug.cgi?id=1677434))
+- Report an error if no files are passed ([bug 1751730](https://bugzilla.mozilla.org/show_bug.cgi?id=1751730))
+- [data-review] Report an error if no metrics match provided bug number ([bug 1752576](https://bugzilla.mozilla.org/show_bug.cgi?id=1752576))
+- [data-review] Include notification_emails in list of those responsible ([bug 1752576](https://bugzilla.mozilla.org/show_bug.cgi?id=1752576))
+- Add support for expiring metrics by the provided major version ([bug 1753194](https://bugzilla.mozilla.org/show_bug.cgi?id=1753194))
+
+## 4.4.0
+
+- Support global file-level tags in metrics.yaml ([bug 1745283](https://bugzilla.mozilla.org/show_bug.cgi?id=1745283))
+- Glinter: Reject metric files if they use `unit` by mistake. It should be `time_unit` ([#432](https://github.com/mozilla/glean_parser/pull/432)).
+- Automatically generate a build date when generating build info ([#431](https://github.com/mozilla/glean_parser/pull/431)).
+ Enabled for Kotlin and Swift.
+ This can be changed with the `build_date` command line option.
+ `build_date=0` will use a static unix epoch time.
+ `build_date=2022-01-03T17:30:00` will parse the ISO8601 string to use (as a UTC timestamp).
+ Other values will throw an error.
+
+ Example:
+
+ glean_parser translate --format kotlin --option build_date=2021-11-01T01:00:00 path/to/metrics.yaml
+
+## 4.3.1
+
+- BUGFIX: Skip tags for code generation ([#409](https://github.com/mozilla/glean_parser/pull/409))
+
+## 4.3.0
+
+- Support tags in glean parser ([bug 1734011](https://bugzilla.mozilla.org/show_bug.cgi?id=1734011))
+
+## 4.2.0
+
+- Improve the schema validation error messages. They will no longer include `OrderedDict(...)` on Python 3.7 and later ([bug 1733395](https://bugzilla.mozilla.org/show_bug.cgi?id=1733395))
+- Officially support Python 3.10
+
+## 4.1.1 (2021-09-28)
+
+- Update private import paths on Javascript / Typescript templates. ([bug 1702468](https://bugzilla.mozilla.org/show_bug.cgi?id=1702468))
+
+## 4.1.0 (2021-09-16)
+
+- Add support for Node.js platform on Javascript / Typescript templates. ([bug 1728982](https://bugzilla.mozilla.org/show_bug.cgi?id=1728982))
+
+## 4.0.0 (2021-08-20)
+
+- Add support for Text metric type ([#374](https://github.com/mozilla/glean_parser/pull/374))
+- Reserve the `default` ping name. It can't be used as a ping name, but it can be used in `send_in_pings` ([#376](https://github.com/mozilla/glean_parser/pull/376))
+
+## 3.8.0 (2021-08-18)
+
+- Expose ping reasons enum on JavaScript / TypeScript templates. ([bug 1719136](https://bugzilla.mozilla.org/show_bug.cgi?id=1719136))
+- Define an interface with the allowed extras for each event on the TypeScript template. ([bug 1693487](https://bugzilla.mozilla.org/show_bug.cgi?id=1693487))
+
+## 3.7.0 (2021-07-13)
+
+- New lint: Check for redundant words in ping names ([#355](https://github.com/mozilla/glean_parser/pull/355))
+- Add support for URL metric type ([#361](https://github.com/mozilla/glean_parser/pull/361))
+
+## 3.6.0 (2021-06-11)
+
+- Add a command `data-review` to generate a skeleton Data Review Request for all metrics matching a supplied bug number. ([bug 1704541](https://bugzilla.mozilla.org/show_bug.cgi?id=1704541))
+- Enable custom distribution outside of GeckoView (`gecko_datapoint` becomes optional)
+
+## 3.5.0 (2021-06-03)
+
+- Transform generated folder into QML Module when building Javascript templates for the Qt platform. ([bug 1707896](https://bugzilla.mozilla.org/show_bug.cgi?id=1707896)
+ - Import the Glean QML module from inside each generated file, removing the requirement to import Glean before importing any of the generated files;
+ - Prodive a `qmldir` file exposing all generated files;
+ - Drop the `namespace` option for Javascript templates;
+ - Add a new `version` option for Javascript templates, required when building for Qt, which expected the Glean QML module version.
+
+## 3.4.0 (2021-05-28)
+
+- Add missing import for Kotlin code ([#339](https://github.com/mozilla/glean_parser/pull/339))
+- Use a plain Kotlin type in the generated interface implementation ([#339](https://github.com/mozilla/glean_parser/pull/339))
+- Generate additional generics for event metrics ([#339](https://github.com/mozilla/glean_parser/pull/339))
+- For Kotlin skip generating `GleanBuildInfo.kt` when requested (with `with_buildinfo=false`) ([#341](https://github.com/mozilla/glean_parser/pull/341))
+
+## 3.3.2 (2021-05-18)
+
+- Fix another bug in the Swift code generation when generating extra keys ([#334](https://github.com/mozilla/glean_parser/pull/334))
+
+## 3.3.1 (2021-05-18)
+
+- Fix Swift code generation bug for pings ([#333](https://github.com/mozilla/glean_parser/pull/333))
+
+## 3.3.0 (2021-05-18)
+
+- Generate new event API construct ([#321](https://github.com/mozilla/glean_parser/pull/321))
+
+## 3.2.0 (2021-04-28)
+
+- Add option to add extra introductory text to generated markdown ([#298](https://github.com/mozilla/glean_parser/pull/298))
+- Add support for Qt in Javascript templates ([bug 1706252](https://bugzilla.mozilla.org/show_bug.cgi?id=1706252))
+ - Javascript templates will now accept the `platform` option. If this option is set to `qt`
+ the generated templates will be Qt compatible. Default value is `webext`.
+
+## 3.1.2 (2021-04-21)
+
+- BUGFIX: Remove the "DO NOT COMMIT" notice from the documentation.
+
+## 3.1.1 (2021-04-19)
+
+- Recommend to not commit as well as to not edit the generated files. ([bug 1706042](https://bugzilla.mozilla.org/show_bug.cgi?id=1706042))
+- BUGFIX: Include import statement for labeled metric subtypes in Javascript and Typescript templates.
+
+## 3.1.0 (2021-04-16)
+
+- Add support for labeled metric types in Javascript and Typescript templates.
+
+## 3.0.0 (2021-04-13)
+
+- Raise limit on number of statically-defined lables to 100. ([bug 1702263](https://bugzilla.mozilla.org/show_bug.cgi?id=1702263))
+- BUGFIX: Version 2.0.0 of the schema now allows the "special" `glean_.*` ping names for Glean-internal use again.
+- Remove support for JWE metric types.
+
+## 2.5.0 (2021-02-23)
+
+- Add parser and object model support for `rate` metric type. ([bug 1645166](https://bugzilla.mozilla.org/show_bug.cgi?id=1645166))
+- Add parser and object model support for telemetry_mirror property. ([bug 1685406](https://bugzilla.mozilla.org/show_bug.cgi?id=1685406))
+- Update the Javascript template to match Glean.js expectations. ([bug 1693516](https://bugzilla.mozilla.org/show_bug.cgi?id=1693516))
+ - Glean.js has updated it's export strategy. It will now export each metric type as an independent module;
+ - Glean.js has dropped support for non ES6 modules.
+- Add support for generating Typescript code. ([bug 1692157](https://bugzilla.mozilla.org/show_bug.cgi?id=1692157))
+ - The templates added generate metrics and pings code for Glean.js.
+
+## 2.4.0 (2021-02-18)
+
+- **Experimental:** `glean_parser` has a new subcommand `coverage` to convert raw coverage reports
+ into something consumable by coverage tools, such as codecov.io
+- The path to the file that each metric is defined in is now stored on the
+ `Metric` object in `defined_in["filepath"]`.
+
+## 2.3.0 (2021-02-17)
+
+- Leverage the `glean_namespace` to provide correct import when building for Javascript.
+
+## 2.2.0 (2021-02-11)
+
+- The Kotlin generator now generates static build information that can be passed
+ into `Glean.initialize` to avoid calling the package manager at runtime.
+
+## 2.1.0 (2021-02-10)
+
+- Add support for generating Javascript code.
+ - The templates added generate metrics and pings code for Glean.js.
+
+## 2.0.0 (2021-02-05)
+
+- New versions 2.0.0 of the `metrics.yaml` and `pings.yaml` schemas now ship
+ with `glean_parser`. These schemas are different from version 1.0.0 in the
+ following ways:
+
+ - Bugs must be specified as URLs. Bug numbers are disallowed.
+ - The legacy ping names containing underscores are no longer allowed. These
+ included `deletion_request`, `bookmarks_sync`, `history_sync`,
+ `session_end`, `all_pings`, `glean_*`). In these cases, the `_` should be
+ replaced with `-`.
+
+ To upgrade your app or library to use the new schema, replace the version in
+ the `$schema` value with `2-0-0`.
+
+- **Breaking change:** It is now an error to use bug numbers (rather than URLs)
+ in ping definitions.
+
+- Add the line number that metrics and pings were originally defined in the yaml
+ files.
+
+## 1.29.1 (2020-12-17)
+
+- BUGFIX: Linter output can now be redirected correctly (1675771).
+
+## 1.29.0 (2020-10-07)
+
+- **Breaking change:** `glean_parser` will now return an error code when any of
+ the input files do not exist (unless the `--allow-missing-files` flag is
+ passed).
+- Generated code now includes a comment next to each metric containing the name
+ of the metric in its original `snake_case` form.
+- When metrics don't provide a `unit` parameter, it is not included in the
+ output (as provided by probe-scraper).
+
+## 1.28.6 (2020-09-24)
+
+- BUGFIX: Ensure Kotlin arguments are deterministically ordered
+
+## 1.28.5 (2020-09-14)
+
+- Fix deploy step to update pip before deploying to pypi.
+
+## 1.28.4 (2020-09-14)
+
+- The `SUPERFLUOUS_NO_LINT` warning has been removed from the glinter.
+ It likely did more harm than good, and makes it hard to make
+ `metrics.yaml` files that pass across different versions of
+ `glean_parser`.
+- Expired metrics will now produce a linter warning, `EXPIRED_METRIC`.
+- Expiry dates that are more than 730 days (\~2 years) in the future
+ will produce a linter warning, `EXPIRATION_DATE_TOO_FAR`.
+- Allow using the Quantity metric type outside of Gecko.
+- New parser configs `custom_is_expired` and `custom_validate_expires`
+ added. These are both functions that take the `expires` value of the
+ metric and return a bool. (See `Metric.is_expired` and
+ `Metric.validate_expires`). These will allow FOG to provide custom
+ validation for its version-based `expires` values.
+
+## 1.28.3 (2020-07-28)
+
+- BUGFIX: Support HashSet and Dictionary in the C\## generated code.
+
+## 1.28.2 (2020-07-28)
+
+- BUGFIX: Generate valid C\## code when using Labeled metric types.
+
+## 1.28.1 (2020-07-24)
+
+- BUGFIX: Add missing column to correctly render markdown tables in generated
+ documentation.
+
+## 1.28.0 (2020-07-23)
+
+- **Breaking change:** The internal ping `deletion-request` was misnamed in
+ pings.py causing the linter to not allow use of the correctly named ping for
+ adding legacy ids to. Consuming apps will need to update their metrics.yaml if
+ they are using `deletion_request` in any `send_in_pings` to `deletion-request`
+ after updating.
+
+## 1.27.0 (2020-07-21)
+
+- Rename the `data_category` field to `data_sensitivity` to be clearer.
+
+## 1.26.0 (2020-07-21)
+
+- Add support for JWE metric types.
+- Add a `data_sensitivity` field to all metrics for specifying the type of data
+ collected in the field.
+
+## 1.25.0 (2020-07-17)
+
+- Add support for generating C\## code.
+- BUGFIX: The memory unit is now correctly passed to the MemoryDistribution
+ metric type in Swift.
+
+## 1.24.0 (2020-06-30)
+
+- BUGFIX: look for metrics in send\_if\_empty pings. Metrics for these kinds of
+ pings were being ignored.
+
+## 1.23.0 (2020-06-27)
+
+- Support for Python 3.5 has been dropped.
+- BUGFIX: The ordering of event extra keys will now match with their enum,
+ fixing a serious bug where keys of extras may not match the correct values in
+ the data payload. See <https://bugzilla.mozilla.org/show_bug.cgi?id=1648768>.
+
+## 1.22.0 (2020-05-28)
+
+- **Breaking change:** (Swift only) Combine all metrics and pings into a single
+ generated file `Metrics.swift`.
+
+## 1.21.0 (2020-05-25)
+
+- `glinter` messages have been improved with more details and to be more
+ actionable.
+- A maximum of 10 `extra_keys` is now enforced for `event` metric types.
+- BUGFIX: the `Lifetime` enum values now match the values of the implementation
+ in mozilla/glean.
+
+## 1.20.4 (2020-05-07)
+
+- BUGFIX: yamllint errors are now reported using the correct file name.
+
+## 1.20.3 (2020-05-06)
+
+- Support for using `timing_distribution`'s `time_unit` parameter to control
+ the range of acceptable values is documented. The default unit for this use
+ case is `nanosecond` to avoid creating a breaking change. See [bug
+ 1630997](https://bugzilla.mozilla.org/show_bug.cgi?id=1630997) for more
+ information.
+
+## 1.20.2 (2020-04-24)
+
+- Dependencies that depend on the version of Python being used are now specified
+ using the [Declaring platform specific dependencies syntax in
+ setuptools](https://setuptools.readthedocs.io/en/latest/setuptools.html##declaring-platform-specific-dependencies).
+ This means that more recent versions of dependencies are likely to be
+ installed on Python 3.6 and later, and unnecessary backport libraries won't
+ be installed on more recent Python versions.
+
+## 1.20.1 (2020-04-21)
+
+- The minimum version of the runtime dependencies has been lowered to increase
+ compatibility with other tools. These minimum versions are now tested in CI,
+ in addition to testing the latest versions of the dependencies that was
+ already happening in CI.
+
+## 1.20.0 (2020-04-15)
+
+- **Breaking change:** glinter errors found during the `translate` command will
+ now return an error code. glinter warnings will be displayed, but not return
+ an error code.
+- `glean_parser` now produces a linter warning when `user` lifetime metrics are
+ set to expire. See [bug
+ 1604854](https://bugzilla.mozilla.org/show_bug.cgi?id=1604854) for additional
+ context.
+
+## 1.19.0 (2020-03-18)
+
+- **Breaking change:** The regular expression used to validate labels is
+ stricter and more correct.
+- Add more information about pings to markdown documentation:
+ - State whether the ping includes client id;
+ - Add list of data review links;
+ - Add list of related bugs links.
+- `glean_parser` now makes it easier to write external translation
+ functions for different language targets.
+- BUGFIX: `glean_parser` now works on 32-bit Windows.
+
+## 1.18.3 (2020-02-24)
+
+- Dropped the `inflection` dependency.
+- Constrained the `zipp` and `MarkupSafe` transitive dependencies to versions
+ that support Python 3.5.
+
+## 1.18.2 (2020-02-14)
+
+- BUGFIX: Fix rendering of first element of reason list.
+
+## 1.18.1 (2020-02-14)
+
+- BUGFIX: Reason codes are displayed in markdown output for built-in
+ pings as well.
+- BUGFIX: Reason descriptions are indented correctly in markdown
+ output.
+- BUGFIX: To avoid a compiler error, the `@JvmName` annotation isn't
+ added to private members.
+
+## 1.18.0 (2020-02-13)
+
+- **Breaking Change (Java API)** Have the metrics names in Java match the names
+ in Kotlin. See [Bug
+ 1588060](https://bugzilla.mozilla.org/show_bug.cgi?id=1588060).
+- The reasons a ping are sent are now included in the generated markdown
+ documentation.
+
+## 1.17.3 (2020-02-05)
+
+- BUGFIX: The version of Jinja2 now specifies < 3.0, since that version no
+ longer supports Python 3.5.
+
+## 1.17.2 (2020-02-05)
+
+- BUGFIX: Fixes an import error in generated Kotlin code.
+
+## 1.17.1 (2020-02-05)
+
+- BUGFIX: Generated Swift code now includes `import Glean`, unless generating
+ for a Glean-internal build.
+
+## 1.17.0 (2020-02-03)
+
+- Remove default schema URL from `validate_ping`
+- Make `schema` argument required for CLI
+- BUGFIX: Avoid default import in Swift code for Glean itself
+- BUGFIX: Restore order of fields in generated Swift code
+
+## 1.16.0 (2020-01-15)
+
+- Support for `reason` codes on pings was added.
+
+## 1.15.6 (2020-02-06)
+
+- BUGFIX: The version of Jinja2 now specifies < 3.0, since that version no
+ longer supports Python 3.5 (backported from 1.17.3).
+
+## 1.15.5 (2019-12-19)
+
+- BUGFIX: Also allow the legacy name `all_pings` for `send_in_pings` parameter
+ on metrics
+
+## 1.15.4 (2019-12-19)
+
+- BUGFIX: Also allow the legacy name `all_pings`
+
+## 1.15.3 (2019-12-13)
+
+- Add project title to markdown template.
+- Remove "Sorry about that" from markdown template.
+- BUGFIX: Replace dashes in variable names to force proper naming
+
+## 1.15.2 (2019-12-12)
+
+- BUGFIX: Use a pure Python library for iso8601 so there is no compilation
+ required.
+
+## 1.15.1 (2019-12-12)
+
+- BUGFIX: Add some additional ping names to the non-kebab-case allow list.
+
+## 1.15.0 (2019-12-12)
+
+- Restrict new pings names to be kebab-case and change `all_pings` to
+ `all-pings`
+
+## 1.14.0 (2019-12-06)
+
+- `glean_parser` now supports Python versions 3.5, 3.6, 3.7 and 3.8.
+
+## 1.13.0 (2019-12-04)
+
+- The `translate` command will no longer clear extra files in the output
+ directory.
+- BUGFIX: Ensure all newlines in comments are prefixed with comment markers
+- BUGFIX: Escape Swift keywords in variable names in generated code
+- Generate documentation for pings that are sent if empty
+
+## 1.12.0 (2019-11-27)
+
+- Reserve the `deletion_request` ping name
+- Added a new flag `send_if_empty` for pings
+
+## 1.11.0 (2019-11-13)
+
+- The `glinter` command now performs `yamllint` validation on registry files.
+
+## 1.10.0 (2019-11-11)
+
+- The Kotlin linter `detekt` is now run during CI, and for local
+ testing if installed.
+- Python 3.8 is now tested in CI (in addition to Python 3.7). Using
+ `tox` for this doesn't work in modern versions of CircleCI, so the
+ `tox` configuration has been removed.
+- `yamllint` has been added to test the YAML files on CI.
+- ⚠ Metric types that don't yet have implementations in glean-core
+ have been removed. This includes `enumeration`, `rate`, `usage`, and
+ `use_counter`, as well as many labeled metrics that don't exist.
+
+## 1.9.5 (2019-10-22)
+
+- Allow a Swift lint for generated code
+- New lint: Restrict what metric can go into the `baseline` ping
+- New lint: Warn for slight misspellings in ping names
+- BUGFIX: change Labeled types labels from lists to sets.
+
+## 1.9.4 (2019-10-16)
+
+- Use lists instead of sets in Labeled types labels to ensure that the order of
+ the labels passed to the `metrics.yaml` is kept.
+- `glinter` will now check for duplicate labels and error if there are any.
+
+## 1.9.3 (2019-10-09)
+
+- Add labels from Labeled types to the Extra column in the Markdown template.
+
+## 1.9.2 (2019-10-08)
+
+- BUGFIX: Don't call `is_internal_metric` on `Ping` objects.
+
+## 1.9.1 (2019-10-07)
+
+- Don't include Glean internal metrics in the generated markdown.
+
+## 1.9.0 (2019-10-04)
+
+- Glinter now warns when bug numbers (rather than URLs) are used.
+- BUGFIX: add `HistogramType` and `MemoryUnit` imports in Kotlin generated code.
+
+## 1.8.4 (2019-10-02)
+
+- Removed unsupported labeled metric types.
+
+## 1.8.3 (2019-10-02)
+
+- Fix indentation for generated Swift code
+
+## 1.8.2 (2019-10-01)
+
+- Created labeled metrics and events in Swift code and wrap it in a
+ configured namespace
+
+## 1.8.1 (2019-09-27)
+
+- BUGFIX: `memory_unit` is now passed to the Kotlin generator.
+
+## 1.8.0 (2019-09-26)
+
+- A new parser config, `do_not_disable_expired`, was added to turn off the
+ feature that expired metrics are automatically disabled. This is useful if you
+ want to retain the disabled value that is explicitly in the `metrics.yaml`
+ file.
+- `glinter` will now report about superfluous `no_lint` entries.
+
+## 1.7.0 (2019-09-24)
+
+- A `glinter` tool is now included to find common mistakes in metric naming
+ and setup. This check is run during `translate` and warnings will be
+ displayed. ⚠ These warnings will be treated as errors in a future revision.
+
+## 1.6.1 (2019-09-17)
+
+- BUGFIX: `GleanGeckoMetricsMapping` must include `LabeledMetricType`
+ and `CounterMetricType`.
+
+## 1.6.0 (2019-09-17)
+
+- NEW: Support for outputting metrics in Swift.
+- BUGFIX: Provides a helpful error message when `geckoview_datapoint` is used on
+ an metric type that doesn't support GeckoView exfiltration.
+- Generate a lookup table for Gecko categorical histograms in
+ `GleanGeckoMetricsMapping`.
+- Introduce a 'Swift' output generator.
+
+## 1.4.1 (2019-08-28)
+
+- Documentation only.
+
+## 1.4.0 (2019-08-27)
+
+- Added support for generating markdown documentation from `metrics.yaml` files.
+
+## 1.3.0 (2019-08-22)
+
+- `quantity` metric type has been added.
+
+## 1.2.1 (2019-08-13)
+
+- BUGFIX: `includeClientId` was not being output for PingType.
+
+## 1.2.0 (2019-08-13)
+
+- `memory_distribution` metric type has been added.
+- `custom_distribution` metric type has been added.
+- `labeled_timespan` is no longer an allowed metric type.
+
+## 1.1.0 (2019-08-05)
+
+- Add a special `all_pings` value to `send_in_pings`.
+
+## 1.0.0 (2019-07-29)
+
+- First release to start following strict semver.
+
+## 0.1.0 (2018-10-15)
+
+- First release on PyPI.
diff --git a/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/RECORD b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/RECORD
new file mode 100644
index 0000000000..417484d30b
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/RECORD
@@ -0,0 +1,44 @@
+glean_parser/__init__.py,sha256=bJljD052_0y-efcBhYpllICVCXOMHLcXRLNyrvfgt5A,533
+glean_parser/__main__.py,sha256=Rw0PpuQtAvdHJMK1YLozeZkc6x1yjeNZwidu4faovdk,8633
+glean_parser/coverage.py,sha256=2IwC4XMDtDamMkBFoYilmqJzW4gyypq65YVCur8SNas,4405
+glean_parser/data_review.py,sha256=BweeeTkNNS6HrIDkztawhbDByrk_-Avxpg7YeST3VAs,2152
+glean_parser/javascript.py,sha256=w4ZhNBHBKWYk0h3t7G0Ud2tR__hRqzn9dlEXNKLdQrA,11230
+glean_parser/javascript_server.py,sha256=SDV9tPL1uZMyS1VSyo5lOFuNPFHZu-PZxr1vhND-GzM,7971
+glean_parser/kotlin.py,sha256=5z8_74xlqvHDsedwZhGf1_qb7swPEgIZumkJIuj3ef8,12598
+glean_parser/lint.py,sha256=STqdgyOhR4Q3fHivSizgn9bOOyqrNHhzjaqyJxz6qzI,19948
+glean_parser/markdown.py,sha256=GkCr1CrV6mnRQseT6FO1-JJ7Eup8X3lxUfRMBTxXpe4,9066
+glean_parser/metrics.py,sha256=uWOJdZRB9udMni2EWXcL3P1T4pRIlJ5kiE5fJsHkmdU,12450
+glean_parser/parser.py,sha256=cUOnvSXKfEBg8YTpRcWiPcMwpFpK1TTqsVO_zjUtpR4,15309
+glean_parser/pings.py,sha256=la9HdJTjtSqC7vc5-JuANW0otbozTnFARlIMgKoypGU,2982
+glean_parser/ruby_server.py,sha256=-bNXjfXWwHWUHmLJVvfi6jCyw8q0MBwx9VXVWQ3bU-A,5189
+glean_parser/rust.py,sha256=PJzTfYWzAumJYCP5IYPc6fhS_Qa30Q8NTK9plg3sDnk,6744
+glean_parser/swift.py,sha256=T1BSGahd9wUd6VDeNC89SdN6M34jKXDlydMpSI0QLOs,8379
+glean_parser/tags.py,sha256=bemKYvcbMO4JrghiNSe-A4BNNDtx_FlUPkgrPPJy84Y,1391
+glean_parser/translate.py,sha256=C7FY7AAbnVsPZOu2bKELW1CfTwnvLGpmgzY7uMDqOec,8233
+glean_parser/translation_options.py,sha256=Lxzr6G7MP0tC_ZYlZXftS4j0SLiqO-5mGVTEc7ggXis,2037
+glean_parser/util.py,sha256=X5YFAU4kWdDJjMsJzXH-QJVSjUJc_qvXktiM-dJSfzo,16004
+glean_parser/validate_ping.py,sha256=0TNvILH6dtzJDys3W8Kqorw6kk03me73OCUDtpoHcXU,2118
+glean_parser/schemas/metrics.1-0-0.schema.yaml,sha256=cND3cvi6iBfPUVmtfIBQfGJV9AALpbvN7nu8E33_J-o,19566
+glean_parser/schemas/metrics.2-0-0.schema.yaml,sha256=sfrARxefWy1WN5HxUKjwjN8lGobbPds5l7Y46VHfP1g,25849
+glean_parser/schemas/pings.1-0-0.schema.yaml,sha256=hwCnsKpEysmrmVp-QHGBArEkVY3vaU1rVsxlTwhAzws,4315
+glean_parser/schemas/pings.2-0-0.schema.yaml,sha256=l-nIuyXJ9-D0X_U6hzGVbhIBhtZDg-rGau-RDrhgpng,4705
+glean_parser/schemas/tags.1-0-0.schema.yaml,sha256=OGXIJlvvVW1vaqB_NVZnwKeZ-sLlfH57vjBSHbj6DNI,1231
+glean_parser/templates/data_review.jinja2,sha256=jeYU29T1zLSyu9fKBBFu5BFPfIw8_hmOUXw8RXhRXK8,3287
+glean_parser/templates/javascript.buildinfo.jinja2,sha256=4mXiZCQIk9if4lxlA05kpSIL4a95IdwGwqle2OqqNAs,474
+glean_parser/templates/javascript.jinja2,sha256=cT_bG-jC6m4afECXmcsqHwiiHjRuVtJnfv90OD2Mwxw,2669
+glean_parser/templates/javascript_server.jinja2,sha256=H991yQOKJMwSgM0bLEA-Q5Z15LWsfEPh6bTYz_owSCU,9423
+glean_parser/templates/kotlin.buildinfo.jinja2,sha256=X0lk2SNu5OIIj2i6mUyF9CWFQIonLgfqkgT5fA-5G6c,920
+glean_parser/templates/kotlin.geckoview.jinja2,sha256=MJOgtoDXmBjE9pwk-G6T89y36RZuMbDWM_-DBN_gFJo,5099
+glean_parser/templates/kotlin.jinja2,sha256=3DqUMXJRkmTvSp_5IRyvGmw5iXYWdox7coMFe3YDxcc,5247
+glean_parser/templates/markdown.jinja2,sha256=vAHHGGm28HRDPd3zO_wQMAUZIuxE9uQ7hl3NpXxcKV4,3425
+glean_parser/templates/qmldir.jinja2,sha256=m6IGsp-tgTiOfQ7VN8XW6GqX0gJqJkt3B6Pkaul6FVo,156
+glean_parser/templates/ruby_server.jinja2,sha256=vm4BEenOqzomQNTLFfMOzlWHARnsWUjTBbnR-v2cadI,6247
+glean_parser/templates/rust.jinja2,sha256=pdbjq_JGm8XWHsVXk0m2xZ5Pd-Y9T_zxJfZKBoT0ERU,3635
+glean_parser/templates/swift.jinja2,sha256=NfZdvrG8LGT4H2AWk-vB_GDTMcpW1XZJcApO4OF5AYE,4874
+glean_parser-11.0.1.dist-info/AUTHORS.md,sha256=yxgj8MioO4wUnrh0gmfb8l3DJJrf-l4HmmEDbQsbbNI,455
+glean_parser-11.0.1.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
+glean_parser-11.0.1.dist-info/METADATA,sha256=z5yLEYgY4EV1e_cHNQhenhkwK5ryURgljfTfaYK-NYs,30877
+glean_parser-11.0.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
+glean_parser-11.0.1.dist-info/entry_points.txt,sha256=mf9d3sv8BwSjjR58x9KDnpVkONCnv3fPQC2NjJl15Xg,68
+glean_parser-11.0.1.dist-info/top_level.txt,sha256=q7T3duD-9tYZFyDry6Wv2LcdMsK2jGnzdDFhxWcT2Z8,13
+glean_parser-11.0.1.dist-info/RECORD,,
diff --git a/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/WHEEL b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/WHEEL
new file mode 100644
index 0000000000..98c0d20b7a
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.42.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/entry_points.txt b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/entry_points.txt
new file mode 100644
index 0000000000..08fde9d655
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/entry_points.txt
@@ -0,0 +1,2 @@
+[console_scripts]
+glean_parser = glean_parser.__main__:main_wrapper
diff --git a/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/top_level.txt b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/top_level.txt
new file mode 100644
index 0000000000..a7f3a37918
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser-11.0.1.dist-info/top_level.txt
@@ -0,0 +1 @@
+glean_parser
diff --git a/third_party/python/glean_parser/glean_parser/__init__.py b/third_party/python/glean_parser/glean_parser/__init__.py
new file mode 100644
index 0000000000..ddca930c79
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""Top-level package for Glean parser."""
+
+from pkg_resources import get_distribution, DistributionNotFound
+
+try:
+ __version__ = get_distribution(__name__).version
+except DistributionNotFound:
+ # package is not installed
+ pass
+
+__author__ = """The Glean Team"""
+__email__ = "glean-team@mozilla.com"
diff --git a/third_party/python/glean_parser/glean_parser/__main__.py b/third_party/python/glean_parser/glean_parser/__main__.py
new file mode 100644
index 0000000000..7714502b56
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/__main__.py
@@ -0,0 +1,351 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""Console script for glean_parser."""
+
+import datetime
+import io
+from pathlib import Path
+import sys
+
+import click
+import json
+
+
+import glean_parser
+
+
+from . import coverage as mod_coverage
+from . import data_review as mod_data_review
+from . import lint
+from . import translate as mod_translate
+from . import validate_ping
+from . import translation_options
+
+
+CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
+
+
+@click.command(context_settings=CONTEXT_SETTINGS)
+@click.argument(
+ "input",
+ type=click.Path(exists=False, dir_okay=False, file_okay=True, readable=True),
+ nargs=-1,
+)
+@click.option(
+ "--output",
+ "-o",
+ type=click.Path(dir_okay=True, file_okay=False, writable=True),
+ nargs=1,
+ required=True,
+)
+@click.option(
+ "--format",
+ "-f",
+ type=click.Choice(list(mod_translate.OUTPUTTERS.keys())),
+ required=True,
+)
+@click.option(
+ "--option",
+ "-s",
+ help="Backend-specific option. Must be of the form key=value.\
+ Pass 'help' for valid options",
+ type=str,
+ multiple=True,
+ required=False,
+ is_eager=True,
+ callback=translation_options.translate_options,
+)
+@click.option(
+ "--allow-reserved",
+ is_flag=True,
+ help=(
+ "If provided, allow the use of reserved fields. "
+ "Should only be set when building the Glean library itself."
+ ),
+)
+@click.option(
+ "--allow-missing-files",
+ is_flag=True,
+ help=("Do not treat missing input files as an error."),
+)
+@click.option(
+ "--require-tags",
+ is_flag=True,
+ help=("Require tags to be specified for metrics and pings."),
+)
+@click.option(
+ "--expire-by-version",
+ help="Expire metrics by version, with the provided major version.",
+ type=click.INT,
+ required=False,
+)
+def translate(
+ input,
+ format,
+ output,
+ option,
+ allow_reserved,
+ allow_missing_files,
+ require_tags,
+ expire_by_version,
+):
+ """
+ Translate metrics.yaml and pings.yaml files to other formats.
+ """
+ option_dict = {}
+ for opt in option:
+ key, val = opt.split("=", 1)
+ option_dict[key] = val
+
+ sys.exit(
+ mod_translate.translate(
+ [Path(x) for x in input],
+ format,
+ Path(output),
+ option_dict,
+ {
+ "allow_reserved": allow_reserved,
+ "allow_missing_files": allow_missing_files,
+ "require_tags": require_tags,
+ "expire_by_version": expire_by_version,
+ },
+ )
+ )
+
+
+@click.command()
+@click.option(
+ "--schema",
+ "-s",
+ type=str,
+ nargs=1,
+ required=True,
+ help=("HTTP url or file path to Glean ping schema. If remote, will cache to disk."),
+)
+def check(schema):
+ """
+ Validate the contents of a Glean ping.
+
+ The ping contents are read from stdin, and the validation errors are
+ written to stdout.
+ """
+ sys.exit(
+ validate_ping.validate_ping(
+ io.TextIOWrapper(sys.stdin.buffer, encoding="utf-8"),
+ io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8"),
+ schema_url=schema,
+ )
+ )
+
+
+@click.command()
+@click.argument(
+ "input",
+ type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True),
+ nargs=-1,
+)
+@click.option(
+ "--allow-reserved",
+ is_flag=True,
+ help=(
+ "If provided, allow the use of reserved fields. "
+ "Should only be set when building the Glean library itself."
+ ),
+)
+@click.option(
+ "--allow-missing-files",
+ is_flag=True,
+ help=("Do not treat missing input files as an error."),
+)
+@click.option(
+ "--require-tags",
+ is_flag=True,
+ help=("Require tags to be specified for metrics and pings."),
+)
+def glinter(input, allow_reserved, allow_missing_files, require_tags):
+ """
+ Runs a linter over the metrics.
+ """
+ sys.exit(
+ lint.glinter(
+ [Path(x) for x in input],
+ {
+ "allow_reserved": allow_reserved,
+ "allow_missing_files": allow_missing_files,
+ "require_tags": require_tags,
+ },
+ )
+ )
+
+
+@click.command()
+@click.argument(
+ "input",
+ type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True),
+ nargs=-1,
+)
+@click.option(
+ "--allow-reserved",
+ is_flag=True,
+ help=(
+ "If provided, allow the use of reserved fields. "
+ "Should only be set when building the Glean library itself."
+ ),
+)
+@click.option(
+ "--allow-missing-files",
+ is_flag=True,
+ help=("Do not treat missing input files as an error."),
+)
+@click.option(
+ "--require-tags",
+ is_flag=True,
+ help=("Require tags to be specified for metrics and pings."),
+)
+def dump(input, allow_reserved, allow_missing_files, require_tags):
+ """
+ Dump the list of metrics/pings as JSON to stdout.
+ """
+
+ results = glean_parser.parser.parse_objects(
+ [Path(x) for x in input],
+ {
+ "allow_reserved": allow_reserved,
+ "allow_missing_files": allow_missing_files,
+ "require_tags": require_tags,
+ },
+ )
+ errs = list(results)
+ assert len(errs) == 0
+
+ metrics = {
+ metric.identifier(): metric.serialize()
+ for category, probes in results.value.items()
+ for probe_name, metric in probes.items()
+ }
+
+ def date_serializer(o):
+ if isinstance(o, datetime.datetime):
+ return o.isoformat()
+
+ print(
+ json.dumps(
+ metrics,
+ sort_keys=True,
+ indent=2,
+ separators=(",", ": "),
+ default=date_serializer,
+ )
+ )
+
+
+@click.command()
+@click.option(
+ "-c",
+ "--coverage_file",
+ type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True),
+ required=True,
+ multiple=True,
+)
+@click.argument(
+ "metrics_files",
+ type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True),
+ nargs=-1,
+)
+@click.option(
+ "-o",
+ "--output",
+ type=click.Path(exists=False, dir_okay=False, file_okay=True, writable=True),
+ required=True,
+)
+@click.option(
+ "--format",
+ "-f",
+ type=click.Choice(list(mod_coverage.OUTPUTTERS.keys())),
+ required=True,
+)
+@click.option(
+ "--allow-reserved",
+ is_flag=True,
+ help=(
+ "If provided, allow the use of reserved fields. "
+ "Should only be set when building the Glean library itself."
+ ),
+)
+def coverage(coverage_file, metrics_files, format, output, allow_reserved):
+ """
+ Produce a coverage analysis file given raw coverage output and a set of
+ metrics.yaml files.
+ """
+ sys.exit(
+ mod_coverage.coverage(
+ [Path(x) for x in coverage_file],
+ [Path(x) for x in metrics_files],
+ format,
+ Path(output),
+ {
+ "allow_reserved": allow_reserved,
+ },
+ )
+ )
+
+
+@click.command()
+@click.argument("bug", type=str)
+@click.argument(
+ "metrics_files",
+ type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True),
+ nargs=-1,
+)
+def data_review_request(bug, metrics_files):
+ """
+ Generate a skeleton Data Review Request for all metrics in METRICS_FILES
+ whose bug_numbers fields contain the provided BUG string.
+
+ For example, providing "1694739" matches
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1694739".
+ To ensure substrings don't match, the provided bug string will match only
+ if it is bounded by non-word characters.
+
+ Prints to stdout.
+ """
+ sys.exit(mod_data_review.generate(bug, [Path(x) for x in metrics_files]))
+
+
+@click.group()
+@click.version_option(glean_parser.__version__, prog_name="glean_parser")
+def main(args=None):
+ """Command line utility for glean_parser."""
+ pass
+
+
+main.add_command(translate)
+main.add_command(check)
+main.add_command(glinter)
+main.add_command(dump)
+main.add_command(coverage)
+main.add_command(data_review_request, "data-review")
+
+
+def main_wrapper(args=None):
+ """
+ A simple wrapper around click's `main` to display the glean_parser version
+ when there is an error.
+ """
+ try:
+ main(args=args)
+ except SystemExit as e:
+ if e.code != 0:
+ print(
+ f"ERROR running glean_parser v{glean_parser.__version__}",
+ file=sys.stderr,
+ )
+ raise
+
+
+if __name__ == "__main__":
+ main_wrapper() # pragma: no cover
diff --git a/third_party/python/glean_parser/glean_parser/coverage.py b/third_party/python/glean_parser/glean_parser/coverage.py
new file mode 100644
index 0000000000..776ea3183d
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/coverage.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Produce coverage reports from the raw information produced by the
+`GLEAN_TEST_COVERAGE` feature.
+"""
+
+import json
+from .metrics import ObjectTree
+from pathlib import Path
+import sys
+from typing import Any, Dict, List, Optional, Sequence, Set
+
+
+from . import parser
+from . import util
+
+
+def _outputter_codecovio(metrics: ObjectTree, output_path: Path):
+ """
+ Output coverage in codecov.io format as defined here:
+
+ https://docs.codecov.io/docs/codecov-custom-coverage-format
+
+ :param metrics: The tree of metrics, already annotated with coverage by
+ `_annotate_coverage`.
+ :param output_path: The file to output to.
+ """
+ coverage: Dict[str, List] = {}
+ for category in metrics.values():
+ for metric in category.values():
+ defined_in = metric.defined_in
+ if defined_in is not None:
+ path = defined_in["filepath"]
+ if path not in coverage:
+ with open(path) as fd:
+ nlines = len(list(fd.readlines()))
+ lines = [None] * nlines
+ coverage[path] = lines
+ file_section = coverage[path]
+ file_section[int(defined_in["line"])] = getattr(metric, "covered", 0)
+
+ with open(output_path, "w") as fd:
+ json.dump({"coverage": coverage}, fd)
+
+
+OUTPUTTERS = {"codecovio": _outputter_codecovio}
+
+
+def _annotate_coverage(metrics, coverage_entries):
+ """
+ Annotate each metric with whether it is covered. Sets the attribute
+ `covered` to 1 on each metric that is covered.
+ """
+ mapping = {}
+ for category in metrics.values():
+ for metric in category.values():
+ mapping[metric.identifier()] = metric
+
+ for entry in coverage_entries:
+ metric_id = _coverage_entry_to_metric_id(entry)
+ if metric_id in mapping:
+ mapping[metric_id].covered = 1
+
+
+def _coverage_entry_to_metric_id(entry: str) -> str:
+ """
+ Convert a coverage entry to a metric id.
+
+ Technically, the coverage entries are rkv database keys, so are not just
+ the metric identifier. This extracts the metric identifier part out.
+ """
+ # If getting a glean error count, report it as covering the metric the
+ # error occurred in, not the `glean.error.*` metric itself.
+ if entry.startswith("glean.error."):
+ entry = entry.split("/")[-1]
+ # If a labeled metric, strip off the label part
+ return entry.split("/")[0]
+
+
+def _read_coverage_entries(coverage_reports: List[Path]) -> Set[str]:
+ """
+ Read coverage entries from one or more files, and deduplicates them.
+ """
+ entries = set()
+
+ for coverage_report in coverage_reports:
+ with open(coverage_report) as fd:
+ for line in fd.readlines():
+ entries.add(line.strip())
+
+ return entries
+
+
+def coverage(
+ coverage_reports: List[Path],
+ metrics_files: Sequence[Path],
+ output_format: str,
+ output_file: Path,
+ parser_config: Optional[Dict[str, Any]] = None,
+ file=sys.stderr,
+) -> int:
+ """
+ Commandline helper for coverage.
+
+ :param coverage_reports: List of coverage report files, output from the
+ Glean SDK when the `GLEAN_TEST_COVERAGE` environment variable is set.
+ :param metrics_files: List of Path objects to load metrics from.
+ :param output_format: The coverage output format to produce. Must be one of
+ `OUTPUTTERS.keys()`.
+ :param output_file: Path to output coverage report to.
+ :param parser_config: Parser configuration object, passed to
+ `parser.parse_objects`.
+ :return: Non-zero if there were any errors.
+ """
+
+ if parser_config is None:
+ parser_config = {}
+
+ if output_format not in OUTPUTTERS:
+ raise ValueError(f"Unknown outputter {output_format}")
+
+ metrics_files = util.ensure_list(metrics_files)
+
+ all_objects = parser.parse_objects(metrics_files, parser_config)
+
+ if util.report_validation_errors(all_objects):
+ return 1
+
+ entries = _read_coverage_entries(coverage_reports)
+
+ _annotate_coverage(all_objects.value, entries)
+
+ OUTPUTTERS[output_format](all_objects.value, output_file)
+
+ return 0
diff --git a/third_party/python/glean_parser/glean_parser/data_review.py b/third_party/python/glean_parser/glean_parser/data_review.py
new file mode 100644
index 0000000000..2267d49315
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/data_review.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Produce skeleton Data Review Requests.
+"""
+
+from pathlib import Path
+from typing import Sequence
+import re
+
+
+from . import parser
+from . import util
+
+
+def generate(
+ bug: str,
+ metrics_files: Sequence[Path],
+) -> int:
+ """
+ Commandline helper for Data Review Request template generation.
+
+ :param bug: pattern to match in metrics' bug_numbers lists.
+ :param metrics_files: List of Path objects to load metrics from.
+ :return: Non-zero if there were any errors.
+ """
+
+ metrics_files = util.ensure_list(metrics_files)
+
+ # Accept any value of expires.
+ parser_options = {
+ "allow_reserved": True,
+ "custom_is_expired": lambda expires: False,
+ "custom_validate_expires": lambda expires: True,
+ }
+ all_objects = parser.parse_objects(metrics_files, parser_options)
+
+ if util.report_validation_errors(all_objects):
+ return 1
+
+ # I tried [\W\Z] but it complained. So `|` it is.
+ reobj = re.compile(f"\\W{bug}\\W|\\W{bug}$")
+ durations = set()
+ responsible_emails = set()
+ filtered_metrics = list()
+ for metrics in all_objects.value.values():
+ for metric in metrics.values():
+ if not any([len(reobj.findall(bug)) == 1 for bug in metric.bugs]):
+ continue
+
+ filtered_metrics.append(metric)
+
+ durations.add(metric.expires)
+
+ if metric.expires == "never":
+ responsible_emails.update(metric.notification_emails)
+
+ if len(filtered_metrics) == 0:
+ print(f"I'm sorry, I couldn't find metrics matching the bug number {bug}.")
+ return 1
+
+ template = util.get_jinja2_template(
+ "data_review.jinja2",
+ filters=(("snake_case", util.snake_case),),
+ )
+
+ print(
+ template.render(
+ metrics=filtered_metrics,
+ durations=durations,
+ responsible_emails=responsible_emails,
+ )
+ )
+
+ return 0
diff --git a/third_party/python/glean_parser/glean_parser/javascript.py b/third_party/python/glean_parser/glean_parser/javascript.py
new file mode 100644
index 0000000000..1473065beb
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/javascript.py
@@ -0,0 +1,322 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Outputter to generate Javascript code for metrics.
+"""
+
+import enum
+import json
+from pathlib import Path
+from typing import Any, Dict, Optional, Callable
+
+from . import __version__
+from . import metrics
+from . import util
+
+
+def javascript_datatypes_filter(value: util.JSONType) -> str:
+ """
+ A Jinja2 filter that renders Javascript literals.
+
+ Based on Python's JSONEncoder, but overrides:
+ - lists to use listOf
+ - sets to use setOf
+ - Rate objects to a CommonMetricData initializer
+ (for external Denominators' Numerators lists)
+ """
+
+ class JavascriptEncoder(json.JSONEncoder):
+ def iterencode(self, value):
+ if isinstance(value, enum.Enum):
+ yield from super().iterencode(util.camelize(value.name))
+ elif isinstance(value, list):
+ yield "["
+ first = True
+ for subvalue in value:
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield "]"
+ elif isinstance(value, set):
+ yield "["
+ first = True
+ for subvalue in sorted(list(value)):
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield "]"
+ elif isinstance(value, metrics.Rate):
+ yield "CommonMetricData("
+ first = True
+ for arg_name in util.common_metric_args:
+ if hasattr(value, arg_name):
+ if not first:
+ yield ", "
+ yield f"{util.camelize(arg_name)} = "
+ yield from self.iterencode(getattr(value, arg_name))
+ first = False
+ yield ")"
+ else:
+ yield from super().iterencode(value)
+
+ return "".join(JavascriptEncoder().iterencode(value))
+
+
+def class_name_factory(platform: str) -> Callable[[str], str]:
+ """
+ Returns a function that receives an obj_type and
+ returns the correct class name for that type in the current platform.
+ """
+
+ def class_name(obj_type: str) -> str:
+ if obj_type == "ping":
+ class_name = "PingType"
+ else:
+ if obj_type.startswith("labeled_"):
+ obj_type = obj_type[8:]
+ class_name = util.Camelize(obj_type) + "MetricType"
+
+ if platform == "qt":
+ return "Glean.Glean._private." + class_name
+
+ return class_name
+
+ return class_name
+
+
+def extra_type_name(extra_type: str) -> str:
+ """
+ Returns the equivalent TypeScript type to an extra type.
+ """
+ if extra_type == "quantity":
+ return "number"
+
+ return extra_type
+
+
+def import_path(obj_type: str) -> str:
+ """
+ Returns the import path of the given object inside the @mozilla/glean package.
+ """
+ if obj_type == "ping":
+ import_path = "ping"
+ else:
+ if obj_type.startswith("labeled_"):
+ obj_type = obj_type[8:]
+ import_path = "metrics/" + obj_type
+
+ return import_path
+
+
+def args(obj_type: str) -> Dict[str, object]:
+ """
+ Returns the list of arguments for each object type.
+ """
+ if obj_type == "ping":
+ return {"common": util.ping_args, "extra": []}
+
+ return {"common": util.common_metric_args, "extra": util.extra_metric_args}
+
+
+def generate_build_date(date: Optional[str]) -> str:
+ """
+ Generate the build Date object.
+ """
+
+ ts = util.build_date(date)
+
+ data = [
+ str(ts.year),
+ # In JavaScript the first month of the year in calendars is JANUARY which is 0.
+ # In Python it's 1-based
+ str(ts.month - 1),
+ str(ts.day),
+ str(ts.hour),
+ str(ts.minute),
+ str(ts.second),
+ ]
+ components = ", ".join(data)
+
+ # DatetimeMetricType takes a `Date` instance.
+ return f"new Date({components})" # noqa
+
+
+def output(
+ lang: str,
+ objs: metrics.ObjectTree,
+ output_dir: Path,
+ options: Optional[Dict[str, Any]] = None,
+) -> None:
+ """
+ Given a tree of objects, output Javascript or Typescript code to `output_dir`.
+
+ :param lang: Either "javascript" or "typescript";
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ :param options: options dictionary, with the following optional keys:
+ - `platform`: Which platform are we building for. Options are `webext` and `qt`.
+ Default is `webext`.
+ - `version`: The version of the Glean.js Qt library being used.
+ This option is mandatory when targeting Qt. Note that the version
+ string must only contain the major and minor version i.e. 0.14.
+ - `with_buildinfo`: If "true" a `gleanBuildInfo.(js|ts)` file is generated.
+ Otherwise generation of that file is skipped. Defaults to "false".
+ - `build_date`: If set to `0` a static unix epoch time will be used.
+ If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
+ it will use that date.
+ Other values will throw an error.
+ If not set it will use the current date & time.
+ """
+
+ if options is None:
+ options = {}
+
+ platform = options.get("platform", "webext")
+ accepted_platforms = ["qt", "webext", "node"]
+ if platform not in accepted_platforms:
+ raise ValueError(
+ f"Unknown platform: {platform}. Accepted platforms are: {accepted_platforms}." # noqa
+ )
+ version = options.get("version")
+ if platform == "qt" and version is None:
+ raise ValueError(
+ "'version' option is required when building for the 'qt' platform."
+ )
+
+ template = util.get_jinja2_template(
+ "javascript.jinja2",
+ filters=(
+ ("class_name", class_name_factory(platform)),
+ ("extra_type_name", extra_type_name),
+ ("import_path", import_path),
+ ("js", javascript_datatypes_filter),
+ ("args", args),
+ ),
+ )
+
+ for category_key, category_val in objs.items():
+ extension = ".js" if lang == "javascript" else ".ts"
+ filename = util.camelize(category_key) + extension
+ filepath = output_dir / filename
+
+ types = set(
+ [
+ # This takes care of the regular metric type imports
+ # as well as the labeled metric subtype imports,
+ # thus the removal of the `labeled_` substring.
+ #
+ # The actual LabeledMetricType import is conditioned after
+ # the `has_labeled_metrics` boolean.
+ obj.type if not obj.type.startswith("labeled_") else obj.type[8:]
+ for obj in category_val.values()
+ ]
+ )
+ has_labeled_metrics = any(
+ getattr(metric, "labeled", False) for metric in category_val.values()
+ )
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ category_name=category_key,
+ objs=category_val,
+ extra_args=util.extra_args,
+ platform=platform,
+ version=version,
+ has_labeled_metrics=has_labeled_metrics,
+ types=types,
+ lang=lang,
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
+
+ with_buildinfo = options.get("with_buildinfo", "").lower() == "true"
+ build_date = options.get("build_date", None)
+ if with_buildinfo:
+ # Write out the special "build info" file
+ template = util.get_jinja2_template(
+ "javascript.buildinfo.jinja2",
+ )
+ # This filename needs to start with "glean" so it can never
+ # clash with a metric category
+ filename = "gleanBuildInfo" + extension
+ filepath = output_dir / filename
+
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ platform=platform,
+ build_date=generate_build_date(build_date),
+ )
+ )
+ fd.write("\n")
+
+ if platform == "qt":
+ # Explicitly create a qmldir file when building for Qt
+ template = util.get_jinja2_template("qmldir.jinja2")
+ filepath = output_dir / "qmldir"
+
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__, categories=objs.keys(), version=version
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
+
+
+def output_javascript(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Javascript code to `output_dir`.
+
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ :param options: options dictionary, with the following optional keys:
+
+ - `namespace`: The identifier of the global variable to assign to.
+ This will only have and effect for Qt and static web sites.
+ Default is `Glean`.
+ - `platform`: Which platform are we building for. Options are `webext` and `qt`.
+ Default is `webext`.
+ """
+
+ output("javascript", objs, output_dir, options)
+
+
+def output_typescript(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Typescript code to `output_dir`.
+
+ # Note
+
+ The only difference between the typescript and javascript templates,
+ currently is the file extension.
+
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ :param options: options dictionary, with the following optional keys:
+
+ - `namespace`: The identifier of the global variable to assign to.
+ This will only have and effect for Qt and static web sites.
+ Default is `Glean`.
+ - `platform`: Which platform are we building for. Options are `webext` and `qt`.
+ Default is `webext`.
+ """
+
+ output("typescript", objs, output_dir, options)
diff --git a/third_party/python/glean_parser/glean_parser/javascript_server.py b/third_party/python/glean_parser/glean_parser/javascript_server.py
new file mode 100644
index 0000000000..cdaa0cb403
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/javascript_server.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Outputter to generate server Javascript code for collecting events.
+
+This outputter is different from the rest of the outputters in that the code it
+generates does not use the Glean SDK. It is meant to be used to collect events
+in server-side environments. In these environments SDK assumptions to measurement
+window and connectivity don't hold.
+Generated code takes care of assembling pings with metrics, serializing to messages
+conforming to Glean schema, and logging with mozlog. Then it's the role of the ingestion
+pipeline to pick the messages up and process.
+
+Warning: this outputter supports limited set of metrics,
+see `SUPPORTED_METRIC_TYPES` below.
+
+There are two patterns for event structure supported in this environment:
+* Events as `Event` metric type, where we generate a single class per ping with
+ `record{event_name}` method for each event metric. This is recommended to use for new
+ applications as it allows to fully leverage standard Data Platform tools
+ post-ingestion.
+* Custom pings-as-events, where for each ping we generate a class with a single `record`
+ method, usually with an `event_name` string metric.
+
+Therefore, unlike in other outputters, here we don't generate classes for each metric.
+"""
+from collections import defaultdict
+from pathlib import Path
+from typing import Any, Dict, Optional, List
+
+from . import __version__
+from . import metrics
+from . import util
+
+# Adding a metric here will require updating the `generate_js_metric_type` function
+# and might require changes to the template.
+SUPPORTED_METRIC_TYPES = ["string", "event"]
+
+
+def event_class_name(ping_name: str, event_metric_exists: bool) -> str:
+ # For compatibility with FxA codebase we don't want to add "Logger" suffix
+ # when custom pings without event metrics are used.
+ suffix = "Logger" if event_metric_exists else ""
+ return util.Camelize(ping_name) + "ServerEvent" + suffix
+
+
+def generate_metric_name(metric: metrics.Metric) -> str:
+ return f"{metric.category}.{metric.name}"
+
+
+def generate_metric_argument_name(metric: metrics.Metric) -> str:
+ return f"{metric.category}_{metric.name}"
+
+
+def generate_js_metric_type(metric: metrics.Metric) -> str:
+ return metric.type
+
+
+def generate_ping_factory_method(ping: str, event_metric_exists: bool) -> str:
+ # `ServerEventLogger` better describes role of the class that this factory
+ # method generates, but for compatibility with existing FxA codebase
+ # we use `Event` suffix if no event metrics are defined.
+ suffix = "ServerEventLogger" if event_metric_exists else "Event"
+ return f"create{util.Camelize(ping)}{suffix}"
+
+
+def generate_event_metric_record_function_name(metric: metrics.Metric) -> str:
+ return f"record{util.Camelize(metric.category)}{util.Camelize(metric.name)}"
+
+
+def clean_string(s: str) -> str:
+ return s.replace("\n", " ").rstrip()
+
+
+def output(
+ lang: str,
+ objs: metrics.ObjectTree,
+ output_dir: Path,
+) -> None:
+ """
+ Given a tree of objects, output Javascript or Typescript code to `output_dir`.
+
+ The output is a single file containing all the code for assembling pings with
+ metrics, serializing, and submitting.
+
+ :param lang: Either "javascript" or "typescript";
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ """
+
+ template = util.get_jinja2_template(
+ "javascript_server.jinja2",
+ filters=(
+ ("event_class_name", event_class_name),
+ ("metric_name", generate_metric_name),
+ ("metric_argument_name", generate_metric_argument_name),
+ ("js_metric_type", generate_js_metric_type),
+ ("factory_method", generate_ping_factory_method),
+ (
+ "event_metric_record_function_name",
+ generate_event_metric_record_function_name,
+ ),
+ ("clean_string", clean_string),
+ ),
+ )
+
+ event_metric_exists = False
+
+ # Go through all metrics in objs and build a map of
+ # ping->list of metric categories->list of metrics
+ # for easier processing in the template.
+ ping_to_metrics: Dict[str, Dict[str, List[metrics.Metric]]] = defaultdict(dict)
+ for _category_key, category_val in objs.items():
+ for _metric_name, metric in category_val.items():
+ if isinstance(metric, metrics.Metric):
+ if metric.type not in SUPPORTED_METRIC_TYPES:
+ print(
+ "❌ Ignoring unsupported metric type: "
+ + f"{metric.type}:{metric.name}."
+ + " Reach out to Glean team to add support for this"
+ + " metric type."
+ )
+ continue
+ if metric.type == "event":
+ # This is used in the template - generated code is slightly
+ # different when event metric type is used.
+ event_metric_exists = True
+ for ping in metric.send_in_pings:
+ metrics_by_type = ping_to_metrics[ping]
+ metrics_list = metrics_by_type.setdefault(metric.type, [])
+ metrics_list.append(metric)
+
+ PING_METRIC_ERROR_MSG = (
+ " Server-side environment is simplified and this"
+ + " parser doesn't generate individual metric files. Make sure to pass all"
+ + " your ping and metric definitions in a single invocation of the parser."
+ )
+ if "pings" not in objs:
+ # If events are meant to be sent in custom pings, we need to make sure they
+ # are defined. Otherwise we won't have destination tables defined and
+ # submissions won't pass validation at ingestion.
+ if event_metric_exists:
+ if "events" not in ping_to_metrics:
+ # Event metrics can be sent in standard `events` ping
+ # or in custom pings.
+ print(
+ "❌ "
+ + PING_METRIC_ERROR_MSG
+ + "\n You need to either send your event metrics in standard"
+ + " `events` ping or define a custom one."
+ )
+ return
+ else:
+ print("❌ No ping definition found." + PING_METRIC_ERROR_MSG)
+ return
+
+ if not ping_to_metrics:
+ print("❌ No pings with metrics found." + PING_METRIC_ERROR_MSG)
+ return
+
+ extension = ".js" if lang == "javascript" else ".ts"
+ filepath = output_dir / ("server_events" + extension)
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ pings=ping_to_metrics,
+ event_metric_exists=event_metric_exists,
+ lang=lang,
+ )
+ )
+
+
+def output_javascript(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Javascript code to `output_dir`.
+
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ """
+
+ output("javascript", objs, output_dir)
+
+
+def output_typescript(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Typescript code to `output_dir`.
+
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ """
+
+ output("typescript", objs, output_dir)
diff --git a/third_party/python/glean_parser/glean_parser/kotlin.py b/third_party/python/glean_parser/glean_parser/kotlin.py
new file mode 100644
index 0000000000..82cc63d237
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/kotlin.py
@@ -0,0 +1,356 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Outputter to generate Kotlin code for metrics.
+"""
+
+from collections import OrderedDict
+import enum
+import json
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Union # noqa
+
+from . import __version__
+from . import metrics
+from . import pings
+from . import tags
+from . import util
+from .util import DictWrapper
+
+
+def kotlin_datatypes_filter(value: util.JSONType) -> str:
+ """
+ A Jinja2 filter that renders Kotlin literals.
+
+ Based on Python's JSONEncoder, but overrides:
+ - lists to use listOf
+ - dicts to use mapOf
+ - sets to use setOf
+ - enums to use the like-named Kotlin enum
+ - Rate objects to a CommonMetricData initializer
+ (for external Denominators' Numerators lists)
+ """
+
+ class KotlinEncoder(json.JSONEncoder):
+ def iterencode(self, value):
+ if isinstance(value, list):
+ yield "listOf("
+ first = True
+ for subvalue in value:
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield ")"
+ elif isinstance(value, dict):
+ yield "mapOf("
+ first = True
+ for key, subvalue in value.items():
+ if not first:
+ yield ", "
+ yield from self.iterencode(key)
+ yield " to "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield ")"
+ elif isinstance(value, enum.Enum):
+ # UniFFI generates SCREAMING_CASE enum variants.
+ yield (value.__class__.__name__ + "." + util.screaming_case(value.name))
+ elif isinstance(value, set):
+ yield "setOf("
+ first = True
+ for subvalue in sorted(list(value)):
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield ")"
+ elif isinstance(value, metrics.Rate):
+ yield "CommonMetricData("
+ first = True
+ for arg_name in util.common_metric_args:
+ if hasattr(value, arg_name):
+ if not first:
+ yield ", "
+ yield f"{util.camelize(arg_name)} = "
+ yield from self.iterencode(getattr(value, arg_name))
+ first = False
+ yield ")"
+ else:
+ yield from super().iterencode(value)
+
+ return "".join(KotlinEncoder().iterencode(value))
+
+
+def type_name(obj: Union[metrics.Metric, pings.Ping]) -> str:
+ """
+ Returns the Kotlin type to use for a given metric or ping object.
+ """
+ generate_enums = getattr(obj, "_generate_enums", [])
+ if len(generate_enums):
+ generic = None
+ for member, suffix in generate_enums:
+ if len(getattr(obj, member)):
+ if isinstance(obj, metrics.Event):
+ generic = util.Camelize(obj.name) + suffix
+ else:
+ generic = util.camelize(obj.name) + suffix
+ else:
+ if isinstance(obj, metrics.Event):
+ generic = "NoExtras"
+ else:
+ generic = "No" + suffix
+
+ return "{}<{}>".format(class_name(obj.type), generic)
+
+ return class_name(obj.type)
+
+
+def extra_type_name(typ: str) -> str:
+ """
+ Returns the corresponding Kotlin type for event's extra key types.
+ """
+
+ if typ == "boolean":
+ return "Boolean"
+ elif typ == "string":
+ return "String"
+ elif typ == "quantity":
+ return "Int"
+ else:
+ return "UNSUPPORTED"
+
+
+def class_name(obj_type: str) -> str:
+ """
+ Returns the Kotlin class name for a given metric or ping type.
+ """
+ if obj_type == "ping":
+ return "PingType"
+ if obj_type.startswith("labeled_"):
+ obj_type = obj_type[8:]
+ return util.Camelize(obj_type) + "MetricType"
+
+
+def generate_build_date(date: Optional[str]) -> str:
+ """
+ Generate the build timestamp.
+ """
+
+ ts = util.build_date(date)
+
+ data = [
+ str(ts.year),
+ # In Java the first month of the year in calendars is JANUARY which is 0.
+ # In Python it's 1-based
+ str(ts.month - 1),
+ str(ts.day),
+ str(ts.hour),
+ str(ts.minute),
+ str(ts.second),
+ ]
+ components = ", ".join(data)
+
+ # DatetimeMetricType takes a `Calendar` instance.
+ return f'Calendar.getInstance(TimeZone.getTimeZone("GMT+0")).also {{ cal -> cal.set({components}) }}' # noqa
+
+
+def output_gecko_lookup(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, generate a Kotlin map between Gecko histograms and
+ Glean SDK metric types.
+
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ :param options: options dictionary, with the following optional keys:
+
+ - `namespace`: The package namespace to declare at the top of the
+ generated files. Defaults to `GleanMetrics`.
+ - `glean_namespace`: The package namespace of the glean library itself.
+ This is where glean objects will be imported from in the generated
+ code.
+ """
+ if options is None:
+ options = {}
+
+ template = util.get_jinja2_template(
+ "kotlin.geckoview.jinja2",
+ filters=(
+ ("kotlin", kotlin_datatypes_filter),
+ ("type_name", type_name),
+ ("class_name", class_name),
+ ),
+ )
+
+ namespace = options.get("namespace", "GleanMetrics")
+ glean_namespace = options.get("glean_namespace", "mozilla.components.service.glean")
+
+ # Build a dictionary that contains data for metrics that are
+ # histogram-like/scalar-like and contain a gecko_datapoint, with this format:
+ #
+ # {
+ # "histograms": {
+ # "category": [
+ # {"gecko_datapoint": "the-datapoint", "name": "the-metric-name"},
+ # ...
+ # ],
+ # ...
+ # },
+ # "other-type": {}
+ # }
+ gecko_metrics: Dict[str, Dict[str, List[Dict[str, str]]]] = DictWrapper()
+
+ # Define scalar-like types.
+ SCALAR_LIKE_TYPES = ["boolean", "string", "quantity"]
+
+ for category_key, category_val in objs.items():
+ # Support exfiltration of Gecko metrics from products using both the
+ # Glean SDK and GeckoView. See bug 1566356 for more context.
+ for metric in category_val.values():
+ # This is not a Gecko metric, skip it.
+ if (
+ isinstance(metric, pings.Ping)
+ or isinstance(metric, tags.Tag)
+ or not getattr(metric, "gecko_datapoint", False)
+ ):
+ continue
+
+ # Put scalars in their own categories, histogram-like in "histograms" and
+ # categorical histograms in "categoricals".
+ type_category = "histograms"
+ if metric.type in SCALAR_LIKE_TYPES:
+ type_category = metric.type
+ elif metric.type == "labeled_counter":
+ # Labeled counters with a 'gecko_datapoint' property
+ # are categorical histograms.
+ type_category = "categoricals"
+
+ gecko_metrics.setdefault(type_category, OrderedDict())
+ gecko_metrics[type_category].setdefault(category_key, [])
+
+ gecko_metrics[type_category][category_key].append(
+ {"gecko_datapoint": metric.gecko_datapoint, "name": metric.name}
+ )
+
+ if not gecko_metrics:
+ # Bail out and don't create a file if no gecko metrics
+ # are found.
+ return
+
+ filepath = output_dir / "GleanGeckoMetricsMapping.kt"
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ gecko_metrics=gecko_metrics,
+ namespace=namespace,
+ glean_namespace=glean_namespace,
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
+
+
+def output_kotlin(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Kotlin code to `output_dir`.
+
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ :param options: options dictionary, with the following optional keys:
+
+ - `namespace`: The package namespace to declare at the top of the
+ generated files. Defaults to `GleanMetrics`.
+ - `glean_namespace`: The package namespace of the glean library itself.
+ This is where glean objects will be imported from in the generated
+ code.
+ - `with_buildinfo`: If "true" a `GleanBuildInfo.kt` file is generated.
+ Otherwise generation of that file is skipped.
+ Defaults to "true".
+ - `build_date`: If set to `0` a static unix epoch time will be used.
+ If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
+ it will use that date.
+ Other values will throw an error.
+ If not set it will use the current date & time.
+ """
+ if options is None:
+ options = {}
+
+ namespace = options.get("namespace", "GleanMetrics")
+ glean_namespace = options.get("glean_namespace", "mozilla.components.service.glean")
+ namespace_package = namespace[: namespace.rfind(".")]
+ with_buildinfo = options.get("with_buildinfo", "true").lower() == "true"
+ build_date = options.get("build_date", None)
+
+ # Write out the special "build info" object
+ template = util.get_jinja2_template(
+ "kotlin.buildinfo.jinja2",
+ )
+
+ if with_buildinfo:
+ build_date = generate_build_date(build_date)
+ # This filename needs to start with "Glean" so it can never clash with a
+ # metric category
+ with (output_dir / "GleanBuildInfo.kt").open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ namespace=namespace,
+ namespace_package=namespace_package,
+ glean_namespace=glean_namespace,
+ build_date=build_date,
+ )
+ )
+ fd.write("\n")
+
+ template = util.get_jinja2_template(
+ "kotlin.jinja2",
+ filters=(
+ ("kotlin", kotlin_datatypes_filter),
+ ("type_name", type_name),
+ ("extra_type_name", extra_type_name),
+ ("class_name", class_name),
+ ),
+ )
+
+ for category_key, category_val in objs.items():
+ filename = util.Camelize(category_key) + ".kt"
+ filepath = output_dir / filename
+
+ obj_types = sorted(
+ list(set(class_name(obj.type) for obj in category_val.values()))
+ )
+ has_labeled_metrics = any(
+ getattr(metric, "labeled", False) for metric in category_val.values()
+ )
+
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ category_name=category_key,
+ objs=category_val,
+ obj_types=obj_types,
+ common_metric_args=util.common_metric_args,
+ extra_metric_args=util.extra_metric_args,
+ ping_args=util.ping_args,
+ namespace=namespace,
+ has_labeled_metrics=has_labeled_metrics,
+ glean_namespace=glean_namespace,
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
+
+ # TODO: Maybe this should just be a separate outputter?
+ output_gecko_lookup(objs, output_dir, options)
diff --git a/third_party/python/glean_parser/glean_parser/lint.py b/third_party/python/glean_parser/glean_parser/lint.py
new file mode 100644
index 0000000000..891d05ad63
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/lint.py
@@ -0,0 +1,653 @@
+# 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 enum
+from pathlib import Path
+import re
+import sys
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Generator,
+ List,
+ Iterable,
+ Optional,
+ Tuple,
+ Union,
+) # noqa
+
+
+from . import metrics
+from . import parser
+from . import pings
+from . import tags
+from . import util
+
+
+# Yield only an error message
+LintGenerator = Generator[str, None, None]
+
+# Yield fully constructed GlinterNits
+NitGenerator = Generator["GlinterNit", None, None]
+
+
+class CheckType(enum.Enum):
+ warning = 0
+ error = 1
+
+
+def _split_words(name: str) -> List[str]:
+ """
+ Helper function to split words on either `.` or `_`.
+ """
+ return re.split("[._-]", name)
+
+
+def _english_list(items: List[str]) -> str:
+ """
+ Helper function to format a list [A, B, C] as "'A', 'B', or 'C'".
+ """
+ if len(items) == 0:
+ return ""
+ elif len(items) == 1:
+ return f"'{items[0]}'"
+ else:
+ return "{}, or '{}'".format(
+ ", ".join([f"'{x}'" for x in items[:-1]]), items[-1]
+ )
+
+
+def _hamming_distance(str1: str, str2: str) -> int:
+ """
+ Count the # of differences between strings str1 and str2,
+ padding the shorter one with whitespace
+ """
+
+ diffs = 0
+ if len(str1) < len(str2):
+ str1, str2 = str2, str1
+ len_dist = len(str1) - len(str2)
+ str2 += " " * len_dist
+
+ for ch1, ch2 in zip(str1, str2):
+ if ch1 != ch2:
+ diffs += 1
+ return diffs
+
+
+def check_common_prefix(
+ category_name: str, metrics: Iterable[metrics.Metric]
+) -> LintGenerator:
+ """
+ Check if all metrics begin with a common prefix.
+ """
+ metric_words = sorted([_split_words(metric.name) for metric in metrics])
+
+ if len(metric_words) < 2:
+ return
+
+ first = metric_words[0]
+ last = metric_words[-1]
+
+ for i in range(min(len(first), len(last))):
+ if first[i] != last[i]:
+ break
+
+ if i > 0:
+ common_prefix = "_".join(first[:i])
+ yield (
+ f"Within category '{category_name}', all metrics begin with "
+ f"prefix '{common_prefix}'."
+ "Remove the prefixes on the metric names and (possibly) "
+ "rename the category."
+ )
+
+
+def check_unit_in_name(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ """
+ The metric name ends in a unit.
+ """
+ TIME_UNIT_ABBREV = {
+ "nanosecond": "ns",
+ "microsecond": "us",
+ "millisecond": "ms",
+ "second": "s",
+ "minute": "m",
+ "hour": "h",
+ "day": "d",
+ }
+
+ MEMORY_UNIT_ABBREV = {
+ "byte": "b",
+ "kilobyte": "kb",
+ "megabyte": "mb",
+ "gigabyte": "gb",
+ }
+
+ name_words = _split_words(metric.name)
+ unit_in_name = name_words[-1]
+
+ time_unit = getattr(metric, "time_unit", None)
+ memory_unit = getattr(metric, "memory_unit", None)
+ unit = getattr(metric, "unit", None)
+
+ if time_unit is not None:
+ if (
+ unit_in_name == TIME_UNIT_ABBREV.get(time_unit.name)
+ or unit_in_name == time_unit.name
+ ):
+ yield (
+ f"Suffix '{unit_in_name}' is redundant with time_unit "
+ f"'{time_unit.name}'. Only include time_unit."
+ )
+ elif (
+ unit_in_name in TIME_UNIT_ABBREV.keys()
+ or unit_in_name in TIME_UNIT_ABBREV.values()
+ ):
+ yield (
+ f"Suffix '{unit_in_name}' doesn't match time_unit "
+ f"'{time_unit.name}'. "
+ "Confirm the unit is correct and only include time_unit."
+ )
+
+ elif memory_unit is not None:
+ if (
+ unit_in_name == MEMORY_UNIT_ABBREV.get(memory_unit.name)
+ or unit_in_name == memory_unit.name
+ ):
+ yield (
+ f"Suffix '{unit_in_name}' is redundant with memory_unit "
+ f"'{memory_unit.name}'. "
+ "Only include memory_unit."
+ )
+ elif (
+ unit_in_name in MEMORY_UNIT_ABBREV.keys()
+ or unit_in_name in MEMORY_UNIT_ABBREV.values()
+ ):
+ yield (
+ f"Suffix '{unit_in_name}' doesn't match memory_unit "
+ f"{memory_unit.name}'. "
+ "Confirm the unit is correct and only include memory_unit."
+ )
+
+ elif unit is not None:
+ if unit_in_name == unit:
+ yield (
+ f"Suffix '{unit_in_name}' is redundant with unit param "
+ f"'{unit}'. "
+ "Only include unit."
+ )
+
+
+def check_category_generic(
+ category_name: str, metrics: Iterable[metrics.Metric]
+) -> LintGenerator:
+ """
+ The category name is too generic.
+ """
+ GENERIC_CATEGORIES = ["metrics", "events"]
+
+ if category_name in GENERIC_CATEGORIES:
+ yield (
+ f"Category '{category_name}' is too generic. "
+ f"Don't use {_english_list(GENERIC_CATEGORIES)} for category names"
+ )
+
+
+def check_bug_number(
+ metric: Union[metrics.Metric, pings.Ping], parser_config: Dict[str, Any]
+) -> LintGenerator:
+ number_bugs = [str(bug) for bug in metric.bugs if isinstance(bug, int)]
+
+ if len(number_bugs):
+ yield (
+ f"For bugs {', '.join(number_bugs)}: "
+ "Bug numbers are deprecated and should be changed to full URLs. "
+ f"For example, use 'http://bugzilla.mozilla.org/{number_bugs[0]}' "
+ f"instead of '{number_bugs[0]}'."
+ )
+
+
+def check_valid_in_baseline(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ allow_reserved = parser_config.get("allow_reserved", False)
+
+ if not allow_reserved and "baseline" in metric.send_in_pings:
+ yield (
+ "The baseline ping is Glean-internal. "
+ "Remove 'baseline' from the send_in_pings array."
+ )
+
+
+def check_misspelled_pings(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ for ping in metric.send_in_pings:
+ for builtin in pings.RESERVED_PING_NAMES:
+ distance = _hamming_distance(ping, builtin)
+ if distance == 1:
+ yield f"Ping '{ping}' seems misspelled. Did you mean '{builtin}'?"
+
+
+def check_tags_required(
+ metric_or_ping: Union[metrics.Metric, pings.Ping], parser_config: Dict[str, Any]
+) -> LintGenerator:
+ if parser_config.get("require_tags", False) and not len(
+ metric_or_ping.metadata.get("tags", [])
+ ):
+ yield "Tags are required but no tags specified"
+
+
+def check_user_lifetime_expiration(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ if metric.lifetime == metrics.Lifetime.user and metric.expires != "never":
+ yield (
+ "Metrics with 'user' lifetime cannot have an expiration date. "
+ "They live as long as the user profile does. "
+ "Set expires to 'never'."
+ )
+
+
+def check_expired_date(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ try:
+ metric.validate_expires()
+ except ValueError as e:
+ yield (str(e))
+
+
+def check_expired_metric(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ if metric.is_expired():
+ yield ("Metric has expired. Please consider removing it.")
+
+
+def check_old_event_api(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ # Glean v52.0.0 removed the old events API.
+ # The metrics-2-0-0 schema still supports it.
+ # We want to warn about it.
+ # This can go when we introduce 3-0-0
+
+ if not isinstance(metric, metrics.Event):
+ return
+
+ if not all("type" in x for x in metric.extra_keys.values()):
+ yield ("The old event API is gone. Extra keys require a type.")
+
+
+def check_metric_on_events_lifetime(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ """A non-event metric on the Events ping only makes sense if its value
+ is immutable over the life of the ping."""
+ if (
+ "events" in metric.send_in_pings
+ and "all_pings" not in metric.send_in_pings
+ and metric.type != "event"
+ and metric.lifetime == metrics.Lifetime.ping
+ ):
+ yield (
+ "Non-event metrics sent on the Events ping should not have the ping"
+ " lifetime."
+ )
+
+
+def check_unexpected_unit(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ """
+ `unit` was allowed on all metrics and recently disallowed.
+ We now warn about its use on all but quantity and custom distribution
+ metrics.
+ """
+ allowed_types = [metrics.Quantity, metrics.CustomDistribution]
+ if not any([isinstance(metric, ty) for ty in allowed_types]) and metric.unit:
+ yield (
+ "The `unit` property is only allowed for quantity "
+ + "and custom distribution metrics."
+ )
+
+
+def check_empty_datareview(
+ metric: metrics.Metric, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ disallowed_datareview = ["", "todo"]
+ data_reviews = [dr.lower() in disallowed_datareview for dr in metric.data_reviews]
+
+ if any(data_reviews):
+ yield "List of data reviews should not contain empty strings or TODO markers."
+
+
+def check_redundant_ping(
+ pings: pings.Ping, parser_config: Dict[str, Any]
+) -> LintGenerator:
+ """
+ Check if the pings contains 'ping' as the prefix or suffix, or 'ping' or 'custom'
+ """
+ ping_words = _split_words(pings.name)
+
+ if len(ping_words) != 0:
+ ping_first_word = ping_words[0]
+ ping_last_word = ping_words[-1]
+
+ if ping_first_word == "ping":
+ yield ("The prefix 'ping' is redundant.")
+ elif ping_last_word == "ping":
+ yield ("The suffix 'ping' is redundant.")
+ elif "ping" in ping_words:
+ yield ("The word 'ping' is redundant.")
+ elif "custom" in ping_words:
+ yield ("The word 'custom' is redundant.")
+
+
+def check_unknown_ping(
+ check_name: str,
+ check_type: CheckType,
+ all_pings: Dict[str, pings.Ping],
+ metrics: Dict[str, metrics.Metric],
+ parser_config: Dict[str, Any],
+) -> NitGenerator:
+ """
+ Check that all pings in `send_in_pings` for all metrics are either a builtin ping
+ or in the list of defined custom pings.
+ """
+ available_pings = [p for p in all_pings]
+
+ for _, metric in metrics.items():
+ if check_name in metric.no_lint:
+ continue
+
+ send_in_pings = metric.send_in_pings
+ for target_ping in send_in_pings:
+ if target_ping in pings.RESERVED_PING_NAMES:
+ continue
+
+ if target_ping not in available_pings:
+ msg = f"Ping `{target_ping} `in `send_in_pings` is unknown."
+ name = ".".join([metric.category, metric.name])
+ nit = GlinterNit(
+ check_name,
+ name,
+ msg,
+ check_type,
+ )
+ yield nit
+
+
+# The checks that operate on an entire category of metrics:
+# {NAME: (function, is_error)}
+CATEGORY_CHECKS: Dict[
+ str, Tuple[Callable[[str, Iterable[metrics.Metric]], LintGenerator], CheckType]
+] = {
+ "COMMON_PREFIX": (check_common_prefix, CheckType.error),
+ "CATEGORY_GENERIC": (check_category_generic, CheckType.error),
+}
+
+
+# The checks that operate on individual metrics:
+# {NAME: (function, is_error)}
+METRIC_CHECKS: Dict[
+ str, Tuple[Callable[[metrics.Metric, dict], LintGenerator], CheckType]
+] = {
+ "UNIT_IN_NAME": (check_unit_in_name, CheckType.error),
+ "BUG_NUMBER": (check_bug_number, CheckType.error),
+ "BASELINE_PING": (check_valid_in_baseline, CheckType.error),
+ "MISSPELLED_PING": (check_misspelled_pings, CheckType.error),
+ "TAGS_REQUIRED": (check_tags_required, CheckType.error),
+ "EXPIRATION_DATE_TOO_FAR": (check_expired_date, CheckType.warning),
+ "USER_LIFETIME_EXPIRATION": (check_user_lifetime_expiration, CheckType.warning),
+ "EXPIRED": (check_expired_metric, CheckType.warning),
+ "OLD_EVENT_API": (check_old_event_api, CheckType.warning),
+ "METRIC_ON_EVENTS_LIFETIME": (check_metric_on_events_lifetime, CheckType.error),
+ "UNEXPECTED_UNIT": (check_unexpected_unit, CheckType.warning),
+ "EMPTY_DATAREVIEW": (check_empty_datareview, CheckType.warning),
+}
+
+
+# The checks that operate on individual pings:
+# {NAME: (function, is_error)}
+PING_CHECKS: Dict[
+ str, Tuple[Callable[[pings.Ping, dict], LintGenerator], CheckType]
+] = {
+ "BUG_NUMBER": (check_bug_number, CheckType.error),
+ "TAGS_REQUIRED": (check_tags_required, CheckType.error),
+ "REDUNDANT_PING": (check_redundant_ping, CheckType.error),
+}
+
+ALL_OBJECT_CHECKS: Dict[
+ str,
+ Tuple[
+ Callable[
+ # check name, check type, pings, metrics, config
+ [str, CheckType, dict, dict, dict],
+ NitGenerator,
+ ],
+ CheckType,
+ ],
+] = {
+ "UNKNOWN_PING_REFERENCED": (check_unknown_ping, CheckType.error),
+}
+
+
+class GlinterNit:
+ def __init__(self, check_name: str, name: str, msg: str, check_type: CheckType):
+ self.check_name = check_name
+ self.name = name
+ self.msg = msg
+ self.check_type = check_type
+
+ def format(self):
+ return (
+ f"{self.check_type.name.upper()}: {self.check_name}: "
+ f"{self.name}: {self.msg}"
+ )
+
+
+def _lint_item_tags(
+ item_name: str,
+ item_type: str,
+ item_tag_names: List[str],
+ valid_tag_names: List[str],
+) -> List[GlinterNit]:
+ invalid_tags = [tag for tag in item_tag_names if tag not in valid_tag_names]
+ return (
+ [
+ GlinterNit(
+ "INVALID_TAGS",
+ item_name,
+ f"Invalid tags specified in {item_type}: {', '.join(invalid_tags)}",
+ CheckType.error,
+ )
+ ]
+ if len(invalid_tags)
+ else []
+ )
+
+
+def _lint_pings(
+ category: Dict[str, Union[metrics.Metric, pings.Ping, tags.Tag]],
+ parser_config: Dict[str, Any],
+ valid_tag_names: List[str],
+) -> List[GlinterNit]:
+ nits: List[GlinterNit] = []
+
+ for ping_name, ping in sorted(list(category.items())):
+ assert isinstance(ping, pings.Ping)
+ for check_name, (check_func, check_type) in PING_CHECKS.items():
+ new_nits = list(check_func(ping, parser_config))
+ if len(new_nits):
+ if check_name not in ping.no_lint:
+ nits.extend(
+ GlinterNit(
+ check_name,
+ ping_name,
+ msg,
+ check_type,
+ )
+ for msg in new_nits
+ )
+ nits.extend(
+ _lint_item_tags(
+ ping_name,
+ "ping",
+ ping.metadata.get("tags", []),
+ valid_tag_names,
+ )
+ )
+ return nits
+
+
+def _lint_all_objects(
+ objects: Dict[str, Dict[str, Union[metrics.Metric, pings.Ping, tags.Tag]]],
+ parser_config: Dict[str, Any],
+) -> List[GlinterNit]:
+ nits: List[GlinterNit] = []
+
+ pings = objects.get("pings")
+ if not pings:
+ return []
+
+ metrics = objects.get("all_metrics")
+ if not metrics:
+ return []
+
+ for check_name, (check_func, check_type) in ALL_OBJECT_CHECKS.items():
+ new_nits = list(
+ check_func(check_name, check_type, pings, metrics, parser_config)
+ )
+ nits.extend(new_nits)
+
+ return nits
+
+
+def lint_metrics(
+ objs: metrics.ObjectTree,
+ parser_config: Optional[Dict[str, Any]] = None,
+ file=sys.stderr,
+) -> List[GlinterNit]:
+ """
+ Performs glinter checks on a set of metrics objects.
+
+ :param objs: Tree of metric objects, as returns by `parser.parse_objects`.
+ :param file: The stream to write errors to.
+ :returns: List of nits.
+ """
+ if parser_config is None:
+ parser_config = {}
+
+ nits: List[GlinterNit] = []
+ valid_tag_names = [tag for tag in objs.get("tags", [])]
+
+ nits.extend(_lint_all_objects(objs, parser_config))
+
+ for category_name, category in sorted(list(objs.items())):
+ if category_name == "pings":
+ nits.extend(_lint_pings(category, parser_config, valid_tag_names))
+ continue
+
+ if category_name == "tags":
+ # currently we have no linting for tags
+ continue
+
+ # Make sure the category has only Metrics, not Pings or Tags
+ category_metrics = dict(
+ (name, metric)
+ for (name, metric) in category.items()
+ if isinstance(metric, metrics.Metric)
+ )
+
+ for cat_check_name, (cat_check_func, check_type) in CATEGORY_CHECKS.items():
+ if any(
+ cat_check_name in metric.no_lint for metric in category_metrics.values()
+ ):
+ continue
+ nits.extend(
+ GlinterNit(cat_check_name, category_name, msg, check_type)
+ for msg in cat_check_func(category_name, category_metrics.values())
+ )
+
+ for _metric_name, metric in sorted(list(category_metrics.items())):
+ for check_name, (check_func, check_type) in METRIC_CHECKS.items():
+ new_nits = list(check_func(metric, parser_config))
+ if len(new_nits):
+ if check_name not in metric.no_lint:
+ nits.extend(
+ GlinterNit(
+ check_name,
+ ".".join([metric.category, metric.name]),
+ msg,
+ check_type,
+ )
+ for msg in new_nits
+ )
+
+ # also check that tags for metric are valid
+ nits.extend(
+ _lint_item_tags(
+ ".".join([metric.category, metric.name]),
+ "metric",
+ metric.metadata.get("tags", []),
+ valid_tag_names,
+ )
+ )
+
+ if len(nits):
+ print("Sorry, Glean found some glinter nits:", file=file)
+ for nit in nits:
+ print(nit.format(), file=file)
+ print("", file=file)
+ print("Please fix the above nits to continue.", file=file)
+ print(
+ "To disable a check, add a `no_lint` parameter "
+ "with a list of check names to disable.\n"
+ "This parameter can appear with each individual metric, or at the "
+ "top-level to affect the entire file.",
+ file=file,
+ )
+
+ return nits
+
+
+def glinter(
+ input_filepaths: Iterable[Path],
+ parser_config: Optional[Dict[str, Any]] = None,
+ file=sys.stderr,
+) -> int:
+ """
+ Commandline helper for glinter.
+
+ :param input_filepaths: List of Path objects to load metrics from.
+ :param parser_config: Parser configuration object, passed to
+ `parser.parse_objects`.
+ :param file: The stream to write the errors to.
+ :return: Non-zero if there were any glinter errors.
+ """
+ if parser_config is None:
+ parser_config = {}
+
+ errors = 0
+
+ objs = parser.parse_objects(input_filepaths, parser_config)
+ errors += util.report_validation_errors(objs)
+
+ nits = lint_metrics(objs.value, parser_config=parser_config, file=file)
+ errors += len([nit for nit in nits if nit.check_type == CheckType.error])
+
+ if errors == 0:
+ print("✨ Your metrics are Glean! ✨", file=file)
+ return 0
+
+ print(f"❌ Found {errors} errors.")
+
+ return 1
diff --git a/third_party/python/glean_parser/glean_parser/markdown.py b/third_party/python/glean_parser/glean_parser/markdown.py
new file mode 100644
index 0000000000..68b288945f
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/markdown.py
@@ -0,0 +1,273 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Outputter to generate Markdown documentation for metrics.
+"""
+
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple, Union
+from urllib.parse import urlsplit, parse_qs
+
+
+from . import __version__
+from . import metrics
+from . import pings
+from . import util
+from collections import defaultdict
+
+
+def extra_info(obj: Union[metrics.Metric, pings.Ping]) -> List[Tuple[str, str]]:
+ """
+ Returns a list of string to string tuples with extra information for the type
+ (e.g. extra keys for events) or an empty list if nothing is available.
+ """
+ extra_info = []
+
+ if isinstance(obj, metrics.Event):
+ for key in obj.allowed_extra_keys:
+ extra_info.append((key, obj.extra_keys[key]["description"]))
+
+ if isinstance(obj, metrics.Labeled) and obj.ordered_labels is not None:
+ for label in obj.ordered_labels:
+ extra_info.append((label, None))
+
+ if isinstance(obj, metrics.Quantity):
+ extra_info.append(("unit", obj.unit))
+
+ return extra_info
+
+
+def ping_desc(
+ ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
+) -> str:
+ """
+ Return a text description of the ping. If a custom_pings_cache
+ is available, look in there for non-reserved ping names description.
+ """
+ desc = ""
+
+ if ping_name in pings.RESERVED_PING_NAMES:
+ desc = (
+ "This is a built-in ping that is assembled out of the "
+ "box by the Glean SDK."
+ )
+ elif ping_name == "all-pings":
+ desc = "These metrics are sent in every ping."
+ elif custom_pings_cache is not None and ping_name in custom_pings_cache:
+ desc = custom_pings_cache[ping_name].description
+
+ return desc
+
+
+def metrics_docs(obj_name: str) -> str:
+ """
+ Return a link to the documentation entry for the Glean SDK metric of the
+ requested type.
+ """
+ # We need to fixup labeled stuff, as types are singular and docs refer
+ # to them as plural.
+ fixedup_name = obj_name
+ if obj_name.startswith("labeled_"):
+ fixedup_name += "s"
+
+ return f"https://mozilla.github.io/glean/book/user/metrics/{fixedup_name}.html"
+
+
+def ping_docs(ping_name: str) -> str:
+ """
+ Return a link to the documentation entry for the requested Glean SDK
+ built-in ping.
+ """
+ if ping_name not in pings.RESERVED_PING_NAMES:
+ return ""
+
+ return f"https://mozilla.github.io/glean/book/user/pings/{ping_name}.html"
+
+
+def if_empty(
+ ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
+) -> bool:
+ if custom_pings_cache is not None and ping_name in custom_pings_cache:
+ return custom_pings_cache[ping_name].send_if_empty
+ else:
+ return False
+
+
+def ping_reasons(
+ ping_name: str, custom_pings_cache: Dict[str, pings.Ping]
+) -> Dict[str, str]:
+ """
+ Returns the reasons dictionary for the ping.
+ """
+ if ping_name == "all-pings":
+ return {}
+ elif ping_name in custom_pings_cache:
+ return custom_pings_cache[ping_name].reasons
+
+ return {}
+
+
+def ping_data_reviews(
+ ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
+) -> Optional[List[str]]:
+ if custom_pings_cache is not None and ping_name in custom_pings_cache:
+ return custom_pings_cache[ping_name].data_reviews
+ else:
+ return None
+
+
+def ping_review_title(data_url: str, index: int) -> str:
+ """
+ Return a title for a data review in human readable form.
+
+ :param data_url: A url for data review.
+ :param index: Position of the data review on list (e.g: 1, 2, 3...).
+ """
+ url_object = urlsplit(data_url)
+
+ # Bugzilla urls like `https://bugzilla.mozilla.org/show_bug.cgi?id=1581647`
+ query = url_object.query
+ params = parse_qs(query)
+
+ # GitHub urls like `https://github.com/mozilla-mobile/fenix/pull/1707`
+ path = url_object.path
+ short_url = path[1:].replace("/pull/", "#")
+
+ if params and params["id"]:
+ return f"Bug {params['id'][0]}"
+ elif url_object.netloc == "github.com":
+ return short_url
+
+ return f"Review {index}"
+
+
+def ping_bugs(
+ ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
+) -> Optional[List[str]]:
+ if custom_pings_cache is not None and ping_name in custom_pings_cache:
+ return custom_pings_cache[ping_name].bugs
+ else:
+ return None
+
+
+def ping_include_client_id(
+ ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None
+) -> bool:
+ if custom_pings_cache is not None and ping_name in custom_pings_cache:
+ return custom_pings_cache[ping_name].include_client_id
+ else:
+ return False
+
+
+def data_sensitivity_numbers(
+ data_sensitivity: Optional[List[metrics.DataSensitivity]],
+) -> str:
+ if data_sensitivity is None:
+ return "unknown"
+ else:
+ return ", ".join(str(x.value) for x in data_sensitivity)
+
+
+def output_markdown(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Markdown docs to `output_dir`.
+
+ This produces a single `metrics.md`. The file contains a table of
+ contents and a section for each ping metrics are collected for.
+
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ :param options: options dictionary, with the following optional key:
+ - `project_title`: The projects title.
+ """
+ if options is None:
+ options = {}
+
+ # Build a dictionary that associates pings with their metrics.
+ #
+ # {
+ # "baseline": [
+ # { ... metric data ... },
+ # ...
+ # ],
+ # "metrics": [
+ # { ... metric data ... },
+ # ...
+ # ],
+ # ...
+ # }
+ #
+ # This also builds a dictionary of custom pings, if available.
+ custom_pings_cache: Dict[str, pings.Ping] = defaultdict()
+ metrics_by_pings: Dict[str, List[metrics.Metric]] = defaultdict(list)
+ for _category_key, category_val in objs.items():
+ for obj in category_val.values():
+ # Filter out custom pings. We will need them for extracting
+ # the description
+ if isinstance(obj, pings.Ping):
+ custom_pings_cache[obj.name] = obj
+ # Pings that have `send_if_empty` set to true,
+ # might not have any metrics. They need to at least have an
+ # empty array of metrics to show up on the template.
+ if obj.send_if_empty and not metrics_by_pings[obj.name]:
+ metrics_by_pings[obj.name] = []
+
+ # If this is an internal Glean metric, and we don't
+ # want docs for it.
+ if isinstance(obj, metrics.Metric) and not obj.is_internal_metric():
+ # If we get here, obj is definitely a metric we want
+ # docs for.
+ for ping_name in obj.send_in_pings:
+ metrics_by_pings[ping_name].append(obj)
+
+ # Sort the metrics by their identifier, to make them show up nicely
+ # in the docs and to make generated docs reproducible.
+ for ping_name in metrics_by_pings:
+ metrics_by_pings[ping_name] = sorted(
+ metrics_by_pings[ping_name], key=lambda x: x.identifier()
+ )
+
+ project_title = options.get("project_title", "this project")
+ introduction_extra = options.get("introduction_extra")
+
+ template = util.get_jinja2_template(
+ "markdown.jinja2",
+ filters=(
+ ("extra_info", extra_info),
+ ("metrics_docs", metrics_docs),
+ ("ping_desc", lambda x: ping_desc(x, custom_pings_cache)),
+ ("ping_send_if_empty", lambda x: if_empty(x, custom_pings_cache)),
+ ("ping_docs", ping_docs),
+ ("ping_reasons", lambda x: ping_reasons(x, custom_pings_cache)),
+ ("ping_data_reviews", lambda x: ping_data_reviews(x, custom_pings_cache)),
+ ("ping_review_title", ping_review_title),
+ ("ping_bugs", lambda x: ping_bugs(x, custom_pings_cache)),
+ (
+ "ping_include_client_id",
+ lambda x: ping_include_client_id(x, custom_pings_cache),
+ ),
+ ("data_sensitivity_numbers", data_sensitivity_numbers),
+ ),
+ )
+
+ filename = "metrics.md"
+ filepath = output_dir / filename
+
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ metrics_by_pings=metrics_by_pings,
+ project_title=project_title,
+ introduction_extra=introduction_extra,
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
diff --git a/third_party/python/glean_parser/glean_parser/metrics.py b/third_party/python/glean_parser/glean_parser/metrics.py
new file mode 100644
index 0000000000..5738239f97
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/metrics.py
@@ -0,0 +1,437 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Classes for each of the high-level metric types.
+"""
+
+import enum
+from typing import Any, Dict, List, Optional, Type, Union # noqa
+
+
+from . import pings
+from . import tags
+from . import util
+
+
+# Important: if the values are ever changing here, make sure
+# to also fix mozilla/glean. Otherwise language bindings may
+# break there.
+class Lifetime(enum.Enum):
+ ping = 0
+ application = 1
+ user = 2
+
+
+class DataSensitivity(enum.Enum):
+ technical = 1
+ interaction = 2
+ stored_content = 3
+ web_activity = 3 # Old, deprecated name
+ highly_sensitive = 4
+
+
+class Metric:
+ typename: str = "ERROR"
+ glean_internal_metric_cat: str = "glean.internal.metrics"
+ metric_types: Dict[str, Any] = {}
+ default_store_names: List[str] = ["metrics"]
+
+ def __init__(
+ self,
+ type: str,
+ category: str,
+ name: str,
+ bugs: List[str],
+ description: str,
+ notification_emails: List[str],
+ expires: Any,
+ metadata: Optional[Dict] = None,
+ data_reviews: Optional[List[str]] = None,
+ version: int = 0,
+ disabled: bool = False,
+ lifetime: str = "ping",
+ send_in_pings: Optional[List[str]] = None,
+ unit: Optional[str] = None,
+ gecko_datapoint: str = "",
+ no_lint: Optional[List[str]] = None,
+ data_sensitivity: Optional[List[str]] = None,
+ defined_in: Optional[Dict] = None,
+ telemetry_mirror: Optional[str] = None,
+ _config: Optional[Dict[str, Any]] = None,
+ _validated: bool = False,
+ ):
+ # Avoid cyclical import
+ from . import parser
+
+ self.type = type
+ self.category = category
+ self.name = name
+ self.bugs = bugs
+ self.description = description
+ self.notification_emails = notification_emails
+ self.expires = expires
+ if metadata is None:
+ metadata = {}
+ self.metadata = metadata
+ if data_reviews is None:
+ data_reviews = []
+ self.data_reviews = data_reviews
+ self.version = version
+ self.disabled = disabled
+ self.lifetime = getattr(Lifetime, lifetime)
+ if send_in_pings is None:
+ send_in_pings = ["default"]
+ self.send_in_pings = send_in_pings
+ self.unit = unit
+ self.gecko_datapoint = gecko_datapoint
+ if no_lint is None:
+ no_lint = []
+ self.no_lint = no_lint
+ if data_sensitivity is not None:
+ self.data_sensitivity = [
+ getattr(DataSensitivity, x) for x in data_sensitivity
+ ]
+ self.defined_in = defined_in
+ if telemetry_mirror is not None:
+ self.telemetry_mirror = telemetry_mirror
+
+ # _validated indicates whether this metric has already been jsonschema
+ # validated (but not any of the Python-level validation).
+ if not _validated:
+ data = {
+ "$schema": parser.METRICS_ID,
+ self.category: {self.name: self._serialize_input()},
+ } # type: Dict[str, util.JSONType]
+ for error in parser.validate(data):
+ raise ValueError(error)
+
+ # Store the config, but only after validation.
+ if _config is None:
+ _config = {}
+ self._config = _config
+
+ # Metrics in the special category "glean.internal.metrics" need to have
+ # an empty category string when identifying the metrics in the ping.
+ if self.category == Metric.glean_internal_metric_cat:
+ self.category = ""
+
+ def __init_subclass__(cls, **kwargs):
+ # Create a mapping of all of the subclasses of this class
+ if cls not in Metric.metric_types and hasattr(cls, "typename"):
+ Metric.metric_types[cls.typename] = cls
+ super().__init_subclass__(**kwargs)
+
+ @classmethod
+ def make_metric(
+ cls,
+ category: str,
+ name: str,
+ metric_info: Dict[str, util.JSONType],
+ config: Optional[Dict[str, Any]] = None,
+ validated: bool = False,
+ ):
+ """
+ Given a metric_info dictionary from metrics.yaml, return a metric
+ instance.
+
+ :param: category The category the metric lives in
+ :param: name The name of the metric
+ :param: metric_info A dictionary of the remaining metric parameters
+ :param: config A dictionary containing commandline configuration
+ parameters
+ :param: validated True if the metric has already gone through
+ jsonschema validation
+ :return: A new Metric instance.
+ """
+ if config is None:
+ config = {}
+
+ metric_type = metric_info["type"]
+ if not isinstance(metric_type, str):
+ raise TypeError(f"Unknown metric type {metric_type}")
+ return cls.metric_types[metric_type](
+ category=category,
+ name=name,
+ defined_in=getattr(metric_info, "defined_in", None),
+ _validated=validated,
+ _config=config,
+ **metric_info,
+ )
+
+ def serialize(self) -> Dict[str, util.JSONType]:
+ """
+ Serialize the metric back to JSON object model.
+ """
+ d = self.__dict__.copy()
+ # Convert enum fields back to strings
+ for key, val in d.items():
+ if isinstance(val, enum.Enum):
+ d[key] = d[key].name
+ if isinstance(val, set):
+ d[key] = sorted(list(val))
+ if isinstance(val, list) and len(val) and isinstance(val[0], enum.Enum):
+ d[key] = [x.name for x in val]
+ del d["name"]
+ del d["category"]
+ if not d["unit"]:
+ d.pop("unit")
+ d.pop("_config", None)
+ d.pop("_generate_enums", None)
+ return d
+
+ def _serialize_input(self) -> Dict[str, util.JSONType]:
+ d = self.serialize()
+ modified_dict = util.remove_output_params(d, "defined_in")
+ return modified_dict
+
+ def identifier(self) -> str:
+ """
+ Create an identifier unique for this metric.
+ Generally, category.name; however, Glean internal
+ metrics only use name.
+ """
+ if not self.category:
+ return self.name
+ return ".".join((self.category, self.name))
+
+ def is_disabled(self) -> bool:
+ return self.disabled or self.is_expired()
+
+ def is_expired(self) -> bool:
+ def default_handler(expires) -> bool:
+ return util.is_expired(expires, self._config.get("expire_by_version"))
+
+ return self._config.get("custom_is_expired", default_handler)(self.expires)
+
+ def validate_expires(self):
+ def default_handler(expires):
+ return util.validate_expires(expires, self._config.get("expire_by_version"))
+
+ return self._config.get("custom_validate_expires", default_handler)(
+ self.expires
+ )
+
+ def is_internal_metric(self) -> bool:
+ return self.category in (Metric.glean_internal_metric_cat, "")
+
+
+class Boolean(Metric):
+ typename = "boolean"
+
+
+class String(Metric):
+ typename = "string"
+
+
+class StringList(Metric):
+ typename = "string_list"
+
+
+class Counter(Metric):
+ typename = "counter"
+
+
+class Quantity(Metric):
+ typename = "quantity"
+
+
+class TimeUnit(enum.Enum):
+ nanosecond = 0
+ microsecond = 1
+ millisecond = 2
+ second = 3
+ minute = 4
+ hour = 5
+ day = 6
+
+
+class TimeBase(Metric):
+ def __init__(self, *args, **kwargs):
+ self.time_unit = getattr(TimeUnit, kwargs.pop("time_unit", "millisecond"))
+ super().__init__(*args, **kwargs)
+
+
+class Timespan(TimeBase):
+ typename = "timespan"
+
+
+class TimingDistribution(TimeBase):
+ typename = "timing_distribution"
+
+ def __init__(self, *args, **kwargs):
+ self.time_unit = getattr(TimeUnit, kwargs.pop("time_unit", "nanosecond"))
+ Metric.__init__(self, *args, **kwargs)
+
+
+class MemoryUnit(enum.Enum):
+ byte = 0
+ kilobyte = 1
+ megabyte = 2
+ gigabyte = 3
+
+
+class MemoryDistribution(Metric):
+ typename = "memory_distribution"
+
+ def __init__(self, *args, **kwargs):
+ self.memory_unit = getattr(MemoryUnit, kwargs.pop("memory_unit", "byte"))
+ super().__init__(*args, **kwargs)
+
+
+class HistogramType(enum.Enum):
+ linear = 0
+ exponential = 1
+
+
+class CustomDistribution(Metric):
+ typename = "custom_distribution"
+
+ def __init__(self, *args, **kwargs):
+ self.range_min = kwargs.pop("range_min", 1)
+ self.range_max = kwargs.pop("range_max")
+ self.bucket_count = kwargs.pop("bucket_count")
+ self.histogram_type = getattr(
+ HistogramType, kwargs.pop("histogram_type", "exponential")
+ )
+ super().__init__(*args, **kwargs)
+
+
+class Datetime(TimeBase):
+ typename = "datetime"
+
+
+class Event(Metric):
+ typename = "event"
+
+ default_store_names = ["events"]
+
+ def __init__(self, *args, **kwargs):
+ self.extra_keys = kwargs.pop("extra_keys", {})
+ self.validate_extra_keys(self.extra_keys, kwargs.get("_config", {}))
+ super().__init__(*args, **kwargs)
+ self._generate_enums = [("allowed_extra_keys_with_types", "Extra")]
+
+ @property
+ def allowed_extra_keys(self):
+ # Sort keys so that output is deterministic
+ return sorted(list(self.extra_keys.keys()))
+
+ @property
+ def allowed_extra_keys_with_types(self):
+ # Sort keys so that output is deterministic
+ return sorted(
+ [(k, v.get("type", "string")) for (k, v) in self.extra_keys.items()],
+ key=lambda x: x[0],
+ )
+
+ @staticmethod
+ def validate_extra_keys(extra_keys: Dict[str, str], config: Dict[str, Any]) -> None:
+ if not config.get("allow_reserved") and any(
+ k.startswith("glean.") for k in extra_keys.keys()
+ ):
+ raise ValueError(
+ "Extra keys beginning with 'glean.' are reserved for "
+ "Glean internal use."
+ )
+
+
+class Uuid(Metric):
+ typename = "uuid"
+
+
+class Url(Metric):
+ typename = "url"
+
+
+class Jwe(Metric):
+ typename = "jwe"
+
+ def __init__(self, *args, **kwargs):
+ raise ValueError(
+ "JWE support was removed. "
+ "If you require this send an email to glean-team@mozilla.com."
+ )
+
+
+class CowString(str):
+ """
+ Wrapper class for strings that should be represented
+ as a `Cow<'static, str>` in Rust,
+ or `String` in other target languages.
+
+ This wraps `str`, so unless `CowString` is specifically
+ handled it acts (and serializes)
+ as a string.
+ """
+
+ def __init__(self, val: str):
+ self.inner: str = val
+
+ def __eq__(self, other):
+ return self.inner == other.inner
+
+ def __hash__(self):
+ return self.inner.__hash__()
+
+ def __lt__(self, other):
+ return self.inner.__lt__(other.inner)
+
+
+class Labeled(Metric):
+ labeled = True
+
+ def __init__(self, *args, **kwargs):
+ labels = kwargs.pop("labels", None)
+ if labels is not None:
+ self.ordered_labels = labels
+ self.labels = set([CowString(label) for label in labels])
+ else:
+ self.ordered_labels = None
+ self.labels = None
+ super().__init__(*args, **kwargs)
+
+ def serialize(self) -> Dict[str, util.JSONType]:
+ """
+ Serialize the metric back to JSON object model.
+ """
+ d = super().serialize()
+ d["labels"] = self.ordered_labels
+ del d["ordered_labels"]
+ return d
+
+
+class LabeledBoolean(Labeled, Boolean):
+ typename = "labeled_boolean"
+
+
+class LabeledString(Labeled, String):
+ typename = "labeled_string"
+
+
+class LabeledCounter(Labeled, Counter):
+ typename = "labeled_counter"
+
+
+class Rate(Metric):
+ typename = "rate"
+
+ def __init__(self, *args, **kwargs):
+ self.denominator_metric = kwargs.pop("denominator_metric", None)
+ super().__init__(*args, **kwargs)
+
+
+class Denominator(Counter):
+ typename = "denominator"
+ # A denominator is a counter with an additional list of numerators.
+ numerators: List[Rate] = []
+
+
+class Text(Metric):
+ typename = "text"
+
+
+ObjectTree = Dict[str, Dict[str, Union[Metric, pings.Ping, tags.Tag]]]
diff --git a/third_party/python/glean_parser/glean_parser/parser.py b/third_party/python/glean_parser/glean_parser/parser.py
new file mode 100644
index 0000000000..5ca584ac1e
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/parser.py
@@ -0,0 +1,446 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Code for parsing metrics.yaml files.
+"""
+
+import functools
+from pathlib import Path
+import textwrap
+from typing import Any, Dict, Generator, Iterable, Optional, Tuple, Union
+
+import jsonschema # type: ignore
+from jsonschema.exceptions import ValidationError # type: ignore
+
+from .metrics import Metric, ObjectTree
+from .pings import Ping, RESERVED_PING_NAMES
+from .tags import Tag
+from . import util
+from .util import DictWrapper
+
+
+ROOT_DIR = Path(__file__).parent
+SCHEMAS_DIR = ROOT_DIR / "schemas"
+
+METRICS_ID = "moz://mozilla.org/schemas/glean/metrics/2-0-0"
+PINGS_ID = "moz://mozilla.org/schemas/glean/pings/2-0-0"
+TAGS_ID = "moz://mozilla.org/schemas/glean/tags/1-0-0"
+
+
+def _update_validator(validator):
+ """
+ Adds some custom validators to the jsonschema validator that produce
+ nicer error messages.
+ """
+
+ def required(validator, required, instance, schema):
+ if not validator.is_type(instance, "object"):
+ return
+ missing_properties = set(
+ property for property in required if property not in instance
+ )
+ if len(missing_properties):
+ missing_properties = sorted(list(missing_properties))
+ yield ValidationError(
+ f"Missing required properties: {', '.join(missing_properties)}"
+ )
+
+ validator.VALIDATORS["required"] = required
+
+
+def _load_file(
+ filepath: Path, parser_config: Dict[str, Any]
+) -> Generator[str, None, Tuple[Dict[str, util.JSONType], Optional[str]]]:
+ """
+ Load a metrics.yaml or pings.yaml format file.
+
+ If the `filepath` does not exist, raises `FileNotFoundError`, unless
+ `parser_config["allow_missing_files"]` is `True`.
+ """
+ try:
+ content = util.load_yaml_or_json(filepath)
+ except FileNotFoundError:
+ if not parser_config.get("allow_missing_files", False):
+ raise
+ else:
+ return {}, None
+ except Exception as e:
+ yield util.format_error(filepath, "", textwrap.fill(str(e)))
+ return {}, None
+
+ if content is None:
+ yield util.format_error(filepath, "", f"'{filepath}' file can not be empty.")
+ return {}, None
+
+ if not isinstance(content, dict):
+ return {}, None
+
+ if content == {}:
+ return {}, None
+
+ schema_key = content.get("$schema")
+ if not isinstance(schema_key, str):
+ raise TypeError(f"Invalid schema key {schema_key}")
+
+ filetype: Optional[str] = None
+ try:
+ filetype = schema_key.split("/")[-2]
+ except IndexError:
+ filetype = None
+
+ if filetype not in ("metrics", "pings", "tags"):
+ filetype = None
+
+ for error in validate(content, filepath):
+ content = {}
+ yield error
+
+ return content, filetype
+
+
+@functools.lru_cache(maxsize=1)
+def _load_schemas() -> Dict[str, Tuple[Any, Any]]:
+ """
+ Load all of the known schemas from disk, and put them in a map based on the
+ schema's $id.
+ """
+ schemas = {}
+ for schema_path in SCHEMAS_DIR.glob("*.yaml"):
+ schema = util.load_yaml_or_json(schema_path)
+ resolver = util.get_null_resolver(schema)
+ validator_class = jsonschema.validators.validator_for(schema)
+ _update_validator(validator_class)
+ validator_class.check_schema(schema)
+ validator = validator_class(schema, resolver=resolver)
+ schemas[schema["$id"]] = (schema, validator)
+ return schemas
+
+
+def _get_schema(
+ schema_id: str, filepath: Union[str, Path] = "<input>"
+) -> Tuple[Any, Any]:
+ """
+ Get the schema for the given schema $id.
+ """
+ schemas = _load_schemas()
+ if schema_id not in schemas:
+ raise ValueError(
+ util.format_error(
+ filepath,
+ "",
+ f"$schema key must be one of {', '.join(schemas.keys())}",
+ )
+ )
+ return schemas[schema_id]
+
+
+def _get_schema_for_content(
+ content: Dict[str, util.JSONType], filepath: Union[str, Path]
+) -> Tuple[Any, Any]:
+ """
+ Get the appropriate schema for the given JSON content.
+ """
+ schema_url = content.get("$schema")
+ if not isinstance(schema_url, str):
+ raise TypeError("Invalid $schema type {schema_url}")
+ return _get_schema(schema_url, filepath)
+
+
+def validate(
+ content: Dict[str, util.JSONType], filepath: Union[str, Path] = "<input>"
+) -> Generator[str, None, None]:
+ """
+ Validate the given content against the appropriate schema.
+ """
+ try:
+ schema, validator = _get_schema_for_content(content, filepath)
+ except ValueError as e:
+ yield str(e)
+ else:
+ yield from (
+ util.format_error(filepath, "", util.pprint_validation_error(e))
+ for e in validator.iter_errors(content)
+ )
+
+
+def _instantiate_metrics(
+ all_objects: ObjectTree,
+ sources: Dict[Any, Path],
+ content: Dict[str, util.JSONType],
+ filepath: Path,
+ config: Dict[str, Any],
+) -> Generator[str, None, None]:
+ """
+ Load a list of metrics.yaml files, convert the JSON information into Metric
+ objects, and merge them into a single tree.
+ """
+ global_no_lint = content.get("no_lint", [])
+ global_tags = content.get("$tags", [])
+ assert isinstance(global_tags, list)
+
+ for category_key, category_val in sorted(content.items()):
+ if category_key.startswith("$"):
+ continue
+ if category_key == "no_lint":
+ continue
+ if not config.get("allow_reserved") and category_key.split(".")[0] == "glean":
+ yield util.format_error(
+ filepath,
+ f"For category '{category_key}'",
+ "Categories beginning with 'glean' are reserved for "
+ "Glean internal use.",
+ )
+ continue
+ all_objects.setdefault(category_key, DictWrapper())
+
+ if not isinstance(category_val, dict):
+ raise TypeError(f"Invalid content for {category_key}")
+
+ for metric_key, metric_val in sorted(category_val.items()):
+ try:
+ metric_obj = Metric.make_metric(
+ category_key, metric_key, metric_val, validated=True, config=config
+ )
+ except Exception as e:
+ yield util.format_error(
+ filepath,
+ f"On instance {category_key}.{metric_key}",
+ str(e),
+ metric_val.defined_in["line"],
+ )
+ metric_obj = None
+ else:
+ if (
+ not config.get("allow_reserved")
+ and "all-pings" in metric_obj.send_in_pings
+ ):
+ yield util.format_error(
+ filepath,
+ f"On instance {category_key}.{metric_key}",
+ 'Only internal metrics may specify "all-pings" '
+ 'in "send_in_pings"',
+ metric_val.defined_in["line"],
+ )
+ metric_obj = None
+
+ if metric_obj is not None:
+ metric_obj.no_lint = sorted(set(metric_obj.no_lint + global_no_lint))
+ if len(global_tags):
+ metric_obj.metadata["tags"] = sorted(
+ set(metric_obj.metadata.get("tags", []) + global_tags)
+ )
+
+ if isinstance(filepath, Path):
+ metric_obj.defined_in["filepath"] = str(filepath)
+
+ already_seen = sources.get((category_key, metric_key))
+ if already_seen is not None:
+ # We've seen this metric name already
+ yield util.format_error(
+ filepath,
+ "",
+ (
+ f"Duplicate metric name '{category_key}.{metric_key}' "
+ f"already defined in '{already_seen}'"
+ ),
+ metric_obj.defined_in["line"],
+ )
+ else:
+ all_objects[category_key][metric_key] = metric_obj
+ sources[(category_key, metric_key)] = filepath
+
+
+def _instantiate_pings(
+ all_objects: ObjectTree,
+ sources: Dict[Any, Path],
+ content: Dict[str, util.JSONType],
+ filepath: Path,
+ config: Dict[str, Any],
+) -> Generator[str, None, None]:
+ """
+ Load a list of pings.yaml files, convert the JSON information into Ping
+ objects.
+ """
+ global_no_lint = content.get("no_lint", [])
+ assert isinstance(global_no_lint, list)
+
+ for ping_key, ping_val in sorted(content.items()):
+ if ping_key.startswith("$"):
+ continue
+ if ping_key == "no_lint":
+ continue
+ if not config.get("allow_reserved"):
+ if ping_key in RESERVED_PING_NAMES:
+ yield util.format_error(
+ filepath,
+ f"For ping '{ping_key}'",
+ f"Ping uses a reserved name ({RESERVED_PING_NAMES})",
+ )
+ continue
+ if not isinstance(ping_val, dict):
+ raise TypeError(f"Invalid content for ping {ping_key}")
+ ping_val["name"] = ping_key
+ try:
+ ping_obj = Ping(
+ defined_in=getattr(ping_val, "defined_in", None),
+ _validated=True,
+ **ping_val,
+ )
+ except Exception as e:
+ yield util.format_error(filepath, f"On instance '{ping_key}'", str(e))
+ continue
+
+ if ping_obj is not None:
+ ping_obj.no_lint = sorted(set(ping_obj.no_lint + global_no_lint))
+
+ if isinstance(filepath, Path) and ping_obj.defined_in is not None:
+ ping_obj.defined_in["filepath"] = str(filepath)
+
+ already_seen = sources.get(ping_key)
+ if already_seen is not None:
+ # We've seen this ping name already
+ yield util.format_error(
+ filepath,
+ "",
+ f"Duplicate ping name '{ping_key}' "
+ f"already defined in '{already_seen}'",
+ )
+ else:
+ all_objects.setdefault("pings", {})[ping_key] = ping_obj
+ sources[ping_key] = filepath
+
+
+def _instantiate_tags(
+ all_objects: ObjectTree,
+ sources: Dict[Any, Path],
+ content: Dict[str, util.JSONType],
+ filepath: Path,
+ config: Dict[str, Any],
+) -> Generator[str, None, None]:
+ """
+ Load a list of tags.yaml files, convert the JSON information into Tag
+ objects.
+ """
+ global_no_lint = content.get("no_lint", [])
+ assert isinstance(global_no_lint, list)
+
+ for tag_key, tag_val in sorted(content.items()):
+ if tag_key.startswith("$"):
+ continue
+ if tag_key == "no_lint":
+ continue
+ if not isinstance(tag_val, dict):
+ raise TypeError(f"Invalid content for tag {tag_key}")
+ tag_val["name"] = tag_key
+ try:
+ tag_obj = Tag(
+ defined_in=getattr(tag_val, "defined_in", None),
+ _validated=True,
+ **tag_val,
+ )
+ except Exception as e:
+ yield util.format_error(filepath, f"On instance '{tag_key}'", str(e))
+ continue
+
+ if tag_obj is not None:
+ tag_obj.no_lint = sorted(set(tag_obj.no_lint + global_no_lint))
+
+ if isinstance(filepath, Path) and tag_obj.defined_in is not None:
+ tag_obj.defined_in["filepath"] = str(filepath)
+
+ already_seen = sources.get(tag_key)
+ if already_seen is not None:
+ # We've seen this tag name already
+ yield util.format_error(
+ filepath,
+ "",
+ f"Duplicate tag name '{tag_key}' "
+ f"already defined in '{already_seen}'",
+ )
+ else:
+ all_objects.setdefault("tags", {})[tag_key] = tag_obj
+ sources[tag_key] = filepath
+
+
+def _preprocess_objects(objs: ObjectTree, config: Dict[str, Any]) -> ObjectTree:
+ """
+ Preprocess the object tree to better set defaults.
+ """
+ for category in objs.values():
+ for obj in category.values():
+ if not isinstance(obj, Metric):
+ continue
+
+ if not config.get("do_not_disable_expired", False) and hasattr(
+ obj, "is_disabled"
+ ):
+ obj.disabled = obj.is_disabled()
+
+ if hasattr(obj, "send_in_pings"):
+ if "default" in obj.send_in_pings:
+ obj.send_in_pings = obj.default_store_names + [
+ x for x in obj.send_in_pings if x != "default"
+ ]
+ obj.send_in_pings = sorted(list(set(obj.send_in_pings)))
+ return objs
+
+
+@util.keep_value
+def parse_objects(
+ filepaths: Iterable[Path], config: Optional[Dict[str, Any]] = None
+) -> Generator[str, None, ObjectTree]:
+ """
+ Parse one or more metrics.yaml and/or pings.yaml files, returning a tree of
+ `metrics.Metric`, `pings.Ping`, and `tags.Tag` instances.
+
+ The result is a generator over any errors. If there are no errors, the
+ actual metrics can be obtained from `result.value`. For example::
+
+ result = metrics.parse_metrics(filepaths)
+ for err in result:
+ print(err)
+ all_metrics = result.value
+
+ The result value is a dictionary of category names to categories, where
+ each category is a dictionary from metric name to `metrics.Metric`
+ instances. There are also the special categories `pings` and `tags`
+ containing all of the `pings.Ping` and `tags.Tag` instances, respectively.
+
+ :param filepaths: list of Path objects to metrics.yaml, pings.yaml, and/or
+ tags.yaml files
+ :param config: A dictionary of options that change parsing behavior.
+ Supported keys are:
+
+ - `allow_reserved`: Allow values reserved for internal Glean use.
+ - `do_not_disable_expired`: Don't mark expired metrics as disabled.
+ This is useful when you want to retain the original "disabled"
+ value from the `metrics.yaml`, rather than having it overridden when
+ the metric expires.
+ - `allow_missing_files`: Do not raise a `FileNotFoundError` if any of
+ the input `filepaths` do not exist.
+ """
+ if config is None:
+ config = {}
+
+ all_objects: ObjectTree = DictWrapper()
+ sources: Dict[Any, Path] = {}
+ filepaths = util.ensure_list(filepaths)
+ for filepath in filepaths:
+ content, filetype = yield from _load_file(filepath, config)
+ if filetype == "metrics":
+ yield from _instantiate_metrics(
+ all_objects, sources, content, filepath, config
+ )
+ elif filetype == "pings":
+ yield from _instantiate_pings(
+ all_objects, sources, content, filepath, config
+ )
+ elif filetype == "tags":
+ yield from _instantiate_tags(
+ all_objects, sources, content, filepath, config
+ )
+ return _preprocess_objects(all_objects, config)
diff --git a/third_party/python/glean_parser/glean_parser/pings.py b/third_party/python/glean_parser/glean_parser/pings.py
new file mode 100644
index 0000000000..3099fa1d16
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/pings.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Classes for managing the description of pings.
+"""
+
+from typing import Dict, List, Optional
+
+
+from . import util
+
+
+RESERVED_PING_NAMES = ["baseline", "metrics", "events", "deletion-request", "default"]
+
+
+class Ping:
+ def __init__(
+ self,
+ name: str,
+ description: str,
+ bugs: List[str],
+ notification_emails: List[str],
+ metadata: Optional[Dict] = None,
+ data_reviews: Optional[List[str]] = None,
+ include_client_id: bool = False,
+ send_if_empty: bool = False,
+ reasons: Optional[Dict[str, str]] = None,
+ defined_in: Optional[Dict] = None,
+ no_lint: Optional[List[str]] = None,
+ _validated: bool = False,
+ ):
+ # Avoid cyclical import
+ from . import parser
+
+ self.name = name
+ self.description = description
+
+ self.bugs = bugs
+ self.notification_emails = notification_emails
+ if metadata is None:
+ metadata = {}
+ self.metadata = metadata
+ self.precise_timestamps = self.metadata.get("precise_timestamps", True)
+ if data_reviews is None:
+ data_reviews = []
+ self.data_reviews = data_reviews
+ self.include_client_id = include_client_id
+ self.send_if_empty = send_if_empty
+ if reasons is None:
+ reasons = {}
+ self.reasons = reasons
+ self.defined_in = defined_in
+ if no_lint is None:
+ no_lint = []
+ self.no_lint = no_lint
+
+ # _validated indicates whether this ping has already been jsonschema
+ # validated (but not any of the Python-level validation).
+ if not _validated:
+ data: Dict[str, util.JSONType] = {
+ "$schema": parser.PINGS_ID,
+ self.name: self._serialize_input(),
+ }
+ for error in parser.validate(data):
+ raise ValueError(error)
+
+ _generate_enums = [("reason_codes", "ReasonCodes")]
+
+ @property
+ def type(self) -> str:
+ return "ping"
+
+ @property
+ def reason_codes(self) -> List[str]:
+ return sorted(list(self.reasons.keys()))
+
+ def serialize(self) -> Dict[str, util.JSONType]:
+ """
+ Serialize the metric back to JSON object model.
+ """
+ d = self.__dict__.copy()
+ del d["name"]
+ return d
+
+ def _serialize_input(self) -> Dict[str, util.JSONType]:
+ d = self.serialize()
+ modified_dict = util.remove_output_params(d, "defined_in")
+ modified_dict = util.remove_output_params(modified_dict, "precise_timestamps")
+ return modified_dict
+
+ def identifier(self) -> str:
+ """
+ Used for the "generated from ..." comment in the output.
+ """
+ return self.name
diff --git a/third_party/python/glean_parser/glean_parser/ruby_server.py b/third_party/python/glean_parser/glean_parser/ruby_server.py
new file mode 100644
index 0000000000..bbca3df80c
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/ruby_server.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Outputter to generate server ruby code for collecting events.
+
+This outputter is different from the rest of the outputters in that the code it
+generates does not use the Glean SDK. It is meant to be used to collect events
+using "events as pings" pattern in server-side environments. In these environments
+SDK assumptions to measurement window and connectivity don't hold.
+Generated code takes care of assembling pings with metrics, serializing to messages
+conforming to Glean schema, and logging using a standard Ruby logger.
+Then it's the role of the ingestion pipeline to pick the messages up and process.
+
+Warning: this outputter supports a limited set of metrics,
+see `SUPPORTED_METRIC_TYPES` below.
+"""
+from collections import defaultdict
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from . import __version__, metrics, util
+
+SUPPORTED_METRIC_TYPES = ["string", "event"]
+
+
+def ping_class_name(pingName: str) -> str:
+ return f"Glean{util.Camelize(pingName)}Logger"
+
+
+def generate_metric_name(metric: metrics.Metric) -> str:
+ return f"{metric.category}.{metric.name}"
+
+
+def generate_metric_argument_name(metric: metrics.Metric) -> str:
+ return f"{metric.category}_{metric.name}"
+
+
+def generate_metric_argument_description(metric: metrics.Metric) -> str:
+ return metric.description.replace("\n", " ").rstrip()
+
+
+def event_class_name(metric: metrics.Metric) -> str:
+ return f"{util.Camelize(generate_metric_argument_name(metric))}Event"
+
+
+def output_ruby(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]]
+) -> None:
+ """
+ Given a tree of objects, output ruby code to `output_dir`.
+
+ The output is a single file containing all the code for assembling pings with
+ metrics, serializing, and submitting.
+
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ """
+
+ template = util.get_jinja2_template(
+ "ruby_server.jinja2",
+ filters=(
+ ("ping_class_name", ping_class_name),
+ ("metric_name", generate_metric_name),
+ ("metric_argument_name", generate_metric_argument_name),
+ ("metric_argument_description", generate_metric_argument_description),
+ ("event_class_name", event_class_name),
+ ),
+ )
+
+ # In this environment we don't use a concept of measurement window for collecting
+ # metrics. Only "events as pings" are supported.
+ # For each ping we generate code which contains all the logic for assembling it
+ # with metrics, serializing, and submitting. Therefore we don't generate classes for
+ # each metric as in standard outputters.
+ PING_METRIC_ERROR_MSG = (
+ " Server-side environment is simplified and only supports the events ping type."
+ + " You should not be including pings.yaml with your parser call"
+ + " or referencing any other pings in your metric configuration."
+ )
+ if "pings" in objs:
+ print("❌ Ping definition found." + PING_METRIC_ERROR_MSG)
+ return
+
+ # Go through all metrics in objs and build a map of
+ # ping->list of metric categories->list of metrics
+ # for easier processing in the template.
+ ping_to_metrics: Dict[str, Dict[str, List[metrics.Metric]]] = defaultdict(dict)
+ for _category_key, category_val in objs.items():
+ for _metric_name, metric in category_val.items():
+ if isinstance(metric, metrics.Metric):
+ if metric.type not in SUPPORTED_METRIC_TYPES:
+ print(
+ "❌ Ignoring unsupported metric type: "
+ + f"{metric.type}:{metric.name}."
+ + " Reach out to Glean team to add support for this"
+ + " metric type."
+ )
+ continue
+ for ping in metric.send_in_pings:
+ if ping != "events":
+ (
+ print(
+ "❌ Non-events ping reference found."
+ + PING_METRIC_ERROR_MSG
+ + f"Ignoring the {ping} ping type."
+ )
+ )
+ continue
+ metrics_by_type = ping_to_metrics[ping]
+ metrics_list = metrics_by_type.setdefault(metric.type, [])
+ metrics_list.append(metric)
+ if "event" not in ping_to_metrics["events"]:
+ print("❌ No event metrics found...at least one event metric is required")
+ return
+ extension = ".rb"
+ filepath = output_dir / ("server_events" + extension)
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ pings=ping_to_metrics,
+ )
+ )
diff --git a/third_party/python/glean_parser/glean_parser/rust.py b/third_party/python/glean_parser/glean_parser/rust.py
new file mode 100644
index 0000000000..eb3355e382
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/rust.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Outputter to generate Rust code for metrics.
+"""
+
+import enum
+import json
+from pathlib import Path
+from typing import Any, Dict, Optional, Union
+
+from . import __version__
+from . import metrics
+from . import pings
+from . import tags
+from . import util
+
+
+def rust_datatypes_filter(value):
+ """
+ A Jinja2 filter that renders Rust literals.
+
+ Based on Python's JSONEncoder, but overrides:
+ - dicts and sets to raise an error
+ - sets to vec![] (used in labels)
+ - enums to become Class::Value
+ - lists to vec![] (used in send_in_pings)
+ - null to None
+ - strings to "value".into()
+ - Rate objects to a CommonMetricData initializer
+ (for external Denominators' Numerators lists)
+ """
+
+ class RustEncoder(json.JSONEncoder):
+ def iterencode(self, value):
+ if isinstance(value, dict):
+ raise ValueError("RustEncoder doesn't know dicts {}".format(str(value)))
+ elif isinstance(value, enum.Enum):
+ yield (value.__class__.__name__ + "::" + util.Camelize(value.name))
+ elif isinstance(value, set):
+ yield "vec!["
+ first = True
+ for subvalue in sorted(list(value)):
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield "]"
+ elif isinstance(value, list):
+ yield "vec!["
+ first = True
+ for subvalue in list(value):
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield "]"
+ elif value is None:
+ yield "None"
+ # `CowStr` is a `str`, so needs to be before next case
+ elif isinstance(value, metrics.CowString):
+ yield f'::std::borrow::Cow::from("{value.inner}")'
+ elif isinstance(value, str):
+ yield f'"{value}".into()'
+ elif isinstance(value, metrics.Rate):
+ yield "CommonMetricData("
+ first = True
+ for arg_name in util.common_metric_args:
+ if hasattr(value, arg_name):
+ if not first:
+ yield ", "
+ yield f"{util.camelize(arg_name)} = "
+ yield from self.iterencode(getattr(value, arg_name))
+ first = False
+ yield ")"
+ else:
+ yield from super().iterencode(value)
+
+ return "".join(RustEncoder().iterencode(value))
+
+
+def ctor(obj):
+ """
+ Returns the scope and name of the constructor to use for a metric object.
+ Necessary because LabeledMetric<T> is constructed using LabeledMetric::new
+ not LabeledMetric<T>::new
+ """
+ if getattr(obj, "labeled", False):
+ return "LabeledMetric::new"
+ return class_name(obj.type) + "::new"
+
+
+def type_name(obj):
+ """
+ Returns the Rust type to use for a given metric or ping object.
+ """
+
+ if getattr(obj, "labeled", False):
+ return "LabeledMetric<{}>".format(class_name(obj.type))
+ generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons?
+ if len(generate_enums):
+ generic = None
+ for name, suffix in generate_enums:
+ if len(getattr(obj, name)):
+ generic = util.Camelize(obj.name) + suffix
+ else:
+ if isinstance(obj, metrics.Event):
+ generic = "NoExtra"
+ else:
+ generic = "No" + suffix
+
+ return "{}<{}>".format(class_name(obj.type), generic)
+
+ return class_name(obj.type)
+
+
+def extra_type_name(typ: str) -> str:
+ """
+ Returns the corresponding Rust type for event's extra key types.
+ """
+
+ if typ == "boolean":
+ return "bool"
+ elif typ == "string":
+ return "String"
+ elif typ == "quantity":
+ return "u32"
+ else:
+ return "UNSUPPORTED"
+
+
+def class_name(obj_type):
+ """
+ Returns the Rust class name for a given metric or ping type.
+ """
+ if obj_type == "ping":
+ return "Ping"
+ if obj_type.startswith("labeled_"):
+ obj_type = obj_type[8:]
+ return util.Camelize(obj_type) + "Metric"
+
+
+def extra_keys(allowed_extra_keys):
+ """
+ Returns the &'static [&'static str] ALLOWED_EXTRA_KEYS for impl ExtraKeys
+ """
+ return "&[" + ", ".join([f'"{key}"' for key in allowed_extra_keys]) + "]"
+
+
+class Category:
+ """
+ Data struct holding information about a metric to be used in the template.
+ """
+
+ def __init__(
+ self,
+ name: str,
+ objs: Dict[str, Union[metrics.Metric, pings.Ping, tags.Tag]],
+ contains_pings: bool,
+ ):
+ self.name = name
+ self.objs = objs
+ self.contains_pings = contains_pings
+
+
+def output_rust(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Rust code to `output_dir`.
+
+ :param objs: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ :param options: options dictionary, not currently used for Rust
+ """
+
+ if options is None:
+ options = {}
+
+ template = util.get_jinja2_template(
+ "rust.jinja2",
+ filters=(
+ ("rust", rust_datatypes_filter),
+ ("snake_case", util.snake_case),
+ ("camelize", util.camelize),
+ ("type_name", type_name),
+ ("extra_type_name", extra_type_name),
+ ("ctor", ctor),
+ ("extra_keys", extra_keys),
+ ),
+ )
+
+ filename = "glean_metrics.rs"
+ filepath = output_dir / filename
+ categories = []
+
+ for category_key, category_val in objs.items():
+ contains_pings = any(
+ isinstance(obj, pings.Ping) for obj in category_val.values()
+ )
+
+ cat = Category(category_key, category_val, contains_pings)
+ categories.append(cat)
+
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ categories=categories,
+ extra_metric_args=util.extra_metric_args,
+ common_metric_args=util.common_metric_args,
+ )
+ )
diff --git a/third_party/python/glean_parser/glean_parser/schemas/metrics.1-0-0.schema.yaml b/third_party/python/glean_parser/glean_parser/schemas/metrics.1-0-0.schema.yaml
new file mode 100644
index 0000000000..047124b771
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/schemas/metrics.1-0-0.schema.yaml
@@ -0,0 +1,605 @@
+# 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/.
+
+---
+$schema: http://json-schema.org/draft-07/schema#
+title: Metrics
+description: |
+ Schema for the metrics.yaml files for Mozilla's Glean telemetry SDK.
+
+ The top-level of the `metrics.yaml` file has a key defining each category of
+ metrics. Categories must be snake_case, and they may also have dots `.` to
+ define subcategories.
+
+$id: moz://mozilla.org/schemas/glean/metrics/1-0-0
+
+definitions:
+ token:
+ type: string
+ pattern: "^[A-Za-z_][A-Za-z0-9_\\.]*$"
+
+ snake_case:
+ type: string
+ pattern: "^[a-z_][a-z0-9_]*$"
+
+ dotted_snake_case:
+ type: string
+ pattern: "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})*$"
+ maxLength: 40
+
+ kebab_case:
+ type: string
+ # Bug 1601270; we allow 3 specific existing snake_cased ping names for now,
+ # but these special cases can be removed once the number of legacy clients
+ # sufficiently dwindles, likely in 2020H2.
+ pattern: "^[a-z][a-z0-9-]{0,29}$\
+ |^deletion_request$|^bookmarks_sync$|^history_sync$|^session_end$|^all_pings$|^glean_.*$"
+
+ long_id:
+ allOf:
+ - $ref: "#/definitions/snake_case"
+ - maxLength: 40
+
+ short_id:
+ allOf:
+ - $ref: "#/definitions/snake_case"
+ - maxLength: 30
+
+ labeled_metric_id:
+ type: string
+ pattern: "^[a-z_][a-z0-9_-]{0,29}(\\.[a-z_][a-z0-9_-]{0,29})*$"
+ maxLength: 71 # Note: this should be category + metric + 1
+
+ metric:
+ description: |
+ Describes a single metric.
+
+ See https://mozilla.github.io/glean_parser/metrics-yaml.html
+
+ type: object
+
+ additionalProperties: false
+
+ properties:
+ type:
+ title: Metric type
+ description: |
+ **Required.**
+
+ Specifies the type of a metric, like "counter" or "event". This
+ defines which operations are valid for the metric, how it is stored
+ and how data analysis tooling displays it.
+
+ The supported types are:
+ - `event`: Record a specific event (with optional metadata).
+ Additional properties: `extra_keys`.
+
+ - `boolean`: A metric storing values of true or false.
+
+ - `string`: A metric storing Unicode string values.
+
+ - `string_list`: a list of Unicode strings.
+
+ - `counter`: A numeric value that can only be incremented.
+
+ - `quantity`: A numeric value that is set directly.
+
+ - `timespan`: Represents a time interval. Additional properties:
+ `time_unit`.
+
+ - `timing_distribution`: Record the distribution of multiple
+ timings. Additional properties: `time_unit`.
+
+ - `datetime`: A date/time value. Represented as an ISO datetime in
+ UTC. Additional properties: `time_unit`.
+
+ - `uuid`: Record a UUID v4.
+
+ - `jwe`: Record a [JWE](https://tools.ietf.org/html/rfc7516) value.
+
+ - `memory_distribution`: A histogram for recording memory usage
+ values. Additional properties: `memory_unit`.
+
+ - `custom_distribution`: A histogram with a custom range and number
+ of buckets. This metric type is for legacy support only and is
+ only allowed for metrics coming from GeckoView. Additional
+ properties: `range_min`, `range_max`, `bucket_count`,
+ `histogram_type`.
+
+ - `rate`: Used to record the rate something happens relative to some
+ other thing. For example, the number of HTTP connections that
+ experience an error relative to the number of total HTTP
+ connections made.
+
+ - Additionally, labeled versions of many metric types are supported.
+ These support the `labels`_ parameter, allowing multiple instances
+ of the metric to be stored at a given set of labels. The labeled
+ metric types include:
+
+ `labeled_boolean`, `labeled_string`, `labeled_counter`.
+
+ type: string
+ enum:
+ - event
+ - boolean
+ - string
+ - string_list
+ - counter
+ - quantity
+ - timespan
+ - timing_distribution
+ - custom_distribution
+ - memory_distribution
+ - datetime
+ - uuid
+ - jwe
+ - labeled_boolean
+ - labeled_string
+ - labeled_counter
+
+ description:
+ title: Description
+ description: |
+ **Required.**
+
+ A textual description of what this metric does, what it means, and its
+ edge cases or any other helpful information.
+
+ Descriptions may contain [markdown
+ syntax](https://www.markdownguide.org/basic-syntax/).
+ type: string
+
+ lifetime:
+ title: Lifetime
+ description: |
+ Defines the lifetime of the metric. It must be one of the following
+ values:
+
+ - `ping` (default): The metric is reset each time it is sent in a
+ ping.
+
+ - `user`: The metric contains a property that is part of the user's
+ profile and is never reset.
+
+ - `application`: The metric contains a property that is related to the
+ application, and is reset only at application restarts.
+ enum:
+ - ping
+ - user
+ - application
+ default: ping
+
+ send_in_pings:
+ title: Send in pings
+ description: |
+ Which pings the metric should be sent on. If not specified, the metric
+ is sent on the "default ping", which is the `events` ping for events,
+ and the `metrics` ping for everything else. Most metrics don't need to
+ specify this.
+
+ (There is an additional special value of `all-pings` for internal
+ Glean metrics only that is used to indicate that a metric may appear
+ in any ping.)
+ type: array
+ items:
+ $ref: "#/definitions/kebab_case"
+ default:
+ - default
+
+ notification_emails:
+ title: Notification emails
+ description: |
+ **Required.**
+
+ A list of email addresses to notify for important events with the
+ metric or when people with context or ownership for the metric need to
+ be contacted.
+ type: array
+ minItems: 1
+ items:
+ type: string
+ format: email
+
+ bugs:
+ title: Related bugs
+ description: |
+ **Required.**
+
+ A list of bug URLs (e.g. Bugzilla and Github) that are relevant to
+ this metric, e.g., tracking its original implementation or later
+ changes to it.
+
+ Using bug numbers alone is deprecated and will be an error in the
+ future. Each entry should be a full URL to the bug in its tracker.
+ type: array
+ minItems: 1
+ items:
+ anyOf:
+ - type: integer # Keep supporting integer for backward-compat
+ - type: string
+ format: uri
+
+ data_reviews:
+ title: Review references
+ description: |
+ **Required.**
+
+ A list of URIs to any data collection reviews relevant to the metric.
+ type: array
+ items:
+ type: string
+ format: uri
+
+ disabled:
+ title: Disabled
+ description: |
+ If `true`, the metric is disabled, and any metric collection on it
+ will be silently ignored at runtime.
+ type: boolean
+ default: false
+
+ expires:
+ title: Expires
+ description: |
+ **Required.**
+
+ By default it may be one of the following values:
+ - `<build date>`: An ISO date `yyyy-mm-dd` in UTC on which the
+ metric expires. For example, `2019-03-13`. This date is checked at
+ build time. Except in special cases, this form should be used so
+ that the metric automatically "sunsets" after a period of time.
+ - `never`: This metric never expires.
+ - `expired`: This metric is manually expired.
+
+ The default may be overriden in certain applications by the
+ `custom_validate_expires` and `custom_is_expired` configs.
+ type: string
+
+ version:
+ title: Metric version
+ description: |
+ The version of the metric. A monotonically increasing value. If not
+ provided, defaults to 0.
+
+ time_unit:
+ title: Time unit
+ description: |
+ For timespans and datetimes, specifies the unit that the metric will
+ be stored and displayed in. If not provided, it defaults to
+ "millisecond". Time values are sent to the backend as integers, so
+ `time_unit`_ determines the maximum resolution at which timespans are
+ recorded. Times are always truncated, not rounded, to the nearest time
+ unit. For example, a measurement of 25 ns will be returned as 0 ms if
+ `time_unit` is `"millisecond"`.
+
+ For timing distributions, times are always recorded and sent in
+ nanoseconds, but `time_unit` controls the minimum and maximum values.
+ If not provided, it defaults to "nanosecond".
+
+ - nanosecond: 1ns <= x <= 10 minutes
+ - microsecond: 1μs <= x <= ~6.94 days
+ - millisecond: 1ms <= x <= ~19 years
+
+ Valid when `type`_ is `timespan`, `timing_distribution` or `datetime`.
+ enum:
+ - nanosecond
+ - microsecond
+ - millisecond
+ - second
+ - minute
+ - hour
+ - day
+
+ memory_unit:
+ title: Memory unit
+ description: |
+ The unit that the incoming memory size values are recorded in.
+
+ The units are the power-of-2 units, so "kilobyte" is correctly a
+ "kibibyte".
+
+ - kilobyte == 2^10 == 1,024 bytes
+ - megabyte == 2^20 == 1,048,576 bytes
+ - gigabyte == 2^30 == 1,073,741,824 bytes
+
+ Values are automatically converted to and transmitted as bytes.
+
+ Valid when `type`_ is `memory_distribution`.
+ enum:
+ - byte
+ - kilobyte
+ - megabyte
+ - gigabyte
+
+ labels:
+ title: Labels
+ description: |
+ A list of labels for a labeled metric. If provided, the labels are
+ enforced at run time, and recording to an unknown label is recorded
+ to the special label `__other__`. If not provided, the labels
+ may be anything, but using too many unique labels will put some
+ labels in the special label `__other__`.
+
+ Valid with any of the labeled metric types.
+ anyOf:
+ - type: array
+ uniqueItems: true
+ items:
+ $ref: "#/definitions/labeled_metric_id"
+ maxItems: 16
+ - type: "null"
+
+ extra_keys:
+ title: Extra keys
+ description: |
+ The acceptable keys on the "extra" object sent with events. This is an
+ object mapping the key to an object containing metadata about the key.
+ A maximum of 10 extra keys is allowed.
+ This metadata object has the following keys:
+
+ - `description`: **Required.** A description of the key.
+
+ Valid when `type`_ is `event`.
+ type: object
+ propertyNames:
+ $ref: "#/definitions/dotted_snake_case"
+ additionalProperties:
+ type: object
+ properties:
+ description:
+ type: string
+ required:
+ - description
+ maxProperties: 10
+ default: {}
+
+ gecko_datapoint:
+ title: Gecko Datapoint
+ description: |
+ This is a Gecko-specific property. It is the name of the Gecko metric
+ to accumulate the data from, when using the Glean SDK in a product
+ using GeckoView. See bug 1566356 for more context.
+
+ type: string
+
+ range_min:
+ title: Range minimum
+ description: |
+ The minimum value of a custom distribution.
+
+ Valid when `type`_ is `custom_distribution`.
+ type: number
+ default: 1
+
+ range_max:
+ title: Range maximum
+ description: |
+ The maximum value of a custom distribution.
+
+ Required when `type`_ is `custom_distribution`.
+ type: number
+
+ bucket_count:
+ title: Bucket count
+ description: |
+ The number of buckets to include in a custom distribution.
+
+ Required when `type`_ is `custom_distribution`.
+ type: number
+ minimum: 1
+ maximum: 100
+
+ histogram_type:
+ title: Histogram type
+ description: |
+ The type of histogram bucketing to use:
+ - `linear`: The buckets are linearly spaced within the range.
+ - `exponential`: The buckets use the natural logarithmic so the
+ smaller-valued buckets are smaller in size than the higher-valued
+ buckets.
+
+ Required when `type`_ is `custom_distribution`.
+ enum:
+ - linear
+ - exponential
+
+ unit:
+ title: Unit
+ description: |
+ The unit of the metric, for metrics that don't already require a
+ meaningful unit, such as `time_unit`.
+ This is provided for informational purposes only and doesn't have any
+ effect on data collection.
+ type: string
+
+ no_lint:
+ title: Lint checks to skip
+ description: |
+ This parameter lists any lint checks to skip for this metric only.
+ type: array
+ items:
+ type: string
+
+ decrypted_name:
+ title: Decrypted name
+ description: |
+ Name of the column where to persist the decrypted value
+ stored in the JWE after processing.
+
+ Required when `type`_ is `jwe`.
+ type: string
+ pattern: "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})*$"
+
+ data_sensitivity:
+ title: The level of data sensitivity
+ description: |
+ There are four data collection categories related to data sensitivity
+ [defined here](https://wiki.mozilla.org/Firefox/Data_Collection):
+
+ - **Category 1: Technical Data:** (`technical`) Information about the
+ machine or Firefox itself. Examples include OS, available memory,
+ crashes and errors, outcome of automated processes like updates,
+ safebrowsing, activation, version \#s, and buildid. This also
+ includes compatibility information about features and APIs used by
+ websites, addons, and other 3rd-party software that interact with
+ Firefox during usage.
+
+ - **Category 2: Interaction Data:** (`interaction`) Information about
+ the user’s direct engagement with Firefox. Examples include how many
+ tabs, addons, or windows a user has open; uses of specific Firefox
+ features; session length, scrolls and clicks; and the status of
+ discrete user preferences.
+
+ - **Category 3: Web activity data:** (`web_activity`) Information
+ about user web browsing that could be considered sensitive. Examples
+ include users’ specific web browsing history; general information
+ about their web browsing history (such as TLDs or categories of
+ webpages visited over time); and potentially certain types of
+ interaction data about specific webpages visited.
+
+ - **Category 4: Highly sensitive data:** (`highly_sensitive`)
+ Information that directly identifies a person, or if combined with
+ other data could identify a person. Examples include e-mail,
+ usernames, identifiers such as google ad id, apple id, fxaccount,
+ city or country (unless small ones are explicitly filtered out), or
+ certain cookies. It may be embedded within specific website content,
+ such as memory contents, dumps, captures of screen data, or DOM
+ data.
+ type: array
+ items:
+ enum:
+ - technical
+ - interaction
+ - web_activity
+ - highly_sensitive
+ type: string
+ minLength: 1
+ uniqueItems: true
+
+ required:
+ - type
+ - bugs
+ - description
+ - notification_emails
+ - data_reviews
+ - expires
+
+type: object
+
+propertyNames:
+ anyOf:
+ - allOf:
+ - $ref: "#/definitions/dotted_snake_case"
+ - not:
+ description: "'pings' is reserved as a category name."
+ const: pings
+ - enum: ['$schema']
+
+properties:
+ $schema:
+ type: string
+ format: url
+
+ no_lint:
+ title: Lint checks to skip globally
+ description: |
+ This parameter lists any lint checks to skip for this whole file.
+ type: array
+ items:
+ type: string
+
+additionalProperties:
+ type: object
+ propertyNames:
+ anyOf:
+ - $ref: "#/definitions/short_id"
+ additionalProperties:
+ allOf:
+ - $ref: "#/definitions/metric"
+ -
+ if:
+ properties:
+ type:
+ const: event
+ then:
+ properties:
+ lifetime:
+ description: |
+ Event metrics must have ping lifetime.
+ const: ping
+ - if:
+ not:
+ properties:
+ type:
+ enum:
+ - timing_distribution
+ - custom_distribution
+ - memory_distribution
+ - quantity
+ - boolean
+ - string
+ - labeled_counter
+ then:
+ properties:
+ gecko_datapoint:
+ description: |
+ `gecko_datapoint` is only allowed for `timing_distribution`,
+ `custom_distribution`, `memory_distribution`, `quantity`,
+ `boolean`, `string` and `labeled_counter`.
+ maxLength: 0
+ -
+ if:
+ properties:
+ type:
+ enum:
+ - custom_distribution
+ then:
+ required:
+ - gecko_datapoint
+ description: |
+ `custom_distribution` is only allowed for Gecko
+ metrics.
+ -
+ if:
+ properties:
+ type:
+ const: custom_distribution
+ then:
+ required:
+ - range_max
+ - bucket_count
+ - histogram_type
+ description: |
+ `custom_distribution` is missing required parameters `range_max`,
+ `bucket_count` and `histogram_type`.
+ -
+ if:
+ properties:
+ type:
+ const: memory_distribution
+ then:
+ required:
+ - memory_unit
+ description: |
+ `memory_distribution` is missing required parameter `memory_unit`.
+ -
+ if:
+ properties:
+ type:
+ const: quantity
+ then:
+ required:
+ - unit
+ description: |
+ `quantity` is missing required parameter `unit`.
+ -
+ if:
+ properties:
+ type:
+ const: jwe
+ then:
+ required:
+ - decrypted_name
+ description: |
+ `jwe` is missing required parameter `decrypted_name`.
diff --git a/third_party/python/glean_parser/glean_parser/schemas/metrics.2-0-0.schema.yaml b/third_party/python/glean_parser/glean_parser/schemas/metrics.2-0-0.schema.yaml
new file mode 100644
index 0000000000..0bc8d500c6
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/schemas/metrics.2-0-0.schema.yaml
@@ -0,0 +1,770 @@
+# 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/.
+
+---
+$schema: http://json-schema.org/draft-07/schema#
+title: Metrics
+description: |
+ Schema for the metrics.yaml files for Mozilla's Glean telemetry SDK.
+
+ The top-level of the `metrics.yaml` file has a key defining each category of
+ metrics. Categories must be snake_case, and they may also have dots `.` to
+ define subcategories.
+
+$id: moz://mozilla.org/schemas/glean/metrics/2-0-0
+
+definitions:
+ token:
+ type: string
+ pattern: "^[A-Za-z_][A-Za-z0-9_\\.]*$"
+
+ snake_case:
+ type: string
+ pattern: "^[a-z_][a-z0-9_]*$"
+
+ dotted_snake_case:
+ type: string
+ pattern: "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})*$"
+ maxLength: 40
+
+ # Prior to version 2.0.0 of the schema, special ping names with underscores
+ # were also supported.
+ kebab_case:
+ type: string
+ pattern: "^[a-z][a-z0-9-]{0,29}$"
+
+ long_id:
+ allOf:
+ - $ref: "#/definitions/snake_case"
+ - maxLength: 40
+
+ short_id:
+ allOf:
+ - $ref: "#/definitions/snake_case"
+ - maxLength: 70
+
+ labeled_metric_id:
+ type: string
+ pattern: "^[ -~]+$"
+ maxLength: 71 # Note: this should be category + metric + 1
+
+ metric:
+ description: |
+ Describes a single metric.
+
+ See https://mozilla.github.io/glean_parser/metrics-yaml.html
+
+ type: object
+
+ additionalProperties: false
+
+ properties:
+ type:
+ title: Metric type
+ description: |
+ **Required.**
+
+ Specifies the type of a metric, like "counter" or "event". This
+ defines which operations are valid for the metric, how it is stored
+ and how data analysis tooling displays it.
+
+ The supported types are:
+ - `event`: Record a specific event (with optional metadata).
+ Additional properties: `extra_keys`.
+
+ - `boolean`: A metric storing values of true or false.
+
+ - `string`: A metric storing Unicode string values.
+
+ - `string_list`: a list of Unicode strings.
+
+ - `counter`: A numeric value that can only be incremented.
+
+ - `quantity`: A numeric value that is set directly.
+
+ - `timespan`: Represents a time interval. Additional properties:
+ `time_unit`.
+
+ - `timing_distribution`: Record the distribution of multiple
+ timings. Additional properties: `time_unit`.
+
+ - `datetime`: A date/time value. Represented as an ISO datetime in
+ UTC. Additional properties: `time_unit`.
+
+ - `uuid`: Record a UUID v4.
+
+ - `url`: Record a valid URL string.
+
+ - `memory_distribution`: A histogram for recording memory usage
+ values. Additional properties: `memory_unit`.
+
+ - `custom_distribution`: A histogram with a custom range and number
+ of buckets. This metric type is for legacy support only and is
+ only allowed for metrics coming from GeckoView. Additional
+ properties: `range_min`, `range_max`, `bucket_count`,
+ `histogram_type`.
+
+ - `rate`: Used to record the rate something happens relative to some
+ other thing. For example, the number of HTTP connections that
+ experience an error relative to the number of total HTTP
+ connections made.
+
+ - Additionally, labeled versions of many metric types are supported.
+ These support the `labels`_ parameter, allowing multiple instances
+ of the metric to be stored at a given set of labels. The labeled
+ metric types include:
+
+ `labeled_boolean`, `labeled_string`, `labeled_counter`.
+
+ - `text`: Record long text data.
+
+ type: string
+ enum:
+ - event
+ - boolean
+ - string
+ - string_list
+ - counter
+ - quantity
+ - timespan
+ - timing_distribution
+ - custom_distribution
+ - memory_distribution
+ - datetime
+ - uuid
+ - url
+ - jwe
+ - labeled_boolean
+ - labeled_string
+ - labeled_counter
+ - rate
+ - text
+
+ description:
+ title: Description
+ description: |
+ **Required.**
+
+ A textual description of what this metric does, what it means, and its
+ edge cases or any other helpful information.
+
+ Descriptions may contain [markdown
+ syntax](https://www.markdownguide.org/basic-syntax/).
+ type: string
+
+ metadata:
+ title: Metadata
+ description: |
+ Additional metadata about this metric. Currently limited to a list of
+ tags.
+ type: object
+ properties:
+ tags:
+ title: Tags
+ description: Which tags are specified for this metric.
+ type: array
+ items:
+ type: string
+ maxLength: 80
+ default: {}
+
+ lifetime:
+ title: Lifetime
+ description: |
+ Defines the lifetime of the metric. It must be one of the following
+ values:
+
+ - `ping` (default): The metric is reset each time it is sent in a
+ ping.
+
+ - `user`: The metric contains a property that is part of the user's
+ profile and is never reset.
+
+ - `application`: The metric contains a property that is related to the
+ application, and is reset only at application restarts.
+ enum:
+ - ping
+ - user
+ - application
+ default: ping
+
+ send_in_pings:
+ title: Send in pings
+ description: |
+ Which pings the metric should be sent on. If not specified, the metric
+ is sent on the "default ping", which is the `events` ping for events,
+ and the `metrics` ping for everything else. Most metrics don't need to
+ specify this.
+
+ (There is an additional special value of `all-pings` for internal
+ Glean metrics only that is used to indicate that a metric may appear
+ in any ping.)
+ type: array
+ items:
+ anyOf:
+ - $ref: "#/definitions/kebab_case"
+ # Allow "special" ping names that start with "glean_" used
+ # internally by the Glean SDK
+ - type: string
+ pattern: "^glean_.*$"
+ default:
+ - default
+
+ notification_emails:
+ title: Notification emails
+ description: |
+ **Required.**
+
+ A list of email addresses to notify for important events with the
+ metric or when people with context or ownership for the metric need to
+ be contacted.
+ type: array
+ minItems: 1
+ items:
+ type: string
+ format: email
+
+ bugs:
+ title: Related bugs
+ description: |
+ **Required.**
+
+ A list of bug URLs (e.g. Bugzilla and Github) that are relevant to
+ this metric, e.g., tracking its original implementation or later
+ changes to it.
+
+ Prior to version 2.0.0 of the schema, bugs could also be integers.
+ type: array
+ minItems: 1
+ items:
+ type: string
+ format: uri
+
+ data_reviews:
+ title: Review references
+ description: |
+ **Required.**
+
+ A list of URIs to any data collection reviews relevant to the metric.
+ type: array
+ items:
+ type: string
+ format: uri
+
+ disabled:
+ title: Disabled
+ description: |
+ If `true`, the metric is disabled, and any metric collection on it
+ will be silently ignored at runtime.
+ type: boolean
+ default: false
+
+ expires:
+ title: Expires
+ description: |
+ **Required.**
+
+ By default it may be one of the following values:
+ - `<build date>`: An ISO date `yyyy-mm-dd` in UTC on which the
+ metric expires. For example, `2019-03-13`. This date is checked at
+ build time. Except in special cases, this form should be used so
+ that the metric automatically "sunsets" after a period of time.
+ - `<major version>`: An integer greater than 0 representing the
+ major version the metric expires in. For example, `11`. The
+ version is checked at build time against the major provided to the
+ glean_parser and is only valid if a major version is provided at
+ built time. If no major version is provided at build time and
+ expiration by major version is used for a metric, an error is
+ raised.
+ Note that mixing expiration by date and version is not allowed
+ within a product.
+ - `never`: This metric never expires.
+ - `expired`: This metric is manually expired.
+
+ The default may be overriden in certain applications by the
+ `custom_validate_expires` and `custom_is_expired` configs.
+ oneOf:
+ - type: string
+ - type: integer
+ minimum: 1
+
+ version:
+ title: Metric version
+ description: |
+ The version of the metric. A monotonically increasing value. If not
+ provided, defaults to 0.
+
+ time_unit:
+ title: Time unit
+ description: |
+ For timespans and datetimes, specifies the unit that the metric will
+ be stored and displayed in. If not provided, it defaults to
+ "millisecond". Time values are sent to the backend as integers, so
+ `time_unit`_ determines the maximum resolution at which timespans are
+ recorded. Times are always truncated, not rounded, to the nearest time
+ unit. For example, a measurement of 25 ns will be returned as 0 ms if
+ `time_unit` is `"millisecond"`.
+
+ For timing distributions, times are always recorded and sent in
+ nanoseconds, but `time_unit` controls the minimum and maximum values.
+ If not provided, it defaults to "nanosecond".
+
+ - nanosecond: 1ns <= x <= 10 minutes
+ - microsecond: 1μs <= x <= ~6.94 days
+ - millisecond: 1ms <= x <= ~19 years
+
+ Valid when `type`_ is `timespan`, `timing_distribution` or `datetime`.
+ enum:
+ - nanosecond
+ - microsecond
+ - millisecond
+ - second
+ - minute
+ - hour
+ - day
+
+ memory_unit:
+ title: Memory unit
+ description: |
+ The unit that the incoming memory size values are recorded in.
+
+ The units are the power-of-2 units, so "kilobyte" is correctly a
+ "kibibyte".
+
+ - kilobyte == 2^10 == 1,024 bytes
+ - megabyte == 2^20 == 1,048,576 bytes
+ - gigabyte == 2^30 == 1,073,741,824 bytes
+
+ Values are automatically converted to and transmitted as bytes.
+
+ Valid when `type`_ is `memory_distribution`.
+ enum:
+ - byte
+ - kilobyte
+ - megabyte
+ - gigabyte
+
+ labels:
+ title: Labels
+ description: |
+ A list of labels for a labeled metric. If provided, the labels are
+ enforced at run time, and recording to an unknown label is recorded
+ to the special label `__other__`. If not provided, the labels
+ may be anything, but using too many unique labels will put some
+ labels in the special label `__other__`.
+
+ Valid with any of the labeled metric types.
+ anyOf:
+ - type: array
+ uniqueItems: true
+ items:
+ $ref: "#/definitions/labeled_metric_id"
+ maxItems: 4096
+ - type: "null"
+
+ extra_keys:
+ title: Extra keys
+ description: |
+ The acceptable keys on the "extra" object sent with events. This is an
+ object mapping the key to an object containing metadata about the key.
+ A maximum of 50 extra keys is allowed.
+ This metadata object has the following keys:
+
+ - `description`: **Required.** A description of the key.
+
+ Valid when `type`_ is `event`.
+ type: object
+ propertyNames:
+ $ref: "#/definitions/dotted_snake_case"
+ additionalProperties:
+ type: object
+ properties:
+ description:
+ type: string
+ type:
+ type: string
+ enum:
+ - string
+ - boolean
+ - quantity
+ required:
+ - description
+ maxProperties: 50
+ default: {}
+
+ gecko_datapoint:
+ title: Gecko Datapoint
+ description: |
+ This is a Gecko-specific property. It is the name of the Gecko metric
+ to accumulate the data from, when using the Glean SDK in a product
+ using GeckoView. See bug 1566356 for more context.
+
+ type: string
+
+ range_min:
+ title: Range minimum
+ description: |
+ The minimum value of a custom distribution.
+
+ Valid when `type`_ is `custom_distribution`.
+ type: number
+ default: 1
+
+ range_max:
+ title: Range maximum
+ description: |
+ The maximum value of a custom distribution.
+
+ Required when `type`_ is `custom_distribution`.
+ type: number
+
+ bucket_count:
+ title: Bucket count
+ description: |
+ The number of buckets to include in a custom distribution.
+
+ Required when `type`_ is `custom_distribution`.
+ type: number
+ minimum: 1
+ maximum: 100
+
+ histogram_type:
+ title: Histogram type
+ description: |
+ The type of histogram bucketing to use:
+ - `linear`: The buckets are linearly spaced within the range.
+ - `exponential`: The buckets use the natural logarithmic so the
+ smaller-valued buckets are smaller in size than the higher-valued
+ buckets.
+
+ Required when `type`_ is `custom_distribution`.
+ enum:
+ - linear
+ - exponential
+
+ unit:
+ title: Unit
+ description: |
+ The unit of the metric.
+ This is only required for metrics
+ that don't already require a meaningful unit, e.g. `quantity`
+ This is provided for informational purposes only and doesn't have any
+ effect on data collection.
+
+ Metric types like `timespan`, `datetime`
+ and `timing_distribution` take a `time_unit` instead.
+ type: string
+
+ no_lint:
+ title: Lint checks to skip
+ description: |
+ This parameter lists any lint checks to skip for this metric only.
+ type: array
+ items:
+ type: string
+
+ data_sensitivity:
+ title: The level of data sensitivity
+ description: |
+ There are four data collection categories related to data sensitivity
+ [defined here](https://wiki.mozilla.org/Firefox/Data_Collection):
+
+ - **Category 1: Technical Data:** (`technical`) Information about the
+ machine or Firefox itself. Examples include OS, available memory,
+ crashes and errors, outcome of automated processes like updates,
+ safebrowsing, activation, version \#s, and buildid. This also
+ includes compatibility information about features and APIs used by
+ websites, addons, and other 3rd-party software that interact with
+ Firefox during usage.
+
+ - **Category 2: Interaction Data:** (`interaction`) Information about
+ the user’s direct engagement with Firefox. Examples include how many
+ tabs, addons, or windows a user has open; uses of specific Firefox
+ features; session length, scrolls and clicks; and the status of
+ discrete user preferences.
+ It also includes information about the user's in-product journeys
+ and product choices helpful to understand engagement (attitudes).
+ For example, selections of add-ons or tiles to determine
+ potential interest categories etc.
+
+ - **Category 3: Stored Content & Communications:**
+ (`stored_content`, formerly Web activity data, `web_activity`)
+ Information about what people store, sync, communicate or connect to
+ where the information is generally considered to be more sensitive
+ and personal in nature.
+ Examples include users' saved URLs or URL history,
+ specific web browsing history, general information
+ about their web browsing history
+ (such as TLDs or categories of webpages visited over time)
+ and potentially certain types of interaction data
+ about specific web pages or stories visited
+ (such as highlighted portions of a story).
+ It also includes information such as content saved by users to
+ an individual account like saved URLs, tags, notes, passwords
+ and files as well as communications that users have with one another
+ through a Mozilla service.
+
+ - **Category 4: Highly sensitive data
+ or clearly identifiable personal data:** (`highly_sensitive`)
+
+ Information that directly identifies a person, or if combined with
+ other data could identify a person.
+ This data may be embedded within specific website content,
+ such as memory contents, dumps, captures of screen data,
+ or DOM data.
+ Examples include account registration data like name, password,
+ and email address associated with an account,
+ payment data in connection with subscriptions or donations,
+ contact information such as phone numbers or mailing addresses,
+ email addresses associated with surveys, promotions
+ and customer support contacts.
+ It also includes any data from different categories that,
+ when combined, can identify a person, device, household or account.
+ For example Category 1 log data combined with Category 3 saved URLs.
+ Additional examples are: voice audio commands
+ (including a voice audio file), speech-to-text or text-to-speech
+ (including transcripts), biometric data, demographic information,
+ and precise location data associated with a persistent identifier,
+ individual or small population cohorts.
+ This is location inferred or determined from mechanisms
+ other than IP such as wi-fi access points, Bluetooth beacons,
+ cell phone towers or provided directly to us,
+ such as in a survey or a profile.
+ type: array
+ items:
+ enum:
+ - technical
+ - interaction
+ - stored_content
+ - web_activity
+ - highly_sensitive
+ type: string
+ minLength: 1
+ uniqueItems: true
+
+ telemetry_mirror:
+ title: Which probe in Telemetry to mirror this metric's value to.
+ description: |
+ The C++ enum form of the Scalar, Event, or Histogram to which we
+ should mirror values.
+ Use is limited to Firefox Desktop only.
+ Has no effect when used with non-FOG outputters.
+ See FOG's documentation on mirroring for details -
+ https://firefox-source-docs.mozilla.org/toolkit/components/glean/mirroring.html
+ type: string
+ minLength: 6
+
+ denominator_metric:
+ title: The name of the denominator for this `rate` metric.
+ description: |
+ Denominators for `rate` metrics may be private and internal
+ or shared and external.
+ External denominators are `counter` metrics.
+ This field names the `counter` metric that serves as this
+ `rate` metric's external denominator.
+ The named denominator must be defined in this component
+ so glean_parser can find it.
+ type: string
+
+ required:
+ - type
+ - bugs
+ - description
+ - notification_emails
+ - data_reviews
+ - expires
+
+type: object
+
+propertyNames:
+ anyOf:
+ - allOf:
+ - $ref: "#/definitions/dotted_snake_case"
+ - not:
+ description: "'pings' is reserved as a category name."
+ const: pings
+ - not:
+ description: "'tags' is reserved as a category name."
+ const: tags
+ - enum: ['$schema', '$tags']
+
+properties:
+ $schema:
+ type: string
+ format: url
+
+ no_lint:
+ title: Lint checks to skip globally
+ description: |
+ This parameter lists any lint checks to skip for this whole file.
+ type: array
+ items:
+ type: string
+
+ $tags:
+ title: Tags that apply to the whole file
+ description: |
+ This denotes the list of tags that apply to all metrics in this file.
+ type: array
+ items:
+ type: string
+
+additionalProperties:
+ type: object
+ propertyNames:
+ anyOf:
+ - $ref: "#/definitions/short_id"
+ additionalProperties:
+ allOf:
+ - $ref: "#/definitions/metric"
+ -
+ if:
+ properties:
+ type:
+ const: event
+ then:
+ properties:
+ lifetime:
+ description: |
+ Event metrics must have ping lifetime.
+ const: ping
+ - if:
+ not:
+ properties:
+ type:
+ enum:
+ - timing_distribution
+ - custom_distribution
+ - memory_distribution
+ - quantity
+ - boolean
+ - string
+ - labeled_counter
+ then:
+ properties:
+ gecko_datapoint:
+ description: |
+ `gecko_datapoint` is only allowed for `timing_distribution`,
+ `custom_distribution`, `memory_distribution`, `quantity`,
+ `boolean`, `string` and `labeled_counter`.
+ maxLength: 0
+ -
+ if:
+ properties:
+ type:
+ const: custom_distribution
+ then:
+ required:
+ - range_max
+ - bucket_count
+ - histogram_type
+ description: |
+ `custom_distribution` is missing required parameters `range_max`,
+ `bucket_count` and `histogram_type`.
+ -
+ if:
+ properties:
+ type:
+ const: memory_distribution
+ then:
+ required:
+ - memory_unit
+ description: |
+ `memory_distribution` is missing required parameter `memory_unit`.
+ -
+ if:
+ properties:
+ type:
+ const: quantity
+ then:
+ required:
+ - unit
+ description: |
+ `quantity` is missing required parameter `unit`.
+ -
+ if:
+ properties:
+ type:
+ const: jwe
+ then:
+ required:
+ - jwe_support_was_removed
+ description: |
+ JWE support was removed.
+ If you require this send an email to glean-team@mozilla.com.
+ - if:
+ not:
+ properties:
+ type:
+ const: rate
+ then:
+ properties:
+ denominator_metric:
+ description: |
+ `denominator_metric` is only allowed for `rate`.
+ maxLength: 0
+ -
+ if:
+ properties:
+ type:
+ const: text
+ then:
+ properties:
+ lifetime:
+ description: >
+ Text metrics must have ping or application lifetime.
+ enum:
+ - ping
+ - application
+
+ data_sensitivity:
+ description: >
+ Text metrics require Category 3
+ (`stored_content` / `web_activity`)
+ or Category 4 (`highly_sensitive`).
+ type: array
+ items:
+ enum:
+ - stored_content
+ - web_activity
+ - highly_sensitive
+
+ send_in_pings:
+ description: |
+ Text metrics can only be sent in custom pings.
+ Built-in pings are not allowed.
+ type: array
+ items:
+ allOf:
+ - $ref: "#/definitions/kebab_case"
+ - not:
+ description: >
+ Text metrics can only be sent in custom pings.
+ Built-in pings are not allowed."
+ pattern:
+ "^(metrics|baseline|events|deletion-request|default|glean_.*)$"
+
+ -
+ if:
+ # This is a schema check:
+ # This is true when the checked YAML passes the schema validation.
+ #
+ # If it has a datetime/timing_distribution/timespan type
+ # AND has a `unit` property, then...
+ properties:
+ type:
+ enum:
+ - datetime
+ - timing_distribution
+ - timespan
+ required:
+ - unit
+ # ... then `time_unit` is required,
+ # because that's the only way we can force this to fail.
+ then:
+ required:
+ - time_unit
+ description: |
+ This metric type uses the (optional) `time_unit` parameter,
+ not `unit`.
diff --git a/third_party/python/glean_parser/glean_parser/schemas/pings.1-0-0.schema.yaml b/third_party/python/glean_parser/glean_parser/schemas/pings.1-0-0.schema.yaml
new file mode 100644
index 0000000000..c15a4c85ac
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/schemas/pings.1-0-0.schema.yaml
@@ -0,0 +1,157 @@
+# 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/.
+
+---
+$schema: http://json-schema.org/draft-07/schema#
+title: Pings
+description: |
+ Schema for the pings.yaml files for Mozilla's Glean telemetry SDK.
+
+ The top-level of the `pings.yaml` file has a key defining the name of each
+ ping. The values contain metadata about that ping. Ping names must be
+ kebab-case per https://docs.telemetry.mozilla.org/cookbooks/new_ping.html
+
+$id: moz://mozilla.org/schemas/glean/pings/1-0-0
+
+definitions:
+ dotted_snake_case:
+ type: string
+ pattern: "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})*$"
+ maxLength: 40
+ kebab_case:
+ type: string
+ # Bug 1601270; we allow 3 specific existing snake_cased ping names for now,
+ # but these special cases can be removed once the number of legacy clients
+ # sufficiently dwindles, likely in 2020H2.
+ pattern: "^[a-z][a-z0-9-]{0,29}$\
+ |^deletion_request$|^bookmarks_sync$|^history_sync$|^session_end$|^all_pings$|^glean_.*$"
+
+type: object
+
+propertyNames:
+ allOf:
+ - anyOf:
+ - $ref: "#/definitions/kebab_case"
+ - enum: ['$schema', 'no_lint']
+ - not:
+ enum: ['all-pings']
+
+properties:
+ $schema:
+ type: string
+ format: url
+
+ no_lint:
+ title: Lint checks to skip globally
+ description: |
+ This parameter lists any lint checks to skip for this whole file.
+ type: array
+ items:
+ type: string
+
+additionalProperties:
+ type: object
+ properties:
+ description:
+ title: Description
+ description: |
+ **Required.**
+
+ A textual description of the purpose of this ping and what it contains.
+
+ Descriptions may contain [markdown
+ syntax](https://www.markdownguide.org/basic-syntax/).
+ type: string
+
+ include_client_id:
+ title: Include client id
+ description: |
+ **Required.**
+
+ When `true`, include the `client_id` value in the ping.
+ type: boolean
+
+ send_if_empty:
+ title: Send if empty
+ description: |
+ When `false` a ping is sent only if it contains data (the default).
+ When `true` a ping is sent even if it contains no data.
+ type: boolean
+
+ notification_emails:
+ title: Notification emails
+ description: |
+ **Required.**
+
+ A list of email addresses to notify for important events with the
+ ping or when people with context or ownership for the ping need to
+ be contacted.
+ type: array
+ minItems: 1
+ items:
+ type: string
+ format: email
+
+ bugs:
+ title: Related bugs
+ description: |
+ **Required.**
+
+ A list of bugs (e.g. Bugzilla and Github) that are relevant to this
+ ping, e.g., tracking its original implementation or later changes to
+ it.
+
+ If a number, it is an ID to an issue in the default tracker (e.g.
+ Mozilla's Bugzilla instance). If a string, it must be a URI to a bug
+ page in a tracker.
+ type: array
+ minItems: 1
+ items:
+ anyOf:
+ - type: integer # Keep supporting integer for backward-compat
+ - type: string
+ format: uri
+
+ data_reviews:
+ title: Review references
+ description: |
+ **Required.**
+
+ A list of URIs to any data collection reviews relevant to the ping.
+ type: array
+ items:
+ type: string
+ format: uri
+
+ reasons:
+ title: The reasons this ping can be sent.
+ description: |
+ A list of reasons that the ping might be triggered. Sent in the ping's
+ `ping_info.reason` field.
+
+ Specified as a mapping from reason codes (which are short strings), to
+ a textual description of the reason.
+ type: object
+ propertyNames:
+ type: string
+ maxLength: 30
+ additionalProperties:
+ type: string
+
+ no_lint:
+ title: Lint checks to skip
+ description: |
+ This parameter lists any lint checks to skip for this metric only.
+ type: array
+ items:
+ type: string
+
+ required:
+ - description
+ - include_client_id
+ - bugs
+ - notification_emails
+ - data_reviews
+
+ additionalProperties: false
diff --git a/third_party/python/glean_parser/glean_parser/schemas/pings.2-0-0.schema.yaml b/third_party/python/glean_parser/glean_parser/schemas/pings.2-0-0.schema.yaml
new file mode 100644
index 0000000000..2f25405d45
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/schemas/pings.2-0-0.schema.yaml
@@ -0,0 +1,178 @@
+# 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/.
+
+---
+$schema: http://json-schema.org/draft-07/schema#
+title: Pings
+description: |
+ Schema for the pings.yaml files for Mozilla's Glean telemetry SDK.
+
+ The top-level of the `pings.yaml` file has a key defining the name of each
+ ping. The values contain metadata about that ping. Ping names must be
+ kebab-case per https://docs.telemetry.mozilla.org/cookbooks/new_ping.html
+
+$id: moz://mozilla.org/schemas/glean/pings/2-0-0
+
+definitions:
+ dotted_snake_case:
+ type: string
+ pattern: "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})*$"
+ maxLength: 40
+ # Prior to version 2.0.0 of the schema, special ping names with underscores
+ # were also supported.
+ kebab_case:
+ type: string
+ pattern: "^[a-z][a-z0-9-]{0,29}$"
+
+type: object
+
+propertyNames:
+ allOf:
+ - anyOf:
+ - $ref: "#/definitions/kebab_case"
+ - enum: ['$schema', 'no_lint']
+ - not:
+ enum: ['all-pings']
+
+properties:
+ $schema:
+ type: string
+ format: url
+
+ no_lint:
+ title: Lint checks to skip globally
+ description: |
+ This parameter lists any lint checks to skip for this whole file.
+ type: array
+ items:
+ type: string
+
+additionalProperties:
+ type: object
+ properties:
+ description:
+ title: Description
+ description: |
+ **Required.**
+
+ A textual description of the purpose of this ping and what it contains.
+
+ Descriptions may contain [markdown
+ syntax](https://www.markdownguide.org/basic-syntax/).
+ type: string
+
+ metadata:
+ title: Metadata
+ description: |
+ Additional metadata about this ping. Currently limited to a list of
+ tags.
+ type: object
+ properties:
+ tags:
+ title: Tags
+ description: Which tags are specified for this ping.
+ type: array
+ items:
+ type: string
+ maxLength: 80
+ precise_timestamps:
+ title: Precise Timestamps
+ description: |
+ When `true` Glean uses millisecond-precise timestamps for
+ the ping's start/end time (the default).
+ When `false` Glean uses minute-precise timestamps for
+ the ping's start/end time.
+ type: boolean
+
+ default: {}
+
+ include_client_id:
+ title: Include client id
+ description: |
+ **Required.**
+
+ When `true`, include the `client_id` value in the ping.
+ type: boolean
+
+ send_if_empty:
+ title: Send if empty
+ description: |
+ When `false` a ping is sent only if it contains data (the default).
+ When `true` a ping is sent even if it contains no data.
+ type: boolean
+
+ notification_emails:
+ title: Notification emails
+ description: |
+ **Required.**
+
+ A list of email addresses to notify for important events with the
+ ping or when people with context or ownership for the ping need to
+ be contacted.
+ type: array
+ minItems: 1
+ items:
+ type: string
+ format: email
+
+ bugs:
+ title: Related bugs
+ description: |
+ **Required.**
+
+ A list of bugs (e.g. Bugzilla and Github) that are relevant to this
+ ping, e.g., tracking its original implementation or later changes to
+ it.
+
+ It must be a URI to a bug page in a tracker.
+
+ Prior to version 2.0.0 of the schema, bugs could also be integers.
+ type: array
+ minItems: 1
+ items:
+ type: string
+ format: uri
+
+ data_reviews:
+ title: Review references
+ description: |
+ **Required.**
+
+ A list of URIs to any data collection reviews relevant to the ping.
+ type: array
+ items:
+ type: string
+ format: uri
+
+ reasons:
+ title: The reasons this ping can be sent.
+ description: |
+ A list of reasons that the ping might be triggered. Sent in the ping's
+ `ping_info.reason` field.
+
+ Specified as a mapping from reason codes (which are short strings), to
+ a textual description of the reason.
+ type: object
+ propertyNames:
+ type: string
+ maxLength: 30
+ additionalProperties:
+ type: string
+
+ no_lint:
+ title: Lint checks to skip
+ description: |
+ This parameter lists any lint checks to skip for this metric only.
+ type: array
+ items:
+ type: string
+
+ required:
+ - description
+ - include_client_id
+ - bugs
+ - notification_emails
+ - data_reviews
+
+ additionalProperties: false
diff --git a/third_party/python/glean_parser/glean_parser/schemas/tags.1-0-0.schema.yaml b/third_party/python/glean_parser/glean_parser/schemas/tags.1-0-0.schema.yaml
new file mode 100644
index 0000000000..aa0f083bcf
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/schemas/tags.1-0-0.schema.yaml
@@ -0,0 +1,51 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+---
+$schema: http://json-schema.org/draft-07/schema#
+title: Tags
+description: |
+ Schema for the tags.yaml files for Mozilla's Glean telemetry SDK.
+
+ The top-level of the `tags.yaml` file has a key defining the name of each
+ tag. The values contain metadata about that tag (currently just a
+ description).
+
+$id: moz://mozilla.org/schemas/glean/tags/1-0-0
+
+type: object
+
+propertyNames:
+ type: string
+ maxLength: 80
+
+properties:
+ $schema:
+ type: string
+ format: url
+
+ no_lint:
+ title: Lint checks to skip globally
+ description: |
+ This parameter lists any lint checks to skip for this whole file.
+ type: array
+ items:
+ type: string
+
+additionalProperties:
+ type: object
+ properties:
+ description:
+ title: Description
+ description: |
+ **Required.**
+
+ A textual description of this tag.
+
+ Descriptions may contain [markdown
+ syntax](https://www.markdownguide.org/basic-syntax/).
+ type: string
+ required:
+ - description
+ additionalProperties: false
diff --git a/third_party/python/glean_parser/glean_parser/swift.py b/third_party/python/glean_parser/glean_parser/swift.py
new file mode 100644
index 0000000000..c745c4d9ac
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/swift.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Outputter to generate Swift code for metrics.
+"""
+
+import enum
+import json
+from pathlib import Path
+from typing import Any, Dict, Optional, Union
+
+from . import __version__
+from . import metrics
+from . import pings
+from . import tags
+from . import util
+
+# An (imcomplete) list of reserved keywords in Swift.
+# These will be replaced in generated code by their escaped form.
+SWIFT_RESERVED_NAMES = ["internal", "typealias"]
+
+
+def swift_datatypes_filter(value: util.JSONType) -> str:
+ """
+ A Jinja2 filter that renders Swift literals.
+
+ Based on Python's JSONEncoder, but overrides:
+ - dicts to use `[key: value]`
+ - sets to use `[...]`
+ - enums to use the like-named Swift enum
+ - Rate objects to a CommonMetricData initializer
+ (for external Denominators' Numerators lists)
+ """
+
+ class SwiftEncoder(json.JSONEncoder):
+ def iterencode(self, value):
+ if isinstance(value, dict):
+ yield "["
+ first = True
+ for key, subvalue in value.items():
+ if not first:
+ yield ", "
+ yield from self.iterencode(key)
+ yield ": "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield "]"
+ elif isinstance(value, enum.Enum):
+ yield ("." + util.camelize(value.name))
+ elif isinstance(value, list):
+ yield "["
+ first = True
+ for subvalue in value:
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield "]"
+ elif isinstance(value, set):
+ yield "["
+ first = True
+ for subvalue in sorted(list(value)):
+ if not first:
+ yield ", "
+ yield from self.iterencode(subvalue)
+ first = False
+ yield "]"
+ elif value is None:
+ yield "nil"
+ elif isinstance(value, metrics.Rate):
+ yield "CommonMetricData("
+ first = True
+ for arg_name in util.common_metric_args:
+ if hasattr(value, arg_name):
+ if not first:
+ yield ", "
+ yield f"{util.camelize(arg_name)}: "
+ yield from self.iterencode(getattr(value, arg_name))
+ first = False
+ yield ")"
+ else:
+ yield from super().iterencode(value)
+
+ return "".join(SwiftEncoder().iterencode(value))
+
+
+def type_name(obj: Union[metrics.Metric, pings.Ping]) -> str:
+ """
+ Returns the Swift type to use for a given metric or ping object.
+ """
+ generate_enums = getattr(obj, "_generate_enums", [])
+ if len(generate_enums):
+ generic = None
+ for member, suffix in generate_enums:
+ if len(getattr(obj, member)):
+ generic = util.Camelize(obj.name) + suffix
+ else:
+ if isinstance(obj, metrics.Event):
+ generic = "NoExtras"
+ else:
+ generic = "No" + suffix
+
+ return "{}<{}>".format(class_name(obj.type), generic)
+
+ return class_name(obj.type)
+
+
+def extra_type_name(typ: str) -> str:
+ """
+ Returns the corresponding Kotlin type for event's extra key types.
+ """
+
+ if typ == "boolean":
+ return "Bool"
+ elif typ == "string":
+ return "String"
+ elif typ == "quantity":
+ return "Int32"
+ else:
+ return "UNSUPPORTED"
+
+
+def class_name(obj_type: str) -> str:
+ """
+ Returns the Swift class name for a given metric or ping type.
+ """
+ if obj_type == "ping":
+ return "Ping"
+ if obj_type.startswith("labeled_"):
+ obj_type = obj_type[8:]
+ return util.Camelize(obj_type) + "MetricType"
+
+
+def variable_name(var: str) -> str:
+ """
+ Returns a valid Swift variable name, escaping keywords if necessary.
+ """
+ if var in SWIFT_RESERVED_NAMES:
+ return "`" + var + "`"
+ else:
+ return var
+
+
+class BuildInfo:
+ def __init__(self, build_date):
+ self.build_date = build_date
+
+
+def generate_build_date(date: Optional[str]) -> str:
+ """
+ Generate the build timestamp.
+ """
+
+ ts = util.build_date(date)
+
+ data = [
+ ("year", ts.year),
+ ("month", ts.month),
+ ("day", ts.day),
+ ("hour", ts.hour),
+ ("minute", ts.minute),
+ ("second", ts.second),
+ ]
+
+ # The internal DatetimeMetricType API can take a `DateComponents` object,
+ # which lets us easily specify the timezone.
+ components = ", ".join([f"{name}: {val}" for (name, val) in data])
+ return f'DateComponents(calendar: Calendar.current, timeZone: TimeZone(abbreviation: "UTC"), {components})' # noqa
+
+
+class Category:
+ """
+ Data struct holding information about a metric to be used in the template.
+ """
+
+ name: str
+ objs: Dict[str, Union[metrics.Metric, pings.Ping, tags.Tag]]
+ contains_pings: bool
+
+
+def output_swift(
+ objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
+) -> None:
+ """
+ Given a tree of objects, output Swift code to `output_dir`.
+
+ :param objects: A tree of objects (metrics and pings) as returned from
+ `parser.parse_objects`.
+ :param output_dir: Path to an output directory to write to.
+ :param options: options dictionary, with the following optional keys:
+ - namespace: The namespace to generate metrics in
+ - glean_namespace: The namespace to import Glean from
+ - allow_reserved: When True, this is a Glean-internal build
+ - with_buildinfo: If "true" the `GleanBuildInfo` is generated.
+ Otherwise generation of that file is skipped.
+ Defaults to "true".
+ - build_date: If set to `0` a static unix epoch time will be used.
+ If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
+ it will use that date.
+ Other values will throw an error.
+ If not set it will use the current date & time.
+ """
+ if options is None:
+ options = {}
+
+ template = util.get_jinja2_template(
+ "swift.jinja2",
+ filters=(
+ ("swift", swift_datatypes_filter),
+ ("type_name", type_name),
+ ("class_name", class_name),
+ ("variable_name", variable_name),
+ ("extra_type_name", extra_type_name),
+ ),
+ )
+
+ namespace = options.get("namespace", "GleanMetrics")
+ glean_namespace = options.get("glean_namespace", "Glean")
+ with_buildinfo = options.get("with_buildinfo", "true").lower() == "true"
+ build_date = options.get("build_date", None)
+ build_info = None
+ if with_buildinfo:
+ build_date = generate_build_date(build_date)
+ build_info = BuildInfo(build_date=build_date)
+
+ filename = "Metrics.swift"
+ filepath = output_dir / filename
+ categories = []
+
+ for category_key, category_val in objs.items():
+ contains_pings = any(
+ isinstance(obj, pings.Ping) for obj in category_val.values()
+ )
+
+ cat = Category()
+ cat.name = category_key
+ cat.objs = category_val
+ cat.contains_pings = contains_pings
+
+ categories.append(cat)
+
+ with filepath.open("w", encoding="utf-8") as fd:
+ fd.write(
+ template.render(
+ parser_version=__version__,
+ categories=categories,
+ common_metric_args=util.common_metric_args,
+ extra_metric_args=util.extra_metric_args,
+ namespace=namespace,
+ glean_namespace=glean_namespace,
+ allow_reserved=options.get("allow_reserved", False),
+ build_info=build_info,
+ )
+ )
+ # Jinja2 squashes the final newline, so we explicitly add it
+ fd.write("\n")
diff --git a/third_party/python/glean_parser/glean_parser/tags.py b/third_party/python/glean_parser/glean_parser/tags.py
new file mode 100644
index 0000000000..680b99731b
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/tags.py
@@ -0,0 +1,49 @@
+from typing import Dict, List, Optional
+from . import util
+
+
+class Tag:
+ def __init__(
+ self,
+ name: str,
+ description: str,
+ defined_in: Optional[Dict] = None,
+ no_lint: Optional[List[str]] = None,
+ _validated: bool = False,
+ ):
+ # Avoid cyclical import
+ from . import parser
+
+ self.name = name
+ self.description = description
+ self.defined_in = defined_in
+ if no_lint is None:
+ no_lint = []
+ self.no_lint = no_lint
+
+ # _validated indicates whether this tag has already been jsonschema
+ # validated (but not any of the Python-level validation).
+ if not _validated:
+ data: Dict[str, util.JSONType] = {
+ "$schema": parser.TAGS_ID,
+ self.name: self._serialize_input(),
+ }
+ for error in parser.validate(data):
+ raise ValueError(error)
+
+ @property
+ def type(self) -> str:
+ return "tag"
+
+ def _serialize_input(self) -> Dict[str, util.JSONType]:
+ d = self.serialize()
+ modified_dict = util.remove_output_params(d, "defined_in")
+ return modified_dict
+
+ def serialize(self) -> Dict[str, util.JSONType]:
+ """
+ Serialize the tag back to JSON object model.
+ """
+ d = self.__dict__.copy()
+ del d["name"]
+ return d
diff --git a/third_party/python/glean_parser/glean_parser/templates/data_review.jinja2 b/third_party/python/glean_parser/glean_parser/templates/data_review.jinja2
new file mode 100644
index 0000000000..b3541805ed
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/data_review.jinja2
@@ -0,0 +1,82 @@
+!! Reminder: it is your responsibility to complete and check the correctness of
+!! this automatically-generated request skeleton before requesting Data
+!! Collection Review. See https://wiki.mozilla.org/Data_Collection for details.
+{# Data Review Request Template pulled from
+ https://github.com/mozilla/data-review/blob/main/request.md #}
+
+DATA REVIEW REQUEST
+1. What questions will you answer with this data?
+
+{{ "TODO: Fill this in." if not questions }}
+
+2. Why does Mozilla need to answer these questions? Are there benefits for users?
+ Do we need this information to address product or business requirements?
+
+{{ "TODO: Fill this in." if not why }}
+
+3. What alternative methods did you consider to answer these questions?
+ Why were they not sufficient?
+
+{{ "TODO: Fill this in." if not methods }}
+
+4. Can current instrumentation answer these questions?
+
+{{ "TODO: Fill this in." if not current_instrumentation_answers }}
+
+5. List all proposed measurements and indicate the category of data collection for each
+ measurement, using the Firefox data collection categories found on the Mozilla wiki.
+
+Measurement Name | Measurement Description | Data Collection Category | Tracking Bug
+---------------- | ----------------------- | ------------------------ | ------------
+{% for metric in metrics %}
+{% if metric.type == "event" and metric.allowed_extra_keys %}
+{% for extra_name, extra_detail in metric.extra_keys.items() %}
+`{{ metric.category|snake_case }}.{{ metric.name|snake_case }}#{{ extra_name }} | {{ extra_detail["description"]|replace("\n", " ") }} | {{ metric.data_sensitivity|join(", ", attribute="name") }} | {{ metric.bugs|last }}
+{% endfor %}
+{% else %}
+`{{ metric.category|snake_case }}.{{ metric.name|snake_case }}` | {{ metric.description|replace("\n", " ") }} | {{ metric.data_sensitivity|join(", ", attribute="name") }} | {{ metric.bugs|last }}
+{% endif %}
+{% endfor %}
+
+6. Please provide a link to the documentation for this data collection which
+ describes the ultimate data set in a public, complete, and accurate way.
+
+This collection is Glean so is documented [in the Glean Dictionary](https://dictionary.telemetry.mozilla.org).
+
+7. How long will this data be collected?
+
+{% if durations|length == 1 %}
+{% for duration in durations %}
+{% if duration == "never" %}
+This collection will be collected permanently.
+{% else %}
+This collection has expiry '{{duration}}'.
+{% endif %}
+{% endfor %}
+{% else %}
+Parts of this collection expire at different times: {{ durations|join(", ") }}.
+{% endif %}
+{% if "never" in durations %}
+{{ responsible_emails|join(", ") }} will be responsible for the permanent collections.
+{% endif %}
+
+8. What populations will you measure?
+
+All channels, countries, and locales. No filters.
+
+9. If this data collection is default on, what is the opt-out mechanism for users?
+
+These collections are Glean. The opt-out can be found in the product's preferences.
+
+10. Please provide a general description of how you will analyze this data.
+
+{{ "TODO: Fill this in." if not analysis_how }}
+
+11. Where do you intend to share the results of your analysis?
+
+{{ "TODO: Fill this in." if not analysis_where }}
+
+12. Is there a third-party tool (i.e. not Glean or Telemetry) that you
+ are proposing to use for this data collection?
+
+No.
diff --git a/third_party/python/glean_parser/glean_parser/templates/javascript.buildinfo.jinja2 b/third_party/python/glean_parser/glean_parser/templates/javascript.buildinfo.jinja2
new file mode 100644
index 0000000000..79968d3d0a
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/javascript.buildinfo.jinja2
@@ -0,0 +1,11 @@
+/*
+ * AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. DO NOT COMMIT.
+ */
+{# The rendered markdown is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+{% if platform != "qt" %}export {% endif %}const buildDate = {{ build_date }};
diff --git a/third_party/python/glean_parser/glean_parser/templates/javascript.jinja2 b/third_party/python/glean_parser/glean_parser/templates/javascript.jinja2
new file mode 100644
index 0000000000..4036e8922f
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/javascript.jinja2
@@ -0,0 +1,73 @@
+{# The final Javascript/Typescript code is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+{% macro obj_declaration(obj) %}
+new {{ obj.type|class_name }}{% if obj.extra_keys and lang == "typescript" %}<{
+ {% for name, type in obj.allowed_extra_keys_with_types %}
+ {{ name }}?: {{ type|extra_type_name }},
+ {% endfor %}
+}>{% endif %}({
+ {% for arg_name in (obj.type|args).common if obj[arg_name] is defined %}
+ {{ arg_name|camelize }}: {{ obj[arg_name]|js }},
+ {% endfor %}
+}{% for arg_name in (obj.type|args).extra if obj[arg_name] is defined %}, {{ obj[arg_name]|js }}{% endfor %}){% endmacro %}
+{% macro labeled_obj_declaration(obj) %}
+new {{ "labeled"|class_name }}({
+ {% for arg_name in (obj.type|args).common if obj[arg_name] is defined %}
+ {{ arg_name|camelize }}: {{ obj[arg_name]|js }},
+ {% endfor %}
+}, {{ obj.type|class_name }}{% if obj.labels is not none %}, {{ obj.labels|js }}{% endif %}){% endmacro %}
+/* 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/. */
+
+// AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. DO NOT COMMIT.
+
+{% if platform != "qt" %}
+{% if has_labeled_metrics %}
+import LabeledMetricType from "@mozilla/glean/private/metrics/labeled";
+{% endif %}
+{% for type in types %}
+import {{ type|class_name }} from "@mozilla/glean/private/{{ type|import_path }}";
+{% endfor %}
+{% else %}
+.import org.mozilla.Glean {{ version }} as Glean
+{% endif %}
+
+{% for obj in objs.values() %}
+/**
+ * {{ obj.description|wordwrap() | replace("\n", "\n * ") }}
+ *
+ * Generated from `{{ obj.identifier() }}`.
+ */
+{% if obj.labeled %}
+{% if platform != "qt" %}export {% endif %}const {{ obj.name|camelize }} = {{ labeled_obj_declaration(obj) }};
+{% else %}
+{% if platform != "qt" %}export {% endif %}const {{ obj.name|camelize }} = {{ obj_declaration(obj) }};
+{% endif %}
+
+{% if obj|attr("_generate_enums") %}
+{% for name, suffix in obj["_generate_enums"] %}
+{% if obj|attr(name)|length and name == "reason_codes" %}
+/**
+ * Reason codes for `{{ obj.identifier() }}`.
+ *
+ * @readonly
+ * @enum {string}
+ */
+{% if lang == "typescript" %}
+export enum {{ obj.name|Camelize }}{{ name|Camelize }} {
+ {% for key in obj|attr(name) %}
+ {{ key|Camelize }} = "{{ key }}",
+ {% endfor %}
+}
+{% else %}
+{% if platform != "qt" %}export {% endif %}const {{ obj.name|Camelize }}{{ name|Camelize }} = {
+ {% for key in obj|attr(name) %}
+ "{{ key|Camelize }}": "{{ key }}",
+ {% endfor %}
+}
+{% endif %}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% endfor %}
diff --git a/third_party/python/glean_parser/glean_parser/templates/javascript_server.jinja2 b/third_party/python/glean_parser/glean_parser/templates/javascript_server.jinja2
new file mode 100644
index 0000000000..0a89f081f6
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/javascript_server.jinja2
@@ -0,0 +1,285 @@
+{# The final Javascript/Typescript code is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+/* 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/. */
+
+// AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. DO NOT COMMIT.
+
+// This requires `uuid` and `mozlog` libraries to be in the environment
+{% if lang == "typescript" %}
+// @types/uuid and mozlog types definitions are required in devDependencies
+// for the latter see https://github.com/mozilla/fxa/blob/85bda71cda376c417b8c850ba82aa14252208c3c/types/mozlog/index.d.ts
+{% endif %}
+import { v4 as uuidv4 } from 'uuid';
+import mozlog{% if lang == "typescript" %}, { Logger }{% endif %} from 'mozlog';
+
+const GLEAN_EVENT_MOZLOG_TYPE = 'glean-server-event';
+{% if lang == "typescript" %}
+type LoggerOptions = { app: string; fmt?: 'heka' };
+{% if event_metric_exists %}
+type Event = {
+ category: string;
+ name: string;
+ extra: Record<string, any>;
+ timestamp?: number;
+};
+{% endif %}
+{% endif %}
+
+let _logger{% if lang == "typescript" %}: Logger{% endif %};
+
+{% for ping, metrics_by_type in pings.items() %}
+class {{ ping|event_class_name(event_metric_exists) }} {
+ {% if lang == "typescript" %}
+ _applicationId: string;
+ _appDisplayVersion: string;
+ _channel: string;
+ {% endif %}
+ /**
+ * Create {{ ping|event_class_name(event_metric_exists) }} instance.
+ *
+ * @param {string} applicationId - The application ID.
+ * @param {string} appDisplayVersion - The application display version.
+ * @param {string} channel - The channel.
+ * @param {LoggerOptions} logger_options - The logger options.
+ */
+ {% if lang == "typescript" %}
+ constructor(
+ applicationId: string,
+ appDisplayVersion: string,
+ channel: string,
+ logger_options: LoggerOptions
+ ) {
+ {% else %}
+ constructor(applicationId, appDisplayVersion, channel, logger_options) {
+ {% endif %}
+ this._applicationId = applicationId;
+ this._appDisplayVersion = appDisplayVersion;
+ this._channel = channel;
+
+ if (!_logger) {
+ // append '-glean' to `logger_options.app` to avoid collision with other loggers and double logging
+ logger_options.app = logger_options.app + '-glean';
+ // set the format to `heka` so messages are properly ingested and decoded
+ logger_options.fmt = 'heka';
+ {% if lang == "typescript" %}
+ // mozlog types declaration requires a typePrefix to be passed when creating a logger
+ // we don't want a typePrefix, so we pass `undefined`
+ _logger = mozlog(logger_options)(undefined);
+ {% else %}
+ _logger = mozlog(logger_options)();
+ {% endif %}
+ }
+ }
+ {% if event_metric_exists %}
+ #record({
+ {% else %}
+ /**
+ * Record and submit a server event object.
+ * Event is logged using internal mozlog logger.
+ *
+ * @param {string} user_agent - The user agent.
+ * @param {string} ip_address - The IP address. Will be used to decode Geo
+ * information and scrubbed at ingestion.
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% for metric in metrics %}
+ * @param { {{-metric|js_metric_type-}} } {{ metric|metric_argument_name }} - {{ metric.description|clean_string }}.
+ {% endfor %}
+ {% endfor %}
+ */
+ record({
+ {% endif %}
+ user_agent,
+ ip_address,
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% if metric_type != 'event' %}
+ {% for metric in metrics %}
+ {{ metric|metric_argument_name }},
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ {% if event_metric_exists %}
+ event,
+ {% endif %}
+ {% if lang == "typescript" %}
+ }: {
+ user_agent: string,
+ ip_address: string,
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% if metric_type != 'event' %}
+ {% for metric in metrics %}
+ {{ metric|metric_argument_name }}: {{ metric|js_metric_type }},
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ {% if event_metric_exists %}
+ event: Event
+ {% endif %}
+ {% endif %}
+ }) {
+ const now = new Date();
+ const timestamp = now.toISOString();
+ {% if event_metric_exists %}
+ event.timestamp = now.getTime();
+ {% endif %}
+ const eventPayload = {
+ metrics: {
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% if metric_type != 'event' %}
+ {{ metric_type }}: {
+ {% for metric in metrics %}
+ '{{ metric|metric_name }}': {{ metric|metric_argument_name }},
+ {% endfor %}
+ },
+ {% endif %}
+ {% endfor %}
+ },
+ {% if event_metric_exists %}
+ events: [event],
+ {% endif %}
+ ping_info: {
+ seq: 0, // this is required, however doesn't seem to be useful in server context
+ start_time: timestamp,
+ end_time: timestamp,
+ },
+ // `Unknown` fields below are required in the Glean schema, however they are not useful in server context
+ client_info: {
+ telemetry_sdk_build: 'glean_parser v{{ parser_version }}',
+ first_run_date: 'Unknown',
+ os: 'Unknown',
+ os_version: 'Unknown',
+ architecture: 'Unknown',
+ app_build: 'Unknown',
+ app_display_version: this._appDisplayVersion,
+ app_channel: this._channel,
+ },
+ };
+ const eventPayloadSerialized = JSON.stringify(eventPayload);
+
+ // This is the message structure that Decoder expects: https://github.com/mozilla/gcp-ingestion/pull/2400
+ const ping = {
+ document_namespace: this._applicationId,
+ document_type: '{{ ping }}',
+ document_version: '1',
+ document_id: uuidv4(),
+ user_agent: user_agent,
+ ip_address: ip_address,
+ payload: eventPayloadSerialized,
+ };
+
+ // this is similar to how FxA currently logs with mozlog: https://github.com/mozilla/fxa/blob/4c5c702a7fcbf6f8c6b1f175e9172cdd21471eac/packages/fxa-auth-server/lib/log.js#L289
+ _logger.info(GLEAN_EVENT_MOZLOG_TYPE, ping);
+ }
+ {% if event_metric_exists %}
+ {% for event in metrics_by_type["event"] %}
+ /**
+ * Record and submit a {{ event.category }}_{{ event.name }} event:
+ * {{ event.description|clean_string }}
+ * Event is logged using internal mozlog logger.
+ *
+ * @param {string} user_agent - The user agent.
+ * @param {string} ip_address - The IP address. Will be used to decode Geo
+ * information and scrubbed at ingestion.
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% if metric_type != 'event' %}
+ {% for metric in metrics %}
+ * @param { {{-metric|js_metric_type-}} } {{ metric|metric_argument_name }} - {{ metric.description|clean_string }}.
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ {% if event.extra_keys %}
+ {% for extra, metadata in event.extra_keys.items() %}
+ * @param { {{-metadata.type-}} } {{ extra }} - {{ metadata.description|clean_string }}.
+ {% endfor %}
+ {% endif %}
+ */
+ {{ event|event_metric_record_function_name }}({
+ user_agent,
+ ip_address,
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% if metric_type != 'event' %}
+ {% for metric in metrics %}
+ {{ metric|metric_argument_name }},
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ {% for extra, metadata in event.extra_keys.items() %}
+ {{ extra }},
+ {% endfor %}
+ {% if lang == "typescript" %}
+ }: {
+ user_agent: string,
+ ip_address: string,
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% if metric_type != 'event' %}
+ {% for metric in metrics %}
+ {{ metric|metric_argument_name }}: {{ metric|js_metric_type }},
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ {% for extra, metadata in event.extra_keys.items() %}
+ {{ extra }}: {{metadata.type}},
+ {% endfor %}
+ {% endif %}
+ }) {
+ let event = {
+ 'category': '{{ event.category }}',
+ 'name': '{{ event.name }}',
+ {% if event.extra_keys %}
+ 'extra': {
+ {% for extra, metadata in event.extra_keys.items() %}
+ '{{ extra }}': {{ extra }},
+ {% endfor %}
+ },
+ {% endif %}
+ };
+ this.#record({
+ user_agent,
+ ip_address,
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% if metric_type != 'event' %}
+ {% for metric in metrics %}
+ {{ metric|metric_argument_name }},
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ event
+ });
+ }
+ {% endfor %}
+ {% endif %}
+}
+{% endfor %}
+{% for ping in pings %}
+
+/**
+ * Factory function that creates an instance of Glean Server Event Logger to
+ * record `{{ ping }}` ping events.
+ * @param {string} applicationId - The application ID.
+ * @param {string} appDisplayVersion - The application display version.
+ * @param {string} channel - The channel.
+ * @param {Object} logger_options - The logger options.
+ * @returns {EventsServerEventLogger} An instance of EventsServerEventLogger.
+ */
+export const {{ ping|factory_method(event_metric_exists) }} = function ({
+ applicationId,
+ appDisplayVersion,
+ channel,
+ logger_options
+{% if lang == "typescript" %}
+}: {
+ applicationId: string;
+ appDisplayVersion: string;
+ channel: string;
+ logger_options: LoggerOptions;
+{% endif %}
+}) {
+ return new {{ ping|event_class_name(event_metric_exists) }}(
+ applicationId,
+ appDisplayVersion,
+ channel,
+ logger_options
+ );
+};
+{% endfor %}
diff --git a/third_party/python/glean_parser/glean_parser/templates/kotlin.buildinfo.jinja2 b/third_party/python/glean_parser/glean_parser/templates/kotlin.buildinfo.jinja2
new file mode 100644
index 0000000000..2e0db5b302
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/kotlin.buildinfo.jinja2
@@ -0,0 +1,31 @@
+// -*- mode: kotlin -*-
+
+/*
+ * AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. DO NOT COMMIT.
+ */
+{# The rendered markdown is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* 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/. */
+
+@file:Suppress("PackageNaming", "MaxLineLength")
+
+package {{ namespace }}
+
+import java.util.Calendar
+import java.util.TimeZone
+import {{ glean_namespace }}.BuildInfo
+import {{ namespace_package }}.BuildConfig
+
+@Suppress("MagicNumber")
+internal object GleanBuildInfo {
+ val buildInfo: BuildInfo by lazy {
+ BuildInfo(
+ versionCode = BuildConfig.VERSION_CODE.toString(),
+ versionName = BuildConfig.VERSION_NAME,
+ buildDate = {{ build_date }}
+ )
+ }
+}
diff --git a/third_party/python/glean_parser/glean_parser/templates/kotlin.geckoview.jinja2 b/third_party/python/glean_parser/glean_parser/templates/kotlin.geckoview.jinja2
new file mode 100644
index 0000000000..f58c788e93
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/kotlin.geckoview.jinja2
@@ -0,0 +1,124 @@
+// -*- mode: kotlin -*-
+
+/*
+ * AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. DO NOT COMMIT.
+ */
+{# The rendered markdown is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* 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/. */
+
+@file:Suppress("PackageNaming", "MaxLineLength")
+package {{ namespace }}
+
+import {{ glean_namespace }}.private.BooleanMetricType // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.CounterMetricType // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.HistogramMetricBase // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.LabeledMetricType // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.QuantityMetricType // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.StringMetricType // ktlint-disable import-ordering no-unused-imports
+
+/*
+ * This class performs the mapping between Gecko metrics and Glean SDK
+ * metric types.
+ */
+internal object GleanGeckoMetricsMapping {
+ // Support exfiltration of Gecko histograms from products using both the
+ // Glean SDK and GeckoView. See bug 1566356 for more context.
+ @Suppress("UNUSED_PARAMETER")
+ fun getHistogram(geckoMetricName: String): HistogramMetricBase? {
+ {% if 'histograms' in gecko_metrics %}
+ return when (geckoMetricName) {
+ {% for category in gecko_metrics['histograms'].keys()|sort %}
+ // From {{ category|Camelize }}.kt
+ {% for metric in gecko_metrics['histograms'][category] %}
+ "{{ metric.gecko_datapoint }}" -> {{ category|Camelize }}.{{ metric.name|camelize }}
+ {% endfor %}
+ {%- endfor %}
+ else -> null
+ }
+ {% else %}
+ return null
+ {% endif %}
+ }
+
+ // Support exfiltration of Gecko categorical histograms from products using
+ // both the Glean SDK and GeckoView. See bug 1571740 for more context.
+ @Suppress("UNUSED_PARAMETER")
+ fun getCategoricalMetric(
+ geckoMetricName: String
+ ): LabeledMetricType<CounterMetricType>? {
+ {% if 'categoricals' in gecko_metrics %}
+ return when (geckoMetricName) {
+ {% for category in gecko_metrics['categoricals'].keys()|sort %}
+ // From {{ category|Camelize }}.kt
+ {% for metric in gecko_metrics['categoricals'][category] %}
+ "{{ metric.gecko_datapoint }}" -> {{ category|Camelize }}.{{ metric.name|camelize }}
+ {% endfor %}
+ {%- endfor %}
+ else -> null
+ }
+ {% else %}
+ return null
+ {% endif %}
+ }
+
+ // Support exfiltration of Gecko boolean scalars from products using both the
+ // Glean SDK and GeckoView. See bug 1579365 for more context.
+ @Suppress("UNUSED_PARAMETER")
+ fun getBooleanScalar(geckoMetricName: String): BooleanMetricType? {
+ {% if 'boolean' in gecko_metrics %}
+ return when (geckoMetricName) {
+ {% for category in gecko_metrics['boolean'].keys()|sort %}
+ // From {{ category|Camelize }}.kt
+ {% for metric in gecko_metrics['boolean'][category] %}
+ "{{ metric.gecko_datapoint }}" -> {{ category|Camelize }}.{{ metric.name|camelize }}
+ {% endfor %}
+ {%- endfor %}
+ else -> null
+ }
+ {% else %}
+ return null
+ {% endif %}
+ }
+
+ // Support exfiltration of Gecko string scalars from products using both the
+ // Glean SDK and GeckoView. See bug 1579365 for more context.
+ @Suppress("UNUSED_PARAMETER")
+ fun getStringScalar(geckoMetricName: String): StringMetricType? {
+ {% if 'string' in gecko_metrics %}
+ return when (geckoMetricName) {
+ {% for category in gecko_metrics['string'].keys()|sort %}
+ // From {{ category|Camelize }}.kt
+ {% for metric in gecko_metrics['string'][category] %}
+ "{{ metric.gecko_datapoint }}" -> {{ category|Camelize }}.{{ metric.name|camelize }}
+ {% endfor %}
+ {%- endfor %}
+ else -> null
+ }
+ {% else %}
+ return null
+ {% endif %}
+ }
+
+ // Support exfiltration of Gecko quantity scalars from products using both the
+ // Glean SDK and GeckoView. See bug 1579365 for more context.
+ @Suppress("UNUSED_PARAMETER")
+ fun getQuantityScalar(geckoMetricName: String): QuantityMetricType? {
+ {% if 'quantity' in gecko_metrics %}
+ return when (geckoMetricName) {
+ {% for category in gecko_metrics['quantity'].keys()|sort %}
+ // From {{ category|Camelize }}.kt
+ {% for metric in gecko_metrics['quantity'][category] %}
+ "{{ metric.gecko_datapoint }}" -> {{ category|Camelize }}.{{ metric.name|camelize }}
+ {% endfor %}
+ {%- endfor %}
+ else -> null
+ }
+ {% else %}
+ return null
+ {% endif %}
+ }
+}
diff --git a/third_party/python/glean_parser/glean_parser/templates/kotlin.jinja2 b/third_party/python/glean_parser/glean_parser/templates/kotlin.jinja2
new file mode 100644
index 0000000000..bd800af01d
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/kotlin.jinja2
@@ -0,0 +1,133 @@
+// -*- mode: kotlin -*-
+
+/*
+ * AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. DO NOT COMMIT.
+ */
+{# The rendered markdown is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* 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/. */
+{%- macro obj_declaration(obj, suffix='', access='', lazy=False) -%}
+{% if (access != "private ") -%}
+@get:JvmName("{{ obj.name|camelize }}{{ suffix }}")
+{% endif -%}
+{{ access }}val {{ obj.name|camelize }}{{ suffix }}: {{ obj|type_name }}{% if lazy %} by lazy { {%- else %} ={% endif %} // generated from {{ obj.identifier() }}
+{% if obj.type == 'ping' %}
+ {{ obj|type_name }}(
+ {% for arg_name in ping_args if obj[arg_name] is defined %}
+ {{ arg_name|camelize }} = {{ obj[arg_name]|kotlin }}{{ "," if not loop.last }}
+ {% endfor %}
+ )
+{% else %}
+ {{ obj|type_name }}(
+ CommonMetricData(
+ {% for arg_name in common_metric_args if obj[arg_name] is defined %}
+ {{ arg_name|camelize }} = {{ obj[arg_name]|kotlin }}{{ "," if not loop.last }}
+ {% endfor %}
+ ){%- for arg_name in extra_metric_args if obj[arg_name] is defined -%}
+ , {{ arg_name|camelize }} = {{ obj[arg_name]|kotlin }}
+ {%- endfor -%}
+ )
+{% endif %}
+{% if lazy %}}{% endif %}
+{%- endmacro -%}
+
+{%- macro reason_enum_decl(obj, name, suffix) -%}
+@Suppress("ClassNaming", "EnumNaming")
+enum class {{ obj.name|camelize }}{{ suffix }} : ReasonCode {
+{% for key in obj|attr(name) %}
+ {{ key|camelize }} {
+ override fun code(): Int = {{ loop.index-1 }}
+ }{{ "," if not loop.last }}{{ ";" if loop.last }}
+
+{% endfor %}
+}
+{%- endmacro %}
+
+{%- macro struct_decl(obj, name, suffix) -%}
+@Suppress("ClassNaming", "EnumNaming")
+data class {{ obj.name|Camelize }}{{ suffix }}(
+{% for item, typ in obj|attr(name) %}
+ val {{ item|camelize }}: {{typ|extra_type_name}}? = null{{ "," if not loop.last }}
+{% endfor %}
+) : EventExtras {
+ override fun toExtraRecord(): Map<String, String> {
+ val map = mutableMapOf<String, String>()
+
+ {% for item in obj|attr(name) %}
+ this.{{ item[0]|camelize }}?.let {
+ map.put("{{item[0]}}", it.toString())
+ }
+ {% endfor %}
+ return map
+ }
+}
+{%- endmacro -%}
+
+/* ktlint-disable no-blank-line-before-rbrace */
+@file:Suppress("PackageNaming", "MaxLineLength")
+package {{ namespace }}
+
+import {{ glean_namespace }}.private.CommonMetricData // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.EventExtras // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.HistogramType // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.Lifetime // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.MemoryUnit // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.NoExtras // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.ReasonCode // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.NoReasonCodes // ktlint-disable import-ordering no-unused-imports
+import {{ glean_namespace }}.private.TimeUnit // ktlint-disable import-ordering no-unused-imports
+{% for obj_type in obj_types %}
+import {{ glean_namespace }}.private.{{ obj_type }} // ktlint-disable import-ordering
+{% endfor %}
+{% if has_labeled_metrics %}
+import {{ glean_namespace }}.private.LabeledMetricType // ktlint-disable import-ordering
+{% endif %}
+
+internal object {{ category_name|Camelize }} {
+{% for obj in objs.values() %}
+ {% if obj.type == "ping" %}
+ {% if obj|attr("_generate_enums") %}
+ {% for name, suffix in obj["_generate_enums"] %}
+ {% if obj|attr(name)|length %}
+ {{ reason_enum_decl(obj, name, suffix)|indent }}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ {% else %}
+ {% if obj|attr("_generate_enums") %}
+ {% for name, suffix in obj["_generate_enums"] %}
+ {% if obj|attr(name)|length %}
+ {{ struct_decl(obj, name, suffix)|indent }}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ {% endif %}
+{% endfor %}
+{% for obj in objs.values() %}
+ {% if obj.labeled %}
+ {{ obj_declaration(obj, 'Label', 'private ') | indent }}
+ /**
+ * {{ obj.description|wordwrap() | replace('\n', '\n * ') }}
+ */
+ val {{ obj.name|camelize }}: LabeledMetricType<{{ obj|type_name }}> by lazy { // generated from {{ obj.identifier() }}
+ LabeledMetricType(
+ category = {{ obj.category|kotlin }},
+ name = {{ obj.name|kotlin }},
+ subMetric = {{ obj.name|camelize }}Label,
+ disabled = {{ obj.is_disabled()|kotlin }},
+ lifetime = {{ obj.lifetime|kotlin }},
+ sendInPings = {{ obj.send_in_pings|kotlin }},
+ labels = {{ obj.labels|kotlin }}
+ )
+ }
+ {% else %}
+ /**
+ * {{ obj.description|wordwrap() | replace('\n', '\n * ') }}
+ */
+ {{ obj_declaration(obj, lazy=obj.type != 'ping') | indent }}
+ {% endif %}
+{%- endfor %}
+}
diff --git a/third_party/python/glean_parser/glean_parser/templates/markdown.jinja2 b/third_party/python/glean_parser/glean_parser/templates/markdown.jinja2
new file mode 100644
index 0000000000..9370311247
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/markdown.jinja2
@@ -0,0 +1,98 @@
+<!-- AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. -->
+{# The rendered markdown is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+# Metrics
+
+This document enumerates the metrics collected by {{ project_title }} using the [Glean SDK](https://mozilla.github.io/glean/book/index.html).
+This project may depend on other projects which also collect metrics.
+This means you might have to go searching through the dependency tree to get a full picture of everything collected by this project.
+{% if introduction_extra %}
+
+{{ introduction_extra }}
+{% endif %}
+
+# Pings
+
+{% for ping_name in metrics_by_pings.keys()|sort %}
+- [{{ ping_name }}]({{ '#' }}{{ ping_name|replace(" ","-") }})
+{% endfor %}
+
+{% for ping_name in metrics_by_pings.keys()|sort %}
+{% raw %}##{% endraw %} {{ ping_name }}
+
+{% if ping_name|ping_desc and ping_name|ping_desc|length > 0 %}
+{{ ping_name|ping_desc }}
+
+{% if ping_name|ping_docs|length > 0 %}
+See the Glean SDK documentation for the [`{{ ping_name }}` ping]({{ ping_name|ping_docs }}).
+
+{% endif %}
+{% endif %}
+{% if ping_name|ping_send_if_empty %}
+This ping is sent if empty.
+
+{% endif %}
+{% if ping_name|ping_include_client_id %}
+This ping includes the [client id](https://mozilla.github.io/glean/book/user/pings/index.html#the-client_info-section).
+
+{% endif %}
+{% if ping_name|ping_data_reviews %}
+**Data reviews for this ping:**
+
+{% for review in ping_name|ping_data_reviews %}
+- <{{review}}>
+{% endfor %}
+
+{% endif %}
+{% if ping_name|ping_bugs %}
+**Bugs related to this ping:**
+
+{% for bug in ping_name|ping_bugs %}
+- {% if bug|int != 0 %}{{bug}}{% else %}<{{bug}}>{% endif %}
+
+{% endfor %}
+
+{% endif %}
+{% if ping_name|ping_reasons %}
+**Reasons this ping may be sent:**
+
+{% for (reason, desc) in ping_name|ping_reasons|dictsort %}
+- `{{ reason }}`: {{ desc|indent(6, first=False) }}
+{% endfor %}
+
+{% endif %}
+All Glean pings contain built-in metrics in the [`ping_info`](https://mozilla.github.io/glean/book/user/pings/index.html#the-ping_info-section) and [`client_info`](https://mozilla.github.io/glean/book/user/pings/index.html#the-client_info-section) sections.
+
+{% if metrics_by_pings[ping_name] %}
+In addition to those built-in metrics, the following metrics are added to the ping:
+
+| Name | Type | Description | Data reviews | Extras | Expiration | [Data Sensitivity](https://wiki.mozilla.org/Firefox/Data_Collection) |
+| --- | --- | --- | --- | --- | --- | --- |
+{% for metric in metrics_by_pings[ping_name] %}
+| {{ metric.identifier() }} |
+{{- '['}}{{ metric.type }}]({{ metric.type|metrics_docs }}) |
+{{- metric.description|replace("\n", " ") }} |
+{%- for data_review in metric.data_reviews %}
+[{{ data_review|ping_review_title(loop.index) }}]({{ data_review }}){{ ", " if not loop.last }}
+{%- endfor -%} |
+{%- if metric|extra_info -%}
+<ul>
+{%- for property, desc in metric|extra_info %}
+<li>{{ property }}{%- if desc is not none -%}: {{ desc|replace("\n", " ") }}{%- endif -%}</li>
+{%- endfor -%}
+</ul>
+{%- endif -%} |
+{{- metric.expires }} |
+{{- metric.data_sensitivity|data_sensitivity_numbers }} |
+{% endfor %}
+{% else %}
+This ping contains no metrics.
+{% endif %}
+
+{% endfor %}
+Data categories are [defined here](https://wiki.mozilla.org/Firefox/Data_Collection).
+
+<!-- AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. -->
+{# The rendered markdown is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
diff --git a/third_party/python/glean_parser/glean_parser/templates/qmldir.jinja2 b/third_party/python/glean_parser/glean_parser/templates/qmldir.jinja2
new file mode 100644
index 0000000000..f511912808
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/qmldir.jinja2
@@ -0,0 +1,4 @@
+{% for category in categories|sort %}
+{{ category|Camelize }} {{ version }} {{ category|camelize }}.js
+{% endfor %}
+depends org.mozilla.Glean {{ version }}
diff --git a/third_party/python/glean_parser/glean_parser/templates/ruby_server.jinja2 b/third_party/python/glean_parser/glean_parser/templates/ruby_server.jinja2
new file mode 100644
index 0000000000..d5c4f30c8f
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/ruby_server.jinja2
@@ -0,0 +1,180 @@
+{# The final Ruby code is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+# 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/.
+
+# AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT.
+
+# frozen_string_literal: true
+
+# requires json, securerandom, and logger libs
+require 'json'
+require 'securerandom'
+require 'logger'
+
+# this will be used for identifying logs that need to forward to Moz Data Pipeline
+GLEAN_EVENT_MOZLOG_TYPE = 'glean-server-event'
+
+module Glean
+{% for ping, metrics_by_type in pings.items() %}
+ class {{ ping|ping_class_name }}
+ def initialize(app_id:, app_display_version:, app_channel:, logger_options:)
+ @app_id = app_id # string - Application Id to identify application per Glean standards
+ @app_display_version = app_display_version # string - Version of application emitting the event
+ @app_channel = app_channel # string - Application Id to identify application per Glean standards
+ @logger = Logger.new(logger_options)
+
+ # Logger configuration
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
+ date_format = datetime.to_i
+ logger_name = 'glean'
+ "#{JSON.dump(Timestamp: date_format.to_s, Logger: logger_name.to_s, Type: GLEAN_EVENT_MOZLOG_TYPE.to_s, Severity: severity.ljust(5).to_s, Pid: Process.pid.to_s, Fields: msg)}\n"
+ end
+
+ # Generated events
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% for metric in metrics %}
+ {% if metric.type == 'event' %}
+ # {{ metric|metric_argument_description }}
+ @{{ metric|metric_argument_name }} = {{ metric|event_class_name }}.new(self)
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+ end
+
+ def _record(
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% if metric_type != 'event' %}
+ {% for metric in metrics %}
+ # {{ metric|metric_argument_description }}
+ {{ metric|metric_argument_name }}:,
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ # full user_agent value from controller context
+ user_agent:,
+ # ip address value from controller context
+ ip_address:,
+ # event being sent in the ping
+ event:
+ )
+ t_utc = Time.now.utc
+ # create raw metrics hash that can have nil values
+ metrics_raw = {
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% if metric_type != 'event' %}
+ '{{ metric_type }}' => {
+ {% for metric in metrics %}
+ '{{ metric|metric_name }}' => {{ metric|metric_argument_name }},
+ {% endfor %}
+ },
+ {% endif %}
+ {% endfor %}
+ }
+ # filter out key value pairs where value is nil
+ metrics_raw.each do |key, value|
+ metrics_raw[key] = value.compact.transform_values(&:to_s)
+ end
+ # filter out metrics with empty hashes
+ metrics = metrics_raw.reject { |_k, v| v.empty? }
+ event_payload = {
+ # `Unknown` fields below are required in the Glean schema, however they are not useful in server context.
+ 'client_info' => {
+ 'telemetry_sdk_build' => 'glean_parser v{{ parser_version }}',
+ 'first_run_date' => 'Unknown',
+ 'os' => 'Unknown',
+ 'os_version' => 'Unknown',
+ 'architecture' => 'Unknown',
+ 'app_build' => 'Unknown',
+ 'app_display_version' => @app_display_version,
+ 'app_channel' => @app_channel,
+ },
+ 'ping_info' => {
+ 'seq' => 0,
+ 'start_time' => t_utc,
+ 'end_time' => t_utc,
+ },
+ 'metrics' => metrics,
+ 'events' => event,
+ }
+ serialized_event_payload = event_payload.to_json
+ # This is the message structure that Decoder expects: https://github.com/mozilla/gcp-ingestion/pull/2400.
+ ping = {
+ 'document_namespace' => @app_id,
+ 'document_type' => '{{ ping }}',
+ 'document_version' => '1',
+ 'document_id' => SecureRandom.uuid,
+ 'user_agent' => user_agent,
+ 'ip_address' => ip_address,
+ 'payload' => serialized_event_payload,
+ }
+ @logger.info(ping)
+ end
+ {% for metric_type, metrics in metrics_by_type.items() %}
+ {% for metric in metrics %}
+ {% if metric.type == 'event' %}
+ attr_accessor :{{ metric|metric_argument_name }}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+ end
+{% endfor %}
+
+{% for event in pings["events"]["event"] %}
+ class {{ event|event_class_name }}
+ # {{ event|metric_argument_description }}
+ def initialize(glean)
+ @glean = glean
+ end
+
+ def record(
+ # extras to pass into event detail
+ {% for extra, metadata in event.extra_keys.items() %}
+ {{ extra }}:,
+ {% endfor %}
+ {% for ping, metric_types in pings.items() %}
+ {% for metric_type, metrics in metric_types.items() %}
+ {% if metric_type != 'event' %}
+ {% for metric in metrics %}
+ # {{ metric|metric_argument_description }}
+ {{ metric|metric_argument_name }}:,
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+ # full user_agent value from controller context
+ user_agent:,
+ # ip address value from controller context
+ ip_address:
+ )
+ event = [
+ {
+ 'category' => '{{ event.category }}',
+ 'name' => '{{ event.name }}',
+ 'timestamp' => (Time.now.utc.to_f * 1000).to_i,
+ 'extra' => [
+ {% for extra, metadata in event.extra_keys.items() %}
+ ['{{ extra }}', {{ extra }}],
+ {% endfor %}
+ ].to_h,
+ },
+ ]
+ @glean._record(
+ {% for ping, metric_types in pings.items() %}
+ {% for metric_type, metrics in metric_types.items() %}
+ {% if metric_type != 'event' %}
+ {% for metric in metrics %}
+ {{ metric|metric_argument_name }}: {{ metric|metric_argument_name }},
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+ user_agent: user_agent,
+ ip_address: ip_address,
+ event: event
+ )
+ end
+ end
+end
+{% endfor %}
diff --git a/third_party/python/glean_parser/glean_parser/templates/rust.jinja2 b/third_party/python/glean_parser/glean_parser/templates/rust.jinja2
new file mode 100644
index 0000000000..51e458cddf
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/rust.jinja2
@@ -0,0 +1,89 @@
+// -*- mode: Rust -*-
+
+// AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. DO NOT COMMIT.
+{# The rendered source is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* 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/. */
+
+{% macro generate_extra_keys(obj) %}
+{% for name, _ in obj["_generate_enums"] %}
+{# we always use the `extra` suffix, because we only expose the new event API #}
+{% set suffix = "Extra" %}
+{% if obj|attr(name)|length %}
+ {{ extra_keys_with_types(obj, name, suffix)|indent }}
+{% endif %}
+{% endfor %}
+{% endmacro %}
+{% macro extra_keys_with_types(obj, name, suffix) %}
+#[derive(Default, Debug, Clone, Hash, Eq, PartialEq)]
+pub struct {{ obj.name|Camelize }}{{ suffix }} {
+ {% for item, type in obj|attr(name) %}
+ pub {{ item|snake_case }}: Option<{{type|extra_type_name}}>,
+ {% endfor %}
+}
+
+impl ExtraKeys for {{ obj.name|Camelize }}{{ suffix }} {
+ const ALLOWED_KEYS: &'static [&'static str] = {{ obj.allowed_extra_keys|extra_keys }};
+
+ fn into_ffi_extra(self) -> ::std::collections::HashMap<::std::string::String, ::std::string::String> {
+ let mut map = ::std::collections::HashMap::new();
+ {% for key, _ in obj|attr(name) %}
+ self.{{key|snake_case}}.and_then(|val| map.insert("{{key}}".to_string(), val));
+ {% endfor %}
+ map
+ }
+}
+{% endmacro %}
+{% for category in categories %}
+{% if category.contains_pings %}
+{% for obj in category.objs.values() %}
+#[allow(non_upper_case_globals, dead_code)]
+/// {{ obj.description|wordwrap() | replace('\n', '\n/// ') }}
+#[rustfmt::skip]
+pub static {{ obj.name|snake_case }}: ::glean::private::__export::Lazy<::glean::private::PingType> =
+ ::glean::private::__export::Lazy::new(|| ::glean::private::PingType::new("{{ obj.name }}", {{ obj.include_client_id|rust }}, {{ obj.send_if_empty|rust }}, {{ obj.precise_timestamps|rust }}, {{ obj.reason_codes|rust }}));
+{% endfor %}
+{% else %}
+pub mod {{ category.name|snake_case }} {
+ #[allow(unused_imports)] // HistogramType might be unusued, let's avoid warnings
+ use glean::{private::*, traits::ExtraKeys, traits::NoExtraKeys, CommonMetricData, HistogramType, Lifetime, TimeUnit, MemoryUnit};
+ {% for obj in category.objs.values() %}
+
+ {% if obj|attr("_generate_enums") %}
+{{ generate_extra_keys(obj) }}
+ {%- endif %}
+ #[allow(non_upper_case_globals, dead_code)]
+ /// generated from {{ category.name }}.{{ obj.name }}
+ ///
+ /// {{ obj.description|wordwrap() | replace('\n', '\n /// ') }}
+ pub static {{ obj.name|snake_case }}: ::glean::private::__export::Lazy<{{ obj|type_name }}> = ::glean::private::__export::Lazy::new(|| {
+ {{ obj|ctor }}(CommonMetricData {
+ category: {{ obj.category|rust }},
+ name: {{ obj.name|rust }},
+ send_in_pings: {{ obj.send_in_pings|rust }},
+ lifetime: {{ obj.lifetime|rust }},
+ disabled: {{ obj.is_disabled()|rust }},
+ ..Default::default()
+ }
+ {%- for arg_name in extra_metric_args if obj[arg_name] is defined and arg_name != 'allowed_extra_keys' -%}
+ , {{ obj[arg_name]|rust }}
+ {%- endfor -%}
+ {{ ", " if obj.labeled else ")\n" }}
+ {%- if obj.labeled -%}
+ {%- if obj.labels -%}
+ Some({{ obj.labels|rust }})
+ {%- else -%}
+ None
+ {%- endif -%})
+ {% endif %}
+ });
+ {% endfor %}
+}
+{% endif %}
+{% endfor %}
+{% if metric_by_type|length > 0 %}
+
+{% endif %}
diff --git a/third_party/python/glean_parser/glean_parser/templates/swift.jinja2 b/third_party/python/glean_parser/glean_parser/templates/swift.jinja2
new file mode 100644
index 0000000000..82ad37bf20
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/templates/swift.jinja2
@@ -0,0 +1,139 @@
+// -*- mode: Swift -*-
+
+// AUTOGENERATED BY glean_parser v{{ parser_version }}. DO NOT EDIT. DO NOT COMMIT.
+{# The rendered markdown is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+#if canImport(Foundation)
+ import Foundation
+#endif
+
+/* 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/. */
+{% macro obj_declaration(obj, suffix='', access='') %}
+{{ access }}static let {{ obj.name|camelize|variable_name }}{{ suffix }} = {{ obj|type_name }}( // generated from {{ obj.identifier() }}
+ CommonMetricData(
+ {% for arg_name in common_metric_args if obj[arg_name] is defined %}
+ {{ arg_name|camelize }}: {{ obj[arg_name]|swift }}{{ "," if not loop.last }}
+ {% endfor %}
+ )
+ {% for arg_name in extra_metric_args if obj[arg_name] is defined %}
+ , {{ obj[arg_name]|swift }}
+ {% endfor %}
+ )
+{% endmacro %}
+
+{% macro struct_decl(obj, name, suffix) %}
+struct {{ obj.name|Camelize }}{{ suffix }}: EventExtras {
+ {% for item, typ in obj|attr(name) %}
+ var {{ item|camelize|variable_name }}: {{typ|extra_type_name}}?
+ {% endfor %}
+
+ func toExtraRecord() -> [String: String] {
+ var record = [String: String]()
+
+ {% for item in obj|attr(name) %}
+ if let {{ item[0]|camelize }} = self.{{item[0]|camelize}} {
+ record["{{item[0]}}"] = String({{ item[0]|camelize }})
+ }
+ {% endfor %}
+
+ return record
+ }
+ }
+{% endmacro %}
+
+{% if not allow_reserved %}
+import {{ glean_namespace }}
+
+{% endif %}
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable nesting
+// swiftlint:disable line_length
+// swiftlint:disable identifier_name
+// swiftlint:disable force_try
+
+extension {{ namespace }} {
+ {% if build_info %}
+ class GleanBuild {
+ private init() {
+ // Intentionally left private, no external user can instantiate a new global object.
+ }
+
+ public static let info = BuildInfo(buildDate: {{ build_info.build_date }})
+ }
+ {% endif %}
+
+ {% for category in categories %}
+ {% if category.contains_pings %}
+ class {{ category.name|Camelize }} {
+ public static let shared = {{ category.name|Camelize }}()
+ private init() {
+ // Intentionally left private, no external user can instantiate a new global object.
+ }
+
+ {% for obj in category.objs.values() %}
+ {% if obj|attr("_generate_enums") %}
+ {% for name, suffix in obj["_generate_enums"] %}
+ {% if obj|attr(name)|length %}
+ enum {{ obj.name|Camelize }}{{ suffix }}: Int, ReasonCodes {
+ {% for key in obj|attr(name) %}
+ case {{ key|camelize|variable_name }} = {{ loop.index-1 }}
+ {% endfor %}
+
+ public func index() -> Int {
+ return self.rawValue
+ }
+ }
+
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ /// {{ obj.description|wordwrap() | replace('\n', '\n /// ') }}
+ let {{ obj.name|camelize|variable_name }} = {{obj|type_name}}(
+ name: {{ obj.name|swift }},
+ includeClientId: {{obj.include_client_id|swift}},
+ sendIfEmpty: {{obj.send_if_empty|swift}},
+ preciseTimestamps: {{obj.precise_timestamps|swift}},
+ reasonCodes: {{obj.reason_codes|swift}}
+ )
+
+ {% endfor %}
+ }
+
+ {% else %}
+ enum {{ category.name|Camelize }} {
+ {% for obj in category.objs.values() %}
+ {% if obj|attr("_generate_enums") %}
+ {% for name, suffix in obj["_generate_enums"] %}
+ {% if obj|attr(name)|length %}
+ {{ struct_decl(obj, name, suffix)|indent }}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ {% for obj in category.objs.values() %}
+ {% if obj.labeled %}
+ {{ obj_declaration(obj, 'Label', 'private ') | indent }}
+ /// {{ obj.description|wordwrap() | replace('\n', '\n /// ') }}
+ static let {{ obj.name|camelize|variable_name }} = try! LabeledMetricType<{{ obj|type_name }}>( // generated from {{ obj.identifier() }}
+ category: {{ obj.category|swift }},
+ name: {{ obj.name|swift }},
+ sendInPings: {{ obj.send_in_pings|swift }},
+ lifetime: {{ obj.lifetime|swift }},
+ disabled: {{ obj.is_disabled()|swift }},
+ subMetric: {{ obj.name|camelize }}Label,
+ labels: {{ obj.labels|swift }}
+ )
+
+ {% else %}
+ /// {{ obj.description|wordwrap() | replace('\n', '\n /// ') }}
+ {{ obj_declaration(obj) | indent }}
+ {% endif %}
+ {% endfor %}
+ }
+
+ {% endif %}
+ {% endfor %}
+}
diff --git a/third_party/python/glean_parser/glean_parser/translate.py b/third_party/python/glean_parser/glean_parser/translate.py
new file mode 100644
index 0000000000..021fce47fb
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/translate.py
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+High-level interface for translating `metrics.yaml` into other formats.
+"""
+
+from pathlib import Path
+import os
+import shutil
+import sys
+import tempfile
+from typing import Any, Callable, Dict, Iterable, List, Optional
+
+from . import lint
+from . import parser
+from . import javascript
+from . import javascript_server
+from . import kotlin
+from . import markdown
+from . import metrics
+from . import ruby_server
+from . import rust
+from . import swift
+from . import util
+
+
+class Outputter:
+ """
+ Class to define an output format.
+
+ Each outputter in the table has the following member values:
+
+ - output_func: the main function of the outputter, the one which
+ does the actual translation.
+
+ - clear_patterns: A list of glob patterns to clear in the directory before
+ writing new results to it.
+ """
+
+ def __init__(
+ self,
+ output_func: Callable[[metrics.ObjectTree, Path, Dict[str, Any]], None],
+ clear_patterns: Optional[List[str]] = None,
+ ):
+ if clear_patterns is None:
+ clear_patterns = []
+
+ self.output_func = output_func
+ self.clear_patterns = clear_patterns
+
+
+OUTPUTTERS = {
+ "javascript": Outputter(javascript.output_javascript, []),
+ "typescript": Outputter(javascript.output_typescript, []),
+ "javascript_server": Outputter(javascript_server.output_javascript, []),
+ "typescript_server": Outputter(javascript_server.output_typescript, []),
+ "ruby_server": Outputter(ruby_server.output_ruby, []),
+ "kotlin": Outputter(kotlin.output_kotlin, ["*.kt"]),
+ "markdown": Outputter(markdown.output_markdown, []),
+ "swift": Outputter(swift.output_swift, ["*.swift"]),
+ "rust": Outputter(rust.output_rust, []),
+}
+
+
+def transform_metrics(objects):
+ """
+ Transform the object model from one that represents the YAML definitions
+ to one that reflects the type specifics needed by code generators.
+
+ e.g. This will transform a `rate` to be a `numerator` if its denominator is
+ external.
+ """
+ counters = {}
+ numerators_by_denominator: Dict[str, Any] = {}
+ for category_name, category_val in objects.items():
+ if category_name == "tags":
+ continue
+ for metric in category_val.values():
+ fqmn = metric.identifier()
+ if getattr(metric, "type", None) == "counter":
+ counters[fqmn] = metric
+ denominator_name = getattr(metric, "denominator_metric", None)
+ if denominator_name:
+ metric.type = "numerator"
+ numerators_by_denominator.setdefault(denominator_name, [])
+ numerators_by_denominator[denominator_name].append(metric)
+
+ for denominator_name, numerators in numerators_by_denominator.items():
+ if denominator_name not in counters:
+ raise ValueError(
+ f"No `counter` named {denominator_name} found to be used as"
+ "denominator for {numerators}",
+ file=sys.stderr,
+ )
+ counters[denominator_name].__class__ = metrics.Denominator
+ counters[denominator_name].type = "denominator"
+ counters[denominator_name].numerators = numerators
+
+
+def translate_metrics(
+ input_filepaths: Iterable[Path],
+ output_dir: Path,
+ translation_func: Callable[[metrics.ObjectTree, Path, Dict[str, Any]], None],
+ clear_patterns: Optional[List[str]] = None,
+ options: Optional[Dict[str, Any]] = None,
+ parser_config: Optional[Dict[str, Any]] = None,
+):
+ """
+ Translate the files in `input_filepaths` by running the metrics through a
+ translation function and writing the results in `output_dir`.
+
+ :param input_filepaths: list of paths to input metrics.yaml files
+ :param output_dir: the path to the output directory
+ :param translation_func: the function that actually performs the translation.
+ It is passed the following arguments:
+
+ - metrics_objects: The tree of metrics as pings as returned by
+ `parser.parse_objects`.
+ - output_dir: The path to the output directory.
+ - options: A dictionary of output format-specific options.
+
+ Examples of translation functions are in `kotlin.py` and `swift.py`.
+ :param clear_patterns: a list of glob patterns of files to clear before
+ generating the output files. By default, no files will be cleared (i.e.
+ the directory should be left alone).
+ :param options: dictionary of options. The available options are backend
+ format specific. These are passed unchanged to `translation_func`.
+ :param parser_config: A dictionary of options that change parsing behavior.
+ See `parser.parse_metrics` for more info.
+ """
+ if clear_patterns is None:
+ clear_patterns = []
+
+ if options is None:
+ options = {}
+
+ if parser_config is None:
+ parser_config = {}
+
+ input_filepaths = util.ensure_list(input_filepaths)
+
+ allow_missing_files = parser_config.get("allow_missing_files", False)
+ if not input_filepaths and not allow_missing_files:
+ print("❌ No metric files specified. ", end="")
+ print("Use `--allow-missing-files` to not treat this as an error.")
+ return 1
+
+ if lint.glinter(input_filepaths, parser_config):
+ return 1
+
+ all_objects = parser.parse_objects(input_filepaths, parser_config)
+
+ if util.report_validation_errors(all_objects):
+ return 1
+
+ # allow_reserved is also relevant to the translators, so copy it there
+ if parser_config.get("allow_reserved"):
+ options["allow_reserved"] = True
+
+ # We don't render tags anywhere yet.
+ all_objects.value.pop("tags", None)
+
+ # Apply additional general transformations to all metrics
+ transform_metrics(all_objects.value)
+
+ # Write everything out to a temporary directory, and then move it to the
+ # real directory, for transactional integrity.
+ with tempfile.TemporaryDirectory() as tempdir:
+ tempdir_path = Path(tempdir)
+ translation_func(all_objects.value, tempdir_path, options)
+
+ if output_dir.is_file():
+ output_dir.unlink()
+ elif output_dir.is_dir() and len(clear_patterns):
+ for clear_pattern in clear_patterns:
+ for filepath in output_dir.glob(clear_pattern):
+ filepath.unlink()
+ if len(list(output_dir.iterdir())):
+ print(f"Extra contents found in '{output_dir}'.")
+
+ # We can't use shutil.copytree alone if the directory already exists.
+ # However, if it doesn't exist, make sure to create one otherwise
+ # shutil.copy will fail.
+ os.makedirs(str(output_dir), exist_ok=True)
+ for filename in tempdir_path.glob("*"):
+ shutil.copy(str(filename), str(output_dir))
+
+ return 0
+
+
+def translate(
+ input_filepaths: Iterable[Path],
+ output_format: str,
+ output_dir: Path,
+ options: Optional[Dict[str, Any]] = None,
+ parser_config: Optional[Dict[str, Any]] = None,
+):
+ """
+ Translate the files in `input_filepaths` to the given `output_format` and
+ put the results in `output_dir`.
+
+ :param input_filepaths: list of paths to input metrics.yaml files
+ :param output_format: the name of the output format
+ :param output_dir: the path to the output directory
+ :param options: dictionary of options. The available options are backend
+ format specific.
+ :param parser_config: A dictionary of options that change parsing behavior.
+ See `parser.parse_metrics` for more info.
+ """
+ if options is None:
+ options = {}
+
+ if parser_config is None:
+ parser_config = {}
+
+ format_desc = OUTPUTTERS.get(output_format, None)
+
+ if format_desc is None:
+ raise ValueError(f"Unknown output format '{output_format}'")
+
+ return translate_metrics(
+ input_filepaths,
+ output_dir,
+ format_desc.output_func,
+ format_desc.clear_patterns,
+ options,
+ parser_config,
+ )
diff --git a/third_party/python/glean_parser/glean_parser/translation_options.py b/third_party/python/glean_parser/glean_parser/translation_options.py
new file mode 100644
index 0000000000..48774fee3c
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/translation_options.py
@@ -0,0 +1,54 @@
+import pydoc
+
+
+def translate_options(ctx, param, value):
+ text = """Target language options for Translate function
+
+These are backend specific and optional, provide as key:value
+
+Rust: no options.
+
+Swift:
+- `namespace`: The namespace to generate metrics in
+- `glean_namespace`: The namespace to import Glean from
+- `allow_reserved`: When True, this is a Glean-internal build
+- `with_buildinfo`: If "true" the `GleanBuildInfo` is generated.
+ Otherwise generation of that file is skipped.
+ Defaults to "true".
+- `build_date`: If set to `0` a static unix epoch time will be used.
+ If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
+ it will use that date.
+ Other values will throw an error.
+ If not set it will use the current date & time.
+
+Kotlin:
+- `namespace`: The package namespace to declare at the top of the
+ generated files. Defaults to `GleanMetrics`.
+- `glean_namespace`: The package namespace of the glean library itself.
+ This is where glean objects will be imported from in the generated
+ code.
+
+JavaScript:
+- `platform`: Which platform are we building for. Options are `webext` and `qt`.
+ Default is `webext`.
+- `version`: The version of the Glean.js Qt library being used.
+ This option is mandatory when targeting Qt. Note that the version
+ string must only contain the major and minor version i.e. 0.14.
+- `with_buildinfo`: If "true" a `gleanBuildInfo.(js|ts)` file is generated.
+ Otherwise generation of that file is skipped. Defaults to "false".
+- `build_date`: If set to `0` a static unix epoch time will be used.
+ If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
+ it will use that date.
+ Other values will throw an error.
+ If not set it will use the current date & time.
+
+Markdown:
+- `project_title`: The project's title.
+
+(press q to exit)"""
+
+ if value:
+ if value[0].lower() == "help":
+ pydoc.pager(text)
+ ctx.exit()
+ return value
diff --git a/third_party/python/glean_parser/glean_parser/util.py b/third_party/python/glean_parser/glean_parser/util.py
new file mode 100644
index 0000000000..edaeed9578
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/util.py
@@ -0,0 +1,533 @@
+# -*- coding: utf-8 -*-
+
+# 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 datetime
+import functools
+import json
+from pathlib import Path
+import sys
+import textwrap
+from typing import Any, Callable, Iterable, Sequence, Tuple, Union, Optional
+import urllib.request
+
+import appdirs # type: ignore
+import diskcache # type: ignore
+import jinja2
+import jsonschema # type: ignore
+from jsonschema import _utils # type: ignore
+import yaml
+
+
+def date_fromisoformat(datestr: str) -> datetime.date:
+ return datetime.date.fromisoformat(datestr)
+
+
+def datetime_fromisoformat(datestr: str) -> datetime.datetime:
+ return datetime.datetime.fromisoformat(datestr)
+
+
+TESTING_MODE = "pytest" in sys.modules
+
+
+JSONType = Union[list, dict, str, int, float, None]
+"""
+The types supported by JSON.
+
+This is only an approximation -- this should really be a recursive type.
+"""
+
+
+class DictWrapper(dict):
+ pass
+
+
+class _NoDatesSafeLoader(yaml.SafeLoader):
+ @classmethod
+ def remove_implicit_resolver(cls, tag_to_remove):
+ """
+ Remove implicit resolvers for a particular tag
+
+ Takes care not to modify resolvers in super classes.
+
+ We want to load datetimes as strings, not dates, because we
+ go on to serialise as json which doesn't have the advanced types
+ of yaml, and leads to incompatibilities down the track.
+ """
+ if "yaml_implicit_resolvers" not in cls.__dict__:
+ cls.yaml_implicit_resolvers = cls.yaml_implicit_resolvers.copy()
+
+ for first_letter, mappings in cls.yaml_implicit_resolvers.items():
+ cls.yaml_implicit_resolvers[first_letter] = [
+ (tag, regexp) for tag, regexp in mappings if tag != tag_to_remove
+ ]
+
+
+# Since we use JSON schema to validate, and JSON schema doesn't support
+# datetimes, we don't want the YAML loader to give us datetimes -- just
+# strings.
+_NoDatesSafeLoader.remove_implicit_resolver("tag:yaml.org,2002:timestamp")
+
+
+def yaml_load(stream):
+ """
+ Map line number to yaml nodes, and preserve the order
+ of metrics as they appear in the metrics.yaml file.
+ """
+
+ class SafeLineLoader(_NoDatesSafeLoader):
+ pass
+
+ def _construct_mapping_adding_line(loader, node):
+ loader.flatten_mapping(node)
+ mapping = DictWrapper(loader.construct_pairs(node))
+ mapping.defined_in = {"line": node.start_mark.line}
+ return mapping
+
+ SafeLineLoader.add_constructor(
+ yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_mapping_adding_line
+ )
+ return yaml.load(stream, SafeLineLoader)
+
+
+def ordered_yaml_dump(data, **kwargs):
+ class OrderedDumper(yaml.Dumper):
+ pass
+
+ def _dict_representer(dumper, data):
+ return dumper.represent_mapping(
+ yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, data.items()
+ )
+
+ OrderedDumper.add_representer(DictWrapper, _dict_representer)
+ return yaml.dump(data, Dumper=OrderedDumper, **kwargs)
+
+
+def load_yaml_or_json(path: Path):
+ """
+ Load the content from either a .json or .yaml file, based on the filename
+ extension.
+
+ :param path: `pathlib.Path` object
+ :rtype object: The tree of objects as a result of parsing the file.
+ :raises ValueError: The file is neither a .json, .yml or .yaml file.
+ :raises FileNotFoundError: The file does not exist.
+ """
+ # If in py.test, support bits of literal JSON/YAML content
+ if TESTING_MODE and isinstance(path, dict):
+ return yaml_load(yaml.dump(path))
+
+ if path.suffix == ".json":
+ with path.open("r", encoding="utf-8") as fd:
+ return json.load(fd)
+ elif path.suffix in (".yml", ".yaml", ".yamlx"):
+ with path.open("r", encoding="utf-8") as fd:
+ return yaml_load(fd)
+ else:
+ raise ValueError(f"Unknown file extension {path.suffix}")
+
+
+def ensure_list(value: Any) -> Sequence[Any]:
+ """
+ Ensures that the value is a list. If it is anything but a list or tuple, a
+ list with a single element containing only value is returned.
+ """
+ if not isinstance(value, (list, tuple)):
+ return [value]
+ return value
+
+
+def to_camel_case(input: str, capitalize_first_letter: bool) -> str:
+ """
+ Convert the value to camelCase.
+
+ This additionally replaces any '.' with '_'. The first letter is capitalized
+ depending on `capitalize_first_letter`.
+ """
+ sanitized_input = input.replace(".", "_").replace("-", "_")
+ # Filter out any empty token. This could happen due to leading '_' or
+ # consecutive '__'.
+ tokens = [s.capitalize() for s in sanitized_input.split("_") if len(s) != 0]
+ # If we're not meant to capitalize the first letter, then lowercase it.
+ if not capitalize_first_letter:
+ tokens[0] = tokens[0].lower()
+ # Finally join the tokens and capitalize.
+ return "".join(tokens)
+
+
+def camelize(value: str) -> str:
+ """
+ Convert the value to camelCase (with a lower case first letter).
+
+ This is a thin wrapper around inflection.camelize that handles dots in
+ addition to underscores.
+ """
+ return to_camel_case(value, False)
+
+
+def Camelize(value: str) -> str:
+ """
+ Convert the value to CamelCase (with an upper case first letter).
+
+ This is a thin wrapper around inflection.camelize that handles dots in
+ addition to underscores.
+ """
+ return to_camel_case(value, True)
+
+
+def snake_case(value: str) -> str:
+ """
+ Convert the value to snake_case.
+ """
+ return value.lower().replace(".", "_").replace("-", "_")
+
+
+def screaming_case(value: str) -> str:
+ """
+ Convert the value to SCREAMING_SNAKE_CASE.
+ """
+ return value.upper().replace(".", "_").replace("-", "_")
+
+
+@functools.lru_cache()
+def get_jinja2_template(
+ template_name: str, filters: Iterable[Tuple[str, Callable]] = ()
+):
+ """
+ Get a Jinja2 template that ships with glean_parser.
+
+ The template has extra filters for camel-casing identifiers.
+
+ :param template_name: Name of a file in ``glean_parser/templates``
+ :param filters: tuple of 2-tuple. A tuple of (name, func) pairs defining
+ additional filters.
+ """
+ env = jinja2.Environment(
+ loader=jinja2.PackageLoader("glean_parser", "templates"),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ )
+
+ env.filters["camelize"] = camelize
+ env.filters["Camelize"] = Camelize
+ env.filters["scream"] = screaming_case
+ for filter_name, filter_func in filters:
+ env.filters[filter_name] = filter_func
+
+ return env.get_template(template_name)
+
+
+def keep_value(f):
+ """
+ Wrap a generator so the value it returns (rather than yields), will be
+ accessible on the .value attribute when the generator is exhausted.
+ """
+
+ class ValueKeepingGenerator(object):
+ def __init__(self, g):
+ self.g = g
+ self.value = None
+
+ def __iter__(self):
+ self.value = yield from self.g
+
+ @functools.wraps(f)
+ def g(*args, **kwargs):
+ return ValueKeepingGenerator(f(*args, **kwargs))
+
+ return g
+
+
+def get_null_resolver(schema):
+ """
+ Returns a JSON Pointer resolver that does nothing.
+
+ This lets us handle the moz: URLs in our schemas.
+ """
+
+ class NullResolver(jsonschema.RefResolver):
+ def resolve_remote(self, uri):
+ if uri in self.store:
+ return self.store[uri]
+ if uri == "":
+ return self.referrer
+
+ return NullResolver.from_schema(schema)
+
+
+def fetch_remote_url(url: str, cache: bool = True):
+ """
+ Fetches the contents from an HTTP url or local file path, and optionally
+ caches it to disk.
+ """
+ # Include the Python version in the cache key, since caches aren't
+ # sharable across Python versions.
+ key = (url, str(sys.version_info))
+
+ is_http = url.startswith("http")
+
+ if not is_http:
+ with open(url, "r", encoding="utf-8") as fd:
+ return fd.read()
+
+ if cache:
+ cache_dir = appdirs.user_cache_dir("glean_parser", "mozilla")
+ with diskcache.Cache(cache_dir) as dc:
+ if key in dc:
+ return dc[key]
+
+ contents: str = urllib.request.urlopen(url).read()
+
+ if cache:
+ with diskcache.Cache(cache_dir) as dc:
+ dc[key] = contents
+
+ return contents
+
+
+_unset = _utils.Unset()
+
+
+def pprint_validation_error(error) -> str:
+ """
+ A version of jsonschema's ValidationError __str__ method that doesn't
+ include the schema fragment that failed. This makes the error messages
+ much more succinct.
+
+ It also shows any subschemas of anyOf/allOf that failed, if any (what
+ jsonschema calls "context").
+ """
+ essential_for_verbose = (
+ error.validator,
+ error.validator_value,
+ error.instance,
+ error.schema,
+ )
+ if any(m is _unset for m in essential_for_verbose):
+ return textwrap.fill(error.message)
+
+ instance = error.instance
+ for path in list(error.relative_path)[::-1]:
+ if isinstance(path, str):
+ instance = {path: instance}
+ else:
+ instance = [instance]
+
+ yaml_instance = ordered_yaml_dump(instance, width=72, default_flow_style=False)
+
+ parts = ["```", yaml_instance.rstrip(), "```", "", textwrap.fill(error.message)]
+ if error.context:
+ parts.extend(
+ textwrap.fill(x.message, initial_indent=" ", subsequent_indent=" ")
+ for x in error.context
+ )
+
+ description = error.schema.get("description")
+ if description:
+ parts.extend(
+ ["", "Documentation for this node:", textwrap.indent(description, " ")]
+ )
+
+ return "\n".join(parts)
+
+
+def format_error(
+ filepath: Union[str, Path],
+ header: str,
+ content: str,
+ lineno: Optional[int] = None,
+) -> str:
+ """
+ Format a jsonshema validation error.
+ """
+ if isinstance(filepath, Path):
+ filepath = filepath.resolve()
+ else:
+ filepath = "<string>"
+ if lineno:
+ filepath = f"{filepath}:{lineno}"
+ if header:
+ return f"{filepath}: {header}\n{textwrap.indent(content, ' ')}"
+ else:
+ return f"{filepath}:\n{textwrap.indent(content, ' ')}"
+
+
+def parse_expiration_date(expires: str) -> datetime.date:
+ """
+ Parses the expired field date (yyyy-mm-dd) as a date.
+ Raises a ValueError in case the string is not properly formatted.
+ """
+ try:
+ return date_fromisoformat(expires)
+ except (TypeError, ValueError):
+ raise ValueError(
+ f"Invalid expiration date '{expires}'. "
+ "Must be of the form yyyy-mm-dd in UTC."
+ )
+
+
+def parse_expiration_version(expires: str) -> int:
+ """
+ Parses the expired field version string as an integer.
+ Raises a ValueError in case the string does not contain a valid
+ positive integer.
+ """
+ try:
+ if isinstance(expires, int):
+ version_number = int(expires)
+ if version_number > 0:
+ return version_number
+ # Fall-through: if it's not an integer or is not greater than zero,
+ # raise an error.
+ raise ValueError()
+ except ValueError:
+ raise ValueError(
+ f"Invalid expiration version '{expires}'. Must be a positive integer."
+ )
+
+
+def is_expired(expires: str, major_version: Optional[int] = None) -> bool:
+ """
+ Parses the `expires` field in a metric or ping and returns whether
+ the object should be considered expired.
+ """
+ if expires == "never":
+ return False
+ elif expires == "expired":
+ return True
+ elif major_version is not None:
+ return parse_expiration_version(expires) <= major_version
+ else:
+ date = parse_expiration_date(expires)
+ return date <= datetime.datetime.utcnow().date()
+
+
+def validate_expires(expires: str, major_version: Optional[int] = None) -> None:
+ """
+ If expiration by major version is enabled, raises a ValueError in
+ case `expires` is not a positive integer.
+ Otherwise raises a ValueError in case the `expires` is not ISO8601
+ parseable, or in case the date is more than 730 days (~2 years) in
+ the future.
+ """
+ if expires in ("never", "expired"):
+ return
+
+ if major_version is not None:
+ parse_expiration_version(expires)
+ # Don't need to keep parsing dates if expiration by version
+ # is enabled. We don't allow mixing dates and versions for a
+ # single product.
+ return
+
+ date = parse_expiration_date(expires)
+ max_date = datetime.datetime.now() + datetime.timedelta(days=730)
+ if date > max_date.date():
+ raise ValueError(
+ f"'{expires}' is more than 730 days (~2 years) in the future.",
+ "Please make sure this is intentional.",
+ "You can supress this warning by adding EXPIRATION_DATE_TOO_FAR to no_lint",
+ "See: https://mozilla.github.io/glean_parser/metrics-yaml.html#no_lint",
+ )
+
+
+def build_date(date: Optional[str]) -> datetime.datetime:
+ """
+ Generate the build timestamp.
+
+ If `date` is set to `0` a static unix epoch time will be used.
+ If `date` it is set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
+ it will use that date.
+ Note that any timezone offset will be ignored and UTC will be used.
+ Otherwise it will throw an error.
+
+ If `date` is `None` it will use the current date & time.
+ """
+
+ if date is not None:
+ date = str(date)
+ if date == "0":
+ ts = datetime.datetime(1970, 1, 1, 0, 0, 0)
+ else:
+ ts = datetime_fromisoformat(date).replace(tzinfo=datetime.timezone.utc)
+ else:
+ ts = datetime.datetime.utcnow()
+
+ return ts
+
+
+def report_validation_errors(all_objects):
+ """
+ Report any validation errors found to the console.
+
+ Returns the number of errors reported.
+ """
+ found_errors = 0
+ for error in all_objects:
+ found_errors += 1
+ print("=" * 78, file=sys.stderr)
+ print(error, file=sys.stderr)
+ return found_errors
+
+
+def remove_output_params(d, output_params):
+ """
+ Remove output-only params, such as "defined_in",
+ in order to validate the output against the input schema.
+ """
+ modified_dict = {}
+ for key, value in d.items():
+ if key is not output_params:
+ modified_dict[key] = value
+ return modified_dict
+
+
+# Names of parameters to pass to all metrics constructors constructors.
+common_metric_args = [
+ "category",
+ "name",
+ "send_in_pings",
+ "lifetime",
+ "disabled",
+]
+
+
+# Names of parameters that only apply to some of the metrics types.
+# **CAUTION**: This list needs to be in the order the Swift & Rust type constructors
+# expects them. (The other language bindings don't care about the order).
+extra_metric_args = [
+ "time_unit",
+ "memory_unit",
+ "allowed_extra_keys",
+ "reason_codes",
+ "range_min",
+ "range_max",
+ "bucket_count",
+ "histogram_type",
+ "numerators",
+]
+
+
+# This includes only things that the language bindings care about, not things
+# that are metadata-only or are resolved into other parameters at parse time.
+# **CAUTION**: This list needs to be in the order the Swift & Rust type constructors
+# expects them. (The other language bindings don't care about the order). The
+# `test_order_of_fields` test checks that the generated code is valid.
+# **DO NOT CHANGE THE ORDER OR ADD NEW FIELDS IN THE MIDDLE**
+metric_args = common_metric_args + extra_metric_args
+
+
+# Names of ping parameters to pass to constructors.
+ping_args = [
+ "name",
+ "include_client_id",
+ "send_if_empty",
+ "precise_timestamps",
+ "reason_codes",
+]
+
+
+# Names of parameters to pass to both metric and ping constructors (no duplicates).
+extra_args = metric_args + [v for v in ping_args if v not in metric_args]
diff --git a/third_party/python/glean_parser/glean_parser/validate_ping.py b/third_party/python/glean_parser/glean_parser/validate_ping.py
new file mode 100644
index 0000000000..33598149eb
--- /dev/null
+++ b/third_party/python/glean_parser/glean_parser/validate_ping.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+"""
+Validates the contents of a Glean ping against the schema.
+"""
+
+import functools
+import io
+import json
+from pathlib import Path
+import sys
+
+import jsonschema # type: ignore
+
+from . import util
+
+
+ROOT_DIR = Path(__file__).parent
+SCHEMAS_DIR = ROOT_DIR / "schemas"
+
+
+@functools.lru_cache(maxsize=1)
+def _get_ping_schema(schema_url):
+ contents = util.fetch_remote_url(schema_url)
+ return json.loads(contents)
+
+
+def _validate_ping(ins, outs, schema_url):
+ schema = _get_ping_schema(schema_url)
+
+ resolver = util.get_null_resolver(schema)
+
+ document = json.load(ins)
+
+ validator_class = jsonschema.validators.validator_for(schema)
+ validator = validator_class(schema, resolver=resolver)
+
+ has_error = 0
+ for error in validator.iter_errors(document):
+ outs.write("=" * 76)
+ outs.write("\n")
+ outs.write(util.format_error("", "", util.pprint_validation_error(error)))
+ outs.write("\n")
+ has_error = 1
+
+ return has_error
+
+
+def validate_ping(ins, outs=None, schema_url=None):
+ """
+ Validates the contents of a Glean ping.
+
+ :param ins: Input stream or file path to the ping contents to validate
+ :param outs: Output stream to write errors to. (Defaults to stdout)
+ :param schema_url: HTTP URL or local filesystem path to Glean ping schema.
+ Defaults to the current version of the schema in
+ mozilla-pipeline-schemas.
+ :rtype: int 1 if any errors occurred, otherwise 0.
+ """
+ if schema_url is None:
+ raise TypeError("Missing required argument 'schema_url'")
+
+ if outs is None:
+ outs = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
+
+ if isinstance(ins, (str, bytes, Path)):
+ with open(ins, "r", encoding="utf-8") as fd:
+ return _validate_ping(fd, outs, schema_url=schema_url)
+ else:
+ return _validate_ping(ins, outs, schema_url=schema_url)