Skip to content

Commit 7f97f31

Browse files
committed
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.
1 parent d1dcf6c commit 7f97f31

3 files changed

Lines changed: 31 additions & 13 deletions

File tree

apps/sim/app/api/users/me/usage-logs/export/route.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ describe('GET /api/users/me/usage-logs/export', () => {
173173
expect(mockGetUserUsageLogs).toHaveBeenNthCalledWith(
174174
2,
175175
'user-1',
176-
expect.objectContaining({ cursor: 'log-1' })
176+
expect.objectContaining({
177+
cursor: 'log-1',
178+
cursorCreatedAt: new Date('2026-07-01T00:00:00.000Z'),
179+
})
177180
)
178181
expect(csv.split('\n')).toHaveLength(3)
179182
})

apps/sim/app/api/users/me/usage-logs/export/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
4343

4444
const rows: Awaited<ReturnType<typeof getUserUsageLogs>>['logs'] = []
4545
let cursor: string | undefined
46+
let cursorCreatedAt: Date | undefined
4647
let truncated = false
4748
while (rows.length < EXPORT_SAFETY_CAP) {
4849
const page = await getUserUsageLogs(auth.userId, {
@@ -52,12 +53,15 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
5253
endDate: dateRange.endDate,
5354
limit: Math.min(EXPORT_PAGE_SIZE, EXPORT_SAFETY_CAP - rows.length),
5455
cursor,
56+
cursorCreatedAt,
5557
includeSummary: false,
5658
})
5759
rows.push(...page.logs)
5860
if (!page.pagination.hasMore) break
5961
truncated = rows.length >= EXPORT_SAFETY_CAP
6062
cursor = page.pagination.nextCursor
63+
const lastRow = page.logs[page.logs.length - 1]
64+
cursorCreatedAt = lastRow ? new Date(lastRow.createdAt) : undefined
6165
}
6266

6367
if (truncated) {

apps/sim/lib/billing/core/usage-log.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,12 @@ export interface GetUsageLogsOptions {
533533
limit?: number
534534
/** Cursor for pagination (log ID) */
535535
cursor?: string
536+
/**
537+
* The cursor row's `createdAt`, when the caller already has it (e.g. a
538+
* multi-page export loop holding the previous page's rows in memory).
539+
* Skips the row lookup that would otherwise resolve it from `cursor`.
540+
*/
541+
cursorCreatedAt?: Date
536542
/**
537543
* Whether to compute the full-filter `summary` aggregate (default `true`).
538544
* A cursor-paginated caller collecting every page (e.g. a CSV export) only
@@ -591,6 +597,7 @@ export async function getUserUsageLogs(
591597
endDate,
592598
limit = 50,
593599
cursor,
600+
cursorCreatedAt,
594601
includeSummary = true,
595602
} = options
596603

@@ -614,20 +621,24 @@ export async function getUserUsageLogs(
614621
}
615622

616623
if (cursor) {
617-
// Cursor resolution stays on the primary: the page itself reads a
618-
// load-balanced replica, and a laggier sibling replica missing the cursor
619-
// row would silently restart pagination from page 1.
620-
const cursorLog = await db
621-
.select({ createdAt: usageLog.createdAt })
622-
.from(usageLog)
623-
.where(eq(usageLog.id, cursor))
624-
.limit(1)
624+
let resolvedCursorCreatedAt = cursorCreatedAt
625+
626+
if (!resolvedCursorCreatedAt) {
627+
// Cursor resolution stays on the primary: the page itself reads a
628+
// load-balanced replica, and a laggier sibling replica missing the
629+
// cursor row would silently restart pagination from page 1.
630+
const cursorLog = await db
631+
.select({ createdAt: usageLog.createdAt })
632+
.from(usageLog)
633+
.where(eq(usageLog.id, cursor))
634+
.limit(1)
635+
resolvedCursorCreatedAt = cursorLog[0]?.createdAt
636+
}
625637

626-
if (cursorLog.length > 0) {
627-
const cursorCreatedAt = cursorLog[0].createdAt
638+
if (resolvedCursorCreatedAt) {
628639
const cursorCondition = or(
629-
lt(usageLog.createdAt, cursorCreatedAt),
630-
and(eq(usageLog.createdAt, cursorCreatedAt), lt(usageLog.id, cursor))
640+
lt(usageLog.createdAt, resolvedCursorCreatedAt),
641+
and(eq(usageLog.createdAt, resolvedCursorCreatedAt), lt(usageLog.id, cursor))
631642
)
632643
if (cursorCondition) conditions.push(cursorCondition)
633644
}

0 commit comments

Comments
 (0)