From 8955fd2758fb1a06dbfcf20c9c48c4d83ec19b70 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:23:40 +0000 Subject: [PATCH] fix(cli): Stop dashboard revisions pagination from skipping rows `sentry dashboard revisions --limit 150` fetched two full pages of 100, trimmed the last 50 rows, and still stored the server cursor, so `-c next` silently skipped those 50 revisions. Later pages now request only the remaining budget, like the shared paginate() helper, and the cursor is dropped if an endpoint still returns more than requested. Ported from getsentry/cli#1118. Co-Authored-By: BYK --- .../cli/src/commands/dashboard/revisions.ts | 14 ++++-- .../test/commands/dashboard/revisions.test.ts | 49 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/dashboard/revisions.ts b/packages/cli/src/commands/dashboard/revisions.ts index 0a14c3f91..7cc551bac 100644 --- a/packages/cli/src/commands/dashboard/revisions.ts +++ b/packages/cli/src/commands/dashboard/revisions.ts @@ -169,7 +169,6 @@ export const revisionsCommand = buildCommand({ contextKey, ); - const perPage = Math.min(flags.limit, API_MAX_PER_PAGE); const results: DashboardRevision[] = []; let cursor = rawCursor; let nextCursor: string | undefined; @@ -187,7 +186,12 @@ export const revisionsCommand = buildCommand({ for (let page = 0; page < MAX_PAGINATION_PAGES; page++) { const { data, nextCursor: nc } = await listDashboardRevisionsPaginated(orgSlug, dashboardId, { - perPage, + // Ask only for the remaining budget so the last page's + // cursor points at the first row we did not return. + perPage: Math.min( + flags.limit - results.length, + API_MAX_PER_PAGE, + ), cursor, }); results.push(...data); @@ -202,8 +206,10 @@ export const revisionsCommand = buildCommand({ ); const trimmed = results.slice(0, flags.limit); - const hasMore = results.length > flags.limit || !!nextCursor; - const cursorToStore = hasMore ? nextCursor : undefined; + // A page larger than requested would make the server cursor skip the + // trimmed rows, so drop it (same as autoPaginate). + const cursorToStore = results.length > flags.limit ? undefined : nextCursor; + const hasMore = cursorToStore !== undefined; advancePaginationState( PAGINATION_KEY, diff --git a/packages/cli/test/commands/dashboard/revisions.test.ts b/packages/cli/test/commands/dashboard/revisions.test.ts index ec2f9e260..1d6b3a442 100644 --- a/packages/cli/test/commands/dashboard/revisions.test.ts +++ b/packages/cli/test/commands/dashboard/revisions.test.ts @@ -294,6 +294,55 @@ describe("dashboard revisions command", () => { expect(parsed.hasMore).toBe(false); }); + test("auto-pagination: later pages request only the remaining budget", async () => { + listDashboardRevisionsPaginatedSpy + .mockResolvedValueOnce({ + data: [REVISION_A, REVISION_B], + nextCursor: "cursor-page-2", + }) + .mockResolvedValueOnce({ + data: [REVISION_C], + nextCursor: "cursor-page-3", + }); + + const { context, stdoutWrite } = createMockContext(); + const func = await revisionsCommand.loader(); + await func.call(context, defaultFlags({ json: true, limit: 3 }), "123"); + + expect(listDashboardRevisionsPaginatedSpy).toHaveBeenLastCalledWith( + "test-org", + "123", + { perPage: 1, cursor: "cursor-page-2" }, + ); + const parsed = JSON.parse(stdoutWrite.mock.calls.map((c) => c[0]).join("")); + expect(parsed.data).toHaveLength(3); + expect(parsed.hasMore).toBe(true); + expect(parsed.nextCursor).toBe("cursor-page-3"); + }); + + test("auto-pagination: drops the cursor when a page overshoots the limit", async () => { + // An endpoint that ignores per_page returns more rows than asked for; its + // cursor would skip the trimmed rows on `-c next`. + listDashboardRevisionsPaginatedSpy + .mockResolvedValueOnce({ + data: [REVISION_A, REVISION_B], + nextCursor: "cursor-page-2", + }) + .mockResolvedValueOnce({ + data: [REVISION_C, REVISION_A], + nextCursor: "cursor-page-3", + }); + + const { context, stdoutWrite } = createMockContext(); + const func = await revisionsCommand.loader(); + await func.call(context, defaultFlags({ json: true, limit: 3 }), "123"); + + const parsed = JSON.parse(stdoutWrite.mock.calls.map((c) => c[0]).join("")); + expect(parsed.data).toHaveLength(3); + expect(parsed.hasMore).toBe(false); + expect(parsed.nextCursor).toBeUndefined(); + }); + // ------------------------------------------------------------------------- // Org/dashboard argument parsing // -------------------------------------------------------------------------