feat(workflow_engine): Add in hook for producing occurrences from the stateful detector - #1
feat(workflow_engine): Add in hook for producing occurrences from the stateful detector#1linxia0415 wants to merge 2 commits into
Conversation
… stateful detector 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.
📝 WalkthroughWalkthroughThis pull request refactors the detector evaluation system to return dictionary-keyed results instead of lists, introduces an occurrence-building abstraction for stateful handlers, adds a detector group-type lookup property, and updates all associated tests to validate the new contracts and data flows. ChangesDetector evaluation refactoring
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/sentry/incidents/grouptype.py`:
- Around line 10-12: MetricAlertDetectorHandler currently subclasses
StatefulDetectorHandler but omits the required abstract methods causing a
TypeError at instantiation (see
Detector.detector_handler/group_type.detector_handler); either implement the
required methods—counter_names, get_dedupe_value, get_group_key_values, and
build_occurrence_and_event_data—on MetricAlertDetectorHandler with appropriate
stubs that return valid types for the stateful flow, or change the base class
back to DetectorHandler until a full stateful implementation is ready; update
MetricAlertDetectorHandler to include these method definitions (matching
signatures expected by StatefulDetectorHandler) so instantiation no longer
fails.
In `@tests/sentry/workflow_engine/processors/test_detector.py`:
- Around line 190-192: The test uses
build_mock_occurrence_and_event(detector.detector_handler, "group_2", 6,
PriorityLevel.HIGH) but group_vals defines "group_2" as 10, so update the
expected occurrence fixture to use the actual group_2 value (10) instead of 6;
locate the call to build_mock_occurrence_and_event (variables occurrence_2 and
event_data_2) and change the numeric argument to match group_vals["group_2"] so
the test asserts the propagated value correctly.
- Around line 86-92: The test function create_detector_and_conditions shadows
the builtin name "type"; rename the parameter (for example to detector_type or
type_) and update its use inside the function (the call to create_detector
passing type=...) and any callers in this test file to use the new parameter
name to satisfy Ruff A001/A002; also optionally align the mismatched test data
for group_2 (change the helper call value from 6 to 10 or vice versa) if you
intend the value to be meaningful.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ec31ae3a-f504-4e96-964f-0fe799cdded4
📒 Files selected for processing (4)
src/sentry/incidents/grouptype.pysrc/sentry/workflow_engine/models/detector.pysrc/sentry/workflow_engine/processors/detector.pytests/sentry/workflow_engine/processors/test_detector.py
| # 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 |
There was a problem hiding this comment.
Missing implementations of required abstract methods will cause runtime TypeError.
MetricAlertDetectorHandler extends StatefulDetectorHandler but doesn't implement the required abstract methods: counter_names, get_dedupe_value, get_group_key_values, and build_occurrence_and_event_data. When Detector.detector_handler attempts to instantiate this class (via group_type.detector_handler(self) in detector.py:86), Python will raise a TypeError.
Either implement the required abstract methods or keep this handler as a non-stateful DetectorHandler until the stateful implementation is complete.
Minimal stub implementation to prevent runtime error
-class MetricAlertDetectorHandler(StatefulDetectorHandler[QuerySubscriptionUpdate]):
- pass
+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
+
+class MetricAlertDetectorHandler(StatefulDetectorHandler[QuerySubscriptionUpdate]):
+ `@property`
+ def counter_names(self) -> list[str]:
+ return []
+
+ def get_dedupe_value(self, data_packet: DataPacket[QuerySubscriptionUpdate]) -> int:
+ raise NotImplementedError("MetricAlertDetectorHandler is not yet fully implemented")
+
+ def get_group_key_values(self, data_packet: DataPacket[QuerySubscriptionUpdate]) -> dict[str, int]:
+ raise NotImplementedError("MetricAlertDetectorHandler is not yet fully implemented")
+
+ def build_occurrence_and_event_data(
+ self, group_key: DetectorGroupKey, value: int, new_status: PriorityLevel
+ ) -> tuple[IssueOccurrence, dict[str, Any]]:
+ raise NotImplementedError("MetricAlertDetectorHandler is not yet fully implemented")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/sentry/incidents/grouptype.py` around lines 10 - 12,
MetricAlertDetectorHandler currently subclasses StatefulDetectorHandler but
omits the required abstract methods causing a TypeError at instantiation (see
Detector.detector_handler/group_type.detector_handler); either implement the
required methods—counter_names, get_dedupe_value, get_group_key_values, and
build_occurrence_and_event_data—on MetricAlertDetectorHandler with appropriate
stubs that return valid types for the stateful flow, or change the base class
back to DetectorHandler until a full stateful implementation is ready; update
MetricAlertDetectorHandler to include these method definitions (matching
signatures expected by StatefulDetectorHandler) so instantiation no longer
fails.
| def create_detector_and_conditions(self, type: str | None = None): | ||
| if type is None: | ||
| type = "handler_with_state" | ||
| detector = self.create_detector( | ||
| workflow_condition_group=self.create_data_condition_group(), | ||
| type=type, | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n 'def create_detector_and_conditions\(self, type:' tests/sentry/workflow_engine/processors/test_detector.py
rg -n '^\s*type\s*=' tests/sentry/workflow_engine/processors/test_detector.pyRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 293
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/sentry/workflow_engine/processors/test_detector.py"
echo "== Context around create_detector_and_conditions =="
nl -ba "$FILE" | sed -n '70,110p'
echo
echo "== Any other 'type' variable assignments nearby =="
rg -n '^\s*type\s*=' "$FILE" -n
echo
echo "== Ruff config: check whether A001/A002 are selected/treated as errors =="
ls -la | rg -n 'pyproject\.toml|ruff\.toml|setup\.cfg|tox\.ini' || true
rg -n --hidden "A002|A001" pyproject.toml setup.cfg tox.ini ruff.toml 2>/dev/null || true
rg -n --hidden "tool\.ruff|\\[tool\\.ruff\\]" pyproject.toml setup.cfg tox.ini ruff.toml 2>/dev/null || trueRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 217
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/sentry/workflow_engine/processors/test_detector.py"
echo "== List repo root for Ruff config =="
ls -la | head -n 200
echo
echo "== Show context around create_detector_and_conditions =="
cat -n "$FILE" | sed -n '75,105p'
echo
echo "== Any other 'type' variable assignments nearby =="
rg -n '^\s*type\s*=' "$FILE" || true
echo
echo "== Ruff config: locate likely files =="
ls -la | rg -n 'pyproject\.toml|ruff\.toml|setup\.cfg|tox\.ini' || true
echo
echo "== Ruff config: search for tool.ruff and rule selection (A001/A002) =="
for f in pyproject.toml ruff.toml setup.cfg tox.ini; do
if [ -f "$f" ]; then
echo "-- $f --"
rg -n "tool\.ruff|\\[tool\.ruff\\]|select|ignore|A001|A002" "$f" || true
fi
doneRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 7462
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="ruff.toml"
echo "== ruff.toml (first 120 lines) =="
sed -n '1,120p' "$FILE"
echo
echo "== ruff.toml: any A001/A002 mentions =="
rg -n "A001|A002" ruff.toml pyproject.toml setup.cfg || true
echo
echo "== pyproject.toml: tool.ruff sections (snippets around tool.ruff) =="
rg -n "\\[tool\\.ruff\\]" -n pyproject.toml || true
rg -n "enable_error_code|select|ignore|lint\\.select|lint\\.ignore|per-file-ignores|tool\\.ruff" pyproject.toml | head -n 80 || true
echo
echo "== setup.cfg: per-file-ignores entries near A001/A002 (snippets) =="
rg -n "per-file-ignores|A001|A002" setup.cfg || true
echo
echo "== Does ruff ignore these rules in tests/...? (grep for tests.sentry pattern) =="
rg -n "tests/|tests\\." pyproject.toml ruff.toml setup.cfg | head -n 120 || trueRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 5898
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/sentry/workflow_engine/processors/test_detector.py"
echo "== Locate test_state_results_multi_group =="
rg -n "test_state_results_multi_group" "$FILE"
echo
echo "== Show surrounding code (function body area) =="
# try to capture a reasonable window around the match
line=$(rg -n "test_state_results_multi_group" "$FILE" | head -n1 | cut -d: -f1)
start=$(( line>40 ? line-40 : 1 ))
end=$(( line+160 ))
sed -n "${start},${end}p" "$FILE" | cat -n
echo
echo "== All occurrences of group_2 in this file =="
rg -n "\"group_2\"" "$FILE"
echo
echo "== Any expected/fixture dicts mentioning group_2 and values =="
rg -n "group_2.*(6|10)" "$FILE" || true
echo
echo "== Create_detector_and_conditions definition (full for quick context) =="
sed -n '70,110p' "$FILE"Repository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 11671
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Inspect build_mock_occurrence_and_event: does it use `value`? =="
FILE="tests/sentry/workflow_engine/processors/test_detector.py"
rg -n "def build_mock_occurrence_and_event" "$FILE"
# print the whole helper (small)
sed -n '110,160p' "$FILE" | cat -n
echo
echo "== Search for Ruff check command used in CI/pre-commit =="
rg -n "ruff (check|lint)|pre-commit.*ruff|ruff\.toml|A001|A002" .pre-commit-config.yaml Makefile .github -S || true
echo
echo "== Search for ruff execution scripts =="
rg -n "ruff check" -S .github scripts tools bin Makefile || trueRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 2669
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/sentry/workflow_engine/processors/test_detector.py"
echo "== Confirm build_mock_occurrence_and_event implementation uses `value` =="
# Print the full helper (it’s small); stop at next top-level def/class to avoid huge output
python3 - <<'PY'
import re
path="tests/sentry/workflow_engine/processors/test_detector.py"
with open(path,'r',encoding='utf-8') as f:
s=f.read().splitlines()
start=None
for i,line in enumerate(s):
if line.startswith("def build_mock_occurrence_and_event("):
start=i
break
if start is None:
raise SystemExit("helper not found")
# find end: next top-level "def " or "class " at column 0 after start
end=len(s)
for j in range(start+1,len(s)):
if re.match(r'^(def|class) ', s[j]) and not s[j].startswith("def build_mock_occurrence_and_event("):
end=j
break
for k in range(start, min(end, start+120)):
print(f"{k+1:4d}:{s[k]}")
PY
echo
echo "== Show Ruff configuration (ruff.toml select includes A) =="
sed -n '1,80p' ruff.toml
echo
echo "== Find Ruff usage in pre-commit / CI =="
rg -n "ruff (check|lint)|pre-commit.*ruff|\\bA001\\b|\\bA002\\b" .pre-commit-config.yaml .github scripts tools bin Makefile || trueRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 2756
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Ruff invocation in pre-commit config =="
rg -n "ruff (check|lint)|pre-commit.*ruff" .pre-commit-config.yaml .github/workflows .github -S || true
echo
echo "== Ruff invocation in Makefile / scripts =="
rg -n "ruff (check|lint)" Makefile scripts tools bin -S || true
echo
echo "== Does repo enable A (builtins shadowing) as error? (select includes A) =="
rg -n "\\bselect\\b|\\\"A\\\"" ruff.toml || true
echo
echo "== Per-file ignores in ruff.toml =="
cat ruff.toml | sed -n '1,120p'Repository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 1533
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find where ruff is run in this repo =="
rg -n "ruff (check|lint)|\\bruff\\b" .github .pre-commit-config.yaml Makefile scripts tools bin -S || true
echo
echo "== Inspect detector occurrence construction code =="
FILE="src/sentry/workflow_engine/processors/detector.py"
ls -ლა src/sentry/workflow_engine/processors 2>/dev/null | head || true
rg -n "create_issue_occurrence_from_result|occurrence|event_data|group_vals|group_val" "$FILE" -S || true
echo
echo "== Show relevant sections of detector.py around create_issue_occurrence_from_result (if present) =="
line=$(rg -n "create_issue_occurrence_from_result" "$FILE" -S | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
start=$(( line>60 ? line-60 : 1 ))
end=$(( line+220 ))
sed -n "${start},${end}p" "$FILE" | cat -n
else
echo "No create_issue_occurrence_from_result symbol found in $FILE"
fiRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 14570
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/sentry/workflow_engine/processors/test_detector.py"
echo "== Locate MockDetectorStateHandler.build_occurrence_and_event_data =="
rg -n "class MockDetectorStateHandler|build_occurrence_and_event_data" "$FILE"
echo
echo "== Show MockDetectorStateHandler class (bounded window) =="
# Print a window around the class definition and its build_occurrence_and_event_data
class_line=$(rg -n "^class MockDetectorStateHandler" "$FILE" | head -n1 | cut -d: -f1)
if [ -n "${class_line:-}" ]; then
start=$(( class_line>50 ? class_line-40 : 1 ))
end=$(( class_line+260 ))
sed -n "${start},${end}p" "$FILE" | cat -n
fi
echo
echo "== Search CI/pre-commit for ruff commands =="
rg -n "ruff (check|lint)|ruff\\.toml|pre-commit.*ruff|python -m ruff|ruff\\.ini" .github .pre-commit-config.yaml Makefile scripts tools bin -S || true
echo
echo "== Search for A001/A002 mentions in config or ignore =="
rg -n "A001|A002" .github .pre-commit-config.yaml pyproject.toml ruff.toml setup.cfg -S || trueRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 13233
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="tests/sentry/workflow_engine/processors/test_detector.py"
echo "== Try running ruff on the file (A001/A002) =="
if command -v ruff >/dev/null 2>&1; then
ruff --version
ruff check --select A001,A002 "$FILE" || true
else
echo "ruff not found in PATH"
fi
echo
echo "== Find ruff usage in pre-commit / CI =="
rg -n "ruff\\s+(check|lint)|python -m ruff|\\bruff\\b" .pre-commit-config.yaml .github -S || true
echo
echo "== Also scan for ruff in Makefile/scripts/tools (limited) =="
rg -n "ruff\\s+(check|lint)|python -m ruff|\\bruff\\b" Makefile scripts tools bin -S || trueRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 1426
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Call site: build_occurrence_and_event_data in detector.py =="
FILE="src/sentry/workflow_engine/processors/detector.py"
rg -n "build_occurrence_and_event_data\\(" "$FILE" -S
line=$(rg -n "build_occurrence_and_event_data\\(" "$FILE" -S | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
start=$(( line>80 ? line-80 : 1 ))
end=$(( line+120 ))
sed -n "${start},${end}p" "$FILE" | cat -n
fi
echo
echo "== Where Ruff is invoked (CI/pre-commit/etc) =="
rg -n "\\bruff\\b" .pre-commit-config.yaml Makefile .github scripts tools bin -S || true
rg -n "ruff\\s+check|ruff\\s+lint|python\\s+-m\\s+ruff" .github Makefile scripts tools bin -S || true
rg -n "A001|A002" .pre-commit-config.yaml .github -S || trueRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 9923
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== detector.py: inspect around call at ~299 =="
FILE="src/sentry/workflow_engine/processors/detector.py"
sed -n '260,340p' "$FILE" | cat -n
echo
echo "== Find ruff invocations across repo (broad) =="
rg -n "ruff\s+(check|lint)" . -S || true
rg -n "python\s+-m\s+ruff" . -S || true
rg -n "pre-commit.*ruff|ruff.*pre-commit" . -S || true
echo
echo "== Find A001/A002 handling (ignore/select) =="
rg -n "A001|A002" . -S || trueRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 5576
🏁 Script executed:
#!/bin/bash
rg -n 'def create_detector_and_conditions\(self, type:' tests/sentry/workflow_engine/processors/test_detector.py
rg -n '^\s*type\s*=' tests/sentry/workflow_engine/processors/test_detector.pyRepository: test-code-tool/sentry__sentry__coderabbit__PR80168__20260407
Length of output: 293
Fix Ruff builtin shadowing in create_detector_and_conditions (A001/A002)
ruff check --select A001,A002 fails due to the type argument/local shadowing the Python builtin in tests/sentry/workflow_engine/processors/test_detector.py (~86-88).
Suggested fix
- def create_detector_and_conditions(self, type: str | None = None):
- if type is None:
- type = "handler_with_state"
+ def create_detector_and_conditions(self, detector_type: str | None = None):
+ if detector_type is None:
+ detector_type = "handler_with_state"
detector = self.create_detector(
workflow_condition_group=self.create_data_condition_group(),
- type=type,
+ type=detector_type,
)- The
group_2test data mismatch (packet has10, helper is called with6) doesn’t currently affect asserted occurrence/event payloads becausebuild_mock_occurrence_and_event(...)ignores itsvalueparameter; align the values for clarity if this is intended to be value-sensitive.
📝 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.
| def create_detector_and_conditions(self, type: str | None = None): | |
| if type is None: | |
| type = "handler_with_state" | |
| detector = self.create_detector( | |
| workflow_condition_group=self.create_data_condition_group(), | |
| type=type, | |
| ) | |
| def create_detector_and_conditions(self, detector_type: str | None = None): | |
| if detector_type is None: | |
| detector_type = "handler_with_state" | |
| detector = self.create_detector( | |
| workflow_condition_group=self.create_data_condition_group(), | |
| type=detector_type, | |
| ) |
🧰 Tools
🪛 Ruff (0.15.15)
[error] 86-86: Function argument type is shadowing a Python builtin
(A002)
[error] 88-88: Variable type is shadowing a Python builtin
(A001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/sentry/workflow_engine/processors/test_detector.py` around lines 86 -
92, The test function create_detector_and_conditions shadows the builtin name
"type"; rename the parameter (for example to detector_type or type_) and update
its use inside the function (the call to create_detector passing type=...) and
any callers in this test file to use the new parameter name to satisfy Ruff
A001/A002; also optionally align the mismatched test data for group_2 (change
the helper call value from 6 to 10 or vice versa) if you intend the value to be
meaningful.
| occurrence_2, event_data_2 = build_mock_occurrence_and_event( | ||
| detector.detector_handler, "group_2", 6, PriorityLevel.HIGH | ||
| ) |
There was a problem hiding this comment.
Use the actual group_2 value in expected occurrence fixture.
group_vals sets "group_2": 10, but the expected helper call passes 6. That weakens the test and can hide value-propagation regressions.
Suggested 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.
| 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/sentry/workflow_engine/processors/test_detector.py` around lines 190 -
192, The test uses build_mock_occurrence_and_event(detector.detector_handler,
"group_2", 6, PriorityLevel.HIGH) but group_vals defines "group_2" as 10, so
update the expected occurrence fixture to use the actual group_2 value (10)
instead of 6; locate the call to build_mock_occurrence_and_event (variables
occurrence_2 and event_data_2) and change the numeric argument to match
group_vals["group_2"] so the test asserts the propagated value correctly.
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.
Summary by CodeRabbit
Refactor
Tests