Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 1 addition & 3 deletions src/lib/api/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);

Expand Down
22 changes: 19 additions & 3 deletions src/lib/api/infrastructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -188,8 +189,17 @@ export function unwrapPaginatedResult<T>(
): PaginatedResponse<T> {
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<T> = { data };
if (nextCursor !== undefined) {
out.nextCursor = nextCursor;
}
if (prevCursor !== undefined) {
out.prevCursor = prevCursor;
}
return out;
}

/**
Expand Down Expand Up @@ -270,6 +280,8 @@ export type PaginatedResponse<T> = {
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;
};

/**
Expand Down Expand Up @@ -316,7 +328,11 @@ export async function autoPaginate<T>(
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) };
}

Expand Down
33 changes: 9 additions & 24 deletions src/lib/api/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ 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";

import {
API_MAX_PER_PAGE,
apiRequestToRegion,
autoPaginate,
getOrgSdkConfig,
MAX_PAGINATION_PAGES,
ORG_FANOUT_CONCURRENCY,
Expand All @@ -54,38 +54,23 @@ import { getUserRegions, listOrganizations } from "./organizations.js";
*/
export async function listProjects(orgSlug: string): Promise<SentryProject[]> {
const config = await getOrgSdkConfig(orgSlug);
const allResults: SentryProject[] = [];
let cursor: string | undefined;

for (let page = 0; page < MAX_PAGINATION_PAGES; page++) {
const { data: allResults } = await autoPaginate(async (cursor) => {
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 },
query: { cursor, per_page: API_MAX_PER_PAGE } as {
cursor?: string;
Comment thread
BYK marked this conversation as resolved.
per_page?: number;
},
});

const { data, nextCursor } = unwrapPaginatedResult<SentryProject[]>(
return unwrapPaginatedResult<SentryProject[]>(
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."
);
}
}
}, MAX_PAGINATION_PAGES * API_MAX_PER_PAGE);

// Populate project cache for shell completions (best-effort).
// Mirrors how listOrganizations() calls setOrgRegions().
Expand Down Expand Up @@ -121,7 +106,7 @@ 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<SentryProject[]>(
Expand Down
12 changes: 11 additions & 1 deletion src/lib/api/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,17 @@ 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<SentryRelease[]>(
Expand Down
34 changes: 12 additions & 22 deletions src/lib/api/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { logger } from "../logger.js";

import {
API_MAX_PER_PAGE,
autoPaginate,
getOrgSdkConfig,
MAX_PAGINATION_PAGES,
type PaginatedResponse,
Expand Down Expand Up @@ -59,12 +60,10 @@ 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<SentryRepository[]>(
Expand All @@ -79,33 +78,24 @@ 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
*/
export async function listAllRepositories(
orgSlug: string
): Promise<SentryRepository[]> {
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,
}),
MAX_PAGINATION_PAGES * API_MAX_PER_PAGE
);
return all;
return data;
}

/**
Expand Down
11 changes: 2 additions & 9 deletions src/lib/api/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,13 @@ 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<SentryTeam[]>(
result as
| { data: SentryTeam[]; error: undefined }
| { data: undefined; error: unknown },
"Failed to list teams"
);
return unwrapPaginatedResult<SentryTeam[]>(result, "Failed to list teams");
}

/**
Expand Down
Loading