Skip to content
Draft
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
2 changes: 1 addition & 1 deletion apps/cli-docs/src/content/docs/agent-guidance.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ Display types with default sizes:

Use **common** types for general dashboards. Use **specialized** only when specifically requested. Avoid **internal** types unless the user explicitly asks.

Available datasets: `spans` (default), `errors`, `transactions`, `metrics`, `issue`, `logs`. Run `sentry dashboard widget --help` for dataset descriptions, query formats, and examples.
Available datasets: `spans` (default), `errors`, `metrics`, `issue`, `logs`. Run `sentry dashboard widget --help` for dataset descriptions, query formats, and examples.

**Row-filling examples:**

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ Display types with default sizes:

Use **common** types for general dashboards. Use **specialized** only when specifically requested. Avoid **internal** types unless the user explicitly asks.

Available datasets: `spans` (default), `errors`, `transactions`, `metrics`, `issue`, `logs`. Run `sentry dashboard widget --help` for dataset descriptions, query formats, and examples.
Available datasets: `spans` (default), `errors`, `metrics`, `issue`, `logs`. Run `sentry dashboard widget --help` for dataset descriptions, query formats, and examples.

**Row-filling examples:**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Add a widget to a dashboard

**Flags:**
- `-d, --display <value> - Display type (big_number, line, area, bar, table, stacked_area, top_n, text, categorical_bar, details, wheel, rage_and_dead_clicks, server_tree, agents_traces_table)`
- `--dataset <value> - Widget dataset (default: spans). Accepts canonical names and API synonyms: spans, error-events/errors, transaction-like/transactions, tracemetrics/metrics, logs, issue, discover`
- `--dataset <value> - Widget dataset (default: spans). Accepts canonical names and API synonyms: spans, error-events/errors, tracemetrics/metrics, logs, issue`
- `-q, --query <value>... - Aggregate expression (e.g. count, p95:span.duration)`
- `-w, --where <value> - Search conditions filter (e.g. is:unresolved)`
- `-g, --group-by <value>... - Group-by column (repeatable)`
Expand Down Expand Up @@ -123,7 +123,7 @@ Edit a widget in a dashboard
- `-t, --title <value> - Widget title to match`
- `--new-title <value> - New widget title`
- `-d, --display <value> - Display type (big_number, line, area, bar, table, stacked_area, top_n, text, categorical_bar, details, wheel, rage_and_dead_clicks, server_tree, agents_traces_table)`
- `--dataset <value> - Widget dataset (default: spans). Accepts canonical names and API synonyms: spans, error-events/errors, transaction-like/transactions, tracemetrics/metrics, logs, issue, discover`
- `--dataset <value> - Widget dataset (default: spans). Accepts canonical names and API synonyms: spans, error-events/errors, tracemetrics/metrics, logs, issue`
- `-q, --query <value>... - Aggregate expression (e.g. count, p95:span.duration)`
- `-w, --where <value> - Search conditions filter (e.g. is:unresolved)`
- `-g, --group-by <value>... - Group-by column (repeatable)`
Expand Down
11 changes: 4 additions & 7 deletions packages/cli/src/commands/dashboard/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,18 +807,15 @@ export async function enrichDashboardError(
/**
* User-facing dataset synonyms resolved to the canonical Sentry widget type.
*
* The Sentry API/UI and docs surface names like `errors` and `transactions`
* but widget types use `error-events` and `transaction-like`. The CLI accepts
* both forms so users copying from docs or using API-dataset terminology
* don't have to translate.
* The Sentry API/UI and docs surface names like `errors` but widget types
* use `error-events`. The CLI accepts both forms so users copying from
* docs or using API-dataset terminology don't have to translate.
*
* Keys are lowercase; matching is case-insensitive via {@link normalizeDataset}.
*/
const DATASET_ALIASES: Record<string, string> = {
error: "error-events",
errors: "error-events",
transaction: "transaction-like",
transactions: "transaction-like",
log: "logs",
// `metrics` and `metricsEnhanced` both alias to the canonical `tracemetrics`.
// `metricsEnhanced` is a legacy API synonym and may appear in older docs.
Expand All @@ -838,7 +835,7 @@ const DATASET_ALIASES: Record<string, string> = {
* Must be called once, up-front, and the result threaded through every
* downstream consumer (aggregate validator, warnings, PUT body). Leaving
* an un-normalized value in `flags.dataset` causes dataset-specific
* aggregate validation (e.g. `failure_rate` for `error-events`) to see
* aggregate validation (e.g. `count_if` for `error-events`) to see
* the alias instead of the canonical name and reject valid inputs.
*/
export function normalizeDataset(dataset?: string): string | undefined {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/dashboard/widget/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export const addCommand = buildCommand({
kind: "parsed",
parse: String,
brief:
"Widget dataset (default: spans). Accepts canonical names and API synonyms: spans, error-events/errors, transaction-like/transactions, tracemetrics/metrics, logs, issue, discover",
"Widget dataset (default: spans). Accepts canonical names and API synonyms: spans, error-events/errors, tracemetrics/metrics, logs, issue",
optional: true,
},
query: {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/dashboard/widget/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export const editCommand = buildCommand({
kind: "parsed",
parse: String,
brief:
"Widget dataset (default: spans). Accepts canonical names and API synonyms: spans, error-events/errors, transaction-like/transactions, tracemetrics/metrics, logs, issue, discover",
"Widget dataset (default: spans). Accepts canonical names and API synonyms: spans, error-events/errors, tracemetrics/metrics, logs, issue",
optional: true,
},
query: {
Expand Down Expand Up @@ -335,7 +335,7 @@ export const editCommand = buildCommand({
// Replace flags.dataset with the canonical value so every downstream
// consumer — validateEnumsAndAggregates, validateAggregateNames, and the
// PUT body — sees the normalized name. Without this, dataset-aware
// aggregate validation (e.g. failure_rate for error-events) would fail
// aggregate validation (e.g. count_if for error-events) would fail
// when the user passes --dataset errors.
const normalizedDataset = normalizeDataset(flags.dataset);
const normalizedFlags: EditFlags =
Expand Down
14 changes: 4 additions & 10 deletions packages/cli/src/commands/dashboard/widget/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,15 @@ export const widgetRoute = buildRouteMap({
" Example: p50(value,completion.duration_ms,distribution,none)\n" +
" Supported displays: line, area, bar, big_number,\n" +
" categorical_bar\n" +
" discover Legacy discover queries (adds failure_rate,\n" +
" apdex, etc.)\n" +
" issue Issue-based queries\n" +
" error-events — errors, error Error event queries\n" +
" transaction-like — transactions, transaction\n" +
" Transaction-based queries\n" +
" logs — log Log queries\n\n" +
"Dataset values are case-insensitive; Sentry UI/API names like 'errors'\n" +
"and 'transactions' are accepted in addition to the canonical forms.\n\n" +
"Dataset values are case-insensitive; the Sentry UI/API name 'errors'\n" +
"is accepted in addition to the canonical form.\n\n" +
"Aggregates (spans): count, count_unique, sum, avg, percentile, p50, p75,\n" +
" p90, p95, p99, p100, eps, epm, any, min, max\n" +
"Aggregates (discover adds): failure_count, failure_rate, apdex,\n" +
" count_miserable, user_misery, count_web_vitals, count_if, count_at_least,\n" +
" last_seen, latest_event, var, stddev, cov, corr, performance_score,\n" +
" opportunity_score, count_scores\n" +
"Aggregates (error-events adds): count_if, count_at_least, last_seen,\n" +
" latest_event, var, stddev, cov, corr\n" +
"Aliases: spm → epm, sps → eps, tpm → epm, tps → eps\n\n" +
"tracemetrics query format:\n" +
" aggregation(value,metric_name,metric_type,unit)\n" +
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/lib/api/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ async function queryWidgetTimeseries(
reqParams.topEvents = widget.limit ?? 5;
// Sort by the aggregate to get the actual top N groups.
// The sort param is only supported on the spans dataset —
// errors/discover endpoints reject it with 400.
// the errors endpoint rejects it with 400.
if (dataset === "spans") {
reqParams.sort = query.orderby ?? `-${aggregates[0] ?? "count()"}`;
}
Expand Down Expand Up @@ -542,7 +542,7 @@ async function queryWidgetTable(
start,
end,
// sort is only supported on the spans dataset —
// errors/discover endpoints reject it with 400.
// the errors endpoint rejects it with 400.
sort: dataset === "spans" ? query?.orderby || undefined : undefined,
per_page: widget.limit ?? 10,
environment: options.environment,
Expand Down
35 changes: 9 additions & 26 deletions packages/cli/src/types/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,8 @@ import { logger } from "../lib/logger.js";
* Source: sentry/src/sentry/models/dashboard_widget.py DashboardWidgetTypes.TYPES
*/
export const WIDGET_TYPES = [
"discover",
"issue",
"error-events",
"transaction-like",
"spans",
"logs",
"tracemetrics",
Expand Down Expand Up @@ -272,20 +270,13 @@ export const SPAN_AGGREGATE_FUNCTIONS = [
export type SpanAggregateFunction = (typeof SPAN_AGGREGATE_FUNCTIONS)[number];

/**
* Additional aggregate functions from the discover dataset.
* Available when widgetType is "discover" or "error-events".
* Additional aggregate functions for the error-events dataset.
* Available when widgetType is "error-events".
*
* Source: https://github.com/getsentry/sentry/blob/master/src/sentry/search/events/constants.py
* Dataset: https://github.com/getsentry/sentry/blob/master/src/sentry/search/events/datasets/discover.py
*/
export const DISCOVER_AGGREGATE_FUNCTIONS = [
export const ERROR_AGGREGATE_FUNCTIONS = [
...SPAN_AGGREGATE_FUNCTIONS,
"failure_count",
"failure_rate",
"apdex",
"count_miserable",
"user_misery",
"count_web_vitals",
"count_if",
"count_at_least",
"last_seen",
Expand All @@ -294,21 +285,15 @@ export const DISCOVER_AGGREGATE_FUNCTIONS = [
"stddev",
"cov",
"corr",
"performance_score",
"opportunity_score",
"count_scores",
] as const;

export type DiscoverAggregateFunction =
(typeof DISCOVER_AGGREGATE_FUNCTIONS)[number];
export type ErrorAggregateFunction = (typeof ERROR_AGGREGATE_FUNCTIONS)[number];

/** Valibot schema for validating a span aggregate function name */
export const SpanAggregateFunctionSchema = picklist(SPAN_AGGREGATE_FUNCTIONS);

/** Valibot schema for validating a discover aggregate function name */
export const DiscoverAggregateFunctionSchema = picklist(
DISCOVER_AGGREGATE_FUNCTIONS
);
/** Valibot schema for validating an error-event aggregate function name */
export const ErrorAggregateFunctionSchema = picklist(ERROR_AGGREGATE_FUNCTIONS);

/**
* Valid `is:` filter values for issue search conditions (--where flag).
Expand Down Expand Up @@ -450,8 +435,8 @@ export function validateAggregateNames(
}

const validFunctions: readonly string[] =
dataset === "discover" || dataset === "error-events"
? DISCOVER_AGGREGATE_FUNCTIONS
dataset === "error-events"
? ERROR_AGGREGATE_FUNCTIONS
: SPAN_AGGREGATE_FUNCTIONS;

for (const agg of aggregates) {
Expand Down Expand Up @@ -1013,17 +998,15 @@ export type WidgetDataResult =
*/
const WIDGET_TYPE_TO_DATASET: Record<string, string> = {
spans: "spans",
discover: "discover",
"error-events": "errors",
"transaction-like": "transactions",
logs: "logs",
tracemetrics: "tracemetrics",
};

/**
* Map a widget's `widgetType` to the API `dataset` parameter.
*
* @param widgetType - The widget's dataset type (e.g., "spans", "discover")
* @param widgetType - The widget's dataset type (e.g., "spans", "logs")
* @returns The API dataset string, or null if the type isn't queryable
*/
export function mapWidgetTypeToDataset(
Expand Down
7 changes: 0 additions & 7 deletions packages/cli/test/commands/dashboard/resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,23 +785,16 @@ describe("normalizeDataset", () => {
test("lowercases canonical values (pass-through)", () => {
expect(normalizeDataset("spans")).toBe("spans");
expect(normalizeDataset("error-events")).toBe("error-events");
expect(normalizeDataset("transaction-like")).toBe("transaction-like");
expect(normalizeDataset("tracemetrics")).toBe("tracemetrics");
expect(normalizeDataset("logs")).toBe("logs");
expect(normalizeDataset("issue")).toBe("issue");
expect(normalizeDataset("discover")).toBe("discover");
});

test("resolves error/errors aliases", () => {
expect(normalizeDataset("errors")).toBe("error-events");
expect(normalizeDataset("error")).toBe("error-events");
});

test("resolves transaction/transactions aliases", () => {
expect(normalizeDataset("transactions")).toBe("transaction-like");
expect(normalizeDataset("transaction")).toBe("transaction-like");
});

test("resolves metrics and metricsEnhanced aliases", () => {
expect(normalizeDataset("metrics")).toBe("tracemetrics");
expect(normalizeDataset("metricsEnhanced")).toBe("tracemetrics");
Expand Down
28 changes: 4 additions & 24 deletions packages/cli/test/commands/dashboard/widget/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,26 +282,6 @@ describe("dashboard widget add", () => {
expect(addedWidget.widgetType).toBe("error-events");
});

test("resolves dataset alias 'transactions' to 'transaction-like'", async () => {
const { context } = createMockContext();
const func = await addCommand.loader();
await func.call(
context,
{
json: false,
display: "line",
dataset: "transactions",
query: ["count"],
},
"123",
"Transactions Over Time"
);

const body = updateDashboardSpy.mock.calls[0]?.[2];
const addedWidget = body.widgets.at(-1);
expect(addedWidget.widgetType).toBe("transaction-like");
});

test("resolves dataset alias 'metricsEnhanced' to 'tracemetrics'", async () => {
const { context } = createMockContext();
const func = await addCommand.loader();
Expand All @@ -323,7 +303,7 @@ describe("dashboard widget add", () => {
});

test("dataset alias is resolved BEFORE dataset-aware aggregate validation", async () => {
// failure_rate is only valid for error-events/discover. With the alias
// last_seen is only valid for error-events. With the alias
// "errors", dataset-aware validation must see "error-events" (canonical)
// before deciding whether to accept the aggregate.
const { context } = createMockContext();
Expand All @@ -334,16 +314,16 @@ describe("dashboard widget add", () => {
json: false,
display: "big_number",
dataset: "errors",
query: ["failure_rate"],
query: ["last_seen"],
},
"123",
"Failure Rate"
"Last Seen"
);

const body = updateDashboardSpy.mock.calls[0]?.[2];
const addedWidget = body.widgets.at(-1);
expect(addedWidget.widgetType).toBe("error-events");
expect(addedWidget.queries[0].aggregates).toEqual(["failure_rate()"]);
expect(addedWidget.queries[0].aggregates).toEqual(["last_seen()"]);
});

test("case-insensitive dataset values are accepted", async () => {
Expand Down
36 changes: 14 additions & 22 deletions packages/cli/test/commands/dashboard/widget/edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,7 @@ describe("dashboard widget edit", () => {
const { context } = createMockContext();
const func = await editCommand.loader();
// Should not throw — "text" is untracked, no dataset constraint applies
await func.call(
context,
{ json: false, index: 0, dataset: "discover" },
"123"
);
await func.call(context, { json: false, index: 0, dataset: "logs" }, "123");
expect(updateDashboardSpy).toHaveBeenCalled();
});

Expand Down Expand Up @@ -415,23 +411,23 @@ describe("dashboard widget edit", () => {
const { context } = createMockContext();
const func = await editCommand.loader();

// "failure_rate" is valid for discover but not spans.
// Here we change the existing spans widget to discover dataset while
// also setting a discover-only aggregate. This should succeed.
// "last_seen" is valid for error-events but not spans.
// Here we change the existing spans widget to error-events dataset while
// also setting a last_seen aggregate. This should succeed.
await func.call(
context,
{
json: false,
index: 0,
dataset: "discover",
query: ["failure_rate"],
dataset: "error-events",
query: ["last_seen"],
},
"123"
);

const body = updateDashboardSpy.mock.calls[0]?.[2];
expect(body.widgets[0].widgetType).toBe("discover");
expect(body.widgets[0].queries[0].aggregates).toEqual(["failure_rate()"]);
expect(body.widgets[0].widgetType).toBe("error-events");
expect(body.widgets[0].queries[0].aggregates).toEqual(["last_seen()"]);
});

test("resolves --dataset alias 'errors' to 'error-events' in PUT body", async () => {
Expand All @@ -448,9 +444,9 @@ describe("dashboard widget edit", () => {
});

test("dataset alias is resolved BEFORE dataset-aware aggregate validation", async () => {
// Regression test for the "aliases resolve too late" bug: failure_rate
// Regression test for the "aliases resolve too late" bug: last_seen
// is valid for error-events, so passing --dataset errors --query
// failure_rate must succeed. If the alias is not applied before
// last_seen must succeed. If the alias is not applied before
// validateAggregateNames runs, "errors" falls through the canonical
// branch and "Unknown aggregate function" is thrown.
const { context } = createMockContext();
Expand All @@ -461,27 +457,23 @@ describe("dashboard widget edit", () => {
json: false,
index: 0,
dataset: "errors",
query: ["failure_rate"],
query: ["last_seen"],
},
"123"
);

const body = updateDashboardSpy.mock.calls[0]?.[2];
expect(body.widgets[0].widgetType).toBe("error-events");
expect(body.widgets[0].queries[0].aggregates).toEqual(["failure_rate()"]);
expect(body.widgets[0].queries[0].aggregates).toEqual(["last_seen()"]);
});

test("case-insensitive --dataset values are accepted", async () => {
const { context } = createMockContext();
const func = await editCommand.loader();
await func.call(
context,
{ json: false, index: 0, dataset: "TRANSACTIONS" },
"123"
);
await func.call(context, { json: false, index: 0, dataset: "LOGS" }, "123");

const body = updateDashboardSpy.mock.calls[0]?.[2];
expect(body.widgets[0].widgetType).toBe("transaction-like");
expect(body.widgets[0].widgetType).toBe("logs");
});

test("auto-defaults --limit to 5 when adding --group-by without --limit", async () => {
Expand Down
Loading
Loading