summaryrefslogtreecommitdiffstats
path: root/third_party/python/glean_parser/glean_parser/markdown.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/glean_parser/glean_parser/markdown.py')
-rw-r--r--third_party/python/glean_parser/glean_parser/markdown.py273
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")