From 1f5c6c0abb1a6b93754640d5a1c09431cc69b7b1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 24 Aug 2026 12:07:40 +0000 Subject: [PATCH 1/2] fix(conversations): cap per_page at API_MAX_PER_PAGE and add auto-pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit listConversations passed the user's limit directly as per_page without capping at API_MAX_PER_PAGE (100). The Sentry API silently caps per_page at 100, so users requesting more than 100 conversations got silently truncated results. Additionally, the function only fetched a single page, unlike other list APIs (listTransactions, listSpans, listReplays) which use autoPaginate(). Cap per_page at Math.min(limit, API_MAX_PER_PAGE) and use the autoPaginate helper for multi-page fetches, matching the pattern used by all other list APIs in the codebase. Co-authored-by: Miguel Betegón --- packages/cli/src/lib/api/conversations.ts | 58 ++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/lib/api/conversations.ts b/packages/cli/src/lib/api/conversations.ts index 1dbafa69b..e18c4e985 100644 --- a/packages/cli/src/lib/api/conversations.ts +++ b/packages/cli/src/lib/api/conversations.ts @@ -26,7 +26,9 @@ import { logger } from "../logger.js"; import { resolveOrgRegion } from "../region.js"; import { + API_MAX_PER_PAGE, apiRequestToRegion, + autoPaginate, MAX_PAGINATION_PAGES, type PaginatedResponse, parseLinkHeader, @@ -34,22 +36,27 @@ import { const log = logger.withTag("api.conversations"); -export async function listConversations( +/** + * Fetch a single page of conversations from the AI-conversations endpoint. + * + * Internal helper used by {@link listConversations} for both single-page and + * multi-page (auto-paginating) fetches. + */ +async function fetchConversationsPage( + regionUrl: string, orgSlug: string, options: { query?: string; - limit?: number; cursor?: string; statsPeriod?: string; start?: string; end?: string; project?: string; - } = {} + }, + perPage: number ): Promise> { - const regionUrl = await resolveOrgRegion(orgSlug); - const params: Record = { - per_page: String(options.limit ?? 10), + per_page: String(perPage), }; if (options.statsPeriod) { params.statsPeriod = options.statsPeriod; @@ -81,6 +88,45 @@ export async function listConversations( return { data, nextCursor }; } +/** + * List AI conversations for an organization. + * + * When `limit` exceeds {@link API_MAX_PER_PAGE}, transparently fetches multiple + * pages using cursor-based pagination (bounded by {@link MAX_PAGINATION_PAGES}). + * + * @param orgSlug - Organization slug + * @param options - Query options (query, limit, cursor, statsPeriod, etc.) + * @returns Paginated response with conversation items and optional next cursor + */ +export async function listConversations( + orgSlug: string, + options: { + query?: string; + limit?: number; + cursor?: string; + statsPeriod?: string; + start?: string; + end?: string; + project?: string; + } = {} +): Promise> { + const regionUrl = await resolveOrgRegion(orgSlug); + const limit = options.limit ?? 10; + const perPage = Math.min(limit, API_MAX_PER_PAGE); + + return autoPaginate( + (cursor) => + fetchConversationsPage( + regionUrl, + orgSlug, + { ...options, cursor }, + perPage + ), + limit, + options.cursor + ); +} + export async function getConversationSpans( orgSlug: string, conversationId: string, From 84aacfd09941cbebdbb1ba1a15c96ad20e889cec Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Mon, 24 Aug 2026 15:25:11 +0000 Subject: [PATCH 2/2] test(conversations): add per_page cap + multi-page accumulation coverage --- .../cli/test/lib/api/conversations.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/cli/test/lib/api/conversations.test.ts b/packages/cli/test/lib/api/conversations.test.ts index 8b7449768..ee68f88c7 100644 --- a/packages/cli/test/lib/api/conversations.test.ts +++ b/packages/cli/test/lib/api/conversations.test.ts @@ -236,6 +236,46 @@ describe("listConversations", () => { expect(result.data).toHaveLength(1); expect(result.data[0].conversationId).toBe("conv-abc"); }); + + test("caps per_page at API_MAX_PER_PAGE when limit exceeds it", async () => { + const { getCapturedUrl } = mockOk([]); + + await listConversations(ORG, { limit: 200 }); + + expect(getCapturedUrl()).toContain("per_page=100"); + expect(getCapturedUrl()).not.toContain("per_page=200"); + }); + + test("accumulates results across pages when limit > API_MAX_PER_PAGE", async () => { + const item = (id: string) => ({ + conversationId: id, + flow: [], + errors: 0, + llmCalls: 0, + toolCalls: 0, + totalTokens: 0, + totalCost: 0, + startTimestamp: 1_716_500_000, + endTimestamp: 1_716_500_060, + traceCount: 0, + traceIds: [], + firstInput: "", + lastOutput: "", + toolNames: [], + toolErrors: 0, + }); + + const { getCapturedUrls } = mockSequential([ + { body: [item("c1")], headers: linkHeader("cursor1") }, + { body: [item("c2")], headers: linkHeader("cursor2", "false") }, + ]); + + const result = await listConversations(ORG, { limit: 200 }); + + expect(result.data).toHaveLength(2); + expect(result.data.map((c) => c.conversationId)).toEqual(["c1", "c2"]); + expect(getCapturedUrls()[0]).toContain("per_page=100"); + }); }); // ============================================================================