Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions packages/cli/src/commands/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
};
Expand Down
14 changes: 5 additions & 9 deletions packages/cli/src/commands/trace/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
]);

Expand Down
47 changes: 25 additions & 22 deletions packages/cli/src/lib/api/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getOrganizationWorkflow,
type GetOrganizationWorkflowResponse,
listOrganizationDetectors,
listOrganizationWorkflows,
updateOrganizationDetector,
updateOrganizationWorkflow,
type UpdateOrganizationDetectorResponse,
Expand All @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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.
*/
Expand All @@ -277,20 +275,21 @@ export async function listIssueAlertsPaginated(
projectSlug: string,
options: { perPage?: number; cursor?: string } = {},
): Promise<PaginatedResponse<IssueAlertRule[]>> {
const regionUrl = await resolveOrgRegion(orgSlug);
const { data, headers } = await apiRequestToRegion<IssueAlertRule[]>(
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<IssueAlertRule[]>(
result,
"Failed to list issue alert rules",
);
const { nextCursor } = parseLinkHeader(headers.get("link") ?? null);
return { data: filterAttachedIssueAlertRules(data), nextCursor };
}

Expand Down Expand Up @@ -366,11 +365,15 @@ export async function getIssueAlertRule(
projectSlug: string,
ruleId: string,
): Promise<IssueAlertRule> {
const regionUrl = await resolveOrgRegion(orgSlug);
const { data } = await apiRequestToRegion<IssueAlertRule[]>(
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<IssueAlertRule[]>(
result,
"Failed to fetch issue alert rule",
);
const rule = filterAttachedIssueAlertRules(data)[0];
if (!rule) {
Expand Down
35 changes: 20 additions & 15 deletions packages/cli/src/lib/api/code-mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Auth: requires `org:ci` scope.
*/

import { bulkUpsertOrganizationCodeMappings } from "@sentry/api";
import {
array,
type InferOutput,
Expand All @@ -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");

Expand Down Expand Up @@ -97,7 +97,7 @@ export async function uploadCodeMappings(
options: CodeMappingsUploadOptions,
): Promise<MergedCodeMappingsResponse> {
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 = {
Expand All @@ -119,19 +119,24 @@ export async function uploadCodeMappings(
);
}

const { data } = await apiRequestToRegion<BulkCodeMappingsResponse>(
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;
Expand Down
59 changes: 35 additions & 24 deletions packages/cli/src/lib/api/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createOrganizationDashboard,
getOrganizationDashboard,
listOrganizationDashboards,
listOrganizationEvents,
updateOrganizationDashboard,
} from "@sentry/api";
import { resolveEventsSort } from "@sentry/toolkit-core/events-sort";
Expand Down Expand Up @@ -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,
Expand All @@ -49,6 +52,7 @@ import {
parseLinkHeader,
unwrapPaginatedResult,
unwrapResult,
unwrapValidatedResult,
} from "./infrastructure.js";

/**
Expand Down Expand Up @@ -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<TimeseriesResult> {
const {
regionUrl,
orgSlug,
widget,
dataset,
statsPeriod,
start,
end,
Expand All @@ -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<string, string | string[] | number | undefined> = {
yAxis: aggregates,
query: query.conditions || undefined,
dataset: dataset ?? undefined,
dataset,
statsPeriod,
start,
end,
Expand Down Expand Up @@ -572,12 +579,13 @@ async function queryWidgetTimeseries(
* @param options - Additional query options
*/
async function queryWidgetTable(
params: WidgetQueryParams,
params: DatasetWidgetQueryParams,
): Promise<TableResult> {
const {
regionUrl,
orgSlug,
widget,
dataset,
statsPeriod,
start,
end,
Expand All @@ -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;
Expand Down Expand Up @@ -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];
Expand All @@ -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 {
Expand Down
Loading
Loading