Skip to content

Commit 423bfa2

Browse files
feat: update @sentry/api to 0.133.0 and adopt pagination improvements (#915)
Update `@sentry/api` from 0.113.0 to 0.133.0 and adopt the new pagination type improvements. This eliminates 5 unsafe `as { cursor?: string }` casts, replaces 2 manual pagination loops with `autoPaginate`, and adds `prevCursor` support to `PaginatedResponse`. ## Changes - **`@sentry/api` ^0.133.0** — includes pagination wrappers (`fetchPage_*`, `paginateAll_*`, `paginateUpTo_*`), widened query types (`cursor` + `per_page` now typed), and `prevCursor` in link header parsing - **infrastructure.ts** — `PaginatedResponse<T>` gains `prevCursor?: string`; `unwrapPaginatedResult` passes it through - **projects.ts** — manual 30-line pagination loop → `autoPaginate` one-liner; casts cleaned up - **repositories.ts** — manual 15-line pagination loop → `autoPaginate`; casts cleaned up - **teams.ts, releases.ts, issues.ts, events.ts** — `as { cursor?: string }` casts narrowed/removed; multi-line result casts collapsed ## What's NOT migrated (follow-up) `traces.ts`, `discover.ts`, and `dashboards.ts` widget queries stay on raw `apiRequestToRegion` — they use Zod validation schemas and the SDK wrappers' error handling (plain `Error`) is incompatible with the CLI's `ApiError`/`AuthError` pipeline. ## Testing - `bun test test/lib/api-client.coverage.test.ts` — 91/91 pass - `npx tsc --noEmit` — clean (no new errors) <!-- ## Plan 1. Bump @sentry/api from 0.113.0 to 0.133.0 2. Add prevCursor to PaginatedResponse type and unwrapPaginatedResult 3. Replace manual pagination loops in listProjects() and listAllRepositories() with autoPaginate 4. Clean up as { cursor?: string } casts — cursor and per_page are now in SDK query types 5. Remove unnecessary multi-line result casts 6. Keep CLI's own unwrapPaginatedResult (not SDK's) for ApiError/AuthError preservation 7. Leave traces.ts/discover.ts/dashboards.ts raw call sites for follow-up (Zod validation + error type incompatibility) --> --------- Co-authored-by: Sentry Bot <bot@sentry.io>
1 parent def5a35 commit 423bfa2

8 files changed

Lines changed: 57 additions & 65 deletions

File tree

‎bun.lock‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"@biomejs/biome": "2.3.8",
1212
"@clack/prompts": "^0.11.0",
1313
"@mastra/client-js": "^1.4.0",
14-
"@sentry/api": "^0.113.0",
14+
"@sentry/api": "^0.133.0",
1515
"@sentry/node-core": "10.50.0",
1616
"@sentry/sqlish": "^1.0.0",
1717
"@stricli/auto-complete": "^1.2.4",

‎src/lib/api/events.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,7 @@ export async function listIssueEvents(
237237
});
238238

239239
const paginated = unwrapPaginatedResult(
240-
result as
241-
| { data: IssueEvent[]; error: undefined }
242-
| { data: undefined; error: unknown },
240+
result,
243241
"Failed to list issue events"
244242
);
245243

‎src/lib/api/infrastructure.ts‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { extractRequiredScopes } from "../api-scope.js";
1515
import { getActiveEnvVarName, isEnvTokenActive } from "../db/auth.js";
1616
import { getEnv } from "../env.js";
1717
import { ApiError, AuthError, stringifyUnknown } from "../errors.js";
18+
import { logger } from "../logger.js";
1819
import { resolveOrgRegion } from "../region.js";
1920
import {
2021
getApiBaseUrl,
@@ -188,8 +189,17 @@ export function unwrapPaginatedResult<T>(
188189
): PaginatedResponse<T> {
189190
const response = (result as { response?: Response }).response;
190191
const data = unwrapResult(result, context);
191-
const { nextCursor } = parseLinkHeader(response?.headers.get("link") ?? null);
192-
return { data, nextCursor };
192+
const { nextCursor, prevCursor } = parseLinkHeader(
193+
response?.headers.get("link") ?? null
194+
);
195+
const out: PaginatedResponse<T> = { data };
196+
if (nextCursor !== undefined) {
197+
out.nextCursor = nextCursor;
198+
}
199+
if (prevCursor !== undefined) {
200+
out.prevCursor = prevCursor;
201+
}
202+
return out;
193203
}
194204

195205
/**
@@ -270,6 +280,8 @@ export type PaginatedResponse<T> = {
270280
data: T;
271281
/** Cursor for fetching the next page (undefined if no more pages) */
272282
nextCursor?: string;
283+
/** Cursor for the previous page (undefined on the first page) */
284+
prevCursor?: string;
273285
};
274286

275287
/**
@@ -316,7 +328,11 @@ export async function autoPaginate<T>(
316328
cursor = result.nextCursor;
317329
}
318330

319-
// Safety limit reached — return what we have, no nextCursor
331+
// Safety limit reached — warn and return what we have, no nextCursor
332+
logger.warn(
333+
`Pagination limit reached (${MAX_PAGINATION_PAGES} pages, ${allRows.length} items). ` +
334+
"Results may be incomplete."
335+
);
320336
return { data: allRows.slice(0, limit) };
321337
}
322338

‎src/lib/api/projects.ts‎

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ import {
2727
} from "../db/project-cache.js";
2828
import { getCachedOrganizations } from "../db/regions.js";
2929
import { type AuthGuardSuccess, withAuthGuard } from "../errors.js";
30-
import { logger } from "../logger.js";
3130
import { getApiBaseUrl } from "../sentry-client.js";
3231
import { buildProjectUrl } from "../sentry-urls.js";
3332
import { isAllDigits } from "../utils.js";
3433

3534
import {
3635
API_MAX_PER_PAGE,
3736
apiRequestToRegion,
37+
autoPaginate,
3838
getOrgSdkConfig,
3939
MAX_PAGINATION_PAGES,
4040
ORG_FANOUT_CONCURRENCY,
@@ -54,38 +54,23 @@ import { getUserRegions, listOrganizations } from "./organizations.js";
5454
*/
5555
export async function listProjects(orgSlug: string): Promise<SentryProject[]> {
5656
const config = await getOrgSdkConfig(orgSlug);
57-
const allResults: SentryProject[] = [];
58-
let cursor: string | undefined;
5957

60-
for (let page = 0; page < MAX_PAGINATION_PAGES; page++) {
58+
const { data: allResults } = await autoPaginate(async (cursor) => {
6159
const result = await listAnOrganization_sProjects({
6260
...config,
6361
path: { organization_id_or_slug: orgSlug },
64-
// per_page is supported by Sentry's pagination framework at runtime
65-
// but not yet in the OpenAPI spec
66-
query: { cursor, per_page: API_MAX_PER_PAGE } as { cursor?: string },
62+
query: { cursor, per_page: API_MAX_PER_PAGE } as {
63+
cursor?: string;
64+
per_page?: number;
65+
},
6766
});
68-
69-
const { data, nextCursor } = unwrapPaginatedResult<SentryProject[]>(
67+
return unwrapPaginatedResult<SentryProject[]>(
7068
result as
7169
| { data: SentryProject[]; error: undefined }
7270
| { data: undefined; error: unknown },
7371
"Failed to list projects"
7472
);
75-
allResults.push(...data);
76-
77-
if (!nextCursor) {
78-
break;
79-
}
80-
cursor = nextCursor;
81-
82-
if (page === MAX_PAGINATION_PAGES - 1) {
83-
logger.warn(
84-
`Pagination limit reached (${MAX_PAGINATION_PAGES} pages, ${allResults.length} items). ` +
85-
"Results may be incomplete for this organization."
86-
);
87-
}
88-
}
73+
}, MAX_PAGINATION_PAGES * API_MAX_PER_PAGE);
8974

9075
// Populate project cache for shell completions (best-effort).
9176
// Mirrors how listOrganizations() calls setOrgRegions().
@@ -121,7 +106,7 @@ export async function listProjectsPaginated(
121106
query: {
122107
cursor: options.cursor,
123108
per_page: options.perPage ?? API_MAX_PER_PAGE,
124-
} as { cursor?: string },
109+
} as { cursor?: string; per_page?: number },
125110
});
126111

127112
return unwrapPaginatedResult<SentryProject[]>(

‎src/lib/api/releases.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,17 @@ export async function listReleasesPaginated(
8484
environment: options.environment,
8585
statsPeriod: options.statsPeriod,
8686
status: options.status,
87-
} as { cursor?: string },
87+
} as {
88+
cursor?: string;
89+
per_page?: number;
90+
query?: string;
91+
sort?: string;
92+
health?: number;
93+
project?: number[];
94+
environment?: string[];
95+
statsPeriod?: string;
96+
status?: string;
97+
},
8898
});
8999

90100
return unwrapPaginatedResult<SentryRelease[]>(

‎src/lib/api/repositories.ts‎

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { logger } from "../logger.js";
1212

1313
import {
1414
API_MAX_PER_PAGE,
15+
autoPaginate,
1516
getOrgSdkConfig,
1617
MAX_PAGINATION_PAGES,
1718
type PaginatedResponse,
@@ -59,12 +60,10 @@ export async function listRepositoriesPaginated(
5960
const result = await listAnOrganization_sRepositories({
6061
...config,
6162
path: { organization_id_or_slug: orgSlug },
62-
// per_page is supported by Sentry's pagination framework at runtime
63-
// but not yet in the OpenAPI spec
6463
query: {
6564
cursor: options.cursor,
6665
per_page: options.perPage ?? 25,
67-
} as { cursor?: string },
66+
} as { cursor?: string; per_page?: number },
6867
});
6968

7069
return unwrapPaginatedResult<SentryRepository[]>(
@@ -79,33 +78,24 @@ export async function listRepositoriesPaginated(
7978
* List **all** repositories in an organization by walking every page.
8079
*
8180
* Used by the offline repo cache and anywhere else we need the complete
82-
* set (not just the first page). Stops at {@link MAX_PAGINATION_PAGES}
83-
* as a safety net for pathological cases.
81+
* set (not just the first page). Bounded by `autoPaginate`'s
82+
* {@link MAX_PAGINATION_PAGES} safety limit.
8483
*
8584
* @param orgSlug - Organization slug
8685
* @returns All Sentry-registered repositories across all pages
8786
*/
8887
export async function listAllRepositories(
8988
orgSlug: string
9089
): Promise<SentryRepository[]> {
91-
const all: SentryRepository[] = [];
92-
let cursor: string | undefined;
93-
for (let page = 0; page < MAX_PAGINATION_PAGES; page++) {
94-
const { data, nextCursor } = await listRepositoriesPaginated(orgSlug, {
95-
cursor,
96-
perPage: API_MAX_PER_PAGE,
97-
});
98-
all.push(...data);
99-
if (!nextCursor) {
100-
return all;
101-
}
102-
cursor = nextCursor;
103-
}
104-
log.warn(
105-
`Stopped paginating repositories for '${orgSlug}' after ${MAX_PAGINATION_PAGES} pages — ` +
106-
"some repos may be missing from the cache."
90+
const { data } = await autoPaginate(
91+
(cursor) =>
92+
listRepositoriesPaginated(orgSlug, {
93+
cursor,
94+
perPage: API_MAX_PER_PAGE,
95+
}),
96+
MAX_PAGINATION_PAGES * API_MAX_PER_PAGE
10797
);
108-
return all;
98+
return data;
10999
}
110100

111101
/**

‎src/lib/api/teams.ts‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,13 @@ export async function listTeamsPaginated(
5757
const result = await listAnOrganization_sTeams({
5858
...config,
5959
path: { organization_id_or_slug: orgSlug },
60-
// per_page is supported by Sentry's pagination framework at runtime
61-
// but not yet in the OpenAPI spec
6260
query: {
6361
cursor: options.cursor,
6462
per_page: options.perPage ?? 25,
65-
} as { cursor?: string },
63+
} as { cursor?: string; per_page?: number },
6664
});
6765

68-
return unwrapPaginatedResult<SentryTeam[]>(
69-
result as
70-
| { data: SentryTeam[]; error: undefined }
71-
| { data: undefined; error: unknown },
72-
"Failed to list teams"
73-
);
66+
return unwrapPaginatedResult<SentryTeam[]>(result, "Failed to list teams");
7467
}
7568

7669
/**

0 commit comments

Comments
 (0)