probe_scraper/glean_checks.py (316 lines of code) (raw):

# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ This file contains various sanity checks for Glean. """ import datetime from pathlib import Path from typing import Any, Dict from schema import And, Optional, Schema from probe_scraper import probe_expiry_alert from .scrapers.git_scraper import Commit # Ugly hack to skip certain metrics which we know aren't duplicated, # but show up duplicated due to them being moved from one # to the other application/library. SKIP_METRICS = { "gecko.version": ["gecko", "pine", "firefox-desktop"], "gecko.build_id": ["gecko", "pine", "firefox-desktop"], "broken_site_report.breakage_category": ["gecko", "fenix", "firefox-desktop"], "broken_site_report.description": ["gecko", "fenix", "firefox-desktop"], "broken_site_report.url": ["gecko", "fenix", "firefox-desktop"], "broken_site_report.browser_info.app.default_locales": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.app.default_useragent_string": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.app.fission_enabled": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.graphics.device_pixel_ratio": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.graphics.devices_json": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.graphics.drivers_json": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.graphics.features_json": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.graphics.has_touch_screen": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.graphics.monitors_json": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.prefs.cookie_behavior": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.prefs.global_privacy_control_enabled": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.prefs.installtrigger_enabled": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.prefs.opaque_response_blocking": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.prefs.resist_fingerprinting_enabled": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.prefs.software_webrender": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.browser_info.system.memory": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.languages": ["gecko", "fenix", "firefox-desktop"], "broken_site_report.tab_info.useragent_string": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.antitracking.block_list": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.antitracking.btp_has_purged_site": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.antitracking.has_mixed_active_content_blocked": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.antitracking.has_mixed_display_content_blocked": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.antitracking.has_tracking_content_blocked": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.antitracking.is_private_browsing": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.frameworks.fastclick": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.frameworks.marfeel": [ "gecko", "fenix", "firefox-desktop", ], "broken_site_report.tab_info.frameworks.mobify": [ "gecko", "fenix", "firefox-desktop", ], } def _metric_sort_key(metric: Dict[str, Any]): return ( datetime.datetime.fromisoformat(metric["dates"]["last"]), -metric["reflog-index"]["last"], ) def get_current_metrics_by_repo( metrics_by_repo: Dict[str, Dict[str, Dict[str, Any]]], ) -> Dict[str, Dict[str, Dict[str, Any]]]: """ We have a whole history of these metrics, but expiry only cares about the current state. Return the current state of metrics. """ return { repo_name: { metric_name: sorted(metric["history"], key=_metric_sort_key)[-1] for metric_name, metric in metrics.items() if metric["in-source"] } for repo_name, metrics in metrics_by_repo.items() } def check_glean_metric_structure(data): schema = Schema( { str: { Optional(And(Commit, lambda x: len(x.hash) == 40)): [ And(Path, lambda x: x.exists()) ] } } ) schema.validate(data) DUPLICATE_METRICS_EMAIL_TEMPLATE = """ Glean has detected duplicated metric identifiers coming from the product '{repo.name}'. {duplicates} What to do about this: 1. File a bug to track your investigation. You can just copy this email into the bug Description to get you started. 2. Reply-All to this email to let the list know that you are investigating. Include the bug number so we can help out. 3. Rename the most recently added metric to be more specific. See [1] 4. Make sure a Glean team member reviews any patches. Care needs to be taken that the resolution of this problem is schema-compatible. If you have any problems, please ask for help on the #glean Slack channel. We'll give you a hand. What this is: We have a system called probe-scraper [2] that scrapes the metric information from all Mozilla products using the Glean SDK. All the scraped data is available on the probeinfo service [3]. The scraped definition is used to build things such as the probe-dictionary [4] and other data tools. It detected that one metric that was recently added has an identifier collision with some metric that already existed in the application namespace. So it sent this email out, encouraging you to fix the problem. What happens if you don't fix this: The metrics will compete to send their data in pings, making the data unreliable at best. You can do this! Your Friendly, Neighborhood Glean Team [1] - https://mozilla.github.io/glean/book/user/adding-new-metrics.html#naming-things [2] - https://github.com/mozilla/probe-scraper [3] - https://probeinfo.telemetry.mozilla.org/ [4] - https://telemetry.mozilla.org/probe-dictionary/ """ # noqa class MissingDependencyError(ValueError): pass def check_for_duplicate_metrics(repositories, metrics_by_repo, emails): """ Checks for duplicate metric names across all libraries used by a particular application. It only checks for metrics that exist in the latest (HEAD) commit in each repo, so that it's possible to remove (or disable) the metric in the latest commit and not have this check repeatedly fail. If duplicates are found, e-mails are queued and this returns True. """ found_duplicates = False repo_by_library_name = {} repo_by_name = {} for repo in repositories: for library_name in repo.library_names or []: repo_by_library_name[library_name] = repo.name repo_by_name[repo.name] = repo for repo in repositories: for library_name in repo.dependencies: if library_name not in repo_by_library_name: raise MissingDependencyError( f"{repo.name} missing dependency {library_name}" ) dependencies = [repo.name] + [ repo_by_library_name[library_name] for library_name in repo.dependencies ] metric_sources = {} for dependency in dependencies: # skip if no metrics if not metrics_by_repo[dependency]: continue # otherwise look for the latest timestamp for all metrics -- # metrics which don't appear in the latest can be assumed to # no longer be present last_timestamp = max( [ metric["history"][-1]["dates"]["last"] for metric in metrics_by_repo[dependency].values() ] ) for metric_name, metric in metrics_by_repo[dependency].items(): if metric["history"][-1]["dates"]["last"] == last_timestamp: metric_sources.setdefault(metric_name, []).append(dependency) duplicate_sources = {} for k, v in metric_sources.items(): # Exempt cases when one of the sources is Geckoview Streaming to # avoid false positive duplication accross app channels. # Temporarily exempt cases when one of the sources is server compat library # to avoid raising alarm for metrics defined in fxa's custom ping. v = [ dep for dep in v if ( "engine-gecko" not in dep and "glean-server-metrics-compat" not in dep ) ] if k in SKIP_METRICS.keys(): potential_deps = SKIP_METRICS[k] if any([dep for dep in potential_deps if dep in v]): continue if len(v) > 1: duplicate_sources[k] = v if not len(duplicate_sources): continue found_duplicates = True addresses = set() duplicates = [] for name, sources in duplicate_sources.items(): duplicates.append( "- {!r} defined more than once in {}".format( name, ", ".join(sorted(sources)) ) ) for source in sources: # Send to the repository contacts addresses.update(repo_by_name[source].notification_emails) # Also send to the metric's contacts for history_entry in metrics_by_repo[source][name]["history"]: addresses.update(history_entry["notification_emails"]) duplicates = "\n".join(duplicates) emails[f"duplicate_metrics_{repo.name}"] = { "emails": [ { "subject": "Glean: Duplicate metric identifiers detected", "message": DUPLICATE_METRICS_EMAIL_TEMPLATE.format( duplicates=duplicates, repo=repo ), } ], "addresses": list(addresses), } return found_duplicates EXPIRED_METRICS_EMAIL_TEMPLATE = """ Each metric in the following list from {repo_name} will expire in the next {expire_days} days or has already expired. {expired_metrics} What to do about this: 1. If the metric is no longer needed, remove it from its `metrics.yaml` [1] file. 2. If the metric is still required, resubmit a data review [2] and extend its expiration date. If you have any problems, please ask for help on the #glean Matrix channel[3]. We'll give you a hand. What happens if you don't fix this: The metrics listed above will stop collecting data from builds built after this expiration date, and you will continue to get this e-mail as a reminder. Your Friendly, Neighborhood Glean Team [1] The correct metrics.yaml is in this list: {metrics_yaml_url} [2] https://wiki.mozilla.org/Firefox/Data_Collection [3] https://chat.mozilla.org/#/room/#glean:mozilla.org This is an automated message sent from probe-scraper. See https://github.com/mozilla/probe-scraper for details. """ # noqa def check_for_expired_metrics( repositories, metrics_by_repo, emails, expire_days=14, ): """ Checks for all expired metrics and generates e-mails, one per repository. """ expiration_cutoff = datetime.datetime.utcnow().date() + datetime.timedelta( days=expire_days ) current_metrics_by_repo = get_current_metrics_by_repo(metrics_by_repo) for repo in repositories: metrics = current_metrics_by_repo[repo.name] addresses = set(repo.notification_emails) # Do not send expiry for deprecated repositories if repo.deprecated: continue target_version = None if repo.name == "fenix": try: target_version = ( int(probe_expiry_alert.get_latest_nightly_version()) + 1 ) except ValueError: # Can't parse the version as an int. Welp. pass expired_metrics = [] for metric_name, metric in metrics.items(): if metric["expires"] == "never": continue # `expires` field supports manual expiry, too. if metric["expires"] == "expired": expired_metrics.append(f"- {metric_name} manually expired") addresses.update(metric["notification_emails"]) continue if isinstance(metric["expires"], int): # Uses expire-by-version. if target_version is not None: if metric["expires"] == target_version: expired_metrics.append(f" - {metric_name} in {target_version}") addresses.update(metric["notification_emails"]) continue try: expires = datetime.datetime.strptime( metric["expires"], "%Y-%m-%d" ).date() except ValueError: # String does not contain a date, so we don't currently handle expiration. pass else: if expiration_cutoff >= expires: expired_metrics.append(f"- {metric_name} on {expires}") addresses.update(metric["notification_emails"]) expired_metrics.sort() if len(expired_metrics) == 0: continue metrics_yaml_url = "\n".join( f"{repo.url}/tree/HEAD/{file}" for file in repo.metrics_file_paths ) emails[f"expired_metrics_{repo.name}"] = { "emails": [ { "subject": f"Glean: Expired metrics in {repo.name}", "message": EXPIRED_METRICS_EMAIL_TEMPLATE.format( repo_name=repo.name, expire_days=expire_days, expired_metrics="\n".join(expired_metrics), metrics_yaml_url=metrics_yaml_url, ), } ], "addresses": list(addresses), }