Skip to content

fix(cli): Route sentry api org requests to accounts on other servers - #1978

Merged
BYK merged 1 commit into
mainfrom
fix/cli-api-cross-server-routing
Nov 5, 2026
Merged

BYK merged 1 commit into
mainfrom
fix/cli-api-cross-server-routing

Conversation

@sentry-junior

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

Copy link
Copy Markdown
Contributor

Follow-up to #1976 (#1803).

#1976 routes sentry api organizations/<org>/... to the stored account that knows <org>, but the transport picks the account only after the request URL is built. So it could only choose among accounts on that URL's server. For example, with a SaaS login active, a self-hosted org's account was never used.

This picks the account before the URL is built:

  • getDefaultSdkConfig takes an optional path. When the path names an org, it runs selectAccountForOrg with no server limit before reading getApiBaseUrl(). An account on another server brings its server along (followDefaultAccountHost), so the base URL follows it.
  • prepareRawApiRequest passes the endpoint as path when no baseUrl is given.
  • What stays on one server: a full URL passed to sentry api (baseUrl), and an explicit server (SENTRY_URL/SENTRY_HOST, .sentryclirc). For these, the transport's existing same-server routing still applies.

This stays in the CLI rather than @sentry/api. Switching servers changes which origin receives the credential, and the transport intentionally doesn't rewrite a URL it has already built. Picking the server is the CLI's job, the same way resolveOrgRegion already does it for typed commands.

Tests

In account-routing.test.ts, the old "only consider accounts on the request's server" test becomes three:

  • Another server: an org whose account is on another server gets that account's token and URL. This test fails without the fix.
  • Explicit server: an explicit SENTRY_URL keeps the request on that server.
  • Full URL: a full URL (baseUrl) only considers accounts on its server.

pnpm run tsc and pnpm run lint pass. The CLI suite passes except for 6 permission-based tests that also fail on main in my sandbox (completions, upgrade, resilient DB, code-scanner, file-changes).

The docs fragment apps/cli-docs/src/fragments/commands/auth.md is updated to match.

via @BYK.

--

View Session in Sentry

#1976 routed `sentry api organizations/<org>/...` to the stored account
that knows the org, but only among accounts on the server the request URL
already named, so a self-hosted org stayed unreachable while a SaaS login
was active.

Pick the account before the URL is built: `getDefaultSdkConfig` takes the
request path and runs `selectAccountForOrg` without a server limit, so the
base URL follows the picked account's server. A full URL passed to
`sentry api` and an explicit server still keep the request on that server.

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

vercel Bot commented Nov 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Nov 5, 2026 11:16am UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Nov 5, 2026
@BYK
BYK merged commit 3c5cabd into main Nov 5, 2026
27 checks passed
@BYK
BYK deleted the fix/cli-api-cross-server-routing branch November 5, 2026 11:59
BYK added a commit to getsentry/sentry-api-schema that referenced this pull request Nov 5, 2026
…BaseUrl (#130)

This adds a per-request base URL hook to the generated client, so a host
can send each org-scoped request to the org's region or server
**before** the URL is built. Follow-up to #129, which gave
`CredentialProvider.current` the path's scope. That only helps pick a
credential for a URL that's already built.

```ts
const sentry = createSentryClient({
  baseUrl: "https://sentry.io",
  resolveBaseUrl: async ({ baseUrl, scope }) =>
    scope ? await regionUrlFor(scope.organization) : baseUrl,
});
await sentry.listOrganizationIssues({ path: { organization_id_or_slug: "acme" } });
// → https://us.sentry.io/api/0/organizations/acme/issues/
```

## What changes

- **`Config.resolveBaseUrl`** (types `BaseUrlRequest` and
`BaseUrlResolver`, exported from the root). It receives `{ baseUrl, url,
scope? }`:
  - `baseUrl` is the configured base URL, or `''` when none is set.
  - `url` is the built path and query.
- `scope` is `parseApiPathScope(url)`, and is left out for unscoped
paths.

Its result replaces `baseUrl` for that one request, with trailing
slashes removed. It runs once per request, including `prepareRequest`,
after `requestValidator` and before request interceptors. A per-call
`resolveBaseUrl` replaces the configured one. If it throws, the request
rejects with that error unchanged and `fetch` is never called.
- The new step lives in `scripts/generate-base-url-resolver.mjs`. It
patches the pinned Fetch client in the same way
`generate-request-preparation.mjs` does.
- **`parseApiPathScope`** moves to `lib/api-client/api-path-scope.ts`, a
module with no imports, so the generated client can use it without
bundling `errors.ts`. `api-path.ts` re-exports it, so the public API
doesn't change.

The host still owns region discovery, caching, and trust. The client
only asks the host which base URL to use.

## Consumer

The toolkit CLI will use this to replace `resolveOrgRegion` calls at
each call site and the `getDefaultSdkConfig({ path })` workaround from
getsentry/toolkit#1978. I built that change against a local build of
this branch. CLI `tsc`, lint, and the affected tests pass.

## Tests

`test/base-url-resolution.test.ts` covers:
- An org-scoped operation goes to the resolved base URL. The resolver
gets the base URL, path, query, and scope.
- A project scope is passed through. Unscoped paths get no `scope` key.
- Raw `prepareRequest` URLs are resolved, including when no `baseUrl` is
configured.
- Ordering: the resolver runs after validation, before interceptors, and
once per request.
- A per-call resolver overrides the configured one.
- A resolver that throws rejects the request before any fetch.

`pnpm build`, `pnpm typecheck`, and `pnpm test` (632 tests) pass.

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

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

--

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

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

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

This branch was previously deployed

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

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant