diff --git a/packages/cli/src/commands/explore.ts b/packages/cli/src/commands/explore.ts index 90b2521c5..e8d683b9e 100644 --- a/packages/cli/src/commands/explore.ts +++ b/packages/cli/src/commands/explore.ts @@ -37,7 +37,11 @@ import { hasPreviousPage, resolveCursor, } from "../lib/db/pagination.js"; -import { buildDatasetParser, datasetDisplayName } from "../lib/datasets.js"; +import { + buildDatasetParser, + datasetDisplayName, + isEventsDataset, +} from "../lib/datasets.js"; import { toSearchQueryError, ValidationError } from "../lib/errors.js"; import { toExploreFieldError } from "../lib/explore-errors.js"; import { @@ -514,8 +518,15 @@ function resolveEventsQuery( legacyTransactions && userQuery === IS_TRANSACTION_FILTER ? undefined : userQuery; + const apiDataset = legacyTransactions ? "spans" : dataset; + if (!isEventsDataset(apiDataset)) { + throw new ValidationError( + `The ${datasetDisplayName(apiDataset)} dataset can't be queried through the Events API.`, + "dataset", + ); + } return { - dataset: legacyTransactions ? "spans" : dataset, + dataset: apiDataset, query: scope && scopedQuery ? `${scope} (${scopedQuery})` : scope || scopedQuery, }; diff --git a/packages/cli/src/commands/trace/view.ts b/packages/cli/src/commands/trace/view.ts index c0cb654e8..481d6cca4 100644 --- a/packages/cli/src/commands/trace/view.ts +++ b/packages/cli/src/commands/trace/view.ts @@ -45,7 +45,7 @@ import { type BoundArguments, boundArguments, } from "../../lib/positional-parse.js"; -import { resolveOrg, toNumericId } from "../../lib/resolve-target.js"; +import { resolveOrg } from "../../lib/resolve-target.js"; import { buildTraceUrl } from "../../lib/sentry-web-urls.js"; import { setOrgProjectContext } from "../../lib/telemetry.js"; import { @@ -568,19 +568,15 @@ export const viewCommand = buildCommand({ ? fieldSelection.additionalAttributes : undefined; - // Resolve numeric project ID for API-level filtering - let numericProjectId: number | undefined; + // Keep failing on an unknown project. The trace endpoint ignores + // `project` and returns spans from every project in the trace. if (projectFilter) { - const projectData = await getProject(org, projectFilter); - numericProjectId = toNumericId(projectData.id); + await getProject(org, projectFilter); } // The replay lookup runs alongside the trace fetch to avoid adding latency. const [spans, replayId] = await Promise.all([ - getDetailedTrace(org, traceId, { - additionalAttributes, - projectId: numericProjectId, - }), + getDetailedTrace(org, traceId, { additionalAttributes }), lookupTraceReplay(org, traceId), ]); diff --git a/packages/cli/src/lib/api/alerts.ts b/packages/cli/src/lib/api/alerts.ts index 7933258e5..8d45866d3 100644 --- a/packages/cli/src/lib/api/alerts.ts +++ b/packages/cli/src/lib/api/alerts.ts @@ -16,6 +16,7 @@ import { getOrganizationWorkflow, type GetOrganizationWorkflowResponse, listOrganizationDetectors, + listOrganizationWorkflows, updateOrganizationDetector, updateOrganizationWorkflow, type UpdateOrganizationDetectorResponse, @@ -33,12 +34,9 @@ import { metricAlertSource, } from "../../types/metric-alert.js"; import { ApiError, ValidationError } from "../errors.js"; -import { resolveOrgRegion } from "../region.js"; import { - apiRequestToRegion, getOrgSdkConfig, type PaginatedResponse, - parseLinkHeader, unwrapPaginatedResult, unwrapResult, } from "./infrastructure.js"; @@ -244,7 +242,7 @@ function mapDetectorToMetricAlertRule( // Issue alerts // // Issue alert rules are read from the org-scoped `/organizations/{org}/workflows/` -// endpoint (filtered by `projectSlug`). The legacy project-scoped +// endpoint (filtered by `project`). The legacy project-scoped // `/projects/{org}/{project}/rules/` endpoint was deprecated on 2026-05-14 and // now returns HTTP 410 during recurring brownouts (getsentry/cli#1182). The // workflows endpoint returns rule-shaped payloads, so the existing @@ -254,7 +252,7 @@ function mapDetectorToMetricAlertRule( /** * Keep only workflows that are attached to a detector. * - * A `projectSlug`-filtered workflows response can also include unattached + * A project-filtered workflows response can also include unattached * org-level workflows; those are not issue alert rules, so we drop any entry * with no `detectorIds`. Mirrors the sentry-mcp filter of the same name. */ @@ -277,20 +275,21 @@ export async function listIssueAlertsPaginated( projectSlug: string, options: { perPage?: number; cursor?: string } = {}, ): Promise> { - const regionUrl = await resolveOrgRegion(orgSlug); - const { data, headers } = await apiRequestToRegion( - regionUrl, - `/organizations/${orgSlug}/workflows/`, - { - params: { - projectSlug, - sortBy: "-id", - per_page: options.perPage, - cursor: options.cursor, - }, + const config = await getOrgSdkConfig(orgSlug); + const result = await listOrganizationWorkflows({ + ...config, + path: { organization_id_or_slug: orgSlug }, + query: { + project: [projectSlug], + sortBy: "-id", + per_page: options.perPage, + cursor: options.cursor, }, + }); + const { data, nextCursor } = unwrapPaginatedResult( + result, + "Failed to list issue alert rules", ); - const { nextCursor } = parseLinkHeader(headers.get("link") ?? null); return { data: filterAttachedIssueAlertRules(data), nextCursor }; } @@ -366,11 +365,15 @@ export async function getIssueAlertRule( projectSlug: string, ruleId: string, ): Promise { - const regionUrl = await resolveOrgRegion(orgSlug); - const { data } = await apiRequestToRegion( - regionUrl, - `/organizations/${orgSlug}/workflows/`, - { params: { projectSlug, id: ruleId, per_page: 1 } }, + const config = await getOrgSdkConfig(orgSlug); + const result = await listOrganizationWorkflows({ + ...config, + path: { organization_id_or_slug: orgSlug }, + query: { project: [projectSlug], id: [Number(ruleId)], per_page: 1 }, + }); + const data = unwrapResult( + result, + "Failed to fetch issue alert rule", ); const rule = filterAttachedIssueAlertRules(data)[0]; if (!rule) { diff --git a/packages/cli/src/lib/api/code-mappings.ts b/packages/cli/src/lib/api/code-mappings.ts index 942ed7240..b64ba589b 100644 --- a/packages/cli/src/lib/api/code-mappings.ts +++ b/packages/cli/src/lib/api/code-mappings.ts @@ -9,6 +9,7 @@ * Auth: requires `org:ci` scope. */ +import { bulkUpsertOrganizationCodeMappings } from "@sentry/api"; import { array, type InferOutput, @@ -21,8 +22,7 @@ import { } from "valibot"; import { logger } from "../logger.js"; -import { resolveOrgRegion } from "../region.js"; -import { apiRequestToRegion } from "./infrastructure.js"; +import { getOrgSdkConfig, unwrapValidatedResult } from "./infrastructure.js"; const log = logger.withTag("api.code-mappings"); @@ -97,7 +97,7 @@ export async function uploadCodeMappings( options: CodeMappingsUploadOptions, ): Promise { const { org, project, repository, defaultBranch, mappings } = options; - const regionUrl = await resolveOrgRegion(org); + const config = await getOrgSdkConfig(org); const endpoint = `organizations/${org}/code-mappings/bulk/`; const merged: MergedCodeMappingsResponse = { @@ -119,19 +119,24 @@ export async function uploadCodeMappings( ); } - const { data } = await apiRequestToRegion( - regionUrl, - endpoint, - { - method: "POST", - body: { - project, - repository, - defaultBranch, - mappings: batch, - }, - schema: BulkCodeMappingsResponseSchema, + const result = await bulkUpsertOrganizationCodeMappings({ + ...config, + path: { organization_id_or_slug: org }, + body: { + project, + repository, + default_branch: defaultBranch, + mappings: batch.map((mapping) => ({ + stack_root: mapping.stackRoot, + source_root: mapping.sourceRoot, + })), }, + }); + const data = unwrapValidatedResult( + result, + "Failed to upload code mappings", + BulkCodeMappingsResponseSchema, + endpoint, ); merged.created += data.created; diff --git a/packages/cli/src/lib/api/dashboards.ts b/packages/cli/src/lib/api/dashboards.ts index 8676b9891..152dd80e0 100644 --- a/packages/cli/src/lib/api/dashboards.ts +++ b/packages/cli/src/lib/api/dashboards.ts @@ -9,6 +9,7 @@ import { createOrganizationDashboard, getOrganizationDashboard, listOrganizationDashboards, + listOrganizationEvents, updateOrganizationDashboard, } from "@sentry/api"; import { resolveEventsSort } from "@sentry/toolkit-core/events-sort"; @@ -36,10 +37,12 @@ import { type TimeseriesResult, type WidgetDataResult, } from "../../types/dashboard.js"; +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, @@ -49,6 +52,7 @@ import { parseLinkHeader, unwrapPaginatedResult, unwrapResult, + unwrapValidatedResult, } from "./infrastructure.js"; /** @@ -506,13 +510,17 @@ type WidgetQueryParams = { options?: WidgetQueryOptions; }; +/** Widget query params once the widget's type has resolved to a dataset. */ +type DatasetWidgetQueryParams = WidgetQueryParams & { dataset: EventsDataset }; + async function queryWidgetTimeseries( - params: WidgetQueryParams, + params: DatasetWidgetQueryParams, ): Promise { const { regionUrl, orgSlug, widget, + dataset, statsPeriod, start, end, @@ -524,12 +532,11 @@ async function queryWidgetTimeseries( for (const query of widget.queries ?? []) { const aggregates = query.aggregates ?? []; const columns = query.columns ?? []; - const dataset = mapWidgetTypeToDataset(widget.widgetType); const reqParams: Record = { yAxis: aggregates, query: query.conditions || undefined, - dataset: dataset ?? undefined, + dataset, statsPeriod, start, end, @@ -572,12 +579,13 @@ async function queryWidgetTimeseries( * @param options - Additional query options */ async function queryWidgetTable( - params: WidgetQueryParams, + params: DatasetWidgetQueryParams, ): Promise { const { regionUrl, orgSlug, widget, + dataset, statsPeriod, start, end, @@ -588,27 +596,29 @@ async function queryWidgetTable( ...(query?.columns ?? []), ...(query?.aggregates ?? []), ]; - const dataset = mapWidgetTypeToDataset(widget.widgetType); const request = resolveEventsSort({ fields, sort: query?.orderby }); - const { data } = await apiRequestToRegion( - regionUrl, - `/organizations/${orgSlug}/events/`, - { - params: { - field: request.fields, - query: query?.conditions || undefined, - dataset: dataset ?? undefined, - statsPeriod, - start, - end, - sort: request.sort, - per_page: widget.limit ?? 10, - environment: options.environment, - project: options.project?.map(String), - }, - schema: EventsTableResponseSchema, + const result = await listOrganizationEvents({ + ...getSdkClientConfig(regionUrl), + path: { organization_id_or_slug: orgSlug }, + query: { + field: request.fields, + query: query?.conditions || undefined, + dataset, + statsPeriod, + start, + end, + sort: request.sort, + per_page: widget.limit ?? 10, + environment: options.environment, + project: options.project, }, + }); + const data = unwrapValidatedResult( + result, + "Failed to query widget data", + EventsTableResponseSchema, + `/organizations/${orgSlug}/events/`, ); const meta = data.meta; @@ -666,13 +676,14 @@ async function queryWidgetData( try { // Chart types → timeseries if (TIMESERIES_DISPLAY_TYPES.has(displayType)) { - return await queryWidgetTimeseries(params); + return await queryWidgetTimeseries({ ...params, dataset }); } // big_number → single scalar value if (widget.displayType === "big_number") { const tableResult = await queryWidgetTable({ ...params, + dataset, widget: { ...widget, limit: 1 }, }); const row = tableResult.rows[0]; @@ -687,7 +698,7 @@ async function queryWidgetData( // Table types → tabular if (TABLE_DISPLAY_TYPES.has(displayType)) { - return await queryWidgetTable(params); + return await queryWidgetTable({ ...params, dataset }); } return { diff --git a/packages/cli/src/lib/api/explore.ts b/packages/cli/src/lib/api/explore.ts index cb6527a61..2ad2eff37 100644 --- a/packages/cli/src/lib/api/explore.ts +++ b/packages/cli/src/lib/api/explore.ts @@ -5,27 +5,33 @@ * Explore UI, with user-specified aggregates, datasets, filters, and time ranges. */ -import type { OrganizationEventsTimeseriesResponse } from "@sentry/api"; +import { + listOrganizationEvents, + type OrganizationEventsTimeseriesResponse, +} from "@sentry/api"; import type { EventsTableResponse } from "../../types/dashboard.js"; import { EventsTableResponseSchema } from "../../types/dashboard.js"; +import type { EventsDataset } from "../datasets.js"; import { stringifyUnknown } from "../errors.js"; import { resolveOrgRegion } from "../region.js"; +import { getSdkClientConfig } from "../sentry-sdk-client.js"; import { API_MAX_PER_PAGE, apiRequestToRegion, MAX_PAGINATION_PAGES, type PaginatedResponse, paginate, - parseLinkHeader, + unwrapPaginatedResult, + validateResponse, } from "./infrastructure.js"; /** Options for querying the Explore/Events endpoint. */ export type ExploreQueryOptions = { /** Fields to request — columns, aggregates, or equations */ fields: string[]; - /** Dataset to query: errors, spans, tracemetrics, logs, replays */ - dataset?: string; + /** Dataset to query. Defaults to `errors`. */ + dataset?: EventsDataset; /** Sentry search query filter */ query?: string; /** @@ -136,31 +142,34 @@ async function fetchEventsPage( options: ExploreQueryOptions, perPage: number, ): Promise> { - const { data, headers } = await apiRequestToRegion( - regionUrl, - `/organizations/${orgSlug}/events/`, - { - params: { - dataset: options.dataset ?? "errors", - field: options.fields, - query: options.query || undefined, - project: options.projectId, - sort: options.sort || undefined, - per_page: perPage, - statsPeriod: - options.start || options.end - ? undefined - : (options.statsPeriod ?? "24h"), - start: options.start, - end: options.end, - cursor: options.cursor, - }, - schema: EventsTableResponseSchema, + const result = await listOrganizationEvents({ + ...getSdkClientConfig(regionUrl), + path: { organization_id_or_slug: orgSlug }, + query: { + dataset: options.dataset ?? "errors", + field: options.fields, + query: options.query || undefined, + project: + options.projectId === undefined ? undefined : [options.projectId], + sort: options.sort || undefined, + per_page: perPage, + statsPeriod: + options.start || options.end + ? undefined + : (options.statsPeriod ?? "24h"), + start: options.start, + end: options.end, + cursor: options.cursor, }, + }); + const page = unwrapPaginatedResult(result, "Failed to query events"); + const data: EventsTableResponse = validateResponse( + EventsTableResponseSchema, + page.data, + `/organizations/${orgSlug}/events/`, + page.response?.status ?? 0, ); - - const { nextCursor } = parseLinkHeader(headers.get("link") ?? null); - return { data, nextCursor }; + return { data, nextCursor: page.nextCursor }; } /** Metric metadata returned by {@link queryMetricsMeta}. */ diff --git a/packages/cli/src/lib/api/issues.ts b/packages/cli/src/lib/api/issues.ts index 67be8f909..ac082a08e 100644 --- a/packages/cli/src/lib/api/issues.ts +++ b/packages/cli/src/lib/api/issues.ts @@ -4,8 +4,12 @@ * Functions for listing, retrieving, and updating Sentry issues. */ -import type { ListOrganizationIssuesData } from "@sentry/api"; +import type { + GetOrganizationIssueData, + ListOrganizationIssuesData, +} from "@sentry/api"; import { + getOrganizationIssue, listOrganizationIssues, resolveOrganizationShortId, } from "@sentry/api"; @@ -110,6 +114,34 @@ export function buildIssueListCollapse(options: { return collapse; } +/** Collapse values the issue details endpoint acts on. */ +type IssueDetailsCollapseField = NonNullable< + NonNullable["collapse"] +>[number]; + +const ISSUE_DETAILS_COLLAPSE_FIELDS: ReadonlySet = + new Set(["release", "stats", "tags"]); + +function isIssueDetailsCollapseField( + field: string, +): field is IssueDetailsCollapseField { + return ISSUE_DETAILS_COLLAPSE_FIELDS.has(field); +} + +/** + * Keep the collapse values the issue details endpoint reads. It ignores + * `lifetime`, `filtered` and `unhandled`, which only the list and short-ID + * endpoints act on. + */ +function toIssueDetailsCollapse( + collapse: IssueCollapseField[] | undefined, +): IssueDetailsCollapseField[] | undefined { + const fields = collapse?.flatMap((field) => + isIssueDetailsCollapseField(field) ? [field] : [], + ); + return fields?.length ? fields : undefined; +} + /** * Collapse fields for single-issue detail endpoints. * @@ -121,7 +153,8 @@ export function buildIssueListCollapse(options: { * Note: On the **list** endpoint, `collapse=lifetime` strips top-level * `count`, `userCount`, `firstSeen`, `lastSeen` (see #969). The detail * endpoint preserves these fields regardless of collapse — safe to include - * `lifetime` here. + * `lifetime` here. The short-ID endpoint acts on all four values; the issue + * details endpoint acts only on `stats`, so {@link getIssueInOrg} drops the rest. */ export const ISSUE_DETAIL_COLLAPSE: IssueCollapseField[] = [ "stats", @@ -292,8 +325,9 @@ export function getIssue( issueId: string, options?: { collapse?: IssueCollapseField[] }, ): Promise { + const collapse = toIssueDetailsCollapse(options?.collapse); return apiRequest(`/issues/${issueId}/`, { - params: options?.collapse ? { collapse: options.collapse } : undefined, + params: collapse ? { collapse } : undefined, }); } @@ -303,10 +337,6 @@ export function getIssue( * Uses the org-scoped endpoint with region-aware routing. * Preferred over {@link getIssue} when the org slug is available. * - * Uses raw `apiRequestToRegion` instead of the SDK's `getOrganizationIssue` - * because its `collapse` type allows only `release`, `stats` and `tags`, not - * the `lifetime`, `filtered` and `unhandled` values the CLI sends. - * * @param orgSlug - Organization slug (used for region routing) * @param issueId - Numeric issue ID * @param options - Optional collapse fields to skip expensive backend queries @@ -316,15 +346,13 @@ export async function getIssueInOrg( issueId: string, options?: { collapse?: IssueCollapseField[] }, ): Promise { - const regionUrl = await resolveOrgRegion(orgSlug); - const { data } = await apiRequestToRegion( - regionUrl, - `/organizations/${orgSlug}/issues/${issueId}/`, - { - params: options?.collapse ? { collapse: options.collapse } : undefined, - }, - ); - return data; + const collapse = toIssueDetailsCollapse(options?.collapse); + const result = await getOrganizationIssue({ + ...(await getOrgSdkConfig(orgSlug)), + path: { organization_id_or_slug: orgSlug, issue_id: issueId }, + query: collapse ? { collapse } : undefined, + }); + return unwrapResult(result, "Failed to get issue"); } /** @@ -642,9 +670,9 @@ export async function mergeIssues( `Need at least 2 issues to merge (got ${groupIds.length}).`, ); } - // The bulk mutate endpoint accepts repeated `?id=X` query params plus a - // `{merge: 1}` body. The SDK wraps this but its typed `query` shape - // doesn't expose the array semantics cleanly, so use raw request. + // The bulk mutate endpoint takes repeated numeric `?id=X` query params plus + // a `{merge: true}` body. `updateOrganizationIssues` would fit, but its body + // type marks every field required until getsentry/sentry#130348 lands. const regionUrl = await resolveOrgRegion(orgSlug); const query = groupIds.map((id) => `id=${encodeURIComponent(id)}`).join("&"); const path = `/organizations/${encodeURIComponent(orgSlug)}/issues/?${query}`; @@ -652,7 +680,7 @@ export async function mergeIssues( try { const { data } = await apiRequestToRegion(regionUrl, path, { method: "PUT", - body: { merge: 1 }, + body: { merge: true }, }); // HTTP-layer invalidation covers the region-scoped caches via the // prefix sweep on `/organizations/{org}/issues/`, but it can't see diff --git a/packages/cli/src/lib/api/replays.ts b/packages/cli/src/lib/api/replays.ts index 7a0cb27df..60efcd842 100644 --- a/packages/cli/src/lib/api/replays.ts +++ b/packages/cli/src/lib/api/replays.ts @@ -6,6 +6,7 @@ import { getOrganizationReplay, + getOrganizationReplayCount, type ListProjectReplayRecordingSegmentsResponse, listProjectReplayRecordingSegments, } from "@sentry/api"; @@ -186,6 +187,10 @@ function normalizeReplayProjectId< /** * Fetch a single page of replays from the organization replay index. + * + * Stays on the raw transport: the default fields include `info_ids` and + * `warning_ids`, which `replay list --json` prints but the generated + * `listOrganizationReplays` field enum leaves out. */ async function fetchReplayPage( regionUrl: string, @@ -406,20 +411,24 @@ export async function listReplayIdsForIssue( issueId: string | number, ): Promise { const normalizedIssueId = String(issueId); - const regionUrl = await resolveOrgRegion(orgSlug); - const { data } = await apiRequestToRegion( - regionUrl, - `/organizations/${orgSlug}/replay-count/`, - { - params: { - data_source: "events", - project: "-1", - query: `issue.id:[${normalizedIssueId}]`, - returnIds: true, - statsPeriod: "90d", - }, - schema: ReplayIdsByResourceSchema, + const result = await getOrganizationReplayCount({ + ...(await getOrgSdkConfig(orgSlug)), + path: { organization_id_or_slug: orgSlug }, + query: { + data_source: "errors", + project: [-1], + query: `issue.id:[${normalizedIssueId}]`, + returnIds: true, + statsPeriod: "90d", }, + }); + // With `returnIds`, the endpoint maps each issue to replay IDs instead of + // the counts its documented response describes. + const data = unwrapValidatedResult( + result, + "Failed to list replays for issue", + ReplayIdsByResourceSchema, + `/organizations/${orgSlug}/replay-count/`, ); return data[normalizedIssueId] ?? []; diff --git a/packages/cli/src/lib/api/seer.ts b/packages/cli/src/lib/api/seer.ts index 7e6b1326d..1d5198533 100644 --- a/packages/cli/src/lib/api/seer.ts +++ b/packages/cli/src/lib/api/seer.ts @@ -2,22 +2,22 @@ * Seer AI API functions * * Functions for Seer-powered root cause analysis, autofix state, - * and solution planning. Uses the agent-based (explorer) endpoint - * which returns blocks instead of steps. + * and solution planning. The autofix endpoint returns agent blocks + * instead of steps. */ import { - type AutofixResponse, - AutofixResponseSchema, - type AutofixState, -} from "../../types/seer.js"; + getOrganizationIssueAutofixState, + type StartOrganizationIssueAutofixResponse, + startOrganizationIssueAutofix, +} from "@sentry/api"; +import { AutofixResponseSchema, type AutofixState } from "../../types/seer.js"; -import { resolveOrgRegion } from "../region.js"; - -import { apiRequestToRegion } from "./infrastructure.js"; - -/** Query params to activate the agent-based autofix endpoint */ -const EXPLORER_MODE_PARAMS = { mode: "explorer" }; +import { + getOrgSdkConfig, + unwrapResult, + unwrapValidatedResult, +} from "./infrastructure.js"; /** * Normalize agent status values to the uppercase format used throughout the CLI. @@ -70,20 +70,15 @@ export async function triggerRootCauseAnalysis( orgSlug: string, issueId: string, ): Promise<{ run_id?: number; sentry_run_id: string | null }> { - const regionUrl = await resolveOrgRegion(orgSlug); - - const { data } = await apiRequestToRegion<{ - run_id?: number; - sentry_run_id: string | null; - }>(regionUrl, `/organizations/${orgSlug}/issues/${issueId}/autofix/`, { - method: "POST", - params: EXPLORER_MODE_PARAMS, - body: { - step: "root_cause", - referrer: "api.cli", - }, + const result = await startOrganizationIssueAutofix({ + ...(await getOrgSdkConfig(orgSlug)), + path: { organization_id_or_slug: orgSlug, issue_id: issueId }, + body: { step: "root_cause", referrer: "api.cli" }, }); - return data; + return unwrapResult( + result, + "Failed to start root cause analysis", + ); } /** @@ -98,15 +93,15 @@ export async function getAutofixState( orgSlug: string, issueId: string, ): Promise { - const regionUrl = await resolveOrgRegion(orgSlug); - - const { data } = await apiRequestToRegion( - regionUrl, + const result = await getOrganizationIssueAutofixState({ + ...(await getOrgSdkConfig(orgSlug)), + path: { organization_id_or_slug: orgSlug, issue_id: issueId }, + }); + const data = unwrapValidatedResult( + result, + "Failed to get autofix state", + AutofixResponseSchema, `/organizations/${orgSlug}/issues/${issueId}/autofix/`, - { - params: EXPLORER_MODE_PARAMS, - schema: AutofixResponseSchema, - }, ); if (!data.autofix) { @@ -134,24 +129,17 @@ export async function triggerSolutionPlanning( issueId: string, runId: string | number, ): Promise { - const regionUrl = await resolveOrgRegion(orgSlug); - // A UUID sent under run_id (an IntegerField) fails server-side validation. const runIdBodyField = typeof runId === "string" ? { sentry_run_id: runId } : { run_id: runId }; - const { data } = await apiRequestToRegion( - regionUrl, - `/organizations/${orgSlug}/issues/${issueId}/autofix/`, - { - method: "POST", - params: EXPLORER_MODE_PARAMS, - body: { - step: "solution", - ...runIdBodyField, - referrer: "api.cli", - }, - }, + const result = await startOrganizationIssueAutofix({ + ...(await getOrgSdkConfig(orgSlug)), + path: { organization_id_or_slug: orgSlug, issue_id: issueId }, + body: { step: "solution", ...runIdBodyField, referrer: "api.cli" }, + }); + return unwrapResult( + result, + "Failed to start solution planning", ); - return data; } diff --git a/packages/cli/src/lib/api/traces.ts b/packages/cli/src/lib/api/traces.ts index 559a300aa..5abc06a7f 100644 --- a/packages/cli/src/lib/api/traces.ts +++ b/packages/cli/src/lib/api/traces.ts @@ -114,8 +114,6 @@ export const TRACE_LOOKUP_FALLBACK_STATS_PERIOD = "90d"; type GetDetailedTraceOptions = { /** Extra attribute names to include on each span */ additionalAttributes?: string[]; - /** Numeric project ID to filter spans. Omit or -1 for all projects. */ - projectId?: number; /** * Unix timestamp (seconds) of a known event in this trace. * The backend then searches ±1.5 days around it and does not widen. @@ -138,8 +136,6 @@ async function requestTrace( params: { timestamp: params.timestamp, statsPeriod: params.statsPeriod, - limit: 10_000, - project: params.projectId ?? -1, additional_attributes: params.additionalAttributes, }, }, @@ -152,8 +148,8 @@ async function requestTrace( * This is an internal endpoint not covered by the public API. * Uses region-aware routing for multi-region support. * - * When `projectId` is provided, the API returns only spans belonging to that - * project. Pass `-1` (or omit) to fetch spans from all projects. + * The endpoint always returns spans from every project in the trace; it + * ignores the `project` and `limit` params. * * Time window (matches the Sentry UI): * - Known event `timestamp` → one query around that instant (no retry). @@ -161,7 +157,7 @@ async function requestTrace( * * @param orgSlug - Organization slug * @param traceId - The trace ID (from event.contexts.trace.trace_id) - * @param options - Optional timestamp, additional attributes, and project filter + * @param options - Optional timestamp and additional attributes * @returns Array of root spans with nested children */ export async function getDetailedTrace( @@ -170,10 +166,7 @@ export async function getDetailedTrace( options: GetDetailedTraceOptions = {}, ): Promise { const regionUrl = await resolveOrgRegion(orgSlug); - const shared = { - additionalAttributes: options.additionalAttributes, - projectId: options.projectId, - }; + const shared = { additionalAttributes: options.additionalAttributes }; const timestamp = options.timestamp !== undefined && Number.isFinite(options.timestamp) ? options.timestamp diff --git a/packages/cli/src/lib/datasets.ts b/packages/cli/src/lib/datasets.ts index 1c29205c9..96e9a7cd5 100644 --- a/packages/cli/src/lib/datasets.ts +++ b/packages/cli/src/lib/datasets.ts @@ -3,8 +3,26 @@ * aliases, and the API names they map to. */ +import type { ListOrganizationEventsData } from "@sentry/api"; import { ValidationError } from "./errors.js"; +/** Datasets the public `events/` and `events-timeseries/` endpoints accept. */ +export type EventsDataset = ListOrganizationEventsData["query"]["dataset"]; + +const EVENTS_DATASETS: ReadonlySet = new Set([ + "errors", + "logs", + "profile_functions", + "spans", + "tracemetrics", + "uptime_results", +]); + +/** Whether `dataset` is one the public Events endpoints accept. */ +export function isEventsDataset(dataset: string): dataset is EventsDataset { + return EVENTS_DATASETS.has(dataset); +} + /** API-level dataset names */ export type ExploreDataset = | "errors" diff --git a/packages/cli/src/types/dashboard.ts b/packages/cli/src/types/dashboard.ts index 8e9007c12..d5a3d66c6 100644 --- a/packages/cli/src/types/dashboard.ts +++ b/packages/cli/src/types/dashboard.ts @@ -35,6 +35,7 @@ import { variant, } from "valibot"; +import type { EventsDataset } from "../lib/datasets.js"; import { ValidationError } from "../lib/errors.js"; import { logger } from "../lib/logger.js"; import type { @@ -1070,7 +1071,7 @@ export type WidgetDataResult = InferOutput; * Widget types that don't map to a dataset (issue, preprod-app-size, etc.) * return null and are rendered as "unsupported". */ -const WIDGET_TYPE_TO_DATASET: Record = { +const WIDGET_TYPE_TO_DATASET: Partial> = { spans: "spans", "error-events": "errors", logs: "logs", @@ -1085,7 +1086,7 @@ const WIDGET_TYPE_TO_DATASET: Record = { */ export function mapWidgetTypeToDataset( widgetType: string | undefined, -): string | null { +): EventsDataset | null { if (!widgetType) { return null; } diff --git a/packages/cli/test/commands/alert/issues/list.test.ts b/packages/cli/test/commands/alert/issues/list.test.ts index 631f888bd..ce5504d90 100644 --- a/packages/cli/test/commands/alert/issues/list.test.ts +++ b/packages/cli/test/commands/alert/issues/list.test.ts @@ -564,7 +564,7 @@ describe("alert issues list pagination", () => { expect.objectContaining({ project: "org-two/myproj", status: 500, - message: expect.stringContaining("API request failed"), + message: expect.stringContaining("Failed to list issue alert rules"), }), ]); expect(warnSpy).toHaveBeenCalledWith( @@ -574,7 +574,7 @@ describe("alert issues list pagination", () => { ); }); - test("all project failures preserve ApiError status and endpoint", async () => { + test("all project failures preserve ApiError status", async () => { globalThis.fetch = mockFetch(async (input, init) => { const req = new Request(input, init); const url = new URL(req.url); @@ -610,7 +610,6 @@ describe("alert issues list pagination", () => { ).rejects.toMatchObject({ name: "ApiError", status: 403, - endpoint: "/organizations/test-org/workflows/", } satisfies Partial); }); diff --git a/packages/cli/test/commands/trace/view.func.test.ts b/packages/cli/test/commands/trace/view.func.test.ts index 2f6480a34..393b37da9 100644 --- a/packages/cli/test/commands/trace/view.func.test.ts +++ b/packages/cli/test/commands/trace/view.func.test.ts @@ -281,7 +281,7 @@ describe("viewCommand.func", () => { expect(getDetailedTraceSpy).toHaveBeenCalledWith( "test-org", "aaaa1111bbbb2222cccc3333dddd4444", - { additionalAttributes: ["http.method"], projectId: 42 }, + { additionalAttributes: ["http.method"] }, ); expect(output).toEqual({ traceId: "aaaa1111bbbb2222cccc3333dddd4444", @@ -371,7 +371,7 @@ describe("viewCommand.func", () => { expect(getDetailedTraceSpy).toHaveBeenCalledWith( "test-org", "aaaa1111bbbb2222cccc3333dddd4444", - { additionalAttributes: ["http.method"], projectId: 42 }, + { additionalAttributes: ["http.method"] }, ); }, ); @@ -405,7 +405,7 @@ describe("viewCommand.func", () => { expect(getDetailedTraceSpy).toHaveBeenCalledWith( "test-org", "aaaa1111bbbb2222cccc3333dddd4444", - { additionalAttributes, projectId: 42 }, + { additionalAttributes }, ); }, ); @@ -460,7 +460,7 @@ describe("viewCommand.func", () => { expect(getDetailedTraceSpy).toHaveBeenCalledWith( "test-org", "aaaa1111bbbb2222cccc3333dddd4444", - { additionalAttributes: undefined, projectId: 42 }, + { additionalAttributes: undefined }, ); expect(fetchMultiSpanDetailsSpy).toHaveBeenCalled(); }); @@ -504,7 +504,7 @@ describe("viewCommand.func", () => { expect(getDetailedTraceSpy).toHaveBeenCalledWith( "test-org", "aaaa1111bbbb2222cccc3333dddd4444", - { additionalAttributes: ["spans.foo"], projectId: 42 }, + { additionalAttributes: ["spans.foo"] }, ); }); @@ -515,7 +515,7 @@ describe("viewCommand.func", () => { expect(getDetailedTraceSpy).toHaveBeenCalledWith( "test-org", "aaaa1111bbbb2222cccc3333dddd4444", - { additionalAttributes: undefined, projectId: 42 }, + { additionalAttributes: undefined }, ); expect(fetchMultiSpanDetailsSpy).toHaveBeenCalled(); }); @@ -619,7 +619,6 @@ describe("viewCommand.func", () => { expect(await renderJson("replayId")).toEqual({ replayId: REPLAY_ID }); expect(getDetailedTraceSpy).toHaveBeenCalledWith("test-org", TRACE_ID, { additionalAttributes: undefined, - projectId: undefined, }); }); }); @@ -715,10 +714,7 @@ describe("viewCommand.func", () => { expect(getDetailedTraceSpy).toHaveBeenCalledWith( "my-org", "aaaa1111bbbb2222cccc3333dddd4444", - expect.objectContaining({ projectId: undefined }), - ); - expect(getDetailedTraceSpy.mock.calls[0]?.[2]).not.toHaveProperty( - "timestamp", + { additionalAttributes: undefined }, ); }); @@ -861,7 +857,7 @@ describe("viewCommand.func", () => { expect(getDetailedTraceSpy).toHaveBeenCalledWith( "test-org", traceIdFromEvent, - { additionalAttributes: undefined, projectId: undefined }, + { additionalAttributes: undefined }, ); const output = stdoutWrite.mock.calls.map((c) => c[0]).join(""); diff --git a/packages/cli/test/lib/api-client.coverage.test.ts b/packages/cli/test/lib/api-client.coverage.test.ts index 44c1ed95b..cc1dbeb7f 100644 --- a/packages/cli/test/lib/api-client.coverage.test.ts +++ b/packages/cli/test/lib/api-client.coverage.test.ts @@ -130,15 +130,12 @@ describe("issues.ts", () => { expect(result.title).toBe("Test Issue"); }); - test("passes collapse query params when provided", async () => { + test("passes only the collapse values the details endpoint reads", async () => { const issue = mockIssue(); globalThis.fetch = mockFetch(async (input, init) => { const req = new Request(input!, init); const url = new URL(req.url); - expect(url.searchParams.getAll("collapse")).toEqual([ - "stats", - "lifetime", - ]); + expect(url.searchParams.getAll("collapse")).toEqual(["stats"]); return new Response(JSON.stringify(issue), { status: 200, headers: { "Content-Type": "application/json" }, @@ -165,16 +162,13 @@ describe("issues.ts", () => { expect(result.id).toBe("12345"); }); - test("passes collapse query params when provided", async () => { + test("passes only the collapse values the details endpoint reads", async () => { const issue = mockIssue(); globalThis.fetch = mockFetch(async (input, init) => { const req = new Request(input!, init); const url = new URL(req.url); expect(url.pathname).toContain("/organizations/test-org/issues/12345/"); - expect(url.searchParams.getAll("collapse")).toEqual([ - "stats", - "filtered", - ]); + expect(url.searchParams.getAll("collapse")).toEqual(["stats"]); return new Response(JSON.stringify(issue), { status: 200, headers: { "Content-Type": "application/json" }, @@ -182,7 +176,20 @@ describe("issues.ts", () => { }); await getIssueInOrg("test-org", "12345", { - collapse: ["stats", "filtered"], + collapse: ["stats", "lifetime", "filtered", "unhandled"], + }); + }); + + test("omits collapse when no value applies to the details endpoint", async () => { + const issue = mockIssue(); + globalThis.fetch = mockFetch(async (input, init) => { + const url = new URL(new Request(input!, init).url); + expect(url.searchParams.has("collapse")).toBe(false); + return Response.json(issue); + }); + + await getIssueInOrg("test-org", "12345", { + collapse: ["lifetime", "filtered"], }); }); }); @@ -1158,8 +1165,8 @@ describe("traces.ts", () => { ); const url = new URL(req.url); expect(url.searchParams.get("timestamp")).toBe("1700000000"); - expect(url.searchParams.get("limit")).toBe("10000"); - expect(url.searchParams.get("project")).toBe("-1"); + expect(url.searchParams.has("limit")).toBe(false); + expect(url.searchParams.has("project")).toBe(false); return new Response(JSON.stringify(spans), { status: 200, headers: { "Content-Type": "application/json" }, diff --git a/packages/cli/test/lib/api-client.seer.test.ts b/packages/cli/test/lib/api-client.seer.test.ts index 9e3651907..99fa50aaf 100644 --- a/packages/cli/test/lib/api-client.seer.test.ts +++ b/packages/cli/test/lib/api-client.seer.test.ts @@ -53,7 +53,7 @@ describe("triggerRootCauseAnalysis", () => { expect(capturedRequest?.url).toContain( "/organizations/test-org/issues/123456789/autofix/", ); - expect(capturedRequest?.url).toContain("mode=explorer"); + expect(capturedRequest?.url).not.toContain("mode="); }); test("includes step and stopping_point in request body", async () => { @@ -133,7 +133,7 @@ describe("getAutofixState", () => { expect(capturedRequest?.url).toContain( "/organizations/test-org/issues/123456789/autofix/", ); - expect(capturedRequest?.url).toContain("mode=explorer"); + expect(capturedRequest?.url).not.toContain("mode="); }); test("still parses legacy run_id when sentry_run_id is absent", async () => { @@ -300,13 +300,13 @@ describe("getAutofixState", () => { }); describe("triggerSolutionPlanning", () => { - test("sends POST request to autofix endpoint with explorer mode", async () => { + test("sends POST request to autofix endpoint without a mode param", async () => { let capturedRequest: Request | undefined; let capturedBody: unknown; globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { capturedRequest = new Request(input, init); - capturedBody = await new Request(input, init).json(); + capturedBody = await capturedRequest.clone().json(); return new Response(JSON.stringify({ run_id: 12_345 }), { status: 202, @@ -320,7 +320,7 @@ describe("triggerSolutionPlanning", () => { expect(capturedRequest?.url).toContain( "/organizations/test-org/issues/123456789/autofix/", ); - expect(capturedRequest?.url).toContain("mode=explorer"); + expect(capturedRequest?.url).not.toContain("mode="); expect(capturedBody).toEqual({ step: "solution", run_id: 12_345, diff --git a/packages/cli/test/lib/api/alerts.test.ts b/packages/cli/test/lib/api/alerts.test.ts index dc76f26a5..550ee5a57 100644 --- a/packages/cli/test/lib/api/alerts.test.ts +++ b/packages/cli/test/lib/api/alerts.test.ts @@ -78,7 +78,9 @@ describe("listIssueAlertsPaginated", () => { globalThis.fetch = mockFetch(async (input, init) => { const url = new URL(new Request(input!, init).url); expect(url.pathname).toBe("/api/0/organizations/test-org/workflows/"); - expect(url.searchParams.get("projectSlug")).toBe("test-project"); + expect(url.searchParams.getAll("project")).toEqual(["test-project"]); + expect(url.searchParams.has("projectSlug")).toBe(false); + expect(url.searchParams.get("sortBy")).toBe("-id"); return Response.json([ workflowRule({ id: "1", name: "Attached", detectorIds: [7] }), workflowRule({ id: "2", name: "Unattached", detectorIds: [] }), @@ -96,8 +98,9 @@ describe("getIssueAlertRule", () => { globalThis.fetch = mockFetch(async (input, init) => { const url = new URL(new Request(input!, init).url); expect(url.pathname).toBe("/api/0/organizations/test-org/workflows/"); - expect(url.searchParams.get("projectSlug")).toBe("test-project"); - expect(url.searchParams.get("id")).toBe("42"); + expect(url.searchParams.getAll("project")).toEqual(["test-project"]); + expect(url.searchParams.has("projectSlug")).toBe(false); + expect(url.searchParams.getAll("id")).toEqual(["42"]); return Response.json([workflowRule({ id: "42", name: "My Rule" })]); }); diff --git a/packages/cli/test/lib/api/code-mappings.test.ts b/packages/cli/test/lib/api/code-mappings.test.ts new file mode 100644 index 000000000..dcd099155 --- /dev/null +++ b/packages/cli/test/lib/api/code-mappings.test.ts @@ -0,0 +1,100 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { uploadCodeMappings } from "../../../src/lib/api/code-mappings.js"; +import { DEFAULT_SENTRY_URL } from "../../../src/lib/constants.js"; +import { setAuthToken } from "../../../src/lib/db/auth.js"; +import { setOrgRegion } from "../../../src/lib/db/regions.js"; +import { resetAuthenticatedFetch } from "../../../src/lib/sentry-client.js"; +import { mockFetch, useTestConfigDir } from "../../helpers.js"; + +useTestConfigDir("api-code-mappings-"); + +let originalFetch: typeof globalThis.fetch; + +beforeEach(() => { + originalFetch = globalThis.fetch; + resetAuthenticatedFetch(); + setAuthToken("test-token"); + setOrgRegion("test-org", DEFAULT_SENTRY_URL); +}); + +afterEach(() => { + globalThis.fetch = originalFetch; + resetAuthenticatedFetch(); +}); + +describe("uploadCodeMappings", () => { + test("POSTs a snake_case body to the bulk endpoint", async () => { + let body: unknown; + globalThis.fetch = mockFetch(async (input, init) => { + const req = new Request(input!, init); + expect(req.method).toBe("POST"); + expect(req.url).toBe( + `${DEFAULT_SENTRY_URL}/api/0/organizations/test-org/code-mappings/bulk/`, + ); + body = await req.json(); + return Response.json({ + created: 1, + updated: 0, + errors: 0, + mappings: [ + { stackRoot: "com/example", sourceRoot: "src", status: "created" }, + ], + }); + }); + + const result = await uploadCodeMappings({ + org: "test-org", + project: "test-project", + repository: "owner/repo", + defaultBranch: "main", + mappings: [{ stackRoot: "com/example", sourceRoot: "src" }], + }); + + expect(body).toEqual({ + project: "test-project", + repository: "owner/repo", + default_branch: "main", + mappings: [{ stack_root: "com/example", source_root: "src" }], + }); + expect(result).toEqual({ + created: 1, + updated: 0, + errors: 0, + mappings: [ + { stackRoot: "com/example", sourceRoot: "src", status: "created" }, + ], + }); + }); + + test("returns per-mapping errors from a 207 response", async () => { + globalThis.fetch = mockFetch(async () => + Response.json( + { + created: 0, + updated: 0, + errors: 1, + mappings: [ + { + stackRoot: "com/example", + sourceRoot: "src", + status: "error", + detail: "Repository not found", + }, + ], + }, + { status: 207 }, + ), + ); + + const result = await uploadCodeMappings({ + org: "test-org", + project: "test-project", + repository: "owner/repo", + defaultBranch: "main", + mappings: [{ stackRoot: "com/example", sourceRoot: "src" }], + }); + + expect(result.errors).toBe(1); + expect(result.mappings[0]?.detail).toBe("Repository not found"); + }); +}); diff --git a/packages/cli/test/lib/api/dashboards.test.ts b/packages/cli/test/lib/api/dashboards.test.ts index 01129972c..55f20a584 100644 --- a/packages/cli/test/lib/api/dashboards.test.ts +++ b/packages/cli/test/lib/api/dashboards.test.ts @@ -19,12 +19,22 @@ import type { DashboardWidget, } from "../../../src/types/dashboard.js"; -const { apiRequestToRegionMock, reportCliErrorMock, resolveOrgRegionMock } = - vi.hoisted(() => ({ - apiRequestToRegionMock: vi.fn(), - reportCliErrorMock: vi.fn(), - resolveOrgRegionMock: vi.fn(), - })); +const { + apiRequestToRegionMock, + listOrganizationEventsMock, + reportCliErrorMock, + resolveOrgRegionMock, +} = vi.hoisted(() => ({ + apiRequestToRegionMock: vi.fn(), + listOrganizationEventsMock: vi.fn(), + reportCliErrorMock: vi.fn(), + resolveOrgRegionMock: vi.fn(), +})); + +vi.mock("@sentry/api", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, listOrganizationEvents: listOrganizationEventsMock }; +}); vi.mock("../../../src/lib/error-reporting.js", () => ({ reportCliError: reportCliErrorMock, @@ -284,8 +294,11 @@ describe("widget sort resolution", () => { const REGION = "https://us.sentry.io"; test("sorts error table widgets by their first aggregate", async () => { - apiRequestToRegionMock.mockReset(); - apiRequestToRegionMock.mockResolvedValue({ data: { data: [], meta: {} } }); + listOrganizationEventsMock.mockReset(); + listOrganizationEventsMock.mockResolvedValue({ + data: { data: [], meta: {} }, + response: new Response(), + }); const dashboard = makeDashboard([ { title: "Top failures", @@ -305,11 +318,11 @@ describe("widget sort resolution", () => { await queryAllWidgets(REGION, "test-org", dashboard); - expect(apiRequestToRegionMock).toHaveBeenCalledWith( - REGION, - "/organizations/test-org/events/", + expect(listOrganizationEventsMock).toHaveBeenCalledWith( expect.objectContaining({ - params: expect.objectContaining({ + baseUrl: REGION, + path: { organization_id_or_slug: "test-org" }, + query: expect.objectContaining({ dataset: "errors", field: ["failure.node", "count()"], sort: "-count()", @@ -379,38 +392,48 @@ describe("widget query failures", () => { }, ]); + /** Resolve the events request with an SDK error result. */ + function mockErrorResult( + status: number, + statusText: string, + detail: string, + ): void { + listOrganizationEventsMock.mockReset(); + listOrganizationEventsMock.mockResolvedValue({ + error: { detail }, + response: new Response(null, { status, statusText }), + }); + } + test("reports through the shared policy and keeps the API detail", async () => { - const error = new ApiError( - "API request failed: 400 Bad Request", + mockErrorResult( 400, + "Bad Request", "tags[view_ms,number] is invalid for parameter 1 in p50.\nIts a string type field.", - "/organizations/test-org/events/", ); - apiRequestToRegionMock.mockReset(); - apiRequestToRegionMock.mockRejectedValue(error); reportCliErrorMock.mockReset(); const results = await queryAllWidgets(REGION, "test-org", dashboard); - expect(reportCliErrorMock).toHaveBeenCalledWith(error); + expect(reportCliErrorMock).toHaveBeenCalledWith(expect.any(ApiError)); + expect(reportCliErrorMock.mock.calls[0]?.[0]).toMatchObject({ + status: 400, + }); expect(results.get(0)).toEqual({ type: "error", message: - "API request failed: 400 Bad Request: tags[view_ms,number] is invalid for parameter 1 in p50. Its a string type field.", + "Failed to query widget data: 400 Bad Request: tags[view_ms,number] is invalid for parameter 1 in p50. Its a string type field.", }); }); test("does not repeat a status-text detail", async () => { - apiRequestToRegionMock.mockReset(); - apiRequestToRegionMock.mockRejectedValue( - new ApiError("API request failed: 404 Not Found", 404, "Not Found"), - ); + mockErrorResult(404, "Not Found", "Not Found"); const results = await queryAllWidgets(REGION, "test-org", dashboard); expect(results.get(0)).toEqual({ type: "error", - message: "API request failed: 404 Not Found", + message: "Failed to query widget data: 404 Not Found", }); }); }); diff --git a/packages/cli/test/lib/api/issues.test.ts b/packages/cli/test/lib/api/issues.test.ts index 4ceab3d75..dd5a33cd3 100644 --- a/packages/cli/test/lib/api/issues.test.ts +++ b/packages/cli/test/lib/api/issues.test.ts @@ -203,7 +203,7 @@ describe("mergeIssues", () => { expect(capturedUrl).toContain("id=100"); expect(capturedUrl).toContain("id=200"); expect(capturedUrl).toContain("id=300"); - expect(capturedBody).toEqual({ merge: 1 }); + expect(capturedBody).toEqual({ merge: true }); expect(result).toEqual({ parent: "100", children: ["200", "300"] }); }); diff --git a/packages/cli/test/lib/api/replays.test.ts b/packages/cli/test/lib/api/replays.test.ts index aed2a3d8b..a3346e9a0 100644 --- a/packages/cli/test/lib/api/replays.test.ts +++ b/packages/cli/test/lib/api/replays.test.ts @@ -531,7 +531,7 @@ describe("listReplayIdsForIssue", () => { ); expect(url.searchParams.get("returnIds")).toBe("true"); expect(url.searchParams.get("query")).toBe("issue.id:[12345]"); - expect(url.searchParams.get("data_source")).toBe("events"); + expect(url.searchParams.get("data_source")).toBe("errors"); expect(url.searchParams.get("statsPeriod")).toBe("90d"); expect(url.searchParams.getAll("project")).toEqual(["-1"]); expect(replayIds).toEqual([ diff --git a/packages/cli/test/lib/api/traces.test.ts b/packages/cli/test/lib/api/traces.test.ts index 0d0b8bb11..5636735bb 100644 --- a/packages/cli/test/lib/api/traces.test.ts +++ b/packages/cli/test/lib/api/traces.test.ts @@ -804,8 +804,8 @@ describe("getDetailedTrace", () => { const url = new URL(getCapturedUrls()[0]!); expect(url.searchParams.get("timestamp")).toBe("1700000000"); expect(url.searchParams.get("statsPeriod")).toBeNull(); - expect(url.searchParams.get("limit")).toBe("10000"); - expect(url.searchParams.get("project")).toBe("-1"); + expect(url.searchParams.has("limit")).toBe(false); + expect(url.searchParams.has("project")).toBe(false); }); test("queries 14d and does not retry when spans are returned", async () => { @@ -859,18 +859,16 @@ describe("getDetailedTrace", () => { ).toBeNull(); }); - test("forwards project and additional attributes on both lookups", async () => { + test("forwards additional attributes on both lookups", async () => { const { getCapturedUrls } = mockSequential([[], [SPAN]]); await getDetailedTrace("my-org", "abc123def456", { - projectId: 42, additionalAttributes: ["gen_ai.request.model"], }); expect(getCapturedUrls()).toHaveLength(2); for (const raw of getCapturedUrls()) { const url = new URL(raw); - expect(url.searchParams.get("project")).toBe("42"); expect(url.searchParams.getAll("additional_attributes")).toEqual([ "gen_ai.request.model", ]);