From 8bc627e96f8a73b613489005dc0a98c5699a49b3 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:57:25 +0000 Subject: [PATCH] ref(cli): Bump @sentry/api to 1.49.0 and use trace-logs and dashboard revision operations @sentry/api 1.49.0 publishes the trace-logs endpoint (getsentry/sentry#130357) and the dashboard revision list and restore endpoints (getsentry/sentry#130358), so these calls move from apiRequestToRegion to the generated operations: - listTraceLogs -> listOrganizationTraceLogs - listDashboardRevisionsPaginated -> listOrganizationDashboardRevisions - restoreDashboardRevision -> restoreOrganizationDashboardRevision The CLI's valibot schemas still validate each response, and pagination, region routing and error handling stay the same. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- packages/cli/src/lib/api/dashboards.ts | 66 +++++----- packages/cli/src/lib/api/logs.ts | 37 +++--- .../cli/test/lib/api-client.coverage.test.ts | 3 +- packages/cli/test/lib/api/dashboards.test.ts | 114 ++++++++++++++---- pnpm-lock.yaml | 16 +-- pnpm-workspace.yaml | 4 +- 6 files changed, 155 insertions(+), 85 deletions(-) diff --git a/packages/cli/src/lib/api/dashboards.ts b/packages/cli/src/lib/api/dashboards.ts index 8dd000e6b..7925ed891 100644 --- a/packages/cli/src/lib/api/dashboards.ts +++ b/packages/cli/src/lib/api/dashboards.ts @@ -8,10 +8,12 @@ import { createOrganizationDashboard, getOrganizationDashboard, + listOrganizationDashboardRevisions, listOrganizationDashboards, type ListOrganizationEventsTimeseriesData, listOrganizationEvents, listOrganizationEventsTimeseries, + restoreOrganizationDashboardRevision, updateOrganizationDashboard, } from "@sentry/api"; import { resolveEventsSort } from "@sentry/toolkit-core/events-sort"; @@ -43,18 +45,16 @@ import type { EventsDataset } from "../datasets.js"; import { reportCliError } from "../error-reporting.js"; import { ApiError, stringifyUnknown } from "../errors.js"; -import { resolveOrgRegion } from "../region.js"; import { getSdkClientConfig } from "../sentry-sdk-client.js"; import { - apiRequestToRegion, getOrgSdkConfig, ORG_FANOUT_CONCURRENCY, type PaginatedResponse, - parseLinkHeader, unwrapPaginatedResult, unwrapResult, unwrapValidatedResult, + validateResponse, } from "./infrastructure.js"; /** @@ -192,29 +192,26 @@ export async function listDashboardRevisionsPaginated( dashboardId: string, options: { perPage?: number; cursor?: string } = {}, ): Promise> { - const regionUrl = await resolveOrgRegion(orgSlug); - const params: Record = { - per_page: options.perPage, - cursor: options.cursor, - }; - - const { data, headers } = await apiRequestToRegion( - regionUrl, - `/organizations/${orgSlug}/dashboards/${dashboardId}/revisions/`, - { params, schema: array(DashboardRevisionSchema) }, + const config = getOrgSdkConfig(); + const result = await listOrganizationDashboardRevisions({ + ...config, + path: { + organization_id_or_slug: orgSlug, + dashboard_id: dashboardId as unknown as number, + }, + query: { per_page: options.perPage, cursor: options.cursor }, + }); + const { response, ...page } = unwrapPaginatedResult( + result, + `Failed to list revisions for dashboard '${dashboardId}'`, ); - - const { nextCursor, prevCursor } = parseLinkHeader( - headers.get("link") ?? null, + const data = validateResponse( + array(DashboardRevisionSchema), + page.data, + `/organizations/${orgSlug}/dashboards/${dashboardId}/revisions/`, + response?.status ?? 0, ); - const out: PaginatedResponse = { data }; - if (nextCursor !== undefined) { - out.nextCursor = nextCursor; - } - if (prevCursor !== undefined) { - out.prevCursor = prevCursor; - } - return out; + return { ...page, data }; } /** @@ -230,14 +227,21 @@ export async function restoreDashboardRevision( dashboardId: string, revisionId: string, ): Promise { - const regionUrl = await resolveOrgRegion(orgSlug); - const encodedRevisionId = encodeURIComponent(revisionId); - const { data } = await apiRequestToRegion( - regionUrl, - `/organizations/${orgSlug}/dashboards/${dashboardId}/revisions/${encodedRevisionId}/restore/`, - { method: "POST", schema: DashboardDetailSchema }, + const config = getOrgSdkConfig(); + const result = await restoreOrganizationDashboardRevision({ + ...config, + path: { + organization_id_or_slug: orgSlug, + dashboard_id: dashboardId as unknown as number, + revision_id: revisionId as unknown as number, + }, + }); + return unwrapValidatedResult( + result, + `Failed to restore revision '${revisionId}' of dashboard '${dashboardId}'`, + DashboardDetailSchema, + `/organizations/${orgSlug}/dashboards/${dashboardId}/revisions/${revisionId}/restore/`, ); - return data; } // --------------------------------------------------------------------------- diff --git a/packages/cli/src/lib/api/logs.ts b/packages/cli/src/lib/api/logs.ts index c2498bc99..4905ce2c0 100644 --- a/packages/cli/src/lib/api/logs.ts +++ b/packages/cli/src/lib/api/logs.ts @@ -5,7 +5,7 @@ * including trace-associated logs. */ -import { listOrganizationEvents } from "@sentry/api"; +import { listOrganizationEvents, listOrganizationTraceLogs } from "@sentry/api"; // oxlint-disable-next-line sentry-cli/no-namespace-import -- Sentry SDK recommends namespace import import * as Sentry from "@sentry/node"; import { type GenericSchema, safeParse } from "valibot"; @@ -21,17 +21,15 @@ import { } from "../../types/index.js"; import { ApiError } from "../errors.js"; import { decodeUuidV7Timestamp } from "../hex-id.js"; -import { resolveOrgRegion } from "../region.js"; import { LOG_RETENTION_PERIOD } from "../retention.js"; import { isAllDigits } from "../utils.js"; import { API_MAX_PER_PAGE, - apiRequestToRegion, getOrgSdkConfig, paginate, - parseLinkHeader, unwrapPaginatedResult, unwrapResult, + validateResponse, } from "./infrastructure.js"; import { getTraceItemDetail } from "./traces.js"; @@ -478,16 +476,16 @@ export async function listTraceLogs( traceId: string, options: ListTraceLogsOptions = {}, ): Promise { - const regionUrl = await resolveOrgRegion(orgSlug); + const config = getOrgSdkConfig(); const { data } = await paginate( options, async (perPage, cursor) => { - const { data: response, headers } = await apiRequestToRegion<{ - data: TraceLog[]; - }>(regionUrl, `/organizations/${orgSlug}/trace-logs/`, { - params: { - traceId, + const result = await listOrganizationTraceLogs({ + ...config, + path: { organization_id_or_slug: orgSlug }, + query: { + traceId: [traceId], statsPeriod: options.start || options.end ? undefined @@ -497,13 +495,21 @@ export async function listTraceLogs( per_page: perPage, cursor, query: options.query, - sort: toApiSort(options.sort), + sort: [toApiSort(options.sort)], }, - schema: TraceLogsResponseSchema, }); - const { nextCursor } = parseLinkHeader(headers.get("link") ?? null); - return { data: response.data, nextCursor }; + const page = unwrapPaginatedResult( + result, + "Failed to list trace logs", + ); + const response = validateResponse( + TraceLogsResponseSchema, + page.data, + `/organizations/${orgSlug}/trace-logs/`, + page.response?.status ?? 0, + ); + return { data: response.data, nextCursor: page.nextCursor }; }, API_MAX_PER_PAGE, ); @@ -518,9 +524,6 @@ export async function listTraceLogs( * to enumerate field names. This is the same endpoint the Sentry UI uses when * expanding a log row to show its full attribute set. * - * The endpoint is EXPERIMENTAL and not yet in @sentry/api; called directly via - * apiRequestToRegion following the same pattern as listTraceLogs. - * * @param orgSlug - Organization slug * @param projectSlug - Project slug * @param logId - The sentry.item_id of the log entry diff --git a/packages/cli/test/lib/api-client.coverage.test.ts b/packages/cli/test/lib/api-client.coverage.test.ts index 2faf0ba64..d08250335 100644 --- a/packages/cli/test/lib/api-client.coverage.test.ts +++ b/packages/cli/test/lib/api-client.coverage.test.ts @@ -2059,8 +2059,9 @@ describe("logs.ts (detailed)", () => { const req = new Request(input!, init); const url = new URL(req.url); expect(url.pathname).toContain("/organizations/test-org/trace-logs/"); - expect(url.searchParams.get("traceId")).toBe("abc123def456"); + expect(url.searchParams.getAll("traceId")).toEqual(["abc123def456"]); expect(url.searchParams.get("statsPeriod")).toBe("14d"); + expect(url.searchParams.getAll("sort")).toEqual(["-timestamp"]); return new Response(JSON.stringify(traceLogsResponse), { status: 200, headers: { "Content-Type": "application/json" }, diff --git a/packages/cli/test/lib/api/dashboards.test.ts b/packages/cli/test/lib/api/dashboards.test.ts index 83eb01137..d755a0610 100644 --- a/packages/cli/test/lib/api/dashboards.test.ts +++ b/packages/cli/test/lib/api/dashboards.test.ts @@ -8,6 +8,7 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; import { computeOptimalInterval, + listDashboardRevisionsPaginated, periodToSeconds, queryAllWidgets, restoreDashboardRevision, @@ -19,25 +20,28 @@ import type { } from "../../../src/types/dashboard.js"; const { - apiRequestToRegionMock, + listOrganizationDashboardRevisionsMock, listOrganizationEventsMock, listOrganizationEventsTimeseriesMock, reportCliErrorMock, - resolveOrgRegionMock, + restoreOrganizationDashboardRevisionMock, } = vi.hoisted(() => ({ - apiRequestToRegionMock: vi.fn(), + listOrganizationDashboardRevisionsMock: vi.fn(), listOrganizationEventsMock: vi.fn(), listOrganizationEventsTimeseriesMock: vi.fn(), reportCliErrorMock: vi.fn(), - resolveOrgRegionMock: vi.fn(), + restoreOrganizationDashboardRevisionMock: vi.fn(), })); vi.mock("@sentry/api", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, + listOrganizationDashboardRevisions: listOrganizationDashboardRevisionsMock, listOrganizationEvents: listOrganizationEventsMock, listOrganizationEventsTimeseries: listOrganizationEventsTimeseriesMock, + restoreOrganizationDashboardRevision: + restoreOrganizationDashboardRevisionMock, }; }); @@ -45,44 +49,102 @@ vi.mock("../../../src/lib/error-reporting.js", () => ({ reportCliError: reportCliErrorMock, })); -vi.mock("../../../src/lib/region.js", async (importOriginal) => { - const actual = - await importOriginal(); - return { ...actual, resolveOrgRegion: resolveOrgRegionMock }; -}); - -vi.mock("../../../src/lib/api/infrastructure.js", async (importOriginal) => { - const actual = - await importOriginal< - typeof import("../../../src/lib/api/infrastructure.js") - >(); - return { ...actual, apiRequestToRegion: apiRequestToRegionMock }; -}); - // --------------------------------------------------------------------------- // restoreDashboardRevision // --------------------------------------------------------------------------- describe("restoreDashboardRevision", () => { - test("posts to the revision restore endpoint", async () => { + test("restores the revision through the generated operation", async () => { const dashboard = { id: "123", title: "Restored Dashboard", widgets: [], } as DashboardDetail; - resolveOrgRegionMock.mockResolvedValue("https://us.sentry.io"); - apiRequestToRegionMock.mockResolvedValue({ data: dashboard }); + restoreOrganizationDashboardRevisionMock.mockResolvedValue({ + data: dashboard, + response: new Response(), + }); await expect( restoreDashboardRevision("test-org", "123", "42"), - ).resolves.toBe(dashboard); + ).resolves.toEqual(dashboard); - expect(apiRequestToRegionMock).toHaveBeenCalledWith( - "https://us.sentry.io", - "/organizations/test-org/dashboards/123/revisions/42/restore/", - expect.objectContaining({ method: "POST" }), + expect(restoreOrganizationDashboardRevisionMock).toHaveBeenCalledWith( + expect.objectContaining({ + path: { + organization_id_or_slug: "test-org", + dashboard_id: "123", + revision_id: "42", + }, + }), ); }); + + test("names the revision when the API rejects the restore", async () => { + restoreOrganizationDashboardRevisionMock.mockResolvedValue({ + error: { detail: "Revision not found" }, + response: new Response(null, { status: 404, statusText: "Not Found" }), + }); + + const error = await restoreDashboardRevision("test-org", "123", "42").catch( + (e: unknown) => e, + ); + expect(error).toBeInstanceOf(ApiError); + expect(error).toMatchObject({ status: 404 }); + expect((error as ApiError).message).toContain("revision '42'"); + }); +}); + +// --------------------------------------------------------------------------- +// listDashboardRevisionsPaginated +// --------------------------------------------------------------------------- + +describe("listDashboardRevisionsPaginated", () => { + const revision = { + id: "7", + title: "Before edit", + dateCreated: "2026-11-01T00:00:00Z", + createdBy: null, + source: "edit", + }; + + test("returns validated revisions and the Link cursors", async () => { + listOrganizationDashboardRevisionsMock.mockResolvedValue({ + data: [revision], + response: new Response(null, { + headers: { + link: + '; rel="previous"; results="false"; cursor="0:0:1", ' + + '; rel="next"; results="true"; cursor="0:25:0"', + }, + }), + }); + + const page = await listDashboardRevisionsPaginated("test-org", "123", { + perPage: 25, + cursor: "0:0:0", + }); + + expect(page.data).toEqual([revision]); + expect(page.nextCursor).toBe("0:25:0"); + expect(listOrganizationDashboardRevisionsMock).toHaveBeenCalledWith( + expect.objectContaining({ + path: { organization_id_or_slug: "test-org", dashboard_id: "123" }, + query: { per_page: 25, cursor: "0:0:0" }, + }), + ); + }); + + test("rejects a response that does not match the revision schema", async () => { + listOrganizationDashboardRevisionsMock.mockResolvedValue({ + data: [{ id: 7 }], + response: new Response(), + }); + + await expect( + listDashboardRevisionsPaginated("test-org", "123"), + ).rejects.toThrow("Unexpected response format"); + }); }); // --------------------------------------------------------------------------- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9cde43239..0916d347d 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.46.0 - version: 1.46.0 + specifier: 1.49.0 + version: 1.49.0 '@sentry/cloudflare': specifier: 11.6.0 version: 11.6.0 @@ -513,7 +513,7 @@ importers: version: 2.1.5(hono@4.13.13) '@sentry/api': specifier: 'catalog:' - version: 1.46.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) + version: 1.49.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 @@ -865,7 +865,7 @@ importers: version: 2.4.0 '@sentry/api': specifier: 'catalog:' - version: 1.46.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) + version: 1.49.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 @@ -1048,7 +1048,7 @@ importers: dependencies: '@sentry/api': specifier: 'catalog:' - version: 1.46.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) + version: 1.49.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 @@ -4275,8 +4275,8 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@sentry/api@1.46.0': - resolution: {integrity: sha512-B3QSyPIgLVY4+ndIAdvB50FLDA6XeDmNtyY8FZOb0vJKf6XVZeOemOvDzWBgJPvyVpGs2mkh5+b+VhBq7+ASrw==} + '@sentry/api@1.49.0': + resolution: {integrity: sha512-cvBLSuh3+6CJiVKmKJrh7ZQLXznwvHdI7kG5fryyHak4UhVsyF65kUFb4EqztgaEgPZcMckA04Pa/X+QTW9rdw==} engines: {node: '>=22'} peerDependencies: typescript: '*' @@ -12112,7 +12112,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@sentry/api@1.46.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7)': + '@sentry/api@1.49.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 b00a90285..637ee0bcd 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.46.0 + "@sentry/api": 1.49.0 "@sentry/cloudflare": 11.6.0 "@sentry/core": 11.6.0 "@sentry/node": 11.6.0 @@ -168,7 +168,7 @@ minimumReleaseAgeExclude: - "@oxlint/binding-win32-x64-msvc@1.87.0" - oxfmt@0.72.0 - oxlint@1.87.0 - - "@sentry/api@1.46.0" + - "@sentry/api@1.49.0" - miniflare@5.20261101.0-beta - wrangler@4.156.1 - "@sentry/symbolic@14.0.0-alpha.5"