Skip to content

feat(apidocs)!: Declare the API base path in the spec's server URL - #131065

Merged
BYK merged 3 commits into
masterfrom
feat/apidocs-base-path-in-servers
Nov 13, 2026
Merged

BYK merged 3 commits into
masterfrom
feat/apidocs-base-path-in-servers

Conversation

@sentry-junior

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

Copy link
Copy Markdown
Contributor

Depends on: getsentry/sentry-api-schema#148 and getsentry/sentry-docs#20787. Merge both first. openapi.yml syncs this spec to sentry-api-schema on push to master. That repo then auto-releases @sentry/api and cascades into sentry-docs, and both still assume /api/0 in every path until those PRs land.

The public OpenAPI spec's server URL was the bare origin, and every path spelled out /api/0. Clients had to know the prefix, and the API version in it, instead of reading it from the spec. Now the server URL carries the versioned base path and paths are relative to it.

Before:

"servers": [{ "url": "https://{region}.sentry.io", "variables": { "region": { ... } } }],
"paths": { "/api/0/organizations/{organization_id_or_slug}/releases/": { ... } }

After:

"servers": [{ "url": "https://{region}.sentry.io/api/0", "variables": { "region": { ... } } }],
"paths": { "/organizations/{organization_id_or_slug}/releases/": { ... } }

How:

  • SPECTACULAR_SETTINGS["SERVERS"] sets the URL to https://{region}.sentry.io/api/0.
  • A new last postprocessing hook, relative_paths_hook, strips that base path from every path, so the hooks before it still see full paths. A path outside the base path fails the build.
  • The hook also appends the base path to per-endpoint servers (only /seer/models/ today: https://{region}.sentry.io/api/0). Endpoint classes keep naming origins.
  • I didn't use drf-spectacular's SCHEMA_PATH_PREFIX_TRIM: it doesn't trim the APPEND_PATHS we use, and custom_postprocessing_hook matches on full paths.

Versioning: the spec names /api/0 once. A future v1 is a new server entry or spec with /api/1, and downstream clients read the path from servers[0].url. I picked this over an /api/{version} server variable because a v1 overhaul would come with different paths and schemas, which one spec can't describe. Self-hosted stays as before: clients swap the origin and keep the path.

Breaking change for anyone reading the spec: paths no longer start with /api/0, so join them to the server URL. Known consumers:

Nothing else in this repo reads the spec, apart from the openapi.yml sync and the openapi-diff comment workflow. The private investigation schema reuses these settings and now has the same shape; nothing consumes it.

Checks (run locally):

  • make build-api-docs and make build-investigation-api-docs both pass. Against master, the derefed spec differs only in servers, the path keys and the seer op server.
  • pnpm run validate-api-examples passes.
  • pytest tests/apidocs passes (167). Tests changed:
    • RelativePathsHookTest is new.
    • APIDocsTestCase keeps the server URL path when it swaps the origin. Without that, openapi-core can't match any request (ServerNotFound).
    • Two endpoint tests read schema entries by path and now use relative keys.
  • prek and mypy pass on the changed files.

via @BYK.

--

View Session [Sentry]

The OpenAPI server URL was the bare origin, and every path spelled out /api/0, so
clients had to know the prefix, and the version in it, instead of reading it from
the spec. The server URL is now https://{region}.sentry.io/api/0 and paths are
relative to it.

A last postprocessing hook strips the base path, so the hooks before it still see
full paths. It also adds the base path to per-endpoint servers, which name origins.

BREAKING CHANGE: Spec paths no longer start with /api/0; join them to the server
URL. getsentry/sentry-api-schema#148 handles both shapes.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@sentry-junior
sentry-junior Bot requested review from a team as code owners November 13, 2026 12:30
@sentry-junior
sentry-junior Bot requested a review from BYK November 13, 2026 12:30
@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Nov 13, 2026
BYK added a commit to getsentry/sentry-api-schema that referenced this pull request Nov 13, 2026
…148)

**Merge order:**
[#148](#148)
→
[getsentry/sentry-docs#20787](getsentry/sentry-docs#20787)
→
[getsentry/sentry#131065](getsentry/sentry#131065)
→ the getsentry/toolkit bump, after `@sentry/api` 2.0.0 is published.
Docs has to merge before sentry#131065. When that merges, the spec sync
commits the new spec to sentry-api-schema, which auto-releases
`@sentry/api` and auto-bumps the schema in sentry-docs. Without
sentry-docs#20787 merged first, the docs would drop `/api/0` from paths
and curl examples. This PR supersedes #147, which hard-coded
`API_PREFIX`.

Per @BYK, the server decides the base path. `/api/0` comes from the
spec's server URL. It's exported as `API_BASE_PATH`, and every path in
the package is relative to it. Nothing here hard-codes `0`. A future
`/api/1` spec would produce `API_BASE_PATH = "/api/1"` with no code
change.

**Design**
- `lib/api-base-path.mjs` reads the base path from the path of
`servers[0].url`, makes `paths` relative, and trims server URLs to their
origin. That includes the per-operation override on `listSeerModels`.
- **Until the sentry PR lands,** the base path is the `/api/<version>`
prefix that every path shares. It's marked TODO. Both spec shapes build
the same `src/`; I checked this with `diff -r`. So the sentry change
only syncs a byte-identical package through the bot.
- **`baseUrl` stays the Sentry origin.** That's what users have,
including self-hosted URLs and `links.regionUrl`. The patched `buildUrl`
builds the request URL as origin + `API_BASE_PATH` + operation path.
- **Exported** from `@sentry/api`, `@sentry/api/client` and
`@sentry/api/operations`.
- **The four `/api/0` copies are gone:** in `core.ts`,
`buildSentryApiUrl`, `parseApiPathScope` and cache invalidation. So are
the copies in the idempotent matcher, `normalize-spec` and
`source-analysis`. Matching still finds the base path anywhere in the
URL, as before.

**Before / After**
```ts
// @sentry/api/operations
{ functionName: "listOrganizations", path: "/api/0/organizations/" }
{ functionName: "listOrganizations", path: "/organizations/" }

import { API_BASE_PATH } from "@sentry/api"; // "/api/0", from servers[0].url

// raw requests: url is relative to API_BASE_PATH
sentry.client.prepareRequest({ method: "GET", url: "/api/0/organizations/" }); // before
sentry.client.prepareRequest({ method: "GET", url: "/organizations/" });       // after
// both send https://sentry.io/api/0/organizations/
```

**Breaking: major bump to 2.0.0**
- `operations[].path` and `IdempotentOperation.path` are relative.
Build-time lists derived from the catalog must be regenerated, such as
toolkit's `idempotent-operations.json`.
- Raw `client.request`, `client.get` and `prepareRequest` take relative
`url`s. An old `/api/0/...` URL now gets a doubled base path.
- Generated function names, types and the request URLs of typed
operations don't change: `sentry-client.gen.ts` is byte-identical.

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

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

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

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3A640c5343d03e547097aa9b8a711daaf5)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A640c5343d03e547097aa9b8a711daaf5/?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>
BYK added a commit to getsentry/toolkit that referenced this pull request Nov 13, 2026
**Depends on:** `@sentry/api` 2.0.0, which is published
([getsentry/sentry-api-schema#148](getsentry/sentry-api-schema#148)).
This is the last step of the chain sentry-api-schema#148 →
[sentry-docs#20787](getsentry/sentry-docs#20787)
→ [sentry#131065](getsentry/sentry#131065) →
this PR.

Sentry's OpenAPI spec is moving `/api/0` out of every path and into its
server URL. `@sentry/api` 2.0 reads the base path from that URL, exports
it as `API_BASE_PATH`, and makes operation paths relative. This PR drops
toolkit's private `/api/0` copies and uses the export instead, so the
version number lives only in the spec.

Before:
```ts
const API_PREFIX = "/api/0";                          // cli/src/commands/api.ts
this.apiPrefix = `${protocol}://${host}/api/0`;       // mcp-core api-client
path: operation.path,                                 // "/api/0/organizations/" in 1.x
```
After:
```ts
import { API_BASE_PATH } from "@sentry/api/client";
this.apiPrefix = `${protocol}://${host}${API_BASE_PATH}`;
path: `${API_BASE_PATH}${operation.path}`,            // still "/api/0/organizations/"
```

- **CLI endpoint index:** it keeps full request paths, so `sentry
schema` and `sentry api` still show and accept `/api/0/...`.
- **Requests:** CLI raw requests now pass relative URLs to
`prepareRequest`.
- **Unchanged on purpose:** user-facing help text and examples still say
`/api/0`.
- **Breaking:** only the `@sentry/api` 1.x → 2.0.0 bump. CLI and MCP
behavior doesn't change, so no toolkit major is needed.

**#2229 (stacked on #1807):** on 2.0 it needs two changes:
- replace its local `API_PREFIX = "/api/0"` in `error-reporting.ts` with
`API_BASE_PATH`;
- build `api-path-templates.json` from `` `${API_BASE_PATH}${op.path}`
``, because `op.path` is now relative.

**Checks:**
- `pnpm run tsc` and `pnpm run lint` pass.
- mcp-core vitest: 2301 passed.
- The CLI api, schema, sentry-client, error-reporting, issue-links and
script tests: 82 files, 1685 passed.
- `pnpm install --frozen-lockfile` passes with the
`minimumReleaseAgeExclude` entry moved from 1.83.0 to 2.0.0.

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

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3A640c5343d03e547097aa9b8a711daaf5)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A640c5343d03e547097aa9b8a711daaf5/?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>
BYK added a commit to getsentry/sentry-docs that referenced this pull request Nov 13, 2026
> **Merge order:**
[getsentry/sentry-api-schema#148](getsentry/sentry-api-schema#148)
→
[#20787](#20787)
→
[getsentry/sentry#131065](getsentry/sentry#131065)
→ the getsentry/toolkit bump, after `@sentry/api` 2.0.0 is published.
Docs has to merge before sentry#131065. When that merges, the spec sync
commits the new spec to sentry-api-schema, which auto-releases
`@sentry/api` and auto-bumps the schema in sentry-docs. Without this PR
merged first, the docs would drop `/api/0` from paths and curl examples.
This PR also works with the current spec.

## DESCRIBE YOUR PR

Sentry's OpenAPI spec is moving `/api/0` out of every path and into its
server URL, so clients read the base path (and the API version in it)
from the spec
([getsentry/sentry-api-schema#148](getsentry/sentry-api-schema#148),
then the getsentry/sentry change). The API reference ignored the
spec-level `servers`, so after that change, pages and curl examples
would drop `/api/0`.

`apiLocation()` joins the server URL's path to each operation path. The
server it shows is only the origin, still `https://sentry.io` by
default, or the operation's own server.

Before, with the new spec shape:
```
GET /organizations/
curl https://sentry.io/organizations/
```
After, with either spec shape:
```
GET /api/0/organizations/
curl https://sentry.io/api/0/organizations/
```

**No visual change, so no screenshots.** I ran `apiCategories()` against
a spec built from getsentry/sentry master and against the same spec
built with the change. The page data was byte-identical, including the
per-operation `https://{region}.sentry.io` server on
`/api/0/seer/models/`.

Checks: the `resolveOpenAPI.spec.ts` vitest suite passes, with one new
table test covering both spec shapes and an operation-level server.
ESLint and Prettier pass on the changed files.

## IS YOUR CHANGE URGENT?

- [ ] Urgent deadline (GA date, etc.): YYYY-MM-DD
- [ ] Other deadline: YYYY-MM-DD
- [x] No deadline: Not urgent, can wait up to 1 week+

It does block the getsentry/sentry spec change from merging.

## PRE-MERGE CHECKLIST

- [ ] Checked Vercel preview for correctness, including links
- [ ] PR was reviewed and approved by any necessary SMEs (subject matter
experts)
- [ ] PR was reviewed and approved by a member of the [Sentry docs
team](https://github.com/orgs/getsentry/teams/docs)

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

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

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3A640c5343d03e547097aa9b8a711daaf5)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A640c5343d03e547097aa9b8a711daaf5/?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>

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we can somehow sync the spectacular settings with the api's mounted django route to prevent any future issues?

Mount sentry.api.urls at API_BASE_PATH and build the spec's server URL from
it, so the two can't drift apart.
@sentry-junior

sentry-junior Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

Wondering if we can somehow sync the spectacular settings with the api's mounted django route to prevent any future issues?

Yes, done in 37d5255. There's now one constant, API_BASE_PATH = "/api/0" in sentry/conf/api_base_path.py, and both sides read it:

  • sentry/web/urls.py mounts sentry.api.urls at path(f"{API_BASE_PATH.removeprefix('/')}/", include(...)), replacing re_path(r"^api/0/", ...).
  • SPECTACULAR_SETTINGS["SERVERS"] builds its URL as https://{region}.sentry.io{API_BASE_PATH}.

The constant lives in sentry.conf because conf/server.py can't import sentry.api, which pulls in the API client. If the two ever drift anyway, relative_paths_hook already fails the build when a path isn't under the server URL's path.

Checks:

  • make build-api-docs gives a byte-identical openapi-derefed.json.
  • /api/0/organizations/ and /api/0/ still resolve to the same views.
  • tests/apidocs and the API index tests pass, and so do prek and mypy.

getsentry mounts its own ^api/0/_admin/... routes separately. Those aren't in the public spec, so I left them alone; they could use the same constant in a follow-up.

path() changed the mount's route string from "^api/0/" to "api/0/", which
broke dev toolbar analytics routes and the generated control silo URLs.
Build the same regex from API_BASE_PATH instead.
@BYK
BYK merged commit bc4d548 into master Nov 13, 2026
97 of 98 checks passed
@BYK
BYK deleted the feat/apidocs-base-path-in-servers branch November 13, 2026 15:03
BYK added a commit to getsentry/sentry-api-schema that referenced this pull request Nov 13, 2026
Main has been red since the spec sync from getsentry/sentry#131065
landed (36d0d11). That sync made spec paths relative to the server URL's
`/api/0`. Two tests still look up or compare full `/api/0` paths, so
Build fails and no `@sentry/api` release goes out until this lands.

- `test/descriptions.test.ts` (from #142):

`spec.paths["/api/0/organizations/{organization_id_or_slug}/dashboards/"]`
→ `spec.paths["/organizations/{organization_id_or_slug}/dashboards/"]`
- `test/docs.test.ts` (from #150, written against the old spec),
comparing the export's paths with the spec's:
`` `${operation.method} ${API_BASE_PATH}${operation.path}` `` → ``
`${operation.method} ${operation.path}` ``

No other test reads `spec.paths` by a full `/api/0` key. The remaining
`/api/0` strings in tests are request URLs, which are still correct.

Checks: `pnpm build`, `pnpm typecheck`, `pnpm test` (681 passed) and
`check-script-modes.sh` pass.

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

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

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3A640c5343d03e547097aa9b8a711daaf5)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A640c5343d03e547097aa9b8a711daaf5/?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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant