-
-
Notifications
You must be signed in to change notification settings - Fork 67
feat: add batch stats endpoint support #1451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit 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. 📝 WalkthroughWalkthroughAdds batch support to the stats POST endpoint: accepts single event or array, normalizes input, enforces uniform Changes
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: [...] }`
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this 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".
| // 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 }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
tests/stats.test.ts
Outdated
| describe('[POST] /stats batch operations', () => { | ||
| it('should handle batch of events', async () => { | ||
| const uuid1 = randomUUID().toLowerCase() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
There was a problem hiding this 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
⛔ Files ignored due to path filters (1)
supabase/functions/deno.lockis excluded by!**/*.lock
📒 Files selected for processing (2)
supabase/functions/_backend/plugins/stats.tstests/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 insupabase/functions/_backend/as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
UsecreateHonofromutils/hono.tsfor all Hono framework application initialization and routing
All database operations must usegetPgClient()orgetDrizzleClient()fromutils/pg.tsfor PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must acceptContext<MiddlewareKeyVariables>and usec.get('requestId'),c.get('apikey'), andc.get('auth')for request context
Use structured logging withcloudlog({ requestId: c.get('requestId'), message: '...' })for all backend logging
UsemiddlewareAPISecretfor internal API endpoints andmiddlewareKeyfor external API keys; validate againstowner_orgin theapikeystable
Checkc.get('auth')?.authTypeto determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns withschemafrompostgress_schema.tsfor all database operations; usealiasV2()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:backendfor backend files
Files:
supabase/functions/_backend/plugins/stats.ts
**/*.{vue,ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Run
bun lintto lint Vue, TypeScript, and JavaScript files; usebun lint:fixto auto-fix issues
Files:
supabase/functions/_backend/plugins/stats.tstests/stats.test.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use single quotes and no semicolons per
@antfu/eslint-configrules
Files:
supabase/functions/_backend/plugins/stats.tstests/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 lintor lint/format command before validating any backend or frontend task to ensure consistent formatting
Files:
supabase/functions/_backend/plugins/stats.tstests/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.tsincludinggetEndpointUrl(path)for correct worker routing andUSE_CLOUDFLARE_WORKERS=truefor 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 useit.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 ofdevice_idfor 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.
| // 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!) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "simpleError200" supabase/functions/_backend/utils/hono.tsRepository: Cap-go/capgo
Length of output: 160
🏁 Script executed:
sed -n '213,240p' supabase/functions/_backend/utils/hono.tsRepository: 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.
There was a problem hiding this 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
📒 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 insupabase/functions/_backend/as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
UsecreateHonofromutils/hono.tsfor all Hono framework application initialization and routing
All database operations must usegetPgClient()orgetDrizzleClient()fromutils/pg.tsfor PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must acceptContext<MiddlewareKeyVariables>and usec.get('requestId'),c.get('apikey'), andc.get('auth')for request context
Use structured logging withcloudlog({ requestId: c.get('requestId'), message: '...' })for all backend logging
UsemiddlewareAPISecretfor internal API endpoints andmiddlewareKeyfor external API keys; validate againstowner_orgin theapikeystable
Checkc.get('auth')?.authTypeto determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns withschemafrompostgress_schema.tsfor all database operations; usealiasV2()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:backendfor backend files
Files:
supabase/functions/_backend/plugins/stats.ts
**/*.{vue,ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Run
bun lintto lint Vue, TypeScript, and JavaScript files; usebun lint:fixto 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-configrules
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 lintor 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.
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
b48c267 to
31fc702
Compare
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]>
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]>
31fc702 to
9824d3e
Compare
|
There was a problem hiding this 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 intoinvalid_json_parse_body. Please gate bytypeof === '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
📒 Files selected for processing (2)
supabase/functions/_backend/plugins/stats.tstests/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.tsincludinggetEndpointUrl(path)for correct worker routing andUSE_CLOUDFLARE_WORKERS=truefor CF Workers testing
Files:
tests/stats.test.ts
**/*.{vue,ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Run
bun lintto lint Vue, TypeScript, and JavaScript files; usebun lint:fixto auto-fix issues
Files:
tests/stats.test.tssupabase/functions/_backend/plugins/stats.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use single quotes and no semicolons per
@antfu/eslint-configrules
Files:
tests/stats.test.tssupabase/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 useit.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 lintor lint/format command before validating any backend or frontend task to ensure consistent formatting
Files:
tests/stats.test.tssupabase/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 insupabase/functions/_backend/as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
UsecreateHonofromutils/hono.tsfor all Hono framework application initialization and routing
All database operations must usegetPgClient()orgetDrizzleClient()fromutils/pg.tsfor PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must acceptContext<MiddlewareKeyVariables>and usec.get('requestId'),c.get('apikey'), andc.get('auth')for request context
Use structured logging withcloudlog({ requestId: c.get('requestId'), message: '...' })for all backend logging
UsemiddlewareAPISecretfor internal API endpoints andmiddlewareKeyfor external API keys; validate againstowner_orgin theapikeystable
Checkc.get('auth')?.authTypeto determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns withschemafrompostgress_schema.tsfor all database operations; usealiasV2()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:backendfor 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.
| 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, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.



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
Summary by CodeRabbit
New Features
Bug Fixes
Tests
✏️ Tip: You can customize this high-level summary in your review settings.