Coverage for baibu/metrics/definitions.py: 100%
17 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1"""The metrics the platform counts, with how each one adds up over days.
3The exact definitions are in ``docs/developer/metrics.md``; keep the two in
4step. Every value is an integer count (or a whole number of milliseconds).
5"""
7from dataclasses import dataclass
9from django.utils.translation import gettext_lazy as _
11# How a metric combines over several days.
12SUM = "sum" # events: add the daily values
13DISTINCT = "distinct" # distinct users: days cannot be added; read the value on the last day
14SNAPSHOT = "snapshot" # a state at the end of the day: read the value on the last day
15DURATION = "duration" # a median or percentile in milliseconds: shown per day only
16COHORT = "cohort" # retention: dated by the cohort week, not by a day of activity
19@dataclass(frozen=True)
20class Metric:
21 name: str
22 label: str
23 kind: str
24 # What the dimension holds for a breakdown; empty for a plain total.
25 dimension: str = ""
26 # Breakdowns that could point at a few people (by language or consent
27 # tier) show small values as "fewer than METRICS_MIN_GROUP_SIZE".
28 suppress_small: bool = False
31METRICS: dict[str, Metric] = {
32 metric.name: metric
33 for metric in (
34 Metric("users_new", _("New users"), SUM),
35 Metric("users_total", _("Users"), SNAPSHOT),
36 Metric("active_users", _("Active users"), DISTINCT),
37 Metric("active_users_7d", _("Active users, 7 days"), DISTINCT),
38 Metric("active_users_30d", _("Active users, 30 days"), DISTINCT),
39 Metric("chat_users", _("Users who chatted"), DISTINCT),
40 Metric("contributors", _("Users who contributed"), DISTINCT),
41 Metric("conversations", _("Conversations started"), SUM),
42 Metric("conversations_by_language", _("Conversations by language"), SUM, _("Language"), suppress_small=True),
43 Metric("messages_user", _("Messages sent"), SUM),
44 Metric("messages_assistant", _("Assistant replies"), SUM),
45 Metric("runs", _("Reply attempts"), SUM, _("Status")),
46 Metric("reply_time_median_ms", _("Reply time, median (ms)"), DURATION),
47 Metric("reply_time_p90_ms", _("Reply time, 90th percentile (ms)"), DURATION),
48 Metric("tool_calls", _("Tool calls"), SUM, _("Status")),
49 Metric("submissions", _("Submissions"), SUM),
50 Metric("submissions_by_status", _("Submissions by status"), SUM, _("Status")),
51 Metric("submissions_by_language", _("Submissions by language"), SUM, _("Language"), suppress_small=True),
52 Metric("consent", _("Current consent"), SNAPSHOT, _("Scope and tier"), suppress_small=True),
53 Metric("retention_cohort_size", _("Cohort size"), COHORT),
54 Metric("retention_active", _("Cohort users active"), COHORT, _("Weeks after joining")),
55 )
56}
58# Metrics stored under the day they describe (everything except retention).
59DAILY_METRICS = tuple(name for name, metric in METRICS.items() if metric.kind != COHORT)
60RETENTION_METRICS = tuple(name for name, metric in METRICS.items() if metric.kind == COHORT)
63def is_suppressed(name: str, dimension: str, value: int, min_group_size: int) -> bool:
64 """True if ``value`` is a small, non-zero count in a sensitive breakdown."""
65 metric = METRICS.get(name)
66 return bool(metric and metric.suppress_small and dimension and 0 < value < min_group_size)