Skip to content

Conversation

@mifu67
Copy link
Contributor

@mifu67 mifu67 commented Oct 20, 2025

When a detector is deleted, we need to delete the rule data in Seer. Similar implementation to the legacy alert rule code, but uses detector ID instead of alert rule ID.

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Oct 20, 2025
@codecov
Copy link

codecov bot commented Oct 21, 2025

❌ 8 Tests Failed:

Tests completed Failed Passed Skipped
29497 8 29489 243
View the top 3 failed test(s) by shortest run time
tests.sentry.incidents.test_logic.DeleteAlertRuleTest::test_delete_anomaly_detection_rule
Stack Traces | 3.11s run time
#x1B[1m#x1B[.../sentry/incidents/test_logic.py#x1B[0m:2062: in test_delete_anomaly_detection_rule
    assert mock_seer_request.call_count == 1
#x1B[1m#x1B[31mE   AssertionError: assert 0 == 1#x1B[0m
#x1B[1m#x1B[31mE    +  where 0 = <MagicMock name='urlopen' id='139862062123392'>.call_count#x1B[0m
tests.sentry.incidents.test_logic.DeleteAlertRuleTest::test_delete_anomaly_detection_rule_failure
Stack Traces | 3.12s run time
#x1B[1m#x1B[.../sentry/incidents/test_logic.py#x1B[0m:2187: in test_delete_anomaly_detection_rule_failure
    mock_seer_logger.error.assert_called_with(
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1.../x64/lib/python3.13/unittest/mock.py#x1B[0m:968: in assert_called_with
    raise AssertionError(error_message)
#x1B[1m#x1B[31mE   AssertionError: expected call not found.#x1B[0m
#x1B[1m#x1B[31mE   Expected: error('Request to delete alert rule from Seer was unsuccessful', extra={'rule_id': 36})#x1B[0m
#x1B[1m#x1B[31mE     Actual: not called.#x1B[0m
tests.sentry.incidents.test_logic.DeleteAlertRuleTest::test_delete_anomaly_detection_rule_error
Stack Traces | 3.15s run time
#x1B[1m#x1B[.../sentry/incidents/test_logic.py#x1B[0m:2120: in test_delete_anomaly_detection_rule_error
    mock_seer_logger.error.assert_called_with(
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1.../x64/lib/python3.13/unittest/mock.py#x1B[0m:968: in assert_called_with
    raise AssertionError(error_message)
#x1B[1m#x1B[31mE   AssertionError: expected call not found.#x1B[0m
#x1B[1m#x1B[31mE   Expected: error('Error when hitting Seer delete rule data endpoint', extra={'response_data': 'Bad request', 'rule_id': 24})#x1B[0m
#x1B[1m#x1B[31mE     Actual: not called.#x1B[0m
tests.sentry.incidents.test_logic.DeleteAlertRuleTest::test_delete_anomaly_detection_rule_timeout
Stack Traces | 3.26s run time
#x1B[1m#x1B[.../sentry/incidents/test_logic.py#x1B[0m:2087: in test_delete_anomaly_detection_rule_timeout
    mock_seer_logger.warning.assert_called_with(
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1.../x64/lib/python3.13/unittest/mock.py#x1B[0m:968: in assert_called_with
    raise AssertionError(error_message)
#x1B[1m#x1B[31mE   AssertionError: expected call not found.#x1B[0m
#x1B[1m#x1B[31mE   Expected: warning('Timeout error when hitting Seer delete rule data endpoint', extra={'rule_id': 32})#x1B[0m
#x1B[1m#x1B[31mE     Actual: not called.#x1B[0m
tests.sentry.incidents.test_logic.DeleteAlertRuleTest::test_with_incident_anomaly_detection_rule
Stack Traces | 3.38s run time
#x1B[1m#x1B[.../sentry/incidents/test_logic.py#x1B[0m:1996: in test_with_incident_anomaly_detection_rule
    assert mock_seer_request.call_count == 1
#x1B[1m#x1B[31mE   AssertionError: assert 0 == 1#x1B[0m
#x1B[1m#x1B[31mE    +  where 0 = <MagicMock name='urlopen' id='139839358436416'>.call_count#x1B[0m
tests.sentry.incidents.test_logic.DeleteAlertRuleTest::test_with_incident_anomaly_detection_rule_error
Stack Traces | 3.57s run time
#x1B[1m#x1B[.../sentry/incidents/test_logic.py#x1B[0m:2021: in test_with_incident_anomaly_detection_rule_error
    mock_seer_logger.error.assert_called_with(
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1.../x64/lib/python3.13/unittest/mock.py#x1B[0m:968: in assert_called_with
    raise AssertionError(error_message)
#x1B[1m#x1B[31mE   AssertionError: expected call not found.#x1B[0m
#x1B[1m#x1B[31mE   Expected: error('Error when hitting Seer delete rule data endpoint', extra={'response_data': 'Bad request', 'rule_id': 104})#x1B[0m
#x1B[1m#x1B[31mE     Actual: not called.#x1B[0m
tests.sentry.deletions.test_alert_rule.DeleteAlertRuleTest::test_dynamic_alert_rule
Stack Traces | 3.72s run time
#x1B[1m#x1B[.../sentry/deletions/test_alert_rule.py#x1B[0m:124: in test_dynamic_alert_rule
    assert mock_delete_request.call_count == 1
#x1B[1m#x1B[31mE   AssertionError: assert 0 == 1#x1B[0m
#x1B[1m#x1B[31mE    +  where 0 = <MagicMock name='urlopen' id='140290219011312'>.call_count#x1B[0m
tests.sentry.incidents.test_logic.DeleteAlertRuleTest::test_delete_anomaly_detection_rule_attribute_error
Stack Traces | 3.77s run time
#x1B[1m#x1B[.../sentry/incidents/test_logic.py#x1B[0m:2153: in test_delete_anomaly_detection_rule_attribute_error
    mock_seer_logger.exception.assert_called_with(
#x1B[1m#x1B[.../hostedtoolcache/Python/3.13.1.../x64/lib/python3.13/unittest/mock.py#x1B[0m:968: in assert_called_with
    raise AssertionError(error_message)
#x1B[1m#x1B[31mE   AssertionError: expected call not found.#x1B[0m
#x1B[1m#x1B[31mE   Expected: exception('Failed to parse Seer delete rule data response', extra={'rule_id': 25})#x1B[0m
#x1B[1m#x1B[31mE     Actual: not called.#x1B[0m

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@mifu67 mifu67 requested review from kcons and saponifi3d October 31, 2025 23:04
@mifu67 mifu67 marked this pull request as ready for review October 31, 2025 23:04
@mifu67 mifu67 requested review from a team as code owners October 31, 2025 23:04
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

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.

looks like there's a bit of code we can cleanup before merging, generally the setup for the hooks etc all looks good though!

return True


def delete_rule_in_seer_legacy(alert_rule: "AlertRule") -> bool:
Copy link
Contributor

@saponifi3d saponifi3d Oct 31, 2025

Choose a reason for hiding this comment

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

could this method just invoke return delete_rule_in_seer(alert_rule.id)? (that way we can reduce code replication)

and

Suggested change
def delete_rule_in_seer_legacy(alert_rule: "AlertRule") -> bool:
def delete_rule_in_seer_legacy(alert_rule: AlertRule) -> bool:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The two methods are slightly different in what they pass to Seer. I think we've done this code duplication for all anomaly detection methods so we can easily delete the legacy code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Since we need to maintain this code in the future, it might be a cool time to decompose the method and reuse what we can. That would also mean we don't need to maintain two code paths while we're waiting to remove the legacy code. Instead we cold take the opportunity to make this a bit easier to manage, and a lot easier for us to debug any issues in the mean time.

The way i tend to decompose things with two examples is to just look at those differences and try to determine where it would make the most sense to expose new methods. For example, maybe we could wrap the send to seer and handle errors as a method, then delete_rule_in_seer_legacy could compose those shared methods and make any tweaks it might need.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, I think we can delete the legacy code outright if every alert rule has a detector. The call only needs to happen once.
I see your point about decomp, will look into it.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

)
return False

if response.status >= 400:
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Inconsistent HTTP Error Handling Between Functions

There's an inconsistency in HTTP error handling between delete_rule_in_seer and delete_rule_in_seer_legacy. The new function treats response.status >= 400 as an error, but the legacy one uses > 400. This means the legacy function won't flag HTTP 400 responses from Seer as errors, leading to different behavior for new detectors and legacy alert rules.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fine, I don't think Seer returns any 400s

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.

i think biggest feedback would be to make delete_rule_in_seer a little more debug friendly; we can either decompose the method and share it between alert rule / detector deletion or we can update the logs so we can easily differentiate (i think that'd just be changing rule to detector).

return True


def delete_rule_in_seer_legacy(alert_rule: "AlertRule") -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we need to maintain this code in the future, it might be a cool time to decompose the method and reuse what we can. That would also mean we don't need to maintain two code paths while we're waiting to remove the legacy code. Instead we cold take the opportunity to make this a bit easier to manage, and a lot easier for us to debug any issues in the mean time.

The way i tend to decompose things with two examples is to just look at those differences and try to determine where it would make the most sense to expose new methods. For example, maybe we could wrap the send to seer and handle errors as a method, then delete_rule_in_seer_legacy could compose those shared methods and make any tweaks it might need.

results = json.loads(decoded_data)
except JSONDecodeError:
logger.exception(
"Failed to parse Seer delete rule data response",
Copy link
Contributor

Choose a reason for hiding this comment

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

if we want to keep these methods separate, let's update the exception text so we can determine if it was triggered by delete_rule_in_seer_legacy or this method.

success = delete_rule_in_seer(
alert_rule=alert_rule,
organization=alert_rule.organization,
source_id=source_id,
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Unhandled exception leads to undefined variable usage crash

If QuerySubscription.DoesNotExist exception is raised, the variable source_id is never assigned, but the code continues execution and tries to use source_id in the delete_rule_in_seer() call on line 933, which will raise a NameError. The code should either return early after logging the exception or assign a default value to source_id before the try block.

Fix in Cursor Fix in Web

@mifu67
Copy link
Contributor Author

mifu67 commented Nov 6, 2025

@saponifi3d removed the legacy code. Let me know if you think we should still break down the error handling into a separate method.

@mifu67 mifu67 requested a review from saponifi3d November 6, 2025 02:05
extra={
"rule_id": alert_rule.id,
},
try:
Copy link
Member

Choose a reason for hiding this comment

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

When are we still hitting this code path? is it only for people using the legacy API? If so, don't we need to call this on delete too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I put the delete code in the detector lifecycle hook, so any time a detector is deleted the code will be called. If all alert rules are dual written, then we don't need the call in multiple places.
I didn't hook deletion into updates, however, so this is for users of the legacy API. Good callout that I should create an update hook as well.

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.

5 participants