Source code
Revision control
Copy as Markdown
Other Tools
# -*- 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
"""
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"
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 ""
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)
query = url_object.query
params = parse_qs(query)
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")