Skip to content

fix(api): Keep per_page stable when listPage walks offset cursors - #130518

Closed
sentry-junior[bot] wants to merge 1 commit into
feat/api-packagefrom
fix/api-list-page-offset
Closed

sentry-junior[bot] wants to merge 1 commit into
feat/api-packagefrom
fix/api-list-page-offset

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Stacked on #130472. This fixes the pagination bug Cursor Bugbot found during that review (thread). The bug also ships in the published @sentry/api 1.54.0.

Bug

listPage asked each later page for only the rows still missing: per_page = min(100, limit - items.length). listProjects, listTeams and listOrganizations are served by Sentry's OffsetPaginator. Its cursor is <page size>:<page>:<is_prev>, and when per_page doesn't match the cursor's page size, it reads the cursor's window and keeps only the last per_page rows (src/sentry/api/paginator.py). So 100:1:0 with per_page=50 returns rows 150–199, and rows 100–149 are lost. Any limit above 100 dropped rows without an error.

Fix

listPage now walks through the package's existing paginate, which:

  • uses one per_page for the whole walk. A fresh walk splits limit evenly: 150 becomes 2 × 75.
  • reuses the page size that an offset cursor encodes when resuming, whatever the new limit is.
  • returns a cursor that resumes at the first row not returned, including partway through a page.

listPage still validates each page with the schema and still rejects a page with more rows than requested or a repeated cursor. One behavior changes: if a walk reaches MAX_LIST_PAGES, it now returns the rows it has and a resume cursor instead of throwing. Before this change, a stable page size couldn't hit that cap in a normal walk. Now a resumed small-page cursor can, for example 25:1:0 with limit: 5000.

Tests

  • A model of OffsetPaginator in core.test.ts. It confirms that 100:1:0 with per_page=50 reads rows 150–199. Then it checks two things:
    • a 150-row walk and its resume return rows 0–299 in order, all with per_page=75;
    • resuming 100:1:0 with limit: 50 returns rows 100–149, then 150–199, both requested with per_page=100.
  • The listOrganizations test now expects per_page to stay at 3 on the second request. It was 1, which was the bug.

Package build, typecheck and tests pass (657 tests).

No UI change.

via @BYK.

--

View Session in Sentry

@sentry-junior
sentry-junior Bot requested review from a team as code owners November 9, 2026 21:37
@sentry-junior
sentry-junior Bot added this pull request to stack #130519 November 9, 2026 21:37
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Nov 9, 2026
@sentry-junior
sentry-junior Bot force-pushed the fix/api-list-page-offset branch from f2629b2 to f0a7b4b Compare November 12, 2026 21:36
@sentry

sentry Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Sentry Snapshot Testing

Name Added Removed Changed Renamed Unchanged Skipped Status
sentry-frontend
sentry-frontend
0 0 0 0 824 0 ✅ Unchanged

⚙️ sentry-frontend Snapshot Settings

listPage asked each later page for only the rows still missing. Sentry's
OffsetPaginator reads a cursor's window with the page size the cursor
encodes, so pairing 100:1:0 with per_page=50 returned rows 150-199 and
dropped 100-149. listProjects, listTeams and listOrganizations lost rows
whenever limit was above 100.

listPage now walks through paginate, which keeps one page size per walk,
reuses the size an offset cursor encodes, and returns a cursor that resumes
mid-page.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@BYK

BYK commented Nov 13, 2026

Copy link
Copy Markdown
Member

Can you move this and the next on stack back to sentry-api-schema repo for now as it advanced a lot and it's gonna be easier to get docs aligned first and then move @sentry/api into getsentry/sentry

@sentry-junior

sentry-junior Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

Moved to getsentry/sentry-api-schema#149.

@sentry-junior sentry-junior Bot closed this Nov 13, 2026
BYK added a commit to getsentry/sentry-api-schema that referenced this pull request Nov 13, 2026
Ported from getsentry/sentry#130518, per @BYK's request there: get the
docs aligned here first, then move `@sentry/api` into getsentry/sentry.
The commit applied cleanly on top of 2.0.0. Cursor Bugbot found the bug
while reviewing the move
([thread](getsentry/sentry#130472 (comment))).
It ships in the published `@sentry/api` 2.0.0.

## Bug

`listPage` asked each later page for only the rows still missing:
`per_page = min(100, limit - items.length)`. `listProjects`, `listTeams`
and `listOrganizations` are served by Sentry's `OffsetPaginator`. Its
cursor is `<page size>:<page>:<is_prev>`. When `per_page` doesn't match
the cursor's page size, it reads the cursor's window and keeps only the
last `per_page` rows (`src/sentry/api/paginator.py` in
getsentry/sentry). So `100:1:0` with `per_page=50` returns rows 150–199,
and rows 100–149 are lost. Any `limit` above 100 dropped rows without an
error.

## Fix

`listPage` now walks through the package's existing `paginate`, which:
- uses one `per_page` for the whole walk. A fresh walk splits `limit`
evenly: 150 becomes 2 × 75.
- reuses the page size that an offset cursor encodes when resuming,
whatever the new `limit` is.
- returns a cursor that resumes at the first row not returned, including
partway through a page.

`listPage` still validates each page with the schema, and still rejects
a page with more rows than requested or a repeated cursor. One behavior
changes: if a walk reaches `MAX_LIST_PAGES`, it now returns the rows it
has and a resume cursor instead of throwing. Before this change, a
stable page size couldn't hit that cap in a normal walk. Now a resumed
small-page cursor can, for example `25:1:0` with `limit: 5000`.

## Tests

- `test/api-client/core.test.ts` models `OffsetPaginator`. It confirms
that `100:1:0` with `per_page=50` reads rows 150–199. Then it checks two
things:
- a 150-row walk and its resume return rows 0–299 in order, all with
`per_page=75`;
- resuming `100:1:0` with `limit: 50` returns rows 100–149, then
150–199, both requested with `per_page=100`.
- The `listOrganizations` test now expects `per_page` to stay at 3 on
the second request. It was 1, which was the bug.

`pnpm build`, `pnpm typecheck` and `pnpm test` pass locally (53 files,
675 tests).

No UI change.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3A69da19b385017e68b7f9eace56e9477f -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3A69da19b385017e68b7f9eace56e9477f)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A69da19b385017e68b7f9eace56e9477f/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — 6a2d11f4 Deployed Nov 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant