Skip to content

feat(operations): Expose idempotent flag in the operation catalog - #111

Merged
BYK merged 1 commit into
mainfrom
feat/operation-idempotent
Oct 27, 2026
Merged

BYK merged 1 commit into
mainfrom
feat/operation-idempotent

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Adds idempotent: boolean to every ApiOperation in @sentry/api/operations. Retrying clients such as the CLI and MCP can then decide from the catalog whether to resend a request after a timeout or 5xx, instead of hard-coding that per call site.

Refs getsentry/sentry#128927. The Sentry side, getsentry/sentry#128930, now stamps x-sentry-idempotent: true on operations whose endpoint declares the method in idempotent_methods.

Semantics

  • GET, HEAD, OPTIONS, TRACE: always true (RFC 9110 §9.2.1). Clients get one field to check, with no method special-casing.
  • Other methods: true only when the operation has x-sentry-idempotent: true. PUT and DELETE are not assumed idempotent because not every Sentry handler follows RFC 9110 §9.2.2. Sentry makes the same choice.
  • false means "not known to be idempotent", not "known to be unsafe". It is documented that way in the type docs and the README.

Current effect

Today's openapi-derefed.json has no x-sentry-idempotent markers. The endpoints declared so far (chunk upload, DIF assemble, artifact bundle assemble) are all PRIVATE, so nothing non-safe is marked yet. In the current build, 147 of 266 operations are idempotent: true, all safe methods. Non-safe operations will be marked as they're published with the declaration.

Verification

  • bun run build, bun run typecheck, bun test (167 pass), bun run test:node
  • New test: a marked POST is true; an unmarked POST/PUT/DELETE is false. Existing catalog fixtures assert true for GET and false for an unmarked POST.

via BYK.

--

View Junior Session in Sentry

Read Sentry's x-sentry-idempotent operation extension so retrying clients
can tell which operations are safe to resend after a timeout or 5xx,
instead of hard-coding it per call site.

Refs getsentry/sentry#128927

Co-Authored-By: BYK <burak.kaya@sentry.io>
@BYK
BYK merged commit 00fa748 into main Oct 27, 2026
10 checks passed
@BYK
BYK deleted the feat/operation-idempotent branch October 27, 2026 22:54
julienhmmt pushed a commit to julienhmmt/fork-sentry that referenced this pull request Oct 28, 2026
…ental (getsentry#128999)

Follow-up to getsentry#128930. Publishes the endpoints that declare
`idempotent_methods` so the `x-sentry-idempotent` markers actually reach
the public schema, and through it `@sentry/api`
(getsentry/sentry-api-schema#111 already reads the extension).

Refs getsentry#128927.

**What changes**

All four operations are `PUBLIC_EXPERIMENTAL` and fully documented:

- `GET /organizations/{org}/chunk-upload/`
(`getOrganizationChunkUploadOptions`): the upload URL and limits.
Clients need it before they upload, so it's published with the POST.
- `POST /organizations/{org}/chunk-upload/`
(`uploadOrganizationChunks`): multipart body described by a docs-only
`ChunkUploadSerializer`, following the `ReleaseFileUploadSerializer`
precedent.
- `POST /projects/{org}/{project}/files/difs/assemble/`
(`assembleProjectDebugFiles`)
- `POST /organizations/{org}/artifactbundle/assemble/`
(`assembleOrganizationArtifactBundle`)

The three POSTs carry `x-sentry-idempotent: true`. GET doesn't, because
safe methods are implicit.

**Notes for review**

- **One schema for validation and docs.** The assemble endpoints'
jsonschema moved to module constants that both validation and
`extend_schema` use, so the docs can't drift from what's enforced.
Validation behavior is unchanged.
- **DIF body is a map keyed by checksum.** OpenAPI 3.0 has no
`patternProperties`, so the docs version uses `additionalProperties`,
and the checksum key format is explained in the description.
`_validate_request_body` raised `KeyError: 'properties'` on map bodies.
It now checks field descriptions under the map's `additionalProperties`.
The new `MapRequestBodyTest` covers this.
- **Types.** Added response TypedDicts and typed `batch_assemble`.
`state` is `str`, not a `Literal`, because `ChunkFileState` values are
plain strings.
- **`accept` is now a list.** It was a tuple, which spectacular
documents as a fixed-length array, so the GET options response returns
`list(CHUNK_UPLOAD_ACCEPT)` instead. The JSON output is the same.
- **File parts show as `format: uri`.** That's how spectacular renders
`FileField`, and it matches the existing release-file upload docs. Happy
to override it to `binary` if reviewers prefer.

**Verification**

- `sentry django spectacular --validate --fail-on-warn` passes. The
deref build and `openapi-examples-validator --no-additional-properties`
pass with no errors.
- `ruff check`, `ruff format --check`, `flake8`, and `mypy` are clean on
the changed files.
- Passing: `tests/apidocs/test_hooks.py`,
`test_operation_id_uniqueness.py`, `test_chunk_upload.py`,
`test_organization_artifactbundle_assemble.py` (78 tests).
- `test_dif_assemble.py` needs an objectstore server, which my sandbox
doesn't have, so CI is the authoritative run there. I checked
`parse_assemble_request_payload` directly instead: it accepts valid
payloads and rejects bad checksum keys, a missing `name`, non-hex
chunks, and invalid `debug_id`, the same as before.

<!-- junior-request-attribution:start -->
via **BYK**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:slack%3AC08J1NSPU6S%3A1792772794.175999 -->

--

[View Junior
Session](https://junior-prod.sentry.dev/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999)
[[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC08J1NSPU6S%3A1792772794.175999/?project=4510944073809921)

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

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>
BYK pushed a commit to getsentry/toolkit that referenced this pull request Oct 28, 2026
The CLI now reads which writes are safe to replay from the `@sentry/api`
operation catalog. Before, each call site hard-coded `idempotent: true`.

Sentry publishes chunk-upload and the DIF and artifact bundle assemble
endpoints as `PUBLIC_EXPERIMENTAL`, with `x-sentry-idempotent` set
([sentry#128999](getsentry/sentry#128999)).
`@sentry/api` exposes that as `idempotent` on each operation
([sentry-api-schema#111](getsentry/sentry-api-schema#111)).

**What changes**

- `@sentry/api` goes from `0.400.0` to `0.420.0` in the pnpm catalog, so
the CLI and MCP both get it.
- `generate:schema` also writes
`src/generated/idempotent-operations.json`. The file lists the non-safe
operations that the catalog marks `idempotent`, which today are 3 POSTs.
It is a separate file so the CLI does not load the 140 KB endpoint index
on every request. Like `banner-sixel.ts`, it is committed, because code
outside the CLI (such as `apps/local`) imports `sentry-client.ts`
without running `generate:schema`. Commit it again after an
`@sentry/api` bump. The build regenerates it either way.
- `isIdempotentOperation()` (`src/lib/idempotent-operations.ts`) matches
a request URL against those path templates. It matches from `/api/`
onward, so self-hosted installs served under a path prefix still match.
`fetchWithRetry` treats a match like an explicit opt-in.
- These call sites no longer pass `idempotent: true`: chunk uploads, DIF
assemble (debug files, ProGuard, Dart symbols), and artifact bundle
assemble (source maps, `pollAssembly`).
- Preprod assemble keeps its explicit opt-in. Sentry deliberately leaves
it unmarked, because a resend after the upload stage can create a second
artifact. The CLI's existing argument still applies: its poll loop
already resends the same body.

There is no behavior change for these endpoints today. They retried
before because of the opt-in, and now they retry because Sentry marks
them. If Sentry marks or unmarks an endpoint later, the next
`@sentry/api` bump picks it up.

**Verification**

- `pnpm run tsc` (whole workspace), CLI `pnpm run lint`, and root `pnpm
run lint` all pass.
- `apps/local` vitest (the `Local` CI job) passes with only the
committed generated files present: 38 files, 322 tests.
- MCP core vitest: 1881 passed.
- CLI unit tests: 11652 passed and 6 failed. The same 6 fail on
unmodified `main` in my sandbox. They are file-permission tests
(`EACCES`/`EPERM`, read-only dirs).
- New `test/lib/idempotent-operations.test.ts` covers matching, the
self-hosted prefix, case, and the trailing slash. It also checks that
unmarked writes (preprod assemble, autofix) don't match and that a
template segment can't span two path segments.
- New `sentry-client` tests check that a marked POST retries after a 503
and after a network error with no opt-in. I confirmed both fail when the
catalog lookup is removed.

Refs getsentry/sentry#128927

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3A828b4ad5f18c5305232eec52 -->

--

[View Junior Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A828b4ad5f18c5305232eec52/?project=4510944073809921)

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

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant