diff --git a/apps/cli-docs/src/fragments/commands/api.md b/apps/cli-docs/src/fragments/commands/api.md index 4e33faa53..f568dcb4b 100644 --- a/apps/cli-docs/src/fragments/commands/api.md +++ b/apps/cli-docs/src/fragments/commands/api.md @@ -4,6 +4,13 @@ Endpoints are relative to `/api/0/` — the prefix is added automatically. Absolute HTTP(S) Sentry URLs also work; their origin is validated against your authenticated host. +A relative endpoint that names an organization, such as +`organizations/acme-corp/issues/` or `projects/acme-corp/web/`, is sent to that +organization's region (for example `https://us.sentry.io`) with the stored +account that knows it, as other commands do. The first request for an +organization may look up its region, and `--dry-run` shows the URL that would be +used. An absolute URL is sent as given. + If a successful GET response is interrupted while downloading, the CLI retries within its existing limit of three attempts. This also applies to complete API responses read by other commands. The request timeout covers these response diff --git a/packages/cli/src/lib/api/alerts.ts b/packages/cli/src/lib/api/alerts.ts index 1169822cd..a6e9a0f1f 100644 --- a/packages/cli/src/lib/api/alerts.ts +++ b/packages/cli/src/lib/api/alerts.ts @@ -303,7 +303,7 @@ export async function getIssueAlertWorkflowDocument( orgSlug: string, workflowId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await getOrganizationWorkflow({ ...config, path: { @@ -328,7 +328,7 @@ export async function resolveErrorDetectorId( orgSlug: string, projectSlug: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationDetectors({ ...config, path: { organization_id_or_slug: orgSlug }, @@ -413,7 +413,7 @@ export async function listMetricAlertsPaginated( orgSlug: string, options: { perPage?: number; cursor?: string } = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationDetectors({ ...config, path: { organization_id_or_slug: orgSlug }, @@ -470,7 +470,7 @@ async function getMetricDetector( orgSlug: string, ruleId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await getOrganizationDetector({ ...config, // The SDK types detector_id as number, but detector IDs are opaque strings @@ -532,7 +532,7 @@ export async function updateIssueAlertRule( workflowId: string, body: Record, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await updateOrganizationWorkflow({ ...config, path: { @@ -562,7 +562,7 @@ export async function createIssueAlertRule( orgSlug: string, body: Record, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await createOrganizationWorkflow({ ...config, path: { organization_id_or_slug: orgSlug }, @@ -768,7 +768,7 @@ export async function deleteMetricAlertRule( orgSlug: string, ruleId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await deleteOrganizationDetector({ ...config, // Detector IDs are opaque strings the API accepts verbatim; the SDK types @@ -828,7 +828,7 @@ export async function putMetricAlertRule( ruleId: string, body: Record, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await updateOrganizationDetector({ ...config, path: { @@ -861,7 +861,7 @@ export async function createMetricAlertRule( body: Record, ): Promise> { const projectSlug = resolveDetectorProject(body); - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await createOrganizationProjectDetector({ ...config, path: { diff --git a/packages/cli/src/lib/api/dashboards.ts b/packages/cli/src/lib/api/dashboards.ts index 698d039c4..e7c5a4cb9 100644 --- a/packages/cli/src/lib/api/dashboards.ts +++ b/packages/cli/src/lib/api/dashboards.ts @@ -64,7 +64,7 @@ export async function listDashboardsPaginated( orgSlug: string, options: { perPage?: number; cursor?: string } = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationDashboards({ ...config, @@ -89,7 +89,7 @@ export async function getDashboard( orgSlug: string, dashboardId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await getOrganizationDashboard({ ...config, @@ -116,7 +116,7 @@ export async function createDashboard( orgSlug: string, body: { title: string; widgets?: DashboardWidget[]; projects?: number[] }, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await createOrganizationDashboard({ ...config, @@ -149,7 +149,7 @@ export async function updateDashboard( period?: string | null; }, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await updateOrganizationDashboard({ ...config, diff --git a/packages/cli/src/lib/api/dropped-events.ts b/packages/cli/src/lib/api/dropped-events.ts index 9c46b15c8..c8c11c9e3 100644 --- a/packages/cli/src/lib/api/dropped-events.ts +++ b/packages/cli/src/lib/api/dropped-events.ts @@ -36,7 +36,7 @@ export async function getDroppedEvents( orgSlug: string, options: DroppedEventsOptions = {}, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationEventsDropped({ ...config, path: { organization_id_or_slug: orgSlug }, diff --git a/packages/cli/src/lib/api/events.ts b/packages/cli/src/lib/api/events.ts index 8b08dca80..456312b7f 100644 --- a/packages/cli/src/lib/api/events.ts +++ b/packages/cli/src/lib/api/events.ts @@ -42,7 +42,7 @@ export async function getLatestEvent( orgSlug: string, issueId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await getOrganizationIssueEvent({ ...config, @@ -65,7 +65,7 @@ export async function getEvent( projectSlug: string, eventId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await getProjectEvent({ ...config, @@ -90,7 +90,7 @@ export async function listEventAttachments( projectSlug: string, eventId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const { data } = await autoPaginate( async (cursor) => { // The endpoint accepts `per_page`, but its generated SDK type omits it. @@ -140,7 +140,7 @@ export async function resolveEventInOrg( orgSlug: string, eventId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await sdkResolveAnEventId({ ...config, @@ -259,7 +259,7 @@ export async function listIssueEvents( ): Promise> { const { limit = 25, query, full, cursor, statsPeriod, start, end } = options; - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); return await paginate({ limit, cursor }, async (perPage, pageCursor) => { const result = await listOrganizationIssueEvents({ diff --git a/packages/cli/src/lib/api/infrastructure.ts b/packages/cli/src/lib/api/infrastructure.ts index d0c4acf56..4274326f2 100644 --- a/packages/cli/src/lib/api/infrastructure.ts +++ b/packages/cli/src/lib/api/infrastructure.ts @@ -9,6 +9,7 @@ import { promisify } from "node:util"; import { zstdCompress as zstdCompressCb } from "node:zlib"; import { + type BaseUrlRequest, createSentryClient, parseSentryLinkHeader, type SdkResult, @@ -520,12 +521,27 @@ export function appendSearchParams( } /** - * Get SDK config for an organization's region. - * Resolves the org's region URL and returns the config. + * `resolveBaseUrl` for `@sentry/api` clients: a request whose path names an + * organization goes to that organization's region, on the server of the + * stored account that knows it ({@link resolveOrgRegion}). An explicit + * server still limits the accounts. Unscoped paths keep the base URL. */ -export async function getOrgSdkConfig(orgSlug: string) { - const regionUrl = await resolveOrgRegion(orgSlug); - return getSdkClientConfig(regionUrl); +async function resolveOrgBaseUrl({ + baseUrl, + scope, +}: BaseUrlRequest): Promise { + return scope ? await resolveOrgRegion(scope.organization) : baseUrl; +} + +/** + * SDK config that sends each request to the region of the organization its + * path names ({@link resolveOrgBaseUrl}). + */ +export function getOrgSdkConfig() { + return { + ...getSdkClientConfig(getApiBaseUrl()), + resolveBaseUrl: resolveOrgBaseUrl, + }; } /** @@ -998,7 +1014,8 @@ export type PreparedRawApiRequest = { }; /** - * Prepare a raw request without fetching or refreshing credentials. + * Prepare a raw request without sending it or refreshing credentials; an + * org-scoped path may look up the organization's region first. * Existing params retain append semantics; query overrides use the SDK's * replacement semantics for decoded page cursors and page sizes. */ @@ -1008,9 +1025,14 @@ export async function prepareRawApiRequest( query?: Record, ): Promise { const { method = "GET", body, params, headers, baseUrl } = options; + // A full URL keeps the request on its server; otherwise the path's + // organization picks the account, server, and region. const config = baseUrl ? getSdkConfig(baseUrl, { bufferResponseBody: true }) - : getDefaultSdkConfig({ bufferResponseBody: true, path: endpoint }); + : { + ...getDefaultSdkConfig({ bufferResponseBody: true }), + resolveBaseUrl: resolveOrgBaseUrl, + }; const isStringBody = typeof body === "string"; const request = await createSentryClient(config).client.prepareRequest({ method, diff --git a/packages/cli/src/lib/api/integrations.ts b/packages/cli/src/lib/api/integrations.ts index 3f05a47eb..b7fff7eca 100644 --- a/packages/cli/src/lib/api/integrations.ts +++ b/packages/cli/src/lib/api/integrations.ts @@ -48,7 +48,7 @@ export async function listIntegrationsPaginated( orgSlug: string, options: { cursor?: string; perPage?: number } = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationIntegrations({ ...config, @@ -100,7 +100,7 @@ export async function getIntegration( orgSlug: string, integrationId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await getOrganizationIntegration({ ...config, @@ -127,7 +127,7 @@ export async function deleteIntegration( orgSlug: string, integrationId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await deleteOrganizationIntegration({ ...config, diff --git a/packages/cli/src/lib/api/issue-tags.ts b/packages/cli/src/lib/api/issue-tags.ts index 8f95b1c05..5e95755ec 100644 --- a/packages/cli/src/lib/api/issue-tags.ts +++ b/packages/cli/src/lib/api/issue-tags.ts @@ -22,7 +22,7 @@ export async function getIssueTag( key: string, environment?: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await getOrganizationIssueTag({ ...config, path: { diff --git a/packages/cli/src/lib/api/issues.ts b/packages/cli/src/lib/api/issues.ts index 654744594..c34111b43 100644 --- a/packages/cli/src/lib/api/issues.ts +++ b/packages/cli/src/lib/api/issues.ts @@ -172,7 +172,7 @@ export async function listIssuesPaginated( projects === undefined && projectSlug ? `project:${projectSlug}` : ""; const fullQuery = [projectFilter, options.query].filter(Boolean).join(" "); - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationIssues({ ...config, diff --git a/packages/cli/src/lib/api/logs.ts b/packages/cli/src/lib/api/logs.ts index e14e0d631..c2498bc99 100644 --- a/packages/cli/src/lib/api/logs.ts +++ b/packages/cli/src/lib/api/logs.ts @@ -189,7 +189,7 @@ export async function listLogs( .filter(Boolean) .join(" "); - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); // Merge extra fields (from --fields) with the default set, deduplicating const fields = options.extraFields?.length @@ -304,7 +304,7 @@ function getLogLookupWindow( * Batch size must not exceed {@link API_MAX_PER_PAGE}. */ type GetLogsBatchOptions = { - config: Awaited>; + config: ReturnType; extraFields?: string[]; /** Numeric project ID for direct project selection. @see {@link ListLogsOptions.projectId} */ projectId?: number; @@ -415,7 +415,7 @@ export async function getLogs( options: GetLogsOptions = {}, ): Promise { const { extraFields, projectId } = options; - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const batchOptions: GetLogsBatchOptions = { config, extraFields, projectId }; // Single batch — no splitting needed diff --git a/packages/cli/src/lib/api/monitors.ts b/packages/cli/src/lib/api/monitors.ts index 1539fc7a5..6b7fa3609 100644 --- a/packages/cli/src/lib/api/monitors.ts +++ b/packages/cli/src/lib/api/monitors.ts @@ -30,7 +30,7 @@ import { * @returns Monitors, including nested monitor environments */ export async function listMonitors(orgSlug: string): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const { data: allResults } = await autoPaginate(async (cursor) => { const result = await listOrganizationMonitors({ @@ -63,7 +63,7 @@ export async function listMonitorsPaginated( orgSlug: string, options: { cursor?: string; perPage?: number; query?: string } = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationMonitors({ ...config, diff --git a/packages/cli/src/lib/api/organizations.ts b/packages/cli/src/lib/api/organizations.ts index 48fcc72bc..da2dcdfee 100644 --- a/packages/cli/src/lib/api/organizations.ts +++ b/packages/cli/src/lib/api/organizations.ts @@ -270,7 +270,7 @@ export async function listOrganizationsUncached( export async function getOrganization( orgSlug: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await sdkGetOrganization({ ...config, diff --git a/packages/cli/src/lib/api/projects.ts b/packages/cli/src/lib/api/projects.ts index 7a0727298..e337c4b6e 100644 --- a/packages/cli/src/lib/api/projects.ts +++ b/packages/cli/src/lib/api/projects.ts @@ -73,7 +73,7 @@ function reportBestEffortFailure(message: string, error: unknown): void { * @returns All projects in the organization */ export async function listProjects(orgSlug: string): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const { data: allResults } = await autoPaginate(async (cursor) => { const result = await listOrganizationProjects({ @@ -123,7 +123,7 @@ export async function listProjectsPaginated( orgSlug: string, options: { cursor?: string; perPage?: number; query?: string } = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const perPage = Math.min( options.perPage ?? API_MAX_PER_PAGE, API_MAX_PER_PAGE, @@ -218,7 +218,7 @@ export async function createProject( teamSlug: string, body: CreateProjectBody, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await createTeamProject({ ...config, path: { @@ -376,7 +376,7 @@ export async function createProjectWithAutoTeam( orgSlug: string, body: CreateProjectBody, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await createOrganizationProject({ ...config, path: { organization_id_or_slug: orgSlug }, @@ -407,7 +407,7 @@ export async function deleteProject( orgSlug: string, projectSlug: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await sdkDeleteProject({ ...config, path: { @@ -632,7 +632,7 @@ export async function getProject( orgSlug: string, projectSlug: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); // `collapse=organization` is server-supported but absent from the OpenAPI // spec, so the SDK types `query` as `never` on `GetProjectData`. Cast just @@ -713,7 +713,7 @@ export async function listProjectDsns( projectSlug: string, options: { limit?: number; cursor?: string } = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); return await paginate(options, async (perPage, cursor) => { // The SDK omits the shared per_page parameter supported by this endpoint. @@ -747,7 +747,7 @@ export async function listOrganizationDsns( orgSlug: string, options: { limit?: number; cursor?: string } = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const projectLookups = new Map>(); const limitProjectLookups = pLimit(ORG_FANOUT_CONCURRENCY); @@ -796,7 +796,7 @@ export async function getProjectKeys( projectSlug: string, options: { status?: "active" | "inactive" } = {}, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listProjectKeys({ ...config, diff --git a/packages/cli/src/lib/api/releases.ts b/packages/cli/src/lib/api/releases.ts index 649e210cf..fb306ea72 100644 --- a/packages/cli/src/lib/api/releases.ts +++ b/packages/cli/src/lib/api/releases.ts @@ -99,7 +99,7 @@ export async function listReleasesPaginated( orgSlug: string, options: ListReleasesOptions = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationReleases({ ...config, @@ -196,7 +196,7 @@ export async function getRelease( healthStatsPeriod?: string; }, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await getOrganizationRelease({ ...config, @@ -252,7 +252,7 @@ export async function createRelease( }>; }, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); // Cast body through unknown — the SDK's body type requires `projects: string[]` // as non-optional, but the API accepts it as optional at runtime. @@ -298,7 +298,7 @@ export async function updateRelease( }>; }, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await updateOrganizationRelease({ ...config, @@ -327,7 +327,7 @@ export async function deleteRelease( orgSlug: string, version: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await deleteOrganizationRelease({ ...config, @@ -351,7 +351,7 @@ export async function listReleaseDeploys( orgSlug: string, version: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationReleaseDeploys({ ...config, @@ -386,7 +386,7 @@ export async function createReleaseDeploy( dateFinished?: string; }, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await createOrganizationReleaseDeploy({ ...config, @@ -974,7 +974,7 @@ export async function listProjectEnvironments( orgSlug: string, projectSlug: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await sdkListProjectEnvironments({ ...config, path: { diff --git a/packages/cli/src/lib/api/replays.ts b/packages/cli/src/lib/api/replays.ts index f1a73234b..20fe18647 100644 --- a/packages/cli/src/lib/api/replays.ts +++ b/packages/cli/src/lib/api/replays.ts @@ -369,7 +369,7 @@ async function fetchReplayRecordingSegmentsPage( options: FetchReplayRecordingSegmentsPageOptions, ): Promise> { const { cursor, orgSlug, projectSlugOrId, replayId } = options; - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listProjectReplayRecordingSegments({ ...config, path: { diff --git a/packages/cli/src/lib/api/repositories.ts b/packages/cli/src/lib/api/repositories.ts index 3565095fa..b6625d62f 100644 --- a/packages/cli/src/lib/api/repositories.ts +++ b/packages/cli/src/lib/api/repositories.ts @@ -33,7 +33,7 @@ const log = logger.withTag("api.repositories"); export async function listRepositories( orgSlug: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationRepos({ ...config, @@ -60,7 +60,7 @@ export async function listRepositoriesPaginated( orgSlug: string, options: { cursor?: string; perPage?: number; query?: string } = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationRepos({ ...config, diff --git a/packages/cli/src/lib/api/teams.ts b/packages/cli/src/lib/api/teams.ts index 2e63ad5cd..99e22a5ed 100644 --- a/packages/cli/src/lib/api/teams.ts +++ b/packages/cli/src/lib/api/teams.ts @@ -28,7 +28,7 @@ import { reportCliError } from "../error-reporting.js"; * Uses region-aware routing for multi-region support. */ export async function listTeams(orgSlug: string): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationTeams({ ...config, @@ -51,7 +51,7 @@ export async function listTeamsPaginated( orgSlug: string, options: { cursor?: string; perPage?: number; query?: string } = {}, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await listOrganizationTeams({ ...config, @@ -80,7 +80,7 @@ export async function listProjectTeams( orgSlug: string, projectSlug: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await sdkListProjectTeams({ ...config, path: { @@ -107,7 +107,7 @@ export async function createTeam( orgSlug: string, slug: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await createOrganizationTeam({ ...config, path: { organization_id_or_slug: orgSlug }, @@ -142,7 +142,7 @@ export async function addMemberToTeam( teamSlug: string, memberId: string, ): Promise { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); const result = await addOrganizationMemberTeam({ ...config, path: { diff --git a/packages/cli/src/lib/api/traces.ts b/packages/cli/src/lib/api/traces.ts index 5cb23d332..2c7d28d65 100644 --- a/packages/cli/src/lib/api/traces.ts +++ b/packages/cli/src/lib/api/traces.ts @@ -340,7 +340,7 @@ export async function listTraceItemAttributes( orgSlug: string, options: ListTraceItemAttributesOptions, ): Promise> { - const config = await getOrgSdkConfig(orgSlug); + const config = getOrgSdkConfig(); // `per_page` is accepted by the endpoint's paginator but missing from the // SDK's generated query type, so widen the type rather than cast the call. const query: TraceItemAttributesQuery & { per_page?: number } = { diff --git a/packages/cli/src/lib/sentry-client.ts b/packages/cli/src/lib/sentry-client.ts index 6805b093a..8621a3360 100644 --- a/packages/cli/src/lib/sentry-client.ts +++ b/packages/cli/src/lib/sentry-client.ts @@ -13,11 +13,9 @@ import { getTraceData } from "@sentry/node"; import { computeInvalidationPrefixes, createApiTransport, - type ApiPathScope, type CredentialRequest, extractUrlPath, getValidatedResponseSource, - parseApiPathScope, type ResponseBodyMode, type RetryPolicy, type TransportCache, @@ -325,15 +323,13 @@ async function withHttpSpan( const NUMERIC_ORG_ID = /^\d+$/; /** - * Use the stored account that knows the organization `scope` names - * (`selectAccountForOrg`). `server` limits the candidates to accounts on - * it; without it, an account on another server qualifies and brings its - * server along. + * Use the stored account that knows the organization the request's path + * targets (`selectAccountForOrg`), so requests built without an org's + * region still reach it. Only accounts on the request's server qualify: its + * URL is already built. Requests routed by `resolveOrgBaseUrl` picked an + * account, on any server, before their URL was built. */ -function selectAccountForScope( - scope: ApiPathScope | undefined, - server?: string, -): void { +function selectAccountForRequest({ url, scope }: CredentialRequest): void { if (!scope) { return; } @@ -341,21 +337,10 @@ function selectAccountForScope( selectAccountForOrg( organization, NUMERIC_ORG_ID.test(organization) ? organization : undefined, - { server }, + { server: url }, ); } -/** - * Use the stored account that knows the organization the request's path - * targets, so requests no command resolved an org for still reach it. Only - * accounts on the request's server qualify: its URL is already built. - * Callers that build the URL from {@link getDefaultSdkConfig} with a `path` - * have already picked an account on any server. - */ -function selectAccountForRequest({ url, scope }: CredentialRequest): void { - selectAccountForScope(scope, url); -} - /** * The CLI's transport options: stored credentials, host-scope checks, the * response cache, and the CLI's error classes. Retries, timeouts, the 401 @@ -575,27 +560,14 @@ export function getSdkConfig( /** * Get SDK config for the default API (control silo or self-hosted). * - * When `path` names an organization, the stored account that knows it is - * picked first, on any server, so the base URL follows that account's - * server: `sentry api organizations//` reaches a self-hosted org while - * a SaaS login is active. An explicit server still limits the candidates - * to accounts on it (see `selectAccountForOrg`). - * * The credential is not pinned, so the transport can still pick the stored * account that knows the organization a request's path names; it only picks * among accounts on the server this base URL belongs to. */ export function getDefaultSdkConfig( - options: Pick & { - /** API path relative to `/api/0/`, optionally with a query string. */ - path?: string; - } = {}, + options: Pick = {}, ) { - const { path, ...fetchOptions } = options; - if (path !== undefined) { - selectAccountForScope(parseApiPathScope(path)); - } - return getSdkConfig(getApiBaseUrl(), fetchOptions); + return getSdkConfig(getApiBaseUrl(), options); } /** diff --git a/packages/cli/test/lib/db/account-routing.test.ts b/packages/cli/test/lib/db/account-routing.test.ts index 8f6d27b98..1f8187ef3 100644 --- a/packages/cli/test/lib/db/account-routing.test.ts +++ b/packages/cli/test/lib/db/account-routing.test.ts @@ -467,18 +467,40 @@ describe("requests whose path names an organization", () => { test("use the account that knows the org, like `sentry api`", async () => { login("work-token", SAAS, "work"); - seeOrg("work-org", SAAS); + seeOrg("team-org", SAAS); const personal = login("personal-token", SAAS, "personal"); seeOrgList(SAAS, ["personal-org"]); - await rawApiRequest("organizations/work-org/issues/"); + await rawApiRequest("organizations/team-org/issues/"); await rawApiRequest("projects/personal-org/web/"); - // One invocation never mixes accounts. - expect(sent).toEqual(["Bearer work-token", "Bearer work-token"]); + // One invocation never mixes accounts, so the work account looks up + // personal-org's region before the request. + expect(sent).toEqual([ + "Bearer work-token", + "Bearer work-token", + "Bearer work-token", + ]); + expect(urls).toEqual([ + `${SAAS}/api/0/organizations/team-org/issues/`, + `${SAAS}/api/0/organizations/personal-org/`, + `${SAAS}/api/0/projects/personal-org/web/`, + ]); expect(getActiveAccountId()).toBe(personal); }); + test("go to the org's region, like typed commands", async () => { + login("saas-token", SAAS, "saas"); + seeOrg("us-org", SAAS, "https://us.sentry.io"); + + await rawApiRequest("organizations/us-org/issues/?query=is:unresolved"); + + expect(sent).toEqual(["Bearer saas-token"]); + expect(urls).toEqual([ + "https://us.sentry.io/api/0/organizations/us-org/issues/?query=is:unresolved", + ]); + }); + test("leave unscoped requests on the active account", async () => { login("work-token", SAAS, "work"); seeOrg("work-org", SAAS); @@ -512,8 +534,12 @@ describe("requests whose path names an organization", () => { await rawApiRequest("organizations/acme-org/"); - expect(sent).toEqual(["Bearer saas-token"]); - expect(urls).toEqual([`${SAAS}/api/0/organizations/acme-org/`]); + // The region lookup and the request itself both stay on SaaS. + expect(sent).toEqual(["Bearer saas-token", "Bearer saas-token"]); + expect(urls).toEqual([ + `${SAAS}/api/0/organizations/acme-org/`, + `${SAAS}/api/0/organizations/acme-org/`, + ]); }); test("only consider accounts on an absolute URL's server", async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c2dcf24a..9e55ead19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,8 +43,8 @@ catalogs: specifier: ^1.2.3 version: 1.2.3 '@sentry/api': - specifier: 1.26.0 - version: 1.26.0 + specifier: 1.29.0 + version: 1.29.0 '@sentry/cloudflare': specifier: 11.6.0 version: 11.6.0 @@ -511,7 +511,7 @@ importers: version: 1.46.0(ai@6.0.230(zod@4.6.7))(express@5.3.0)(zod@4.6.7) '@sentry/api': specifier: 'catalog:' - version: 1.26.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) + version: 1.29.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) '@sentry/core': specifier: 'catalog:' version: 11.6.0 @@ -866,7 +866,7 @@ importers: version: 2.4.0 '@sentry/api': specifier: 'catalog:' - version: 1.26.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) + version: 1.29.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) '@sentry/core': specifier: 'catalog:' version: 11.6.0 @@ -1045,7 +1045,7 @@ importers: dependencies: '@sentry/api': specifier: 'catalog:' - version: 1.26.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) + version: 1.29.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) '@sentry/search-syntax': specifier: 'catalog:' version: 1.0.0 @@ -4312,8 +4312,8 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@sentry/api@1.26.0': - resolution: {integrity: sha512-LkzMT2mn2Qrbnf2s+2t/mrXpN1/2o0GO2fodhds2Ntp8kRt/NkilWIETAGUU6vB7XKlLhPZloZT4Yv/HEzCmFA==} + '@sentry/api@1.29.0': + resolution: {integrity: sha512-BkO8cL6vXxl2WONUgD/HLQTAGt4oHpnIALx1msa+MLojWPcrY8zYH+tObaYNuhwZKAUgGjDqw8xll43Oy6BHfA==} engines: {node: '>=22'} peerDependencies: typescript: '*' @@ -12276,7 +12276,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@sentry/api@1.26.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7)': + '@sentry/api@1.29.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7)': dependencies: http-cache-semantics: 4.2.0 optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d73b003d2..fd7eb63ba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -52,7 +52,7 @@ catalog: "@modelcontextprotocol/server": 2.4.0 "@radix-ui/react-accordion": ^1.2.20 "@radix-ui/react-slot": ^1.2.3 - "@sentry/api": 1.26.0 + "@sentry/api": 1.29.0 "@sentry/cloudflare": 11.6.0 "@sentry/core": 11.6.0 "@sentry/node": 11.6.0 @@ -167,7 +167,7 @@ minimumReleaseAgeExclude: - "@oxlint/binding-win32-x64-msvc@1.87.0" - oxfmt@0.72.0 - oxlint@1.87.0 - - "@sentry/api@1.26.0" + - "@sentry/api@1.29.0" - miniflare@5.20261101.0-beta - wrangler@4.156.1 - "@sentry/symbolic@14.0.0-alpha.5"