From f18b7201732d058cf497ff72557bfbd0e36923d4 Mon Sep 17 00:00:00 2001 From: Michael Warkentin Date: Fri, 15 May 2026 16:00:44 -0400 Subject: [PATCH 1/5] feat(api): Use region-specific URLs in API doc curl examples Read the global `servers` field from the OpenAPI spec instead of hardcoding `sentry.io` as the base URL for curl examples. The spec defines `https://{region}.sentry.io` with region enum (us, de), so curl examples now render as `https://us.sentry.io/api/0/...` with a region picker (US | DE tabs) above the code block. For logged-in users, the region is auto-detected from their CodeContext project data so the correct regional endpoint is pre-selected. Refs INC-2335 Co-Authored-By: Claude Co-authored-by: Cursor --- src/build/open-api/types.ts | 12 +++- src/build/resolveOpenAPI.ts | 12 ++-- .../apiExamples/apiExamples.module.scss | 59 ++++++++++++++++ src/components/apiExamples/apiExamples.tsx | 67 ++++++++++++++++++- 4 files changed, 140 insertions(+), 10 deletions(-) diff --git a/src/build/open-api/types.ts b/src/build/open-api/types.ts index 3ed36b1a07cb13..a5983e0933d433 100644 --- a/src/build/open-api/types.ts +++ b/src/build/open-api/types.ts @@ -36,12 +36,20 @@ type Tag = { 'x-sidebar-name': string; }; -type ServerMeta = { - description: string; +export type ServerVariable = { + default: string; + description?: string; + enum?: string[]; +}; + +export type ServerMeta = { + description?: string; url: string; + variables?: Record; }; export type DeRefedOpenAPI = { + servers?: ServerMeta[]; paths: { [key: string]: { [key: string]: { diff --git a/src/build/resolveOpenAPI.ts b/src/build/resolveOpenAPI.ts index 2c32a29c787d4b..2bc8bdae0c3bb9 100644 --- a/src/build/resolveOpenAPI.ts +++ b/src/build/resolveOpenAPI.ts @@ -2,7 +2,7 @@ import {promises as fs} from 'fs'; -import {DeRefedOpenAPI} from './open-api/types'; +import {type DeRefedOpenAPI, type ServerMeta, type ServerVariable} from './open-api/types'; // SENTRY_API_SCHEMA_SHA is used in the sentry-docs GHA workflow in getsentry/sentry-api-schema. // DO NOT change variable name unless you change it in the sentry-docs GHA workflow in getsentry/sentry-api-schema. @@ -75,6 +75,7 @@ export type API = { descriptionMarkdown?: string; requestBodyContent?: any; security?: {[key: string]: string[]}; + serverVariables?: Record; summary?: string; }; @@ -133,17 +134,16 @@ async function apiCategoriesUncached(): Promise { const isDeprecated = isDeprecatedOperationId(apiData.operationId); const cleanOperationId = stripDeprecatedPrefix(apiData.operationId ?? ''); - let server = 'https://sentry.io'; - if (apiData.servers && apiData.servers[0]) { - server = apiData.servers[0].url; - } + const resolvedServer: ServerMeta = apiData.servers?.[0] ?? + data.servers?.[0] ?? {url: 'https://sentry.io'}; apiData.tags.forEach(tag => { categoryMap[tag].apis.push({ apiPath, method, name: cleanOperationId, deprecated: isDeprecated, - server, + server: resolvedServer.url, + serverVariables: resolvedServer.variables, slug: slugify(cleanOperationId), summary: apiData.summary, descriptionMarkdown: apiData.description, diff --git a/src/components/apiExamples/apiExamples.module.scss b/src/components/apiExamples/apiExamples.module.scss index 90ea811ac4fb3f..7e6cc6f3225198 100644 --- a/src/components/apiExamples/apiExamples.module.scss +++ b/src/components/apiExamples/apiExamples.module.scss @@ -19,3 +19,62 @@ margin: 0; } } + +.region-selector { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + font-size: 0.8rem; +} + +.region-label { + color: var(--text-color); + font-weight: 500; +} + +.region-tabs { + display: flex; + border-radius: 3px; + overflow: hidden; + border: 1px solid var(--border-color); +} + +.region-tab { + background: var(--gray-100); + color: var(--text-color); + border: none; + border-right: 1px solid var(--border-color); + padding: 0.2rem 0.6rem; + cursor: pointer; + font-size: 0.75rem; + font-weight: 500; + transition: background-color 0.15s, color 0.15s; + + &:last-child { + border-right: none; + } + + &:hover:not(.region-tab-selected) { + background: var(--gray-200); + } +} + +:global(.dark) .region-tab { + background: #2d2d2d; + color: #9990ab; +} + +:global(.dark) .region-tab:hover:not(.region-tab-selected) { + background: #3d3d3d; +} + +.region-tab-selected { + background: var(--accent); + color: var(--white); +} + +:global(.dark) .region-tab-selected { + background: var(--accent); + color: var(--white); +} diff --git a/src/components/apiExamples/apiExamples.tsx b/src/components/apiExamples/apiExamples.tsx index bb909e1bed1dd9..42c0421d7f0b52 100644 --- a/src/components/apiExamples/apiExamples.tsx +++ b/src/components/apiExamples/apiExamples.tsx @@ -1,11 +1,12 @@ 'use client'; -import {Fragment, useEffect, useState} from 'react'; +import {Fragment, useContext, useEffect, useState} from 'react'; import {Clipboard} from 'react-feather'; import {type API} from 'sentry-docs/build/resolveOpenAPI'; import {CodeBlock} from '../codeBlock'; import codeBlockStyles from '../codeBlock/code-blocks.module.scss'; +import {CodeContext} from '../codeContext'; import {CodeTabs} from '../codeTabs'; import {codeToJsx} from '../highlightCode'; import styles from './apiExamples.module.scss'; @@ -18,13 +19,59 @@ const strFormat = (str: string) => { return s + '.'; }; +function resolveServerUrl( + serverTemplate: string, + variables: Record | undefined, + overrides?: Record +): string { + if (!variables) { + return serverTemplate; + } + let url = serverTemplate; + for (const [name, variable] of Object.entries(variables)) { + const value = overrides?.[name] ?? variable.default; + url = url.replace(`{${name}}`, value); + } + return url; +} + +function detectRegionFromApiUrl(apiUrl: string): string | undefined { + const match = apiUrl.match(/^https?:\/\/(\w+)\.sentry\.io/); + return match?.[1]; +} + type Props = { api: API; }; export function ApiExamples({api}: Props) { + const codeContext = useContext(CodeContext); + const regionVar = api.serverVariables?.region; + const regionOptions = regionVar?.enum ?? []; + + const userRegion = + codeContext?.codeKeywords.USER && codeContext.codeKeywords.PROJECT[0] + ? detectRegionFromApiUrl(codeContext.codeKeywords.PROJECT[0].API_URL) + : undefined; + + const [selectedRegion, setSelectedRegion] = useState( + regionVar?.default ?? 'us' + ); + + useEffect(() => { + if (userRegion && regionOptions.includes(userRegion)) { + setSelectedRegion(userRegion); + } + }, [userRegion, regionOptions]); + + const resolvedServer = resolveServerUrl( + api.server, + api.serverVariables, + {region: selectedRegion} + ); + const apiExample = [ - `curl ${api.server}${api.apiPath}`, + `curl ${resolvedServer}${api.apiPath}`, ` -H 'Authorization: Bearer '`, ]; if (['put', 'options', 'delete'].includes(api.method.toLowerCase())) { @@ -84,6 +131,22 @@ export function ApiExamples({api}: Props) { return ( + {regionOptions.length > 1 && ( +
+ Region +
+ {regionOptions.map(region => ( + + ))} +
+
+ )}
{codeToJsx(apiExample.join(' \\\n'), 'bash')}
From 41ccecc7d3e1c5382e6b42ac9fd56521f48bd6df Mon Sep 17 00:00:00 2001 From: Michael Warkentin Date: Fri, 15 May 2026 16:04:55 -0400 Subject: [PATCH 2/5] fix(api): Add trailing slash anchor to region detection regex Satisfy CodeQL js/regex/missing-regexp-anchor by requiring a `/` after `.sentry.io` so the pattern cannot match lookalike domains. Co-Authored-By: Claude Co-authored-by: Cursor --- src/components/apiExamples/apiExamples.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/apiExamples/apiExamples.tsx b/src/components/apiExamples/apiExamples.tsx index 42c0421d7f0b52..43f05be680a13d 100644 --- a/src/components/apiExamples/apiExamples.tsx +++ b/src/components/apiExamples/apiExamples.tsx @@ -36,7 +36,7 @@ function resolveServerUrl( } function detectRegionFromApiUrl(apiUrl: string): string | undefined { - const match = apiUrl.match(/^https?:\/\/(\w+)\.sentry\.io/); + const match = apiUrl.match(/^https?:\/\/(\w+)\.sentry\.io\//); return match?.[1]; } From 92066d56ad2acc9cea67b2f1a84764cbdd89b2b1 Mon Sep 17 00:00:00 2001 From: Michael Warkentin Date: Fri, 15 May 2026 16:20:57 -0400 Subject: [PATCH 3/5] fix(api): Wrap regionOptions in useMemo to stabilize useEffect deps Fixes react-hooks/exhaustive-deps lint error where the fallback empty array created a new reference on every render. Co-Authored-By: Claude Co-authored-by: Cursor --- src/components/apiExamples/apiExamples.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/apiExamples/apiExamples.tsx b/src/components/apiExamples/apiExamples.tsx index 43f05be680a13d..487aa0a0653ec6 100644 --- a/src/components/apiExamples/apiExamples.tsx +++ b/src/components/apiExamples/apiExamples.tsx @@ -1,6 +1,6 @@ 'use client'; -import {Fragment, useContext, useEffect, useState} from 'react'; +import {Fragment, useContext, useEffect, useMemo, useState} from 'react'; import {Clipboard} from 'react-feather'; import {type API} from 'sentry-docs/build/resolveOpenAPI'; @@ -47,7 +47,7 @@ type Props = { export function ApiExamples({api}: Props) { const codeContext = useContext(CodeContext); const regionVar = api.serverVariables?.region; - const regionOptions = regionVar?.enum ?? []; + const regionOptions = useMemo(() => regionVar?.enum ?? [], [regionVar?.enum]); const userRegion = codeContext?.codeKeywords.USER && codeContext.codeKeywords.PROJECT[0] From b2ecd862e8b8c6ba9bb680e5fc622fdd76f2f718 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 20:21:52 +0000 Subject: [PATCH 4/5] [getsentry/action-github-commit] Auto commit --- src/build/resolveOpenAPI.ts | 6 +++++- src/components/apiExamples/apiExamples.tsx | 12 ++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/build/resolveOpenAPI.ts b/src/build/resolveOpenAPI.ts index 2bc8bdae0c3bb9..85697f4cd3fddf 100644 --- a/src/build/resolveOpenAPI.ts +++ b/src/build/resolveOpenAPI.ts @@ -2,7 +2,11 @@ import {promises as fs} from 'fs'; -import {type DeRefedOpenAPI, type ServerMeta, type ServerVariable} from './open-api/types'; +import { + type DeRefedOpenAPI, + type ServerMeta, + type ServerVariable, +} from './open-api/types'; // SENTRY_API_SCHEMA_SHA is used in the sentry-docs GHA workflow in getsentry/sentry-api-schema. // DO NOT change variable name unless you change it in the sentry-docs GHA workflow in getsentry/sentry-api-schema. diff --git a/src/components/apiExamples/apiExamples.tsx b/src/components/apiExamples/apiExamples.tsx index 487aa0a0653ec6..da5ab0cc36694a 100644 --- a/src/components/apiExamples/apiExamples.tsx +++ b/src/components/apiExamples/apiExamples.tsx @@ -54,9 +54,7 @@ export function ApiExamples({api}: Props) { ? detectRegionFromApiUrl(codeContext.codeKeywords.PROJECT[0].API_URL) : undefined; - const [selectedRegion, setSelectedRegion] = useState( - regionVar?.default ?? 'us' - ); + const [selectedRegion, setSelectedRegion] = useState(regionVar?.default ?? 'us'); useEffect(() => { if (userRegion && regionOptions.includes(userRegion)) { @@ -64,11 +62,9 @@ export function ApiExamples({api}: Props) { } }, [userRegion, regionOptions]); - const resolvedServer = resolveServerUrl( - api.server, - api.serverVariables, - {region: selectedRegion} - ); + const resolvedServer = resolveServerUrl(api.server, api.serverVariables, { + region: selectedRegion, + }); const apiExample = [ `curl ${resolvedServer}${api.apiPath}`, From 5f6253287103332b1c6ba7556745e90759a78332 Mon Sep 17 00:00:00 2001 From: Michael Warkentin Date: Fri, 15 May 2026 17:03:11 -0400 Subject: [PATCH 5/5] fix(api): Use selected project for region auto-detection Read sharedKeywordSelection to resolve the user's currently selected project instead of always reading PROJECT[0]. This ensures the region tab matches when users switch between projects in different regions. Co-Authored-By: Claude Co-authored-by: Cursor --- src/components/apiExamples/apiExamples.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/apiExamples/apiExamples.tsx b/src/components/apiExamples/apiExamples.tsx index da5ab0cc36694a..cf385ed7aca13f 100644 --- a/src/components/apiExamples/apiExamples.tsx +++ b/src/components/apiExamples/apiExamples.tsx @@ -49,11 +49,19 @@ export function ApiExamples({api}: Props) { const regionVar = api.serverVariables?.region; const regionOptions = useMemo(() => regionVar?.enum ?? [], [regionVar?.enum]); - const userRegion = - codeContext?.codeKeywords.USER && codeContext.codeKeywords.PROJECT[0] - ? detectRegionFromApiUrl(codeContext.codeKeywords.PROJECT[0].API_URL) + const [sharedSelection] = codeContext?.sharedKeywordSelection ?? [ + {} as Record, + ]; + const projectIdx = sharedSelection.PROJECT ?? 0; + const selectedProject = + codeContext?.codeKeywords.USER && codeContext.codeKeywords.PROJECT[projectIdx] + ? codeContext.codeKeywords.PROJECT[projectIdx] : undefined; + const userRegion = selectedProject + ? detectRegionFromApiUrl(selectedProject.API_URL) + : undefined; + const [selectedRegion, setSelectedRegion] = useState(regionVar?.default ?? 'us'); useEffect(() => {