From 2efbf2f06aeb689783e59fcaae40fe78881ff3f3 Mon Sep 17 00:00:00 2001 From: Sentry Bot Date: Mon, 4 May 2026 23:47:39 +0000 Subject: [PATCH 1/4] feat: update @sentry/api to 0.133.0 and adopt pagination improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update @sentry/api from 0.113.0 to 0.133.0, which includes pagination wrappers, prevCursor support, and widened query types (cursor and per_page now declared on paginated endpoints). Changes: - Bump @sentry/api to ^0.133.0 in devDependencies - Add prevCursor to PaginatedResponse type and unwrapPaginatedResult - Replace manual pagination loops with autoPaginate in listProjects() and listAllRepositories(), eliminating ~40 lines of cursor management - Narrow 5 unsafe `as { cursor?: string }` casts to typed alternatives now that cursor/per_page are in the SDK query types - Remove unnecessary multi-line result casts in issues, events, teams - Remove stale comments about per_page not being in the OpenAPI spec The CLI keeps its own unwrapPaginatedResult (not the SDK's) because the CLI's error pipeline depends on ApiError/AuthError types that the SDK's unwrapResult does not preserve. traces.ts, discover.ts, and dashboards.ts widget queries are left on raw apiRequestToRegion — they use Zod validation schemas and the error-type incompatibility makes migration to SDK wrappers non-trivial. These will be addressed in a follow-up. --- bun.lock | 4 +-- package.json | 2 +- src/lib/api/events.ts | 4 +-- src/lib/api/infrastructure.ts | 15 ++++++++-- src/lib/api/issues.ts | 4 +-- src/lib/api/projects.ts | 55 ++++++++++++----------------------- src/lib/api/releases.ts | 16 +++++++--- src/lib/api/repositories.ts | 39 +++++++++---------------- src/lib/api/teams.ts | 8 ++--- 9 files changed, 64 insertions(+), 83 deletions(-) diff --git a/bun.lock b/bun.lock index ead0d2419..88f617c34 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "@biomejs/biome": "2.3.8", "@clack/prompts": "^0.11.0", "@mastra/client-js": "^1.4.0", - "@sentry/api": "^0.113.0", + "@sentry/api": "^0.133.0", "@sentry/node-core": "10.50.0", "@sentry/sqlish": "^1.0.0", "@stricli/auto-complete": "^1.2.4", @@ -175,7 +175,7 @@ "@peggyjs/from-mem": ["@peggyjs/from-mem@3.1.3", "", { "dependencies": { "semver": "7.7.4" } }, "sha512-LLlgtfXIaeYXoOYovOI0spLM8ZXaqkAlmcRRrLzHJzLMqkU6Sw0R4KMoCoHx1PjaP815pSCBlS+BN6aD8t1Jgg=="], - "@sentry/api": ["@sentry/api@0.113.0", "", {}, "sha512-28W0Oykb/O+6kH8F+OEd8070N4z7ctawlyUtEvnNZNlaLviDC9Is1X/0JiK2Xb9y2ZNbkWf+/H1y5hXr0WTIOw=="], + "@sentry/api": ["@sentry/api@0.133.0", "", {}, "sha512-flfRUm9T9xgyNEWQqCiNv8wX4QlqOt63tM8dRMbeo26zD4+xEaL4KiaEnPC/W5rXCKg/03FBJysw7rEIuhiedQ=="], "@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], diff --git a/package.json b/package.json index acbc28168..3fef5672d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@biomejs/biome": "2.3.8", "@clack/prompts": "^0.11.0", "@mastra/client-js": "^1.4.0", - "@sentry/api": "^0.113.0", + "@sentry/api": "^0.133.0", "@sentry/node-core": "10.50.0", "@sentry/sqlish": "^1.0.0", "@stricli/auto-complete": "^1.2.4", diff --git a/src/lib/api/events.ts b/src/lib/api/events.ts index adec3f022..22e2f5c91 100644 --- a/src/lib/api/events.ts +++ b/src/lib/api/events.ts @@ -237,9 +237,7 @@ export async function listIssueEvents( }); const paginated = unwrapPaginatedResult( - result as - | { data: IssueEvent[]; error: undefined } - | { data: undefined; error: unknown }, + result, "Failed to list issue events" ); diff --git a/src/lib/api/infrastructure.ts b/src/lib/api/infrastructure.ts index b8407598a..ec809f4cf 100644 --- a/src/lib/api/infrastructure.ts +++ b/src/lib/api/infrastructure.ts @@ -188,8 +188,17 @@ export function unwrapPaginatedResult( ): PaginatedResponse { const response = (result as { response?: Response }).response; const data = unwrapResult(result, context); - const { nextCursor } = parseLinkHeader(response?.headers.get("link") ?? null); - return { data, nextCursor }; + const { nextCursor, prevCursor } = parseLinkHeader( + response?.headers.get("link") ?? null + ); + const out: PaginatedResponse = { data }; + if (nextCursor !== undefined) { + out.nextCursor = nextCursor; + } + if (prevCursor !== undefined) { + out.prevCursor = prevCursor; + } + return out; } /** @@ -270,6 +279,8 @@ export type PaginatedResponse = { data: T; /** Cursor for fetching the next page (undefined if no more pages) */ nextCursor?: string; + /** Cursor for the previous page (undefined on the first page) */ + prevCursor?: string; }; /** diff --git a/src/lib/api/issues.ts b/src/lib/api/issues.ts index c235cff67..4cb25da01 100644 --- a/src/lib/api/issues.ts +++ b/src/lib/api/issues.ts @@ -162,9 +162,7 @@ export async function listIssuesPaginated( }); return unwrapPaginatedResult( - result as - | { data: SentryIssue[]; error: undefined } - | { data: undefined; error: unknown }, + result as { data: SentryIssue[]; error: undefined } | { data: undefined; error: unknown }, "Failed to list issues" ); } diff --git a/src/lib/api/projects.ts b/src/lib/api/projects.ts index 9bae30aac..0489d8bc8 100644 --- a/src/lib/api/projects.ts +++ b/src/lib/api/projects.ts @@ -27,7 +27,6 @@ import { } from "../db/project-cache.js"; import { getCachedOrganizations } from "../db/regions.js"; import { type AuthGuardSuccess, withAuthGuard } from "../errors.js"; -import { logger } from "../logger.js"; import { getApiBaseUrl } from "../sentry-client.js"; import { buildProjectUrl } from "../sentry-urls.js"; import { isAllDigits } from "../utils.js"; @@ -35,8 +34,8 @@ import { isAllDigits } from "../utils.js"; import { API_MAX_PER_PAGE, apiRequestToRegion, + autoPaginate, getOrgSdkConfig, - MAX_PAGINATION_PAGES, ORG_FANOUT_CONCURRENCY, type PaginatedResponse, unwrapPaginatedResult, @@ -54,38 +53,24 @@ import { getUserRegions, listOrganizations } from "./organizations.js"; */ export async function listProjects(orgSlug: string): Promise { const config = await getOrgSdkConfig(orgSlug); - const allResults: SentryProject[] = []; - let cursor: string | undefined; - - for (let page = 0; page < MAX_PAGINATION_PAGES; page++) { - const result = await listAnOrganization_sProjects({ - ...config, - path: { organization_id_or_slug: orgSlug }, - // per_page is supported by Sentry's pagination framework at runtime - // but not yet in the OpenAPI spec - query: { cursor, per_page: API_MAX_PER_PAGE } as { cursor?: string }, - }); - - const { data, nextCursor } = unwrapPaginatedResult( - result as - | { data: SentryProject[]; error: undefined } - | { data: undefined; error: unknown }, - "Failed to list projects" - ); - allResults.push(...data); - - if (!nextCursor) { - break; - } - cursor = nextCursor; - if (page === MAX_PAGINATION_PAGES - 1) { - logger.warn( - `Pagination limit reached (${MAX_PAGINATION_PAGES} pages, ${allResults.length} items). ` + - "Results may be incomplete for this organization." + const { data: allResults } = await autoPaginate( + async (cursor) => { + const result = await listAnOrganization_sProjects({ + ...config, + path: { organization_id_or_slug: orgSlug }, + query: { cursor, per_page: API_MAX_PER_PAGE } as { + cursor?: string; + per_page?: number; + }, + }); + return unwrapPaginatedResult( + result as { data: SentryProject[]; error: undefined } | { data: undefined; error: unknown }, + "Failed to list projects" ); - } - } + }, + Number.MAX_SAFE_INTEGER + ); // Populate project cache for shell completions (best-effort). // Mirrors how listOrganizations() calls setOrgRegions(). @@ -121,13 +106,11 @@ export async function listProjectsPaginated( query: { cursor: options.cursor, per_page: options.perPage ?? API_MAX_PER_PAGE, - } as { cursor?: string }, + } as { cursor?: string; per_page?: number }, }); return unwrapPaginatedResult( - result as - | { data: SentryProject[]; error: undefined } - | { data: undefined; error: unknown }, + result as { data: SentryProject[]; error: undefined } | { data: undefined; error: unknown }, "Failed to list projects" ); } diff --git a/src/lib/api/releases.ts b/src/lib/api/releases.ts index 0a7f737b5..725e1c28a 100644 --- a/src/lib/api/releases.ts +++ b/src/lib/api/releases.ts @@ -84,13 +84,21 @@ export async function listReleasesPaginated( environment: options.environment, statsPeriod: options.statsPeriod, status: options.status, - } as { cursor?: string }, + } as { + cursor?: string; + per_page?: number; + query?: string; + sort?: string; + health?: number; + project?: number[]; + environment?: string[]; + statsPeriod?: string; + status?: string; + }, }); return unwrapPaginatedResult( - result as - | { data: SentryRelease[]; error: undefined } - | { data: undefined; error: unknown }, + result as { data: SentryRelease[]; error: undefined } | { data: undefined; error: unknown }, "Failed to list releases" ); } diff --git a/src/lib/api/repositories.ts b/src/lib/api/repositories.ts index bbb0ffcbf..723b8345e 100644 --- a/src/lib/api/repositories.ts +++ b/src/lib/api/repositories.ts @@ -12,8 +12,8 @@ import { logger } from "../logger.js"; import { API_MAX_PER_PAGE, + autoPaginate, getOrgSdkConfig, - MAX_PAGINATION_PAGES, type PaginatedResponse, unwrapPaginatedResult, unwrapResult, @@ -59,18 +59,14 @@ export async function listRepositoriesPaginated( const result = await listAnOrganization_sRepositories({ ...config, path: { organization_id_or_slug: orgSlug }, - // per_page is supported by Sentry's pagination framework at runtime - // but not yet in the OpenAPI spec query: { cursor: options.cursor, per_page: options.perPage ?? 25, - } as { cursor?: string }, + } as { cursor?: string; per_page?: number }, }); return unwrapPaginatedResult( - result as - | { data: SentryRepository[]; error: undefined } - | { data: undefined; error: unknown }, + result as { data: SentryRepository[]; error: undefined } | { data: undefined; error: unknown }, "Failed to list repositories" ); } @@ -79,8 +75,8 @@ export async function listRepositoriesPaginated( * List **all** repositories in an organization by walking every page. * * Used by the offline repo cache and anywhere else we need the complete - * set (not just the first page). Stops at {@link MAX_PAGINATION_PAGES} - * as a safety net for pathological cases. + * set (not just the first page). Bounded by `autoPaginate`'s + * {@link MAX_PAGINATION_PAGES} safety limit. * * @param orgSlug - Organization slug * @returns All Sentry-registered repositories across all pages @@ -88,24 +84,15 @@ export async function listRepositoriesPaginated( export async function listAllRepositories( orgSlug: string ): Promise { - const all: SentryRepository[] = []; - let cursor: string | undefined; - for (let page = 0; page < MAX_PAGINATION_PAGES; page++) { - const { data, nextCursor } = await listRepositoriesPaginated(orgSlug, { - cursor, - perPage: API_MAX_PER_PAGE, - }); - all.push(...data); - if (!nextCursor) { - return all; - } - cursor = nextCursor; - } - log.warn( - `Stopped paginating repositories for '${orgSlug}' after ${MAX_PAGINATION_PAGES} pages — ` + - "some repos may be missing from the cache." + const { data } = await autoPaginate( + (cursor) => + listRepositoriesPaginated(orgSlug, { + cursor, + perPage: API_MAX_PER_PAGE, + }), + Number.MAX_SAFE_INTEGER ); - return all; + return data; } /** diff --git a/src/lib/api/teams.ts b/src/lib/api/teams.ts index 20181d903..f1a5cf311 100644 --- a/src/lib/api/teams.ts +++ b/src/lib/api/teams.ts @@ -57,18 +57,14 @@ export async function listTeamsPaginated( const result = await listAnOrganization_sTeams({ ...config, path: { organization_id_or_slug: orgSlug }, - // per_page is supported by Sentry's pagination framework at runtime - // but not yet in the OpenAPI spec query: { cursor: options.cursor, per_page: options.perPage ?? 25, - } as { cursor?: string }, + } as { cursor?: string; per_page?: number }, }); return unwrapPaginatedResult( - result as - | { data: SentryTeam[]; error: undefined } - | { data: undefined; error: unknown }, + result, "Failed to list teams" ); } From 211bf6920731874b5a814a4888e2f8705c05f1bf Mon Sep 17 00:00:00 2001 From: Sentry Bot Date: Tue, 5 May 2026 00:02:25 +0000 Subject: [PATCH 2/4] style: apply Biome formatting to match project conventions --- src/lib/api/issues.ts | 4 +++- src/lib/api/projects.ts | 37 +++++++++++++++++++------------------ src/lib/api/releases.ts | 4 +++- src/lib/api/repositories.ts | 4 +++- src/lib/api/teams.ts | 5 +---- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/lib/api/issues.ts b/src/lib/api/issues.ts index 4cb25da01..c235cff67 100644 --- a/src/lib/api/issues.ts +++ b/src/lib/api/issues.ts @@ -162,7 +162,9 @@ export async function listIssuesPaginated( }); return unwrapPaginatedResult( - result as { data: SentryIssue[]; error: undefined } | { data: undefined; error: unknown }, + result as + | { data: SentryIssue[]; error: undefined } + | { data: undefined; error: unknown }, "Failed to list issues" ); } diff --git a/src/lib/api/projects.ts b/src/lib/api/projects.ts index 0489d8bc8..86c4b23c5 100644 --- a/src/lib/api/projects.ts +++ b/src/lib/api/projects.ts @@ -54,23 +54,22 @@ import { getUserRegions, listOrganizations } from "./organizations.js"; export async function listProjects(orgSlug: string): Promise { const config = await getOrgSdkConfig(orgSlug); - const { data: allResults } = await autoPaginate( - async (cursor) => { - const result = await listAnOrganization_sProjects({ - ...config, - path: { organization_id_or_slug: orgSlug }, - query: { cursor, per_page: API_MAX_PER_PAGE } as { - cursor?: string; - per_page?: number; - }, - }); - return unwrapPaginatedResult( - result as { data: SentryProject[]; error: undefined } | { data: undefined; error: unknown }, - "Failed to list projects" - ); - }, - Number.MAX_SAFE_INTEGER - ); + const { data: allResults } = await autoPaginate(async (cursor) => { + const result = await listAnOrganization_sProjects({ + ...config, + path: { organization_id_or_slug: orgSlug }, + query: { cursor, per_page: API_MAX_PER_PAGE } as { + cursor?: string; + per_page?: number; + }, + }); + return unwrapPaginatedResult( + result as + | { data: SentryProject[]; error: undefined } + | { data: undefined; error: unknown }, + "Failed to list projects" + ); + }, Number.MAX_SAFE_INTEGER); // Populate project cache for shell completions (best-effort). // Mirrors how listOrganizations() calls setOrgRegions(). @@ -110,7 +109,9 @@ export async function listProjectsPaginated( }); return unwrapPaginatedResult( - result as { data: SentryProject[]; error: undefined } | { data: undefined; error: unknown }, + result as + | { data: SentryProject[]; error: undefined } + | { data: undefined; error: unknown }, "Failed to list projects" ); } diff --git a/src/lib/api/releases.ts b/src/lib/api/releases.ts index 725e1c28a..ee64aae4f 100644 --- a/src/lib/api/releases.ts +++ b/src/lib/api/releases.ts @@ -98,7 +98,9 @@ export async function listReleasesPaginated( }); return unwrapPaginatedResult( - result as { data: SentryRelease[]; error: undefined } | { data: undefined; error: unknown }, + result as + | { data: SentryRelease[]; error: undefined } + | { data: undefined; error: unknown }, "Failed to list releases" ); } diff --git a/src/lib/api/repositories.ts b/src/lib/api/repositories.ts index 723b8345e..2a557cab6 100644 --- a/src/lib/api/repositories.ts +++ b/src/lib/api/repositories.ts @@ -66,7 +66,9 @@ export async function listRepositoriesPaginated( }); return unwrapPaginatedResult( - result as { data: SentryRepository[]; error: undefined } | { data: undefined; error: unknown }, + result as + | { data: SentryRepository[]; error: undefined } + | { data: undefined; error: unknown }, "Failed to list repositories" ); } diff --git a/src/lib/api/teams.ts b/src/lib/api/teams.ts index f1a5cf311..32001998e 100644 --- a/src/lib/api/teams.ts +++ b/src/lib/api/teams.ts @@ -63,10 +63,7 @@ export async function listTeamsPaginated( } as { cursor?: string; per_page?: number }, }); - return unwrapPaginatedResult( - result, - "Failed to list teams" - ); + return unwrapPaginatedResult(result, "Failed to list teams"); } /** From a5280587d0c126101b4c85498e5d917488c56c5a Mon Sep 17 00:00:00 2001 From: Sentry Bot Date: Tue, 5 May 2026 00:15:25 +0000 Subject: [PATCH 3/4] fix: restore warning log when autoPaginate hits page limit The refactor to use autoPaginate removed per-callsite warning logs that fired when MAX_PAGINATION_PAGES was reached. Move the warning into autoPaginate itself so all callers benefit from the truncation notice. --- src/lib/api/infrastructure.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/api/infrastructure.ts b/src/lib/api/infrastructure.ts index ec809f4cf..275dcf47b 100644 --- a/src/lib/api/infrastructure.ts +++ b/src/lib/api/infrastructure.ts @@ -15,6 +15,7 @@ import { extractRequiredScopes } from "../api-scope.js"; import { getActiveEnvVarName, isEnvTokenActive } from "../db/auth.js"; import { getEnv } from "../env.js"; import { ApiError, AuthError, stringifyUnknown } from "../errors.js"; +import { logger } from "../logger.js"; import { resolveOrgRegion } from "../region.js"; import { getApiBaseUrl, @@ -327,7 +328,11 @@ export async function autoPaginate( cursor = result.nextCursor; } - // Safety limit reached — return what we have, no nextCursor + // Safety limit reached — warn and return what we have, no nextCursor + logger.warn( + `Pagination limit reached (${MAX_PAGINATION_PAGES} pages, ${allRows.length} items). ` + + "Results may be incomplete." + ); return { data: allRows.slice(0, limit) }; } From 1a376c060eb6850642fbb1fe2b979f75c1a2db3c Mon Sep 17 00:00:00 2001 From: Sentry Bot Date: Tue, 5 May 2026 11:56:36 +0000 Subject: [PATCH 4/4] fix: use MAX_PAGINATION_PAGES * API_MAX_PER_PAGE instead of Number.MAX_SAFE_INTEGER Replace Number.MAX_SAFE_INTEGER with MAX_PAGINATION_PAGES * API_MAX_PER_PAGE (50 * 100 = 5000) as the autoPaginate limit in listProjects and listAllRepositories. This makes the row-count cap explicit and aligned with the page-count safety net already inside autoPaginate. --- src/lib/api/projects.ts | 3 ++- src/lib/api/repositories.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/api/projects.ts b/src/lib/api/projects.ts index 86c4b23c5..86c369ae2 100644 --- a/src/lib/api/projects.ts +++ b/src/lib/api/projects.ts @@ -36,6 +36,7 @@ import { apiRequestToRegion, autoPaginate, getOrgSdkConfig, + MAX_PAGINATION_PAGES, ORG_FANOUT_CONCURRENCY, type PaginatedResponse, unwrapPaginatedResult, @@ -69,7 +70,7 @@ export async function listProjects(orgSlug: string): Promise { | { data: undefined; error: unknown }, "Failed to list projects" ); - }, Number.MAX_SAFE_INTEGER); + }, MAX_PAGINATION_PAGES * API_MAX_PER_PAGE); // Populate project cache for shell completions (best-effort). // Mirrors how listOrganizations() calls setOrgRegions(). diff --git a/src/lib/api/repositories.ts b/src/lib/api/repositories.ts index 2a557cab6..b563fe7e3 100644 --- a/src/lib/api/repositories.ts +++ b/src/lib/api/repositories.ts @@ -14,6 +14,7 @@ import { API_MAX_PER_PAGE, autoPaginate, getOrgSdkConfig, + MAX_PAGINATION_PAGES, type PaginatedResponse, unwrapPaginatedResult, unwrapResult, @@ -92,7 +93,7 @@ export async function listAllRepositories( cursor, perPage: API_MAX_PER_PAGE, }), - Number.MAX_SAFE_INTEGER + MAX_PAGINATION_PAGES * API_MAX_PER_PAGE ); return data; }