('total');
const { rows, categories } = useMemo(() => {
if (dimension === 'total') {
- const days = Object.keys(total).sort();
- return { rows: days.map((day) => ({ day, n: total[day] })), categories: ['n'] };
+ const days = [...new Set([...Object.keys(total), ...Object.keys(visitors)])].sort();
+ return {
+ rows: days.map((day) => ({ day, n: total[day] ?? 0, visitors: visitors[day] ?? 0 })),
+ categories: ['n', 'visitors'],
+ };
}
const byDay = dimension === 'device' ? byDevice : dimension === 'browser' ? byBrowser : byOs;
const days = Object.keys(byDay).sort();
@@ -47,7 +61,9 @@ export default function PageviewsChart({ total, byDevice, byBrowser, byOs }: Pro
return row;
});
return { rows, categories };
- }, [dimension, total, byDevice, byBrowser, byOs]);
+ }, [dimension, total, visitors, byDevice, byBrowser, byOs]);
+
+ const lineName = (c: string) => (c === 'n' ? totalLabel : c === 'visitors' ? visitorsLabel : c);
return (
@@ -73,13 +89,13 @@ export default function PageviewsChart({ total, byDevice, byBrowser, byOs }: Pro
- {dimension !== 'total' && }
+
{categories.map((c, i) => (
-- nesmie sa orezat
+ self.assertEqual(stats._normalize_path("/gamex"), "/gamex")
+
+ def test_skipped_paths_not_recorded(self):
+ # "/", "/lobby" a "/game" (aj s dynamickym ID) sa vobec nezapisuju --
+ # vysoka frekvencia bez analytickej hodnoty (api/stats.py _SKIPPED_PATHS).
+ async def _count():
+ from sqlalchemy import func, select as sa_select
+
+ from db.db import async_session
+ from db.models import PageView
+
+ async with async_session() as session:
+ return (
+ await session.execute(sa_select(func.count()).select_from(PageView))
+ ).scalar()
+
+ before = run(_count())
+ for path in ("/", "/lobby", "/lobby/abc-123", "/game", "/game/xyz-789"):
+ run(stats.record_pageview(path=path, referrer="", user_agent=CHROME_UA))
+ after = run(_count())
+ self.assertEqual(after, before)
+
+ def test_event_path_stores_player_id(self):
+ # Konvencia: nazov bez "/" na zaciatku = pomenovany event (napr. "login"
+ # zapisovany zo socket handlera po uspesnom prihlaseni), nie URL cesta.
+ ids = self._make_players(n=1)
+ run(stats.record_pageview(
+ path="login", referrer="", user_agent=CHROME_UA, player_id=ids[0],
+ ))
+
+ async def _last():
+ from sqlalchemy import select as sa_select
+
+ from db.db import async_session
+ from db.models import PageView
+
+ async with async_session() as session:
+ row = (
+ await session.execute(
+ sa_select(PageView).order_by(PageView.id.desc()).limit(1)
+ )
+ ).scalar_one()
+ return row.path, row.player_id
+
+ path, player_id = run(_last())
+ self.assertEqual(path, "login")
+ self.assertEqual(player_id, ids[0])
+
+ data = run(stats.get_daily_stats())
+ self.assertGreaterEqual(data["top_paths"].get("login", 0), 1)
+
+ def test_rules_view_event_recorded_without_player_id(self):
+ run(stats.record_pageview(path="rules_view", referrer="", user_agent=CHROME_UA))
+ data = run(stats.get_daily_stats())
+ self.assertGreaterEqual(data["top_paths"].get("rules_view", 0), 1)
+
+ def test_logged_in_only_scope_counts_login_events_only(self):
+ # Bezne beacony uz neposielaju player_id vobec -- scope "logged_in"
+ # preto filtruje priamo podla path == "login", nie podla pritomnosti
+ # player_id. Obycajna navsteva (aj s player_id) sa do neho nepocita.
+ ids = self._make_players(n=1)
+ ip = "203.0.113.99"
+ before_all = run(stats.get_daily_stats(logged_in_only=False))
+ before_logged_in = run(stats.get_daily_stats(logged_in_only=True))
+ all_before = sum(before_all["pageviews_per_day"].values())
+ logged_in_before = sum(before_logged_in["pageviews_per_day"].values())
+
+ # Bezna navsteva -- ma pribudnut len vo "vsetci", aj keby mala player_id.
+ run(stats.record_pageview(
+ path="/history", referrer="", user_agent=CHROME_UA, ip=ip, player_id=ids[0],
+ ))
+ # Login event -- ma pribudnut v oboch.
+ run(stats.record_pageview(
+ path="login", referrer="", user_agent=CHROME_UA, ip=ip, player_id=ids[0],
+ ))
+
+ after_all = run(stats.get_daily_stats(logged_in_only=False))
+ after_logged_in = run(stats.get_daily_stats(logged_in_only=True))
+
+ self.assertEqual(sum(after_all["pageviews_per_day"].values()), all_before + 2)
+ self.assertEqual(sum(after_logged_in["pageviews_per_day"].values()), logged_in_before + 1)
+
+ def test_logged_in_only_breakdown_tables_count_each_login_not_deduped(self):
+ # browsers/os/device_types/top_referrers/countries pouzivaju v scope
+ # "logged_in" priamy pocet login-eventov, nie unikatny navstevnicky
+ # den -- 2x prihlasenie tym istym prehliadacom/IP v ten isty den sa
+ # ma prejavit ako 2, rovnako ako v grafe pageviews_per_day.
+ ids = self._make_players(n=1)
+ before = run(stats.get_daily_stats(logged_in_only=True))
+ before_browsers = sum(before["browsers"].values())
+
+ run(stats.record_pageview(path="login", referrer="", user_agent=CHROME_UA, player_id=ids[0]))
+ run(stats.record_pageview(path="login", referrer="", user_agent=CHROME_UA, player_id=ids[0]))
+
+ data = run(stats.get_daily_stats(logged_in_only=True))
+ self.assertEqual(sum(data["browsers"].values()), before_browsers + 2)
+
+ def test_logged_in_visitors_per_day_counts_distinct_players_not_ip(self):
+ # V scope "logged_in" ma visitors_per_day znamenat unikatnych HRACOV
+ # (player_id) za den, nie unikatne IP+UA -- 2x prihlasenie toho
+ # isteho hraca (aj z inej IP/prehliadaca) sa ma pocitat len raz, na
+ # rozdiel od pageviews_per_day, kde sa kazdy login pocita zvlast.
+ ids = self._make_players(n=2)
+ before = run(stats.get_daily_stats(logged_in_only=True))
+ before_visitors = sum(before["visitors_per_day"].values())
+ before_pageviews = sum(before["pageviews_per_day"].values())
+
+ # Ten isty hrac, 2x prihlasenie z roznych "zariadeni" (rozne IP/UA).
+ run(stats.record_pageview(
+ path="login", referrer="", user_agent=CHROME_UA, ip="203.0.113.10", player_id=ids[0],
+ ))
+ run(stats.record_pageview(
+ path="login", referrer="", user_agent=CHROME_UA, ip="203.0.113.11", player_id=ids[0],
+ ))
+ # Iny hrac, 1x prihlasenie.
+ run(stats.record_pageview(
+ path="login", referrer="", user_agent=CHROME_UA, ip="203.0.113.12", player_id=ids[1],
+ ))
+
+ data = run(stats.get_daily_stats(logged_in_only=True))
+ self.assertEqual(sum(data["pageviews_per_day"].values()), before_pageviews + 3)
+ self.assertEqual(sum(data["visitors_per_day"].values()), before_visitors + 2)
+
+ def test_visitors_counted_once_per_day_per_ip(self):
+ # 3 kliky z tej istej IP v ten isty den -> pageviews +3, visitors iba 1.
+ ip = "198.51.100.77"
+ for path in ("/history", "/history", "/history"):
+ run(stats.record_pageview(path=path, referrer="", user_agent=CHROME_UA, ip=ip))
+
+ data = run(stats.get_daily_stats())
+ today = next(iter(data["visitors_per_day"]))
+ # V testovej DB su vsetky zaznamy z dneska; unikatnych IP je menej nez klikov.
+ self.assertLess(data["visitors_per_day"][today], data["pageviews_per_day"][today])
+
+ # Tabulky zlozenia publika pocitaju den+IP raz -- 3 kliky tej istej IP
+ # nesmu zdvihnut "Chrome" o 3. Overime, ze pocet je mensi nez pocet klikov.
+ self.assertLess(data["browsers"].get("Chrome", 0), sum(data["pageviews_per_day"].values()))
+
+ def test_same_ip_different_user_agent_counts_as_two_visitors(self):
+ ip = "192.0.2.44"
+ firefox_ua = (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) "
+ "Gecko/20100101 Firefox/126.0"
+ )
+ before = run(stats.get_daily_stats())
+ today_before = sum(before["visitors_per_day"].values())
+
+ # Ta ista IP, dva rozne prehliadace -> 2 navstevnici (kazdy klikne 2x).
+ for _ in range(2):
+ run(stats.record_pageview(path="/history", referrer="", user_agent=CHROME_UA, ip=ip))
+ run(stats.record_pageview(path="/history", referrer="", user_agent=firefox_ua, ip=ip))
+
+ after = run(stats.get_daily_stats())
+ self.assertEqual(sum(after["visitors_per_day"].values()), today_before + 2)
+
def test_pageviews_per_day_breakdown_by_dimension(self):
- run(stats.record_pageview(path="/", referrer="", user_agent=CHROME_UA))
+ run(stats.record_pageview(path="/history", referrer="", user_agent=CHROME_UA))
data = run(stats.get_daily_stats())
today = next(iter(data["pageviews_per_day_by_device"]))
self.assertGreaterEqual(data["pageviews_per_day_by_device"][today].get("pc", 0), 1)
self.assertIn("Chrome", data["pageviews_per_day_by_browser"][today])
self.assertIn("Windows", data["pageviews_per_day_by_os"][today])
+ def test_all_scope_chart_breakdown_matches_composition_tables(self):
+ # Bug hlaseny uzivatelom: v scope "vsetci" graf (pageviews_per_day_by_*)
+ # pocital kazdy klik zvlast, zatial co tabulky nizsie (browsers/os/
+ # device_types) pocitali unikatny "navstevnicky den" -- cisla si tak
+ # nesedeli. Oboje ma teraz rovnaku dedup logiku (jeden navstevnik +
+ # kategoria + den = 1), takze sucty musia byt zhodne.
+ ip = "203.0.113.50"
+ for _ in range(3):
+ run(stats.record_pageview(path="/history", referrer="", user_agent=CHROME_UA, ip=ip))
+
+ data = run(stats.get_daily_stats(logged_in_only=False))
+ chart_pc_total = sum(day.get("pc", 0) for day in data["pageviews_per_day_by_device"].values())
+ chart_chrome_total = sum(
+ day.get("Chrome", 0) for day in data["pageviews_per_day_by_browser"].values()
+ )
+ self.assertEqual(chart_pc_total, data["device_types"].get("pc", 0))
+ self.assertEqual(chart_chrome_total, data["browsers"].get("Chrome", 0))
+
def test_daily_stats_reflect_games_and_players(self):
before = run(stats.get_daily_stats())
base_total_players = before["total_players"]
@@ -126,7 +305,7 @@ class StatsCase(unittest.TestCase):
self.assertGreaterEqual(sum(data["rounds_per_day"].values()), 4)
def test_empty_referrer_excluded_from_top_referrers(self):
- run(stats.record_pageview(path="/lobby", referrer="", user_agent=CHROME_UA))
+ run(stats.record_pageview(path="/history", referrer="", user_agent=CHROME_UA))
data = run(stats.get_daily_stats())
self.assertNotIn("", data["top_referrers"])