Skip to content

Conversation

@riderx
Copy link
Member

@riderx riderx commented Jan 15, 2026

Summary

Enable the stats endpoint to accept both single events and arrays of events in a single request. Single events maintain backward compatibility with { status: 'ok' } responses, while batch requests return detailed per-event results including status and error details for each event.

Test plan

  • Verified batch processing with multiple events succeeds
  • Verified batch with partial failures continues processing remaining events
  • Verified empty batch returns empty results without acquiring DB connection
  • Verified single events still return simple response for backward compatibility
  • All existing stats tests continue to pass

Summary by CodeRabbit

  • New Features

    • Stats endpoint now supports batch processing with per-item results and remains backward-compatible with single-event requests.
    • Input normalization (e.g., device IDs) for consistent handling.
  • Bug Fixes

    • Empty-batch and mixed-app_id batches are detected and reported; per-item errors are returned without blocking other items.
    • Improved structured error reporting for batch responses.
  • Tests

    • Added comprehensive batch tests covering success, partial failures, empty batches, compatibility, and cleanup.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 15, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds batch support to the stats POST endpoint: accepts single event or array, normalizes input, enforces uniform app_id per batch, applies batched rate limiting, processes each event individually producing per-item results (including errors), and returns either a single result or an array of BatchStatsResult.

Changes

Cohort / File(s) Summary
Batch Processing Implementation
supabase/functions/_backend/plugins/stats.ts
Added exported BatchStatsResult and internal PostResult. Added parseBodyRaw, refactored post to return structured results, changed POST handler to accept arrays or single objects, validate uniform app_id, handle empty batches, apply batched rate limiting, iterate events producing per-item results, and replace direct HTTP-helper returns with structured per-item responses.
Comprehensive Batch Tests
tests/stats.test.ts
Added batch-focused tests covering all-success batches, partial failures, empty batch, single-event compatibility, repeated device actions, and mixed-app_id rejection; includes DB assertions, cleanup, and env-aware execution (Supabase vs Cloudflare).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as POST Handler
    participant Parser as parseBodyRaw
    participant Validator as Batch Validator
    participant RateLimiter as Rate Limiter
    participant Processor as Event Processor
    participant DB as Database
    participant Responder as Response Builder

    Client->>Handler: POST /stats (single or batch)
    Handler->>Parser: parseBodyRaw() normalize JSON
    Handler->>Validator: determine single vs batch & validate app_id

    alt Batch Input
        Validator->>Validator: ensure uniform app_id
        alt Mixed app_ids
            Validator-->>Responder: prepare mixed app_id error result(s)
        else Valid Batch
            Validator->>RateLimiter: apply batch rate limit
            RateLimiter->>Processor: iterate events
            loop For Each Event
                Processor->>DB: persist event
                Processor->>Responder: append per-item result (status/error/index)
            end
        end
    else Single Event
        Validator->>RateLimiter: apply single rate limit
        RateLimiter->>Processor: process event
        Processor->>DB: persist event
        Processor->>Responder: single status result
    end

    Responder-->>Client: return single status or `{ status: "ok", results: [...] }`
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🥕📊 I nibble bytes and count each beat,
Batch by batch the events I greet,
Indices aligned, errors tucked neat,
Hops of stats make the ledger complete. 🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add batch stats endpoint support' accurately describes the main feature added—batch processing support for the stats endpoint.
Description check ✅ Passed The description includes a clear summary of the changes and detailed test plan covering batch processing, partial failures, empty batches, backward compatibility, and existing test validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 356bcca13e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 192 to 195
// Rate limit check on first event's app_id (all events in batch should be from same app)
if (isLimited(c, events[0].app_id)) {
return simpleRateLimit({ app_id: events[0].app_id })
}

Choose a reason for hiding this comment

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

P2 Badge Enforce rate limits per app in batch requests

Batch requests only check isLimited for events[0].app_id, but nothing enforces that all events share the same app_id. If a client sends a batch containing multiple apps, only the first app is rate-limited and the rest bypass any LIMITED_APPS throttling, which can defeat per-app rate limits in production. Consider validating that all events share the same app_id, or apply isLimited per event/app.

Useful? React with 👍 / 👎.

Comment on lines 384 to 386
describe('[POST] /stats batch operations', () => {
it('should handle batch of events', async () => {
const uuid1 = randomUUID().toLowerCase()

Choose a reason for hiding this comment

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

P1 Badge Use it.concurrent for new tests per AGENTS.md

Repository guidance in AGENTS.md (repo root) states: “Maximize parallelism: Use it.concurrent() instead of it() to run tests in parallel within the same file.” The newly added batch tests use it(...), which forces sequential execution and violates that requirement. Please switch these new tests to it.concurrent(...) to align with the mandated parallel test strategy.

Useful? React with 👍 / 👎.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@supabase/functions/_backend/plugins/stats.ts`:
- Around line 243-249: The single-event path currently uses simpleError200 which
forces HTTP 200 for validation failures; replace that call with quickError so
the response can carry the proper status code. In the block handling !isBatch
(where results[0] is inspected), call quickError(c, result.error!,
result.message!, result.statusCode ?? result.code ?? 400) instead of
simpleError200 so validation errors return their intended HTTP status (falling
back to 400 if the result has no explicit status field); keep the existing
c.json(BRES) for the 'ok' case.
- Around line 192-206: The batch-level mixed-app_ids check and the isLimited
call use firstAppId (events[0].app_id) before any schema validation; move
validation so firstAppId is guaranteed to be a validated string: run
jsonRequestSchema / parsePluginBody (or otherwise validate events[0].app_id)
prior to computing firstAppId and performing the for-loop and isLimited check,
or alternatively iterate through events and call parsePluginBody for each item
up-front to validate app_id types before calling simpleError200,
simpleRateLimit, or isLimited; ensure references to firstAppId, events, isBatch,
parsePluginBody, jsonRequestSchema, simpleError200, and isLimited are updated
accordingly.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0947b0 and 5dc76d0.

⛔ Files ignored due to path filters (1)
  • supabase/functions/deno.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • supabase/functions/_backend/plugins/stats.ts
  • tests/stats.test.ts
🧰 Additional context used
📓 Path-based instructions (9)
supabase/functions/_backend/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/plugins/stats.ts
supabase/functions/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/plugins/stats.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • supabase/functions/_backend/plugins/stats.ts
  • tests/stats.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • supabase/functions/_backend/plugins/stats.ts
  • tests/stats.test.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

supabase/functions/**/*.ts: Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)
When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Files:

  • supabase/functions/_backend/plugins/stats.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • supabase/functions/_backend/plugins/stats.ts
  • tests/stats.test.ts
supabase/functions/_backend/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Files:

  • supabase/functions/_backend/plugins/stats.ts
tests/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend tests must use helpers from tests/test-utils.ts including getEndpointUrl(path) for correct worker routing and USE_CLOUDFLARE_WORKERS=true for CF Workers testing

Files:

  • tests/stats.test.ts
tests/**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.test.ts: ALL TEST FILES RUN IN PARALLEL - design tests accordingly with proper isolation and use it.concurrent() to maximize parallelism within the same file
Create dedicated seed data for tests that modify shared resources instead of reusing existing data; use unique naming conventions prefixed with test file name or feature being tested
Only reuse existing seed data if you only READ the data without modifying it, or create your OWN child resources under it without modifying the parent
Backend tests located in tests/ directory use Vitest test runner and require running Supabase instance

Files:

  • tests/stats.test.ts
🧠 Learnings (8)
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/_backend/**/*.ts : Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `createHono` from `utils/hono.ts` for all Hono framework application initialization and routing

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/plugin/index.ts : Plugin Worker (port 8788) routes: `/updates`, `/channel_self`, `/stats`

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/api/index.ts : API Worker (port 8787) routes: `/bundle`, `/app`, `/device`, `/channel`, `/private/*`, `/triggers`

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/seed.sql : Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • tests/stats.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to tests/**/*.test.ts : Create dedicated seed data for tests that modify shared resources instead of reusing existing data; use unique naming conventions prefixed with test file name or feature being tested

Applied to files:

  • tests/stats.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to tests/**/*.test.ts : ALL TEST FILES RUN IN PARALLEL - design tests accordingly with proper isolation and use `it.concurrent()` to maximize parallelism within the same file

Applied to files:

  • tests/stats.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to tests/**/*.test.ts : Backend tests located in tests/ directory use Vitest test runner and require running Supabase instance

Applied to files:

  • tests/stats.test.ts
🧬 Code graph analysis (1)
tests/stats.test.ts (2)
tests/test-utils.ts (4)
  • APP_NAME_STATS (53-53)
  • getVersionFromAction (196-199)
  • createAppVersions (201-217)
  • getSupabaseClient (388-407)
supabase/functions/_backend/utils/version.ts (1)
  • version (1-1)
🔇 Additional comments (6)
supabase/functions/_backend/plugins/stats.ts (5)

9-9: Import update looks fine.


23-28: Clear result typing for batch and post flows.

These interfaces make the new response shapes and internal flow easier to reason about.

Also applies to: 61-66


68-99: PostResult wiring is consistent.

Early returns now map cleanly to the caller without losing error detail.

Also applies to: 120-120, 156-156


161-180: Good normalization of device_id for single and batch payloads.


187-190: Batch loop and empty-batch short‑circuit look solid.

Per-item try/catch preserves partial success while keeping the response shape stable.

Also applies to: 208-242, 252-254

tests/stats.test.ts (1)

374-598: Batch test coverage and conditional concurrency look good.

Nice validation of partial failures, empty batches, and backward-compatible single-event responses.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 243 to 249
// For single event, return simple response for backward compatibility
if (!isBatch) {
const result = results[0]
if (result.status === 'ok') {
return c.json(BRES)
}
return simpleError200(c, result.error!, result.message!)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "simpleError200" supabase/functions/_backend/utils/hono.ts

Repository: Cap-go/capgo

Length of output: 160


🏁 Script executed:

sed -n '213,240p' supabase/functions/_backend/utils/hono.ts

Repository: Cap-go/capgo

Length of output: 943


Use appropriate error status codes for single-event validation errors.

The simpleError200 helper function hardcodes HTTP 200 for all errors. Using it for non-batch validation errors will return 200 instead of the likely 400 status codes these errors previously returned, breaking backward compatibility (e.g., existing invalid-action tests). Either confirm this status change is intentional, or use quickError (which accepts dynamic status codes) for single-event validation failures.

🤖 Prompt for AI Agents
In `@supabase/functions/_backend/plugins/stats.ts` around lines 243 - 249, The
single-event path currently uses simpleError200 which forces HTTP 200 for
validation failures; replace that call with quickError so the response can carry
the proper status code. In the block handling !isBatch (where results[0] is
inspected), call quickError(c, result.error!, result.message!, result.statusCode
?? result.code ?? 400) instead of simpleError200 so validation errors return
their intended HTTP status (falling back to 400 if the result has no explicit
status field); keep the existing c.json(BRES) for the 'ok' case.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@supabase/functions/_backend/plugins/stats.ts`:
- Around line 161-175: parseBodyRaw currently calls toLowerCase() on any truthy
device_id which throws for non-strings; update the normalization so you only
call toLowerCase when typeof device_id === "string" (both inside the
Array.isArray branch iterating items and in the single-body branch), leaving
non-string values untouched so schema validation can catch them; reference the
parseBodyRaw function and the AppStats.device_id field when making the change.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5dc76d0 and da25392.

📒 Files selected for processing (1)
  • supabase/functions/_backend/plugins/stats.ts
🧰 Additional context used
📓 Path-based instructions (7)
supabase/functions/_backend/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/plugins/stats.ts
supabase/functions/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/plugins/stats.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • supabase/functions/_backend/plugins/stats.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • supabase/functions/_backend/plugins/stats.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

supabase/functions/**/*.ts: Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)
When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Files:

  • supabase/functions/_backend/plugins/stats.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • supabase/functions/_backend/plugins/stats.ts
supabase/functions/_backend/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Files:

  • supabase/functions/_backend/plugins/stats.ts
🧠 Learnings (5)
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/_backend/**/*.ts : Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/plugin/index.ts : Plugin Worker (port 8788) routes: `/updates`, `/channel_self`, `/stats`

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `createHono` from `utils/hono.ts` for all Hono framework application initialization and routing

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/api/index.ts : API Worker (port 8787) routes: `/bundle`, `/app`, `/device`, `/channel`, `/private/*`, `/triggers`

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
🧬 Code graph analysis (1)
supabase/functions/_backend/plugins/stats.ts (3)
supabase/functions/_backend/utils/types.ts (1)
  • AppStats (23-26)
supabase/functions/_backend/utils/plugin_parser.ts (1)
  • parsePluginBody (36-65)
supabase/functions/_backend/utils/hono.ts (5)
  • MiddlewareKeyVariables (27-41)
  • simpleError (247-249)
  • simpleError200 (213-222)
  • simpleRateLimit (239-245)
  • BRES (135-135)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (3)
supabase/functions/_backend/plugins/stats.ts (3)

9-9: Batch response typing looks solid.

The BatchStatsResult shape (plus the added helper import) makes the batch contract explicit and easier to consume.

Also applies to: 23-28


61-157: Structured PostResult flow is clear.

Returning a unified PostResult keeps the single vs. batch code paths consistent and readable.


182-268: Batch flow and early app_id checks look good.

The empty-batch short‑circuit and per-item aggregation align well with the intended batch behavior.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +161 to +175
async function parseBodyRaw(c: Context): Promise<AppStats | AppStats[]> {
try {
const body = await c.req.json<AppStats | AppStats[]>()
// Normalize device_id to lowercase for both single and array
if (Array.isArray(body)) {
for (const item of body) {
if (item.device_id) {
item.device_id = item.device_id.toLowerCase()
}
}
}
else if (body.device_id) {
body.device_id = body.device_id.toLowerCase()
}
return body
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard device_id normalization for non-strings.

Line 166/173 lowercases any truthy value; if device_id is numeric/boolean, this throws and is reported as invalid_json_parse_body instead of a schema error. Only lowercase when it’s a string.

🔧 Proposed fix
    if (Array.isArray(body)) {
      for (const item of body) {
-        if (item.device_id) {
-          item.device_id = item.device_id.toLowerCase()
-        }
+        if (typeof item.device_id === 'string') {
+          item.device_id = item.device_id.toLowerCase()
+        }
      }
    }
-    else if (body.device_id) {
-      body.device_id = body.device_id.toLowerCase()
-    }
+    else if (typeof body.device_id === 'string') {
+      body.device_id = body.device_id.toLowerCase()
+    }
📝 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
async function parseBodyRaw(c: Context): Promise<AppStats | AppStats[]> {
try {
const body = await c.req.json<AppStats | AppStats[]>()
// Normalize device_id to lowercase for both single and array
if (Array.isArray(body)) {
for (const item of body) {
if (item.device_id) {
item.device_id = item.device_id.toLowerCase()
}
}
}
else if (body.device_id) {
body.device_id = body.device_id.toLowerCase()
}
return body
async function parseBodyRaw(c: Context): Promise<AppStats | AppStats[]> {
try {
const body = await c.req.json<AppStats | AppStats[]>()
// Normalize device_id to lowercase for both single and array
if (Array.isArray(body)) {
for (const item of body) {
if (typeof item.device_id === 'string') {
item.device_id = item.device_id.toLowerCase()
}
}
}
else if (typeof body.device_id === 'string') {
body.device_id = body.device_id.toLowerCase()
}
return body
🤖 Prompt for AI Agents
In `@supabase/functions/_backend/plugins/stats.ts` around lines 161 - 175,
parseBodyRaw currently calls toLowerCase() on any truthy device_id which throws
for non-strings; update the normalization so you only call toLowerCase when
typeof device_id === "string" (both inside the Array.isArray branch iterating
items and in the single-body branch), leaving non-string values untouched so
schema validation can catch them; reference the parseBodyRaw function and the
AppStats.device_id field when making the change.

@riderx riderx force-pushed the riderx/batch-stats-endpoint branch from b48c267 to 31fc702 Compare January 16, 2026 02:26
riderx and others added 5 commits January 16, 2026 02:45
Enable stats endpoint to accept both single events and arrays of events, allowing clients to send multiple events in a single request. Single events maintain backward compatibility with simple { status: 'ok' } responses, while batch requests return detailed results per event including status and error details.

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
- Validate all events in batch have same app_id to prevent rate limit bypass
- Update batch tests to use it.concurrent for parallel execution per AGENTS.md
- Add test for mixed app_id rejection

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
- Validate first event's app_id early before batch/rate-limit checks
- Fix single-event error handling to propagate proper 400 status codes

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
@riderx riderx force-pushed the riderx/batch-stats-endpoint branch from 31fc702 to 9824d3e Compare January 16, 2026 05:46
@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@supabase/functions/_backend/plugins/stats.ts`:
- Around line 221-248: The handler currently ignores the isOnprem flag returned
by post(); before treating a post() success as OK, check result.isOnprem and for
single requests call onPremStats(c) (preserving the 429 behavior) instead of
returning BRES, and for batch requests mark that event's BatchStatsResult as an
error (status: 'error', include result.error and result.message or a canonical
on‑prem message) rather than pushing {status:'ok'}. Adjust the post() success
branch in both the single-path (where parsePluginBody/post/BRES are used) and
inside the batch loop (where results: BatchStatsResult[] is populated) to handle
isOnprem explicitly.
♻️ Duplicate comments (1)
supabase/functions/_backend/plugins/stats.ts (1)

165-174: Guard device_id normalization to strings only.

Calling toLowerCase() on non-strings throws and turns schema errors into invalid_json_parse_body. Please gate by typeof === 'string' for both single and batch items.

🔧 Proposed fix
     if (Array.isArray(body)) {
       for (const item of body) {
-        if (item.device_id) {
-          item.device_id = item.device_id.toLowerCase()
-        }
+        if (typeof item.device_id === 'string') {
+          item.device_id = item.device_id.toLowerCase()
+        }
       }
     }
-    else if (body.device_id) {
+    else if (typeof body.device_id === 'string') {
       body.device_id = body.device_id.toLowerCase()
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da25392 and 9824d3e.

📒 Files selected for processing (2)
  • supabase/functions/_backend/plugins/stats.ts
  • tests/stats.test.ts
🧰 Additional context used
📓 Path-based instructions (9)
tests/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend tests must use helpers from tests/test-utils.ts including getEndpointUrl(path) for correct worker routing and USE_CLOUDFLARE_WORKERS=true for CF Workers testing

Files:

  • tests/stats.test.ts
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint to lint Vue, TypeScript, and JavaScript files; use bun lint:fix to auto-fix issues

Files:

  • tests/stats.test.ts
  • supabase/functions/_backend/plugins/stats.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes and no semicolons per @antfu/eslint-config rules

Files:

  • tests/stats.test.ts
  • supabase/functions/_backend/plugins/stats.ts
tests/**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.test.ts: ALL TEST FILES RUN IN PARALLEL - design tests accordingly with proper isolation and use it.concurrent() to maximize parallelism within the same file
Create dedicated seed data for tests that modify shared resources instead of reusing existing data; use unique naming conventions prefixed with test file name or feature being tested
Only reuse existing seed data if you only READ the data without modifying it, or create your OWN child resources under it without modifying the parent
Backend tests located in tests/ directory use Vitest test runner and require running Supabase instance

Files:

  • tests/stats.test.ts
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Run bun lint or lint/format command before validating any backend or frontend task to ensure consistent formatting

Files:

  • tests/stats.test.ts
  • supabase/functions/_backend/plugins/stats.ts
supabase/functions/_backend/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/plugins/stats.ts
supabase/functions/**/*.{ts,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/plugins/stats.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

supabase/functions/**/*.ts: Never use the Supabase admin SDK with service key for user-facing APIs; always use client SDK with user authentication to enforce RLS policies. Admin SDK should only be used for internal operations (triggers, CRON jobs, etc.)
When admin access is unavoidable for a user-facing endpoint, sanitize all user inputs carefully to prevent PostgREST query injection

Files:

  • supabase/functions/_backend/plugins/stats.ts
supabase/functions/_backend/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Files:

  • supabase/functions/_backend/plugins/stats.ts
🧠 Learnings (8)
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/seed.sql : Update supabase/seed.sql for new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • tests/stats.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to tests/**/*.test.ts : Create dedicated seed data for tests that modify shared resources instead of reusing existing data; use unique naming conventions prefixed with test file name or feature being tested

Applied to files:

  • tests/stats.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to tests/**/*.test.ts : ALL TEST FILES RUN IN PARALLEL - design tests accordingly with proper isolation and use `it.concurrent()` to maximize parallelism within the same file

Applied to files:

  • tests/stats.test.ts
📚 Learning: 2026-01-10T04:55:25.264Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-10T04:55:25.264Z
Learning: Applies to supabase/functions/_backend/**/*.ts : Place core backend logic in supabase/functions/_backend/ with plugins, private, public, triggers, and utils subdirectories

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/plugin/index.ts : Plugin Worker (port 8788) routes: `/updates`, `/channel_self`, `/stats`

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `createHono` from `utils/hono.ts` for all Hono framework application initialization and routing

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/api/index.ts : API Worker (port 8787) routes: `/bundle`, `/app`, `/device`, `/channel`, `/private/*`, `/triggers`

Applied to files:

  • supabase/functions/_backend/plugins/stats.ts
🧬 Code graph analysis (1)
supabase/functions/_backend/plugins/stats.ts (9)
supabase/functions/_backend/utils/pg.ts (2)
  • getDrizzleClient (220-222)
  • getPgClient (187-218)
supabase/functions/_backend/utils/types.ts (1)
  • AppStats (23-26)
supabase/functions/_backend/utils/plugin_parser.ts (2)
  • makeDevice (17-34)
  • parsePluginBody (36-65)
supabase/functions/_backend/utils/appStatus.ts (2)
  • getAppStatus (20-28)
  • setAppStatus (30-37)
supabase/functions/_backend/utils/stats.ts (1)
  • onPremStats (19-41)
supabase/functions/_backend/triggers/on_version_update.ts (1)
  • app (279-279)
supabase/functions/_backend/triggers/on_app_create.ts (1)
  • app (12-12)
supabase/functions/_backend/triggers/cron_email.ts (1)
  • app (93-93)
supabase/functions/_backend/utils/hono.ts (5)
  • MiddlewareKeyVariables (27-41)
  • simpleError (247-249)
  • simpleError200 (213-222)
  • simpleRateLimit (239-245)
  • BRES (135-135)
🪛 GitHub Actions: Run tests
tests/stats.test.ts

[error] 226-226: Test failed during 'bun run test:all'. AssertionError: expected 200 to be 429.

🔇 Additional comments (3)
supabase/functions/_backend/plugins/stats.ts (2)

9-9: Import-only change.


23-28: BatchStatsResult looks good.

tests/stats.test.ts (1)

374-597: Batch test coverage and CF gating look solid.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +221 to +248
if (!isBatch) {
const bodyParsed = parsePluginBody<AppStats>(c, events[0], jsonRequestSchema)
const result = await post(c, drizzleClient, bodyParsed)
if (result.success) {
return c.json(BRES)
}
return simpleError200(c, result.error!, result.message!)
}

const bodyParsed = parsePluginBody<AppStats>(c, body, jsonRequestSchema)
const res = await post(c, getDrizzleClient(pgClient!), bodyParsed)
if (pgClient)
await closeClient(c, pgClient)
return res
// For batch, collect results and handle errors per event
const results: BatchStatsResult[] = []

for (let i = 0; i < events.length; i++) {
const event = events[i]
try {
const bodyParsed = parsePluginBody<AppStats>(c, event, jsonRequestSchema)
const result = await post(c, drizzleClient, bodyParsed)

if (result.success) {
results.push({ status: 'ok', index: i })
}
else {
results.push({
status: 'error',
error: result.error,
message: result.message,
index: i,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

On‑prem responses are masked as 200 OK (single + batch).

post() sets isOnprem, but the handler ignores it and returns { status: 'ok' }, which conflicts with the existing 429 behavior from onPremStats (see supabase/functions/_backend/utils/stats.ts Lines 18‑40). This aligns with the pipeline failure expecting 429 but receiving 200. Handle isOnprem explicitly before the success branch (and mark batch items as errors).

🔧 Proposed fix
     if (!isBatch) {
       const bodyParsed = parsePluginBody<AppStats>(c, events[0], jsonRequestSchema)
       const result = await post(c, drizzleClient, bodyParsed)
+      if (result.isOnprem) {
+        return c.json({ error: 'on_premise_app', message: 'On-premise app detected' }, 429)
+      }
       if (result.success) {
         return c.json(BRES)
       }
       return simpleError200(c, result.error!, result.message!)
     }
@@
         const bodyParsed = parsePluginBody<AppStats>(c, event, jsonRequestSchema)
         const result = await post(c, drizzleClient, bodyParsed)

-        if (result.success) {
+        if (result.isOnprem) {
+          results.push({
+            status: 'error',
+            error: 'on_premise_app',
+            message: 'On-premise app detected',
+            index: i,
+          })
+        }
+        else if (result.success) {
           results.push({ status: 'ok', index: i })
         }
         else {
           results.push({
             status: 'error',
📝 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
if (!isBatch) {
const bodyParsed = parsePluginBody<AppStats>(c, events[0], jsonRequestSchema)
const result = await post(c, drizzleClient, bodyParsed)
if (result.success) {
return c.json(BRES)
}
return simpleError200(c, result.error!, result.message!)
}
const bodyParsed = parsePluginBody<AppStats>(c, body, jsonRequestSchema)
const res = await post(c, getDrizzleClient(pgClient!), bodyParsed)
if (pgClient)
await closeClient(c, pgClient)
return res
// For batch, collect results and handle errors per event
const results: BatchStatsResult[] = []
for (let i = 0; i < events.length; i++) {
const event = events[i]
try {
const bodyParsed = parsePluginBody<AppStats>(c, event, jsonRequestSchema)
const result = await post(c, drizzleClient, bodyParsed)
if (result.success) {
results.push({ status: 'ok', index: i })
}
else {
results.push({
status: 'error',
error: result.error,
message: result.message,
index: i,
})
if (!isBatch) {
const bodyParsed = parsePluginBody<AppStats>(c, events[0], jsonRequestSchema)
const result = await post(c, drizzleClient, bodyParsed)
if (result.isOnprem) {
return c.json({ error: 'on_premise_app', message: 'On-premise app detected' }, 429)
}
if (result.success) {
return c.json(BRES)
}
return simpleError200(c, result.error!, result.message!)
}
// For batch, collect results and handle errors per event
const results: BatchStatsResult[] = []
for (let i = 0; i < events.length; i++) {
const event = events[i]
try {
const bodyParsed = parsePluginBody<AppStats>(c, event, jsonRequestSchema)
const result = await post(c, drizzleClient, bodyParsed)
if (result.isOnprem) {
results.push({
status: 'error',
error: 'on_premise_app',
message: 'On-premise app detected',
index: i,
})
}
else if (result.success) {
results.push({ status: 'ok', index: i })
}
else {
results.push({
status: 'error',
error: result.error,
message: result.message,
index: i,
})
🤖 Prompt for AI Agents
In `@supabase/functions/_backend/plugins/stats.ts` around lines 221 - 248, The
handler currently ignores the isOnprem flag returned by post(); before treating
a post() success as OK, check result.isOnprem and for single requests call
onPremStats(c) (preserving the 429 behavior) instead of returning BRES, and for
batch requests mark that event's BatchStatsResult as an error (status: 'error',
include result.error and result.message or a canonical on‑prem message) rather
than pushing {status:'ok'}. Adjust the post() success branch in both the
single-path (where parsePluginBody/post/BRES are used) and inside the batch loop
(where results: BatchStatsResult[] is populated) to handle isOnprem explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants