Skip to content

fix(alerts): read metric alert rules from org-scoped /detectors/ - #1275

Merged
BYK merged 6 commits into
mainfrom
issue-1274-alerts-legacy-api-migration
Jul 24, 2026
Merged

BYK merged 6 commits into
mainfrom
issue-1274-alerts-legacy-api-migration

Conversation

@jared-outpost

@jared-outpost jared-outpost Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Migrates the metric-alert read path (list / view / rule resolution) off the legacy /organizations/{org}/alert-rules/ endpoint — which is being retired on 2026-08-17 and 410s during brownouts — to the new org-scoped /organizations/{org}/detectors/ endpoint filtered to type:metric_issue. A mapDetectorToMetricAlertRule helper flattens the nested detector payload into the existing MetricAlertRule shape, so command formatters and resolvers work unchanged.

This mirrors the proven issue-alert read migration (#1215/#1216). Metric-alert mutations (create/edit/delete) still use the legacy endpoint pending the follow-up mutation migration tracked in #1277 — same transitional state as issue alerts today.

Testing

  • pnpm run typecheck — clean
  • pnpm run lint — clean
  • npx vitest run test/lib/api/alerts.test.ts test/commands/alert — 150 passing (updated metric-list mocks to /detectors/; added list/get + nested-snubaQuery mapping tests)

Refs #1274, getsentry/toolkit#1482

Migrate the metric-alert read path (list/view/get) off the legacy
/organizations/{org}/alert-rules/ endpoint, which is being retired on
2026-08-17, to the new org-scoped /organizations/{org}/detectors/
endpoint filtered to type:metric_issue.

Detectors return a nested shape, so mapDetectorToMetricAlertRule
flattens each detector into the existing MetricAlertRule shape the
list/view/resolve commands already consume (threshold fields live in
dataSources[0], active/disabled state is the top-level enabled flag).

Mirrors the proven issue-alert read migration (#1215/#1216). Metric
alert mutations (create/edit/delete) still use the legacy endpoint
pending the follow-up mutation migration.

Refs #1274, #1182
@github-actions

github-actions Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-1275/

Built to branch gh-pages at 2026-07-22 12:38 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Codecov Results 📊

✅ Patch coverage is 90.48%. Project has 5479 uncovered lines.
✅ Project coverage is 81.7%. Comparing base (base) to head (head).

Files with missing lines (1)
File Patch % Lines
src/lib/api/alerts.ts 90.48% ⚠️ 4 Missing and 5 partials
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    81.68%    81.70%    +0.02%
==========================================
  Files          426       426         —
  Lines        29909     29945       +36
  Branches     19433     19487       +54
==========================================
+ Hits         24431     24466       +35
- Misses        5478      5479        +1
- Partials      2035      2040        +5

Generated by Codecov Action

@jared-outpost
jared-outpost Bot marked this pull request as ready for review July 22, 2026 11:01
@jared-outpost

jared-outpost Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Marked ready for review — CI is green (build, lint & typecheck, unit tests, and the npm/binary smoke jobs all passed). Self-review found nothing to change.

Couldn't auto-assign a reviewer here (issue reporter is a bot), so please pull in whoever owns the alerts API path.

Comment thread src/lib/api/alerts.ts Outdated
Comment thread src/lib/api/alerts.ts Outdated
Comment thread src/lib/api/alerts.ts
Metric detectors expose the threshold window in seconds via
dataSources/snubaQuery, but MetricAlertRule and every CLI formatter
treat timeWindow as minutes (list/view append 'm'). Convert
seconds->minutes so windows no longer render ~60x too large.

Detectors are project-scoped and expose projectSlug rather than the
legacy projects slug array, so metric alert view was showing
'(all)'; map projects from projectSlug with a projects[] fallback.

Also harden pickDetectorNumber to coerce non-numeric/NaN values to 0
so a malformed window renders as 0m instead of NaNm.

Addresses Cursor Bugbot and Sentry review findings on #1275.
Comment thread src/lib/api/alerts.ts Outdated
The detectors API can nest the SnubaQuery threshold fields
(aggregate/dataset/query/timeWindow) at dataSources[0].queryObj.snubaQuery,
not just under a flat queryObj or snubaQuery. The old queryObj ?? snubaQuery
?? source chain picked the queryObj wrapper and read empty fields off it,
flattening aggregate/query/dataset to empty and timeWindow to 0 on list/view.

Add resolveThresholdSource to walk queryObj.snubaQuery -> queryObj ->
snubaQuery -> dataSource and return the first container that actually exposes
an aggregate/query. Adds a queryObj.snubaQuery mapping test.

Addresses Cursor Bugbot finding on #1275.
Comment thread src/lib/api/alerts.ts
The /detectors/{id}/ endpoint returns any detector kind (uptime, cron,
error), unlike the type:metric_issue-filtered list. getMetricAlertRule
mapped the payload blind, so a numeric id resolving to a non-metric
detector rendered as a metric alert with empty threshold fields instead
of 404ing like the old /alert-rules/{id}/ path.

Type the detector 'type' field and throw a 404 ApiError when it is
present and not metric_issue; an absent type still maps (defensive for
deployments that omit it). Adds a rejection test.

Addresses Cursor Bugbot finding on #1275.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 472b6b8. Configure here.

Comment thread src/lib/api/alerts.ts
Comment thread src/lib/api/alerts.ts Outdated
pickDetectorOwner flattened object owners to name/id, dropping the actor
type. Legacy /alert-rules/ returned owner as an actor string (user:123 /
team:456) and the CLI formatters/JSON still treat it that way, so list/
view emitted a different, less-usable owner value.

Reconstruct type:id from the detector owner object when both are present;
fall back to bare id/name otherwise. Adds a mapping test.

Addresses Cursor Bugbot finding on #1275.
Comment thread src/lib/api/alerts.ts Outdated
Comment thread src/lib/api/alerts.ts Outdated
Comment thread test/commands/alert/metrics/list.test.ts Outdated
Address review feedback on #1275:

- listMetricAlertsPaginated / getMetricAlertRule now call the generated
  @sentry/api listOrganizationDetectors / getOrganizationDetector helpers
  via getOrgSdkConfig, instead of hand-rolled /detectors/ URL strings +
  apiRequestToRegion. Matches the repositories.ts / monitors.ts pattern
  and the AGENTS.md guidance to prefer @sentry/api. Errors now flow through
  the shared unwrap* helpers (endpoint is no longer set, consistent with
  every other SDK-backed module); tests updated accordingly.
- Inline the one-line seconds->minutes timeWindow conversion instead of a
  standalone helper.
- Name the detector fixture dateCreated constant (DETECTOR_DATE_CREATED)
  rather than an inline magic timestamp.
@jared-outpost
jared-outpost Bot requested a review from BYK July 22, 2026 12:38
@BYK
BYK merged commit cce4d63 into main Jul 24, 2026
34 checks passed
@BYK
BYK deleted the issue-1274-alerts-legacy-api-migration branch July 24, 2026 21:17
BYK pushed a commit that referenced this pull request Aug 2, 2026
Migrates metric-alert **create/edit/delete** off the legacy org-scoped
`/organizations/{org}/alert-rules/` endpoint (retiring **2026-08-17**)
onto the detectors write API, completing the read-path migration from
#1275. The CLI commands keep building the flat legacy body; a new
`buildDetectorBody` translator in `alerts.ts` converts it into the
nested detector document (`dataSources` / `conditionGroup` / `config`),
so the command layer is largely unchanged.

- `createMetricAlertRule` → `createOrganizationProjectDetector`
(project-scoped `POST /projects/{project}/detectors/`). Detector create
is project-scoped, so `--project` is now **required** for `create`; the
target is taken from the flat body's `projects`.
- `putMetricAlertRule` → `updateOrganizationDetector` (`PUT
/detectors/{id}/`).
- `getMetricAlertRuleDocument` (edit baseline) → detector `GET`,
returned in the flat `MetricAlertRule` shape so the command-side merge
stays coherent.
- `deleteMetricAlertRule` → `deleteOrganizationDetector` (`DELETE
/detectors/{id}/`).

Dataset → `queryType`/`eventTypes` mapping: error-like datasets
(`errors`/`events`) → `queryType: 0` + `["error","default"]`; everything
else → `queryType: 1` + `["trace_item_span"]`. `timeWindow` is converted
minutes→seconds (inverse of the read mapping), and `status` (0/1) maps
to the detector `enabled` flag.

### Known limitations (best-effort draft)
- `--project` on **edit** is now a no-op: the detector update endpoint
is not project-scoped and has no `projects` field.
- Uses static (threshold) detection only (`config.detectionType:
"static"`); change/dynamic detection types aren't surfaced by the flat
CLI input. Per #1274/#1142 full write parity is still gated on backend
feature gaps.

## Testing
- `pnpm exec vitest run test/lib/api/alerts.test.ts test/commands/alert`
— 170 passed. Added detector-endpoint assertions for create (nested
body, project-scoped path, error-like dataset mapping, missing-project
validation), put, delete, and the flat edit baseline.
- `pnpm exec tsc --noEmit` — clean. `biome check` — clean.

Closes #1277

<!--
## Plan
Design: keep the command layer building the flat legacy body; confine
the flat→nested translation to alerts.ts (mirrors the read-path
precedent). Keeps existing command tests' flat-body contract intact.

SDK surface (@sentry/api 0.253.0): deleteOrganizationDetector,
updateOrganizationDetector, getOrganizationDetector (already used),
createOrganizationProjectDetector (project-scoped POST). No org-scoped
createOrganizationDetector, so create is project-scoped.

alerts.ts: buildDetectorBody(flat) -> { name, type: metric_issue,
dataSources:[buildDetectorDataSource], config:{detectionType:static},
conditionGroup?, enabled?, owner? }. datasetQueryType/datasetEventTypes
keyed on ERROR_LIKE_DATASETS={errors,events}. resolveDetectorProject
picks first non-empty projects[] entry, else ValidationError.
getMetricAlertRuleDocument returns getMetricAlertRule(...) flat shape;
put/create/delete go through the SDK detector fns; remove
fetchMetricAlertRuleJson. detector_id string passed via the same
double-cast pattern used by issue-alert writes.

create.ts: require --project (detectors create is project-scoped).

Tests: rewrite deleteMetricAlertRule to detectors DELETE; add
create/put/getDocument tests; add --project to the 4 create-path command
tests + expected projects in the flat body + a missing-project rejection
test.
-->

Co-authored-by: jared-outpost[bot] <jared-outpost[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant