Skip to content

fix: ratelimit unblocks#5668

Merged
perkinsjr merged 7 commits intomainfrom
ratelimit-logs-fixes
Apr 9, 2026
Merged

fix: ratelimit unblocks#5668
perkinsjr merged 7 commits intomainfrom
ratelimit-logs-fixes

Conversation

@perkinsjr
Copy link
Copy Markdown
Member

What does this PR do?

  • Ratelimit logs query was LEFT JOINing against api_requests_raw_v2 using the full time window, causing ClickHouse timeouts for workspaces with heavy verifyKey usage, (e.g. 2B+ requests/day) even though their ratelimit data was small

    • Split the query into two: a fast primary query against ratelimits_raw_v2 (loads instantly) and a separate targeted enrichment query that fetches request/response metadata for only the returned request IDs with a tight time range
    • Count query failures no longer block the logs response — returns -1 and the UI gracefully hides the total

    Fixes UNKEY-DASHBOARD-4W

    Changes

    • ClickHouse queries (ratelimits.ts): Removed the LEFT JOIN from the logs query, added a new getRatelimitLogEnrichment function that queries api_requests_raw_v2 scoped to specific request IDs
    • tRPC: Added ratelimit.logs.enrichment endpoint, made count query errors non-fatal in both query-logs and query overview-logs
    • Frontend hook (use-logs-query.ts): Logs render immediately with empty enrichment defaults, then a follow-up enrichment query fills in request/response details (limit, duration, reset, region)
    • Types: Split RatelimitLog schema into base fields + RatelimitLogEnrichment, created EnrichedRatelimitLog on the frontend

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

Test plan

  • Verify ratelimit logs load for workspaces with high API key verification volume
  • Verify enrichment columns (Limit, Duration, Resets At, Region) populate after initial load
  • Verify pagination (load more) still works and enriches new pages
  • Verify live polling still works
  • Verify overview logs table still works with count fallback
  • Verify log detail panel still shows request/response headers and bodies

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Ran make fmt on /go directory
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboard Ready Ready Preview, Comment Apr 9, 2026 9:56pm

Request Review

@perkinsjr perkinsjr marked this pull request as draft April 8, 2026 19:22
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 391afa39-1f0f-42da-98c3-4375885f727f

📥 Commits

Reviewing files that changed from the base of the PR and between e1fc5d6 and 34a7b13.

📒 Files selected for processing (1)
  • web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts

📝 Walkthrough

Walkthrough

Client-side enrichment for ratelimit logs was added: base ClickHouse log queries now return core fields only; a new enrichment query and TRPC endpoint fetch supplementary request data. The client hook batches, caches, and merges enrichment into logs; server-side error handling for logs vs count queries was separated.

Changes

Cohort / File(s) Summary
Enrichment hook & types
web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts
Added EnrichedRatelimitLog type, enrichment cache (enrichmentMapRef), enrichLogs/fetchEnrichment (batched, chunk size 100), versioned re-merge, and updated realtime/historical handling to use enriched logs.
Logs table & context
web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx, web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/context/logs.tsx, web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
Switched local table generics and context selectedLog types from RatelimitLog to EnrichedRatelimitLog; adjusted row/selection helpers and conditionally render “of {totalCount}” only when totalCount >= 0.
Log details & standard types
web/apps/dashboard/components/logs/details/log-details/index.tsx
Replaced RatelimitLog references with EnrichedRatelimitLog in StandardLogTypes, createLogSections, and the isStandardLog guard; updated runtime warning text.
Utility functions (local type change)
web/apps/dashboard/app/(app)/[workspaceSlug]/logs/utils.ts, web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/requests/utils.ts
Replaced external RatelimitLog usage with locally declared LogWithRequestResponse structural type for extractResponseField and getRequestHeader parameter typings.
TRPC enrichment endpoint
web/apps/dashboard/lib/trpc/routers/ratelimit/query-logs/enrichment.ts, web/apps/dashboard/lib/trpc/routers/index.ts
Added queryRatelimitLogEnrichment TRPC procedure (validates up to 100 request IDs, time range; uses ratelimit read middleware) and registered it under router.ratelimit.logs.enrichment.
Query error handling
web/apps/dashboard/lib/trpc/routers/ratelimit/query-logs/index.ts, web/apps/dashboard/lib/trpc/routers/ratelimit/query-overview-logs/index.ts
Separated failure handling: logs query errors now throw INTERNAL_SERVER_ERROR; count query failures emit warnings and set total to -1 instead of failing the request.
ClickHouse enrichment layer
web/internal/clickhouse/src/ratelimits.ts, web/internal/clickhouse/src/index.ts
Added ratelimitLogEnrichment schema/types and getRatelimitLogEnrichment function (queries api_requests_raw_v2 by request IDs and time range); removed JOIN-based enrichment from getRatelimitLogs; exposed logEnrichment on ratelimits getter.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Component
    participant Hook as use-logs-query Hook
    participant Cache as Enrichment Cache
    participant TRPC as TRPC Server
    participant CH as ClickHouse

    Client->>Hook: request logs
    Hook->>CH: query base logs (request_id, time, identifier, status)
    CH-->>Hook: base RatelimitLog[]

    Hook->>Cache: check for missing enrichment
    Hook->>Hook: batch missing request_ids (chunks of 100)

    loop per batch
        Hook->>TRPC: ratelimit.logs.enrichment(requestIds, startTime, endTime)
        TRPC->>CH: getRatelimitLogEnrichment(requestIds, timeRange)
        CH-->>TRPC: enrichment[]
        TRPC-->>Hook: { enrichment }
        Hook->>Cache: store enrichment (enrichmentMapRef)
    end

    Hook->>Hook: merge enrichment into logs => EnrichedRatelimitLog[]
    Hook-->>Client: return historicalLogs & realtimeLogs (enriched)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Title check ❓ Inconclusive The title 'fix: ratelimit unblocks' is vague and does not clearly convey the main technical change; it lacks specificity about what 'unblocks' means or what problem is being solved. Consider a more descriptive title such as 'fix: split ratelimit logs query to avoid ClickHouse timeouts' or 'fix: defer ratelimit log enrichment to separate query' to clarify the core change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and follows the template structure well, including issue reference, type of change, detailed test plan, and mostly-completed checklist items.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ratelimit-logs-fixes

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

Copy link
Copy Markdown
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

🧹 Nitpick comments (2)
web/apps/dashboard/lib/trpc/routers/ratelimit/query-overview-logs/index.ts (1)

58-77: Use fault library for error handling as per coding guidelines.

The error branches at lines 59–77 use direct console.* + TRPCError. Per the coding guidelines for **/*.{ts,tsx}, apply the fault library for error handling to align with project standards.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/apps/dashboard/lib/trpc/routers/ratelimit/query-overview-logs/index.ts`
around lines 58 - 77, Replace the direct console logging and manual TRPCError
usage with the project's fault-based error handling: import and use the fault
library in this module and for the logsResult branch create and throw a fault
error that wraps the original logsResult.err and includes context (namespaceId,
descriptive message) instead of throwing TRPCError; for the countResult branch
record the non-fatal failure via fault (e.g., capture/log a warning with
countResult.err and namespaceId) and continue returning logs without total.
Ensure you reference logsResult and countResult when building the fault context
so the original error and namespaceId are attached.
web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts (1)

152-169: Batch requestIds before calling the enrichment route.

The new endpoint caps requestIds at 100, but this helper sends every missing ID in one call. If a caller ever raises limit above 100, the whole enrichment fetch will fail validation and backfill nothing.

📦 Suggested batching
       try {
-        const result = await queryClient.ratelimit.logs.enrichment.fetch({
-          requestIds: unenrichedIds,
-          startTime: minTime,
-          endTime: maxTime,
-        });
-
-        if (result.enrichment.length > 0) {
-          for (const item of result.enrichment) {
-            enrichmentMapRef.current.set(item.request_id, item);
-          }
-          // Trigger re-render to merge enrichment into displayed logs
-          setEnrichmentVersion((v) => v + 1);
+        let updated = false;
+
+        for (let i = 0; i < unenrichedIds.length; i += 100) {
+          const requestIds = unenrichedIds.slice(i, i + 100);
+          const result = await queryClient.ratelimit.logs.enrichment.fetch({
+            requestIds,
+            startTime: minTime,
+            endTime: maxTime,
+          });
+
+          for (const item of result.enrichment) {
+            enrichmentMapRef.current.set(item.request_id, item);
+            updated = true;
+          }
+        }
+
+        if (updated) {
+          setEnrichmentVersion((v) => v + 1);
         }
       } catch (error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts
around lines 152 - 169, The enrichment call currently sends all missing IDs at
once which will fail when requestIds > 100; change the logic in
use-logs-query.ts to batch unenrichedIds into chunks of at most 100 and call
queryClient.ratelimit.logs.enrichment.fetch for each chunk (either sequentially
or via Promise.all), then merge each chunk's results into
enrichmentMapRef.current; ensure you still compute minTime/maxTime once (from
times) and pass them to every fetch call and handle/aggregate errors per-chunk
so one oversized batch doesn't prevent partial backfill.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/apps/dashboard/components/logs/details/log-details/index.tsx`:
- Around line 90-92: Remove the unsafe type assertion on line 91 and use the
existing isStandardLog type guard to narrow log before calling
extractResponseField; specifically, ensure you check isStandardLog(log) (or
otherwise branch where isStandardLog is true) and then call
extractResponseField(log, "meta") so TypeScript knows log is Log |
EnrichedRatelimitLog without using `as`; update the conditional that currently
checks "request_body"|"response_body" to incorporate the isStandardLog guard
before invoking extractResponseField.

In `@web/internal/clickhouse/src/ratelimits.ts`:
- Around line 374-389: The query currently pages only by time which skips rows
with identical time; change pagination to use a stable (time, request_id)
cursor: add a new parameter cursorRequestId and replace the time-only cursor
check with a tuple comparison such as ({cursorTime} IS NULL OR (time <
{cursorTime} OR (time = {cursorTime} AND request_id < {cursorRequestId}))),
update ORDER BY to "ORDER BY time DESC, request_id DESC", and ensure the calling
logic (e.g., getRatelimitOverviewLogs and its router/client cursor payload)
includes and forwards cursorRequestId alongside cursorTime so the next-page
cursor contains both values.

---

Nitpick comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts:
- Around line 152-169: The enrichment call currently sends all missing IDs at
once which will fail when requestIds > 100; change the logic in
use-logs-query.ts to batch unenrichedIds into chunks of at most 100 and call
queryClient.ratelimit.logs.enrichment.fetch for each chunk (either sequentially
or via Promise.all), then merge each chunk's results into
enrichmentMapRef.current; ensure you still compute minTime/maxTime once (from
times) and pass them to every fetch call and handle/aggregate errors per-chunk
so one oversized batch doesn't prevent partial backfill.

In `@web/apps/dashboard/lib/trpc/routers/ratelimit/query-overview-logs/index.ts`:
- Around line 58-77: Replace the direct console logging and manual TRPCError
usage with the project's fault-based error handling: import and use the fault
library in this module and for the logsResult branch create and throw a fault
error that wraps the original logsResult.err and includes context (namespaceId,
descriptive message) instead of throwing TRPCError; for the countResult branch
record the non-fatal failure via fault (e.g., capture/log a warning with
countResult.err and namespaceId) and continue returning logs without total.
Ensure you reference logsResult and countResult when building the fault context
so the original error and namespaceId are attached.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5e52e174-8b9a-437e-a144-017f84453377

📥 Commits

Reviewing files that changed from the base of the PR and between 917e4a1 and 046e7b8.

📒 Files selected for processing (13)
  • web/apps/dashboard/app/(app)/[workspaceSlug]/logs/utils.ts
  • web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/requests/utils.ts
  • web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/_overview/components/table/logs-table.tsx
  • web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts
  • web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/logs-table.tsx
  • web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/context/logs.tsx
  • web/apps/dashboard/components/logs/details/log-details/index.tsx
  • web/apps/dashboard/lib/trpc/routers/index.ts
  • web/apps/dashboard/lib/trpc/routers/ratelimit/query-logs/enrichment.ts
  • web/apps/dashboard/lib/trpc/routers/ratelimit/query-logs/index.ts
  • web/apps/dashboard/lib/trpc/routers/ratelimit/query-overview-logs/index.ts
  • web/internal/clickhouse/src/index.ts
  • web/internal/clickhouse/src/ratelimits.ts

@perkinsjr perkinsjr marked this pull request as ready for review April 8, 2026 20:11
Copy link
Copy Markdown
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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts:
- Around line 224-229: The realtime row insertion currently creates enriched
rows with only ENRICHMENT_DEFAULTS and never applies later-fetched metadata from
enrichmentMapRef, so update the logic in use-logs-query (the code that builds
`enriched` and sets `newMap`) to re-merge enrichment after enrichmentMapRef is
updated: when enrichmentMapRef.current has data for a request_id, merge that
object into the `enriched` object (same way as for historical rows) and update
`newMap.set(log.request_id, enriched)`; also ensure the code path that rebuilds
`historicalLogsMap` (the block around `historicalLogsMap`) also rebuilds the
realtime/newMap entries or triggers the same merge routine so live-polled rows
pick up fetched metadata without a full reload.
- Around line 56-57: The enrichment cache (enrichmentMapRef) is never evicted or
cleared, causing unbounded memory growth; modify the use-logs-query hook to
either cap entries (implement simple LRU or max size) or clear the map on query
resets/changes and bump enrichmentVersion to force UI updates; specifically
update logic around enrichmentMapRef and setEnrichmentVersion in the
useLogsQuery hook (and the related code referenced at the other occurrence) to
call enrichmentMapRef.current.clear() (or trim entries) whenever the
query/filters/page/long-polling session resets and/or when you programmatically
evict oldest entries once a configurable maxEntries threshold is reached.
- Around line 214-230: The updater for setRealtimeLogsMap mutates newLogs inside
the updater (using newLogs.push) which is unsafe because React may call updaters
async or multiple times; instead, compute the list of logs-to-add outside the
updater and then call setRealtimeLogsMap to merge them in. Concretely: iterate
over result.ratelimitLogs to build a local array (e.g., logsToAdd) skipping
entries present in realtime/historical maps or enrichmentMapRef, then call
setRealtimeLogsMap(prev => { const newMap = new Map(prev); for (const log of
logsToAdd) newMap.set(log.request_id, { ...ENRICHMENT_DEFAULTS, ...log,
...(enrichmentMapRef.current.get(log.request_id) ?? {}) }); return newMap; });
finally call fetchEnrichment(logsToAdd) (or the existing enrichment fetch) with
that local array — remove any newLogs.push usage from inside the updater.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9111d417-0e93-43f5-a7a8-03489c780557

📥 Commits

Reviewing files that changed from the base of the PR and between 046e7b8 and e1fc5d6.

📒 Files selected for processing (2)
  • web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts
  • web/apps/dashboard/components/logs/details/log-details/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/apps/dashboard/components/logs/details/log-details/index.tsx

@perkinsjr perkinsjr merged commit f2b1994 into main Apr 9, 2026
13 of 14 checks passed
@perkinsjr perkinsjr deleted the ratelimit-logs-fixes branch April 9, 2026 22:29
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.

3 participants