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
9 changes: 9 additions & 0 deletions apps/cli-docs/src/fragments/commands/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@ 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.

## Inspect metric alert notifications

The detail view includes detection settings, threshold conditions, and linked
notification workflows with their filters, destinations, and enabled states.
Use `--json` to inspect native `config`, `conditionGroup`, `workflowIds`, and
complete `workflows` documents. Linked workflows that return 403 or 404 appear
in `workflowErrors`; their notifications could not be inspected. This is the
current configuration, not a record of alert firings or notification delivery.
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ sentry alert metrics view my-org/12345

# View by name
sentry alert metrics view my-org/'P95 latency alert'

# Inspect native thresholds and notification workflows
sentry alert metrics view my-org/12345 --json
```

### `sentry alert metrics create <target>`
Expand Down
94 changes: 61 additions & 33 deletions packages/cli/src/commands/alert/metrics/view.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import type { SentryContext } from "../../../context.js";
import {
getIssueAlertWorkflowDocument,
getMetricAlertRule,
} from "../../../lib/api-client.js";
import { parseOrgProjectArg } from "../../../lib/arg-parsing.js";
import { openInBrowser } from "../../../lib/browser.js";
import { buildCommand } from "../../../lib/command.js";
import { ApiError } from "../../../lib/errors.js";
import {
formatMetricAlertView,
type MetricAlertViewResult,
} from "../../../lib/formatters/alerts.js";
import { filterFields } from "../../../lib/formatters/json.js";
import { CommandOutput } from "../../../lib/formatters/output.js";
import { resolveOrgOnlyFromArg } from "../../../lib/resolve-target.js";
import { buildMetricAlertsUrl } from "../../../lib/sentry-web-urls.js";
import {
type MetricRuleResolution,
parseMetricRuleArg,
resolveMetricAlertRule,
} from "./rule-resolve.js";
import { metricAlertStatusLabel } from "./status.js";
import { isAllDigits } from "../../../lib/utils.js";
import { parseMetricRuleArg, resolveMetricAlertRule } from "./rule-resolve.js";

const USAGE_HINT = "sentry alert metrics view <org>/<rule-id-or-name>";

Expand All @@ -20,31 +26,15 @@ type ViewFlags = {
readonly fields?: string[];
};

type MetricAlertViewResult = MetricRuleResolution;

function formatMetricAlertView(data: MetricRuleResolution): string {
const { orgSlug, rule } = data;
const status = metricAlertStatusLabel(rule.status);
return [
`Metric alert rule in ${orgSlug}:`,
"",
`ID: ${rule.id}`,
`Name: ${rule.name}`,
`Status: ${status}`,
`Dataset: ${rule.dataset}`,
`Aggregate: ${rule.aggregate}`,
`Query: ${rule.query || "(none)"}`,
`Time Window: ${rule.timeWindow}m`,
`Projects: ${rule.projects.join(", ") || "(all)"}`,
`Environment: ${rule.environment ?? "all"}`,
`Owner: ${rule.owner ?? "none"}`,
].join("\n");
}

export const viewCommand = buildCommand({
docs: {
brief: "View a metric alert rule",
fullDescription: "View a single metric alert rule by ID or name.",
fullDescription:
"View a metric alert rule by ID or name, including detection settings, " +
"threshold conditions, and linked notification workflows.\n\n" +
"JSON preserves native config, conditionGroup, workflowIds, and workflow " +
"documents. Workflows that cannot be read (403/404) are listed in " +
"workflowErrors. This shows current configuration, not delivery history.",
examples: [
{
description:
Expand All @@ -59,14 +49,23 @@ export const viewCommand = buildCommand({
description: "View by name",
command: "sentry alert metrics view my-org/'P95 latency alert'",
},
{
description: "Inspect native thresholds and notification workflows",
command: "sentry alert metrics view my-org/12345 --json",
},
],
},
output: {
human: formatMetricAlertView,
jsonTransform: (data: MetricAlertViewResult) => ({
...data.rule,
org: data.orgSlug,
}),
jsonTransform: (data: MetricAlertViewResult, fields) => {
const result = {
...data.rule,
org: data.orgSlug,
workflows: data.workflows,
workflowErrors: data.workflowErrors,
};
return fields?.length ? filterFields(result, fields) : result;
},
},
parameters: {
positional: {
Expand Down Expand Up @@ -113,6 +112,35 @@ export const viewCommand = buildCommand({
}

const result = await resolveMetricAlertRule([org], ref, USAGE_HINT);
yield new CommandOutput(result);
// Name resolution uses the compact list projection; inspect the detail
// document so it has the same conditions and links as an ID lookup.
const rule = isAllDigits(ref)
? result.rule
: await getMetricAlertRule(result.orgSlug, result.rule.id);
const data: MetricAlertViewResult = {
orgSlug: result.orgSlug,
rule,
workflows: [],
workflowErrors: [],
};
for (const id of new Set(rule.workflowIds ?? [])) {
try {
data.workflows.push(
await getIssueAlertWorkflowDocument(result.orgSlug, id),
);
} catch (error) {
// A missing or inaccessible link must not hide the readable detector
// or be mistaken for an alert with no notification configuration.
if (
error instanceof ApiError &&
(error.status === 403 || error.status === 404)
) {
data.workflowErrors.push({ id, status: error.status });
} else {
throw error;
}
}
}
yield new CommandOutput(data);
},
});
26 changes: 21 additions & 5 deletions packages/cli/src/lib/api/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
createOrganizationWorkflow,
deleteOrganizationDetector,
getOrganizationDetector,
type GetOrganizationDetectorResponse,
getOrganizationWorkflow,
type GetOrganizationWorkflowResponse,
listOrganizationDetectors,
updateOrganizationDetector,
updateOrganizationWorkflow,
Expand Down Expand Up @@ -64,7 +66,12 @@ export type IssueAlertRule = {
};

/** A single metric alert rule (threshold-based, org-scoped) */
export type MetricAlertRule = {
export type MetricAlertRule = Partial<
Pick<
GetOrganizationDetectorResponse,
"config" | "conditionGroup" | "workflowIds"
>
> & {
id: string;
name: string;
/** 0/"0"/absent = active, 1/"1" = disabled */
Expand Down Expand Up @@ -102,6 +109,8 @@ type MetricDetector = {
dataSources?: Record<string, unknown>[] | null;
/** Detection settings (`detectionType`, `comparisonDelta`, …), kept on edit. */
config?: Record<string, unknown> | null;
conditionGroup?: GetOrganizationDetectorResponse["conditionGroup"];
workflowIds?: GetOrganizationDetectorResponse["workflowIds"];
};

/**
Expand Down Expand Up @@ -291,7 +300,7 @@ export async function listIssueAlertsPaginated(
export async function getIssueAlertWorkflowDocument(
orgSlug: string,
workflowId: string,
): Promise<Record<string, unknown>> {
): Promise<GetOrganizationWorkflowResponse> {
const config = await getOrgSdkConfig(orgSlug);
const result = await getOrganizationWorkflow({
...config,
Expand All @@ -300,7 +309,7 @@ export async function getIssueAlertWorkflowDocument(
workflow_id: Number(workflowId),
},
});
return unwrapResult<Record<string, unknown>>(
return unwrapResult<GetOrganizationWorkflowResponse>(
result,
"Failed to fetch issue alert rule",
);
Expand Down Expand Up @@ -429,7 +438,8 @@ export async function listMetricAlertsPaginated(
* Get a single metric alert rule by ID.
*
* Reads from the org-scoped `/detectors/{id}/` endpoint and maps the detector
* onto the flat `MetricAlertRule` shape.
* onto the flat `MetricAlertRule` shape, preserving native detection settings,
* conditions, and workflow links for inspection. List results stay compact.
*
* The `/detectors/{id}/` endpoint returns any detector kind (uptime, cron,
* error, …), unlike the `type:metric_issue`-filtered list. A non-metric id is
Expand All @@ -444,7 +454,13 @@ export async function getMetricAlertRule(
orgSlug: string,
ruleId: string,
): Promise<MetricAlertRule> {
return mapDetectorToMetricAlertRule(await getMetricDetector(orgSlug, ruleId));
const detector = await getMetricDetector(orgSlug, ruleId);
return {
...mapDetectorToMetricAlertRule(detector),
config: detector.config ?? undefined,
conditionGroup: detector.conditionGroup,
workflowIds: detector.workflowIds,
};
}

/** Fetch a metric-issue detector, rejecting other detector kinds with a 404. */
Expand Down
Loading
Loading