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
# 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"
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")