Skip to content

feat(workflow_engine): Add in hook for producing occurrences from the stateful detector#15

Open
ShashankFC wants to merge 1 commit into
workflow-engine-stateful-detector-beforefrom
workflow-engine-stateful-detector-after
Open

feat(workflow_engine): Add in hook for producing occurrences from the stateful detector#15
ShashankFC wants to merge 1 commit into
workflow-engine-stateful-detector-beforefrom
workflow-engine-stateful-detector-after

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Jan 5, 2026

Copy link
Copy Markdown
Collaborator

Test 10

Summary by CodeRabbit

  • Refactor
    • Restructured metric alert detection system to use group-based result tracking instead of list-based aggregation.
    • Enhanced detector evaluation framework with improved state management and occurrence data handling.

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/sentry-coderabbit#10

… stateful detector (#80168)

This adds a hook that can be implemented to produce an occurrence
specific to the detector that is subclassing the StatefulDetector.

Also change the signature of evaluate to return a dict keyed by groupkey
instead of a list. This helps avoid the chance of duplicate results for
the same group key.

<!-- Describe your PR here. -->
@ShashankFC

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 6, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jan 6, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR refactors detector evaluation to return per-group results as dictionaries keyed by DetectorGroupKey instead of lists. It introduces a stateful detector handler pattern, extends DetectorEvaluationResult with occurrence and event data, and adds a new MetricAlertFire group type with an associated detector handler.

Changes

Cohort / File(s) Summary
Detector Handler Refactoring
src/sentry/workflow_engine/processors/detector.py
process_detectors return type changed from list[tuple[Detector, list[DetectorEvaluationResult]]] to list[tuple[Detector, dict[DetectorGroupKey, DetectorEvaluationResult]]]. DetectorHandler.evaluate and StatefulDetectorHandler.evaluate signatures updated to return dict[DetectorGroupKey, DetectorEvaluationResult]. New abstract method build_occurrence_and_event_data added to StatefulDetectorHandler to produce occurrence and event data tuples. Evaluation logic adjusted to accumulate and return dict-based results; OK transitions create StatusChangeMessage, others invoke the new hook.
Detector Model Enhancement
src/sentry/workflow_engine/models/detector.py
Added group_type property that resolves group type class via grouptype.registry.get_by_slug(self.type), returning builtins.type[GroupType] | None. Updated detector handler to use this property instead of direct lookup. Added imports: builtins, GroupType.
Metric Alert Group Type
src/sentry/incidents/grouptype.py
MetricAlertDetectorHandler now inherits from StatefulDetectorHandler[QuerySubscriptionUpdate] instead of DetectorHandler[QuerySubscriptionUpdate]; removed concrete evaluate implementation, leaving pass body. Removed imports of DataPacket, DetectorEvaluationResult, DetectorHandler; added StatefulDetectorHandler import. New frozen dataclass MetricAlertFire introduced with fields: type_id, slug, description, category, creation_quota, default_priority, enable_auto_resolve, enable_escalation_detection, detector_handler (associated with MetricAlertDetectorHandler).
Test Infrastructure Updates
tests/sentry/workflow_engine/processors/test_detector.py
DetectorEvaluationResult extended with occurrence: IssueOccurrence | None and event_data: dict[str, Any] | None fields. process_detectors return type updated to dict[DetectorGroupKey, DetectorEvaluationResult]. Mock handlers (MockDetectorHandler, MockDetectorStateHandler) signatures updated to return dict-based results. New helper build_mock_occurrence_and_event added to generate deterministic occurrence/event pairs. Added imports: datetime, timezone, Any, call, IssueOccurrence, DetectorGroupKey, PayloadType.

Sequence Diagram

sequenceDiagram
    participant DataSource as Data Source
    participant Detector as Detector Processor
    participant Handler as DetectorHandler
    participant StatefulHandler as StatefulDetectorHandler
    participant Registry as GroupType Registry
    
    DataSource->>Detector: DataPacket
    Detector->>Handler: evaluate(data_packet)
    
    alt Stateful Handler
        Handler->>StatefulHandler: evaluate(data_packet)
        StatefulHandler->>StatefulHandler: Check status transition
        
        alt OK Status
            StatefulHandler-->>Handler: dict[DetectorGroupKey,<br/>StatusChangeMessage]
        else Non-OK Status
            StatefulHandler->>StatefulHandler: build_occurrence_and_event_data<br/>(group_key, value, new_status)
            StatefulHandler-->>Handler: dict[DetectorGroupKey,<br/>DetectorEvaluationResult<br/>(occurrence, event_data)]
        end
    else Regular Handler
        Handler-->>Detector: dict[DetectorGroupKey,<br/>DetectorEvaluationResult]
    end
    
    Detector->>Registry: group_type = get_by_slug(type)
    Registry-->>Detector: GroupType class
    Detector-->>DataSource: list[tuple[Detector,<br/>dict[DetectorGroupKey,<br/>DetectorEvaluationResult]]]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Detectors now stateful, results neatly keyed,
Per-group occurrences bloom where once we freed,
MetricAlertFire rises with grace,
Dict-based returns set a swifter pace!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main architectural change: introducing a hook for producing occurrences from stateful detectors, which aligns with the new build_occurrence_and_event_data abstract method added to StatefulDetectorHandler.
✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI Agents
In @src/sentry/incidents/grouptype.py:
- Around line 10-12: MetricAlertDetectorHandler currently subclasses
StatefulDetectorHandler but lacks required abstract members causing TypeError on
instantiation; implement the missing property/method stubs: add a counter_names
property, and implement get_dedupe_value(self, packet: DataPacket) -> Any,
get_group_key_values(self, packet: DataPacket) -> DetectorGroupKey, and
build_occurrence_and_event_data(self, packet: DataPacket) ->
Tuple[IssueOccurrence, Dict[str, Any]] (or the appropriate return types) inside
the MetricAlertDetectorHandler class, using signatures matching
StatefulDetectorHandler, and import the required symbols (Any, IssueOccurrence,
DataPacket, DetectorGroupKey) so the class satisfies the abstract interface and
can be instantiated by MetricAlertFire.detector_handler(self).

In @tests/sentry/workflow_engine/processors/test_detector.py:
- Around line 190-192: The test passes an incorrect value for group_2 to
build_mock_occurrence_and_event: change the third argument in the call using
detector.detector_handler for "group_2" from 6 to 10 so it matches the test
packet's "group_vals" (i.e., call
build_mock_occurrence_and_event(detector.detector_handler, "group_2", 10,
PriorityLevel.HIGH)); this keeps the test consistent and prevents future
surprises if the helper starts using the value.
🧹 Nitpick comments (2)
src/sentry/workflow_engine/processors/detector.py (1)

229-233: Docstring is outdated - still references "list" instead of "dict".

The docstring says "returns a list of DetectorEvaluationResult" but the method now returns dict[DetectorGroupKey, DetectorEvaluationResult].

🔎 Proposed fix
     def evaluate(
         self, data_packet: DataPacket[T]
     ) -> dict[DetectorGroupKey, DetectorEvaluationResult]:
         """
-        Evaluates a given data packet and returns a list of `DetectorEvaluationResult`.
+        Evaluates a given data packet and returns a dict of `DetectorEvaluationResult` keyed by group key.
         There will be one result for each group key result in the packet, unless the
         evaluation is skipped due to various rules.
         """
tests/sentry/workflow_engine/processors/test_detector.py (1)

244-275: Consider adding a note about the unused value parameter.

The value parameter is part of the interface signature but currently unused in the mock implementation. A brief comment would clarify this is intentional for interface compliance.

🔎 Proposed change
 def build_mock_occurrence_and_event(
     handler: StatefulDetectorHandler,
     group_key: DetectorGroupKey,
-    value: int,
+    value: int,  # Unused; kept for interface compatibility with build_occurrence_and_event_data
     new_status: PriorityLevel,
 ) -> tuple[IssueOccurrence, dict[str, Any]]:
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7249010 and de60b7f.

📒 Files selected for processing (4)
  • src/sentry/incidents/grouptype.py
  • src/sentry/workflow_engine/models/detector.py
  • src/sentry/workflow_engine/processors/detector.py
  • tests/sentry/workflow_engine/processors/test_detector.py
🧰 Additional context used
🧬 Code graph analysis (4)
src/sentry/workflow_engine/models/detector.py (1)
src/sentry/issues/grouptype.py (2)
  • GroupType (141-223)
  • get_by_slug (104-107)
src/sentry/incidents/grouptype.py (1)
src/sentry/workflow_engine/processors/detector.py (1)
  • StatefulDetectorHandler (135-407)
src/sentry/workflow_engine/processors/detector.py (2)
src/sentry/types/group.py (1)
  • PriorityLevel (63-80)
src/sentry/workflow_engine/models/detector.py (1)
  • Detector (23-86)
tests/sentry/workflow_engine/processors/test_detector.py (3)
src/sentry/issues/producer.py (1)
  • PayloadType (25-34)
src/sentry/workflow_engine/processors/detector.py (12)
  • StatefulDetectorHandler (135-407)
  • counter_names (144-148)
  • get_dedupe_value (151-156)
  • get_group_key_values (159-164)
  • build_occurrence_and_event_data (167-170)
  • evaluate (126-129)
  • evaluate (226-244)
  • DetectorEvaluationResult (35-44)
  • process_detectors (47-70)
  • build_fingerprint (172-176)
  • commit_state_updates (131-132)
  • commit_state_updates (356-358)
src/sentry/workflow_engine/models/detector.py (2)
  • Detector (23-86)
  • detector_handler (63-86)
🪛 Ruff (0.14.10)
tests/sentry/workflow_engine/processors/test_detector.py

28-28: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


60-60: Unused method argument: data_packet

(ARG002)


247-247: Unused function argument: value

(ARG001)

🔇 Additional comments (9)
src/sentry/workflow_engine/models/detector.py (2)

58-60: LGTM! Clean property extraction.

The new group_type property nicely encapsulates the registry lookup, making the code more DRY and enabling reuse (e.g., in tests via handler.detector.group_type). Using builtins.type[GroupType] correctly avoids shadowing with the model's type field.


62-74: LGTM! Refactored to use the new property.

The detector_handler property now cleanly uses self.group_type, and the error logging still correctly captures the relevant context when the group type is missing.

src/sentry/workflow_engine/processors/detector.py (3)

166-170: LGTM! Well-defined abstract hook for occurrence generation.

The new build_occurrence_and_event_data abstract method provides a clean extension point for subclasses to produce detector-specific occurrences. The signature correctly accepts the group key, value, and new status to build the occurrence.


47-70: LGTM! Process flow correctly adapted for dict-based results.

The process_detectors function properly iterates over detector_results.values() and accumulates results per detector. The commit of state updates after processing each detector ensures consistency.


298-301: The type conversion is safe. The code explicitly checks if new_status == DetectorPriorityLevel.OK: on line 297, ensuring the else block (where PriorityLevel(new_status) is called) only executes when new_status is one of LOW, MEDIUM, or HIGH—all of which have identical integer values in both enums.

tests/sentry/workflow_engine/processors/test_detector.py (3)

27-39: LGTM! Clean mock implementation of StatefulDetectorHandler.

The MockDetectorStateHandler correctly implements all abstract methods required by StatefulDetectorHandler, providing a solid test fixture.

Regarding the static analysis hint about counter_names: while ClassVar annotation would be more precise for a class-level attribute, this is acceptable for test code where the distinction is less critical.


82-84: LGTM! Proper cleanup of the StatusChangeMessage comparator patch.

The tearDown correctly exits the mock patch context, ensuring clean test isolation.


145-172: LGTM! Comprehensive test for stateful detector results.

The test properly verifies the end-to-end flow including the occurrence production to Kafka, validating both the returned results and the side effects.

src/sentry/incidents/grouptype.py (1)

17-27: Type_id = 8001 is unique across all GroupType subclasses. Verified in production code across both src/sentry/issues/grouptype.py and src/sentry/incidents/grouptype.py. All other GroupType implementations use type_ids in different ranges (1, 1010–1019, 2001–2011, 4001, 6001, 7001).

Comment on lines 10 to +12
# TODO: This will be a stateful detector when we build that abstraction
class MetricAlertDetectorHandler(DetectorHandler[QuerySubscriptionUpdate]):
def evaluate(
self, data_packet: DataPacket[QuerySubscriptionUpdate]
) -> list[DetectorEvaluationResult]:
# TODO: Implement
return []
class MetricAlertDetectorHandler(StatefulDetectorHandler[QuerySubscriptionUpdate]):
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing required abstract method implementations will cause runtime errors.

MetricAlertDetectorHandler extends StatefulDetectorHandler but does not implement the required abstract methods:

  • counter_names (property)
  • get_dedupe_value()
  • get_group_key_values()
  • build_occurrence_and_event_data()

This will raise a TypeError when instantiating MetricAlertDetectorHandler via MetricAlertFire.detector_handler(self).

🔎 Suggested stub implementations
 class MetricAlertDetectorHandler(StatefulDetectorHandler[QuerySubscriptionUpdate]):
-    pass
+    @property
+    def counter_names(self) -> list[str]:
+        return []
+
+    def get_dedupe_value(self, data_packet: DataPacket[QuerySubscriptionUpdate]) -> int:
+        raise NotImplementedError("TODO: Implement dedupe extraction")
+
+    def get_group_key_values(self, data_packet: DataPacket[QuerySubscriptionUpdate]) -> dict[str, int]:
+        raise NotImplementedError("TODO: Implement group key extraction")
+
+    def build_occurrence_and_event_data(
+        self, group_key: DetectorGroupKey, value: int, new_status: PriorityLevel
+    ) -> tuple[IssueOccurrence, dict[str, Any]]:
+        raise NotImplementedError("TODO: Implement occurrence building")

You'll also need to add the missing imports:

from typing import Any
from sentry.issues.issue_occurrence import IssueOccurrence
from sentry.workflow_engine.models import DataPacket
from sentry.workflow_engine.types import DetectorGroupKey
🤖 Prompt for AI Agents
In @src/sentry/incidents/grouptype.py around lines 10 - 12,
MetricAlertDetectorHandler currently subclasses StatefulDetectorHandler but
lacks required abstract members causing TypeError on instantiation; implement
the missing property/method stubs: add a counter_names property, and implement
get_dedupe_value(self, packet: DataPacket) -> Any, get_group_key_values(self,
packet: DataPacket) -> DetectorGroupKey, and
build_occurrence_and_event_data(self, packet: DataPacket) ->
Tuple[IssueOccurrence, Dict[str, Any]] (or the appropriate return types) inside
the MetricAlertDetectorHandler class, using signatures matching
StatefulDetectorHandler, and import the required symbols (Any, IssueOccurrence,
DataPacket, DetectorGroupKey) so the class satisfies the abstract interface and
can be instantiated by MetricAlertFire.detector_handler(self).

Comment on lines +190 to 192
occurrence_2, event_data_2 = build_mock_occurrence_and_event(
detector.detector_handler, "group_2", 6, PriorityLevel.HIGH
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incorrect value passed to build_mock_occurrence_and_event for group_2.

The test packet defines "group_vals": {"group_1": 6, "group_2": 10}, but build_mock_occurrence_and_event is called with value=6 for group_2 instead of value=10. While this doesn't affect the test outcome (since value is unused in the helper), it's misleading and could cause issues if the helper is updated to use the value.

🔎 Proposed fix
         occurrence_2, event_data_2 = build_mock_occurrence_and_event(
-            detector.detector_handler, "group_2", 6, PriorityLevel.HIGH
+            detector.detector_handler, "group_2", 10, PriorityLevel.HIGH
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
occurrence_2, event_data_2 = build_mock_occurrence_and_event(
detector.detector_handler, "group_2", 6, PriorityLevel.HIGH
)
occurrence_2, event_data_2 = build_mock_occurrence_and_event(
detector.detector_handler, "group_2", 10, PriorityLevel.HIGH
)
🤖 Prompt for AI Agents
In @tests/sentry/workflow_engine/processors/test_detector.py around lines 190 -
192, The test passes an incorrect value for group_2 to
build_mock_occurrence_and_event: change the third argument in the call using
detector.detector_handler for "group_2" from 6 to 10 so it matches the test
packet's "group_vals" (i.e., call
build_mock_occurrence_and_event(detector.detector_handler, "group_2", 10,
PriorityLevel.HIGH)); this keeps the test consistent and prevents future
surprises if the helper starts using the value.

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.

2 participants