diff options
Diffstat (limited to 'third_party/python/glean_parser/glean_parser/markdown.py')
-rw-r--r-- | third_party/python/glean_parser/glean_parser/markdown.py | 273 |
1 files changed, 273 insertions, 0 deletions
diff --git a/third_party/python/glean_parser/glean_parser/markdown.py b/third_party/python/glean_parser/glean_parser/markdown.py new file mode 100644 index 0000000000..68b288945f --- /dev/null +++ b/third_party/python/glean_parser/glean_parser/markdown.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +Outputter to generate Markdown documentation for metrics. +""" + +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union +from urllib.parse import urlsplit, parse_qs + + +from . import __version__ +from . import metrics +from . import pings +from . import util +from collections import defaultdict + + +def extra_info(obj: Union[metrics.Metric, pings.Ping]) -> List[Tuple[str, str]]: + """ + Returns a list of string to string tuples with extra information for the type + (e.g. extra keys for events) or an empty list if nothing is available. + """ + extra_info = [] + + if isinstance(obj, metrics.Event): + for key in obj.allowed_extra_keys: + extra_info.append((key, obj.extra_keys[key]["description"])) + + if isinstance(obj, metrics.Labeled) and obj.ordered_labels is not None: + for label in obj.ordered_labels: + extra_info.append((label, None)) + + if isinstance(obj, metrics.Quantity): + extra_info.append(("unit", obj.unit)) + + return extra_info + + +def ping_desc( + ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None +) -> str: + """ + Return a text description of the ping. If a custom_pings_cache + is available, look in there for non-reserved ping names description. + """ + desc = "" + + if ping_name in pings.RESERVED_PING_NAMES: + desc = ( + "This is a built-in ping that is assembled out of the " + "box by the Glean SDK." + ) + elif ping_name == "all-pings": + desc = "These metrics are sent in every ping." + elif custom_pings_cache is not None and ping_name in custom_pings_cache: + desc = custom_pings_cache[ping_name].description + + return desc + + +def metrics_docs(obj_name: str) -> str: + """ + Return a link to the documentation entry for the Glean SDK metric of the + requested type. + """ + # We need to fixup labeled stuff, as types are singular and docs refer + # to them as plural. + fixedup_name = obj_name + if obj_name.startswith("labeled_"): + fixedup_name += "s" + + return f"https://mozilla.github.io/glean/book/user/metrics/{fixedup_name}.html" + + +def ping_docs(ping_name: str) -> str: + """ + Return a link to the documentation entry for the requested Glean SDK + built-in ping. + """ + if ping_name not in pings.RESERVED_PING_NAMES: + return "" + + return f"https://mozilla.github.io/glean/book/user/pings/{ping_name}.html" + + +def if_empty( + ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None +) -> bool: + if custom_pings_cache is not None and ping_name in custom_pings_cache: + return custom_pings_cache[ping_name].send_if_empty + else: + return False + + +def ping_reasons( + ping_name: str, custom_pings_cache: Dict[str, pings.Ping] +) -> Dict[str, str]: + """ + Returns the reasons dictionary for the ping. + """ + if ping_name == "all-pings": + return {} + elif ping_name in custom_pings_cache: + return custom_pings_cache[ping_name].reasons + + return {} + + +def ping_data_reviews( + ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None +) -> Optional[List[str]]: + if custom_pings_cache is not None and ping_name in custom_pings_cache: + return custom_pings_cache[ping_name].data_reviews + else: + return None + + +def ping_review_title(data_url: str, index: int) -> str: + """ + Return a title for a data review in human readable form. + + :param data_url: A url for data review. + :param index: Position of the data review on list (e.g: 1, 2, 3...). + """ + url_object = urlsplit(data_url) + + # Bugzilla urls like `https://bugzilla.mozilla.org/show_bug.cgi?id=1581647` + query = url_object.query + params = parse_qs(query) + + # GitHub urls like `https://github.com/mozilla-mobile/fenix/pull/1707` + path = url_object.path + short_url = path[1:].replace("/pull/", "#") + + if params and params["id"]: + return f"Bug {params['id'][0]}" + elif url_object.netloc == "github.com": + return short_url + + return f"Review {index}" + + +def ping_bugs( + ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None +) -> Optional[List[str]]: + if custom_pings_cache is not None and ping_name in custom_pings_cache: + return custom_pings_cache[ping_name].bugs + else: + return None + + +def ping_include_client_id( + ping_name: str, custom_pings_cache: Optional[Dict[str, pings.Ping]] = None +) -> bool: + if custom_pings_cache is not None and ping_name in custom_pings_cache: + return custom_pings_cache[ping_name].include_client_id + else: + return False + + +def data_sensitivity_numbers( + data_sensitivity: Optional[List[metrics.DataSensitivity]], +) -> str: + if data_sensitivity is None: + return "unknown" + else: + return ", ".join(str(x.value) for x in data_sensitivity) + + +def output_markdown( + objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None +) -> None: + """ + Given a tree of objects, output Markdown docs to `output_dir`. + + This produces a single `metrics.md`. The file contains a table of + contents and a section for each ping metrics are collected for. + + :param objects: A tree of objects (metrics and pings) as returned from + `parser.parse_objects`. + :param output_dir: Path to an output directory to write to. + :param options: options dictionary, with the following optional key: + - `project_title`: The projects title. + """ + if options is None: + options = {} + + # Build a dictionary that associates pings with their metrics. + # + # { + # "baseline": [ + # { ... metric data ... }, + # ... + # ], + # "metrics": [ + # { ... metric data ... }, + # ... + # ], + # ... + # } + # + # This also builds a dictionary of custom pings, if available. + custom_pings_cache: Dict[str, pings.Ping] = defaultdict() + metrics_by_pings: Dict[str, List[metrics.Metric]] = defaultdict(list) + for _category_key, category_val in objs.items(): + for obj in category_val.values(): + # Filter out custom pings. We will need them for extracting + # the description + if isinstance(obj, pings.Ping): + custom_pings_cache[obj.name] = obj + # Pings that have `send_if_empty` set to true, + # might not have any metrics. They need to at least have an + # empty array of metrics to show up on the template. + if obj.send_if_empty and not metrics_by_pings[obj.name]: + metrics_by_pings[obj.name] = [] + + # If this is an internal Glean metric, and we don't + # want docs for it. + if isinstance(obj, metrics.Metric) and not obj.is_internal_metric(): + # If we get here, obj is definitely a metric we want + # docs for. + for ping_name in obj.send_in_pings: + metrics_by_pings[ping_name].append(obj) + + # Sort the metrics by their identifier, to make them show up nicely + # in the docs and to make generated docs reproducible. + for ping_name in metrics_by_pings: + metrics_by_pings[ping_name] = sorted( + metrics_by_pings[ping_name], key=lambda x: x.identifier() + ) + + project_title = options.get("project_title", "this project") + introduction_extra = options.get("introduction_extra") + + template = util.get_jinja2_template( + "markdown.jinja2", + filters=( + ("extra_info", extra_info), + ("metrics_docs", metrics_docs), + ("ping_desc", lambda x: ping_desc(x, custom_pings_cache)), + ("ping_send_if_empty", lambda x: if_empty(x, custom_pings_cache)), + ("ping_docs", ping_docs), + ("ping_reasons", lambda x: ping_reasons(x, custom_pings_cache)), + ("ping_data_reviews", lambda x: ping_data_reviews(x, custom_pings_cache)), + ("ping_review_title", ping_review_title), + ("ping_bugs", lambda x: ping_bugs(x, custom_pings_cache)), + ( + "ping_include_client_id", + lambda x: ping_include_client_id(x, custom_pings_cache), + ), + ("data_sensitivity_numbers", data_sensitivity_numbers), + ), + ) + + filename = "metrics.md" + filepath = output_dir / filename + + with filepath.open("w", encoding="utf-8") as fd: + fd.write( + template.render( + parser_version=__version__, + metrics_by_pings=metrics_by_pings, + project_title=project_title, + introduction_extra=introduction_extra, + ) + ) + # Jinja2 squashes the final newline, so we explicitly add it + fd.write("\n") |