Skip to content

Discord: Automate Stat Messages#1012

Open
Prajna1999 wants to merge 9 commits into
mainfrom
feat/automate-stats-messages-basic-queries
Open

Discord: Automate Stat Messages#1012
Prajna1999 wants to merge 9 commits into
mainfrom
feat/automate-stats-messages-basic-queries

Conversation

@Prajna1999

@Prajna1999 Prajna1999 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Issue

Closes #825

Summary

Automates a daily platform stats digest to a Discord channel via webhook. Adds /cron/daily-stats that runs a set of per-organization SQL rollups (LLM call counts, total tokens by model, modality mix, job types, evaluation runs, STT/TTS results, assessments), formats them as compact plain-text tables, chunks past Discord's 2000-char limit, and posts fire-and-forget so a webhook outage never breaks the cron.

Query/format/post logic lives in
app/services/stats.py; the route is a thin orchestrator. Window defaults to 7 days and accepts a ?hours=N override for backfills.

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Summary by CodeRabbit

  • New Features
    • Added automated daily statistics collection across key activity areas.
    • Added a protected endpoint to generate and deliver formatted statistics reports.
    • Added optional Discord webhook delivery, including message chunking and failure handling.
    • Added scheduled daily execution at midnight UTC.
  • Tests
    • Added coverage for report formatting, time windows, message splitting, and webhook behavior.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • ready-for-review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a6acb9e-fb7f-487e-9bb9-7238708b4af8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds daily statistics aggregation across database tables, Discord message formatting and delivery, a protected monitored cron endpoint, webhook configuration, periodic invoker wiring, and service tests.

Changes

Daily statistics pipeline

Layer / File(s) Summary
Statistics aggregation
backend/app/crud/stats.py
Adds time-windowed SQL aggregation for LLM calls, tokens, modalities, job types, evaluation runs, STT/TTS results, and assessments.
Stats rendering and Discord delivery
backend/app/services/stats.py, backend/app/core/config.py
Computes collection windows, formats and chunks statistics messages, and posts them to the optional Discord webhook.
Scheduled cron endpoint
backend/app/api/routes/cron.py, scripts/python/invoke-cron.py
Adds the monitored SUPERUSER-protected daily statistics route and includes it in periodic cron invocation.
Statistics pipeline tests
backend/app/tests/services/test_stats.py
Tests formatting, truncation, chunking, webhook behavior, and configurable collection windows.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CronInvoker
  participant daily_stats_cron_job
  participant collect_daily_stats
  participant Database
  participant Discord
  CronInvoker->>daily_stats_cron_job: GET /cron/daily-stats
  daily_stats_cron_job->>collect_daily_stats: optional window_hours
  collect_daily_stats->>Database: execute time-windowed statistics queries
  Database-->>collect_daily_stats: categorized statistics
  daily_stats_cron_job->>Discord: post formatted message chunks
  Discord-->>daily_stats_cron_job: HTTP response
Loading

Possibly related PRs

Suggested reviewers: akhileshnegi

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR automates Discord stats, but it defaults to a 7-day window and doesn't clearly implement the requested yesterday-only success/failure counts. Change the cron to report yesterday's traffic by default and ensure the Discord message includes the required success and failure counts.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed All changes support the stats automation workflow: cron route, stats collection, Discord posting, config, and tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: automating Discord stats messages.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/automate-stats-messages-basic-queries

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot changed the title feat: basic stats queries feat(stats): Implement basic stats queries Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

OpenAPI changes   ⚪ No API surface changes

Note

This PR does not modify the API contract.

mainc0625318 · generated by oasdiff

@Prajna1999 Prajna1999 self-assigned this Jul 7, 2026
@Prajna1999 Prajna1999 added the enhancement New feature or request label Jul 7, 2026
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.82297% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
backend/app/api/routes/cron.py 33.33% 10 Missing ⚠️
backend/app/crud/stats.py 81.81% 4 Missing ⚠️
backend/app/services/stats.py 98.71% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Prajna1999 Prajna1999 linked an issue Jul 10, 2026 that may be closed by this pull request
@Prajna1999 Prajna1999 changed the title feat(stats): Implement basic stats queries Discord: Automate Stat Messages Jul 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
backend/app/tests/services/test_stats.py (1)

134-134: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silence Ruff unused-argument warnings in mock helpers.

Ruff flags unused url, timeout (and json in flaky_post) in the mock callables. Use *_args, **_kwargs to match the requests.post signature without naming unused parameters.

♻️ Proposed fix
-    def fake_post(url, json, timeout):
+    def fake_post(*_args, **kwargs):
-        posted.append(json["content"])
+        posted.append(kwargs["json"]["content"])
-    def flaky_post(url, json, timeout):
+    def flaky_post(*_args, **_kwargs):
         calls["n"] += 1
         raise requests.ConnectionError("nope")

Also applies to: 161-161

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/tests/services/test_stats.py` at line 134, Update the mock
helpers fake_post and flaky_post in the stats tests to accept unused positional
and keyword arguments via *_args and **_kwargs instead of naming unused request
parameters, while preserving each helper’s existing behavior.

Source: Linters/SAST tools

backend/app/crud/stats.py (1)

76-88: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

SQL identifier interpolation via f-string is a fragile pattern.

_org_count_sql interpolates table directly into the SQL string. All current callers pass hardcoded literals, so there is no immediate injection risk, but this pattern will silently allow SQL injection if a future caller passes dynamic input. Consider adding a whitelist assertion or a comment documenting the constraint.

🛡️ Proposed guard
 def _org_count_sql(table: str) -> TextClause:
+    # table must be a hardcoded literal — never user input
+    assert table.isidentifier(), f"Invalid table name: {table}"
     return text(
         f"""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/crud/stats.py` around lines 76 - 88, Protect the table identifier
interpolated by `_org_count_sql` from future dynamic input. Add an explicit
whitelist validation for the supported table names before constructing the SQL,
or document and enforce that callers may only provide trusted constants; raise
an appropriate error for unsupported values.
backend/app/api/routes/cron.py (1)

151-163: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use consistent logging format.

Line 151 and 162 use f-strings while line 156 uses %-style interpolation. Prefer %-style for logging to enable lazy evaluation, or at minimum be consistent within the same function.

♻️ Suggested consistency fix (f-string → %-style)
 def daily_stats_cron_job(session: SessionDep, hours: int | None = None) -> dict[str, Any]:
-    logger.info(f"[daily_stats_cron_job] Cron job invoked | hours={hours}")
+    logger.info("[daily_stats_cron_job] Cron job invoked | hours=%s", hours)
     try:
         result = collect_daily_stats(session=session, window_hours=hours)
         post_daily_stats_to_discord(format_daily_stats_message(result))
         logger.info(
             "[daily_stats_cron_job] Completed | window: %s",
             result["window"],
         )
         return result
     except Exception as e:
         logger.error(
-            f"[daily_stats_cron_job] Error executing cron job: {e}",
+            "[daily_stats_cron_job] Error executing cron job: %s",
+            e,
             exc_info=True,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/routes/cron.py` around lines 151 - 163, Use consistent lazy
%-style logging in the daily_stats_cron_job function: replace the f-string
messages in the invocation and exception logger calls with format strings and
separate arguments, matching the existing completion log.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/api/routes/cron.py`:
- Line 150: Update the return annotation of daily_stats_cron_job from dict to
dict[str, Any], matching collect_daily_stats and the project’s requirement for
parameterized, specific return types; ensure Any is imported if needed.

In `@backend/app/crud/stats.py`:
- Line 76: Replace the Any annotations in _org_count_sql and _rows with
sqlalchemy.TextClause, importing TextClause as needed; keep the existing text()
SQL construction unchanged.

In `@backend/app/services/stats.py`:
- Around line 22-25: In the window calculation within the stats function,
replace the truthiness check on window_hours with an explicit None check so a
value of 0 produces a zero-duration window while only omitted values use
DAILY_WINDOW.
- Around line 120-131: Update _chunk_message to split any individual section
that exceeds _DISCORD_CHUNK_LIMIT into smaller newline-based chunks before or
during normal paragraph chunking, ensuring every returned chunk stays within the
limit. Preserve section content and ordering so post_daily_stats_to_discord can
send the complete digest without oversized Discord messages.

In `@scripts/python/invoke-cron.py`:
- Line 21: The ENDPOINTS list in invoke-cron.py incorrectly includes
/api/v1/cron/daily-stats in the shared five-minute loop, causing repeated daily
digests. Remove it from the shared list and invoke it through a separate daily
scheduler or guard it with a daily-only condition consistent with the configured
0 0 * * * schedule.

---

Nitpick comments:
In `@backend/app/api/routes/cron.py`:
- Around line 151-163: Use consistent lazy %-style logging in the
daily_stats_cron_job function: replace the f-string messages in the invocation
and exception logger calls with format strings and separate arguments, matching
the existing completion log.

In `@backend/app/crud/stats.py`:
- Around line 76-88: Protect the table identifier interpolated by
`_org_count_sql` from future dynamic input. Add an explicit whitelist validation
for the supported table names before constructing the SQL, or document and
enforce that callers may only provide trusted constants; raise an appropriate
error for unsupported values.

In `@backend/app/tests/services/test_stats.py`:
- Line 134: Update the mock helpers fake_post and flaky_post in the stats tests
to accept unused positional and keyword arguments via *_args and **_kwargs
instead of naming unused request parameters, while preserving each helper’s
existing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1695d50d-6e21-4a4d-848b-95ea5e43a0ae

📥 Commits

Reviewing files that changed from the base of the PR and between 102b61c and bcc57ea.

📒 Files selected for processing (6)
  • backend/app/api/routes/cron.py
  • backend/app/core/config.py
  • backend/app/crud/stats.py
  • backend/app/services/stats.py
  • backend/app/tests/services/test_stats.py
  • scripts/python/invoke-cron.py

monitor_slug="daily-stats-cron-job",
monitor_config=DAILY_STATS_CRON_MONITOR_CONFIG,
)
def daily_stats_cron_job(session: SessionDep, hours: int | None = None) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a parameterized return type.

-> dict is unparameterized (equivalent to dict[Any, Any]). Per coding guidelines, type hints must be specific; use dict[str, Any] to match the return type of collect_daily_stats.

As per coding guidelines: "Use type hints on every function parameter and return value; -> Any is not acceptable unless the type can be narrowed."

🔧 Proposed fix
-def daily_stats_cron_job(session: SessionDep, hours: int | None = None) -> dict:
+def daily_stats_cron_job(session: SessionDep, hours: int | None = None) -> dict[str, Any]:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def daily_stats_cron_job(session: SessionDep, hours: int | None = None) -> dict:
def daily_stats_cron_job(session: SessionDep, hours: int | None = None) -> dict[str, Any]:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/api/routes/cron.py` at line 150, Update the return annotation of
daily_stats_cron_job from dict to dict[str, Any], matching collect_daily_stats
and the project’s requirement for parameterized, specific return types; ensure
Any is imported if needed.

Source: Coding guidelines

Comment thread backend/app/crud/stats.py
)


def _org_count_sql(table: str) -> Any:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Narrow Any type hints to TextClause.

_org_count_sql returns -> Any and _rows accepts stmt: Any, but both can be narrowed to sqlalchemy.TextClause since text() always returns that type. As per coding guidelines, -> Any is not acceptable unless the type cannot be narrowed.

♻️ Proposed fix
-from sqlalchemy import text
+from sqlalchemy import TextClause, text

-def _org_count_sql(table: str) -> Any:
+def _org_count_sql(table: str) -> TextClause:

-def _rows(session: Session, stmt: Any, params: dict[str, Any]) -> list[dict[str, Any]]:
+def _rows(session: Session, stmt: TextClause, params: dict[str, Any]) -> list[dict[str, Any]]:

Also applies to: 97-97

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/crud/stats.py` at line 76, Replace the Any annotations in
_org_count_sql and _rows with sqlalchemy.TextClause, importing TextClause as
needed; keep the existing text() SQL construction unchanged.

Source: Coding guidelines

Comment on lines +22 to +25
end_at = now()
start_at = end_at - (
timedelta(hours=window_hours) if window_hours else DAILY_WINDOW
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

window_hours=0 silently falls back to the 7-day default.

if window_hours else DAILY_WINDOW treats 0 as falsy. Passing hours=0 would return a 7-day window instead of an empty one. Use an explicit None check.

🐛 Proposed fix
-    start_at = end_at - (
-        timedelta(hours=window_hours) if window_hours else DAILY_WINDOW
-    )
+    start_at = end_at - (
+        timedelta(hours=window_hours) if window_hours is not None else DAILY_WINDOW
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
end_at = now()
start_at = end_at - (
timedelta(hours=window_hours) if window_hours else DAILY_WINDOW
)
end_at = now()
start_at = end_at - (
timedelta(hours=window_hours) if window_hours is not None else DAILY_WINDOW
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/stats.py` around lines 22 - 25, In the window
calculation within the stats function, replace the truthiness check on
window_hours with an explicit None check so a value of 0 produces a
zero-duration window while only omitted values use DAILY_WINDOW.

Comment thread backend/app/services/stats.py
Comment thread scripts/python/invoke-cron.py
@AkhileshNegi
AkhileshNegi requested review from Ayush8923 and removed request for AkhileshNegi and vprashrex July 14, 2026 07:30
@Prajna1999
Prajna1999 requested a review from AkhileshNegi July 16, 2026 05:45
Comment thread backend/app/crud/stats.py
)


def _org_count_sql(table: str) -> Any:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we can avoid the any type here. This is good suggestion given by coderabbit https://github.com/ProjectTech4DevAI/kaapi-backend/pull/1012/changes#r3558059640. Please handle it.

Comment on lines +36 to +41
def section_counts(result: dict[str, Any]) -> dict[str, int]:
return {
section: len(rows)
for section, rows in result["stats"].items()
if isinstance(rows, list)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this function is not used anywhere, if this not needed please remove this dead code.


logger = logging.getLogger(__name__)

DAILY_WINDOW = timedelta(hours=168)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this timedelta(hours=168) is 7 days, not daily. as I can see this cron(here) run daily but posts a rolling 7-day window every run → ~85% of each day's report overlaps yesterday's. Intended? If yes, rename to DEFAULT_STATS_WINDOW / WEEKLY_WINDOW so the daily name doesn't mislead. If the intent was true daily deltas, then I think window should be 24h.

Comment thread backend/app/services/stats.py
@Ayush8923 Ayush8923 added reviewed and removed enhancement New feature or request ready-for-review labels Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discord: Automate stats messages

2 participants