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
4 changes: 2 additions & 2 deletions apps/cli-docs/src/fragments/commands/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ sentry alert issues delete my-org/my-project/12345 --dry-run
sentry alert metrics create my-org \
--name "P95 Latency" \
--query "environment:prod" \
--aggregate "p95(transaction.duration)" \
--dataset transactions \
--aggregate "p95(span.duration)" \
--dataset spans \
--time-window 5 \
--trigger '{"alertThreshold":500,"actions":[{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}]}'
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ Create a metric alert rule
**Flags:**
- `--name <value> - Rule name`
- `--query <value> - Metric query filter string`
- `--aggregate <value> - Aggregate expression (for example count(), p95(transaction.duration))`
- `--dataset <value> - Dataset: errors (error-events), transactions (transaction-like), sessions, events, spans, metrics`
- `--aggregate <value> - Aggregate expression (for example count(), p95(span.duration))`
- `--dataset <value> - Dataset: errors (error-events), sessions, events, spans, metrics`
- `--time-window <value> - Evaluation window in minutes`
- `-t, --trigger <value>... - Trigger object JSON (repeatable, or pass one JSON array)`
- `-p, --project <value>... - Project slug filter (repeatable or comma-separated)`
Expand All @@ -170,8 +170,8 @@ Create a metric alert rule
sentry alert metrics create my-org \
--name "P95 Latency" \
--query "environment:prod" \
--aggregate "p95(transaction.duration)" \
--dataset transactions \
--aggregate "p95(span.duration)" \
--dataset spans \
--time-window 5 \
--trigger '{"alertThreshold":500,"actions":[{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}]}'
```
Expand Down Expand Up @@ -201,7 +201,7 @@ Edit a metric alert rule
- `--status <value> - active or disabled`
- `--query <value> - Metric query filter`
- `--aggregate <value> - Aggregate expression`
- `--dataset <value> - Dataset: errors (error-events), transactions (transaction-like), sessions, events, spans, metrics`
- `--dataset <value> - Dataset: errors (error-events), sessions, events, spans, metrics`
- `--time-window <value> - Evaluation window in minutes`
- `-t, --trigger <value>... - Trigger object JSON (repeatable, or pass one JSON array)`
- `-p, --project <value>... - Project slug filter (repeatable or comma-separated)`
Expand Down
9 changes: 4 additions & 5 deletions packages/cli/src/commands/alert/metrics/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ export const createCommand = buildCommand({
" --environment, --owner\n\n" +
"Examples:\n" +
" sentry alert metrics create my-org --name 'P95 latency' \\\n" +
" --query 'environment:prod' --aggregate 'p95(transaction.duration)' \\\n" +
" --dataset transactions --time-window 5 \\\n" +
" --query 'environment:prod' --aggregate 'p95(span.duration)' \\\n" +
" --dataset spans --time-window 5 \\\n" +
' --trigger \'{"alertThreshold":500,"actions":[{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}]}\'\n\n' +
" sentry alert metrics create my-org --name 'Error volume' \\\n" +
" --query 'event.type:error' --aggregate 'count()' --dataset errors \\\n" +
Expand Down Expand Up @@ -122,14 +122,13 @@ export const createCommand = buildCommand({
aggregate: {
kind: "parsed",
parse: String,
brief:
"Aggregate expression (for example count(), p95(transaction.duration))",
brief: "Aggregate expression (for example count(), p95(span.duration))",
},
dataset: {
kind: "parsed",
parse: String,
brief:
"Dataset: errors (error-events), transactions (transaction-like), sessions, events, spans, metrics",
"Dataset: errors (error-events), sessions, events, spans, metrics",
},
"time-window": {
kind: "parsed",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/alert/metrics/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export const editCommand = buildCommand({
"Examples:\n" +
" sentry alert metrics edit my-org/9 --name 'Error budget'\n" +
" sentry alert metrics edit my-org/9 --status disabled\n" +
" sentry alert metrics edit my-org/9 --time-window 15 --dataset transactions",
" sentry alert metrics edit my-org/9 --time-window 15 --dataset spans",
},
output: {
human: formatEdited,
Expand Down Expand Up @@ -213,7 +213,7 @@ export const editCommand = buildCommand({
parse: String,
optional: true,
brief:
"Dataset: errors (error-events), transactions (transaction-like), sessions, events, spans, metrics",
"Dataset: errors (error-events), sessions, events, spans, metrics",
},
"time-window": {
kind: "parsed",
Expand Down
17 changes: 7 additions & 10 deletions packages/cli/src/commands/alert/mutation-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { ValidationError } from "../../lib/errors.js";
const ISSUE_MATCH_MODES = new Set(["all", "any"]);
const METRIC_DATASET_VALUES = new Set([
"errors",
"transactions",
"sessions",
"events",
"spans",
Expand All @@ -19,9 +18,9 @@ const METRIC_DATASET_VALUES = new Set([
*
* Only aliases that resolve to a value already accepted by the metric alert API
* are listed here: the singular forms (`error` → `errors`) and the dashboard
* terminology for the error/transaction datasets (`error-events` → `errors`,
* `transaction-like` → `transactions`). This lets users copying from dashboard
* docs avoid a validation error without changing which dataset is sent.
* terminology for the error dataset (`error-events` → `errors`). This lets
* users copying from dashboard docs avoid a validation error without
* changing which dataset is sent.
*
* Names that denote a *distinct* dataset in the metric alert path — e.g.
* `tracemetrics`, `metricsenhanced`, `eap`, `events_analytics_platform` — are
Expand All @@ -33,12 +32,10 @@ const METRIC_DATASET_VALUES = new Set([
const METRIC_DATASET_ALIASES: Record<string, string> = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add span: "spans" at least and I may even add transaction: "spans" and transactions: "spans" for a smoother transition.

@mjq mjq Aug 31, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BYK The super slick thing to do might be for requests for transaction(s) to route to spans with is_transaction:true in the query. Too magic or just magic enough? Would result in some new special case code of course.

@BYK BYK Aug 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it. We may print a warning/tip using the built-in system we do for gentle nudges. Like do the routing but also tell them "the right way" gently.

// Singular forms
error: "errors",
transaction: "transactions",
session: "sessions",
metric: "metrics",
// Dashboard terminology for the error/transaction datasets
// Dashboard terminology for the error dataset
"error-events": "errors",
"transaction-like": "transactions",
};
const METRIC_TIME_WINDOWS = new Set([
1, 5, 10, 15, 30, 60, 120, 240, 360, 720, 1440,
Expand Down Expand Up @@ -203,9 +200,9 @@ export function normalizeProjectList(
* Normalise a user-provided `--dataset` value to the canonical metric alert
* dataset name accepted by the Sentry API.
*
* Resolves known aliases (e.g. `error-events` → `errors`, `transaction` →
* `transactions`) so that values copied from dashboard docs or using singular
* forms work without manual translation.
* Resolves known aliases (e.g. `error-events` → `errors`) so that values copied
* from dashboard docs or using singular forms work without manual
* translation.
*/
export function normalizeMetricDataset(dataset: string): string {
const lower = dataset.trim().toLowerCase();
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/test/commands/alert/metrics/edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe("alert metrics edit", () => {
status: 1,
query: "event.type:error environment:prod",
aggregate: "count()",
dataset: "transactions",
dataset: "spans",
timeWindow: 15,
triggers: [{ alertThreshold: 200, actions: [{ id: "notify" }] }],
});
Expand All @@ -114,7 +114,7 @@ describe("alert metrics edit", () => {
status: "disabled",
query: "event.type:error environment:prod",
aggregate: "count()",
dataset: "transactions",
dataset: "spans",
"time-window": 15,
trigger: ['{"alertThreshold":200,"actions":[{"id":"notify"}]}'],
json: true,
Expand All @@ -128,7 +128,7 @@ describe("alert metrics edit", () => {
status: 1,
query: "event.type:error environment:prod",
aggregate: "count()",
dataset: "transactions",
dataset: "spans",
timeWindow: 15,
triggers: [{ alertThreshold: 200, actions: [{ id: "notify" }] }],
});
Expand Down
19 changes: 5 additions & 14 deletions packages/cli/test/commands/alert/mutation-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,23 +212,20 @@ describe("normalizeProjectList", () => {
describe("normalizeMetricDataset", () => {
test.each([
["error", "errors"],
["transaction", "transactions"],
["session", "sessions"],
["metric", "metrics"],
["error-events", "errors"],
["transaction-like", "transactions"],
])('maps alias "%s" to "%s"', (input, expected) => {
expect(normalizeMetricDataset(input)).toBe(expected);
});

test("is case-insensitive and trims whitespace", () => {
expect(normalizeMetricDataset("ERROR-EVENTS")).toBe("errors");
expect(normalizeMetricDataset(" Transaction-Like ")).toBe("transactions");
expect(normalizeMetricDataset(" Error-Events ")).toBe("errors");
});

test.each([
"errors",
"transactions",
"sessions",
"events",
"spans",
Expand All @@ -254,14 +251,7 @@ describe("normalizeMetricDataset", () => {
});

describe("validateMetricDataset", () => {
const valid = [
"errors",
"transactions",
"sessions",
"events",
"spans",
"metrics",
];
const valid = ["errors", "sessions", "events", "spans", "metrics"];

for (const dataset of valid) {
test(`passes for "${dataset}"`, () => {
Expand All @@ -272,7 +262,6 @@ describe("validateMetricDataset", () => {
test.each([
"error",
"error-events",
"transaction-like",
"METRIC",
])('passes for alias "%s"', (dataset) => {
expect(() => validateMetricDataset(dataset)).not.toThrow();
Expand All @@ -282,7 +271,9 @@ describe("validateMetricDataset", () => {
"tracemetrics",
"eap",
"events_analytics_platform",
])('throws for non-aliased dataset "%s"', (dataset) => {
"transactions",
"transaction-like",
])('throws for obsolete or non-aliased dataset "%s"', (dataset) => {
expect(() => validateMetricDataset(dataset)).toThrow(ValidationError);
});

Expand Down
32 changes: 16 additions & 16 deletions packages/cli/test/lib/api/alerts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ function metricDetector(overrides: Record<string, unknown>) {
dateCreated: "2026-01-01T00:00:00Z",
dataSources: [
{
aggregate: "p95(transaction.duration)",
dataset: "transactions",
aggregate: "p95(span.duration)",
dataset: "spans",
query: "environment:prod",
// Detectors expose the window in seconds; 300s == 5m.
timeWindow: 300,
Expand Down Expand Up @@ -159,8 +159,8 @@ describe("listMetricAlertsPaginated", () => {
id: "9",
name: "P95 latency",
status: 0,
aggregate: "p95(transaction.duration)",
dataset: "transactions",
aggregate: "p95(span.duration)",
dataset: "spans",
query: "environment:prod",
// 300s from the detector payload is normalized to 5 minutes.
timeWindow: 5,
Expand Down Expand Up @@ -221,8 +221,8 @@ describe("getMetricAlertRule", () => {
queryObj: {
snubaQuery: {
aggregate: "p75(measurements.lcp)",
dataset: "transactions",
query: "transaction.op:pageload",
dataset: "spans",
query: "span.op:pageload",
timeWindow: 600,
},
},
Expand All @@ -234,8 +234,8 @@ describe("getMetricAlertRule", () => {

const rule = await getMetricAlertRule("test-org", "9");
expect(rule.aggregate).toBe("p75(measurements.lcp)");
expect(rule.dataset).toBe("transactions");
expect(rule.query).toBe("transaction.op:pageload");
expect(rule.dataset).toBe("spans");
expect(rule.query).toBe("span.op:pageload");
expect(rule.timeWindow).toBe(10);
});

Expand Down Expand Up @@ -393,8 +393,8 @@ describe("createMetricAlertRule", () => {
type: "metric_issue",
dataSources: [
{
aggregate: "p95(transaction.duration)",
dataset: "transactions",
aggregate: "p95(span.duration)",
dataset: "spans",
query: "environment:prod",
queryType: 1,
eventTypes: ["trace_item_span"],
Expand All @@ -418,8 +418,8 @@ describe("createMetricAlertRule", () => {
const created = await createMetricAlertRule("test-org", {
name: "P95 latency",
query: "environment:prod",
aggregate: "p95(transaction.duration)",
dataset: "transactions",
aggregate: "p95(span.duration)",
dataset: "spans",
timeWindow: 5,
environment: "prod",
triggers: [{ alertThreshold: 500, actions: [{ id: "notify" }] }],
Expand Down Expand Up @@ -480,8 +480,8 @@ describe("getMetricAlertRuleDocument", () => {
expect(doc).toMatchObject({
id: "9",
name: "Baseline",
aggregate: "p95(transaction.duration)",
dataset: "transactions",
aggregate: "p95(span.duration)",
dataset: "spans",
timeWindow: 5,
});
});
Expand All @@ -506,8 +506,8 @@ describe("putMetricAlertRule", () => {
const updated = await putMetricAlertRule("test-org", "9", {
name: "Renamed",
query: "environment:prod",
aggregate: "p95(transaction.duration)",
dataset: "transactions",
aggregate: "p95(span.duration)",
dataset: "spans",
timeWindow: 5,
status: 1,
triggers: [{ alertThreshold: 500, actions: [{ id: "notify" }] }],
Expand Down
Loading