Skip to content

feat(billing): expose credit usage log in Billing settings - #5391

Merged
waleedlatif1 merged 4 commits into
stagingfrom
feat/credit-usage-log
Jul 3, 2026
Merged

feat(billing): expose credit usage log in Billing settings#5391
waleedlatif1 merged 4 commits into
stagingfrom
feat/credit-usage-log

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Add a "Credit usage" section under Billing settings, below Invoices — a paginated, period-filterable list of individual credit-consuming events (model, tool, fixed charges), shown to every plan except Enterprise
  • The backend query already existed (getUserUsageLogs) but the /api/users/me/usage-logs route was dead/unused — wired it up with a proper getUsageLogsContract, a useUsageLogs infinite-query hook, and an emcn-styled UI section matching the existing Invoices list
  • Fixed a stale query-param enum on the route that only allowed 3 of the real 9 usage_log sources
  • Extracted formatCreditsLabel in conversion.ts so the credit-usage rows, the billing cost breakdown, and the trace view all share one formatter instead of diverging

Type of Change

  • New feature

Testing

  • Added route.test.ts (5 tests: auth, dollar→credit conversion, invalid period, start-date resolution per period)
  • Verified live: ran the app against a local seeded DB, forged a Better Auth session, drove it with Playwright — confirmed the section is hidden for an Enterprise-plan user, correctly renders and reconciles totals for a Free-plan user with real usage rows, and the period dropdown re-filters correctly
  • check:api-validation, typecheck, and full test suite all pass

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Add a "Credit usage" section under Billing, below Invoices, showing a
paginated, period-filterable list of individual credit-consuming
events (model, tool, and fixed charges) for every plan except
Enterprise. Wires the existing (previously unused) usage-logs backend
to a proper contract, React Query hook, and emcn-styled UI:
- getUsageLogsContract in contracts/user.ts, broadened the source enum
to match the real usage_log schema
- Rewrote the route to use parseRequest per the API boundary rules
- useUsageLogs infinite-query hook, keyset-paginated
- CreditUsageSection: period dropdown, total-credits summary, row
list with source badges, "Load more"
- Extracted formatCreditsLabel in conversion.ts as the shared
formatter for already-converted integer credits
@vercel

vercelBot commented Jul 3, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
docsSkippedSkippedJul 3, 2026 8:13pm

Request Review

@cursor

cursorBot commented Jul 3, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches a user billing read API (response shape and source filter) and displays usage ledger data; changes are scoped to authenticated self-service with new route tests and no payment writes.

Overview
Adds a Credit usage section to Billing settings (non-Enterprise) so users can see paginated, period-filtered credit events with totals and humanized source labels.

The /api/users/me/usage-logs handler is refactored to validate via getUsageLogsContract and parseRequest, maps dollar costs to credits-only responses (creditCost, totalCredits, bySourceCredits), trims log fields, and expands allowed source values to match the full usage ledger. Period filtering is centralized in resolveStartDate.

Client wiring includes useUsageLogs (infinite query + contract client), URL-synced period via nuqs, and formatCreditsLabel shared with existing dollar-based credit formatting.

Reviewed by Cursor Bugbot for commit 73679b1. Configure here.

Comment threadapps/sim/app/api/users/me/usage-logs/route.ts
@greptile-apps

greptile-appsBot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Wires up the previously-dead /api/users/me/usage-logs route with a proper getUsageLogsContract, expands its source enum from 3 to 9 values, and adds a paginated "Credit usage" section to Billing settings (hidden for Enterprise plans). A shared formatCreditsLabel helper is extracted in conversion.ts so the new log rows, the existing breakdown, and the trace view all pluralize credits identically.

  • Route refactor (route.ts): adopts parseRequest/contract validation, a clean resolveStartDate helper, and strips category from the API response; dollar→credit conversion and summary aggregation are correct.
  • Infinite-query hook + key factory (usage-logs.ts, usage-log-keys.ts): key factory is in a standalone non-'use client' module per project convention; period is in the query key, pageParam/signal correctly exempt; keepPreviousData applies because the key varies.
  • UI section (credit-usage-section.tsx, search-params.ts): period filter is nuqs-backed (replace-history, clear-on-default); totalCredits from pages[0] is the correct full-period aggregate since getUserUsageLogs runs its summary query without the cursor constraint.

Confidence Score: 5/5

Safe to merge — the change is additive (new UI section + contract), the backend query logic was pre-existing, and the gating on !isEnterprise is straightforward.

All changed paths are additive; the route refactor is well-tested (5 targeted test cases), the contract expansion correctly mirrors the real DB enum, and the infinite-query wiring follows established project patterns without touching any shared billing state.

No files require special attention.

Important Files Changed

FilenameOverview
apps/sim/app/api/users/me/usage-logs/route.tsRewrites the handler using parseRequest + getUsageLogsContract; cleans up dollar→credit conversion and removes the try/catch in favour of withRouteHandler; no issues found.
apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/credit-usage-section/credit-usage-section.tsxNew paginated credit-usage UI; totalCredits correctly uses pages[0] which holds the full-period aggregate (backend runs a separate summary query); SOURCE_LABELS covers all 9 enum values; minor display inconsistency for sub-$0.005 entries (formatCreditsLabel has no <1 credit guard).
apps/sim/hooks/queries/usage-logs.tsInfinite-query hook with explicit staleTime, keepPreviousData for period transitions, and correct signal forwarding; key factory lives in the non-'use client' utils file per project rules.
apps/sim/hooks/queries/utils/usage-log-keys.tsStandalone key factory (no 'use client') following the folder-keys / table-keys pattern; source slot is present in the list key for future use.
apps/sim/lib/api/contracts/user.tsAdds usageLogSourceSchema (all 9 sources), usageLogPeriodSchema, response schema, and getUsageLogsContract; expands the stale 3-value source enum to cover all real sources.
apps/sim/lib/billing/credits/conversion.tsExtracts formatCreditsLabel as a shared building block; formatCreditCost now delegates to it, eliminating duplication; no regressions.
apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsxAdds CreditUsageSection below Invoices, gated on !subscription.isEnterprise; minimal, correct change.
apps/sim/app/api/users/me/usage-logs/route.test.tsNew test file covers auth, dollar→credit conversion, invalid period rejection, and period→startDate resolution; all five cases are solid.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as CreditUsageSection
participant nuqs as nuqs URL state
participant RQ as React Query
participant API as GET /api/users/me/usage-logs
participant DB as getUserUsageLogs
UI->>nuqs: read period default 30d
UI->>RQ: useUsageLogs period
RQ->>API: "GET ?period=30d&limit=25"
API->>DB: paginated logs query with cursor
API->>DB: summary aggregate query no cursor
DB-->>API: logs page plus full-period summary
API-->>RQ: logs summary.totalCredits pagination
RQ-->>UI: pages[0] totalCredits shown logs rendered
UI->>RQ: fetchNextPage cursor
RQ->>API: "GET ?period=30d&limit=25&cursor=id"
API->>DB: paginated logs cursor filtered
API->>DB: summary aggregate no cursor
DB-->>API: next page plus same full-period summary
API-->>RQ: additional logs appended
UI->>nuqs: user selects 7d
nuqs-->>UI: period equals 7d replace history
UI->>RQ: useUsageLogs period 7d new queryKey
RQ->>API: "GET ?period=7d&limit=25"
API-->>RQ: new page set
RQ-->>UI: fresh data replaces keepPreviousData placeholder
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI as CreditUsageSection
participant nuqs as nuqs URL state
participant RQ as React Query
participant API as GET /api/users/me/usage-logs
participant DB as getUserUsageLogs
UI->>nuqs: read period default 30d
UI->>RQ: useUsageLogs period
RQ->>API: "GET ?period=30d&limit=25"
API->>DB: paginated logs query with cursor
API->>DB: summary aggregate query no cursor
DB-->>API: logs page plus full-period summary
API-->>RQ: logs summary.totalCredits pagination
RQ-->>UI: pages[0] totalCredits shown logs rendered
UI->>RQ: fetchNextPage cursor
RQ->>API: "GET ?period=30d&limit=25&cursor=id"
API->>DB: paginated logs cursor filtered
API->>DB: summary aggregate no cursor
DB-->>API: next page plus same full-period summary
API-->>RQ: additional logs appended
UI->>nuqs: user selects 7d
nuqs-->>UI: period equals 7d replace history
UI->>RQ: useUsageLogs period 7d new queryKey
RQ->>API: "GET ?period=7d&limit=25"
API-->>RQ: new page set
RQ-->>UI: fresh data replaces keepPreviousData placeholder
Loading

Reviews (6): Last reviewed commit: "improvement(billing): move credit usage ..." | Re-trigger Greptile

Comment threadapps/sim/app/api/users/me/usage-logs/route.ts
Comment threadapps/sim/hooks/queries/usage-logs.ts Outdated
…dary
Greptile P2: usageLogKeys lived in the 'use client' usage-logs.ts hook
file — importing it from a server component (e.g. a future prefetch)
would resolve to a client-reference stub and crash at build/SSR, the
same class of bug that hit tables' key factory before. Extracted to
hooks/queries/utils/usage-log-keys.ts, matching table-keys.ts and
folder-keys.ts.
Also renamed a shadowed `source` map param in the route to `sourceKey`
for clarity.
@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 89fe7e7. Configure here.

The '1d' | '7d' | '30d' | 'all' union was hand-typed in three places
across usage-logs.ts and credit-usage-section.tsx. Extracted
usageLogPeriodSchema in the contract and derived UsageLogPeriod from
it, so the hook, the component, and the key factory all share one
definition instead of risking drift.
/cleanup pass (nuqs rule, react-query-best-practices, emcn review):
- period was a plain useState, but it's exactly the kind of shareable
list filter sim-url-state.md calls out for nuqs — migrated to
billingParsers/useQueryStates so the selection deep-links, survives
reload, and matches every sibling settings section (recently-deleted,
inbox, teammates). Derives its literal values from
usageLogPeriodSchema instead of a fourth copy of the same union.
- Removed an unjustified showSelectedCheck={false} on the period
ChipDropdown — the one other usage in the codebase is a one-shot
action menu with no persistent selection; this is a real filter and
should show the check like the default intends.
- Added placeholderData: keepPreviousData to useUsageLogs — period is
a variable query key, so without it switching periods flashed the
loading empty-state instead of smoothly transitioning.
Verified live: deep-link with ?period=7d pre-selects and loads
correctly, switching periods updates the URL, and the selection
survives a hard reload.
@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 73679b1. Configure here.

@waleedlatif1
waleedlatif1 merged commit ca2a52c into stagingJul 3, 2026
18 checks passed
@waleedlatif1
waleedlatif1 deleted the feat/credit-usage-log branch July 3, 2026 20:21
@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 73679b1. Configure here.

waleedlatif1 added a commit that referenced this pull request Jul 4, 2026
… CSV export (#5405)
* fix(billing): apportion per-row credit costs so they sum to the page total
Cursor Bugbot (medium): each row rounded its own dollar cost to
credits independently while the header total rounded the summed
dollars once — over enough rows those two roundings can visibly
disagree, the exact "line items don't add up to the total" class of
bug apportionCredits was already built to prevent (used by the trace
view / cost breakdown). Route now apportions each page's row credits
against that page's dollar sum instead of rounding rows independently.
Added a test with three sub-cent rows that would each independently
round to 0 credits (but sum to 1) to prove the reconciliation holds.
* fix(billing): dim stale credit usage rows while a new period loads
Cursor Bugbot (medium): keepPreviousData kept the prior period's rows
and total on screen while a newly selected period fetched, but the
dropdown label updated immediately — so during the transition the
displayed numbers were labeled under a period they didn't belong to.
Now reads isPlaceholderData (the standard TanStack Query signal for
"this data is a stale placeholder, not a fresh fetch for the current
key") and dims the list while it's true, matching the same flag
already used for this exact purpose in integration-skills-section.tsx.
* fix(billing): show "<1 credit" for rows apportioned to 0
Cursor Bugbot (low): with apportioned per-row credits, a row with a
real but sub-credit dollarCost can legitimately apportion to 0 credits
once a sibling row absorbs the shared rounding remainder — rendering a
flat "0 credits" reads as if nothing was charged, inconsistent with
formatCreditCost's "<1 credit" wording used elsewhere in billing.
Added dollarCost to the wire response (needed to distinguish a
genuinely free row from a rounded-to-zero one) and a small
formatRowCredits helper that only changes the label, not the
underlying creditCost number, so the page-total reconciliation from
the prior fix is unaffected.
* fix(audit-logs): fix broken Custom range picker, trim time-range presets
Custom range silently did nothing: the time-range trigger was a
ChipSelect (Radix DropdownMenu, modal by default), and selecting
"Custom range" opened the Calendar popover in the same tick the modal
menu began its close/focus-lock cleanup, trapping the popover
non-interactive. Swapped to ChipCombobox (Radix Popover, non-modal),
mirroring the already-working pattern in the main Logs page exactly.
Also trimmed the preset list from 11 to 8 entries (dropped Past 30
minutes/12 hours/14 days) so the menu fits without scrolling.
* feat(billing): dedicated Credit usage page with date-range filter and CSV export
Follow-up to #5391 per team feedback in Slack: move the credit usage
list out of the inline Billing section into its own page, redesign
rows to show source ("Chat", "Workflow: <name>") instead of a raw
model description + badge, and add real date-range filtering and
export.
- Billing settings now shows a compact glance (30-day total + a "View
usage logs" link) instead of the full inline list.
- New /settings/billing/credit-usage page (sibling of [section],
mirrors the secrets/[credentialId] detail-route pattern) with day
presets (Today/7d/30d/All time) plus a working Custom range picker
— the same ChipCombobox+Popover+Calendar wiring the audit-logs fix
in this branch uses, not the broken ChipSelect pattern.
- Rows show the humanized source label, or "Workflow: <name>" for
workflow-sourced events (new server-side workflow-name lookup,
batched per page). Dropped the redundant badge and raw model
description.
- CSV export of the currently-filtered logs via a new GET
.../usage-logs/export route (mode: 'text' contract, synchronous
single-response CSV — the dataset is a bounded per-user ledger, not
a workspace-wide export, so no async job queue needed). Query-filter
logic (date-range resolution, workflow-name lookup) is shared with
the list route via shared.ts rather than duplicated.
- period/startDate/endDate live in the URL via a co-located
search-params.ts; the list query keeps keepPreviousData +
isPlaceholderData dimming during filter transitions, matching the
behavior already shipped in #5391.
Verified live end-to-end: back link navigation, custom range picker
opens and applies, day presets, CSV export downloads and matches the
on-screen rows exactly (credits reconcile with the total), compact
Billing summary + link.
* refactor(billing): move workflow-name enrichment into getUserUsageLogs, dedup helpers
/simplify pass over the credit-usage-page branch (4 parallel review
angles: reuse, simplification, efficiency, altitude):
- getUserUsageLogs now LEFT JOINs workflow and returns workflowName
directly (matching lib/logs/list-logs.ts's established pattern),
eliminating the route-layer resolveWorkflowNames query that both the
list and export routes previously ran independently.
- Added includeSummary (default true) to getUserUsageLogs so the
export route's cursor loop can skip the cursor-independent
SUM/GROUP BY aggregate it never reads — that aggregate was being
recomputed on every page of a paginated export for no reason.
- Fixed an off-by-one in the export's pagination loop: `<=
MAX_EXPORT_ROWS` let it fetch one more full page past the cap only
to discard it; `< MAX_EXPORT_ROWS` with a shrinking per-page limit
never overshoots.
- Deduplicated the SOURCE_LABELS map (was defined identically in both
the page and the export route) into a shared, DB-free
source-labels.ts both can import.
- Export route now builds CSV rows via lib/table/export-format.ts's
toCsvRow/formatCsvValue instead of a hand-rolled escaper.
- Added formatApportionedCreditCost to conversion.ts so the page's row
rendering shares its zero/sub-credit wording with formatCreditCost
instead of re-deriving the same three-way branch.
- Replaced the generic requireStartDateForCustomPeriod<Schema> contract
helper (nontrivial generic bound for a single four-line refine used
at two call sites) with a plain shared error-options object.
- Removed the credit-usage page's dateRangeAppliedRef guard — a
controlled Radix Popover never re-invokes onOpenChange in response
to the parent's own setState call, so the guard was defending
against a re-entrant close that can't happen.
- Added a modal prop to ChipSelect (forwarded to the underlying
DropdownMenu, which already supported it) so a future call site that
hits the same "modal select traps a same-tick Popover" bug the
audit-logs Custom range fix worked around has a real fix available
instead of having to swap components again.
Re-verified live end-to-end after the refactor: workflow-name
resolution, credit reconciliation, and CSV export all still correct.
* fix(billing): drop Dollar cost from the CSV export, strip inline comments
We only surface credits to the user, not the underlying dollar figure
— "Dollar cost" was the one place the export literally displayed a
dollar amount (the rest of the codebase uses dollarCost purely as an
internal signal to distinguish a sub-credit charge from a genuinely
free event, never rendered as a "$" value).
* fix(billing): export honors partial custom date range, surfaces truncation
Greptile (P1) and Cursor Bugbot independently caught the same bug:
handleExport only forwarded startDate/endDate when BOTH were truthy,
but the list query and both API contracts treat endDate as optional
for a custom period (defaults to now). A user landing on a bookmarked
?period=custom&startDate=... URL would see populated rows and an
enabled Export button, then get a 400 on click since the export
omitted the required startDate too. Fixed by forwarding each date
independently, matching the list query's existing behavior.
Also addressed Greptile's other two findings:
- The export route now sets X-Export-Truncated so a 5,000-row-capped
download is visible to the user (a toast), not just a server log.
Reading that header meant switching the trigger from a plain anchor
navigation to fetch+blob — an anchor can't inspect the response
before the browser commits to the download.
- resolveDateRange now throws explicitly when a custom period is
missing startDate instead of silencing the null check with `as
string`, which would have produced a silent Invalid Date if ever
called without prior contract validation.
* fix(billing): remove the export's arbitrary row cap, fix a cursor pagination bug it exposed
A personal credit ledger doesn't have the same unbounded-growth problem
a workspace table does — capping the export at 5,000 rows just meant
long-tenured or high-usage accounts (exactly the ones most likely to
need a full export to reconcile a billing question) got silently
truncated. Replaced the cap with a 50,000-row circuit breaker that
should never fire in normal use (logged as an error, not a warning,
if it ever does) and bumped the page size from 500 to 1,000 to cut
round trips.
Removing the cap surfaced a real, pre-existing bug in
getUserUsageLogs's cursor pagination: a raw `sql` template embedded a
JS Date object directly as a bound parameter, which the postgres
driver can't serialize (unlike drizzle's typed gte/lte operators,
which already handle Date correctly elsewhere in the same function).
It only ever manifested past the first page, which nothing before
this export route's tight multi-page loop reliably exercised.
Replaced the raw sql template with drizzle's typed lt/eq/or/and
operators, matching the pattern already proven correct in this file.
Verified live: seeded 6,000 rows (past the old cap) and confirmed the
export downloads all of them in one request with credits reconciling
exactly against the total.
* perf(billing): skip the redundant cursor lookup when the caller already has it
The export loop holds the previous page's rows in memory, so its next
cursor's createdAt is already known — getUserUsageLogs was still
re-resolving it via an extra DB round trip every page regardless.
Added an optional cursorCreatedAt to skip that lookup when provided;
the list route's existing callers are unaffected since they don't
pass it. Verified live: zero cursor-lookup queries fired across a
3,500-row / 4-page export that previously issued one per page.
* fix(billing): apportion credits over the whole filtered set, not per page/call
Cursor Bugbot caught this: the list route apportioned each page's
rows against only that page's own dollar total, while the export
apportioned every exported row against the complete set's total.
Since apportionment depends on the full set, the same log could show
a different creditCost between the list and the export, or even
between two pages of the same "Load more" list — and the sum of every
loaded row could visibly drift from the "Total" header shown above
them once more than one page had loaded.
Extracted getUsageCreditsByLogId — a single, shared, whole-filter
apportionment lookup both routes now call instead of each computing
their own subset locally. The list route calls it once per page
request (same cost profile as the summary aggregate it already pays
for every page); the export calls it once before its pagination loop,
not per page, keeping the round-trip count this session's earlier fix
already reduced. Also extracted the condition-building shared by the
main query, the summary aggregate, and this new lookup into one
buildUsageLogConditions helper, removing a third copy of that logic.
Verified live: summed every row across 4 "Load more" pages and
confirmed it now matches the reported total exactly (previously could
drift), and confirmed the list and the export produce byte-identical
credit sequences for the same rows.
* fix(billing): make custom-range startDate/endDate nullable, not '' defaulted
startDate/endDate had no sensible static default (they're only ever
meaningful mid-custom-range), so defaulting them to '' via
.withDefault('') meant switching back to a preset left the URL
carrying startDate=&endDate= instead of dropping the params entirely.
Made them nullable (no .withDefault) instead, matching the identical
fields in the main Logs page's own search-params.ts. Verified live —
switching from a custom range back to a preset now clears both params
from the URL completely.
* feat(audit-logs): add CSV export, matching the Credit usage page pattern
Adds an Export chip to the top-right of the Audit Logs page (via
SettingsPanel's actions slot — the same header mechanism the Credit
usage page uses), downloading every audit log matching the current
search/type/date filters as CSV.
- New GET /api/audit-logs/export route: same session + enterprise
admin/owner gating as the existing list route, reuses the shared
buildFilterConditions/buildOrgScopeCondition/queryAuditLogs helpers
(already using drizzle's typed operators for cursor pagination, not
the raw-sql-with-embedded-Date pattern fixed elsewhere this
session), and the same fetch+blob+X-Export-Truncated pattern the
Credit usage export already established.
- Capped at 10,000 rows (not the 50,000 used for a personal credit
ledger) — an org's audit trail can genuinely grow much larger than
one user's usage history, so this is sized for "a reasonable audit
review window," with truncation surfaced via a toast rather than
silently dropped.
- Bumped the API-validation-contract audit's route-count baseline for
the new route.
Verified live against a real enterprise org: switched to "All time,"
exported ~750 real audit log rows, confirmed formatting (quoted
descriptions, actor email fallback) and correct filter scoping.
* fix(billing): skip wasted credit apportionment on the summary fetch, block export during stale data
Cursor Bugbot caught two real issues:
1. The compact Billing summary glance (limit=1) only ever reads
summary.totalCredits, but the list route unconditionally ran
getUsageCreditsByLogId's whole-filter scan on every call including
this one — pure wasted work for a caller that discards the result.
Added an includeCredits query flag (default true, using the shared
booleanQueryFlagSchema) so useUsageSummary can opt out; the main
paginated view keeps it on since it genuinely needs per-row values.
2. Export stayed enabled while useUsageLogs held stale rows via
keepPreviousData mid-filter-transition — a user could change the
period/range and click Export before the new data loaded, exporting
against the new filter while the table still showed the old one.
Export is now also disabled while isPlaceholderData is true.
* fix(billing): deterministic apportionment order, block audit export during stale data
Cursor Bugbot caught two more real issues on the latest push:
1. Same stale-export bug as the earlier Credit usage fix, this time in
Audit Logs: Export stayed enabled while useAuditLogs held prior
rows via keepPreviousData, so it could export against a
just-changed filter while the table still showed the old one. Now
also disabled while isPlaceholderData is true.
2. getUsageCreditsByLogId had no ORDER BY before apportionCredits's
largest-remainder tie-break, so which row absorbed a tied
remainder credit depended on undefined Postgres row order — the
same event's displayed credit could flip between calls (list vs.
export, or even two successive requests). Added the same
`orderBy(desc(createdAt), desc(id))` the main list query already
uses, making the tie-break reproducible.
Verified live: 3 identically-costed rows produced the same tie-break
winner across 3 repeated requests (previously order-dependent).
* fix(billing): distinguish a failed summary fetch from zero usage
The compact Billing glance only branched on isPending, so once
useUsageSummary settled into an error state, totalCredits stayed
undefined and formatCreditsLabel(0) rendered "0 credits" — visually
identical to genuinely having no usage this period. Now shows the
same neutral "—" placeholder for isError as it already does for
isPending.
* fix(billing): gate the credit-usage page server-side for enterprise accounts
Greptile (P1) caught this: hiding the "View usage logs" link on the
Billing page for enterprise accounts doesn't stop direct navigation —
anyone with the URL (bookmark, shared link, browser history) could
still reach the full page and its CSV export, which enterprise
accounts were never supposed to see at all (billing is managed
out-of-band for them).
Added a server-side check in page.tsx before anything renders:
resolve the session, look up the highest-priority subscription, and
redirect to /settings/billing if it's enterprise — matching how
getHighestPrioritySubscription is already used elsewhere for
server-side plan checks, rather than relying on a client-side-only
conditional the way the Billing page's inline section does.
Also fixes loading.tsx: it was a Server Component (no directive)
passing a raw icon function reference into the client Chip component,
which fails RSC serialization. Added 'use client'.
Verified live in a real browser against both an enterprise account
(redirects to Billing before any credit-usage content renders) and a
non-enterprise account (reaches the page normally).
Sign up for freeto 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.

1 participant

@waleedlatif1