mozilla_schema_generator.glean_ping

  1# -*- coding: utf-8 -*-
  2
  3# This Source Code Form is subject to the terms of the Mozilla Public
  4# License, v. 2.0. If a copy of the MPL was not distributed with this
  5# file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7import logging
  8from pathlib import Path
  9from typing import Dict, List, Set
 10
 11from requests import HTTPError
 12
 13from .config import Config
 14from .generic_ping import GenericPing
 15from .probes import GleanProbe
 16from .schema import Schema
 17
 18ROOT_DIR = Path(__file__).parent
 19BUG_1737656_TXT = ROOT_DIR / "configs" / "bug_1737656_affected.txt"
 20
 21logger = logging.getLogger(__name__)
 22
 23DEFAULT_SCHEMA_URL = (
 24    "https://raw.githubusercontent.com/mozilla-services/mozilla-pipeline-schemas"
 25    "/{branch}/schemas/glean/glean/glean.1.schema.json"
 26)
 27
 28MINIMUM_SCHEMA_URL = (
 29    "https://raw.githubusercontent.com/mozilla-services/mozilla-pipeline-schemas"
 30    "/{branch}/schemas/glean/glean/glean-min.1.schema.json"
 31)
 32
 33
 34class GleanPing(GenericPing):
 35    probes_url_template = GenericPing.probe_info_base_url + "/glean/{}/metrics"
 36    ping_url_template = GenericPing.probe_info_base_url + "/glean/{}/pings"
 37    repos_url = GenericPing.probe_info_base_url + "/glean/repositories"
 38    dependencies_url_template = (
 39        GenericPing.probe_info_base_url + "/glean/{}/dependencies"
 40    )
 41
 42    default_dependencies = ["glean-core"]
 43
 44    with open(BUG_1737656_TXT, "r") as f:
 45        bug_1737656_affected_tables = [
 46            line.strip() for line in f.readlines() if line.strip()
 47        ]
 48
 49    def __init__(self, repo, **kwargs):  # TODO: Make env-url optional
 50        self.repo = repo
 51        self.repo_name = repo["name"]
 52        self.app_id = repo["app_id"]
 53        super().__init__(
 54            DEFAULT_SCHEMA_URL,
 55            DEFAULT_SCHEMA_URL,
 56            self.probes_url_template.format(self.repo_name),
 57            **kwargs,
 58        )
 59
 60    def get_schema(self, generic_schema=False) -> Schema:
 61        """
 62        Fetch schema via URL.
 63
 64        Unless *generic_schema* is set to true, this function makes some modifications
 65        to allow some workarounds for proper injection of metrics.
 66        """
 67        schema = super().get_schema()
 68        if generic_schema:
 69            return schema
 70
 71        # We need to inject placeholders for the url2, text2, etc. types as part
 72        # of mitigation for https://bugzilla.mozilla.org/show_bug.cgi?id=1737656
 73        for metric_name in ["labeled_rate", "jwe", "url", "text"]:
 74            metric1 = schema.get(
 75                ("properties", "metrics", "properties", metric_name)
 76            ).copy()
 77            metric1 = schema.set_schema_elem(
 78                ("properties", "metrics", "properties", metric_name + "2"),
 79                metric1,
 80            )
 81
 82        return schema
 83
 84    def get_dependencies(self):
 85        # Get all of the library dependencies for the application that
 86        # are also known about in the repositories file.
 87
 88        # The dependencies are specified using library names, but we need to
 89        # map those back to the name of the repository in the repository file.
 90        try:
 91            dependencies = self._get_json(
 92                self.dependencies_url_template.format(self.repo_name)
 93            )
 94        except HTTPError:
 95            logging.info(f"For {self.repo_name}, using default Glean dependencies")
 96            return self.default_dependencies
 97
 98        dependency_library_names = list(dependencies.keys())
 99
100        repos = GleanPing._get_json(GleanPing.repos_url)
101        repos_by_dependency_name = {}
102        for repo in repos:
103            for library_name in repo.get("library_names", []):
104                repos_by_dependency_name[library_name] = repo["name"]
105
106        dependencies = []
107        for name in dependency_library_names:
108            if name in repos_by_dependency_name:
109                dependencies.append(repos_by_dependency_name[name])
110
111        if len(dependencies) == 0:
112            logging.info(f"For {self.repo_name}, using default Glean dependencies")
113            return self.default_dependencies
114
115        logging.info(f"For {self.repo_name}, found Glean dependencies: {dependencies}")
116        return dependencies
117
118    def get_probes(self) -> List[GleanProbe]:
119        data = self._get_json(self.probes_url)
120        probes = list(data.items())
121
122        for dependency in self.get_dependencies():
123            dependency_probes = self._get_json(
124                self.probes_url_template.format(dependency)
125            )
126            probes += list(dependency_probes.items())
127
128        pings = self.get_pings()
129
130        processed = []
131        for _id, defn in probes:
132            probe = GleanProbe(_id, defn, pings=pings)
133            processed.append(probe)
134
135            # Manual handling of incompatible schema changes
136            issue_118_affected = {
137                "fenix",
138                "fenix-nightly",
139                "firefox-android-nightly",
140                "firefox-android-beta",
141                "firefox-android-release",
142            }
143            if (
144                self.repo_name in issue_118_affected
145                and probe.get_name() == "installation.timestamp"
146            ):
147                logging.info(f"Writing column {probe.get_name()} for compatibility.")
148                # See: https://github.com/mozilla/mozilla-schema-generator/issues/118
149                # Search through history for the "string" type and add a copy of
150                # the probe at that time in history. The changepoint signifies
151                # this event.
152                changepoint_index = 0
153                for definition in probe.definition_history:
154                    if definition["type"] != probe.get_type():
155                        break
156                    changepoint_index += 1
157                # Modify the definition with the truncated history.
158                hist_defn = defn.copy()
159                hist_defn[probe.history_key] = probe.definition_history[
160                    changepoint_index:
161                ]
162                hist_defn["type"] = hist_defn[probe.history_key][0]["type"]
163                incompatible_probe_type = GleanProbe(_id, hist_defn, pings=pings)
164                processed.append(incompatible_probe_type)
165
166            # Handling probe type changes (Bug 1870317)
167            probe_types = {hist["type"] for hist in defn[probe.history_key]}
168            if len(probe_types) > 1:
169                # The probe type changed at some point in history.
170                # Create schema entry for each type.
171                hist_defn = defn.copy()
172
173                # No new entry needs to be created for the current probe type
174                probe_types.remove(defn["type"])
175
176                for hist in hist_defn[probe.history_key]:
177                    # Create a new entry for a historic type
178                    if hist["type"] in probe_types:
179                        hist_defn["type"] = hist["type"]
180                        probe = GleanProbe(_id, hist_defn, pings=pings)
181                        processed.append(probe)
182
183                        # Keep track of the types entries were already created for
184                        probe_types.remove(hist["type"])
185
186        return processed
187
188    def _get_ping_data(self) -> Dict[str, Dict]:
189        url = self.ping_url_template.format(self.repo_name)
190        ping_data = GleanPing._get_json(url)
191        for dependency in self.get_dependencies():
192            dependency_pings = self._get_json(self.ping_url_template.format(dependency))
193            ping_data.update(dependency_pings)
194        return ping_data
195
196    def _get_ping_data_without_dependencies(self) -> Dict[str, Dict]:
197        url = self.ping_url_template.format(self.repo_name)
198        ping_data = GleanPing._get_json(url)
199        return ping_data
200
201    def _get_dependency_pings(self, dependency):
202        return self._get_json(self.ping_url_template.format(dependency))
203
204    def get_pings(self) -> Set[str]:
205        return self._get_ping_data().keys()
206
207    @staticmethod
208    def apply_default_metadata(ping_metadata, default_metadata):
209        """apply_default_metadata recurses down into dicts nested
210        to an arbitrary depth, updating keys. The ``default_metadata`` is merged into
211        ``ping_metadata``.
212        :param ping_metadata: dict onto which the merge is executed
213        :param default_metadata: dct merged into ping_metadata
214        :return: None
215        """
216        for k, v in default_metadata.items():
217            if (
218                k in ping_metadata
219                and isinstance(ping_metadata[k], dict)
220                and isinstance(default_metadata[k], dict)
221            ):
222                GleanPing.apply_default_metadata(ping_metadata[k], default_metadata[k])
223            else:
224                ping_metadata[k] = default_metadata[k]
225
226    def _get_ping_data_and_dependencies_with_default_metadata(self) -> Dict[str, Dict]:
227        # Get the ping data with the pipeline metadata
228        ping_data = self._get_ping_data_without_dependencies()
229
230        # The ping endpoint for the dependency pings does not include any repo defined
231        # moz_pipeline_metadata_defaults so they need to be applied here.
232
233        # 1.  Get repo and pipeline default metadata.
234        repos = GleanPing.get_repos()
235        current_repo = next((x for x in repos if x.get("app_id") == self.app_id), {})
236        default_metadata = current_repo.get("moz_pipeline_metadata_defaults", {})
237
238        # 2.  Apply the default metadata to each dependency defined ping.
239        for dependency in self.get_dependencies():
240            dependency_pings = self._get_dependency_pings(dependency)
241            for dependency_ping in dependency_pings.values():
242                # Although it is counter intuitive to apply the default metadata on top of the
243                # existing dependency ping metadata it does set the repo specific value for
244                # bq_dataset_family instead of using the dependency id for the bq_dataset_family
245                # value.
246                GleanPing.apply_default_metadata(
247                    dependency_ping.get("moz_pipeline_metadata"), default_metadata
248                )
249            ping_data.update(dependency_pings)
250        return ping_data
251
252    @staticmethod
253    def reorder_metadata(metadata):
254        desired_order_list = [
255            "bq_dataset_family",
256            "bq_table",
257            "bq_metadata_format",
258            "include_info_sections",
259            "submission_timestamp_granularity",
260            "expiration_policy",
261            "override_attributes",
262            "jwe_mappings",
263        ]
264        reordered_metadata = {
265            k: metadata[k] for k in desired_order_list if k in metadata
266        }
267
268        # re-order jwe-mappings
269        desired_order_list = ["source_field_path", "decrypted_field_path"]
270        jwe_mapping_metadata = reordered_metadata.get("jwe_mappings")
271        if jwe_mapping_metadata:
272            reordered_jwe_mapping_metadata = []
273            for mapping in jwe_mapping_metadata:
274                reordered_jwe_mapping_metadata.append(
275                    {k: mapping[k] for k in desired_order_list if k in mapping}
276                )
277            reordered_metadata["jwe_mappings"] = reordered_jwe_mapping_metadata
278
279        # future proofing, in case there are other fields added at the ping top level
280        # add them to the end.
281        leftovers = {k: metadata[k] for k in set(metadata) - set(reordered_metadata)}
282        reordered_metadata = {**reordered_metadata, **leftovers}
283        return reordered_metadata
284
285    def get_pings_and_pipeline_metadata(self) -> Dict[str, Dict]:
286        pings = self._get_ping_data_and_dependencies_with_default_metadata()
287        for ping_name, ping_data in pings.items():
288            metadata = ping_data.get("moz_pipeline_metadata")
289            metadata["include_info_sections"] = self._include_info_sections(ping_data)
290
291            # While technically unnecessary, the dictionary elements are re-ordered to match the
292            # currently deployed order and used to verify no difference in output.
293            pings[ping_name] = GleanPing.reorder_metadata(metadata)
294        return pings
295
296    def get_ping_descriptions(self) -> Dict[str, str]:
297        return {
298            k: v["history"][-1]["description"] for k, v in self._get_ping_data().items()
299        }
300
301    def _include_info_sections(self, ping_data) -> bool:
302        # Default to true if not specified.
303        if "history" not in ping_data or len(ping_data["history"]) == 0:
304            return True
305        latest_ping_data = ping_data["history"][-1]
306        return (
307            "include_info_sections" not in latest_ping_data
308            or latest_ping_data["include_info_sections"]
309        )
310
311    def set_schema_url(self, metadata):
312        """
313        Switch between the glean-min and glean schemas if the ping does not require
314        info sections as specified in the parsed ping info in probe scraper.
315        """
316        if not metadata["include_info_sections"]:
317            self.schema_url = MINIMUM_SCHEMA_URL.format(branch=self.branch_name)
318        else:
319            self.schema_url = DEFAULT_SCHEMA_URL.format(branch=self.branch_name)
320
321    def generate_schema(self, config, generic_schema=False) -> Dict[str, Schema]:
322        pings = self.get_pings_and_pipeline_metadata()
323        schemas = {}
324
325        for ping, pipeline_meta in pings.items():
326            matchers = {
327                loc: m.clone(new_table_group=ping) for loc, m in config.matchers.items()
328            }
329
330            # Four newly introduced metric types were incorrectly deployed
331            # as repeated key/value structs in all Glean ping tables existing prior
332            # to November 2021. We maintain the incorrect fields for existing tables
333            # by disabling the associated matchers.
334            # Note that each of these types now has a "2" matcher ("text2", "url2", etc.)
335            # defined that will allow metrics of these types to be injected into proper
336            # structs. The gcp-ingestion repository includes logic to rewrite these
337            # metrics under the "2" names.
338            # See https://bugzilla.mozilla.org/show_bug.cgi?id=1737656
339            bq_identifier = "{bq_dataset_family}.{bq_table}".format(**pipeline_meta)
340            if bq_identifier in self.bug_1737656_affected_tables:
341                matchers = {
342                    loc: m
343                    for loc, m in matchers.items()
344                    if not m.matcher.get("bug_1737656_affected")
345                }
346
347            for matcher in matchers.values():
348                matcher.matcher["send_in_pings"]["contains"] = ping
349            new_config = Config(ping, matchers=matchers)
350
351            defaults = {"mozPipelineMetadata": pipeline_meta}
352
353            # Adjust the schema path if the ping does not require info sections
354            self.set_schema_url(pipeline_meta)
355            if generic_schema:  # Use the generic glean ping schema
356                schema = self.get_schema(generic_schema=True)
357                schema.schema.update(defaults)
358                schemas[new_config.name] = schema
359            else:
360                generated = super().generate_schema(new_config)
361                for schema in generated.values():
362                    # We want to override each individual key with assembled defaults,
363                    # but keep values _inside_ them if they have been set in the schemas.
364                    for key, value in defaults.items():
365                        if key not in schema.schema:
366                            schema.schema[key] = {}
367                        schema.schema[key].update(value)
368                schemas.update(generated)
369
370        return schemas
371
372    @staticmethod
373    def get_repos():
374        """
375        Retrieve metadata for all non-library Glean repositories
376        """
377        repos = GleanPing._get_json(GleanPing.repos_url)
378        return [repo for repo in repos if "library_names" not in repo]
ROOT_DIR = PosixPath('/home/circleci/project/mozilla_schema_generator')
BUG_1737656_TXT = PosixPath('/home/circleci/project/mozilla_schema_generator/configs/bug_1737656_affected.txt')
logger = <Logger mozilla_schema_generator.glean_ping (WARNING)>
DEFAULT_SCHEMA_URL = 'https://raw.githubusercontent.com/mozilla-services/mozilla-pipeline-schemas/{branch}/schemas/glean/glean/glean.1.schema.json'
MINIMUM_SCHEMA_URL = 'https://raw.githubusercontent.com/mozilla-services/mozilla-pipeline-schemas/{branch}/schemas/glean/glean/glean-min.1.schema.json'
 35class GleanPing(GenericPing):
 36    probes_url_template = GenericPing.probe_info_base_url + "/glean/{}/metrics"
 37    ping_url_template = GenericPing.probe_info_base_url + "/glean/{}/pings"
 38    repos_url = GenericPing.probe_info_base_url + "/glean/repositories"
 39    dependencies_url_template = (
 40        GenericPing.probe_info_base_url + "/glean/{}/dependencies"
 41    )
 42
 43    default_dependencies = ["glean-core"]
 44
 45    with open(BUG_1737656_TXT, "r") as f:
 46        bug_1737656_affected_tables = [
 47            line.strip() for line in f.readlines() if line.strip()
 48        ]
 49
 50    def __init__(self, repo, **kwargs):  # TODO: Make env-url optional
 51        self.repo = repo
 52        self.repo_name = repo["name"]
 53        self.app_id = repo["app_id"]
 54        super().__init__(
 55            DEFAULT_SCHEMA_URL,
 56            DEFAULT_SCHEMA_URL,
 57            self.probes_url_template.format(self.repo_name),
 58            **kwargs,
 59        )
 60
 61    def get_schema(self, generic_schema=False) -> Schema:
 62        """
 63        Fetch schema via URL.
 64
 65        Unless *generic_schema* is set to true, this function makes some modifications
 66        to allow some workarounds for proper injection of metrics.
 67        """
 68        schema = super().get_schema()
 69        if generic_schema:
 70            return schema
 71
 72        # We need to inject placeholders for the url2, text2, etc. types as part
 73        # of mitigation for https://bugzilla.mozilla.org/show_bug.cgi?id=1737656
 74        for metric_name in ["labeled_rate", "jwe", "url", "text"]:
 75            metric1 = schema.get(
 76                ("properties", "metrics", "properties", metric_name)
 77            ).copy()
 78            metric1 = schema.set_schema_elem(
 79                ("properties", "metrics", "properties", metric_name + "2"),
 80                metric1,
 81            )
 82
 83        return schema
 84
 85    def get_dependencies(self):
 86        # Get all of the library dependencies for the application that
 87        # are also known about in the repositories file.
 88
 89        # The dependencies are specified using library names, but we need to
 90        # map those back to the name of the repository in the repository file.
 91        try:
 92            dependencies = self._get_json(
 93                self.dependencies_url_template.format(self.repo_name)
 94            )
 95        except HTTPError:
 96            logging.info(f"For {self.repo_name}, using default Glean dependencies")
 97            return self.default_dependencies
 98
 99        dependency_library_names = list(dependencies.keys())
100
101        repos = GleanPing._get_json(GleanPing.repos_url)
102        repos_by_dependency_name = {}
103        for repo in repos:
104            for library_name in repo.get("library_names", []):
105                repos_by_dependency_name[library_name] = repo["name"]
106
107        dependencies = []
108        for name in dependency_library_names:
109            if name in repos_by_dependency_name:
110                dependencies.append(repos_by_dependency_name[name])
111
112        if len(dependencies) == 0:
113            logging.info(f"For {self.repo_name}, using default Glean dependencies")
114            return self.default_dependencies
115
116        logging.info(f"For {self.repo_name}, found Glean dependencies: {dependencies}")
117        return dependencies
118
119    def get_probes(self) -> List[GleanProbe]:
120        data = self._get_json(self.probes_url)
121        probes = list(data.items())
122
123        for dependency in self.get_dependencies():
124            dependency_probes = self._get_json(
125                self.probes_url_template.format(dependency)
126            )
127            probes += list(dependency_probes.items())
128
129        pings = self.get_pings()
130
131        processed = []
132        for _id, defn in probes:
133            probe = GleanProbe(_id, defn, pings=pings)
134            processed.append(probe)
135
136            # Manual handling of incompatible schema changes
137            issue_118_affected = {
138                "fenix",
139                "fenix-nightly",
140                "firefox-android-nightly",
141                "firefox-android-beta",
142                "firefox-android-release",
143            }
144            if (
145                self.repo_name in issue_118_affected
146                and probe.get_name() == "installation.timestamp"
147            ):
148                logging.info(f"Writing column {probe.get_name()} for compatibility.")
149                # See: https://github.com/mozilla/mozilla-schema-generator/issues/118
150                # Search through history for the "string" type and add a copy of
151                # the probe at that time in history. The changepoint signifies
152                # this event.
153                changepoint_index = 0
154                for definition in probe.definition_history:
155                    if definition["type"] != probe.get_type():
156                        break
157                    changepoint_index += 1
158                # Modify the definition with the truncated history.
159                hist_defn = defn.copy()
160                hist_defn[probe.history_key] = probe.definition_history[
161                    changepoint_index:
162                ]
163                hist_defn["type"] = hist_defn[probe.history_key][0]["type"]
164                incompatible_probe_type = GleanProbe(_id, hist_defn, pings=pings)
165                processed.append(incompatible_probe_type)
166
167            # Handling probe type changes (Bug 1870317)
168            probe_types = {hist["type"] for hist in defn[probe.history_key]}
169            if len(probe_types) > 1:
170                # The probe type changed at some point in history.
171                # Create schema entry for each type.
172                hist_defn = defn.copy()
173
174                # No new entry needs to be created for the current probe type
175                probe_types.remove(defn["type"])
176
177                for hist in hist_defn[probe.history_key]:
178                    # Create a new entry for a historic type
179                    if hist["type"] in probe_types:
180                        hist_defn["type"] = hist["type"]
181                        probe = GleanProbe(_id, hist_defn, pings=pings)
182                        processed.append(probe)
183
184                        # Keep track of the types entries were already created for
185                        probe_types.remove(hist["type"])
186
187        return processed
188
189    def _get_ping_data(self) -> Dict[str, Dict]:
190        url = self.ping_url_template.format(self.repo_name)
191        ping_data = GleanPing._get_json(url)
192        for dependency in self.get_dependencies():
193            dependency_pings = self._get_json(self.ping_url_template.format(dependency))
194            ping_data.update(dependency_pings)
195        return ping_data
196
197    def _get_ping_data_without_dependencies(self) -> Dict[str, Dict]:
198        url = self.ping_url_template.format(self.repo_name)
199        ping_data = GleanPing._get_json(url)
200        return ping_data
201
202    def _get_dependency_pings(self, dependency):
203        return self._get_json(self.ping_url_template.format(dependency))
204
205    def get_pings(self) -> Set[str]:
206        return self._get_ping_data().keys()
207
208    @staticmethod
209    def apply_default_metadata(ping_metadata, default_metadata):
210        """apply_default_metadata recurses down into dicts nested
211        to an arbitrary depth, updating keys. The ``default_metadata`` is merged into
212        ``ping_metadata``.
213        :param ping_metadata: dict onto which the merge is executed
214        :param default_metadata: dct merged into ping_metadata
215        :return: None
216        """
217        for k, v in default_metadata.items():
218            if (
219                k in ping_metadata
220                and isinstance(ping_metadata[k], dict)
221                and isinstance(default_metadata[k], dict)
222            ):
223                GleanPing.apply_default_metadata(ping_metadata[k], default_metadata[k])
224            else:
225                ping_metadata[k] = default_metadata[k]
226
227    def _get_ping_data_and_dependencies_with_default_metadata(self) -> Dict[str, Dict]:
228        # Get the ping data with the pipeline metadata
229        ping_data = self._get_ping_data_without_dependencies()
230
231        # The ping endpoint for the dependency pings does not include any repo defined
232        # moz_pipeline_metadata_defaults so they need to be applied here.
233
234        # 1.  Get repo and pipeline default metadata.
235        repos = GleanPing.get_repos()
236        current_repo = next((x for x in repos if x.get("app_id") == self.app_id), {})
237        default_metadata = current_repo.get("moz_pipeline_metadata_defaults", {})
238
239        # 2.  Apply the default metadata to each dependency defined ping.
240        for dependency in self.get_dependencies():
241            dependency_pings = self._get_dependency_pings(dependency)
242            for dependency_ping in dependency_pings.values():
243                # Although it is counter intuitive to apply the default metadata on top of the
244                # existing dependency ping metadata it does set the repo specific value for
245                # bq_dataset_family instead of using the dependency id for the bq_dataset_family
246                # value.
247                GleanPing.apply_default_metadata(
248                    dependency_ping.get("moz_pipeline_metadata"), default_metadata
249                )
250            ping_data.update(dependency_pings)
251        return ping_data
252
253    @staticmethod
254    def reorder_metadata(metadata):
255        desired_order_list = [
256            "bq_dataset_family",
257            "bq_table",
258            "bq_metadata_format",
259            "include_info_sections",
260            "submission_timestamp_granularity",
261            "expiration_policy",
262            "override_attributes",
263            "jwe_mappings",
264        ]
265        reordered_metadata = {
266            k: metadata[k] for k in desired_order_list if k in metadata
267        }
268
269        # re-order jwe-mappings
270        desired_order_list = ["source_field_path", "decrypted_field_path"]
271        jwe_mapping_metadata = reordered_metadata.get("jwe_mappings")
272        if jwe_mapping_metadata:
273            reordered_jwe_mapping_metadata = []
274            for mapping in jwe_mapping_metadata:
275                reordered_jwe_mapping_metadata.append(
276                    {k: mapping[k] for k in desired_order_list if k in mapping}
277                )
278            reordered_metadata["jwe_mappings"] = reordered_jwe_mapping_metadata
279
280        # future proofing, in case there are other fields added at the ping top level
281        # add them to the end.
282        leftovers = {k: metadata[k] for k in set(metadata) - set(reordered_metadata)}
283        reordered_metadata = {**reordered_metadata, **leftovers}
284        return reordered_metadata
285
286    def get_pings_and_pipeline_metadata(self) -> Dict[str, Dict]:
287        pings = self._get_ping_data_and_dependencies_with_default_metadata()
288        for ping_name, ping_data in pings.items():
289            metadata = ping_data.get("moz_pipeline_metadata")
290            metadata["include_info_sections"] = self._include_info_sections(ping_data)
291
292            # While technically unnecessary, the dictionary elements are re-ordered to match the
293            # currently deployed order and used to verify no difference in output.
294            pings[ping_name] = GleanPing.reorder_metadata(metadata)
295        return pings
296
297    def get_ping_descriptions(self) -> Dict[str, str]:
298        return {
299            k: v["history"][-1]["description"] for k, v in self._get_ping_data().items()
300        }
301
302    def _include_info_sections(self, ping_data) -> bool:
303        # Default to true if not specified.
304        if "history" not in ping_data or len(ping_data["history"]) == 0:
305            return True
306        latest_ping_data = ping_data["history"][-1]
307        return (
308            "include_info_sections" not in latest_ping_data
309            or latest_ping_data["include_info_sections"]
310        )
311
312    def set_schema_url(self, metadata):
313        """
314        Switch between the glean-min and glean schemas if the ping does not require
315        info sections as specified in the parsed ping info in probe scraper.
316        """
317        if not metadata["include_info_sections"]:
318            self.schema_url = MINIMUM_SCHEMA_URL.format(branch=self.branch_name)
319        else:
320            self.schema_url = DEFAULT_SCHEMA_URL.format(branch=self.branch_name)
321
322    def generate_schema(self, config, generic_schema=False) -> Dict[str, Schema]:
323        pings = self.get_pings_and_pipeline_metadata()
324        schemas = {}
325
326        for ping, pipeline_meta in pings.items():
327            matchers = {
328                loc: m.clone(new_table_group=ping) for loc, m in config.matchers.items()
329            }
330
331            # Four newly introduced metric types were incorrectly deployed
332            # as repeated key/value structs in all Glean ping tables existing prior
333            # to November 2021. We maintain the incorrect fields for existing tables
334            # by disabling the associated matchers.
335            # Note that each of these types now has a "2" matcher ("text2", "url2", etc.)
336            # defined that will allow metrics of these types to be injected into proper
337            # structs. The gcp-ingestion repository includes logic to rewrite these
338            # metrics under the "2" names.
339            # See https://bugzilla.mozilla.org/show_bug.cgi?id=1737656
340            bq_identifier = "{bq_dataset_family}.{bq_table}".format(**pipeline_meta)
341            if bq_identifier in self.bug_1737656_affected_tables:
342                matchers = {
343                    loc: m
344                    for loc, m in matchers.items()
345                    if not m.matcher.get("bug_1737656_affected")
346                }
347
348            for matcher in matchers.values():
349                matcher.matcher["send_in_pings"]["contains"] = ping
350            new_config = Config(ping, matchers=matchers)
351
352            defaults = {"mozPipelineMetadata": pipeline_meta}
353
354            # Adjust the schema path if the ping does not require info sections
355            self.set_schema_url(pipeline_meta)
356            if generic_schema:  # Use the generic glean ping schema
357                schema = self.get_schema(generic_schema=True)
358                schema.schema.update(defaults)
359                schemas[new_config.name] = schema
360            else:
361                generated = super().generate_schema(new_config)
362                for schema in generated.values():
363                    # We want to override each individual key with assembled defaults,
364                    # but keep values _inside_ them if they have been set in the schemas.
365                    for key, value in defaults.items():
366                        if key not in schema.schema:
367                            schema.schema[key] = {}
368                        schema.schema[key].update(value)
369                schemas.update(generated)
370
371        return schemas
372
373    @staticmethod
374    def get_repos():
375        """
376        Retrieve metadata for all non-library Glean repositories
377        """
378        repos = GleanPing._get_json(GleanPing.repos_url)
379        return [repo for repo in repos if "library_names" not in repo]
GleanPing(repo, **kwargs)
50    def __init__(self, repo, **kwargs):  # TODO: Make env-url optional
51        self.repo = repo
52        self.repo_name = repo["name"]
53        self.app_id = repo["app_id"]
54        super().__init__(
55            DEFAULT_SCHEMA_URL,
56            DEFAULT_SCHEMA_URL,
57            self.probes_url_template.format(self.repo_name),
58            **kwargs,
59        )
probes_url_template = 'https://probeinfo.telemetry.mozilla.org/glean/{}/metrics'
ping_url_template = 'https://probeinfo.telemetry.mozilla.org/glean/{}/pings'
repos_url = 'https://probeinfo.telemetry.mozilla.org/glean/repositories'
dependencies_url_template = 'https://probeinfo.telemetry.mozilla.org/glean/{}/dependencies'
default_dependencies = ['glean-core']
repo
repo_name
app_id
def get_schema(self, generic_schema=False) -> mozilla_schema_generator.schema.Schema:
61    def get_schema(self, generic_schema=False) -> Schema:
62        """
63        Fetch schema via URL.
64
65        Unless *generic_schema* is set to true, this function makes some modifications
66        to allow some workarounds for proper injection of metrics.
67        """
68        schema = super().get_schema()
69        if generic_schema:
70            return schema
71
72        # We need to inject placeholders for the url2, text2, etc. types as part
73        # of mitigation for https://bugzilla.mozilla.org/show_bug.cgi?id=1737656
74        for metric_name in ["labeled_rate", "jwe", "url", "text"]:
75            metric1 = schema.get(
76                ("properties", "metrics", "properties", metric_name)
77            ).copy()
78            metric1 = schema.set_schema_elem(
79                ("properties", "metrics", "properties", metric_name + "2"),
80                metric1,
81            )
82
83        return schema

Fetch schema via URL.

Unless generic_schema is set to true, this function makes some modifications to allow some workarounds for proper injection of metrics.

def get_dependencies(self):
 85    def get_dependencies(self):
 86        # Get all of the library dependencies for the application that
 87        # are also known about in the repositories file.
 88
 89        # The dependencies are specified using library names, but we need to
 90        # map those back to the name of the repository in the repository file.
 91        try:
 92            dependencies = self._get_json(
 93                self.dependencies_url_template.format(self.repo_name)
 94            )
 95        except HTTPError:
 96            logging.info(f"For {self.repo_name}, using default Glean dependencies")
 97            return self.default_dependencies
 98
 99        dependency_library_names = list(dependencies.keys())
100
101        repos = GleanPing._get_json(GleanPing.repos_url)
102        repos_by_dependency_name = {}
103        for repo in repos:
104            for library_name in repo.get("library_names", []):
105                repos_by_dependency_name[library_name] = repo["name"]
106
107        dependencies = []
108        for name in dependency_library_names:
109            if name in repos_by_dependency_name:
110                dependencies.append(repos_by_dependency_name[name])
111
112        if len(dependencies) == 0:
113            logging.info(f"For {self.repo_name}, using default Glean dependencies")
114            return self.default_dependencies
115
116        logging.info(f"For {self.repo_name}, found Glean dependencies: {dependencies}")
117        return dependencies
def get_probes(self) -> List[mozilla_schema_generator.probes.GleanProbe]:
119    def get_probes(self) -> List[GleanProbe]:
120        data = self._get_json(self.probes_url)
121        probes = list(data.items())
122
123        for dependency in self.get_dependencies():
124            dependency_probes = self._get_json(
125                self.probes_url_template.format(dependency)
126            )
127            probes += list(dependency_probes.items())
128
129        pings = self.get_pings()
130
131        processed = []
132        for _id, defn in probes:
133            probe = GleanProbe(_id, defn, pings=pings)
134            processed.append(probe)
135
136            # Manual handling of incompatible schema changes
137            issue_118_affected = {
138                "fenix",
139                "fenix-nightly",
140                "firefox-android-nightly",
141                "firefox-android-beta",
142                "firefox-android-release",
143            }
144            if (
145                self.repo_name in issue_118_affected
146                and probe.get_name() == "installation.timestamp"
147            ):
148                logging.info(f"Writing column {probe.get_name()} for compatibility.")
149                # See: https://github.com/mozilla/mozilla-schema-generator/issues/118
150                # Search through history for the "string" type and add a copy of
151                # the probe at that time in history. The changepoint signifies
152                # this event.
153                changepoint_index = 0
154                for definition in probe.definition_history:
155                    if definition["type"] != probe.get_type():
156                        break
157                    changepoint_index += 1
158                # Modify the definition with the truncated history.
159                hist_defn = defn.copy()
160                hist_defn[probe.history_key] = probe.definition_history[
161                    changepoint_index:
162                ]
163                hist_defn["type"] = hist_defn[probe.history_key][0]["type"]
164                incompatible_probe_type = GleanProbe(_id, hist_defn, pings=pings)
165                processed.append(incompatible_probe_type)
166
167            # Handling probe type changes (Bug 1870317)
168            probe_types = {hist["type"] for hist in defn[probe.history_key]}
169            if len(probe_types) > 1:
170                # The probe type changed at some point in history.
171                # Create schema entry for each type.
172                hist_defn = defn.copy()
173
174                # No new entry needs to be created for the current probe type
175                probe_types.remove(defn["type"])
176
177                for hist in hist_defn[probe.history_key]:
178                    # Create a new entry for a historic type
179                    if hist["type"] in probe_types:
180                        hist_defn["type"] = hist["type"]
181                        probe = GleanProbe(_id, hist_defn, pings=pings)
182                        processed.append(probe)
183
184                        # Keep track of the types entries were already created for
185                        probe_types.remove(hist["type"])
186
187        return processed
def get_pings(self) -> Set[str]:
205    def get_pings(self) -> Set[str]:
206        return self._get_ping_data().keys()
@staticmethod
def apply_default_metadata(ping_metadata, default_metadata):
208    @staticmethod
209    def apply_default_metadata(ping_metadata, default_metadata):
210        """apply_default_metadata recurses down into dicts nested
211        to an arbitrary depth, updating keys. The ``default_metadata`` is merged into
212        ``ping_metadata``.
213        :param ping_metadata: dict onto which the merge is executed
214        :param default_metadata: dct merged into ping_metadata
215        :return: None
216        """
217        for k, v in default_metadata.items():
218            if (
219                k in ping_metadata
220                and isinstance(ping_metadata[k], dict)
221                and isinstance(default_metadata[k], dict)
222            ):
223                GleanPing.apply_default_metadata(ping_metadata[k], default_metadata[k])
224            else:
225                ping_metadata[k] = default_metadata[k]

apply_default_metadata recurses down into dicts nested to an arbitrary depth, updating keys. The default_metadata is merged into ping_metadata.

Parameters
  • ping_metadata: dict onto which the merge is executed
  • default_metadata: dct merged into ping_metadata
Returns

None

@staticmethod
def reorder_metadata(metadata):
253    @staticmethod
254    def reorder_metadata(metadata):
255        desired_order_list = [
256            "bq_dataset_family",
257            "bq_table",
258            "bq_metadata_format",
259            "include_info_sections",
260            "submission_timestamp_granularity",
261            "expiration_policy",
262            "override_attributes",
263            "jwe_mappings",
264        ]
265        reordered_metadata = {
266            k: metadata[k] for k in desired_order_list if k in metadata
267        }
268
269        # re-order jwe-mappings
270        desired_order_list = ["source_field_path", "decrypted_field_path"]
271        jwe_mapping_metadata = reordered_metadata.get("jwe_mappings")
272        if jwe_mapping_metadata:
273            reordered_jwe_mapping_metadata = []
274            for mapping in jwe_mapping_metadata:
275                reordered_jwe_mapping_metadata.append(
276                    {k: mapping[k] for k in desired_order_list if k in mapping}
277                )
278            reordered_metadata["jwe_mappings"] = reordered_jwe_mapping_metadata
279
280        # future proofing, in case there are other fields added at the ping top level
281        # add them to the end.
282        leftovers = {k: metadata[k] for k in set(metadata) - set(reordered_metadata)}
283        reordered_metadata = {**reordered_metadata, **leftovers}
284        return reordered_metadata
def get_pings_and_pipeline_metadata(self) -> Dict[str, Dict]:
286    def get_pings_and_pipeline_metadata(self) -> Dict[str, Dict]:
287        pings = self._get_ping_data_and_dependencies_with_default_metadata()
288        for ping_name, ping_data in pings.items():
289            metadata = ping_data.get("moz_pipeline_metadata")
290            metadata["include_info_sections"] = self._include_info_sections(ping_data)
291
292            # While technically unnecessary, the dictionary elements are re-ordered to match the
293            # currently deployed order and used to verify no difference in output.
294            pings[ping_name] = GleanPing.reorder_metadata(metadata)
295        return pings
def get_ping_descriptions(self) -> Dict[str, str]:
297    def get_ping_descriptions(self) -> Dict[str, str]:
298        return {
299            k: v["history"][-1]["description"] for k, v in self._get_ping_data().items()
300        }
def set_schema_url(self, metadata):
312    def set_schema_url(self, metadata):
313        """
314        Switch between the glean-min and glean schemas if the ping does not require
315        info sections as specified in the parsed ping info in probe scraper.
316        """
317        if not metadata["include_info_sections"]:
318            self.schema_url = MINIMUM_SCHEMA_URL.format(branch=self.branch_name)
319        else:
320            self.schema_url = DEFAULT_SCHEMA_URL.format(branch=self.branch_name)

Switch between the glean-min and glean schemas if the ping does not require info sections as specified in the parsed ping info in probe scraper.

def generate_schema( self, config, generic_schema=False) -> Dict[str, mozilla_schema_generator.schema.Schema]:
322    def generate_schema(self, config, generic_schema=False) -> Dict[str, Schema]:
323        pings = self.get_pings_and_pipeline_metadata()
324        schemas = {}
325
326        for ping, pipeline_meta in pings.items():
327            matchers = {
328                loc: m.clone(new_table_group=ping) for loc, m in config.matchers.items()
329            }
330
331            # Four newly introduced metric types were incorrectly deployed
332            # as repeated key/value structs in all Glean ping tables existing prior
333            # to November 2021. We maintain the incorrect fields for existing tables
334            # by disabling the associated matchers.
335            # Note that each of these types now has a "2" matcher ("text2", "url2", etc.)
336            # defined that will allow metrics of these types to be injected into proper
337            # structs. The gcp-ingestion repository includes logic to rewrite these
338            # metrics under the "2" names.
339            # See https://bugzilla.mozilla.org/show_bug.cgi?id=1737656
340            bq_identifier = "{bq_dataset_family}.{bq_table}".format(**pipeline_meta)
341            if bq_identifier in self.bug_1737656_affected_tables:
342                matchers = {
343                    loc: m
344                    for loc, m in matchers.items()
345                    if not m.matcher.get("bug_1737656_affected")
346                }
347
348            for matcher in matchers.values():
349                matcher.matcher["send_in_pings"]["contains"] = ping
350            new_config = Config(ping, matchers=matchers)
351
352            defaults = {"mozPipelineMetadata": pipeline_meta}
353
354            # Adjust the schema path if the ping does not require info sections
355            self.set_schema_url(pipeline_meta)
356            if generic_schema:  # Use the generic glean ping schema
357                schema = self.get_schema(generic_schema=True)
358                schema.schema.update(defaults)
359                schemas[new_config.name] = schema
360            else:
361                generated = super().generate_schema(new_config)
362                for schema in generated.values():
363                    # We want to override each individual key with assembled defaults,
364                    # but keep values _inside_ them if they have been set in the schemas.
365                    for key, value in defaults.items():
366                        if key not in schema.schema:
367                            schema.schema[key] = {}
368                        schema.schema[key].update(value)
369                schemas.update(generated)
370
371        return schemas
@staticmethod
def get_repos():
373    @staticmethod
374    def get_repos():
375        """
376        Retrieve metadata for all non-library Glean repositories
377        """
378        repos = GleanPing._get_json(GleanPing.repos_url)
379        return [repo for repo in repos if "library_names" not in repo]

Retrieve metadata for all non-library Glean repositories

f = <_io.TextIOWrapper name='/home/circleci/project/mozilla_schema_generator/configs/bug_1737656_affected.txt' mode='r' encoding='UTF-8'>
bug_1737656_affected_tables = ['burnham.baseline_v1', 'burnham.deletion_request_v1', 'burnham.discovery_v1', 'burnham.events_v1', 'burnham.metrics_v1', 'burnham.space_ship_ready_v1', 'burnham.starbase46_v1', 'firefox_desktop_background_update.background_update_v1', 'firefox_desktop_background_update.baseline_v1', 'firefox_desktop_background_update.deletion_request_v1', 'firefox_desktop_background_update.events_v1', 'firefox_desktop_background_update.metrics_v1', 'firefox_desktop.baseline_v1', 'firefox_desktop.deletion_request_v1', 'firefox_desktop.events_v1', 'firefox_desktop.fog_validation_v1', 'firefox_desktop.metrics_v1', 'firefox_installer.install_v1', 'firefox_launcher_process.launcher_process_failure_v1', 'messaging_system.cfr_v1', 'messaging_system.infobar_v1', 'messaging_system.moments_v1', 'messaging_system.onboarding_v1', 'messaging_system.personalization_experiment_v1', 'messaging_system.snippets_v1', 'messaging_system.spotlight_v1', 'messaging_system.undesired_events_v1', 'messaging_system.whats_new_panel_v1', 'mlhackweek_search.action_v1', 'mlhackweek_search.baseline_v1', 'mlhackweek_search.custom_v1', 'mlhackweek_search.deletion_request_v1', 'mlhackweek_search.events_v1', 'mlhackweek_search.metrics_v1', 'mozilla_lockbox.addresses_sync_v1', 'mozilla_lockbox.baseline_v1', 'mozilla_lockbox.bookmarks_sync_v1', 'mozilla_lockbox.creditcards_sync_v1', 'mozilla_lockbox.deletion_request_v1', 'mozilla_lockbox.events_v1', 'mozilla_lockbox.history_sync_v1', 'mozilla_lockbox.logins_sync_v1', 'mozilla_lockbox.metrics_v1', 'mozilla_lockbox.sync_v1', 'mozilla_lockbox.tabs_sync_v1', 'mozilla_mach.baseline_v1', 'mozilla_mach.deletion_request_v1', 'mozilla_mach.events_v1', 'mozilla_mach.metrics_v1', 'mozilla_mach.usage_v1', 'mozillavpn.deletion_request_v1', 'mozillavpn.main_v1', 'mozphab.baseline_v1', 'mozphab.deletion_request_v1', 'mozphab.events_v1', 'mozphab.metrics_v1', 'mozphab.usage_v1', 'org_mozilla_bergamot.custom_v1', 'org_mozilla_bergamot.deletion_request_v1', 'org_mozilla_connect_firefox.baseline_v1', 'org_mozilla_connect_firefox.deletion_request_v1', 'org_mozilla_connect_firefox.events_v1', 'org_mozilla_connect_firefox.metrics_v1', 'org_mozilla_fenix.activation_v1', 'org_mozilla_fenix.addresses_sync_v1', 'org_mozilla_fenix.baseline_v1', 'org_mozilla_fenix.bookmarks_sync_v1', 'org_mozilla_fenix.creditcards_sync_v1', 'org_mozilla_fenix.deletion_request_v1', 'org_mozilla_fenix.events_v1', 'org_mozilla_fenix.first_session_v1', 'org_mozilla_fenix.fog_validation_v1', 'org_mozilla_fenix.history_sync_v1', 'org_mozilla_fenix.installation_v1', 'org_mozilla_fenix.logins_sync_v1', 'org_mozilla_fenix.metrics_v1', 'org_mozilla_fenix.migration_v1', 'org_mozilla_fenix.startup_timeline_v1', 'org_mozilla_fenix.sync_v1', 'org_mozilla_fenix.tabs_sync_v1', 'org_mozilla_fenix_nightly.activation_v1', 'org_mozilla_fenix_nightly.addresses_sync_v1', 'org_mozilla_fenix_nightly.baseline_v1', 'org_mozilla_fenix_nightly.bookmarks_sync_v1', 'org_mozilla_fenix_nightly.creditcards_sync_v1', 'org_mozilla_fenix_nightly.deletion_request_v1', 'org_mozilla_fenix_nightly.events_v1', 'org_mozilla_fenix_nightly.first_session_v1', 'org_mozilla_fenix_nightly.fog_validation_v1', 'org_mozilla_fenix_nightly.history_sync_v1', 'org_mozilla_fenix_nightly.installation_v1', 'org_mozilla_fenix_nightly.logins_sync_v1', 'org_mozilla_fenix_nightly.metrics_v1', 'org_mozilla_fenix_nightly.migration_v1', 'org_mozilla_fenix_nightly.startup_timeline_v1', 'org_mozilla_fenix_nightly.sync_v1', 'org_mozilla_fenix_nightly.tabs_sync_v1', 'org_mozilla_fennec_aurora.activation_v1', 'org_mozilla_fennec_aurora.addresses_sync_v1', 'org_mozilla_fennec_aurora.baseline_v1', 'org_mozilla_fennec_aurora.bookmarks_sync_v1', 'org_mozilla_fennec_aurora.creditcards_sync_v1', 'org_mozilla_fennec_aurora.deletion_request_v1', 'org_mozilla_fennec_aurora.events_v1', 'org_mozilla_fennec_aurora.first_session_v1', 'org_mozilla_fennec_aurora.fog_validation_v1', 'org_mozilla_fennec_aurora.history_sync_v1', 'org_mozilla_fennec_aurora.installation_v1', 'org_mozilla_fennec_aurora.logins_sync_v1', 'org_mozilla_fennec_aurora.metrics_v1', 'org_mozilla_fennec_aurora.migration_v1', 'org_mozilla_fennec_aurora.startup_timeline_v1', 'org_mozilla_fennec_aurora.sync_v1', 'org_mozilla_fennec_aurora.tabs_sync_v1', 'org_mozilla_firefox_beta.activation_v1', 'org_mozilla_firefox_beta.addresses_sync_v1', 'org_mozilla_firefox_beta.baseline_v1', 'org_mozilla_firefox_beta.bookmarks_sync_v1', 'org_mozilla_firefox_beta.creditcards_sync_v1', 'org_mozilla_firefox_beta.deletion_request_v1', 'org_mozilla_firefox_beta.events_v1', 'org_mozilla_firefox_beta.first_session_v1', 'org_mozilla_firefox_beta.fog_validation_v1', 'org_mozilla_firefox_beta.history_sync_v1', 'org_mozilla_firefox_beta.installation_v1', 'org_mozilla_firefox_beta.logins_sync_v1', 'org_mozilla_firefox_beta.metrics_v1', 'org_mozilla_firefox_beta.migration_v1', 'org_mozilla_firefox_beta.startup_timeline_v1', 'org_mozilla_firefox_beta.sync_v1', 'org_mozilla_firefox_beta.tabs_sync_v1', 'org_mozilla_firefox.activation_v1', 'org_mozilla_firefox.addresses_sync_v1', 'org_mozilla_firefox.baseline_v1', 'org_mozilla_firefox.bookmarks_sync_v1', 'org_mozilla_firefox.creditcards_sync_v1', 'org_mozilla_firefox.deletion_request_v1', 'org_mozilla_firefox.events_v1', 'org_mozilla_firefox.first_session_v1', 'org_mozilla_firefox.fog_validation_v1', 'org_mozilla_firefox.history_sync_v1', 'org_mozilla_firefox.installation_v1', 'org_mozilla_firefox.logins_sync_v1', 'org_mozilla_firefox.metrics_v1', 'org_mozilla_firefox.migration_v1', 'org_mozilla_firefox.startup_timeline_v1', 'org_mozilla_firefox.sync_v1', 'org_mozilla_firefox.tabs_sync_v1', 'org_mozilla_firefoxreality.baseline_v1', 'org_mozilla_firefoxreality.deletion_request_v1', 'org_mozilla_firefoxreality.events_v1', 'org_mozilla_firefoxreality.launch_v1', 'org_mozilla_firefoxreality.metrics_v1', 'org_mozilla_focus_beta.activation_v1', 'org_mozilla_focus_beta.baseline_v1', 'org_mozilla_focus_beta.deletion_request_v1', 'org_mozilla_focus_beta.events_v1', 'org_mozilla_focus_beta.metrics_v1', 'org_mozilla_focus.activation_v1', 'org_mozilla_focus.baseline_v1', 'org_mozilla_focus.deletion_request_v1', 'org_mozilla_focus.events_v1', 'org_mozilla_focus.metrics_v1', 'org_mozilla_focus_nightly.activation_v1', 'org_mozilla_focus_nightly.baseline_v1', 'org_mozilla_focus_nightly.deletion_request_v1', 'org_mozilla_focus_nightly.events_v1', 'org_mozilla_focus_nightly.metrics_v1', 'org_mozilla_ios_fennec.baseline_v1', 'org_mozilla_ios_fennec.deletion_request_v1', 'org_mozilla_ios_fennec.events_v1', 'org_mozilla_ios_fennec.metrics_v1', 'org_mozilla_ios_firefox.baseline_v1', 'org_mozilla_ios_firefox.deletion_request_v1', 'org_mozilla_ios_firefox.events_v1', 'org_mozilla_ios_firefox.metrics_v1', 'org_mozilla_ios_firefoxbeta.baseline_v1', 'org_mozilla_ios_firefoxbeta.deletion_request_v1', 'org_mozilla_ios_firefoxbeta.events_v1', 'org_mozilla_ios_firefoxbeta.metrics_v1', 'org_mozilla_ios_focus.baseline_v1', 'org_mozilla_ios_focus.deletion_request_v1', 'org_mozilla_ios_focus.events_v1', 'org_mozilla_ios_focus.metrics_v1', 'org_mozilla_ios_klar.baseline_v1', 'org_mozilla_ios_klar.deletion_request_v1', 'org_mozilla_ios_klar.events_v1', 'org_mozilla_ios_klar.metrics_v1', 'org_mozilla_ios_lockbox.baseline_v1', 'org_mozilla_ios_lockbox.deletion_request_v1', 'org_mozilla_ios_lockbox.events_v1', 'org_mozilla_ios_lockbox.metrics_v1', 'org_mozilla_klar.activation_v1', 'org_mozilla_klar.baseline_v1', 'org_mozilla_klar.deletion_request_v1', 'org_mozilla_klar.events_v1', 'org_mozilla_klar.metrics_v1', 'org_mozilla_mozregression.baseline_v1', 'org_mozilla_mozregression.deletion_request_v1', 'org_mozilla_mozregression.events_v1', 'org_mozilla_mozregression.metrics_v1', 'org_mozilla_mozregression.usage_v1', 'org_mozilla_reference_browser.baseline_v1', 'org_mozilla_reference_browser.deletion_request_v1', 'org_mozilla_reference_browser.events_v1', 'org_mozilla_reference_browser.metrics_v1', 'org_mozilla_tv_firefox.baseline_v1', 'org_mozilla_tv_firefox.deletion_request_v1', 'org_mozilla_tv_firefox.events_v1', 'org_mozilla_tv_firefox.metrics_v1', 'org_mozilla_vrbrowser.addresses_sync_v1', 'org_mozilla_vrbrowser.baseline_v1', 'org_mozilla_vrbrowser.bookmarks_sync_v1', 'org_mozilla_vrbrowser.creditcards_sync_v1', 'org_mozilla_vrbrowser.deletion_request_v1', 'org_mozilla_vrbrowser.events_v1', 'org_mozilla_vrbrowser.history_sync_v1', 'org_mozilla_vrbrowser.logins_sync_v1', 'org_mozilla_vrbrowser.metrics_v1', 'org_mozilla_vrbrowser.session_end_v1', 'org_mozilla_vrbrowser.sync_v1', 'org_mozilla_vrbrowser.tabs_sync_v1', 'rally_core.deletion_request_v1', 'rally_core.demographics_v1', 'rally_core.enrollment_v1', 'rally_core.study_enrollment_v1', 'rally_core.study_unenrollment_v1', 'rally_core.uninstall_deletion_v1', 'rally_debug.deletion_request_v1', 'rally_debug.demographics_v1', 'rally_debug.enrollment_v1', 'rally_debug.study_enrollment_v1', 'rally_debug.study_unenrollment_v1', 'rally_debug.uninstall_deletion_v1', 'rally_study_zero_one.deletion_request_v1', 'rally_study_zero_one.rs01_event_v1', 'rally_study_zero_one.study_enrollment_v1', 'rally_zero_one.deletion_request_v1', 'rally_zero_one.measurements_v1', 'rally_zero_one.pioneer_enrollment_v1']