glean-parser 17.2.0__tar.gz → 18.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. {glean_parser-17.2.0 → glean_parser-18.0.0}/PKG-INFO +3 -4
  2. {glean_parser-17.2.0 → glean_parser-18.0.0}/README.md +1 -1
  3. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/__main__.py +8 -1
  4. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/go_server.py +1 -3
  5. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/lint.py +76 -2
  6. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/markdown.py +2 -2
  7. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/metrics.py +31 -2
  8. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/pings.py +3 -1
  9. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/schemas/metrics.2-0-0.schema.yaml +0 -9
  10. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/schemas/pings.2-0-0.schema.yaml +0 -4
  11. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/kotlin.jinja2 +44 -27
  12. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/rust.jinja2 +16 -1
  13. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/swift.jinja2 +40 -6
  14. {glean_parser-17.2.0 → glean_parser-18.0.0}/pyproject.toml +1 -2
  15. glean_parser-18.0.0/tests/data/events_data_sensitivity.yaml +78 -0
  16. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/object.yaml +26 -0
  17. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_go_server.py +15 -15
  18. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_javascript_server.py +6 -6
  19. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_kotlin.py +5 -3
  20. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_lint.py +231 -0
  21. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_parser.py +78 -2
  22. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_python_server.py +3 -3
  23. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_ruby_server.py +3 -3
  24. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_rust_server.py +29 -22
  25. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_swift.py +9 -6
  26. {glean_parser-17.2.0 → glean_parser-18.0.0}/.gitignore +0 -0
  27. {glean_parser-17.2.0 → glean_parser-18.0.0}/AUTHORS.md +0 -0
  28. {glean_parser-17.2.0 → glean_parser-18.0.0}/LICENSE +0 -0
  29. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/__init__.py +0 -0
  30. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/coverage.py +0 -0
  31. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/data_review.py +0 -0
  32. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/javascript.py +0 -0
  33. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/javascript_server.py +0 -0
  34. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/kotlin.py +0 -0
  35. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/parser.py +0 -0
  36. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/python_server.py +0 -0
  37. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/ruby_server.py +0 -0
  38. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/rust.py +0 -0
  39. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/rust_server.py +0 -0
  40. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/schemas/metrics.1-0-0.schema.yaml +0 -0
  41. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/schemas/pings.1-0-0.schema.yaml +0 -0
  42. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/schemas/tags.1-0-0.schema.yaml +0 -0
  43. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/swift.py +0 -0
  44. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/tags.py +0 -0
  45. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/data_review.jinja2 +0 -0
  46. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/go_server.jinja2 +0 -0
  47. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/javascript.buildinfo.jinja2 +0 -0
  48. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/javascript.jinja2 +0 -0
  49. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/javascript_server.jinja2 +0 -0
  50. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/kotlin.buildinfo.jinja2 +0 -0
  51. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/markdown.jinja2 +0 -0
  52. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/python_server.jinja2 +0 -0
  53. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/qmldir.jinja2 +0 -0
  54. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/ruby_server.jinja2 +0 -0
  55. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/templates/rust_server.jinja2 +0 -0
  56. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/translate.py +0 -0
  57. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/translation_options.py +0 -0
  58. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/util.py +0 -0
  59. {glean_parser-17.2.0 → glean_parser-18.0.0}/glean_parser/validate_ping.py +0 -0
  60. {glean_parser-17.2.0 → glean_parser-18.0.0}/server_telemetry/sdk-metrics-compat.yaml +0 -0
  61. {glean_parser-17.2.0 → glean_parser-18.0.0}/server_telemetry/server-side-pings.yaml +0 -0
  62. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/conftest.py +0 -0
  63. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/all_metrics.yaml +0 -0
  64. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/all_pings.yaml +0 -0
  65. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/attribution.yaml +0 -0
  66. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/bad_attribution.yamlx +0 -0
  67. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/bad_ping.yamlx +0 -0
  68. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/core.yaml +0 -0
  69. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/dual_labeled.yaml +0 -0
  70. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/dual_labeled_invalid.yaml +0 -0
  71. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/duplicate_labeled.yaml +0 -0
  72. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/duplicate_send_in_ping.yaml +0 -0
  73. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/empty.yaml +0 -0
  74. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/event_key_ordering.yaml +0 -0
  75. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/events_with_types.yaml +0 -0
  76. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/fxa-server-metrics.yaml +0 -0
  77. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/fxa-server-pings.yaml +0 -0
  78. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/go_server_custom_ping_only_metrics.yaml +0 -0
  79. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/go_server_custom_ping_only_pings.yaml +0 -0
  80. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/go_server_events_and_custom_ping_metrics.yaml +0 -0
  81. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/go_server_events_and_custom_ping_pings.yaml +0 -0
  82. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/go_server_events_only_metrics.yaml +0 -0
  83. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/go_server_metrics_unsupported.yaml +0 -0
  84. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/invalid-ping-names.yaml +0 -0
  85. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/invalid.yamlx +0 -0
  86. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/jwe.yaml +0 -0
  87. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/metric-with-tags.yaml +0 -0
  88. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/mixed-expirations.yaml +0 -0
  89. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/name_too_similar.yaml +0 -0
  90. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/old_event_api.yamlx +0 -0
  91. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/ordering.yaml +0 -0
  92. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/pings.yaml +0 -0
  93. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/rate.yaml +0 -0
  94. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/redefined_category.yamlx +0 -0
  95. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/redefined_metric.yamlx +0 -0
  96. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/redefined_ping.yamlx +0 -0
  97. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/reserved_categories.yamlx +0 -0
  98. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/ruby_server_metrics_unsupported.yaml +0 -0
  99. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/ruby_server_pings_unsupported.yaml +0 -0
  100. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/rust_server_custom_ping_only_metrics.yaml +0 -0
  101. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/rust_server_custom_ping_only_pings.yaml +0 -0
  102. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/rust_server_events_and_custom_ping_metrics.yaml +0 -0
  103. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/rust_server_events_and_custom_ping_pings.yaml +0 -0
  104. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/rust_server_events_only_metrics.yaml +0 -0
  105. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/rust_server_metrics_unsupported.yaml +0 -0
  106. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/same_name_different_category.yaml +0 -0
  107. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/schema-violation.yaml +0 -0
  108. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/send_if_empty_with_metrics.yaml +0 -0
  109. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_custom_ping_only_compare.go +0 -0
  110. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_custom_ping_only_compare.rs +0 -0
  111. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_events_and_custom_ping_compare.go +0 -0
  112. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_events_and_custom_ping_compare.rs +0 -0
  113. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_events_compare.rb +0 -0
  114. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_events_only_compare.go +0 -0
  115. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_events_only_compare.rs +0 -0
  116. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_metrics_no_events_no_pings.yaml +0 -0
  117. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_metrics_with_event.yaml +0 -0
  118. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/server_pings.yaml +0 -0
  119. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/single_labeled.yaml +0 -0
  120. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/smaller.yaml +0 -0
  121. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/tags.yaml +0 -0
  122. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/telemetry_mirror.yaml +0 -0
  123. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/text.yaml +0 -0
  124. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/text_invalid.yaml +0 -0
  125. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/unknown_ping_used.yaml +0 -0
  126. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/wrong_key.yamlx +0 -0
  127. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/data/yaml_nits.yamlx +0 -0
  128. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/detekt.yml +0 -0
  129. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test-go/test.go.tmpl +0 -0
  130. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test-js/package.json +0 -0
  131. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test-js/test.js.tmpl +0 -0
  132. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test-py/test.py +0 -0
  133. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test-rb/test.rb.tmpl +0 -0
  134. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test-rs/test.rs.tmpl +0 -0
  135. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_cli.py +0 -0
  136. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_javascript.py +0 -0
  137. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_markdown.py +0 -0
  138. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_metrics.py +0 -0
  139. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_pings.py +0 -0
  140. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_rust.py +0 -0
  141. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_tags.py +0 -0
  142. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_translate.py +0 -0
  143. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_utils.py +0 -0
  144. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/test_validate_ping.py +0 -0
  145. {glean_parser-17.2.0 → glean_parser-18.0.0}/tests/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glean-parser
3
- Version: 17.2.0
3
+ Version: 18.0.0
4
4
  Summary: Parser tools for Mozilla's Glean telemetry
5
5
  Project-URL: Homepage, https://mozilla.github.io/glean
6
6
  Project-URL: Repository, https://github.com/mozilla/glean_parser
@@ -12,13 +12,12 @@ Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Natural Language :: English
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
21
- Requires-Python: >=3.8
20
+ Requires-Python: >=3.9
22
21
  Requires-Dist: click>=7
23
22
  Requires-Dist: diskcache>=4
24
23
  Requires-Dist: jinja2>=2.10.1
@@ -45,7 +44,7 @@ code for various integrations, linting and coverage testing.
45
44
 
46
45
  ## Requirements
47
46
 
48
- - Python 3.8 (or later)
47
+ - Python 3.9 (or later)
49
48
 
50
49
  ## Usage
51
50
 
@@ -16,7 +16,7 @@ code for various integrations, linting and coverage testing.
16
16
 
17
17
  ## Requirements
18
18
 
19
- - Python 3.8 (or later)
19
+ - Python 3.9 (or later)
20
20
 
21
21
  ## Usage
22
22
 
@@ -166,7 +166,13 @@ def check(schema):
166
166
  is_flag=True,
167
167
  help=("Require tags to be specified for metrics and pings."),
168
168
  )
169
- def glinter(input, allow_reserved, allow_missing_files, require_tags):
169
+ @click.option(
170
+ "--expire-by-version",
171
+ help="Expire metrics by version, with the provided major version.",
172
+ type=click.INT,
173
+ required=False,
174
+ )
175
+ def glinter(input, allow_reserved, allow_missing_files, require_tags, expire_by_version):
170
176
  """
171
177
  Runs a linter over the metrics.
172
178
  """
@@ -177,6 +183,7 @@ def glinter(input, allow_reserved, allow_missing_files, require_tags):
177
183
  "allow_reserved": allow_reserved,
178
184
  "allow_missing_files": allow_missing_files,
179
185
  "require_tags": require_tags,
186
+ "expire_by_version": expire_by_version,
180
187
  },
181
188
  )
182
189
  )
@@ -147,8 +147,6 @@ def output_go(
147
147
  with filepath.open("w", encoding="utf-8") as fd:
148
148
  fd.write(
149
149
  template.render(
150
- parser_version=__version__,
151
- pings=ping_to_metrics,
152
- events=event_metrics
150
+ parser_version=__version__, pings=ping_to_metrics, events=event_metrics
153
151
  )
154
152
  )
@@ -34,6 +34,12 @@ LintGenerator = Generator[str, None, None]
34
34
  NitGenerator = Generator["GlinterNit", None, None]
35
35
 
36
36
 
37
+ def noop(*args):
38
+ """ A noop `LintGenerator`. Never yields a GlinterNit."""
39
+ return
40
+ yield
41
+
42
+
37
43
  class CheckType(enum.Enum):
38
44
  warning = 0
39
45
  error = 1
@@ -100,7 +106,7 @@ def check_common_prefix(
100
106
  common_prefix = "_".join(first[:i])
101
107
  yield (
102
108
  f"Within category '{category_name}', all metrics begin with "
103
- f"prefix '{common_prefix}'."
109
+ f"prefix '{common_prefix}'. "
104
110
  "Remove the prefixes on the metric names and (possibly) "
105
111
  "rename the category."
106
112
  )
@@ -313,13 +319,43 @@ def check_unexpected_unit(
313
319
  def check_empty_datareview(
314
320
  metric: metrics.Metric, parser_config: Dict[str, Any]
315
321
  ) -> LintGenerator:
316
- disallowed_datareview = ["", "todo"]
322
+ disallowed_datareview = ["", "todo", "tbd"]
317
323
  data_reviews = [dr.lower() in disallowed_datareview for dr in metric.data_reviews]
318
324
 
319
325
  if any(data_reviews):
320
326
  yield "List of data reviews should not contain empty strings or TODO markers."
321
327
 
322
328
 
329
+ def check_event_extras_potential_data_sensitivity_required(
330
+ metric: metrics.Metric, parser_config: Dict[str, Any]
331
+ ) -> LintGenerator:
332
+ # Only looking at event metrics
333
+ if not isinstance(metric, metrics.Event):
334
+ return
335
+
336
+ # TODO(bug 1890648): Not all metrics have `data_sensitivity` defined.
337
+ has_data_sensitivity = hasattr(metric, "data_sensitivity")
338
+ # If already marked as "highly sensitive" no need for further checks
339
+ if has_data_sensitivity and any(
340
+ [
341
+ sensitivity == metrics.DataSensitivity.highly_sensitive
342
+ for sensitivity in metric.data_sensitivity
343
+ ]
344
+ ):
345
+ return
346
+
347
+ # List of potentially sensitive extra key names we want to flag.
348
+ potential_sensitive_names = ["url", "uri"]
349
+ name_list = ", ".join(potential_sensitive_names)
350
+
351
+ for extra_key in metric.extra_keys.keys():
352
+ if extra_key in potential_sensitive_names:
353
+ yield (
354
+ f"`{extra_key}` could potentially be used to collect sensitive data. Increase the metric's data sensitivity or disable the lint."
355
+ + f" (This lint applies for the following extra key names: {name_list})"
356
+ )
357
+
358
+
323
359
  def check_redundant_ping(
324
360
  pings: pings.Ping, parser_config: Dict[str, Any]
325
361
  ) -> LintGenerator:
@@ -432,6 +468,12 @@ METRIC_CHECKS: Dict[
432
468
  "METRIC_ON_EVENTS_LIFETIME": (check_metric_on_events_lifetime, CheckType.error),
433
469
  "UNEXPECTED_UNIT": (check_unexpected_unit, CheckType.warning),
434
470
  "EMPTY_DATAREVIEW": (check_empty_datareview, CheckType.warning),
471
+ "HIGHER_DATA_SENSITIVITY_REQUIRED": (
472
+ check_event_extras_potential_data_sensitivity_required,
473
+ CheckType.warning,
474
+ ),
475
+ # Implemented inline, listed here so that `UNKNOWN_LINT` knows about it.
476
+ "UNUSED_NO_LINT": (noop, CheckType.warning),
435
477
  }
436
478
 
437
479
 
@@ -627,8 +669,40 @@ def lint_metrics(
627
669
  )
628
670
 
629
671
  for _metric_name, metric in sorted(list(category_metrics.items())):
672
+ check_unused_lints = "UNUSED_NO_LINT" not in metric.no_lint
673
+ check_unknown_lint = "UNKNOWN_LINT" not in metric.no_lint
674
+
675
+ if check_unknown_lint and metric.no_lint:
676
+ known_lint_names = (
677
+ set(METRIC_CHECKS.keys())
678
+ | set(ALL_OBJECT_CHECKS.keys())
679
+ | set(CATEGORY_CHECKS.keys())
680
+ )
681
+ unknown_lints = [
682
+ lint for lint in metric.no_lint if lint not in known_lint_names
683
+ ]
684
+ if unknown_lints:
685
+ nits.append(
686
+ GlinterNit(
687
+ "UNKNOWN_LINT",
688
+ ".".join([metric.category, metric.name]),
689
+ f"Metric contains unknown no_lints: {unknown_lints}. Please remove the `no_lint` entry.",
690
+ CheckType.warning,
691
+ )
692
+ )
693
+
630
694
  for check_name, (check_func, check_type) in METRIC_CHECKS.items():
631
695
  new_nits = list(check_func(metric, parser_config))
696
+ if check_unused_lints and check_name in metric.no_lint and not len(new_nits):
697
+ nits.append(
698
+ GlinterNit(
699
+ "UNUSED_NO_LINT",
700
+ ".".join([metric.category, metric.name]),
701
+ f"Metric contains a no_lint: {check_name}, but {check_name} does not apply. Please remove the `no_lint` entry.",
702
+ CheckType.warning,
703
+ )
704
+ )
705
+
632
706
  if len(new_nits):
633
707
  if check_name not in metric.no_lint:
634
708
  nits.extend(
@@ -52,9 +52,9 @@ def ping_desc(
52
52
 
53
53
  if ping_name in pings.RESERVED_PING_NAMES:
54
54
  desc = (
55
- "This is a built-in ping that is assembled out of the "
56
- "box by the Glean SDK."
55
+ "This is a built-in ping that is assembled out of the box by the Glean SDK."
57
56
  )
57
+
58
58
  elif ping_name == "all-pings":
59
59
  desc = "These metrics are sent in every ping."
60
60
  elif custom_pings_cache is not None and ping_name in custom_pings_cache:
@@ -462,8 +462,10 @@ class Object(Metric):
462
462
  self._generate_structure = self.validate_structure(structure)
463
463
  super().__init__(*args, **kwargs)
464
464
 
465
- ALLOWED_TOPLEVEL = {"type", "properties", "items"}
465
+ ALLOWED_TOPLEVEL = {"type", "properties", "items", "description", "oneOf"}
466
466
  ALLOWED_TYPES = ["object", "array", "number", "string", "boolean"]
467
+ ALLOWED_SUBTYPES = ["number", "string", "boolean"]
468
+ ALLOWED_ONEOF_FIELDS = {"description", "type"}
467
469
 
468
470
  @staticmethod
469
471
  def _validate_substructure(structure):
@@ -475,6 +477,34 @@ class Object(Metric):
475
477
  f"Found additional fields: {extra}. Only allowed: {allowed}"
476
478
  )
477
479
 
480
+ if "oneOf" in structure:
481
+ subtypes = structure.pop("oneOf")
482
+ structure["type"] = "oneof"
483
+ structure["subtypes"] = []
484
+ if not subtypes:
485
+ raise ValueError("List of types required.")
486
+
487
+ for typ in subtypes:
488
+ extra = set(typ.keys()) - Object.ALLOWED_ONEOF_FIELDS
489
+ if extra:
490
+ extra = ", ".join(extra)
491
+ allowed = ", ".join(Object.ALLOWED_ONEOF_FIELDS)
492
+ raise ValueError(
493
+ f"Found additional fields: {extra}. Only allowed: {allowed}"
494
+ )
495
+
496
+ ty = typ.get("type")
497
+ if not ty:
498
+ raise ValueError("element of `oneOf` list must contain a type")
499
+
500
+ if ty not in Object.ALLOWED_SUBTYPES:
501
+ raise ValueError(
502
+ f"invalid `type` in `oneOf` list. found: {ty}, only allowed: {Object.ALLOWED_SUBTYPES}"
503
+ )
504
+
505
+ structure["subtypes"].append(ty)
506
+ return structure
507
+
478
508
  if "type" not in structure:
479
509
  raise ValueError(
480
510
  f"missing `type` in object structure. Allowed: {Object.ALLOWED_TYPES}"
@@ -579,7 +609,6 @@ class DualLabeledCounter(Metric):
579
609
  d["categories"] = self.ordered_categories
580
610
  del d["ordered_keys"]
581
611
  del d["ordered_categories"]
582
- del d["dual_labeled"]
583
612
  return d
584
613
 
585
614
 
@@ -48,7 +48,9 @@ class Ping:
48
48
  self.metadata = metadata
49
49
  self.precise_timestamps = self.metadata.get("precise_timestamps", True)
50
50
  self.include_info_sections = self.metadata.get("include_info_sections", True)
51
- self.follows_collection_enabled = self.metadata.get("follows_collection_enabled", True)
51
+ self.follows_collection_enabled = self.metadata.get(
52
+ "follows_collection_enabled", True
53
+ )
52
54
  if enabled is None:
53
55
  enabled = True
54
56
  self.enabled = enabled
@@ -15,10 +15,6 @@ description: |
15
15
  $id: moz://mozilla.org/schemas/glean/metrics/2-0-0
16
16
 
17
17
  definitions:
18
- token:
19
- type: string
20
- pattern: "^[A-Za-z_][A-Za-z0-9_\\.]*$"
21
-
22
18
  snake_case:
23
19
  type: string
24
20
  pattern: "^[a-z_][a-z0-9_]*$"
@@ -39,11 +35,6 @@ definitions:
39
35
  type: string
40
36
  pattern: "^[a-z][a-z0-9-]{0,29}$"
41
37
 
42
- long_id:
43
- allOf:
44
- - $ref: "#/definitions/snake_case"
45
- - maxLength: 40
46
-
47
38
  short_id:
48
39
  allOf:
49
40
  - $ref: "#/definitions/snake_case"
@@ -15,10 +15,6 @@ description: |
15
15
  $id: moz://mozilla.org/schemas/glean/pings/2-0-0
16
16
 
17
17
  definitions:
18
- dotted_snake_case:
19
- type: string
20
- pattern: "^[a-z_][a-z0-9_]{0,29}(\\.[a-z_][a-z0-9_]{0,29})*$"
21
- maxLength: 40
22
18
  # Prior to version 2.0.0 of the schema, special ping names with underscores
23
19
  # were also supported.
24
20
  kebab_case:
@@ -67,8 +67,17 @@ data class {{ obj.name|Camelize }}{{ suffix }}(
67
67
  {%- endmacro -%}
68
68
 
69
69
  {%- macro generate_structure(name, struct) %}
70
- {%- if struct.type == "array" -%}
71
- @Serializable(with = {{name}}.Serializer::class)
70
+ {%- if struct.type == "oneof" -%}
71
+ sealed class {{ name }}(): ObjectSerialize {
72
+ {% for ty in struct.subtypes %}
73
+ class {{ty|Camelize}}(val inner: kotlin.{{ty|structure_type_name}}? = null) : {{ name }}() {
74
+ override fun intoSerializedObject(): kotlin.String {
75
+ return this.inner?.intoSerializedObject() ?: "null"
76
+ }
77
+ }
78
+ {% endfor %}
79
+ }
80
+ {%- elif struct.type == "array" -%}
72
81
  data class {{ name }}(var items: MutableList<{{ name }}Item> = mutableListOf()) : ObjectSerialize {
73
82
  fun add(elem: {{ name }}Item) = items.add(elem)
74
83
 
@@ -83,41 +92,54 @@ data class {{ obj.name|Camelize }}{{ suffix }}(
83
92
  fun set(index: Int, element: {{ name }}Item) = items.set(index, element)
84
93
 
85
94
  override fun intoSerializedObject(): String {
86
- return Json.encodeToString(items)
87
- }
88
-
89
- internal object Serializer : KSerializer<{{name}}> {
90
- @OptIn(kotlinx.serialization.ExperimentalSerializationApi::class)
91
- override val descriptor = listSerialDescriptor<String>()
92
-
93
- override fun deserialize(decoder: Decoder): {{ name }} {
94
- val list = decoder.decodeSerializableValue(ListSerializer(serializer<{{name}}Item>()))
95
- return {{name}}(items = list.toMutableList())
96
- }
97
-
98
- override fun serialize(encoder: Encoder, value: {{name}}) {
99
- encoder.encodeSerializableValue(ListSerializer(serializer<{{name}}Item>()), value.items)
95
+ var json = buildString {
96
+ append("[")
97
+ var first = true
98
+ for (item in items) {
99
+ if (!first) {
100
+ append(",")
101
+ }
102
+ first = false
103
+ append(item.intoSerializedObject())
104
+ }
105
+ append("]")
100
106
  }
107
+ return json
101
108
  }
102
109
  }
103
110
 
104
111
  {{ generate_structure(name ~ "Item", struct["items"]) }}
105
112
 
106
113
  {%- elif struct.type == "object" -%}
107
- @Serializable
108
114
  data class {{ name }}(
109
115
  {% for itemname, val in struct.properties.items() %}
110
116
  {% if val.type == "array" %}
111
117
  var {{itemname|camelize}}: {{ name ~ itemname|Camelize }} = {{ name ~ itemname|Camelize }}(),
112
118
  {% elif val.type == "object" %}
113
119
  var {{itemname|camelize}}: {{ name ~ "Item" ~ itemname|Camelize ~ "Object" }}? = null,
120
+ {% elif val.type == "oneof" %}
121
+ var {{itemname|camelize}}: {{ name ~ itemname|Camelize ~ "Enum" }}? = null,
114
122
  {% else %}
115
123
  var {{itemname|camelize}}: {{val.type|structure_type_name}}? = null,
116
124
  {% endif %}
117
125
  {% endfor %}
118
126
  ): ObjectSerialize {
119
127
  override fun intoSerializedObject(): String {
120
- return Json.encodeToString(this)
128
+ var data: MutableList<String> = mutableListOf()
129
+ {% for itemname, val in struct.properties.items() %}
130
+ this.{{itemname|camelize}}?.let { {{itemname|camelize}} ->
131
+ val elem = buildString {
132
+ append("\"{{itemname}}\":")
133
+ append({{itemname|camelize}}.intoSerializedObject())
134
+ }
135
+ data.add(elem)
136
+ }
137
+ {% endfor %}
138
+ return buildString {
139
+ append("{")
140
+ append(data.joinToString(","))
141
+ append("}")
142
+ }
121
143
  }
122
144
  }
123
145
 
@@ -128,6 +150,9 @@ data class {{ obj.name|Camelize }}{{ suffix }}(
128
150
  {% elif val.type == "object" %}
129
151
  {% set nested_name = name ~ "Item" ~ itemname|Camelize ~ "Object" %}
130
152
  {{ generate_structure(nested_name, val) }}
153
+ {% elif val.type == "oneof" %}
154
+ {% set nested_name = name ~ itemname|Camelize ~ "Enum" %}
155
+ {{ generate_structure(nested_name, val) }}
131
156
  {% endif %}
132
157
  {% endfor %}
133
158
 
@@ -174,15 +199,7 @@ import {{ glean_namespace }}.private.{{ obj_type }} // ktlint-disable import-ord
174
199
  import {{ glean_namespace }}.private.LabeledMetricType // ktlint-disable import-ordering
175
200
  {% endif %}
176
201
  {% if has_object_metrics %}
177
- import kotlinx.serialization.KSerializer
178
- import kotlinx.serialization.Serializable
179
- import kotlinx.serialization.builtins.ListSerializer
180
- import kotlinx.serialization.descriptors.listSerialDescriptor
181
- import kotlinx.serialization.encodeToString
182
- import kotlinx.serialization.encoding.Decoder
183
- import kotlinx.serialization.encoding.Encoder
184
- import kotlinx.serialization.json.Json
185
- import kotlinx.serialization.serializer
202
+ import {{ glean_namespace }}.private.intoSerializedObject
186
203
  {% endif %}
187
204
 
188
205
  {# HACK HACK HACK -- typealiases MUST BE top-level #}
@@ -9,7 +9,16 @@ Jinja2 template is not. Please file bugs! #}
9
9
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
10
10
 
11
11
  {%- macro generate_structure(name, struct) %}
12
- {% if struct.type == "array" %}
12
+ {%- if struct.type == "oneof" -%}
13
+ #[derive(Debug, Hash, Eq, PartialEq, Clone, ::glean::traits::__serde::Serialize, ::glean::traits::__serde::Deserialize)]
14
+ #[serde(crate = "::glean::traits::__serde")]
15
+ #[serde(untagged)]
16
+ pub enum {{ name }} {
17
+ {% for ty in struct.subtypes %}
18
+ {{ty|Camelize}}({{ty|structure_type_name}}),
19
+ {% endfor %}
20
+ }
21
+ {% elif struct.type == "array" %}
13
22
  pub type {{ name }} = Vec<{{ name }}Item>;
14
23
 
15
24
  {{ generate_structure(name ~ "Item", struct["items"]) }}
@@ -26,6 +35,9 @@ Jinja2 template is not. Please file bugs! #}
26
35
  {% elif val.type == "object" %}
27
36
  #[serde(skip_serializing_if = "Option::is_none")]
28
37
  pub {{itemname|snake_case}}: Option<{{ name ~ "Item" ~ itemname|Camelize ~ "Object" }}>,
38
+ {% elif val.type == "oneof" %}
39
+ #[serde(skip_serializing_if = "Option::is_none")]
40
+ pub {{itemname|snake_case}}: Option<{{ name ~ itemname|Camelize ~ "Enum" }}>,
29
41
  {% else %}
30
42
  #[serde(skip_serializing_if = "Option::is_none")]
31
43
  pub {{itemname|snake_case}}: Option<{{val.type|structure_type_name}}>,
@@ -40,6 +52,9 @@ Jinja2 template is not. Please file bugs! #}
40
52
  {% elif val.type == "object" %}
41
53
  {% set nested_name = name ~ "Item" ~ itemname|Camelize ~ "Object" %}
42
54
  {{ generate_structure(nested_name, val) }}
55
+ {% elif val.type == "oneof" %}
56
+ {% set nested_name = name ~ itemname|Camelize ~ "Enum" %}
57
+ {{ generate_structure(nested_name, val) }}
43
58
  {% endif %}
44
59
  {% endfor %}
45
60
 
@@ -45,7 +45,23 @@ struct {{ obj.name|Camelize }}{{ suffix }}: EventExtras {
45
45
  {% endmacro %}
46
46
 
47
47
  {%- macro generate_structure(name, struct) %}
48
- {%- if struct.type == "array" -%}
48
+ {%- if struct.type == "oneof" -%}
49
+ enum {{ name }}: Codable, Equatable, ObjectSerialize {
50
+ {% for ty in struct.subtypes %}
51
+ case {{ty}}({{ty|structure_type_name}})
52
+ {% endfor %}
53
+
54
+ func intoSerializedObject() -> String {
55
+ switch self {
56
+ {% for ty in struct.subtypes %}
57
+ case .{{ty}}(let val):
58
+ return val.intoSerializedObject()
59
+ {% endfor %}
60
+ }
61
+ }
62
+ }
63
+
64
+ {%- elif struct.type == "array" -%}
49
65
  typealias {{ name }} = [{{ name }}Item]
50
66
 
51
67
  {{ generate_structure(name ~ "Item", struct["items"]) }}
@@ -57,15 +73,30 @@ struct {{ obj.name|Camelize }}{{ suffix }}: EventExtras {
57
73
  var {{itemname|camelize|variable_name}}: {{ name ~ itemname|Camelize }} = []
58
74
  {% elif val.type == "object" %}
59
75
  var {{itemname|camelize|variable_name}}: {{ name ~ "Item" ~ itemname|Camelize ~ "Object" }}?
76
+ {% elif val.type == "oneof" %}
77
+ var {{itemname|camelize|variable_name}}: {{ name ~ itemname|Camelize ~ "Enum" }}?
60
78
  {% else %}
61
79
  var {{itemname|camelize|variable_name}}: {{val.type|structure_type_name}}?
62
80
  {% endif %}
63
81
  {% endfor %}
64
82
 
65
83
  func intoSerializedObject() -> String {
66
- let jsonEncoder = JSONEncoder()
67
- let jsonData = try! jsonEncoder.encode(self)
68
- let json = String(data: jsonData, encoding: String.Encoding.utf8)!
84
+ var data = [String]()
85
+ {% for itemname, val in struct.properties.items() %}
86
+ {% if val.type == "array" %}
87
+ if {{itemname|camelize|variable_name}}.count > 0 {
88
+ let {{itemname|camelize|variable_name}} = self.{{itemname|camelize|variable_name}}
89
+ {% else %}
90
+ if let {{itemname}} = self.{{itemname}} {
91
+ {% endif %}
92
+ var elem = "\"{{itemname}}\":"
93
+ elem.append({{itemname|camelize|variable_name}}.intoSerializedObject())
94
+ data.append(elem)
95
+ }
96
+ {% endfor %}
97
+ var json = "{"
98
+ json.append(data.joined(separator: ","))
99
+ json.append("}")
69
100
  return json
70
101
  }
71
102
  }
@@ -77,6 +108,9 @@ struct {{ obj.name|Camelize }}{{ suffix }}: EventExtras {
77
108
  {% elif val.type == "object" %}
78
109
  {% set nested_name = name ~ "Item" ~ itemname|Camelize ~ "Object" %}
79
110
  {{ generate_structure(nested_name, val) }}
111
+ {% elif val.type == "oneof" %}
112
+ {% set nested_name = name ~ itemname|Camelize ~ "Enum" %}
113
+ {{ generate_structure(nested_name, val) }}
80
114
  {% endif %}
81
115
  {% endfor %}
82
116
 
@@ -111,7 +145,7 @@ extension {{ namespace }} {
111
145
 
112
146
  {% for category in categories %}
113
147
  {% if category.contains_pings %}
114
- class {{ category.name|Camelize }} {
148
+ final class {{ category.name|Camelize }}: Sendable {
115
149
  public static let shared = {{ category.name|Camelize }}()
116
150
  private init() {
117
151
  // Intentionally left private, no external user can instantiate a new global object.
@@ -121,7 +155,7 @@ extension {{ namespace }} {
121
155
  {% if obj|attr("_generate_enums") %}
122
156
  {% for name, suffix in obj["_generate_enums"] %}
123
157
  {% if obj|attr(name)|length %}
124
- enum {{ obj.name|Camelize }}{{ suffix }}: Int, ReasonCodes {
158
+ enum {{ obj.name|Camelize }}{{ suffix }}: Int, ReasonCodes, Sendable {
125
159
  {% for key in obj|attr(name) %}
126
160
  case {{ key|camelize|variable_name }} = {{ loop.index-1 }}
127
161
  {% endfor %}
@@ -11,14 +11,13 @@ classifiers = [
11
11
  "Intended Audience :: Developers",
12
12
  "Natural Language :: English",
13
13
  "Programming Language :: Python :: 3",
14
- "Programming Language :: Python :: 3.8",
15
14
  "Programming Language :: Python :: 3.9",
16
15
  "Programming Language :: Python :: 3.10",
17
16
  "Programming Language :: Python :: 3.11",
18
17
  "Programming Language :: Python :: 3.12",
19
18
  "Programming Language :: Python :: 3.13",
20
19
  ]
21
- requires-python = ">=3.8"
20
+ requires-python = ">=3.9"
22
21
  dependencies = [
23
22
  "Click>=7",
24
23
  "diskcache>=4",
@@ -0,0 +1,78 @@
1
+ # Any copyright is dedicated to the Public Domain.
2
+ # https://creativecommons.org/publicdomain/zero/1.0/
3
+
4
+ ---
5
+ $schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
6
+
7
+ event:
8
+ low_sensitivity:
9
+ type: event
10
+ description: |
11
+ Just testing events
12
+ bugs:
13
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1973017
14
+ data_reviews:
15
+ - http://example.com/reviews
16
+ notification_emails:
17
+ - CHANGE-ME@example.com
18
+ extra_keys:
19
+ url:
20
+ type: string
21
+ description: "This is key one"
22
+ expires: never
23
+ data_sensitivity:
24
+ - technical
25
+
26
+ no_data_sensitivity:
27
+ type: event
28
+ description: |
29
+ Just testing events
30
+ bugs:
31
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1973017
32
+ data_reviews:
33
+ - http://example.com/reviews
34
+ notification_emails:
35
+ - CHANGE-ME@example.com
36
+ extra_keys:
37
+ url:
38
+ type: string
39
+ description: "This is key one"
40
+ expires: never
41
+
42
+ correct_sensitivity:
43
+ type: event
44
+ description: |
45
+ Just testing events
46
+ bugs:
47
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1973017
48
+ data_reviews:
49
+ - http://example.com/reviews
50
+ notification_emails:
51
+ - CHANGE-ME@example.com
52
+ extra_keys:
53
+ url:
54
+ type: string
55
+ description: "This is key one"
56
+ expires: never
57
+ data_sensitivity:
58
+ - highly_sensitive
59
+
60
+ exempt:
61
+ type: event
62
+ description: |
63
+ Just testing events
64
+ bugs:
65
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1973017
66
+ data_reviews:
67
+ - http://example.com/reviews
68
+ notification_emails:
69
+ - CHANGE-ME@example.com
70
+ extra_keys:
71
+ url:
72
+ type: string
73
+ description: "This is key one"
74
+ expires: never
75
+ data_sensitivity:
76
+ - technical
77
+ no_lint:
78
+ - HIGHER_DATA_SENSITIVITY_REQUIRED