Skip to content

Conversation

@ceorourke
Copy link
Member

When a static or percent based detector is changed to become a dynamic detector we need to send Seer historical data for that detector so it can detect anomalies. Also when a dynamic detector's snuba query query or aggregate changes we need to update the data Seer has so it's detecting anomalies on the correct data. This PR also handles not updating the existing data if the call to Seer fails for any reason.

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Nov 7, 2025
Comment on lines -154 to -156
"id": self.data_condition_group.id,
"organizationId": self.organization.id,
Copy link
Member Author

Choose a reason for hiding this comment

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

These aren't sent by the front end so I wanted this to be the same. Especially for the ids, it doesn't make sense that we'd be sending these on creation.

@@ -553,6 +551,379 @@ def test_transaction_dataset_deprecation_multiple_data_sources(self) -> None:
):
validator.save()


class TestMetricAlertsUpdateDetectorValidator(TestMetricAlertsDetectorValidator):
def test_update_with_valid_data(self) -> None:
Copy link
Member Author

Choose a reason for hiding this comment

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

We didn't have a simple update test case so I added one

raise DetectorException(
f"Could not create detector, data condition {dcg_id} not found or too many found."
)
# use setattr to avoid saving the models until the Seer call has successfully finished,
Copy link
Member Author

Choose a reason for hiding this comment

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

@codecov
Copy link

codecov bot commented Nov 7, 2025

Codecov Report

❌ Patch coverage is 86.53846% with 7 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...er/anomaly_detection/store_data_workflow_engine.py 81.57% 7 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #102934      +/-   ##
===========================================
+ Coverage   79.08%    80.70%   +1.61%     
===========================================
  Files        9224      9226       +2     
  Lines      393906    394080     +174     
  Branches    25109     25109              
===========================================
+ Hits       311527    318032    +6505     
+ Misses      81931     75600    -6331     
  Partials      448       448              

@ceorourke ceorourke marked this pull request as ready for review November 7, 2025 21:18
@ceorourke ceorourke requested review from a team as code owners November 7, 2025 21:18
resolution=timedelta(seconds=data_source.get("resolution", snuba_query.resolution)),
environment=data_source.get("environment", snuba_query.environment),
event_types=data_source.get("event_types", [event_type for event_type in event_types]),
)
Copy link

Choose a reason for hiding this comment

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

Bug: update_detector_data receives a single data source dict instead of a {"data_sources": [...]} structure, preventing snuba_query updates.
Severity: CRITICAL | Confidence: 0.95

🔍 Detailed Analysis

When update_detector_data is invoked at src/sentry/incidents/metric_issue_detector.py:249, it receives validated_data_source, which is a single dictionary. However, the update_detector_data function expects a dictionary containing a "data_sources" key with a list of data sources. This mismatch causes the internal logic to skip updating the snuba_query object's fields. Consequently, when a dynamic detector's snuba query is updated, the old query, aggregate, and event types are sent to Seer instead of the new values, leading to anomaly detection operating on incorrect metrics.

💡 Suggested Fix

Modify the call to update_detector_data at src/sentry/incidents/metric_issue_detector.py:249 to pass {"data_sources": [validated_data_source]} instead of validated_data_source directly, aligning with the expected input structure.

🤖 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/metric_issue_detector.py#L249

Potential issue: When `update_detector_data` is invoked at
`src/sentry/incidents/metric_issue_detector.py:249`, it receives
`validated_data_source`, which is a single dictionary. However, the
`update_detector_data` function expects a dictionary containing a `"data_sources"` key
with a list of data sources. This mismatch causes the internal logic to skip updating
the `snuba_query` object's fields. Consequently, when a dynamic detector's snuba query
is updated, the old query, aggregate, and event types are sent to Seer instead of the
new values, leading to anomaly detection operating on incorrect metrics.

Did we get this right? 👍 / 👎 to inform future reviews.

@mifu67 mifu67 self-requested a review November 11, 2025 19:00
Copy link
Contributor

@mifu67 mifu67 left a comment

Choose a reason for hiding this comment

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

The update logic itself looks good; just a question about when we should be updating.


# Handle a dynamic detector's snuba query changing
if instance.config.get("detection_type") == AlertRuleDetectionType.DYNAMIC:
if snuba_query.query != data_source.get(
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to check other snuba query fields as well (like timeWindow)? Should we just resend the data every time we update a dynamic detector?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question, I will look into this for a follow up. I think every time we change it isn't necessary (e.g. it could just be changing the name of the detector) but we might be missing some cases we should be updating.

event_types,
)
except (TimeoutError, MaxRetryError, ParseError, ValidationError):
raise ValidationError("Couldn't send data to Seer, unable to update detector")
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't it be better to track timeout, retry failure and parse errors separately as opposed to converting them to ValidationError?

Copy link
Member Author

@ceorourke ceorourke Nov 12, 2025

Choose a reason for hiding this comment

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

I converted them to a ValidationError because this is in the update detector method and this is how we can surface the error to the user - I can break it out into separate ones to be more informative though:

except TimeoutError:
    raise ValidationError("Timed out sending data to Seer, unable to update detector

@ceorourke ceorourke force-pushed the ceorourke/send-historical-data-to-seer-on-update branch from 89441c2 to 7a87a33 Compare November 12, 2025 23:21
if instance.config.get("detection_type") == AlertRuleDetectionType.DYNAMIC:
if snuba_query.query != data_source.get(
"query"
) or snuba_query.aggregate != data_source.get("aggregate"):
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Prevent Unnecessary Seer API Calls

The condition checking if a dynamic detector's query or aggregate changed compares against data_source.get("query") and data_source.get("aggregate") which return None when these fields aren't being updated. This causes the comparison snuba_query.query != None to be True even when the query hasn't changed, triggering unnecessary Seer API calls. The defaults should be the existing values: data_source.get("query", snuba_query.query) and data_source.get("aggregate", snuba_query.aggregate).

Fix in Cursor Fix in Web

@ceorourke ceorourke force-pushed the ceorourke/send-historical-data-to-seer-on-update branch from 0236524 to 2d5dec1 Compare November 13, 2025 17:51
@ceorourke ceorourke merged commit 69c0f91 into master Nov 13, 2025
65 checks passed
@ceorourke ceorourke deleted the ceorourke/send-historical-data-to-seer-on-update branch November 13, 2025 18:17
@sentry
Copy link

sentry bot commented Nov 13, 2025

Issues attributed to commits in this pull request

This pull request was merged and Sentry observed the following issues:

ceorourke added a commit that referenced this pull request Nov 14, 2025
)

Follow up to
#102934 (comment)
to update Seer when anything on the snuba query changes - we were
missing some instances where the data changed in a way Seer would want
to know about but we weren't sending the updates.
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.

4 participants