Repository navigation
docs(openspec): Propose a shared Sentry API layer for the CLI and MCP - #1669
Open
sentry-junior[bot] wants to merge 23 commits into
Open
sentry-junior[bot] wants to merge 23 commits into
sentry-junior[bot] wants to merge 23 commits into
Conversation
Share pagination, response caching, and request transport between the CLI and MCP through toolkit-core on top of @sentry/api, let sentry mcp supply the CLI's authenticated transport, and add a tiered (memory + R2), zstd-compressed cache for the hosted MCP. Co-Authored-By: BYK <burak.kaya@sentry.io>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Benchmark zstd, brotli, gzip and trained zstd dictionaries on real API responses. Keep plain zstd for now, cap entries on compressed size, and move dictionary support to a follow-up gated on safe training data and Workers support (workerd ignores the zstd dictionary option). Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK
added a commit
that referenced
this pull request
Oct 26, 2026
…ry (#1671) `search_issues` was sending AND/OR to the issues endpoint, which rejects them with `Boolean statements containing "OR" or "AND" are not supported in this search`. That's the top `search_issues` failure in `sentry/mcp-server`: about 93K a week across many users, mostly `is:X OR is:Y`. Two things in MCP let them through: - The repair prompt (`search-issues/config.ts`) listed `AND, OR, NOT` as valid syntax and told the agent to keep explicit tokens. - With no agent provider configured, queries went to the API unchecked. **What changes** - **The CLI sanitizer moves to `@sentry/toolkit-core/search-query`.** `rewriteSearchQuery` returns `{ query, notes }` and throws `SearchQueryError`. Each product decides how to report: the CLI wrapper (`cli/src/lib/search-query.ts`) keeps its stderr warning and `ValidationError`, and MCP throws `UserInputError`. CLI behavior is unchanged except one error line, which now reads `key:[val1,val2]` instead of `--query "key:[val1,val2]"`. - **`search_issues` runs it after the optional agent repair**, right before `listIssues`, so it covers the no-provider path too. When it rewrites a query, the result says so and shows the query it ran. Queries it can't rewrite fail with a `UserInputError` that lists alternatives, before any API call. - **New `issueSearch` option** (MCP only for now): - `is:A OR is:B` becomes `status:[A,B]` or `substatus:[A,B]`. `is:` rejects in-lists, but the status and substatus filters it maps to accept them. - Substatus lists must share one parent status, because the endpoint adds a status filter for the first substatus only (`expand_substatus_query_values`). So `is:ongoing OR is:archived_forever` is still rejected. - Parentheses that reduce to plain terms are unwrapped: `is:unresolved (is:regressed OR is:escalating)` becomes `is:unresolved substatus:[regressed,escalating]`. - **The agent prompt and tool description now say** there's no AND/OR/NOT or parentheses, and give the in-list, `status:`, and `substatus:` forms instead. **Checked against the live API** (sentry org, `junior` project, 14d, `X-Hits`): - `status:[unresolved,resolved]` returned 165, which is 159 (`is:unresolved`) + 6 (`is:resolved`). - `substatus:[new,ongoing]` returned 159, which is 53 (`is:new`) + 106 (`is:ongoing`). - `substatus:[ongoing,archived_forever]` returned only unresolved issues, and `[archived_forever,ongoing]` returned none. That's the first-substatus behavior the parent-status check guards against. - `(is:unresolved)` returned `[]` and `NOT is:resolved` returned `[]` (NOT is searched as free text), both with a 200. That's why the prompt now says `!` instead of NOT, and why parentheses get unwrapped. - `is:[unresolved,resolved]` returned a 400: `"in" syntax invalid for "is" search`. **Reviewing the move** Git shows the core file as new, because the CLI path still holds the wrapper. To see only what changed in the moved code: ``` git diff main:packages/cli/src/lib/search-query.ts HEAD:packages/toolkit-core/src/search-query.ts ``` **Not in this PR** - The CLI's `issue list` could opt into `issueSearch`. I left it off to keep CLI behavior unchanged. - MCP's other query tools don't use the shared sanitizer yet. - The 429 retry in `get_sentry_resource` is task 4.1 of #1669. **Checks** - `pnpm run tsc` and `pnpm run lint` pass. - Tests pass for `toolkit-core` (356), `mcp-core` (1833), and the CLI search-query, issue, log, span, and replay tests (532). - The full CLI `test/lib` + `test/commands` run has 6 failures in filesystem-permission tests (completions, upgrade lock, code scanner, file-changes). The completions and upgrade-lock failures also happen on `main` in this sandbox, so they look environment-specific. <!-- 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: Burak Yigit Kaya <126780+BYK@users.noreply.github.com> Co-authored-by: Andrew Liu <andrew.liu@sentry.io>
The getsentry/sentry#128818 spike found that no event or trace endpoint is safe to cache as immutable. Issue and project event details embed next/previous IDs and user reports, `latest` resolves to different events over time, and full traces gain spans. The CLI's 24-hour tier still matches all of them, including `/issues/<id>/events/latest/`. Replace the immutable tier with a 5-minute detail tier limited to organization event details and project trace-item details, and move issue events, project events, and full traces to 60 seconds. Add the CLI tier correction as its own task group ahead of the toolkit-core move. R2 now persists only detail-tier entries that the server marks `private, max-age`, for at most 5 minutes, and needs a security sign-off before production. Use zstd level 3 for every tier instead of level 9 for R2. Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK
added a commit
that referenced
this pull request
Oct 26, 2026
…1672) The CLI and MCP core pinned different `@sentry/api` versions, so the two products could disagree on generated types and request helpers. This adds `@sentry/api: 0.400.0` to the pnpm catalog and points `packages/cli` and `packages/mcp-core` at `catalog:`. The lockfile now resolves a single `@sentry/api` version. MCP core needed no code changes for the bump. This is task 1 of the `unify-sentry-api-layer` OpenSpec change in #1669. Checks run locally: - `pnpm install --frozen-lockfile` - `pnpm --filter @sentry/mcp-core run tsc` - `pnpm --filter @sentry/mcp-core run test` (131 files, 1,831 passed, 6 skipped) - `pnpm --filter @sentry/mcp-core run build` - `tsc` for `@sentry/mcp-server` and `@sentry/mcp-cloudflare` - Formatter check on changed files The full root quality gate and the CLI suite were not run locally; CI covers them. <!-- junior-request-attribution:start --> via **BYK**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf --> -- [View Junior Session in Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf/?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>
…ders Neither product handles 429s well. The CLI retries every method, POSTs included, and ignores rate-limit headers. The MCP retries only GET 502/503/504, so the ~32.6K rate-limited GETs it sees a week fail immediately. Add task 5.0 for a pure `toolkit-core/src/api-retry.ts` policy: retry only GET/HEAD/OPTIONS unless a request opts in as idempotent, wait for Retry-After or X-Sentry-Rate-Limit-Reset with a clock-skew guard and caps, fall back to jittered backoff, and sleep abortably. The MCP adopts it first. Task 5.1 now moves idempotent-only retry through it and calls the CLI's all-methods retry a bug fixed separately; 6.1 drops the MCP's own retry once it is on the transport so retries don't stack. Add a sentry-api-transport spec delta for these rules. Co-Authored-By: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
Contributor
Author
|
Plan update (a81439e): retries and 429s.
|
…uses Sentry's RatelimitMiddleware rejects a 429 in process_request, before the view runs, so replaying one cannot repeat a write. The sentry-api-transport spec now retries 429s for any method, and retries 5xx, timeouts, and network errors only for GET/HEAD/OPTIONS or an explicit idempotent opt-in. The retried statuses are 429, 500, 502, 503, and 504. Other 5xx codes are deterministic. The clock-skew wording now matches the Date-bounded delay from #1673. The long requirements are split so `openspec validate --strict` passes.
Contributor
Author
|
Correction to my earlier plan comment (b5523f5), following review on #1673:
The |
This was referenced Oct 27, 2026
Merged
BYK
added a commit
that referenced
this pull request
Oct 27, 2026
…tions on missing org (#1678) ## Problem Telemetry from `sentry/mcp-server` (spans over 7 days, extrapolated from 0.3 sampling, so user counts are lower bounds): - **Missing `organizationSlug`:** about 23.8K failures from 861+ users. One user sent ~17.7K of them, all `search_issues` with `period:"1h"`. Without them it's ~6.1K failures across ~860 users. All of these came in on the unscoped `/mcp` URL. They fail in the MCP SDK's input validation before any handler runs, so the zod error message is the only place to add guidance without changing the schema. - **`period: "1h"` on `search_issues`:** about 25.6K failures from ~50 users, mostly that same user. - **Minute periods:** values like `30m`, `15m` and `10m` on `search_errors`, `search_logs` and `search_traces` were never accepted. That's about 1.5K failures from ~300 users. Sentry reads `m` as minutes (read-only GET check: `30m` returned 28 errors, against 1,269 for `24h`), and the issues endpoint accepts `statsPeriod=1h`. ## Changes - **`SEARCH_ISSUES_PERIOD_VALUES`** now includes `1h`. - **`ParamPeriod`** now accepts minutes (`/^\d+[mhdw]$/`), and the rejection message says "(minutes)". - **A missing or null `organizationSlug`** now returns: `Required. Call find_organizations() to list your organizations, then retry with organizationSlug set to one of their slugs.` This works on SDK v1 and v2 and doesn't change the JSON schema. - **Tool definitions** are regenerated. Token overhead goes up by 13 (10,066 → 10,079). ## Held back - **Defaulting to the user's only org.** This would mean making `organizationSlug` optional on ~50 tools, and fetching the org list plus a region lookup on every call that has no org. That's the org endpoint, cache and region-routing code that #1669 reworks, so it's left for that work. - **Listing the user's orgs in the error, with a ready-to-copy retry.** Same blocker: the SDK rejects the call before our handler sees it. ## Checks - `pnpm run tsc` and `pnpm run lint` pass. - mcp-core: 1847 passed, 6 skipped. ## Risk A model that means months by `3m` would get 3 minutes of data. Telemetry shows callers mean minutes; there are only a handful of `6m`/`12m`. None of this fixes the stuck script. <!-- 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: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK
added a commit
that referenced
this pull request
Oct 27, 2026
…#1679) ## Problem These are the most common field and sort errors in `sentry/mcp-server` telemetry (7 days, extrapolated from 0.3 sampling, so user counts are lower bounds). Each fix was checked against the live API. - **`search_logs` with `min/max(timestamp)`:** ~2.6K failures from ~470 users. Log timestamps are strings, so the fix is `min/max(timestamp_precise)` (nanoseconds). Metrics behaves the same way (43 failures). - **`search_errors` with `first_seen()`:** ~2.3K failures from ~590 users. That function doesn't exist; use `min(timestamp)`, plus `max(timestamp)` or `last_seen()`. About 700 of these came from fields the embedded agent picked. - **`search_errors` sorted by `issue`:** ~1K failures from ~270 users. Sentry rejects this sort even when `issue` is selected; use `-count()` or `issue.id`. ## Changes - **New `tools/support/search-events/field-hints.ts`.** It adds a hint only when the request actually sent the rejected expression and Sentry's error message matches it. - Hints are fixed text and never include caller input, and Sentry's own message is kept. - Anything that doesn't match is re-thrown unchanged as `ApiValidationError`. - **Hints are added in two places:** the validation failure, and the final Events API 400. The second is needed because the validate endpoint accepts the `first_seen()` and `issue`-sort cases. - **`min(timestamp)`** is added to the embedded agent's errors field list. ## Not covered - `timestamp.to_day` / `to_hour` on spans and logs (~600 failures): there's no clear one-line replacement. - Metrics `count()` arity (~350), `p10`/`p25` (~150), and `count(span)` (0). ## Checks - tsc, lint (oxfmt) and all mcp-core tests pass (1837 passed, 6 skipped). - Tool definitions are unchanged (10,066 tokens). ## Relation to #1669 Task 7.3 moves the explore, traces and logs endpoints onto `@sentry/api`. These hints only depend on the error being an `ApiValidationError` and on its message, so they should carry over as long as 400s still map to that class. <!-- 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: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK
added a commit
that referenced
this pull request
Oct 27, 2026
…rs (#1673) The hosted MCP gets rate limited by the Sentry API a lot. In the last 7 days `sentry/mcp-server` logged about 32.6K `http.client` 429s, nearly all on GETs, usually "Limit is 40 requests in 1 seconds", plus about 72 concurrency-limit hits. `SentryApiService.request()` only retried 502/503/504, so every 429 failed the tool call. `ApiRateLimitError.retryAfter` was read from body fields Sentry never sends, so nothing could say when to try again. Sentry's 429 body is just `{"detail": ...}`. The timing comes in headers set by `src/sentry/middleware/ratelimit.py`: `X-Sentry-Rate-Limit-Reset` is the Unix second when the next window starts, and `Remaining` is `0` only when the fixed window is used up. Concurrency 429s carry a reset that doesn't apply to them. This adds `@sentry/toolkit-core/api-retry`, a pure retry policy with no Node APIs, so it is safe on Workers. It is task 5.0 in #1669. The MCP adopts it here: - **What gets retried:** GET and HEAD requests that get a 429 or a 502/503/504. Writes are never retried. The policy's `idempotent` opt-in exists for the CLI's content-addressed uploads, but the MCP doesn't use it. - **How long a 429 waits:** until `Retry-After` (seconds or HTTP-date) if a proxy sent one, otherwise until `X-Sentry-Rate-Limit-Reset`. The reset is measured against the response `Date` when the local clock is off by more than 2s, plus a small jitter. Concurrency 429s and gateway errors use equal-jitter exponential backoff starting at 250ms. - **Caps:** at most 2 retries, 2s per wait and 3s in total. Sentry's windows are mostly 1s, so the reset is usually under a second away and both retries fit in about 2s. A reset further out than 2s means a longer window. Then the call fails at once instead of holding the Worker request open. The error is an `ApiRateLimitError` with a real `retryAfterMs`, and its message ends with "Retry in N seconds." When there is no header, it says to wait a few seconds and make fewer parallel requests. - **Abort:** aborting the request's `signal` ends the wait immediately. `ApiRateLimitError.retryAfter` (seconds, from the body) is replaced by `retryAfterMs`, taken from the headers. Nothing read the old field. The CLI currently has its own interim retry fix in a separate CLI-only PR. It moves onto this shared module in a follow-up after that PR lands. Once the MCP is on the shared transport (#1669 task 6.1), `SentryApiService` drops this loop so retries don't stack. <!-- junior-request-attribution:start --> via @BYK. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3A0bac2c6dfda59b43b43e21a1118f2684 --> -- [View Junior Session](https://junior-prod.sentry.dev/conversations/agent%3A0bac2c6dfda59b43b43e21a1118f2684) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A0bac2c6dfda59b43b43e21a1118f2684/?project=4510944073809921) <!-- junior-session-footer:end --> --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
BYK
added a commit
that referenced
this pull request
Oct 27, 2026
…reset on 429 (#1677) ## Problem `packages/cli/src/lib/sentry-client.ts` retried **every** HTTP method, including POST/PUT/DELETE, on 408/429/500/502/503/504 and on network errors and timeouts. That can replay writes. A 429 also only got the generic 1s/2s backoff: the client never read Sentry's `X-Sentry-Rate-Limit-Reset` header, and Sentry doesn't send `Retry-After` on API rate limits. ## Behavior changes - **5xx, timeouts and network errors** now retry only for GET, HEAD and OPTIONS, or when a request sets the new `idempotent: true` opt-in on `getSdkConfig` / `ApiRequestOptions`. - **429 retries for any method.** Sentry's rate-limit middleware rejects the request in `process_view`, before the endpoint runs, so nothing was applied. - **Retried statuses stay 408/429/500/502/503/504.** Deterministic 5xx like 501 and 505 are not retried. - **PUT and DELETE are deliberately not retried.** Sentry PUTs have side effects (merge, activity), and a replayed DELETE returns 404. - **The 401-refresh retry still covers every method**, and no longer waits 1s first. - **A timed-out write** now gets a "may still have been applied" hint. - **The 429 wait** is the longer of `X-Sentry-Rate-Limit-Reset` (read only when Remaining is 0) and `Retry-After` (seconds or HTTP date), both measured against the response `Date` to guard against clock skew. Concurrency-limit 429s, or 429s with no hint, use jittered backoff: the upper half of min(1s·2^n, 10s). - **Caps are unchanged:** `MAX_RETRIES` is 2 and each wait is at most 10s. - **New `RateLimitError`** (extends `ApiError`, status 429). It's thrown when the wait would exceed the cap or retries run out. The message is agent-friendly: server detail, then "Retry in N seconds; do not repeat the request before then. Make fewer requests in parallel." - **The policy is pure**, with no CLI imports, in `packages/cli/src/lib/retry-policy.ts`. That way the CLI can switch to the shared `@sentry/toolkit-core/api-retry` from #1673 without a rewrite. ## POST callers given the `idempotent` opt-in Each was checked against its getsentry/sentry handler: - **Chunk uploads** (`chunk-upload.ts`): blobs are stored by SHA-1 (`FileBlob.from_files`). - **Artifact-bundle assemble and `pollAssembly`** (`sourcemaps.ts`). - **DIF assemble** in `debug-files.ts`, `proguard.ts` and `dart-symbols.ts`: the server checks the cached assemble status by checksum. **Not opted in:** preprod assemble (each call creates a new `PreprodArtifact`), Seer `/autofix/`, snapshot create and archive trigger, and SDK and `sentry api` writes. ## Tests - The POST-retry body-cloning tests now go through the 429 and opt-in paths. - New cases: - a rate-limited POST is resent with the same body - a 501 is not retried - a 503 on a non-idempotent method is not retried - the reset-header and `Retry-After` wait, clock skew, and the cap leading to `RateLimitError` - the exact rate-limit message text - `api-client.coverage` used 429 as a sample error and now uses 400. ## Checks - `tsc --noEmit` and `pnpm run lint` pass. - The sentry-client, retry-policy, api-client and api-client.coverage tests pass: 204/204. - Full CLI suite on `bf3b710`: 11,482 passed and 6 failed. The same 6 fail on `main` (`37c7193`) in this sandbox. They're chmod-based permission tests (completions ×2, code scanner, init file-changes, telemetry readonly, upgrade lock), and the sandbox runs with full capabilities, so they can't fail the way they expect. - After the 429-any-method change (`7099e2b`), all other CLI test files that mention 429 pass (1,195), apart from the 2 known sandbox permission failures. ## Relation to #1669 This matches the retry policy in #1669 (task 5.0 and the `sentry-api-transport` spec). Task 5.1 will move this behavior into toolkit-core's shared transport unchanged. <!-- 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: Burak Yigit Kaya <126780+BYK@users.noreply.github.com>
Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK
added a commit
that referenced
this pull request
Oct 27, 2026
The CLI's fallback "immutable" cache tier kept any `/events/<id>/`, `/trace/<id>/`, or `/trace-items/<id>/` response for 24 hours. It was also checked before the `/issues/` volatile pattern, so `getLatestEvent` (`/issues/<id>/events/latest/`) was cached for a day: re-running `sentry issue view` an hour later could show an outdated "latest" event. The getsentry/sentry#128818 spike ([findings](getsentry/sentry#128818 (comment))) found that none of these responses are actually immutable. Event payloads embed `nextEventID`/`previousEventID`, user reports, and release data, reprocessing and merges can change them for the same ID, `latest`/`oldest`/`recommended` resolve to different events over time, and full traces gain spans as they arrive. This removes the `immutable` tier: | Tier | TTL | Matches | | --- | --- | --- | | no-cache | 0 | autofix and root-cause polling (unchanged) | | volatile | 60 s | `/issues/` (now including issue events), project event details, full traces, `dataset=logs\|spans`, trace logs | | stable | 5 m | everything else, now including organization event details and trace-item details | Server `Cache-Control` headers still take precedence, so when getsentry/sentry#128828 ships its `private, max-age=300` headers the CLI follows them without a release. A TODO points at getsentry/sentry-api-schema#81 and #102 for classifying by generated `@sentry/api` operation metadata instead of URL regexes. Notes for review: - **No migration.** Freshness is recomputed from `createdAt` and the current tier on read, so existing 24-hour entries on disk are treated by the new rules immediately. - **`immutableMinTimeToLive` removed.** It pointed at the deleted tier constant. The `http-cache-semantics` default is the same 24 hours, and it only applies to responses where the server sends `immutable`. - **Cost:** repeated reads of project/issue events and full traces will hit the API after 60 seconds instead of 24 hours; organization event and trace-item details after 5 minutes. Task group 2 of the `unify-sentry-api-layer` OpenSpec change in #1669. **Checks run locally:** - New `classifyUrl` cases plus fake-clock expiry tests for org event details (hit at 4 min, miss at 6 min), issue latest event, project event details, full traces, and server `max-age` overriding the fallback. 8 of them fail against the old classifier. - `pnpm exec vitest run test/lib/response-cache*.test.ts`: 60 passed - CLI `typecheck`, `lint`, and `oxlint`/`oxfmt` on the changed files: clean - `pnpm run test:unit` (first revision): 11,583 passed, 6 failed. The same 6 fail on unmodified `main` in my sandbox: filesystem-permission tests (`completions`, `code-scanner`, `file-changes`, `telemetry` readonly DB, `upgrade` lock) that depend on the environment, not this change. <!-- junior-session-footer:start --> <!-- junior-conversation-id:local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf --> -- [View Junior Session in Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A0ab4fc2bd5a2e23f2c25efcf/?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>
…tead Matches #1681 as merged: the CLI has no separate detail tier, since it would share the stable TTL. R2 eligibility on the hosted MCP becomes an explicit URL allowlist owned by that store. Mark task group 2 done. Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK
added a commit
that referenced
this pull request
Oct 29, 2026
Follow-up to #1677 and #1673, and the last part of #1669 task 5.0. The CLI's interim `retry-policy.ts` duplicated `@sentry/toolkit-core/api-retry`, with small differences between the two. The CLI now uses the shared module, so the CLI and MCP have one rule for which statuses and methods get retried and how long a 429 waits. Each still sets its own limits: the CLI's `RETRY_POLICY` keeps 2 retries, a 1s base and a 10s per-wait cap, and adds a 30s total. Changes in `toolkit-core/src/api-retry.ts`: - New export `isIdempotentRequest(method, idempotent)`. The CLI uses it for the timeout error wording and for `generate-api-schema`'s idempotent-operation filter. - `nextRetryDelayMs` now accepts `status: undefined` for a network error or timeout, which is retried only for idempotent requests. The MCP always passes a status, so its behavior is unchanged. Behavior changes in the CLI, all from taking on the shared rule: - **408 is no longer retried.** The shared list is 429/500/502/503/504, as #1669's 5.0 spec says. Sentry doesn't send 408 itself. - **Clock skew:** with a skewed local clock, the wait is now clamped into the 1s window allowed by the response `Date` header, instead of being measured from `Date`. One `sentry-client` test asserted the old ≥900ms wait; it now asserts that the wait stays inside that window and not the hour of skew. - **Retry-After:** when both `Retry-After` and the rate-limit reset are present, `Retry-After` wins. Before, the CLI took the longer of the two. - **Jitter:** a server-requested wait gets up to `baseDelayMs / 2` of extra delay. Backoff without a server hint works the same as before. Two things stay the same: 401 token refresh still works for any method, and writes that Sentry marks idempotent (#1706) still retry. <!-- junior-request-attribution:start --> via **BYK**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:slack%3AC08J1NSPU6S%3A1793057920.879309 --> -- [View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3AC08J1NSPU6S%3A1793057920.879309) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC08J1NSPU6S%3A1793057920.879309/?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>
sentry-junior Bot
added a commit
that referenced
this pull request
Oct 29, 2026
Move the CLI's request loop from sentry-client.ts into @sentry/toolkit-core/api-transport: the Authorization header, the 401 refresh (any method, once), per-attempt timeouts, retries through api-retry, request-body replay, validated redirect following with origin provenance, and GET cache lookup, revalidation, storage, and post-write invalidation. The host injects a CredentialProvider, header rules, a cache, its error classes, a logger, and a span hook, so the module stays free of Node APIs. The CLI now configures the transport with its stored credentials, host-scope checks, disk cache, telemetry span, and error classes. Implements #1669 tasks 5.1 and 5.2. Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK
added a commit
that referenced
this pull request
Oct 29, 2026
Moves the CLI's request loop out of `packages/cli/src/lib/sentry-client.ts` and into a new Workers-safe module, `@sentry/toolkit-core/api-transport`, then switches the CLI over to it. This covers tasks **5.1** and **5.2** of #1669 (`unify-sentry-api-layer`). It builds on the shared retry policy from #1710. ### What's now shared `createApiTransport` returns a fetch-shaped `request()` that owns: - **Auth**: it sets the `Authorization` header from an injected `CredentialProvider`. The provider supplies the current credential, an optional refresh before expiry, and the refresh after a 401. A 401 refreshes once and replays the request for any method. - **Retries** through `api-retry`. A 429 retries for any method. A 500/502/503/504, a timeout, or a network error retries only for GET/HEAD/OPTIONS, or when the request is marked `idempotent`. Waits follow `Retry-After` or `X-Sentry-Rate-Limit-Reset`, under the caller's caps. - **Per-attempt timeouts**: a fixed value or a function of the URL, so the CLI keeps `ENDPOINT_TIMEOUT_OVERRIDES`. - **Body replay** on every attempt, stream bodies included, plus an optional buffered-body mode (`auto` / `buffer` / `stream`). In `buffer` mode, a body that fails mid-read can still be retried. - **Validated redirects**: it follows redirects by hand, sends the credential only to origins the host trusts, and records where the response came from (`getValidatedResponseSource`). - **Cache port** (`TransportCache`): it looks up GETs, revalidates stale entries with their `ETag`, stores successful GETs, and invalidates entries after a successful write. The host plugs in header rules (which may reject a request), its error classes for timeouts, rate limits, and fatal fetch errors (such as TLS failures), a logger, and a span hook. The module uses no Node APIs. ### CLI wiring `sentry-client.ts` now just configures the transport. It passes in its stored credentials and refresh logic, its host-scope and claim checks (`prepareHeaders`), the custom TLS fetch, the disk response cache, its tracing span, and its existing error classes. It keeps `RETRY_POLICY`, `ENDPOINT_TIMEOUT_OVERRIDES`, and the `x-sentry-cli-retry` marker. `getResponseRequestOrigin` and `getResponseCredentialIdentity` now read the transport's provenance. `fetch-input.ts` re-exports `extractInputUrl` from the shared module. ### Behavior changes The goal is the same CLI behavior. One intentional difference: - An unused response body (one dropped before a retry, a refresh, or a redirect hop) is now cancelled without awaiting the cancel. When a stream is teed, cancelling one branch only settles after the other branch is read. Interceptors such as MSW hand out teed streams, so the old awaited cancel could hang a retry indefinitely. A regression test covers this. ### Stacking (#1669) - Done: 5.0 shared retry policy (#1673, #1710). - This PR: 5.1 shared transport and 5.2 the CLI switch. - Next, stacked on this branch and opened after it merges: - 5.3: a request-scoped fresh-read bypass. - 6.1: `fetch` injection for the MCP. `SentryApiService` drops its own retry loop so retries don't stack. The OpenSpec tasks file is unchanged here. ### Verification - `toolkit-core`: 21 files, 269 tests pass, including the new `api-transport.test.ts`. It covers the retry scenarios, 401 refresh, timeouts, fatal errors, header rules, redirects, the cache, and the teed-body regression. - CLI unit tests: 11,735 pass. 6 fail, and those same 6 fail on clean `main` in this sandbox (file-permission tests in completions, telemetry, upgrade, code-scanner, file-changes). - `tsc --noEmit` passes for `toolkit-core` and `cli`. `oxlint`, `oxfmt --check`, and `ast-grep scan` are clean on the changed paths. <!-- junior-request-attribution:start --> via **BYK**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3Aa6eddc0161cff61914454da9f74fcdf1 --> -- [View Junior Session in Sentry](https://sentry.sentry.io/explore/conversations/agent%3Aa6eddc0161cff61914454da9f74fcdf1/?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>
sentry-junior Bot
added a commit
that referenced
this pull request
Oct 29, 2026
Add a `fresh` hook to the API transport. While it returns true, GETs skip the cache lookup and go to the network, and the response still replaces the cached entry, so later reads see it. Hosts back the hook with request-scoped state, such as AsyncLocalStorage around a tool call. Implements #1669 task 5.3. Co-Authored-By: BYK <burak.kaya@sentry.io>
5.3 is in review in #1720, and 6.1 is stacked on it. Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK
added a commit
that referenced
this pull request
Oct 29, 2026
This adds a request-scoped fresh-read bypass to the shared API transport in `@sentry/toolkit-core/api-transport`. It is task **5.3** of #1669 (`unify-sentry-api-layer`) and builds on the shared transport from #1719. ### What's shared `createApiTransport` accepts a new optional `fresh?(): boolean` hook. While the hook returns true: - GETs skip the cache lookup and go to the network. - The response is still stored, so it replaces the cached entry and later reads see the new data. A host backs the hook with request-scoped state, for example `AsyncLocalStorage` around a single MCP tool call that asked for `fresh: true`. That keeps the bypass to one call rather than the whole process. This is the transport side of the MCP's `fresh` input planned in 6.3. ### Behavior changes None for current callers. The hook is optional, and no host sets it yet. The CLI's `--fresh` flag keeps its existing process-wide bypass in `cli/src/lib/response-cache.ts`, with reads skipped and writes kept as before. This PR leaves that unchanged. ### Relation to #1669 - Done: 5.0 shared retry policy (#1673, #1710), and 5.1 + 5.2 shared transport with the CLI switch (#1719). - This PR: 5.3. - Next, stacked on this branch and opened once it merges: 6.1. That adds `fetch` to `McpServerOptions` and routes all `SentryApiService` and `@sentry/api` requests through it. `SentryApiService` drops its own retry loop, so retries happen in the shared transport only. ### Verification - New transport test `skips the cache in a fresh scope and replaces the entry`. It stores v1, then reads in a fresh scope and gets v2 from the network. Once the fresh scope ends, the next read is a cache hit for v2. - `toolkit-core`: 21 files and 270 tests pass. - CLI request, cache and API tests (`test/lib/sentry-client*`, `test/lib/response-cache*`, `test/lib/api`): 49 files and 1,049 tests pass. - `tsc --noEmit` passes for `toolkit-core` and `cli`. - `oxlint`, `oxfmt --check` and `ast-grep scan` are clean on the changed files. <!-- junior-request-attribution:start --> via **BYK**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3Aa6eddc0161cff61914454da9f74fcdf1 --> -- [View Junior Session in Sentry](https://sentry.sentry.io/explore/conversations/agent%3Aa6eddc0161cff61914454da9f74fcdf1/?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>
sentry-junior Bot
added a commit
that referenced
this pull request
Oct 29, 2026
Add `fetch` to McpServerOptions and ServerContext. SentryApiService sends every request through it, including @sentry/api operations, so a host can supply its authenticated transport. Without one, requests go through the shared @sentry/toolkit-core/api-transport with REQUEST_RETRY_POLICY. SentryApiService drops its own retry loop, so retries happen in one layer and do not stack. ApiRateLimitError still takes retryAfterMs from the final response's headers, and an abort during a retry wait keeps the caller's reason. The transport's credentials and timeouts become optional: without a CredentialProvider it keeps the request's Authorization header, never refreshes, and skips the cache. Implements #1669 task 6.1. Co-Authored-By: BYK <burak.kaya@sentry.io>
This was referenced Oct 29, 2026
BYK
added a commit
that referenced
this pull request
Oct 29, 2026
Bumps `@sentry/api` from 0.423.0 to 0.425.0, the latest published version. It changes the catalog entry and the `minimumReleaseAgeExclude` entry in `pnpm-workspace.yaml`, plus the lockfile. ### What changes upstream - **Pagination continuations** (sentry-api-schema#115, #116): - `paginateUpTo` now returns a serializable `continuation`, and accepts `resume`, so a batch can pick up inside a partly consumed page. - `consumePage` and `CollectionPageFetcher` let a caller reuse a page it already fetched. - `BudgetedPageFetcher`'s second parameter is now the request budget. On a replay it repeats the original budget. - **New module:** `source-analysis`, which brings a new optional `typescript` peer. The lockfile resolves it to the existing `typescript@5.9.3`. - Everything else is additive: - `types.gen.d.ts` is unchanged. - No exports were removed. ### Impact here - **Nothing in this repo calls the changed helpers yet.** That covers `paginateUpTo`, `paginateAll`, `consumePage` and `fetchPage`. `unwrapPaginatedResult` is used in `cli/src/lib/api/alerts.ts` and `dashboards.ts`, and its signature is unchanged. No code changes were needed. - **toolkit-core's own pagination** (`paginate` and `fetchAllPages` in `toolkit-core/src/pagination.ts`) is a separate implementation and isn't affected. - It already resumes inside a page by writing the consumed rows into the cursor's `is_prev` field. That keeps cursors as plain Sentry cursor strings, which the API and older CLIs still accept. - Moving it onto `@sentry/api`'s object-based continuation is a possible follow-up for #1669. It would mean deciding how to serialize the continuation for the CLI's `--cursor` and the MCP's cursor inputs, so I left it out of this bump. ### Verification - `pnpm install --frozen-lockfile` is clean. - `tsc --noEmit` passes for `toolkit-core`, `mcp-core`, `mcp-server`, `mcp-cloudflare` and `cli`. That includes the CLI's regenerated SDK, which produces no diff. - `toolkit-core`: 270 tests pass. `mcp-core`: 1,885 pass, 6 skipped. - CLI unit tests: 11,760 pass. 7 fail in this sandbox: - 6 are the file-permission tests that also fail on clean `main` here (completions, telemetry, upgrade, code-scanner, file-changes). - The 7th, `completions.property`, failed because the gitignored `completion-targets.json` was stale after #1636. It passes after `generate:completions`, and CI regenerates that file. The lockfile needed one workaround. pnpm checks the existing lockfile entries against `minimumReleaseAge` before it resolves anything. 0.423.0 is under the cutoff, and its exclusion is dropped in this PR, so a plain `pnpm install` refused to run. I removed the old `@sentry/api` entries and resolved again. The lockfile diff only swaps 0.423.0 for 0.425.0, with the published integrity hash. <!-- junior-request-attribution:start --> via **BYK**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3Aa6eddc0161cff61914454da9f74fcdf1 --> -- [View Junior Session in Sentry](https://sentry.sentry.io/explore/conversations/agent%3Aa6eddc0161cff61914454da9f74fcdf1/?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>
sentry-junior Bot
added a commit
that referenced
this pull request
Oct 29, 2026
Add `fetch` to McpServerOptions and ServerContext. SentryApiService sends every request through it, including @sentry/api operations, so a host can supply its authenticated transport. Without one, requests go through the shared @sentry/toolkit-core/api-transport with REQUEST_RETRY_POLICY. SentryApiService drops its own retry loop, so retries happen in one layer and do not stack. ApiRateLimitError still takes retryAfterMs from the final response's headers, and an abort during a retry wait keeps the caller's reason. The transport's credentials and timeouts become optional: without a CredentialProvider it keeps the request's Authorization header, never refreshes, and skips the cache. Implements #1669 task 6.1. Co-Authored-By: BYK <burak.kaya@sentry.io>
Step 1 consolidates the MCP's SentryApiService and the CLI's API modules into one browser-safe client in toolkit-core, in one PR. Step 2 moves it into @sentry/api. The host-supplied MCP fetch and sentry mcp on the CLI session move to a deferred phase. Co-Authored-By: BYK <burak.kaya@sentry.io>
The shared client takes host-supplied Standard Schema validators, so the MCP keeps Zod and the CLI keeps valibot, and shared code depends on neither at runtime. Co-Authored-By: BYK <burak.kaya@sentry.io>
Dependencies run cli → mcp-core → toolkit-core, and mcp-cloudflare → mcp-core; mcp-core never imports the CLI. The MCP shares the CLI SDK's operations by moving them into the shared client, and the CLI's host-only capabilities become optional interfaces that `sentry mcp` injects. Task group 11 picks this up, alongside the @sentry/mcp-server retirement and its lint and manifest cycle checks. Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK
added a commit
that referenced
this pull request
Oct 29, 2026
This PR sends every MCP request to the Sentry API through the shared transport, and moves retries out of `SentryApiService` into it. It is task **6.1** of #1669 (`unify-sentry-api-layer`) and builds on the shared transport (#1719) and the fresh-read hook (#1720). ### What changed - **One transport:** `SentryApiService` sends every request through `createApiTransport` from `@sentry/toolkit-core/api-transport`, using the MCP's existing `REQUEST_RETRY_POLICY`. That includes `@sentry/api` operations, which already go through `request()`. - **One retry layer:** `SentryApiService` no longer has its own retry loop or network-failure bookkeeping. The shared transport retries a 429 for any method, and a 500/502/503/504 or a network failure for GET and HEAD. `SentryApiService` still parses the final error response, so `ApiRateLimitError.retryAfterMs` is still read from `Retry-After` or `X-Sentry-Rate-Limit-Reset`. - **Transport options:** `credentials` and `timeoutMs` on `createApiTransport` are now optional. Without a `CredentialProvider`, the transport keeps the `Authorization` header the request already has, never refreshes, and skips the cache, because cache entries are tied to a credential. The MCP uses it this way, and still sets no timeout, as before. - **Abort reason:** an abort while a request is in flight now rejects with the caller's `signal.reason`, instead of being treated as a retryable network failure. An earlier revision also added a host-supplied `fetch` option to `McpServerOptions` and `ServerContext`. That is gone: per the revised #1669 plan, the shared API layer moves into toolkit-core and then `@sentry/api`, and the CLI passing its fetch to `sentry mcp` is deferred. ### Intentional behavior changes - **`allowStatuses`:** a status a caller lists there is now returned only after the transport has finished retrying it. Before, it came back on the first attempt. Today's callers only list 404 and 400, which are never retried, so they see no difference. - **Abort during a retry wait:** if the caller's signal aborts while the transport is waiting to retry, the abort error is rethrown as-is. It is no longer wrapped as a connection failure. ### Relation to #1669 - Done: 5.0 (#1673, #1710), 5.1 and 5.2 (#1719), 5.3 (#1720). - This PR: 6.1. - Next: consolidate the MCP's `SentryApiService` and the CLI's API modules into one browser-safe API client in toolkit-core, stacked on this PR. ### Verification - New `mcp-core` tests: - A GET that is rate-limited on every attempt reaches the API at most `maxRetries + 1` times. - An in-flight abort rejects with the caller's reason, after one attempt. - New `toolkit-core` tests: - Without credentials, the request's own `Authorization` is kept. - Without credentials, the cache is never consulted. - An in-flight abort keeps the caller's reason. - Suites: | Package | Result | | --- | --- | | `toolkit-core` | 273 pass | | `mcp-core` | 1,899 pass, 6 skipped | | `mcp-server` | 87 pass | | `mcp-cloudflare` | 443 pass | - `tsc --noEmit` passes for `toolkit-core`, `mcp-core`, `mcp-server`, `mcp-cloudflare`, and `cli`. `oxlint` and `oxfmt --check` are clean on the changed files. - Test changes: - Hand-built response mocks in `client.test.ts` gained a `status`, because the transport reads it to decide on a retry. - Header assertions in `client.test.ts` and `mcp-handler.test.ts` now read through `Headers`, because the transport passes a `Headers` object. <!-- junior-request-attribution:start --> via **BYK**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3Aa6eddc0161cff61914454da9f74fcdf1 --> -- [View Junior Session in Sentry](https://sentry.sentry.io/explore/conversations/agent%3Aa6eddc0161cff61914454da9f74fcdf1/?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
that referenced
this pull request
Oct 29, 2026
…se reading (#1732) Starts the shared Sentry API client in `toolkit-core/src/api-client/` (step 1 of #1669, task group 8). The MCP and the CLI both use it for errors, API paths, and response reading. It is the first of a stack of PRs by layer (see "Next" below). ## What changes - **Portability guard (8.1).** Lint bans `node:*` imports and Node globals such as `Buffer` and `process` in `toolkit-core`. A second typecheck (`tsconfig.browser.json`) checks it against the DOM library with no Node types, and `pnpm run tsc` runs both. The `node:zlib` cache codec is the one exception. Its types move to `response-cache/body.ts`, so the policy and store modules stay portable. - **Standard Schema validation.** `validateResponse` takes any Standard Schema validator: Zod from the MCP, valibot from the CLI. `toolkit-core` depends only on `@standard-schema/spec` types. Lint bans `zod` and `valibot` imports in its source; they are dev dependencies for the tests only. - **Shared errors and `apiPath`.** `ApiError`, its status subclasses, `createApiError`, `UserInputError`, `ConfigurationError`, and `apiPath` move from `mcp-core` to `@sentry/toolkit-core/api-client/*`. `mcp-core/src/errors.ts` re-exports the two general errors, so its importers don't change. - **Shared response reading.** `readErrorResponse` reads a failed response once: its text, parsed JSON, string `detail`, whether it is an HTML page, and the rate-limit wait. `readJsonResponse` is the MCP's Content-Type-checked JSON parse. - `SentryApiService` uses both. - The CLI's `throwRawApiError` uses `readErrorResponse`. - Each product still maps the result to its own errors (MCP `ApiError` subclasses, CLI exit-coded `ApiError`). - **mcp-core builds ESM only.** Once the errors moved, the CJS build hit a rolldown "canonical name not found" panic. Nothing loads that CJS output: - `@sentry/mcp-core` is `private` and `"type": "module"`. - All 29 of its exports point at `.js`, with no `require` condition. - Every consumer is in the workspace: `mcp-server` bundles it as a devDependency through those `.js` exports, `mcp-cloudflare` bundles it with Vite, and `mcp-test-client` and `mcp-server-evals` are private ESM packages. - No source requires it or reads its `.cjs` files. `@sentry/mcp-server`'s own CJS and ESM output is unchanged. I tried upgrading tsdown first, to 0.15.12, 0.18.4, 0.20.3, 0.22.14, and 0.23.4. Every version broke the `mcp-server-mocks` build: 0.15.12 failed declaration output (`members is not iterable`), and 0.18.4 and later hit a rolldown JSON parse "recursion limit exceeded" on `src/fixtures/trace.json`. 0.18.4 and 0.20.3 also failed the `mcp-core` and `mcp-server` builds. Bundling toolkit-core into the dist (`noExternal`) would have duplicated the error classes across dists and broken `instanceof` between packages. - **Docs.** The `toolkit-core` README now has an "API client" section. `api-patterns.md`, `error-handling.md`, and the logging skill point at the new error module. ## Behavior changes - **MCP, unreadable error body.** If the body of a failed response cannot be read, the caller now gets an `ApiError` for the status instead of the raw stream error. - **MCP, "no string detail" warning.** The `validationErrorMessage` on the "Failed to parse validated API error response" warning is now a fixed string instead of Zod's message. `ApiErrorSchema` is deleted; nothing else used it. Messages, details, `retryAfterMs`, and the CLI's error detail are otherwise unchanged. ## Next This is the bottom of a stack of PRs by layer for the rest of #1669 group 8. Each one is based on the PR below it: 1. Move pagination, cache policy, retry, and transport under `api-client/`. 2. Shared endpoint wrappers on top of `@sentry/api`, with `SentryApiService` switched onto them. 3. The CLI host seam, with the CLI's `lib/api/` wrappers switched onto the shared client. 4. Merge the wrappers both products share. This PR does not touch `packages/mcp-server`, `packages/cli/src/lib/mcp.ts`, or the evals stdio entry; the `@sentry/mcp-server` retirement PR owns those. ## Commits Each codemod run is a generated commit whose message gives the command, and each hand fix is a separate commit: 1. `13053adab` Portability guard (hand) 2. `f13df0d24` Standard Schema validation (hand) 3. `bea21d648` mcp-core ESM only (hand) 4. `0d083bdf3` Move errors and `apiPath` (**generated**, `move-shared-errors.mjs`) 5. `4ddc54813` Move the `client.ts` request-path test back next to `client.ts` (hand) 6. `143da6e39` Shared response reading (hand) 7. `b776eb55b` MCP JSON parsing on the shared reader (**generated**, `use-shared-json-response.mjs`) 8. `44976334d` MCP error responses on the shared reader (hand) 9. `7027a482d` CLI raw error responses on the shared reader (hand) 10. `bf0b248ac` Docs (hand) ## Codemods The scripts are not in the tree. To re-run one, check out the parent of its generated commit, run the script from a scratch directory with `ts-morph@28.0.0` installed, then run oxfmt on the changed files. The output should match the commit. <details> <summary><code>move-shared-errors.mjs</code> (commit 0d083bd)</summary> ```sh node move-shared-errors.mjs <repo-root> oxfmt <added and modified files> ``` ```js // Codemod: move the MCP's API error classes and API path helper into // toolkit-core's shared api-client, and rewrite every importer. // // Usage: node move-shared-errors.mjs <toolkit-repo-root> // (run from a directory where `ts-morph@28.0.0` is installed) import { realpathSync } from "node:fs"; import path from "node:path"; import { Project } from "ts-morph"; const root = path.resolve(process.argv[2] ?? "."); const mcpSrc = path.join(root, "packages/mcp-core/src"); const coreSrc = path.join(root, "packages/toolkit-core/src"); // Classes the shared client throws, moved out of mcp-core's general errors. const MOVED_CLASSES = ["UserInputError", "ConfigurationError"]; // mcp-core files that move into toolkit-core/src/api-client/. const MOVED_FILES = [ "api-client/errors.ts", "api-client/api-path.ts", "api-client/api-path.test.ts", ]; const project = new Project({ tsConfigFilePath: path.join(root, "packages/mcp-core/tsconfig.json"), }); project.addSourceFilesAtPaths(path.join(coreSrc, "**/*.ts")); // 1. Move the classes into the API errors module; mcp-core/src/errors.ts // re-exports them so its existing importers keep working. const generalErrors = project.getSourceFileOrThrow( path.join(mcpSrc, "errors.ts"), ); const apiErrors = project.getSourceFileOrThrow( path.join(mcpSrc, "api-client/errors.ts"), ); const classTexts = MOVED_CLASSES.map((name) => { const declaration = generalErrors.getClassOrThrow(name); const text = declaration.getFullText().trim(); declaration.remove(); return text; }); apiErrors.insertText(0, `${classTexts.join("\n\n")}\n\n`); generalErrors.insertExportDeclaration(0, { namedExports: [...MOVED_CLASSES].sort(), moduleSpecifier: "./api-client/errors", }); // 2. Move the files. ts-morph rewrites relative imports in and to them. const movedPaths = new Set(); for (const file of MOVED_FILES) { const sourceFile = project.getSourceFileOrThrow(path.join(mcpSrc, file)); sourceFile.move(path.join(coreSrc, "api-client", path.basename(file))); movedPaths.add(sourceFile.getFilePath()); } // Moved files take the moved classes from the API errors module directly, // not through mcp-core's re-export. for (const movedPath of movedPaths) { const sourceFile = project.getSourceFileOrThrow(movedPath); for (const declaration of sourceFile.getImportDeclarations()) { if (declaration.getModuleSpecifierSourceFile() !== generalErrors) continue; const names = declaration.getNamedImports().map((n) => n.getName()); if (names.every((name) => MOVED_CLASSES.includes(name))) { declaration.setModuleSpecifier(apiErrors); } } } // 3. Normalize specifiers that point into toolkit-core: relative with `.js` // inside toolkit-core, `@sentry/toolkit-core/<path>` everywhere else. function realPath(file) { try { return realpathSync(file); } catch { return file; } } for (const sourceFile of project.getSourceFiles()) { const importer = sourceFile.getFilePath(); const declarations = [ ...sourceFile.getImportDeclarations(), ...sourceFile.getExportDeclarations(), ]; for (const declaration of declarations) { const target = declaration.getModuleSpecifierSourceFile(); if (!target) continue; const targetPath = realPath(target.getFilePath()); if (!targetPath.startsWith(`${coreSrc}/`)) continue; // Leave specifiers the codemod did not affect as they are. if (!movedPaths.has(importer) && !movedPaths.has(targetPath)) continue; const withoutExt = targetPath.replace(/\.ts$/, ""); let specifier; if (importer.startsWith(`${coreSrc}/`)) { specifier = path.relative(path.dirname(importer), withoutExt); if (!specifier.startsWith(".")) specifier = `./${specifier}`; specifier = `${specifier}.js`; } else { specifier = `@sentry/toolkit-core/${path .relative(coreSrc, withoutExt) .replace(/\/index$/, "")}`; } if (declaration.getModuleSpecifierValue() !== specifier) { declaration.setModuleSpecifier(specifier); } } } await project.save(); ``` </details> <details> <summary><code>use-shared-json-response.mjs</code> (commit b776eb5)</summary> ```sh node use-shared-json-response.mjs <repo-root> oxfmt packages/mcp-core/src/api-client/client.ts ``` ```js // Codemod: replace SentryApiService's private parseJsonResponse with the // shared readJsonResponse from toolkit-core, and delete the private copy. // // Usage: node use-shared-json-response.mjs <toolkit-repo-root> // (run from a directory where `ts-morph@28.0.0` is installed) import path from "node:path"; import { Project, SyntaxKind } from "ts-morph"; const root = path.resolve(process.argv[2] ?? "."); const clientPath = path.join(root, "packages/mcp-core/src/api-client/client.ts"); const project = new Project({ skipAddingFilesFromTsConfig: true }); const client = project.addSourceFileAtPath(clientPath); const service = client.getClassOrThrow("SentryApiService"); // 1. `this.parseJsonResponse(x)` -> `readJsonResponse(x)`. const calls = client .getDescendantsOfKind(SyntaxKind.CallExpression) .filter((call) => call.getExpression().getText() === "this.parseJsonResponse") .reverse(); for (const call of calls) { call.getExpression().replaceWithText("readJsonResponse"); } // 2. Delete the private method it replaces. service.getMethodOrThrow("parseJsonResponse").remove(); // 3. Import the shared helper. client.addImportDeclaration({ namedImports: ["readJsonResponse"], moduleSpecifier: "@sentry/toolkit-core/api-client/response", }); await project.save(); console.log(`rewrote ${calls.length} call sites`); ``` </details> ## Verification At `bf0b248ac`, rebased on `main` after #1722 merged: - `pnpm run tsc` passes. It includes toolkit-core's browser-target typecheck and the CLI typecheck. - `pnpm run lint` passes (oxlint, oxfmt, ast-grep rules), and so does `pnpm run docs:check`. - Tests: toolkit-core 306, mcp-core 1881 (6 skipped), and mcp-cloudflare 443 pass. - Before the rebase, the full `pnpm run test` also passed mcp-server (87), mcp-test-client (80), docs-mcp (49), local (322), and cli-docs. - CLI (`test/lib`, `test/commands`): 11718 pass and 6 fail in 5 files (`completions`, `dsn/code-scanner`, `init/tools/file-changes`, `telemetry`, `upgrade`). - All 6 test permission errors (EACCES/EPERM/read-only). They fail only because the sandbox runs with `CAP_DAC_OVERRIDE`, so a `chmod 000` file stays readable. - This PR touches none of that code; CI is the check for them. - Both generated commits reproduce exactly when their codemod is re-run on the rebased parent. - The diff has no codemod scripts, ast-grep or ts-morph config, or ts-morph dependency. <!-- junior-request-attribution:start --> via **BYK**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3Aa6eddc0161cff61914454da9f74fcdf1 --> -- [View Junior Session in Sentry](https://sentry.sentry.io/explore/conversations/agent%3Aa6eddc0161cff61914454da9f74fcdf1/?project=4510944073809921) <!-- junior-session-footer:end --> --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
This was referenced Oct 29, 2026
… 8.3 and 8.4 Co-Authored-By: BYK <burak.kaya@sentry.io>
BYK
added a commit
that referenced
this pull request
Oct 29, 2026
… api-client (#1736) Moves pagination, the response-cache policy, retry, and transport under `toolkit-core/src/api-client/`, the layout they will have inside `@sentry/api` (#1669, task 8.3). Every importer is rewritten. Nothing else changes. It's the second PR in the group 8 stack. #1732, the foundation, has merged, so this one targets `main`. ## What changes - **Moves.** Only the relative imports inside the moved files change: - `pagination.ts` → `api-client/pagination.ts` - `api-retry.ts` → `api-client/retry.ts` - `api-transport.ts` → `api-client/transport.ts` - `response-cache/{body,index,invalidation,keys,memory-store,policy,store,tiers}.ts` → `api-client/response-cache/` - Their tests move with them. - **Stays put:** `response-cache/codec.ts`. It's the `node:zlib` codec, host storage that won't move into `@sentry/api`. It's still excluded from the browser typecheck, and the `./response-cache/codec` export doesn't change. - **Exports:** `./pagination`, `./api-retry`, `./api-transport`, and `./response-cache` are replaced by the existing `./api-client/*` wildcard plus one `./api-client/response-cache` entry for the directory index. `@sentry/toolkit-core` is `private`, and every consumer is in this workspace and rewritten here. - **Importers.** The codemod rewrites 11 import specifiers across the CLI (including `script/generate-api-schema.ts`), `mcp-core/src/api-client/client.ts`, and toolkit-core. - **Docs:** `api-patterns.md`, the toolkit-core README, and code comments name the new subpaths. ## Behavior changes None. This PR does not touch `packages/mcp-server`, `packages/cli/src/lib/mcp.ts`, or the evals stdio entry. The `@sentry/mcp-server` retirement PR owns those. ## Commits 1. `85fae9151` Move the modules and rewrite importers (**generated**, `move-transport-layer.mjs`) 2. `85df504be` Point comments and docs at the new paths (hand) ## Verification - The generated commit reproduces exactly: check out `origin/main` (`9409280c1`), run the command below, then oxfmt on the changed files. The tree matches `85fae9151`. - `pnpm run tsc` (including toolkit-core's browser typecheck), `pnpm run lint`, and `node scripts/check-doc-links.mjs` pass. - Tests: toolkit-core 306 passed; mcp-core 1881 passed, 6 skipped; mcp-cloudflare 443 passed; mcp-server 87 passed; CLI `test/lib test/commands test/script` 11895 passed, 46 skipped, **6 failed**. - The 6 CLI failures are sandbox-only, and they fail the same way without this change. The sandbox runs with `CAP_DAC_OVERRIDE`, so these permission tests can't make a file unreadable: `completions` (EACCES, EPERM), `dsn/code-scanner` (unreadable file), `init/tools/file-changes` (I/O failure), `telemetry` (read-only DB), and `upgrade` (`acquireLock` permission error). CI runs them normally. ## Codemod The script isn't in the tree. To re-run it, check out the parent of the generated commit and run it from a scratch directory with `ts-morph@28.0.0` installed. Then run oxfmt on the added, modified, and renamed package files. <details> <summary><code>move-transport-layer.mjs</code> (commit 85fae91)</summary> ```sh node move-transport-layer.mjs <repo-root> git add -A -- packages oxfmt $(git diff --cached --name-only --diff-filter=AMR) ``` ```js // Codemod: move toolkit-core's pagination, response-cache policy, api-retry, // and api-transport modules under src/api-client/, and rewrite every // importer and the package's exports. The node:zlib cache codec stays at // src/response-cache/codec.ts as a host-side module. // // Usage: node move-transport-layer.mjs <toolkit-repo-root> // (run from a directory where `ts-morph@28.0.0` is installed) import { readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { Node, Project } from "ts-morph"; const root = path.resolve(process.argv[2] ?? "."); const coreDir = path.join(root, "packages/toolkit-core"); const coreSrc = path.join(coreDir, "src"); // Old path -> new path, relative to toolkit-core/src. const MOVES = new Map([ ["pagination.ts", "api-client/pagination.ts"], ["pagination.test.ts", "api-client/pagination.test.ts"], ["api-retry.ts", "api-client/retry.ts"], ["api-retry.test.ts", "api-client/retry.test.ts"], ["api-transport.ts", "api-client/transport.ts"], ["api-transport.test.ts", "api-client/transport.test.ts"], ]); const CACHE_FILES = [ "body.ts", "index.ts", "invalidation.ts", "invalidation.test.ts", "keys.ts", "keys.test.ts", "memory-store.ts", "memory-store.test.ts", "policy.ts", "policy.test.ts", "policy.property.test.ts", "store.ts", "tiers.ts", "tiers.test.ts", ]; for (const file of CACHE_FILES) { MOVES.set(`response-cache/${file}`, `api-client/response-cache/${file}`); } // Package subpath -> new subpath. `response-cache/codec` does not move. const SPECIFIERS = new Map([ ["@sentry/toolkit-core/api-retry", "@sentry/toolkit-core/api-client/retry"], [ "@sentry/toolkit-core/api-transport", "@sentry/toolkit-core/api-client/transport", ], [ "@sentry/toolkit-core/pagination", "@sentry/toolkit-core/api-client/pagination", ], [ "@sentry/toolkit-core/response-cache", "@sentry/toolkit-core/api-client/response-cache", ], ]); // 1. Move the files inside toolkit-core. ts-morph rewrites relative imports // in and to them; then give every rewritten specifier the `.js` suffix // the package uses. const core = new Project({ tsConfigFilePath: path.join(coreDir, "tsconfig.json"), }); const moved = new Set(); for (const [from, to] of MOVES) { const sourceFile = core.getSourceFileOrThrow(path.join(coreSrc, from)); sourceFile.move(path.join(coreSrc, to)); moved.add(sourceFile.getFilePath()); } for (const sourceFile of core.getSourceFiles()) { const importer = sourceFile.getFilePath(); for (const declaration of [ ...sourceFile.getImportDeclarations(), ...sourceFile.getExportDeclarations(), ]) { const target = declaration.getModuleSpecifierSourceFile(); if (!target || !declaration.getModuleSpecifierValue()?.startsWith(".")) { continue; } const targetPath = target.getFilePath(); if (!moved.has(importer) && !moved.has(targetPath)) continue; let specifier = path.relative( path.dirname(importer), targetPath.replace(/\.ts$/, ""), ); if (!specifier.startsWith(".")) specifier = `./${specifier}`; declaration.setModuleSpecifier(`${specifier}.js`); } } await core.save(); // 2. Rewrite package specifiers everywhere else, including dynamic imports // and vi.mock() calls: any string literal equal to an old subpath. const others = new Project({ skipAddingFilesFromTsConfig: true }); others.addSourceFilesAtPaths([ path.join(root, "{packages,apps}/*/{src,test,script,scripts}/**/*.{ts,tsx,mts}"), `!${path.join(root, "**/node_modules/**")}`, `!${path.join(root, "**/dist/**")}`, `!${path.join(coreDir, "**")}`, ]); let rewritten = 0; for (const sourceFile of others.getSourceFiles()) { let changed = false; sourceFile.forEachDescendant((node) => { if (!Node.isStringLiteral(node)) return; const next = SPECIFIERS.get(node.getLiteralValue()); if (next) { node.setLiteralValue(next); changed = true; rewritten += 1; } }); if (changed) await sourceFile.save(); } // 3. Point the package exports at the new paths. The `./api-client/*` // wildcard covers pagination, retry, and transport; the cache needs its // index. const manifestPath = path.join(coreDir, "package.json"); const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); const exports = {}; for (const [key, value] of Object.entries(manifest.exports)) { if (["./api-retry", "./api-transport", "./pagination", "./response-cache"].includes(key)) { continue; } if (key === "./api-client/*") { exports["./api-client/response-cache"] = { types: "./src/api-client/response-cache/index.ts", default: "./src/api-client/response-cache/index.ts", }; } exports[key] = value; } manifest.exports = exports; writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); console.log(`moved ${moved.size} files, rewrote ${rewritten} specifiers`); ``` </details> <!-- junior-request-attribution:start --> via **BYK**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3Aa6eddc0161cff61914454da9f74fcdf1 --> -- [View Junior Session in Sentry](https://sentry.sentry.io/explore/conversations/agent%3Aa6eddc0161cff61914454da9f74fcdf1/?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>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the OpenSpec change
unify-sentry-api-layer. It covers the proposal, the capability specs, the design, and the tasks. It is docs only; implementation lands in follow-up PRs.What it proposes
/issues/<id>/events/latest/), project events, and full traces. Those change after they are first read, so they drop to 60 seconds; organization event and trace-item details fall to the default 5 minutes. Landed in fix(cli): Stop caching events and traces for 24 hours #1681 (task group 2).toolkit-core, on top of@sentry/api. The CLI and MCP then share one implementation, with credentials, storage, and telemetry supplied by each host. Pagination landed in ref(toolkit-core): Share limit-bounded pagination with the MCP #1683 (task group 3) and the cache policy in ref(toolkit-core): Share the response cache policy with the MCP #1688 (task 4.1), and the body codec in feat(toolkit-core): Add a body codec for cached responses #1695 (task 4.2).SentryApiServicesends every request through the shared transport, with no retry loop of its own (ref(mcp): Send Sentry API requests through the shared transport #1722).SentryApiService-like layer, the MCP'sSentryApiServiceand the CLI's API modules, into one shared client intoolkit-core, in one PR. It covers endpoint wrappers by domain on@sentry/apioperations, errors (ApiError, rate limits,retryAfterMs), Zod validation (@sentry/api/zodwhere it covers the endpoint), pagination, cache policy, retries, and the transport (task group 8).@sentry/api, switch toolkit to it, and delete the toolkit-core copies (task group 9).@sentry/api, the modules that move with it, and web-standard APIs, so it runs in Node, Workers, and browsers, and the Sentry frontend can use it. Credentials, storage, the fresh-read scope, and telemetry are injected by each host. A lint rule and a browser-target typecheck enforce it.sentry mcppassing the CLI's fetch, widening CLI OAuth scopes, deprecating the standalone MCP login, and using the CLI as an SDK from the MCP.private, max-age, for at most 5 minutes. Enabling it in production needs a security sign-off on the stale window after access revocation or deletion.freshinput, like the CLI's--fresh. The request-scoped flag lives in the MCP host, not in shared code.@sentry/apiversion from the catalog.Why not
createSentrySDK()@sentry/mcp-server, so the MCP calling the SDK would create a dependency cycle.Evidence behind the design
mcp-core/src/api-client/client.tshave operations in@sentry/api@0.404.0. This was a pattern match, so treat it as approximate.dictionaryoption, and a dictionary trained on real data embeds verbatim strings, so it must not ship in a public package.node:zlibzstd works in local workerd under the current compatibility date. Task 7.2 confirms it on a deployed preview.immutable, and only organization event details and project trace-item details safe forprivate, max-age=300. Prototype: feat(api): Add opt-in max-age mode to ConditionalGetResponseMixin sentry#128828.@sentry/apiin ref(mcp): Move issue and event reads onto @sentry/api #1713.Checks:
openspec validate unify-sentry-api-layer --strictandoxfmtpass.via BYK.
--
View Junior Session in Sentry