Repository navigation
feat(toolkit-core): Share the API transport with the CLI - #1719
Merged
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
BYK
approved these changes
Oct 29, 2026
BYK
enabled auto-merge (squash)
October 29, 2026 12:05
sentry-junior Bot
added a commit
that referenced
this pull request
Oct 29, 2026
5.3 is in review in #1720, and 6.1 is stacked on it. Co-Authored-By: BYK <burak.kaya@sentry.io>
Comment on lines
+478
to
+494
| if (response.status === 401 && attempt < retry.maxRetries) { | ||
| const next = | ||
| refreshed || !credentials.refresh | ||
| ? undefined | ||
| : await credentials.refresh(credential); | ||
| if (!next) { | ||
| return { action: "done", response }; | ||
| } | ||
| refreshed = true; | ||
| credential = next; | ||
| headers.set("Authorization", authorization(next)); | ||
| if (config.refreshedRequestHeader) { | ||
| headers.set(config.refreshedRequestHeader, "1"); | ||
| } | ||
| cancelBody(response); | ||
| return { action: "retry", delayMs: 0 }; | ||
| } |
There was a problem hiding this comment.
401 credential refresh consumes api-retry attempt budget
A 401 refresh is gated on attempt < retry.maxRetries and reuses the same attempt counter as transient retries, so an expired-token refresh leaves one fewer 5xx/timeout retry and starts backoff at the next step. Gate refresh only on !refreshed and keep that replay out of the retry budget.
Evidence
api-retry.tsstates a 401 credential refresh is not a retry; the transport may replay once because the endpoint never ran.onResponseonly refreshes whenattempt < retry.maxRetries, then returns{ action: "retry", delayMs: 0 }, which advances the sharedfor (let attempt = 0; ; attempt++)loop.nextRetryDelayMsstill uses that incrementedattemptfor both the remaining retry count (attempt >= maxRetries) and backoff (baseDelayMs * 2 ** attempt).- With the CLI/MCP default
maxRetries: 2,401 → 503 → 503fails after refresh, while503 → 503 → 200would succeed without the 401.
Identified by Warden · code-review · TTN-XGY
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>
This was referenced Oct 29, 2026
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>
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.
Moves the CLI's request loop out of
packages/cli/src/lib/sentry-client.tsand 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
createApiTransportreturns a fetch-shapedrequest()that owns:Authorizationheader from an injectedCredentialProvider. 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.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 markedidempotent. Waits followRetry-AfterorX-Sentry-Rate-Limit-Reset, under the caller's caps.ENDPOINT_TIMEOUT_OVERRIDES.auto/buffer/stream). Inbuffermode, a body that fails mid-read can still be retried.getValidatedResponseSource).TransportCache): it looks up GETs, revalidates stale entries with theirETag, 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.tsnow 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 keepsRETRY_POLICY,ENDPOINT_TIMEOUT_OVERRIDES, and thex-sentry-cli-retrymarker.getResponseRequestOriginandgetResponseCredentialIdentitynow read the transport's provenance.fetch-input.tsre-exportsextractInputUrlfrom the shared module.Behavior changes
The goal is the same CLI behavior. One intentional difference:
Stacking (#1669)
fetchinjection for the MCP.SentryApiServicedrops 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 newapi-transport.test.ts. It covers the retry scenarios, 401 refresh, timeouts, fatal errors, header rules, redirects, the cache, and the teed-body regression.mainin this sandbox (file-permission tests in completions, telemetry, upgrade, code-scanner, file-changes).tsc --noEmitpasses fortoolkit-coreandcli.oxlint,oxfmt --check, andast-grep scanare clean on the changed paths.via BYK.
--
View Junior Session in Sentry