feat(server): stats queries support include_test filter

Leaderboard and rank queries take an optional include_test param
(default false). Real users never see soak-harness traffic unless
they explicitly opt in via ?include_test=true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-11 00:33:38 -04:00
parent 0891e6c979
commit b5a25b4ae5
2 changed files with 30 additions and 9 deletions

View File

@@ -159,6 +159,7 @@ async def get_leaderboard(
metric: str = Query("wins", pattern="^(wins|win_rate|avg_score|knockouts|streak|rating)$"), metric: str = Query("wins", pattern="^(wins|win_rate|avg_score|knockouts|streak|rating)$"),
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
include_test: bool = Query(False, description="Include soak-harness test accounts"),
service: StatsService = Depends(get_stats_service_dep), service: StatsService = Depends(get_stats_service_dep),
): ):
""" """
@@ -172,8 +173,9 @@ async def get_leaderboard(
- streak: Best win streak - streak: Best win streak
Players must have 5+ games to appear on leaderboards. Players must have 5+ games to appear on leaderboards.
By default, soak-harness test accounts are hidden.
""" """
entries = await service.get_leaderboard(metric, limit, offset) entries = await service.get_leaderboard(metric, limit, offset, include_test)
return { return {
"metric": metric, "metric": metric,
@@ -228,10 +230,11 @@ async def get_player_stats(
async def get_player_rank( async def get_player_rank(
user_id: str, user_id: str,
metric: str = Query("wins", pattern="^(wins|win_rate|avg_score|knockouts|streak|rating)$"), metric: str = Query("wins", pattern="^(wins|win_rate|avg_score|knockouts|streak|rating)$"),
include_test: bool = Query(False, description="Include soak-harness test accounts"),
service: StatsService = Depends(get_stats_service_dep), service: StatsService = Depends(get_stats_service_dep),
): ):
"""Get player's rank on a leaderboard.""" """Get player's rank on a leaderboard."""
rank = await service.get_player_rank(user_id, metric) rank = await service.get_player_rank(user_id, metric, include_test)
return { return {
"user_id": user_id, "user_id": user_id,
@@ -348,11 +351,12 @@ async def get_my_stats(
@router.get("/me/rank", response_model=PlayerRankResponse) @router.get("/me/rank", response_model=PlayerRankResponse)
async def get_my_rank( async def get_my_rank(
metric: str = Query("wins", pattern="^(wins|win_rate|avg_score|knockouts|streak|rating)$"), metric: str = Query("wins", pattern="^(wins|win_rate|avg_score|knockouts|streak|rating)$"),
include_test: bool = Query(False, description="Include soak-harness test accounts"),
user: User = Depends(require_user), user: User = Depends(require_user),
service: StatsService = Depends(get_stats_service_dep), service: StatsService = Depends(get_stats_service_dep),
): ):
"""Get current user's rank on a leaderboard.""" """Get current user's rank on a leaderboard."""
rank = await service.get_player_rank(user.id, metric) rank = await service.get_player_rank(user.id, metric, include_test)
return { return {
"user_id": user.id, "user_id": user.id,

View File

@@ -171,6 +171,7 @@ class StatsService:
metric: str = "wins", metric: str = "wins",
limit: int = 50, limit: int = 50,
offset: int = 0, offset: int = 0,
include_test: bool = False,
) -> List[LeaderboardEntry]: ) -> List[LeaderboardEntry]:
""" """
Get leaderboard by metric. Get leaderboard by metric.
@@ -179,6 +180,8 @@ class StatsService:
metric: Ranking metric - wins, win_rate, avg_score, knockouts, streak. metric: Ranking metric - wins, win_rate, avg_score, knockouts, streak.
limit: Maximum entries to return. limit: Maximum entries to return.
offset: Pagination offset. offset: Pagination offset.
include_test: If True, include soak-harness test accounts. Default
False so real users never see synthetic load-test traffic.
Returns: Returns:
List of LeaderboardEntry sorted by metric. List of LeaderboardEntry sorted by metric.
@@ -212,9 +215,10 @@ class StatsService:
COALESCE(rating, 1500) as rating, COALESCE(rating, 1500) as rating,
ROW_NUMBER() OVER (ORDER BY {column} {direction}) as rank ROW_NUMBER() OVER (ORDER BY {column} {direction}) as rank
FROM leaderboard_overall FROM leaderboard_overall
WHERE ($3 OR NOT is_test_account)
ORDER BY {column} {direction} ORDER BY {column} {direction}
LIMIT $1 OFFSET $2 LIMIT $1 OFFSET $2
""", limit, offset) """, limit, offset, include_test)
else: else:
# Fall back to direct query # Fall back to direct query
rows = await conn.fetch(f""" rows = await conn.fetch(f"""
@@ -230,9 +234,10 @@ class StatsService:
WHERE s.games_played >= 5 WHERE s.games_played >= 5
AND u.deleted_at IS NULL AND u.deleted_at IS NULL
AND (u.is_banned = false OR u.is_banned IS NULL) AND (u.is_banned = false OR u.is_banned IS NULL)
AND ($3 OR NOT COALESCE(u.is_test_account, FALSE))
ORDER BY {column} {direction} ORDER BY {column} {direction}
LIMIT $1 OFFSET $2 LIMIT $1 OFFSET $2
""", limit, offset) """, limit, offset, include_test)
return [ return [
LeaderboardEntry( LeaderboardEntry(
@@ -246,16 +251,26 @@ class StatsService:
for row in rows for row in rows
] ]
async def get_player_rank(self, user_id: str, metric: str = "wins") -> Optional[int]: async def get_player_rank(
self,
user_id: str,
metric: str = "wins",
include_test: bool = False,
) -> Optional[int]:
""" """
Get a player's rank on a leaderboard. Get a player's rank on a leaderboard.
Args: Args:
user_id: User UUID. user_id: User UUID.
metric: Ranking metric. metric: Ranking metric.
include_test: If True, rank within a leaderboard that includes
soak-harness test accounts. Default False matches the public
leaderboard view.
Returns: Returns:
Rank number or None if not ranked (< 5 games or not found). Rank number or None if not ranked (< 5 games or not found, or
filtered out because they are a test account and include_test is
False).
""" """
order_map = { order_map = {
"wins": ("games_won", "DESC"), "wins": ("games_won", "DESC"),
@@ -288,9 +303,10 @@ class StatsService:
SELECT rank FROM ( SELECT rank FROM (
SELECT user_id, ROW_NUMBER() OVER (ORDER BY {column} {direction}) as rank SELECT user_id, ROW_NUMBER() OVER (ORDER BY {column} {direction}) as rank
FROM leaderboard_overall FROM leaderboard_overall
WHERE ($2 OR NOT is_test_account)
) ranked ) ranked
WHERE user_id = $1 WHERE user_id = $1
""", user_id) """, user_id, include_test)
else: else:
row = await conn.fetchrow(f""" row = await conn.fetchrow(f"""
SELECT rank FROM ( SELECT rank FROM (
@@ -300,9 +316,10 @@ class StatsService:
WHERE s.games_played >= 5 WHERE s.games_played >= 5
AND u.deleted_at IS NULL AND u.deleted_at IS NULL
AND (u.is_banned = false OR u.is_banned IS NULL) AND (u.is_banned = false OR u.is_banned IS NULL)
AND ($2 OR NOT COALESCE(u.is_test_account, FALSE))
) ranked ) ranked
WHERE user_id = $1 WHERE user_id = $1
""", user_id) """, user_id, include_test)
return row["rank"] if row else None return row["rank"] if row else None