Skip to content

Request logs - #3719

Merged
steven-tey merged 59 commits into
mainfrom
request-logs
Apr 12, 2026
Merged

Request logs#3719
steven-tey merged 59 commits into
mainfrom
request-logs

Conversation

@devkiran

@devkiran devkiran commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • API Logs dashboard: paginated, filterable view (method, status, route, token, request type) with persisted search and counts.
    • Log details page: syntax-highlighted request/response bodies, timeline, status/method badges, actor info, copyable IDs.
    • Webhook & API request logging: captures Stripe/AppsFlyer and API events for visibility with per-plan retention windows.
    • UI/navigation: new “Logs” entry and accompanying icon in workspace settings.

@vercel

vercel Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Apr 12, 2026 9:25pm

Request Review

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a complete API/webhook logging system: datasources and pipes for Tinybird, server routes to list/count/fetch logs, capture/ingest logic for requests and webhooks, Prisma-based enrichment, retention utilities, client UI for listing and viewing log details with filters, and instrumentation in middleware and webhook handlers.

Changes

Cohort / File(s) Summary
Tinybird infra & pipes
packages/tinybird/datasources/dub_api_logs.datasource, packages/tinybird/datasources/dub_api_logs_id.datasource, packages/tinybird/pipes/dub_api_logs_id_pipe.pipe, packages/tinybird/pipes/get_api_logs.pipe, packages/tinybird/pipes/get_api_log_by_id.pipe, packages/tinybird/pipes/get_api_logs_count.pipe
Add datasources and pipes for storing, materializing, querying, and counting API/webhook logs (90-day TTL, MV for id pipe, list/count/by-id endpoints).
Server API routes
apps/web/app/api/logs/route.ts, apps/web/app/api/logs/[logId]/route.ts, apps/web/app/api/logs/count/route.ts
New protected endpoints: GET /api/logs (list with filters/pagination), GET /api/logs/count (counts/grouping), GET /api/logs/:logId (single fetch with retention check).
Capture & ingestion
apps/web/lib/api-logs/capture-request-log.ts, apps/web/lib/api-logs/capture-webhook-log.ts, apps/web/lib/api-logs/record-api-log.ts
Implement request/webhook capture, route-pattern matching, async ingestion to Tinybird with retries/exponential backoff, and safe JSON parsing of bodies.
Querying & enrichment
apps/web/lib/api-logs/get-api-logs.ts, apps/web/lib/api-logs/get-api-log.ts, apps/web/lib/api-logs/get-api-logs-count.ts, apps/web/lib/api-logs/enrich-api-logs.ts, apps/web/lib/api-logs/api-log-retention.ts
Provide Tinybird-backed query/count helpers, single-log fetcher, enrichment with Prisma token/user lookups, and plan-based retention window computation.
Schemas, constants, types
apps/web/lib/api-logs/constants.ts, apps/web/lib/api-logs/schemas.ts, apps/web/lib/types.ts
Add route patterns, method/status constants, retention mapping, comprehensive Zod schemas and derived TypeScript types for logs and request types.
Middleware & webhook instrumentation
apps/web/lib/auth/workspace.ts, apps/web/app/(ee)/api/appsflyer/webhook/route.ts, apps/web/app/(ee)/api/stripe/integration/webhook/route.ts
Instrument workspace middleware to call captureRequestLog for mutation requests; schedule captureWebhookLog via waitUntil in AppsFlyer and Stripe webhook handlers.
Client UI (list & detail)
apps/web/ui/logs/logs-table.tsx, apps/web/ui/logs/use-log-filters.ts, apps/web/ui/logs/log-utils.ts, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/logs/page.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/logs/page-client.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/logs/[logId]/page.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/logs/[logId]/page-client.tsx
Add LogsTable with SWR-driven pagination/filters, client-side useLogFilters hook, status badge util, server+client pages for list and detail, JSON highlighting and sidebar metadata.
ID / types / minor UI & tests
apps/web/lib/api/create-id.ts, apps/web/lib/auth/token-cache.ts, apps/web/ui/layout/sidebar/app-sidebar-nav.tsx, apps/web/ui/shared/search-box.tsx, apps/web/ui/modals/reject-partner-application-modal.tsx, apps/web/ui/partners/partner-application-details.tsx, apps/web/lib/integrations/appsflyer/schema.ts, apps/web/tests/commissions/bulk-updates.test.ts, apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
Add req_ ID prefix support, token cache id field, add "Logs" nav entry with new StackY3 icon, minor UI formatting tweaks, test/formatting edits, and a small import reorder in Stripe helper file.
Icons
packages/ui/src/icons/nucleo/stack-y-3.tsx, packages/ui/src/icons/nucleo/index.ts, packages/ui/src/icons/index.tsx
Introduce new StackY3 icon and export it; adjust re-export ordering.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Request
    participant Middleware as Workspace Middleware
    participant Handler as Route Handler
    participant Capture as captureRequestLog
    participant Record as recordApiLog
    participant Tinybird as Tinybird Ingestion

    Client->>Middleware: HTTP request
    Middleware->>Middleware: extract session, token, workspace, startTime
    Middleware->>Handler: invoke route handler
    Handler->>Middleware: return Response
    Middleware->>Capture: call captureRequestLog (if mutation & workspace)
    Capture->>Capture: parse bodies, match routePattern
    Capture->>Record: enqueue recordApiLog via waitUntil
    Record->>Tinybird: ingest payload (retry/backoff)
    Middleware->>Client: respond
Loading
sequenceDiagram
    participant Browser as Logs UI
    participant Router as Next.js Router
    participant APIRoute as GET /api/logs
    participant Middleware as withWorkspace
    participant GetLogs as getApiLogs
    participant Tinybird as Tinybird Query
    participant Enrich as enrichApiLogs
    participant Prisma as Prisma DB

    Browser->>Router: request logs with filters/page
    Router->>APIRoute: call API route
    APIRoute->>Middleware: check workspace & perms
    Middleware->>GetLogs: call with filters & date range
    GetLogs->>Tinybird: run query (WHERE, LIMIT/OFFSET)
    Tinybird->>GetLogs: return raw logs
    GetLogs->>Enrich: enrich logs
    Enrich->>Prisma: fetch tokens & users by ids
    Prisma->>Enrich: return entities
    Enrich->>APIRoute: return enriched logs
    APIRoute->>Browser: JSON response
Loading
sequenceDiagram
    participant Webhook as Webhook Handler
    participant Capture as captureWebhookLog
    participant Record as recordApiLog
    participant Tinybird as Tinybird Ingestion
    participant Caller as External Sender

    Caller->>Webhook: POST webhook
    Webhook->>Webhook: process event -> finalResponse
    Webhook->>Capture: waitUntil(captureWebhookLog(...))
    Capture->>Record: call recordApiLog(requestType: "webhook")
    Record->>Tinybird: ingest (retry/backoff)
    Webhook->>Caller: return finalResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • pepeladeira

Poem

🐰
I hopped through logs both big and small,
Counting routes and bodies, one and all.
Tinybird nests each thump and trill,
Webhooks and requests—neatly still.
A carrot cheer for every recorded call!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Request logs' is directly related to the main change: introducing comprehensive API/webhook request logging infrastructure including new log capture, storage, display, and query capabilities.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch request-logs

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.

devkiran added 2 commits April 8, 2026 22:44
- Create log-utils.ts with shared ApiLog type and badge variant helpers
- Add LogsTable with method/status/endpoint/API key filters, pagination
- Refactor detail page to use shared utils from log-utils.ts
- Add use-log-filters hook with URL param-based filter state
devkiran added 6 commits April 9, 2026 14:15
- Add exponential backoff retry (3 attempts) to recordApiLog
- Move Logs nav item after Tracking in Developer sidebar
- Update StackY3 icon SVG and use it in empty state
- Simplify empty state description
- Introduced HTTP_METHODS and HTTP_STATUS_CODES constants in api-logs/constants.ts
- Updated use-log-filters.ts to utilize the new constants for method and status options
- Added new filter for "Endpoint" with options from LOGGED_PATH_PREFIXES
- Updated icons for "Method" and "Status" filters to ArrowsOppositeDirectionX and CircleCheck respectively
- Removed duplicate "Endpoint" filter definition
- Replace startsWith with minimatch for flexible glob pattern matching in shouldLogRoute
- Rename LOGGED_PATH_PREFIXES to LOGGED_API_PATH_PATTERNS with glob suffixes
- Add /api/commissions/** and /api/bounties/** to logged paths
- Color-code status filter icons (green for 2xx, red for others)
- Reorder filters: status, endpoint, method, API key
devkiran added 2 commits April 9, 2026 15:01
- Move endpoint column first, timestamp last in logs table
- Simplify recordApiLog retry to use return await
- Left column: request/response JSON bodies with Shiki syntax highlighting
- Right column: log details sidebar (endpoint, date, status, method, duration, etc.)
- Breadcrumb title matching commission detail page pattern
- Loading skeleton and error states
- TimestampTooltip on date, CopyButton on request ID
- 4xx status codes now use error variant instead of warning
devkiran added 2 commits April 9, 2026 16:00
- Add enrichApiLogs() to batch-fetch tokens and users from Prisma
- Add apiLogEnrichedSchema with token/user fields
- Enrich logs in both list and detail API endpoints
- Show token partialKey or user name+avatar in table and detail sidebar
- Introduced API_LOGS_MAX_PAGE_SIZE constant set to 10 in constants.ts
- Updated get-api-logs.ts and schemas.ts to import the new constant
- Removed redundant API_LOGS_MAX_PAGE_SIZE definition from schemas.ts
- Simplified logs-table.tsx by removing unused useRouterStuff import
devkiran added 3 commits April 9, 2026 17:51
…tion

- Replace LIKE with startsWith/exact match in Tinybird pipes to avoid
  parameterized query escaping issues and prevent LIKE pattern injection
- Fix pagination by passing API_LOGS_MAX_PAGE_SIZE to usePagination and
  including page param in SWR fetch key
- Add request ID search input and pass requestId through to Tinybird
- Fix SearchBoxPersisted using hardcoded "search" key instead of urlParam
- Add column filter buttons (path, method, status code) to logs table
- Increase JSON preview max height to 800px
- Gate captureRequestLog to enterprise plan only
- Gate Logs sidebar nav item to enterprise plan workspaces
- Move EnrichedApiLog type to shared types.ts
- Use HTTP_METHODS constant in capture-request-log
- Increase JSON preview max height to 800px

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/web/app/api/logs/route.ts (1)

11-17: ⚠️ Potential issue | 🟠 Major

Map page / pageSize before calling getApiLogs.

getApiLogsQuerySchema still parses page-based params, but this route forwards them unchanged. That keeps pagination as a no-op if getApiLogs still expects limit / offset, so page 2+ will keep returning the first batch.

🛠️ Suggested fix
-    const filters = getApiLogsQuerySchema.parse(searchParams);
+    const { page = 1, pageSize, ...filters } =
+      getApiLogsQuerySchema.parse(searchParams);

     const logs = await getApiLogs({
       ...filters,
       ...getApiLogsDateRange(workspace.plan),
       workspaceId: workspace.id,
+      limit: pageSize,
+      offset: (page - 1) * pageSize,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/logs/route.ts` around lines 11 - 17, The route currently
parses pagination with getApiLogsQuerySchema and passes filters straight into
getApiLogs, but getApiLogs expects limit/offset; map page and pageSize from
filters to limit and offset before calling getApiLogs (e.g., compute limit =
pageSize, offset = (page - 1) * pageSize) and pass those along with
...getApiLogsDateRange(workspace.plan) and workspaceId: workspace.id; update the
usage of the parsed filters variable so getApiLogs receives limit/offset instead
of page/pageSize.
apps/web/app/api/logs/[logId]/route.ts (1)

23-25: ⚠️ Potential issue | 🟠 Major

Parse the retention cutoff as UTC.

getApiLogsDateRange() returns a ClickHouse-formatted UTC string, but new Date(start) parses that format in the server's local timezone. The retention boundary will drift accordingly.

🛠️ Suggested fix
     const { start } = getApiLogsDateRange(workspace.plan);
+    const retentionStart = new Date(start.replace(" ", "T") + "Z");

-    if (new Date(log.timestamp) < new Date(start)) {
+    if (new Date(log.timestamp) < retentionStart) {
       throw new DubApiError({
         code: "not_found",
         message: "API log not found.",
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/logs/`[logId]/route.ts around lines 23 - 25,
getApiLogsDateRange returns a UTC timestamp string but new Date(start) treats it
as local time, causing retention cutoff drift; update the comparison in the
route handling (the new Date(log.timestamp) < new Date(start) check) to parse
the returned start as UTC — e.g., transform the ClickHouse string from
getApiLogsDateRange(workspace.plan).start into an ISO UTC form (replace the
space with 'T' and append 'Z' or otherwise normalize to UTC) before constructing
the Date object, then compare against new Date(log.timestamp).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/settings/logs/[logId]/page-client.tsx:
- Around line 185-208: The UI currently treats
highlightedRequest/highlightedResponse empty strings as "no body", causing valid
payloads to show "No request body" while Shiki highlighting initializes; change
the empty-state checks to use the raw payloads (log.request_body and
log.response_body) to decide whether there is content, and keep the
highlightedRequest/highlightedResponse variables to indicate highlighting state
— when the raw body exists but highlighted* is empty, render a loading indicator
or a plain preformatted/raw-text fallback until highlighting completes or fails;
update the render logic around highlightedRequest, highlightedResponse,
log.request_body, and log.response_body accordingly.

In `@apps/web/ui/logs/logs-table.tsx`:
- Around line 107-111: meta.filterParams currently forwards row.original.method
verbatim but getApiLogsQuerySchema only accepts POST, PATCH, PUT, DELETE; update
the meta.filterParams implementation in logs-table.tsx to validate
row.original.method against the allowed set (e.g. const allowed = new
Set(['POST','PATCH','PUT','DELETE'])) and only return { method } when allowed;
otherwise return an empty object or undefined so the inline filter button isn't
emitted for unsupported methods. Reference the meta.filterParams function and
row.original.method and ensure behavior matches getApiLogsQuerySchema.

---

Duplicate comments:
In `@apps/web/app/api/logs/`[logId]/route.ts:
- Around line 23-25: getApiLogsDateRange returns a UTC timestamp string but new
Date(start) treats it as local time, causing retention cutoff drift; update the
comparison in the route handling (the new Date(log.timestamp) < new Date(start)
check) to parse the returned start as UTC — e.g., transform the ClickHouse
string from getApiLogsDateRange(workspace.plan).start into an ISO UTC form
(replace the space with 'T' and append 'Z' or otherwise normalize to UTC) before
constructing the Date object, then compare against new Date(log.timestamp).

In `@apps/web/app/api/logs/route.ts`:
- Around line 11-17: The route currently parses pagination with
getApiLogsQuerySchema and passes filters straight into getApiLogs, but
getApiLogs expects limit/offset; map page and pageSize from filters to limit and
offset before calling getApiLogs (e.g., compute limit = pageSize, offset = (page
- 1) * pageSize) and pass those along with
...getApiLogsDateRange(workspace.plan) and workspaceId: workspace.id; update the
usage of the parsed filters variable so getApiLogs receives limit/offset instead
of page/pageSize.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34133f6e-23f9-4674-8d0f-0f703521d33a

📥 Commits

Reviewing files that changed from the base of the PR and between 60430ad and ef55a18.

📒 Files selected for processing (7)
  • apps/web/app/api/logs/[logId]/route.ts
  • apps/web/app/api/logs/count/route.ts
  • apps/web/app/api/logs/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/logs/[logId]/page-client.tsx
  • apps/web/lib/swr/use-api-logs-count.ts
  • apps/web/ui/logs/logs-table.tsx
  • packages/tinybird/datasources/dub_api_logs_id.datasource
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/lib/swr/use-api-logs-count.ts
  • packages/tinybird/datasources/dub_api_logs_id.datasource

Comment on lines +185 to +208
{highlightedRequest ? (
<div
className="shiki-wrapper max-h-[800px] overflow-auto rounded-xl border border-neutral-200 bg-white p-4 text-sm"
dangerouslySetInnerHTML={{ __html: highlightedRequest }}
/>
) : (
<div className="rounded-xl border border-neutral-200 bg-white p-4 font-mono text-xs text-neutral-500">
No request body
</div>
)}
</div>
<div className="flex flex-col gap-2">
<h3 className="text-content-emphasis text-lg font-semibold">
Response body
</h3>
{highlightedResponse ? (
<div
className="shiki-wrapper max-h-[800px] overflow-auto rounded-xl border border-neutral-200 bg-white p-4 text-sm"
dangerouslySetInnerHTML={{ __html: highlightedResponse }}
/>
) : (
<div className="rounded-xl border border-neutral-200 bg-white p-4 font-mono text-xs text-neutral-500">
No response body
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Separate “no body” from “highlighter still loading”.

Both sections key off highlightedRequest / highlightedResponse, which start as empty strings. That makes valid payloads render as “No request body” / “No response body” until the Shiki import finishes, and forever if it fails. Base the empty-state on log.request_body / log.response_body instead, and show a loading or raw-text fallback while highlighting initializes.

🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 187-187: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)


[warning] 202-202: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

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

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/settings/logs/[logId]/page-client.tsx
around lines 185 - 208, The UI currently treats
highlightedRequest/highlightedResponse empty strings as "no body", causing valid
payloads to show "No request body" while Shiki highlighting initializes; change
the empty-state checks to use the raw payloads (log.request_body and
log.response_body) to decide whether there is content, and keep the
highlightedRequest/highlightedResponse variables to indicate highlighting state
— when the raw body exists but highlighted* is empty, render a loading indicator
or a plain preformatted/raw-text fallback until highlighting completes or fails;
update the render logic around highlightedRequest, highlightedResponse,
log.request_body, and log.response_body accordingly.

Comment on lines +107 to +111
meta: {
filterParams: ({ row }: { row: Row<EnrichedApiLog> }) => ({
method: row.original.method,
}),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't emit method filters the backend won't parse.

This forwards row.original.method verbatim, but getApiLogsQuerySchema only accepts POST, PATCH, PUT, and DELETE. A row with GET (or any other method) will turn the inline filter button into a failing /api/logs request. Either widen the schema or suppress the filter action for unsupported methods.

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

In `@apps/web/ui/logs/logs-table.tsx` around lines 107 - 111, meta.filterParams
currently forwards row.original.method verbatim but getApiLogsQuerySchema only
accepts POST, PATCH, PUT, DELETE; update the meta.filterParams implementation in
logs-table.tsx to validate row.original.method against the allowed set (e.g.
const allowed = new Set(['POST','PATCH','PUT','DELETE'])) and only return {
method } when allowed; otherwise return an empty object or undefined so the
inline filter button isn't emitted for unsupported methods. Reference the
meta.filterParams function and row.original.method and ensure behavior matches
getApiLogsQuerySchema.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
apps/web/lib/api-logs/record-api-log.ts (2)

59-60: ⚠️ Potential issue | 🟡 Minor

Guard serialization so circular payloads don’t break log ingestion.

JSON.stringify can throw on circular structures, which causes this call path to fail before ingest retries can help.

🛡️ Suggested change
+const safeStringify = (value: unknown) => {
+  try {
+    return JSON.stringify(value);
+  } catch {
+    return '"[unserializable]"';
+  }
+};
+
 export const recordApiLog = async ({
@@
-    request_body: JSON.stringify(requestBody),
-    response_body: JSON.stringify(responseBody),
+    request_body: safeStringify(requestBody),
+    response_body: safeStringify(responseBody),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/api-logs/record-api-log.ts` around lines 59 - 60, The current
JSON.stringify(requestBody)/JSON.stringify(responseBody) can throw on circular
structures; update the record-api-log logic (e.g., in recordApiLog) to guard
serialization by using a safe serializer: implement or call a
safeSerialize(value) helper that attempts JSON.stringify(value) in try/catch and
on failure returns a fallback string (e.g., "[unserializable]" plus minimal
error info) or uses a circular-safe replacer/serializer, then replace the direct
JSON.stringify calls for request_body and response_body with
safeSerialize(requestBody) and safeSerialize(responseBody).

59-60: ⚠️ Potential issue | 🟠 Major

Avoid persisting/logging unsanitized request/response bodies.

This currently stores raw bodies and also prints the full payload on failure (Line 79), which can leak secrets/PII. Redact sensitive keys and log only metadata in error paths.

🔒 Suggested tightening
+const SENSITIVE_KEYS = /password|token|authorization|secret|api[-_]?key|cookie|ssn|credit_?card/i;
+const MAX_BODY_LEN = 2048;
+
+const sanitize = (value: unknown): string => {
+  try {
+    const seen = new WeakSet<object>();
+    const json = JSON.stringify(value, (k, v) => {
+      if (SENSITIVE_KEYS.test(k)) return "[redacted]";
+      if (v && typeof v === "object") {
+        if (seen.has(v as object)) return "[circular]";
+        seen.add(v as object);
+      }
+      return v;
+    });
+    return json.length > MAX_BODY_LEN ? `${json.slice(0, MAX_BODY_LEN)}…` : json;
+  } catch {
+    return '"[unserializable]"';
+  }
+};
@@
-    request_body: JSON.stringify(requestBody),
-    response_body: JSON.stringify(responseBody),
+    request_body: sanitize(requestBody),
+    response_body: sanitize(responseBody),
@@
-      console.error("Failed to record API log", error, JSON.stringify(apiLog));
+      console.error("Failed to record API log", error, {
+        id: apiLog.id,
+        timestamp: apiLog.timestamp,
+        method: apiLog.method,
+        path: apiLog.path,
+        route_pattern: apiLog.route_pattern,
+        status_code: apiLog.status_code,
+        request_type: apiLog.request_type,
+        workspace_id: apiLog.workspace_id,
+      });

Also applies to: 79-83

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

In `@apps/web/lib/api-logs/record-api-log.ts` around lines 59 - 60, The code
currently persists raw requestBody and responseBody (see request_body:
JSON.stringify(requestBody) and response_body: JSON.stringify(responseBody)) and
prints full payload on the error path around line 79; instead, implement a
sanitizer that redacts sensitive keys (e.g., "password", "token",
"authorization", "apiKey", "ssn", "creditCard") from requestBody and
responseBody before stringifying for storage in recordApiLog (or whatever
function wraps this block), and change the error logging path to emit only
non-sensitive metadata (status, headers summary, truncated length) rather than
the full payload; locate the serialization calls and replace them with calls to
the sanitizer/metadata extractor so only redacted bodies are persisted and only
metadata is logged on failures.
🧹 Nitpick comments (1)
apps/web/lib/api-logs/record-api-log.ts (1)

54-54: Normalize only a leading /api prefix.

Line 54 uses replace("/api/", "/"), which can rewrite a non-leading /api/ segment. Make normalization prefix-only to avoid unintended path mutation.

♻️ Suggested change
-    path: path.replace("/api/", "/"), // remove the /api/ prefix from the path
+    path:
+      path === "/api"
+        ? "/"
+        : path.startsWith("/api/")
+          ? path.slice(4)
+          : path, // remove only leading /api prefix
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/api-logs/record-api-log.ts` at line 54, The current
normalization uses path.replace("/api/", "/"), which can replace non-leading
`/api/` segments; change it to only strip a leading `/api` by using a
prefix-aware regex such as path.replace(/^\/api(\/|$)/, "/") so "/api/foo" ->
"/foo" and "/api" -> "/"; update the occurrence of path.replace in
record-api-log (the path property assignment) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/lib/api-logs/record-api-log.ts`:
- Around line 59-60: The current
JSON.stringify(requestBody)/JSON.stringify(responseBody) can throw on circular
structures; update the record-api-log logic (e.g., in recordApiLog) to guard
serialization by using a safe serializer: implement or call a
safeSerialize(value) helper that attempts JSON.stringify(value) in try/catch and
on failure returns a fallback string (e.g., "[unserializable]" plus minimal
error info) or uses a circular-safe replacer/serializer, then replace the direct
JSON.stringify calls for request_body and response_body with
safeSerialize(requestBody) and safeSerialize(responseBody).
- Around line 59-60: The code currently persists raw requestBody and
responseBody (see request_body: JSON.stringify(requestBody) and response_body:
JSON.stringify(responseBody)) and prints full payload on the error path around
line 79; instead, implement a sanitizer that redacts sensitive keys (e.g.,
"password", "token", "authorization", "apiKey", "ssn", "creditCard") from
requestBody and responseBody before stringifying for storage in recordApiLog (or
whatever function wraps this block), and change the error logging path to emit
only non-sensitive metadata (status, headers summary, truncated length) rather
than the full payload; locate the serialization calls and replace them with
calls to the sanitizer/metadata extractor so only redacted bodies are persisted
and only metadata is logged on failures.

---

Nitpick comments:
In `@apps/web/lib/api-logs/record-api-log.ts`:
- Line 54: The current normalization uses path.replace("/api/", "/"), which can
replace non-leading `/api/` segments; change it to only strip a leading `/api`
by using a prefix-aware regex such as path.replace(/^\/api(\/|$)/, "/") so
"/api/foo" -> "/foo" and "/api" -> "/"; update the occurrence of path.replace in
record-api-log (the path property assignment) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0a745d85-8172-40bd-ba2e-dfc53fb00052

📥 Commits

Reviewing files that changed from the base of the PR and between ef55a18 and bd50e79.

📒 Files selected for processing (5)
  • apps/web/app/api/logs/[logId]/route.ts
  • apps/web/lib/api-logs/capture-request-log.ts
  • apps/web/lib/api-logs/constants.ts
  • apps/web/lib/api-logs/record-api-log.ts
  • apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/web/lib/api-logs/constants.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/ui/layout/sidebar/app-sidebar-nav.tsx
  • apps/web/app/api/logs/[logId]/route.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/web/ui/logs/use-log-filters.ts (1)

135-139: ⚠️ Potential issue | 🟡 Minor

onRemoveAll should still clear page.

Clearing filters can shrink the result set, so keeping the old page cursor can strand the user on an empty page. onSelect and onRemove already reset pagination; this handler should stay consistent.

♻️ Proposed fix
   const onRemoveAll = useCallback(
     () =>
       queryParams({
-        del: ["method", "statusCode", "routePattern", "tokenId", "requestType"],
+        del: [
+          "method",
+          "statusCode",
+          "routePattern",
+          "tokenId",
+          "requestType",
+          "page",
+        ],
       }),
     [queryParams],
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/ui/logs/use-log-filters.ts` around lines 135 - 139, The onRemoveAll
handler currently calls queryParams({ del:
["method","statusCode","routePattern","tokenId","requestType"] }) but does not
clear pagination; modify the onRemoveAll implementation (the function named
onRemoveAll that calls queryParams) to also remove the "page" param (e.g.,
include "page" in the del array or explicitly reset page) so it matches
onSelect/onRemove behavior and avoids leaving the user on an empty page.
apps/web/lib/api-logs/schemas.ts (1)

80-88: ⚠️ Potential issue | 🟠 Major

Allow GET here, or stop the UI from emitting it.

The logs UI can surface GET rows and build method=GET filters, but this schema rejects that query. That turns a valid row action into a failing /api/logs request. This is the same mismatch raised earlier on the table side; the schema is the root cause.

♻️ Proposed fix
 export const getApiLogsQuerySchema = z
   .object({
     routePattern: z.string().optional(),
-    method: z.enum(["POST", "PATCH", "PUT", "DELETE"]).optional(),
+    method: z.enum(["GET", "POST", "PATCH", "PUT", "DELETE"]).optional(),
     statusCode: z.coerce.number().int().optional(),
     tokenId: z.string().optional(),
     requestId: z.string().optional(),
     requestType: requestTypeSchema.optional(),
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/api-logs/schemas.ts` around lines 80 - 88, The
getApiLogsQuerySchema rejects GET requests causing valid UI filters to fail;
update the method enum in getApiLogsQuerySchema (the `method` property) to
include "GET" (i.e., allow "GET","POST","PATCH","PUT","DELETE") so the schema
matches the UI, or alternatively change the UI to stop emitting method=GET
filters—prefer adding "GET" to the z.enum for immediate compatibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/ui/logs/use-log-filters.ts`:
- Around line 38-49: activeFilters currently omits requestId so UI and
empty-state logic don't reflect that hidden filter; update the useMemo that
builds activeFilters (using searchParamsObj destructured into method,
statusCode, routePattern, tokenId, requestType) to also include requestId when
present (e.g., add ...(requestId ? [{ key: "requestId", value: requestId }] :
[])); repeat the same change for the other filter-construction occurrence that
mirrors lines 143-156 so both places track requestId consistently.
- Around line 51-54: The grouped fetch for routePattern uses useApiLogsCount
while the active routePattern filter is still applied, causing the response to
only include the currently selected route; update the useApiLogsCount call where
routePatterns is defined so it explicitly omits or clears the routePattern
filter when building params (e.g., remove/override the routePattern field in the
request payload) whenever selectedFilter === "routePattern" so the dropdown
receives all endpoint options; locate the call to useApiLogsCount and adjust the
filters/params you pass (or clone and delete routePattern) so the grouped
request is not constrained by the active routePattern.

---

Duplicate comments:
In `@apps/web/lib/api-logs/schemas.ts`:
- Around line 80-88: The getApiLogsQuerySchema rejects GET requests causing
valid UI filters to fail; update the method enum in getApiLogsQuerySchema (the
`method` property) to include "GET" (i.e., allow
"GET","POST","PATCH","PUT","DELETE") so the schema matches the UI, or
alternatively change the UI to stop emitting method=GET filters—prefer adding
"GET" to the z.enum for immediate compatibility.

In `@apps/web/ui/logs/use-log-filters.ts`:
- Around line 135-139: The onRemoveAll handler currently calls queryParams({
del: ["method","statusCode","routePattern","tokenId","requestType"] }) but does
not clear pagination; modify the onRemoveAll implementation (the function named
onRemoveAll that calls queryParams) to also remove the "page" param (e.g.,
include "page" in the del array or explicitly reset page) so it matches
onSelect/onRemove behavior and avoids leaving the user on an empty page.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54178e86-b6b7-461e-af36-fbf6dcd200b3

📥 Commits

Reviewing files that changed from the base of the PR and between bd50e79 and fb5d7ee.

📒 Files selected for processing (7)
  • apps/web/app/api/logs/count/route.ts
  • apps/web/lib/api-logs/get-api-logs-count.ts
  • apps/web/lib/api-logs/schemas.ts
  • apps/web/lib/swr/use-api-logs-count.ts
  • apps/web/lib/types.ts
  • apps/web/ui/logs/logs-table.tsx
  • apps/web/ui/logs/use-log-filters.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/lib/swr/use-api-logs-count.ts
  • apps/web/lib/types.ts

Comment thread apps/web/ui/logs/use-log-filters.ts
Comment thread apps/web/ui/logs/use-log-filters.ts
@steven-tey

Copy link
Copy Markdown
Collaborator

@coderabbitai final review, any breaking changes?

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me investigate the key areas for potential breaking changes.


🏁 Script executed:

#!/bin/bash
echo "=== NAV_GROUPS usages (checking if defaultProgramId was being passed) ==="
rg -n "NAV_GROUPS" --type=tsx --type=ts -A3 -B3

echo ""
echo "=== SearchBox usages (checking urlParam usage) ==="
rg -n "SearchBox" --type=tsx --type=ts -C3

echo ""
echo "=== withWorkspace signature changes (checking request clone / startTime) ==="
rg -n "withWorkspace" apps/web/lib/auth/workspace.ts -A5 -B5

echo ""
echo "=== token-cache id field callers ==="
rg -n "TokenCacheItem\|tokenCacheItemSchema\|token\.id" apps/web/lib/auth/ -A2 -B2

echo ""
echo "=== SearchBox urlParam prop usage ==="
rg -n "urlParam" --type=tsx --type=ts -C3

Length of output: 610


🏁 Script executed:

#!/bin/bash
echo "=== NAV_GROUPS usages ==="
rg -n "NAV_GROUPS" -g "*.ts" -g "*.tsx" -A3 -B3

echo ""
echo "=== SearchBox urlParam prop ==="
rg -n "urlParam" -g "*.ts" -g "*.tsx" -C3

echo ""
echo "=== SearchBox component usages ==="
rg -n "<SearchBox" -g "*.ts" -g "*.tsx" -C2

echo ""
echo "=== token.id usage in workspace.ts ==="
rg -n "token\.id\|token\?\.id\|captureRequestLog" apps/web/lib/auth/workspace.ts -C3

echo ""
echo "=== workspace.ts full auth flow around captureRequestLog ==="
rg -n "captureRequestLog\|reqForLog\|startTime\|session\s*=\|token\s*=" apps/web/lib/auth/workspace.ts -C2

echo ""
echo "=== getApiLogsDateRange uses formatUTCDateTimeClickhouse - check what format that produces ==="
rg -n "formatUTCDateTimeClickhouse" -g "*.ts" -g "*.tsx" -C3

echo ""
echo "=== check if any existing SearchBox passes urlParam ==="
rg -n "SearchBox" -g "*.ts" -g "*.tsx" -B2 -A5

Length of output: 67953


@steven-tey: After reviewing the full diff and tracing all changed call sites, there are no breaking changes in this PR. Here's a summary of what I checked:


✅ Safe Changes (non-breaking)

Area Change Assessment
SearchBoxPersisted (search-box.tsx) Set branch now uses dynamic [urlParam] key instead of hardcoded "search" Bug fixurlParam defaults to "search", so all ~10 existing callers that omit urlParam are unaffected
NAV_GROUPS (app-sidebar-nav.tsx) Removed defaultProgramId from destructured params Internal only — not exported; SidebarNav passes the factory reference, and no callers supply defaultProgramId in the data object
tokenCacheItemSchema (token-cache.ts) Added id: z.string().nullish() Additive — nullish field, existing cached items without id still parse fine
withWorkspace (workspace.ts) Clones request as reqForLog before handler; original req flows to handler unchanged SafecaptureRequestLog uses the clone for JSON parsing; original request stream is untouched
New API routes: /api/logs, /api/logs/count, /api/logs/[logId] All new endpoints Additive
New Tinybird datasources/pipes dub_api_logs, dub_api_logs_id, get_api_logs, etc. Additive

⚠️ Things to Double-Check at Deploy Time

  1. Tinybird deployment order: dub_api_logs datasource must be pushed before dub_api_logs_id_pipe (materialized view), which in turn must exist before dub_api_logs_id. If deployed out of order, the materialized view will fail.

  2. getStatusCodeBadgeVariant (log-utils.ts): Currently maps both 4xx and 5xx (and anything outside 2xx) to the "error" badge variant — no distinct treatment for server errors. Likely intentional but worth confirming the UI intent.

@steven-tey

Copy link
Copy Markdown
Collaborator

@coderabbitai please fully review the changes in stripe/integration/webhook – we need to make sure it's bulletproof. Anything that could break?

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me pull the actual file contents for a thorough review.


🏁 Script executed:

#!/bin/bash
echo "=== stripe/integration/webhook/route.ts ==="
cat apps/web/app/\(ee\)/api/stripe/integration/webhook/route.ts

echo ""
echo "=== checkout-session-completed.ts ==="
cat apps/web/app/\(ee\)/api/stripe/integration/webhook/checkout-session-completed.ts

echo ""
echo "=== capture-webhook-log.ts ==="
cat apps/web/lib/api-logs/capture-webhook-log.ts

echo ""
echo "=== record-api-log.ts ==="
cat apps/web/lib/api-logs/record-api-log.ts

echo ""
echo "=== all files under stripe/integration/webhook/ ==="
fd . apps/web/app/\(ee\)/api/stripe/integration/webhook/

echo ""
echo "=== look for waitUntil imports/usage in route.ts ==="
rg -n "waitUntil\|captureWebhookLog\|startTime\|finalResponse\|logAndRespond" apps/web/app/\(ee\)/api/stripe/integration/webhook/route.ts -C3

Length of output: 34704


@steven-tey: Here's a thorough breakdown of the Stripe integration webhook changes. No breaking changes found, but there are a few things worth verifying:


✅ What's safe

  1. Early returns are untouched — missing/invalid signature (400), unsupported events (200), and test events on the live endpoint all return before the new waitUntil block. The logging path is strictly additive.

  2. Request body consumptionreq.text() is called first (for Stripe signature verification), but captureWebhookLog never reads from req — it receives the already-parsed event object. No body-consumption conflict.

  3. result variable capturewaitUntil is called synchronously after the switch block completes, so result is fully assigned before the async IIFE begins. No race condition.

  4. path normalization/api/stripe/integration/webhook correctly becomes /stripe/integration/webhook via the replace("/api/", "/") in recordApiLog.

  5. checkout-session-completed.ts — only an import reorder, zero logic change.


⚠️ Things to verify / potential gotchas

1. responseBody is double-JSON-encoded

In route.ts, captureWebhookLog is called with:

responseBody: finalResponse  // e.g. "[checkout.session.completed]: OK"

captureWebhookLogparseResponseBody passes strings through unchanged. Then recordApiLog calls JSON.stringify(responseBody), turning the string into '"[checkout.session.completed]: OK"' — a JSON-encoded string, not an object. The log UI will show it wrapped in quotes. Low severity, but worth knowing.


2. requestBody: event — full Stripe event stored in Tinybird

The entire Stripe event object is passed as requestBody and gets JSON.stringify'd into the request_body field. checkout.session.completed events in particular are large (nested charge, customer, subscription objects). If the stringified payload exceeds Tinybird's field size limit, the log ingestion will fail. The retry loop + console.error in recordApiLog will handle it gracefully (no crash), but you'd silently lose those log entries after 4 failed attempts.

Recommendation: Consider truncating or redacting requestBody before passing it to captureWebhookLog (e.g., { id: event.id, type: event.type, account: event.account }).


3. No try/catch around prisma.findUnique inside waitUntil

waitUntil(
  (async () => {
    if (!result.workspaceId && ...) {
      const workspace = await prisma.project.findUnique(...); // can throw
      ...
    }
    if (result.workspaceId) {
      await captureWebhookLog(...);
    }
  })(),
);

If the Prisma query throws (e.g., DB blip), the unhandled rejection inside waitUntil is silently dropped by Vercel's runtime. The main request is unaffected, but you'd lose the log entry with no error surfaced. A simple try/catch with a console.error would make this observable.


4. statusCode: 200 is hardcoded

statusCode: 200,

Since the integration webhook always returns 200 to Stripe (via logAndRespond), this is accurate by design. Not a bug — just worth being explicit about.


5. Unhandled-exception paths are not logged

If any handler (e.g., chargeRefunded, invoicePaid) throws an unhandled exception, the switch block propagates the error, waitUntil is never reached, and no log is captured. withAxiom catches this and returns a 500, but there will be no entry in the request logs for that event. Again, not a regression — just a known gap.


Summary

Issue Severity Breaking?
responseBody double-encoded as JSON string Low No
Full Stripe event as requestBody (size concern) Medium No — fails gracefully
No try/catch in waitUntil IIFE Low No
statusCode: 200 hardcoded Info No
Exceptions in handlers not logged Low No

🧠 Learnings used
Learnt from: steven-tey
Repo: dubinc/dub PR: 3679
File: apps/web/app/(ee)/api/discount-codes/route.ts:69-90
Timestamp: 2026-04-01T17:57:15.397Z
Learning: In the dubinc/dub codebase, a workspace can only ever have one installed Stripe integration at a time. Therefore, `prisma.installedIntegration.findFirst({ where: { projectId, integrationId: STRIPE_INTEGRATION_ID } })` is always deterministic and there is no risk of returning the wrong row. Do not flag this pattern as nondeterministic in future reviews.

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