Skip to content

feat(ACI): Make OrganizationAlertRuleDetails DELETE method backwards compatible#109845

Open
ceorourke wants to merge 10 commits intomasterfrom
ceorourke/ISWF-2145
Open

feat(ACI): Make OrganizationAlertRuleDetails DELETE method backwards compatible#109845
ceorourke wants to merge 10 commits intomasterfrom
ceorourke/ISWF-2145

Conversation

@ceorourke
Copy link
Member

Make the delete an alert rule method backwards compatible by accepting a detector ID and using the OrganizationDetectorDetailsEndpoint delete strategy. This also implements a new base class and convert_args to look up either a detector or an alert rule, and removes some old code using the feature flag from before we needed this to work in the UI. For now, this just makes the other methods fail if a detector is passed in.

@ceorourke ceorourke requested review from a team as code owners March 3, 2026 23:12
@linear
Copy link

linear bot commented Mar 3, 2026

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Mar 3, 2026
except Detector.DoesNotExist:
raise ResourceDoesNotExist

return args, kwargs
Copy link
Contributor

Choose a reason for hiding this comment

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

Feature-flagged orgs lose access to unmigrated alert rules

Medium Severity

When workflow-engine-rule-serializers is enabled, convert_args has no fallback to look up an AlertRule directly. If an alert rule exists but hasn't been migrated (no AlertRuleDetector row), the AlertRuleDetector lookup fails, then get_object_id_from_fake_id produces a negative number, and the Detector lookup also fails, resulting in a ResourceDoesNotExist (404). A valid, unmigrated alert rule becomes completely inaccessible through this endpoint.

Fix in Cursor Fix in Web

Copy link
Member

Choose a reason for hiding this comment

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

This seems perfectly ok. Unmigrated rules shouldn't exist, and where they do, we seem to intend to ignore them.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

Backend Test Failures

Failures on 43261a5 in this run:

tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py::AlertRuleDetailsGetEndpointTest::test_workflow_engine_serializerlog
tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py:238: in test_workflow_engine_serializer
    resp = self.get_success_response(self.organization.slug, self.alert_rule.id)
src/sentry/testutils/cases.py:628: in get_success_response
    assert_status_code(response, status.HTTP_200_OK)
src/sentry/testutils/asserts.py:47: in assert_status_code
    assert minimum <= response.status_code < maximum, (
E   AssertionError: (400, b'{"alert_rule":["Passing a detector through this endpoint is not yet supported"]}')
E   assert 400 < 201
E    +  where 400 = <Response status_code=400, "application/json">.status_code
tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py::AlertRuleDetailsPutEndpointTest::test_workflow_engine_serializerlog
tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py:808: in test_workflow_engine_serializer
    resp = self.get_success_response(
src/sentry/testutils/cases.py:633: in get_success_response
    assert_status_code(response, status.HTTP_200_OK)
src/sentry/testutils/asserts.py:47: in assert_status_code
    assert minimum <= response.status_code < maximum, (
E   AssertionError: (400, b'{"alert_rule":["Passing a detector through this endpoint is not yet supported"]}')
E   assert 400 < 201
E    +  where 400 = <Response status_code=400, "application/json">.status_code

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

Backend Test Failures

Failures on 4cda2ac in this run:

tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py::AlertRuleDetailsPutEndpointTest::test_workflow_engine_serializerlog
tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py:808: in test_workflow_engine_serializer
    resp = self.get_success_response(
src/sentry/testutils/cases.py:633: in get_success_response
    assert_status_code(response, status.HTTP_200_OK)
src/sentry/testutils/asserts.py:47: in assert_status_code
    assert minimum <= response.status_code < maximum, (
E   AssertionError: (400, b'{"alert_rule":["Passing a detector through this endpoint is not yet supported"]}')
E   assert 400 < 201
E    +  where 400 = <Response status_code=400, "application/json">.status_code
tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py::AlertRuleDetailsGetEndpointTest::test_workflow_engine_serializerlog
tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py:238: in test_workflow_engine_serializer
    resp = self.get_success_response(self.organization.slug, self.alert_rule.id)
src/sentry/testutils/cases.py:628: in get_success_response
    assert_status_code(response, status.HTTP_200_OK)
src/sentry/testutils/asserts.py:47: in assert_status_code
    assert minimum <= response.status_code < maximum, (
E   AssertionError: (400, b'{"alert_rule":["Passing a detector through this endpoint is not yet supported"]}')
E   assert 400 < 201
E    +  where 400 = <Response status_code=400, "application/json">.status_code

Copy link
Contributor

@saponifi3d saponifi3d left a comment

Choose a reason for hiding this comment

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

just super nits, lgtm! :shipit:



class WorkflowEngineOrganizationAlertRuleEndpoint(OrganizationAlertRuleEndpoint):
def convert_args(
Copy link
Contributor

Choose a reason for hiding this comment

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

Not for this pr / yet, but this code looks fairly similar to the convert args on WorkflowEngineOrganizationRuleEndpoint -- if we have another instance of this, it might be worth refactoring a little to pull the shared logic out.

raise PermissionDenied

validator = get_detector_validator(request, detector.project, detector.type, instance=detector)
validator.delete()
Copy link
Member

Choose a reason for hiding this comment

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

Don't we want to do this async so the DataSources can get cleaned up too?
I realize this is copied code, but.. a concern.

Copy link
Member Author

Choose a reason for hiding this comment

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

The base class schedules the deletion and marks it as pending https://github.com/getsentry/sentry/blob/master/src/sentry/workflow_engine/endpoints/validators/base/detector.py#L221-L231

I added a data source and data source detector to the test to make sure they're removed

Copy link
Member

Choose a reason for hiding this comment

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

ah! I totally missed that this was the validator, not the detector >_<
carry on.


return Response(status=status.HTTP_204_NO_CONTENT)
except AlreadyDeletedError:
return Response(
Copy link
Member

Choose a reason for hiding this comment

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

Interesting, I'd expect us to return a 204 or maybe a 404 if it is already deleted.

@ceorourke ceorourke enabled auto-merge (squash) March 4, 2026 18:48
@ceorourke ceorourke disabled auto-merge March 4, 2026 18:48
Comment on lines +125 to +131
if features.has("organizations:workflow-engine-rule-serializers", organization):
try:
ard = AlertRuleDetector.objects.get(
alert_rule_id=validated_alert_rule_id,
detector__project__organization=organization,
)
kwargs["alert_rule"] = ard.detector
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: With the workflow-engine-rule-serializers flag on, GET/PUT requests for dual-written alert rules incorrectly resolve to a Detector and fail with a 400 error.
Severity: HIGH

Suggested Fix

Update the fetch_alert_rule and update_alert_rule methods to correctly handle Detector objects that are resolved from an alert_rule_id, similar to how the DELETE method handles them. This will prevent the endpoint from incorrectly returning a 400 error for valid, dual-written alert rules.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/sentry/incidents/endpoints/bases.py#L125-L131

Potential issue: When the `organizations:workflow-engine-rule-serializers` feature flag
is enabled, the `convert_args` method resolves alert rule IDs for dual-written rules
into `Detector` objects. However, the GET and PUT endpoint methods that receive this
object are implemented to reject `Detector` instances and return a 400 error. This
causes requests to fetch or update these specific alert rules to fail for any
organization with the feature flag active, breaking a core workflow.

Copy link
Member Author

Choose a reason for hiding this comment

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

that is intentional

Comment on lines +309 to +313
if isinstance(alert_rule, Detector):
project = alert_rule.project
else:
# check to see if there's a project associated with the alert rule
project = alert_rule.projects.get()
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: A missing .select_related("project") when fetching Detector objects in convert_args causes an extra database query per request when project access is checked.
Severity: MEDIUM

Suggested Fix

Add .select_related("project") to the queries that fetch Detector instances in the convert_args method. This applies to both the lookup via AlertRuleDetector.objects.get() and the direct lookup via Detector.objects.get() to ensure the project is pre-fetched and avoid the extra query.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/sentry/incidents/endpoints/organization_alert_rule_details.py#L309-L313

Potential issue: In `WorkflowEngineOrganizationAlertRuleEndpoint.convert_args`, when a
`Detector` instance is fetched, it is done without pre-fetching the related project via
`select_related("project")`. Subsequently, the `_check_project_access` decorator
accesses `alert_rule.project`, which triggers an additional, unnecessary database query
to fetch the project for each request. This introduces a performance regression for all
GET, PUT, and DELETE requests that handle a `Detector` object.

Copy link
Contributor

@cursor cursor bot left a comment

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 2 potential issues.

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

except AlreadyDeletedError:
return Response(
"This rule has already been deleted", status=status.HTTP_400_BAD_REQUEST
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Atomic transaction can roll back successful detector deletion

Medium Severity

The Detector path in remove_alert_rule wraps both remove_detector and delete_alert_rule in a single transaction.atomic block. If delete_alert_rule raises any uncaught exception (e.g., an unexpected DB error), the entire transaction rolls back — including the detector deletion that remove_detector already completed. The code's intent is to always delete the detector and optionally clean up the linked AlertRule (as evidenced by catching AlertRuleDetector.DoesNotExist and AlertRule.DoesNotExist to still return 204). Wrapping both in one transaction contradicts that intent, since a failure deleting the AlertRule also undoes the detector deletion, potentially leaving the user unable to delete the detector on retry if the alert rule issue persists.

Fix in Cursor Fix in Web

except AlreadyDeletedError:
return Response(
"This rule has already been deleted", status=status.HTTP_400_BAD_REQUEST
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Unreachable AlreadyDeletedError handler rolls back detector deletion

Low Severity

The except AlreadyDeletedError handler on the Detector path is effectively unreachable. AlreadyDeletedError is only raised by delete_alert_rule when alert_rule.status == AlertRuleStatus.SNAPSHOT.value, but AlertRule.objects.get() uses the AlertRuleManager which excludes SNAPSHOT-status rules. Since Django model instances don't auto-refresh, the in-memory status will never be SNAPSHOT at the point delete_alert_rule checks it. This dead handler adds misleading code and, if somehow reached, would roll back the detector deletion — returning a 400 "already deleted" error even though the detector has not been deleted.

Fix in Cursor Fix in Web

Copy link
Member

@kcons kcons left a comment

Choose a reason for hiding this comment

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

ok, I think the bots may have a point in one or two of these cases, but I'll let you resolve that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants