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
21 changes: 19 additions & 2 deletions apps/cli-docs/src/fragments/commands/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,33 @@ sentry alert issues delete my-org/my-project/12345 --dry-run
### Create a metric alert rule

```bash
# Create an organization metric alert rule
# Alert when P95 span duration goes above 500ms
sentry alert metrics create my-org \
--name "P95 Latency" \
--project my-app \
--query "environment:prod" \
--aggregate "p95(span.duration)" \
--dataset spans \
--time-window 5 \
--trigger '{"alertThreshold":500,"actions":[{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}]}'
--trigger '{"alertThreshold":500}'

# Alert on any matching log line and notify through an existing alert
sentry alert metrics create my-org \
--name "AI chat failures" \
--project my-app \
--query "action:ai.chat result:error" \
--aggregate "count()" \
--dataset logs \
--time-window 5 \
--trigger '{"alertThreshold":0,"resolveThreshold":0}' \
--workflow 12345
```

Triggers take `alertThreshold`, an optional `label` (`critical` or `warning`),
`resolveThreshold`, and `thresholdType` (`0` above, `1` below). Notification
actions are not part of triggers; pass `--workflow` with the ID of an existing
alert to notify.

### List metric alert rules

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,25 +155,38 @@ Create a metric alert rule
- `--name <value> - Rule name`
- `--query <value> - Metric query filter string`
- `--aggregate <value> - Aggregate expression (for example count(), p95(span.duration))`
- `--dataset <value> - Dataset: errors (error-events), sessions, events, spans, metrics (transaction(s) routes to spans)`
- `--dataset <value> - Dataset: errors, events, spans, logs, metrics, sessions (error-events is errors; transaction(s) routes to spans)`
- `--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)`
- `--environment <value> - Environment filter`
- `--owner <value> - Owner value accepted by Sentry API`
- `--workflow <value>... - Alert (workflow) id to notify (repeatable or comma-separated)`
- `-n, --dry-run - Show what would happen without making changes`

**Examples:**

```bash
# Create an organization metric alert rule
# Alert when P95 span duration goes above 500ms
sentry alert metrics create my-org \
--name "P95 Latency" \
--project my-app \
--query "environment:prod" \
--aggregate "p95(span.duration)" \
--dataset spans \
--time-window 5 \
--trigger '{"alertThreshold":500,"actions":[{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}]}'
--trigger '{"alertThreshold":500}'

# Alert on any matching log line and notify through an existing alert
sentry alert metrics create my-org \
--name "AI chat failures" \
--project my-app \
--query "action:ai.chat result:error" \
--aggregate "count()" \
--dataset logs \
--time-window 5 \
--trigger '{"alertThreshold":0,"resolveThreshold":0}' \
--workflow 12345
```

### `sentry alert metrics delete <org/rule-id-or-name>`
Expand Down Expand Up @@ -201,12 +214,13 @@ 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), sessions, events, spans, metrics (transaction(s) routes to spans)`
- `--dataset <value> - Dataset: errors, events, spans, logs, metrics, sessions (error-events is errors; transaction(s) routes to spans)`
- `--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)`
- `--environment <value> - Environment value (pass empty string to clear)`
- `--owner <value> - Owner value (pass empty string to clear)`
- `--workflow <value>... - Replace connected alert (workflow) ids (repeatable or comma-separated; empty string disconnects all)`

**Examples:**

Expand Down
48 changes: 35 additions & 13 deletions packages/cli/src/commands/alert/metrics/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ import { warning } from "../../../lib/formatters/colors.js";
import { CommandOutput } from "../../../lib/formatters/output.js";
import { DRY_RUN_ALIASES, DRY_RUN_FLAG } from "../../../lib/mutate-command.js";
import { resolveOrgOnlyFromArg } from "../../../lib/resolve-target.js";
import {
METRIC_ALERT_DATASET_BRIEF,
parseMetricTriggers,
} from "../../../types/metric-alert.js";
import {
normalizeProjectList,
parseJsonObjectList,
parseWorkflowIds,
resolveMetricDataset,
validateMetricDataset,
validateMetricTimeWindow,
validateMetricTriggers,
} from "../mutation-utils.js";
import { metricAlertStatusLabel } from "./status.js";

Expand All @@ -32,6 +36,7 @@ type CreateFlags = {
readonly project?: string[];
readonly environment?: string;
readonly owner?: string;
readonly workflow?: string[];
readonly "dry-run": boolean;
readonly json: boolean;
readonly fields?: string[];
Expand Down Expand Up @@ -69,20 +74,26 @@ export const createCommand = buildCommand({
docs: {
brief: "Create a metric alert rule",
fullDescription:
"Create an organization-scoped metric alert rule.\n\n" +
"Create a metric alert rule (a metric monitor in the detectors API).\n\n" +
"Required fields:\n" +
" --name, --query, --aggregate, --dataset, --time-window, --trigger (>=1), --project\n\n" +
"Optional fields:\n" +
" --environment, --owner\n\n" +
" --environment, --owner, --workflow\n\n" +
"Triggers:\n" +
' {"alertThreshold":N, "label":"critical"|"warning", "resolveThreshold":N, "thresholdType":0|1}\n' +
" thresholdType 0 alerts above the threshold (default), 1 below. Without resolveThreshold\n" +
" the rule resolves at the critical threshold. Native detector conditions\n" +
' ({"type":"gt","comparison":N,"conditionResult":75}) are passed through as-is.\n' +
" Notifications are not part of triggers: pass --workflow with an existing alert id.\n\n" +
"Examples:\n" +
" sentry alert metrics create my-org/ --name 'P95 latency' \\\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" +
' --time-window 15 --trigger \'[{"alertThreshold":100,"actions":[{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}]}]\' \\\n' +
" --project my-app --dry-run",
" --dataset spans --time-window 5 --project my-app \\\n" +
" --trigger '{\"alertThreshold\":500}'\n\n" +
" sentry alert metrics create my-org/ --name 'AI chat failures' \\\n" +
" --query 'action:ai.chat result:error' --aggregate 'count()' --dataset logs \\\n" +
' --time-window 5 --trigger \'{"alertThreshold":0,"resolveThreshold":0}\' \\\n' +
" --project my-app --workflow 123 --dry-run",
},
output: {
human: formatCreated,
Expand Down Expand Up @@ -118,8 +129,7 @@ export const createCommand = buildCommand({
dataset: {
kind: "parsed",
parse: String,
brief:
"Dataset: errors (error-events), sessions, events, spans, metrics (transaction(s) routes to spans)",
brief: METRIC_ALERT_DATASET_BRIEF,
},
"time-window": {
kind: "parsed",
Expand Down Expand Up @@ -152,6 +162,13 @@ export const createCommand = buildCommand({
optional: true,
brief: "Owner value accepted by Sentry API",
},
workflow: {
kind: "parsed",
parse: String,
variadic: true,
optional: true,
brief: "Alert (workflow) id to notify (repeatable or comma-separated)",
},
"dry-run": DRY_RUN_FLAG,
},
aliases: { ...DRY_RUN_ALIASES, t: "trigger", p: "project" },
Expand Down Expand Up @@ -179,8 +196,9 @@ export const createCommand = buildCommand({
this.stderr.write(`${warning(`Tip: ${datasetNotice}`)}\n`);
}

const triggers = parseJsonObjectList(flags.trigger, "trigger");
validateMetricTriggers(triggers);
const triggers = parseMetricTriggers(
parseJsonObjectList(flags.trigger, "trigger"),
);
const projects = normalizeProjectList(flags.project);
if (!projects || projects.length === 0) {
throw new ValidationError(
Expand Down Expand Up @@ -208,6 +226,10 @@ export const createCommand = buildCommand({
if (flags.owner !== undefined) {
body.owner = flags.owner;
}
const workflowIds = parseWorkflowIds(flags.workflow);
if (workflowIds) {
body.workflowIds = workflowIds;
}

if (flags["dry-run"]) {
yield new CommandOutput({
Expand Down
33 changes: 23 additions & 10 deletions packages/cli/src/commands/alert/metrics/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@ import { ValidationError } from "../../../lib/errors.js";
import { warning } from "../../../lib/formatters/colors.js";
import { CommandOutput } from "../../../lib/formatters/output.js";
import { resolveOrgOnlyFromArg } from "../../../lib/resolve-target.js";
import {
METRIC_ALERT_DATASET_BRIEF,
parseMetricTriggers,
} from "../../../types/metric-alert.js";
import {
normalizeMetricDataset,
normalizeProjectList,
parseJsonObjectList,
parseStatusFlag,
parseWorkflowIds,
resolveMetricDataset,
statusToMetricValue,
validateMetricDataset,
validateMetricTimeWindow,
validateMetricTriggers,
} from "../mutation-utils.js";
import { parseMetricRuleArg, resolveMetricAlertRule } from "./rule-resolve.js";
import { metricAlertStatusLabel } from "./status.js";
Expand All @@ -42,6 +46,7 @@ type EditFlags = {
readonly project?: string[];
readonly environment?: string;
readonly owner?: string;
readonly workflow?: string[];
readonly json: boolean;
readonly fields?: string[];
};
Expand All @@ -62,7 +67,8 @@ function hasMetricMutations(flags: EditFlags): boolean {
flags.trigger !== undefined ||
flags.project !== undefined ||
flags.environment !== undefined ||
flags.owner !== undefined
flags.owner !== undefined ||
flags.workflow !== undefined
);
}

Expand Down Expand Up @@ -118,7 +124,9 @@ function applyMetricOptionalFields(
flags: EditFlags,
): Record<string, unknown> {
if (flags.trigger !== undefined) {
body.triggers = parseJsonObjectList(flags.trigger, "trigger");
body.triggers = parseMetricTriggers(
parseJsonObjectList(flags.trigger, "trigger"),
);
}
if (flags.project !== undefined) {
body.projects = normalizeProjectList(flags.project) ?? [];
Expand All @@ -130,18 +138,16 @@ function applyMetricOptionalFields(
if (flags.owner !== undefined) {
body.owner = flags.owner.trim() === "" ? null : flags.owner;
}
if (flags.workflow !== undefined) {
body.workflowIds = parseWorkflowIds(flags.workflow) ?? [];
}
return body;
}

function validateMetricBody(
body: Record<string, unknown>,
flags: EditFlags,
): void {
if (flags.trigger !== undefined) {
validateMetricTriggers(
body.triggers as Record<string, unknown>[] | undefined,
);
}
if (flags.dataset !== undefined) {
validateMetricDataset(normalizeMetricDataset(String(body.dataset)));
}
Expand Down Expand Up @@ -221,8 +227,7 @@ export const editCommand = buildCommand({
kind: "parsed",
parse: String,
optional: true,
brief:
"Dataset: errors (error-events), sessions, events, spans, metrics (transaction(s) routes to spans)",
brief: METRIC_ALERT_DATASET_BRIEF,
},
"time-window": {
kind: "parsed",
Expand Down Expand Up @@ -256,6 +261,14 @@ export const editCommand = buildCommand({
optional: true,
brief: "Owner value (pass empty string to clear)",
},
workflow: {
kind: "parsed",
parse: String,
variadic: true,
optional: true,
brief:
"Replace connected alert (workflow) ids (repeatable or comma-separated; empty string disconnects all)",
},
},
aliases: {
t: "trigger",
Expand Down
Loading
Loading