Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/kernelbot/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from dataclasses import asdict
from typing import Annotated, Any, Optional

from fastapi import Depends, FastAPI, Header, HTTPException, Request, UploadFile
from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse

from kernelbot.env import env
Expand Down Expand Up @@ -586,9 +586,10 @@ async def admin_stats(
_: Annotated[None, Depends(require_admin)],
db_context=Depends(get_db),
last_day_only: bool = False,
leaderboard_name: Optional[str] = Query(None, description="Filter stats to a specific leaderboard name"),
) -> dict:
Comment on lines 586 to 590
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new leaderboard_name query parameter will show up in OpenAPI, but without any description/constraints. Consider using FastAPI's Query(...) metadata (e.g., description like 'Filter stats to a specific leaderboard name') so API consumers can discover the feature from docs and generated clients.

Copilot uses AI. Check for mistakes.
with db_context as db:
stats = db.generate_stats(last_day_only)
stats = db.generate_stats(last_day_only, leaderboard_name)
return {"status": "ok", "stats": stats}


Expand Down
12 changes: 9 additions & 3 deletions src/kernelbot/cogs/admin_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,8 +764,14 @@ async def update_competition(
logger.exception("Error updating problem set", exc_info=e)

@with_error_handling
@discord.app_commands.describe(last_day_only="Only show stats for the last day")
async def show_bot_stats(self, interaction: discord.Interaction, last_day_only: bool):
@discord.app_commands.describe(
last_day_only="Only show stats for the last day",
leaderboard_name="Filter stats to a specific leaderboard (default: all)",
)
@discord.app_commands.autocomplete(leaderboard_name=leaderboard_name_autocomplete)
async def show_bot_stats(
self, interaction: discord.Interaction, last_day_only: bool, leaderboard_name: Optional[str] = None
):
is_admin = await self.admin_check(interaction)
if not is_admin:
await send_discord_message(
Expand All @@ -776,7 +782,7 @@ async def show_bot_stats(self, interaction: discord.Interaction, last_day_only:
return

with self.bot.leaderboard_db as db:
stats = db.generate_stats(last_day_only)
stats = db.generate_stats(last_day_only, leaderboard_name)
msg = """```"""
for k, v in stats.items():
msg += f"\n{k} = {v}"
Expand Down
86 changes: 62 additions & 24 deletions src/libkernelbot/leaderboard_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,15 +713,33 @@ def get_leaderboard_submissions(

return result

def generate_stats(self, last_day: bool):
def generate_stats(self, last_day: bool, leaderboard_name: Optional[str] = None):
try:
return self._generate_stats(last_day)
return self._generate_stats(last_day, leaderboard_name)
except Exception as e:
logger.exception("error generating stats", exc_info=e)
raise

def _generate_runner_stats(self, last_day: bool = False):
select_expr = "WHERE NOW() - s.submission_time <= interval '24 hours'" if last_day else ""
@staticmethod
def _stats_filter(last_day: bool, leaderboard_name: Optional[str], submission_alias: str = "s"):
joins = ""
conditions = []
params = []

if leaderboard_name:
joins = f"JOIN leaderboard.leaderboard lb ON {submission_alias}.leaderboard_id = lb.id"
conditions.append("lb.name = %s")
params.append(leaderboard_name)

if last_day:
conditions.append(f"NOW() - {submission_alias}.submission_time <= interval '24 hours'")

where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ""
return joins, where_clause, params

def _generate_runner_stats(self, last_day: bool = False, leaderboard_name: Optional[str] = None):
joins, where_clause, params = self._stats_filter(last_day, leaderboard_name)

# per-runner stats
self.cursor.execute(
f"""
Expand All @@ -735,9 +753,11 @@ def _generate_runner_stats(self, last_day: bool = False):
AVG(runs.start_time - s.submission_time),
SUM(runs.end_time - runs.start_time)
FROM leaderboard.runs JOIN leaderboard.submission s ON submission_id = s.id
{select_expr}
{joins}
{where_clause}
GROUP BY runner;
"""
""",
params,
)

result = {}
Expand All @@ -752,18 +772,21 @@ def _generate_runner_stats(self, last_day: bool = False):

return result

def _generate_submission_stats(self, last_day: bool = False):
select_expr = "WHERE NOW() - submission_time <= interval '24 hours'" if last_day else ""
def _generate_submission_stats(self, last_day: bool = False, leaderboard_name: Optional[str] = None):
joins, where_clause, params = self._stats_filter(last_day, leaderboard_name)

Comment on lines 775 to 777
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JOIN/conditions/params/where-clause construction is duplicated across _generate_runner_stats, _generate_submission_stats, and the heavy-hitters query in _generate_stats. To reduce drift and make future filters easier to add, consider extracting a small helper that returns (joins_sql, where_sql, params) given last_day + leaderboard_name (and any table alias differences), and reuse it in all three places.

Copilot uses AI. Check for mistakes.
self.cursor.execute(
f"""
SELECT
COUNT(*),
COUNT(*) FILTER (WHERE NOT done),
COUNT(DISTINCT user_id)
FROM leaderboard.submission
{select_expr}
COUNT(*) FILTER (WHERE NOT s.done),
COUNT(DISTINCT s.user_id)
FROM leaderboard.submission s
{joins}
{where_clause}
;
"""
""",
params,
)
Comment on lines 775 to 790
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In _generate_submission_stats, several columns in the WHERE/SELECT are unqualified (submission_time, done, user_id), while the join condition references leaderboard.submission explicitly. For clarity and to avoid future ambiguity if similarly named columns are added via joins, consider aliasing leaderboard.submission (e.g., FROM leaderboard.submission s) and consistently qualifying references (s.submission_time, s.done, s.user_id, and s.leaderboard_id).

Copilot uses AI. Check for mistakes.
num_sub, num_sub_wait, num_users = self.cursor.fetchone()
return {
Expand All @@ -772,30 +795,44 @@ def _generate_submission_stats(self, last_day: bool = False):
"num_users": num_users,
}

def _generate_stats(self, last_day: bool = False):
result = self._generate_submission_stats(last_day)
result.update(self._generate_runner_stats(last_day))
def _generate_stats(self, last_day: bool = False, leaderboard_name: Optional[str] = None):
result = self._generate_submission_stats(last_day, leaderboard_name)
result.update(self._generate_runner_stats(last_day, leaderboard_name))

# code-level stats
if not last_day:
self.cursor.execute(
"""
SELECT COUNT(*) FROM leaderboard.code_files;
"""
)
if leaderboard_name:
self.cursor.execute(
"""
SELECT COUNT(DISTINCT s.code_id)
FROM leaderboard.submission s
JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id
WHERE lb.name = %s;
""",
(leaderboard_name,),
)
else:
self.cursor.execute(
"""
SELECT COUNT(*) FROM leaderboard.code_files;
"""
)
result["num_unique_codes"] = self.cursor.fetchone()[0]

else:
# calculate heavy hitters
joins, where_clause, params = self._stats_filter(last_day, leaderboard_name)

self.cursor.execute(
"""
f"""
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The query string has been changed from a regular string to an f-string, but the parameterized values are still passed separately via the params list. This mixing of f-string interpolation for structure (joins, where_clause) with parameterized queries for values is correct, but ensure all user-supplied values (like leaderboard_name) continue to use parameterized queries to prevent SQL injection.

Copilot uses AI. Check for mistakes.
WITH run_durations AS (
SELECT
s.user_id AS user_id,
r.end_time - r.start_time AS duration
FROM leaderboard.runs r
JOIN leaderboard.submission s ON r.submission_id = s.id
WHERE NOW() - s.submission_time <= interval '24 hours'
{joins}
{where_clause}
)
SELECT
user_id,
Expand All @@ -804,7 +841,8 @@ def _generate_stats(self, last_day: bool = False):
GROUP BY user_id
ORDER BY total DESC
LIMIT 10;
"""
""",
params,
)

for row in self.cursor.fetchall():
Expand Down
22 changes: 21 additions & 1 deletion tests/test_admin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,27 @@ def test_admin_stats_last_day_only(self, test_client, mock_backend):
headers={"Authorization": "Bearer test_token"}
)
assert response.status_code == 200
mock_backend.db.generate_stats.assert_called_once_with(True)
mock_backend.db.generate_stats.assert_called_once()
args, kwargs = mock_backend.db.generate_stats.call_args
assert args[0] is True # last_day_only
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test verifies the last_day_only parameter but doesn't check the leaderboard_name parameter. Add an assertion to verify that leaderboard_name is None or not passed when not specified in the request.

Copilot uses AI. Check for mistakes.

def test_admin_stats_with_leaderboard_name(self, test_client, mock_backend):
"""GET /admin/stats with leaderboard_name parameter."""
mock_backend.db.__enter__ = MagicMock(return_value=mock_backend.db)
mock_backend.db.__exit__ = MagicMock(return_value=None)
mock_backend.db.generate_stats = MagicMock(return_value={
"num_submissions": 5,
"num_users": 3,
})

response = test_client.get(
"/admin/stats?leaderboard_name=my-leaderboard",
headers={"Authorization": "Bearer test_token"}
)
assert response.status_code == 200
mock_backend.db.generate_stats.assert_called_once()
args, kwargs = mock_backend.db.generate_stats.call_args
assert args[1] == "my-leaderboard" # leaderboard_name
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should verify the last_day_only parameter value in addition to leaderboard_name. Add an assertion to ensure last_day_only defaults to False when not specified.

Copilot uses AI. Check for mistakes.


class TestAdminSubmissions:
Expand Down
23 changes: 23 additions & 0 deletions tests/test_leaderboard_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,29 @@ def test_generate_stats(database, submit_leaderboard):
"total_runtime.A100": datetime.timedelta(seconds=35),
}

# Same results when filtering by the correct leaderboard
assert db.generate_stats(False, leaderboard_name="submit-leaderboard") == {
"avg_delay.A100": datetime.timedelta(seconds=10),
"max_delay.A100": datetime.timedelta(seconds=20),
"num_run.A100": 3,
"num_submissions": 1,
"num_unique_codes": 1,
"num_users": 1,
"runs_passed.A100": 3,
"runs_scored.A100": 3,
"runs_secret.A100": 1,
"sub_waiting": 0,
"total_runtime.A100": datetime.timedelta(seconds=35),
}

# Empty results when filtering by a non-existent leaderboard
assert db.generate_stats(False, leaderboard_name="nonexistent") == {
"num_submissions": 0,
"sub_waiting": 0,
"num_users": 0,
"num_unique_codes": 0,
}


def test_get_user_submissions_empty(database, submit_leaderboard):
"""Test get_user_submissions returns empty list for user with no submissions"""
Expand Down
Loading