Coverage for app/backend/src/tests/test_metrics.py: 98%
97 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-06 00:27 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-06 00:27 +0000
1from datetime import timedelta
3import pytest
4from google.protobuf import empty_pb2
5from sqlalchemy import update
7from couchers.db import session_scope
8from couchers.materialized_views import refresh_materialized_views
9from couchers.metrics import (
10 _set_hacky_labeled_gauges_funcs,
11 active_users_by_platform_gauge,
12 active_users_by_platform_statement,
13 active_users_by_recency_gauge,
14 active_users_mobile_fraction_gauge,
15 users_per_community_gauge,
16)
17from couchers.models import ClientPlatform, User, UserActivity
18from couchers.utils import now
19from tests.fixtures.db import generate_user
20from tests.test_communities import create_community
23@pytest.fixture(autouse=True)
24def _(testconfig):
25 pass
28def _populate(gauge):
29 for registered_gauge, f in _set_hacky_labeled_gauges_funcs: 29 ↛ 33line 29 didn't jump to line 33 because the loop on line 29 didn't complete
30 if registered_gauge is gauge:
31 f(registered_gauge)
32 return
33 raise AssertionError("gauge is not a registered labeled gauge")
36def _sample_values(gauge):
37 return {
38 sample.labels[gauge._labelnames[0]]: sample.value for metric in gauge.collect() for sample in metric.samples
39 }
42def test_users_per_community_gauge(db):
43 user1, _ = generate_user()
44 user2, _ = generate_user()
45 user3, _ = generate_user()
47 with session_scope() as session:
48 world = create_community(session, 0, 100, "World", [user1], [], None)
49 macroregion = create_community(session, 0, 50, "Macroregion", [user2], [], world)
50 region = create_community(session, 0, 25, "Region", [user3], [user1], macroregion)
51 # subregion is below the region level and must be excluded
52 create_community(session, 0, 10, "Subregion", [user2], [user3], region)
54 # the gauge reads from the cluster_subscription_counts materialized view
55 refresh_materialized_views(empty_pb2.Empty())
57 _populate(users_per_community_gauge)
58 values = _sample_values(users_per_community_gauge)
60 assert values["World"] == 1
61 assert values["Macroregion"] == 1
62 assert values["Region"] == 2
63 assert "Subregion" not in values
66def test_active_users_by_recency_gauge(db):
67 ages = {
68 "<1d": timedelta(hours=2),
69 "1d-1w": timedelta(days=3),
70 "1w-1m": timedelta(days=14),
71 "1m-6m": timedelta(days=60),
72 "6m-12m": timedelta(days=250),
73 "12m-24m": timedelta(days=500),
74 "24m+": timedelta(days=800),
75 }
76 user_ids_by_bucket = {}
77 for bucket in ages:
78 user, _ = generate_user()
79 user_ids_by_bucket[bucket] = user.id
81 with session_scope() as session:
82 for bucket, age in ages.items():
83 session.execute(update(User).where(User.id == user_ids_by_bucket[bucket]).values(last_active=now() - age))
85 _populate(active_users_by_recency_gauge)
86 values = _sample_values(active_users_by_recency_gauge)
88 for bucket in ages:
89 assert values[bucket] == 1
92def _add_activity(user_id: int, client_platform: ClientPlatform | None, age: timedelta = timedelta(minutes=1)) -> None:
93 with session_scope() as session:
94 session.add(UserActivity(user_id=user_id, period=now() - age, client_platform=client_platform, api_calls=1))
97def _platform_metrics() -> dict[str, int]:
98 with session_scope() as session:
99 return dict(session.execute(active_users_by_platform_statement()).one()._mapping)
102def _gauge_value(gauge) -> float:
103 return float(next(sample.value for metric in gauge.collect() for sample in metric.samples))
106def test_active_users_by_platform(db):
107 web_desktop_user, _ = generate_user()
108 web_mobile_user, _ = generate_user()
109 ios_user, _ = generate_user()
110 android_user, _ = generate_user()
111 other_user, _ = generate_user()
113 _add_activity(web_desktop_user.id, ClientPlatform.web_desktop)
114 _add_activity(web_mobile_user.id, ClientPlatform.web_mobile)
115 _add_activity(ios_user.id, ClientPlatform.app_ios)
116 _add_activity(android_user.id, ClientPlatform.app_android)
117 _add_activity(other_user.id, None)
119 # the "other" (null platform) user is counted in the total but in no platform bucket, and mobile is the union of
120 # web_mobile + app_ios + app_android
121 assert _platform_metrics() == {
122 "total": 5,
123 "mobile": 3,
124 "web_desktop": 1,
125 "web_mobile": 1,
126 "app_ios": 1,
127 "app_android": 1,
128 }
131def test_active_users_by_platform_excludes_invisible_users(db):
132 visible_user, _ = generate_user()
133 deleted_user, _ = generate_user(delete_user=True)
135 _add_activity(visible_user.id, ClientPlatform.web_desktop)
136 _add_activity(deleted_user.id, ClientPlatform.web_desktop)
138 assert _platform_metrics() == {
139 "total": 1,
140 "mobile": 0,
141 "web_desktop": 1,
142 "web_mobile": 0,
143 "app_ios": 0,
144 "app_android": 0,
145 }
148def test_active_users_by_platform_counts_user_once_per_platform(db):
149 user, _ = generate_user()
151 _add_activity(user.id, ClientPlatform.web_desktop)
152 _add_activity(user.id, ClientPlatform.app_ios)
154 # the user is counted once in the total and once in mobile (they had app_ios activity), but appears in both the
155 # web_desktop and app_ios breakdowns
156 assert _platform_metrics() == {
157 "total": 1,
158 "mobile": 1,
159 "web_desktop": 1,
160 "web_mobile": 0,
161 "app_ios": 1,
162 "app_android": 0,
163 }
166def test_active_users_by_platform_excludes_old_activity(db):
167 user, _ = generate_user()
169 _add_activity(user.id, ClientPlatform.app_ios, age=timedelta(days=2))
171 assert _platform_metrics() == {
172 "total": 0,
173 "mobile": 0,
174 "web_desktop": 0,
175 "web_mobile": 0,
176 "app_ios": 0,
177 "app_android": 0,
178 }
181def test_active_users_mobile_fraction_gauge(db):
182 desktop_user, _ = generate_user()
183 mobile_web_user, _ = generate_user()
184 ios_user, _ = generate_user()
185 android_user, _ = generate_user()
187 _add_activity(desktop_user.id, ClientPlatform.web_desktop)
188 _add_activity(mobile_web_user.id, ClientPlatform.web_mobile)
189 _add_activity(ios_user.id, ClientPlatform.app_ios)
190 _add_activity(android_user.id, ClientPlatform.app_android)
192 # populating the breakdown gauge also sets the mobile fraction gauge: 3 of 4 active users are on mobile
193 _populate(active_users_by_platform_gauge)
194 assert _gauge_value(active_users_mobile_fraction_gauge) == pytest.approx(0.75)