diff --git a/packages/cli/script/lib/api-schema.ts b/packages/cli/script/lib/api-schema.ts index 56ea95f38..9d8f567ce 100644 --- a/packages/cli/script/lib/api-schema.ts +++ b/packages/cli/script/lib/api-schema.ts @@ -1,9 +1,13 @@ /** Project SDK operation metadata into the CLI's bundled discovery index. */ +import { API_BASE_PATH } from "@sentry/api/client"; import type { ApiOperation } from "@sentry/api/operations"; import type { ApiEndpoint } from "../../src/lib/api-schema.js"; -/** Keep CLI grouping, output names and legacy deprecation detection local. */ +/** + * Keep CLI grouping, output names and legacy deprecation detection local. + * Paths are full request paths, as users type and see them. + */ export function projectApiOperations( operations: readonly ApiOperation[], ): ApiEndpoint[] { @@ -11,7 +15,7 @@ export function projectApiOperations( .map((operation) => ({ fn: operation.functionName, method: operation.method, - path: operation.path, + path: `${API_BASE_PATH}${operation.path}`, summary: operation.summary, description: operation.description, tags: operation.tags ? [...operation.tags] : undefined, @@ -39,6 +43,5 @@ function deriveResource(url: string): string { const segments = url .split("/") .filter((s) => s.length > 0 && !s.startsWith("{")); - const meaningful = segments.filter((s) => s !== "api" && s !== "0"); - return meaningful.at(-1) ?? "unknown"; + return segments.at(-1) ?? "unknown"; } diff --git a/packages/cli/src/commands/api.ts b/packages/cli/src/commands/api.ts index 072d5b693..5cd042e5f 100644 --- a/packages/cli/src/commands/api.ts +++ b/packages/cli/src/commands/api.ts @@ -8,7 +8,7 @@ import { access, readFile } from "node:fs/promises"; // oxlint-disable-next-line sentry-cli/no-namespace-import -- Sentry SDK recommends namespace import import * as Sentry from "@sentry/node"; -import { parseApiPathScope } from "@sentry/api/client"; +import { API_BASE_PATH, parseApiPathScope } from "@sentry/api/client"; import type { SentryContext } from "../context.js"; import { appendSearchParams, @@ -63,8 +63,9 @@ const log = logger.withTag("api"); /** Strips line breaks and surrounding indentation from copy-pasted endpoints. */ const LINE_BREAK_PATTERN = /[ \t]*[\r\n]+[ \t]*/g; -const API_PREFIX = "/api/0"; -const API_PREFIX_WITH_SLASH = `${API_PREFIX}/`; +const API_PREFIX_WITH_SLASH = `${API_BASE_PATH}/`; +/** The base path as users type it, without the leading slash. */ +const BARE_API_PREFIX = API_BASE_PATH.slice(1); const PAGINATION_KEY = "api"; type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; @@ -161,10 +162,10 @@ function splitAbsoluteApiUrl(url: URL): AbsoluteApiTarget { strippedApiPrefix: true, }; } - if (url.pathname.endsWith(API_PREFIX)) { + if (url.pathname.endsWith(API_BASE_PATH)) { return { endpoint: url.search, - baseUrl: `${url.origin}${url.pathname.slice(0, -API_PREFIX.length)}`, + baseUrl: `${url.origin}${url.pathname.slice(0, -API_BASE_PATH.length)}`, strippedApiPrefix: true, }; } @@ -218,12 +219,11 @@ export function normalizeEndpoint(endpoint: string): string { ? relativeEndpoint.slice(1) : relativeEndpoint; - // Strip api/0/ prefix if user accidentally included it — the base URL - // already includes /api/0/, so keeping it would produce a doubled path - // like /api/0/api/0/... (see CLI-K1). - // Also strip bare "api/0" to maintain idempotency. - if (trimmed.startsWith("api/0/") || trimmed === "api/0") { - trimmed = trimmed.slice(trimmed.startsWith("api/0/") ? 6 : 5); + // Strip the API base path if the user included it — the client adds it, + // so keeping it would produce a doubled path like /api/0/api/0/... + // (see CLI-K1). Also strip the bare base path to maintain idempotency. + if (hasBareApiPrefix(trimmed)) { + trimmed = trimmed.slice(BARE_API_PREFIX.length + 1); } // Split path and query string @@ -240,6 +240,12 @@ export function normalizeEndpoint(endpoint: string): string { return `${normalizedPath}${query}`; } +function hasBareApiPrefix(endpoint: string): boolean { + return ( + endpoint.startsWith(`${BARE_API_PREFIX}/`) || endpoint === BARE_API_PREFIX + ); +} + function resolveApiTarget(endpoint: string): { normalizedEndpoint: string; requestBaseUrl?: string; @@ -259,8 +265,7 @@ function resolveApiTarget(endpoint: string): { requestBaseUrl: absoluteTarget?.baseUrl, strippedApiPrefix: absoluteTarget?.strippedApiPrefix ?? - (withoutLeadingSlash.startsWith("api/0/") || - withoutLeadingSlash === "api/0"), + hasBareApiPrefix(withoutLeadingSlash), }; } @@ -1105,7 +1110,10 @@ export function formatEmptyErrorBody( endpoint: string | undefined, ): string { const statusLabel = [status, statusText].filter(Boolean).join(" "); - const request = method && endpoint ? ` — ${method} /api/0/${endpoint}` : ""; + const request = + method && endpoint + ? ` — ${method} ${API_PREFIX_WITH_SLASH}${endpoint}` + : ""; return `HTTP ${statusLabel}${request}`; } @@ -1569,7 +1577,7 @@ export const apiCommand = buildCommand({ brief: topLevelBrief("api"), fullDescription: "Make a raw API request to the Sentry API. Similar to 'gh api' for GitHub. " + - "The endpoint is relative to /api/0/ (do not include the prefix). " + + `The endpoint is relative to ${API_PREFIX_WITH_SLASH} (do not include the prefix). ` + "Absolute HTTP(S) Sentry URLs are also accepted; their origin is " + "validated against your authenticated host. " + "Authentication is handled automatically using your stored credentials.\n\n" + @@ -1661,7 +1669,7 @@ export const apiCommand = buildCommand({ kind: "tuple", parameters: [ { - brief: "API endpoint relative to /api/0/, or an absolute HTTP(S) URL", + brief: `API endpoint relative to ${API_PREFIX_WITH_SLASH}, or an absolute HTTP(S) URL`, parse: parseApiEndpoint, placeholder: "endpoint", }, @@ -1756,7 +1764,7 @@ export const apiCommand = buildCommand({ // only surface the detail at debug level for troubleshooting // (getsentry/cli#785 item #11). log.debug( - "Stripped /api/0/ prefix from endpoint (auto-added by the API client)", + `Stripped ${API_PREFIX_WITH_SLASH} prefix from endpoint (auto-added by the API client)`, ); } const { body, params } = await resolveBody(flags, stdin); diff --git a/packages/cli/src/lib/api-schema.ts b/packages/cli/src/lib/api-schema.ts index 08a991565..e96517f05 100644 --- a/packages/cli/src/lib/api-schema.ts +++ b/packages/cli/src/lib/api-schema.ts @@ -6,6 +6,7 @@ * `script/generate-api-schema.ts` from Sentry's OpenAPI specification. */ +import { API_BASE_PATH } from "@sentry/api/client"; import type { ApiOperation } from "@sentry/api/operations"; import endpoints from "../generated/api-schema.json" with { type: "json" }; import { fuzzyMatch } from "./fuzzy.js"; @@ -453,15 +454,15 @@ export function getAllEndpoints(): readonly ApiEndpoint[] { } /** The prefix `api` adds to an endpoint, left out of completed paths. */ -const API_PATH_PREFIX = /^\/api\/0\//; +const API_PATH_PREFIX = `${API_BASE_PATH}/`; /** - * Every schema path without its `/api/0/` prefix, each once: the endpoints + * Every schema path without the API base path, each once: the endpoints * `sentry api ` completes (see `CompletionValues.apiPaths`). */ export function getApiCompletionPaths(): string[] { return [ - ...new Set(schema.map(({ path }) => path.replace(API_PATH_PREFIX, ""))), + ...new Set(schema.map(({ path }) => path.slice(API_PATH_PREFIX.length))), ]; } diff --git a/packages/cli/src/lib/api/completion-seed.ts b/packages/cli/src/lib/api/completion-seed.ts index fc3b63fcc..f6caea972 100644 --- a/packages/cli/src/lib/api/completion-seed.ts +++ b/packages/cli/src/lib/api/completion-seed.ts @@ -45,6 +45,7 @@ import type { ListProjectTeamsData, ResolveOrganizationShortIdData, } from "@sentry/api"; +import { API_BASE_PATH } from "@sentry/api/client"; import { type CompletionEntity, type CompletionEntityKind, @@ -300,22 +301,22 @@ export const ATTRIBUTE_KINDS = { const COMPLETION_SOURCES: readonly CompletionSource[] = [ { kind: "issue", - url: "/api/0/organizations/{organization_id_or_slug}/issues/" satisfies ListOrganizationIssuesData["url"], + url: "/organizations/{organization_id_or_slug}/issues/" satisfies ListOrganizationIssuesData["url"], extract: issueEntities, }, { kind: "issue", - url: "/api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/" satisfies GetOrganizationIssueData["url"], + url: "/organizations/{organization_id_or_slug}/issues/{issue_id}/" satisfies GetOrganizationIssueData["url"], extract: issueEntities, }, { kind: "tag-key", - url: "/api/0/organizations/{organization_id_or_slug}/issues/{issue_id}/" satisfies GetOrganizationIssueData["url"], + url: "/organizations/{organization_id_or_slug}/issues/{issue_id}/" satisfies GetOrganizationIssueData["url"], extract: tagKeyEntities, }, { kind: "issue", - url: "/api/0/organizations/{organization_id_or_slug}/shortids/{issue_id}/" satisfies ResolveOrganizationShortIdData["url"], + url: "/organizations/{organization_id_or_slug}/shortids/{issue_id}/" satisfies ResolveOrganizationShortIdData["url"], extract: (body) => issueEntities( typeof body === "object" && body !== null @@ -325,107 +326,107 @@ const COMPLETION_SOURCES: readonly CompletionSource[] = [ }, { kind: "dashboard", - url: "/api/0/organizations/{organization_id_or_slug}/dashboards/" satisfies ListOrganizationDashboardsData["url"], + url: "/organizations/{organization_id_or_slug}/dashboards/" satisfies ListOrganizationDashboardsData["url"], extract: dashboardEntities, }, { kind: "dashboard", - url: "/api/0/organizations/{organization_id_or_slug}/dashboards/{dashboard_id}/" satisfies GetOrganizationDashboardData["url"], + url: "/organizations/{organization_id_or_slug}/dashboards/{dashboard_id}/" satisfies GetOrganizationDashboardData["url"], extract: dashboardEntities, }, { kind: "environment", - url: "/api/0/organizations/{organization_id_or_slug}/environments/" satisfies ListOrganizationEnvironmentsData["url"], + url: "/organizations/{organization_id_or_slug}/environments/" satisfies ListOrganizationEnvironmentsData["url"], extract: fieldEntities("name"), }, { kind: "environment", - url: "/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/environments/" satisfies ListProjectEnvironmentsData["url"], + url: "/projects/{organization_id_or_slug}/{project_id_or_slug}/environments/" satisfies ListProjectEnvironmentsData["url"], extract: fieldEntities("name"), }, { kind: "release", - url: "/api/0/organizations/{organization_id_or_slug}/releases/" satisfies ListOrganizationReleasesData["url"], + url: "/organizations/{organization_id_or_slug}/releases/" satisfies ListOrganizationReleasesData["url"], extract: fieldEntities("version", "shortVersion"), }, { kind: "release", - url: "/api/0/organizations/{organization_id_or_slug}/releases/{version}/" satisfies GetOrganizationReleaseData["url"], + url: "/organizations/{organization_id_or_slug}/releases/{version}/" satisfies GetOrganizationReleaseData["url"], extract: fieldEntities("version", "shortVersion"), }, { kind: "release", - url: "/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/releases/" satisfies ListProjectReleasesData["url"], + url: "/projects/{organization_id_or_slug}/{project_id_or_slug}/releases/" satisfies ListProjectReleasesData["url"], extract: fieldEntities("version", "shortVersion"), }, { kind: "team", - url: "/api/0/organizations/{organization_id_or_slug}/teams/" satisfies ListOrganizationTeamsData["url"], + url: "/organizations/{organization_id_or_slug}/teams/" satisfies ListOrganizationTeamsData["url"], extract: fieldEntities("slug", "name"), }, { kind: "team", - url: "/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/teams/" satisfies ListProjectTeamsData["url"], + url: "/projects/{organization_id_or_slug}/{project_id_or_slug}/teams/" satisfies ListProjectTeamsData["url"], extract: fieldEntities("slug", "name"), }, { kind: "owner", - url: "/api/0/organizations/{organization_id_or_slug}/teams/" satisfies ListOrganizationTeamsData["url"], + url: "/organizations/{organization_id_or_slug}/teams/" satisfies ListOrganizationTeamsData["url"], extract: teamOwnerEntities, }, { kind: "owner", - url: "/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/teams/" satisfies ListProjectTeamsData["url"], + url: "/projects/{organization_id_or_slug}/{project_id_or_slug}/teams/" satisfies ListProjectTeamsData["url"], extract: teamOwnerEntities, }, { kind: "monitor", - url: "/api/0/organizations/{organization_id_or_slug}/monitors/" satisfies ListOrganizationMonitorsData["url"], + url: "/organizations/{organization_id_or_slug}/monitors/" satisfies ListOrganizationMonitorsData["url"], extract: fieldEntities("slug", "name"), }, { kind: "monitor", - url: "/api/0/organizations/{organization_id_or_slug}/monitors/{monitor_id_or_slug}/" satisfies GetOrganizationMonitorData["url"], + url: "/organizations/{organization_id_or_slug}/monitors/{monitor_id_or_slug}/" satisfies GetOrganizationMonitorData["url"], extract: fieldEntities("slug", "name"), }, { kind: "monitor", - url: "/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/monitors/{monitor_id_or_slug}/" satisfies GetProjectMonitorData["url"], + url: "/projects/{organization_id_or_slug}/{project_id_or_slug}/monitors/{monitor_id_or_slug}/" satisfies GetProjectMonitorData["url"], extract: fieldEntities("slug", "name"), }, { kind: "workflow", - url: "/api/0/organizations/{organization_id_or_slug}/workflows/" satisfies ListOrganizationWorkflowsData["url"], + url: "/organizations/{organization_id_or_slug}/workflows/" satisfies ListOrganizationWorkflowsData["url"], extract: fieldEntities("id", "name"), }, { kind: "workflow", - url: "/api/0/organizations/{organization_id_or_slug}/workflows/{workflow_id}/" satisfies GetOrganizationWorkflowData["url"], + url: "/organizations/{organization_id_or_slug}/workflows/{workflow_id}/" satisfies GetOrganizationWorkflowData["url"], extract: fieldEntities("id", "name"), }, { kind: "integration", - url: "/api/0/organizations/{organization_id_or_slug}/integrations/" satisfies ListOrganizationIntegrationsData["url"], + url: "/organizations/{organization_id_or_slug}/integrations/" satisfies ListOrganizationIntegrationsData["url"], extract: integrationEntities, }, { kind: "sentry-app", - url: "/api/0/organizations/{organization_id_or_slug}/sentry-app-installations/" satisfies ListOrganizationSentryAppInstallationsData["url"], + url: "/organizations/{organization_id_or_slug}/sentry-app-installations/" satisfies ListOrganizationSentryAppInstallationsData["url"], extract: sentryAppEntities, }, { kind: "app-link-field", - url: "/api/0/organizations/{organization_id_or_slug}/sentry-app-components/" satisfies ListOrganizationSentryAppComponentsData["url"], + url: "/organizations/{organization_id_or_slug}/sentry-app-components/" satisfies ListOrganizationSentryAppComponentsData["url"], extract: appLinkFieldEntities, }, { kind: "dsn", - url: "/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/keys/" satisfies ListProjectKeysData["url"], + url: "/projects/{organization_id_or_slug}/{project_id_or_slug}/keys/" satisfies ListProjectKeysData["url"], extract: dsnEntities, }, ...Object.entries(ATTRIBUTE_KINDS).map(([dataset, kind]) => ({ kind, - url: "/api/0/organizations/{organization_id_or_slug}/trace-items/attributes/" satisfies ListOrganizationTraceItemAttributesData["url"], + url: "/organizations/{organization_id_or_slug}/trace-items/attributes/" satisfies ListOrganizationTraceItemAttributesData["url"], query: { dataset }, extract: attributeEntities, })), @@ -435,13 +436,14 @@ const COMPLETION_SOURCES: readonly CompletionSource[] = [ const TEMPLATE_PARAM_RE = /\{(\w+)\}/g; /** - * A regex matching paths that end in `template`. The organization + * A regex matching paths that end in the API base path and `template`. The organization * parameter is the first capture group; other parameters match any * segment. */ function templatePattern(template: string): RegExp { - const pattern = template.replace(TEMPLATE_PARAM_RE, (_, name) => - name === "organization_id_or_slug" ? "([^/]+)" : "[^/]+", + const pattern = `${API_BASE_PATH}${template}`.replace( + TEMPLATE_PARAM_RE, + (_, name) => (name === "organization_id_or_slug" ? "([^/]+)" : "[^/]+"), ); return new RegExp(`${pattern}$`); } diff --git a/packages/cli/src/lib/api/infrastructure.ts b/packages/cli/src/lib/api/infrastructure.ts index e6b1305bb..f85dda632 100644 --- a/packages/cli/src/lib/api/infrastructure.ts +++ b/packages/cli/src/lib/api/infrastructure.ts @@ -1268,7 +1268,8 @@ export async function prepareRawApiRequest( const isStringBody = typeof body === "string"; const request = await createSentryClient(config).client.prepareRequest({ method, - url: buildSentryApiUrl("", appendSearchParams(endpoint, params)), + // Relative to the API base path, which the client adds. + url: `/${appendSearchParams(endpoint, params).replace(/^\/+/, "")}`, query, body, headers: isStringBody ? { "Content-Type": null, ...headers } : headers, diff --git a/packages/cli/src/lib/api/issues.ts b/packages/cli/src/lib/api/issues.ts index 2ea242b08..78986c5ba 100644 --- a/packages/cli/src/lib/api/issues.ts +++ b/packages/cli/src/lib/api/issues.ts @@ -17,6 +17,7 @@ import { updateOrganizationIssue, updateOrganizationIssues, } from "@sentry/api"; +import { API_BASE_PATH } from "@sentry/api/client"; import pLimit from "p-limit"; import type { SentryIssue } from "../../types/index.js"; @@ -736,7 +737,7 @@ export async function getSharedIssue( ): Promise<{ id: string }> { const path = `organizations/${encodeURIComponent(orgSlug)}/shared/issues/${encodeURIComponent(shareId)}/`; const normalizedBase = baseUrl.replace(TRAILING_SLASH_RE, ""); - const url = `${normalizedBase}/api/0/${path}`; + const url = `${normalizedBase}${API_BASE_PATH}/${path}`; const headers = new Headers({ "Content-Type": "application/json" }); // URL-scoped: headers only attach when `url`'s origin matches the trusted // host, so IAP tokens etc. can't leak to an attacker-controlled share URL. diff --git a/packages/cli/src/lib/api/preprod-artifacts.ts b/packages/cli/src/lib/api/preprod-artifacts.ts index 8c4dcb4f5..db560a9cd 100644 --- a/packages/cli/src/lib/api/preprod-artifacts.ts +++ b/packages/cli/src/lib/api/preprod-artifacts.ts @@ -27,6 +27,7 @@ import { type UploadProjectPreprodArtifactSnapshotData, uploadProjectPreprodArtifactSnapshot, } from "@sentry/api"; +import { API_BASE_PATH } from "@sentry/api/client"; import { array, boolean, @@ -614,7 +615,7 @@ export async function openSnapshotArchive( snapshotId: string, ): Promise { const regionUrl = stripTrailingSlashes(await resolveOrgRegion(org)); - const url = `${regionUrl}/api/0/${snapshotArchiveEndpoint( + const url = `${regionUrl}${API_BASE_PATH}/${snapshotArchiveEndpoint( org, snapshotId, )}?download`; diff --git a/packages/cli/src/lib/event-attachments.ts b/packages/cli/src/lib/event-attachments.ts index 90b920f7d..b6fec1d93 100644 --- a/packages/cli/src/lib/event-attachments.ts +++ b/packages/cli/src/lib/event-attachments.ts @@ -7,6 +7,7 @@ import { inlineCode, text } from "@sentry/toolkit-core/markdown"; import type { EventAttachmentDetailsResponse } from "@sentry/api"; +import { API_BASE_PATH } from "@sentry/api/client"; import type { EventViewAttachment, SentryEvent } from "../types/index.js"; import { listEventAttachments } from "./api-client.js"; import { sentry } from "./app-identity.js"; @@ -78,7 +79,7 @@ function attachmentDownloadUrl( ] .map(encodeURIComponent) .join("/"); - return `${origin}/api/0/${path}/?download=1`; + return `${origin}${API_BASE_PATH}/${path}/?download=1`; } async function resolveAttachmentApiBase(org: string): Promise { diff --git a/packages/cli/src/lib/issue-links.ts b/packages/cli/src/lib/issue-links.ts index 1901ab310..d871f92d7 100644 --- a/packages/cli/src/lib/issue-links.ts +++ b/packages/cli/src/lib/issue-links.ts @@ -3,6 +3,7 @@ * and Sentry Apps. These operations leave the Sentry issue's status unchanged. */ +import { API_BASE_PATH } from "@sentry/api/client"; import { type AppIssueLink, findAppIssueLink, @@ -228,7 +229,7 @@ async function invalidateIssueLinks( ): Promise { const regionUrl = await resolveOrgRegion(options.orgSlug); const base = getApiBaseUrl(); - const issuePath = `/api/0/organizations/${encodeURIComponent(options.orgSlug)}/issues/${encodeURIComponent(options.issueId)}/`; + const issuePath = `${API_BASE_PATH}/organizations/${encodeURIComponent(options.orgSlug)}/issues/${encodeURIComponent(options.issueId)}/`; await Promise.all([ invalidateCachedResponsesMatching(new URL(issuePath, regionUrl).href), invalidateCachedResponsesMatching(new URL(issuePath, base).href), diff --git a/packages/cli/src/lib/sentry-client.ts b/packages/cli/src/lib/sentry-client.ts index 140f06568..b3a30743f 100644 --- a/packages/cli/src/lib/sentry-client.ts +++ b/packages/cli/src/lib/sentry-client.ts @@ -572,8 +572,8 @@ export function getSdkConfig( : regionUrl; return { - // SDK functions already include /api/0/ in their URL paths, - // so baseUrl should be the plain region URL without /api/0. + // The SDK adds the API base path to its operation paths, so baseUrl + // is the plain region URL. baseUrl: normalizedBase, fetch: getAuthenticatedFetch(options), throwOnError: false as const, diff --git a/packages/cli/test/script/generate-api-schema.test.ts b/packages/cli/test/script/generate-api-schema.test.ts index 033819d71..19904638c 100644 --- a/packages/cli/test/script/generate-api-schema.test.ts +++ b/packages/cli/test/script/generate-api-schema.test.ts @@ -7,7 +7,7 @@ const events = { functionName: "listOrganizationEvents", operationId: "List an Organization's Events", method: "GET", - path: "/api/0/organizations/{organization_id_or_slug}/events/", + path: "/organizations/{organization_id_or_slug}/events/", summary: "Query events", description: "Select the dataset to query.", tags: ["Explore"], @@ -57,7 +57,7 @@ describe("SDK catalog projection", () => { { ...events, operationId: "A", functionName: "deprecatedListEvents" }, { ...events, - path: "/api/0/projects/{org}/{project}/alerts/{alert_id}/", + path: "/projects/{org}/{project}/alerts/{alert_id}/", operationId: "Get a project alert", }, ]); diff --git a/packages/cli/test/script/source-api-usage.test.ts b/packages/cli/test/script/source-api-usage.test.ts index a1ac0f5f8..6b9499a86 100644 --- a/packages/cli/test/script/source-api-usage.test.ts +++ b/packages/cli/test/script/source-api-usage.test.ts @@ -8,22 +8,22 @@ const operations: SourceApiOperation[] = [ { functionName: "listOrganizationEvents", method: "GET", - path: "/api/0/organizations/{organization_id_or_slug}/events/", + path: "/organizations/{organization_id_or_slug}/events/", }, { functionName: "getOrganizationTrace", method: "GET", - path: "/api/0/organizations/{organization_id_or_slug}/trace/{trace_id}/", + path: "/organizations/{organization_id_or_slug}/trace/{trace_id}/", }, { functionName: "deleteProject", method: "DELETE", - path: "/api/0/projects/{organization_id_or_slug}/{project_id_or_slug}/", + path: "/projects/{organization_id_or_slug}/{project_id_or_slug}/", }, { functionName: "deleteOrganizationWorkflow", method: "DELETE", - path: "/api/0/organizations/{organization_id_or_slug}/workflows/{workflow_id}/", + path: "/organizations/{organization_id_or_slug}/workflows/{workflow_id}/", }, ]; @@ -169,7 +169,7 @@ describe("source API usage", () => { { ...operations[0], functionName: "anotherEvents", - path: "/api/0/organizations/{org}/events/", + path: "/organizations/{org}/events/", }, ]); const result = analysis.exports.get("src/commands/raw.ts")!; diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index ecc3f46e1..88e55080a 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -17,6 +17,7 @@ import { } from "@sentry/api"; import { operations } from "@sentry/api/operations"; import { + API_BASE_PATH, ApiNotFoundError, apiPath, type ApiTarget, @@ -658,7 +659,7 @@ export class SentryApiService { this.utmSource = utmSource; this.host = host; this.protocol = protocol; - this.apiPrefix = `${protocol}://${host}/api/0`; + this.apiPrefix = `${protocol}://${host}${API_BASE_PATH}`; } /** @@ -671,7 +672,7 @@ export class SentryApiService { */ setHost(host: string) { this.host = host; - this.apiPrefix = `${this.protocol}://${this.host}/api/0`; + this.apiPrefix = `${this.protocol}://${this.host}${API_BASE_PATH}`; } /** @@ -5119,8 +5120,8 @@ export class SentryApiService { const response = imageUrl.startsWith("https://") ? await fetch(imageUrl) : await this.request( - imageUrl.startsWith("/api/0") - ? imageUrl.slice("/api/0".length) + imageUrl.startsWith(API_BASE_PATH) + ? imageUrl.slice(API_BASE_PATH.length) : imageUrl, ); if (!response.ok) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f86d541ad..a5c12c315 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.83.0 - version: 1.83.0 + specifier: 2.0.0 + version: 2.0.0 '@sentry/cloudflare': specifier: 11.12.0 version: 11.12.0 @@ -504,7 +504,7 @@ importers: version: 2.1.7(hono@4.13.18) '@sentry/api': specifier: 'catalog:' - version: 1.83.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) + version: 2.0.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) '@sentry/core': specifier: 'catalog:' version: 11.12.0 @@ -838,7 +838,7 @@ importers: version: 2.4.0 '@sentry/api': specifier: 'catalog:' - version: 1.83.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) + version: 2.0.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) '@sentry/core': specifier: 'catalog:' version: 11.12.0 @@ -1015,7 +1015,7 @@ importers: dependencies: '@sentry/api': specifier: 'catalog:' - version: 1.83.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) + version: 2.0.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 @@ -4149,8 +4149,8 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@sentry/api@1.83.0': - resolution: {integrity: sha512-zUyKYoW86hPt7WmDsmy3oQdUsQgMbAiTOl70U1VyXzD8bANuvWGiAH/dJAzLhHd6hj9Qq/vkYbqVBxTkn7e6Ag==} + '@sentry/api@2.0.0': + resolution: {integrity: sha512-G7QUnqRExbwywo2P/q2XCmpO+FxKxDYyK85uflfFtRE2LfGst0rDgyzxyk2NIUmRm1KvSbvd8bU7NndTbMNctg==} engines: {node: '>=22'} peerDependencies: typescript: '*' @@ -11415,7 +11415,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@sentry/api@1.83.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7)': + '@sentry/api@2.0.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7)': dependencies: http-cache-semantics: 4.2.0 p-limit: 7.3.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6d4c78a98..26ea6b6da 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -51,7 +51,7 @@ catalog: "@modelcontextprotocol/server": 2.4.0 "@radix-ui/react-accordion": ^1.2.20 "@radix-ui/react-slot": ^1.2.3 - "@sentry/api": 1.83.0 + "@sentry/api": 2.0.0 "@sentry/cloudflare": 11.12.0 "@sentry/core": 11.12.0 "@sentry/node": 11.12.0 @@ -169,7 +169,7 @@ minimumReleaseAgeExclude: - "@oxlint/binding-win32-x64-msvc@1.87.0" - oxfmt@0.72.0 - oxlint@1.87.0 - - "@sentry/api@1.83.0" + - "@sentry/api@2.0.0" - miniflare@5.20261101.0-beta - wrangler@4.156.1 - "@sentry/symbolic@14.0.0-alpha.5"