feat(workflow_engine): Add in hook for producing occurrences from the stateful detector
getsentry/sentry#80168
Golden recall: 100%
Extra findings: 3
Golden Comments (2/2 found)
| # | Comment | Severity | Status | FriendlyReviewer Detail |
|---|---|---|---|---|
| #1 | MetricAlertDetectorHandler inherits from StatefulDetectorHandler but only contains pass, failing to implement its required abstract methods: counter_names (property), get_dedupe_value(), get_group_key_values(), and build_occurrence_and_event_data(). This will cause a TypeError at runtime when the class is instantiated |
HIGH | ✓ Found | "The new abstract method build_occurrence_and_event_data in StatefulDetectorHandler (detector.py) leaves the placeholder MetricAlertDetectorHandler abstract and uninstantiable, causing Detector.detector_handler to raise TypeError for any detector of type metric_alert_fire. Provide stub implementations of the abstract members or leave detector_handler unset" (grouptype.py L11) |
| #2 | Docstring says this returns a list of DetectorEvaluationResult, but the method now returns a dict keyed by DetectorGroupKey. Consider updating the docstring to match the new return type |
LOW | ✓ Found | "The evaluate docstring still says it returns a list of DetectorEvaluationResult, but the method now returns a dict keyed by group key. Update the docstring to match the new contract" (detector.py L225) |
Supplementary Findings (3 findings)
| Finding | File | Severity | Legitimate? |
|---|---|---|---|
| Redis dedupe guard is check-then-act (TOCTOU): concurrent workers can both pass it and emit duplicate occurrences; a crash between emit and state commit reprocesses the packet | detector.py:58 | MEDIUM | ✓ Real race, listed as a legitimate unscored finding in the report |
test_state_results_multi_group passes value=6 for group_2 while the packet value is 10 — the test wouldn't catch a wrong per-group value |
test_detector.py:192 | LOW | ✓ Accurate test-fidelity point |
Stale TODO comment ("will be a stateful detector") directly above a class that already extends StatefulDetectorHandler |
grouptype.py:9 | LOW | ✓ Accurate; the comment became stale with this PR |
Honest Analysis
FriendlyReviewer's full review
As posted on the PR — the complete output from FriendlyReviewer.
Hi there 👋
| 🌥️ Tech | 🌤️ Feat |
|---|---|
| 1 high, 1 medium, 3 low | 1 ok, 1 mitigated |
MR implements the new occurrence-production hook and switches evaluate to return a dict keyed by group key, which dedupes within a single evaluation. The hook implementation is sound and test coverage is good. However, the new abstract hook makes the placeholder MetricAlertDetectorHandler uninstantiable, which will crash process_detectors for metric-alert fire detectors; the Redis dedupe guard still has a TOCTOU race that can emit duplicate occurrences across workers; and a few doc/test fidelity issues remain. Overall the change requires follow-up before merge.
These might need a close look
- 🔴 src/sentry/incidents/grouptype.py (L11)
The new abstract methodbuild_occurrence_and_event_datainStatefulDetectorHandler(detector.py) leaves the placeholderMetricAlertDetectorHandlerabstract and uninstantiable, causingDetector.detector_handlerto raiseTypeErrorfor any detector of typemetric_alert_fire. Provide stub implementations of the abstract members or leavedetector_handlerunset onMetricAlertFireuntil the companion stateful implementation lands.
Worth checking
- 🟡 src/sentry/workflow_engine/processors/detector.py (L58)
The Redis dedupe guard is check-then-act:dedupe_valueis read before evaluation, the new value is written only later in_bulk_commit_redis_state, and the occurrence is pushed to Kafka beforecommit_state_updates. Concurrent workers can both pass the guard and emit duplicate occurrences, and a crash between emit and state commit reprocesses the packet. Make the dedupe check atomic (e.g. SETNX/compare-and-swap) or commit state before emitting.
Small things (take or leave)
- 🔵 src/sentry/workflow_engine/processors/detector.py (L225)
Theevaluatedocstring still says it returns a list ofDetectorEvaluationResult, but the method now returns a dict keyed by group key. Update the docstring to match the new contract. - 🔵 tests/sentry/workflow_engine/processors/test_detector.py (L192)
Intest_state_results_multi_group,build_mock_occurrence_and_eventis called withvalue=6forgroup_2while the packet value is 10. The current mock ignoresvalue, so the test passes, but it would not catch a regression where the hook receives the wrong per-group value. Pass the real packet value (10) forgroup_2to make the test faithful to the hook contract. - 🔵 src/sentry/incidents/grouptype.py (L9)
The comment# TODO: This will be a stateful detector when we build that abstractionis now stale because the class directly below it already extendsStatefulDetectorHandler. Reword it to describe the remaining work (implementing the abstract occurrence/state methods) rather than implying the abstraction still needs to be built.
✅ Feature-level checklist
Looks good
- ✅ Add a hook that can be implemented to produce an occurrence specific to the detector subclassing the StatefulDetector.
Thebuild_occurrence_and_event_dataabstract hook is added and flows throughDetectorEvaluationResultinto occurrence creation. The hook is only invoked when status is not OK, and the priority conversion is type-safe. The placeholder handler issue is separate from the hook's existence.
Partially covered
- ⚠️ Change the signature of evaluate to return a dict keyed by groupkey instead of a list, to avoid duplicate results for the same group key.
evaluatenow returns a dict keyed byDetectorGroupKey, deduplicating results within a single evaluation. However, the cross-worker Redis dedupe guard is still a check-then-act race that can produce duplicate occurrences for the same group key, so the stated dedupe goal is only partially satisfied.
FriendlyReviewer found both golden comments — 100% recall — on a dataset of only two goldens. Both hits are precise: golden #1 (the placeholder
MetricAlertDetectorHandlerleft abstract →TypeErroron instantiation) matches the grouptype.py L11 finding with the same mechanism and the same remediation, and golden #2 (the staleevaluatedocstring, detector.py L225) matches essentially word for word.The review's best unscored finding is the Redis dedupe TOCTOU (detector.py L58): the guard is check-then-act —
dedupe_valueis read before evaluation, the new value is written only later in_bulk_commit_redis_state, and the occurrence is pushed to Kafka beforecommit_state_updates. Concurrent workers can both pass the guard and emit duplicate occurrences, and a crash between emit and state commit reprocesses the packet. It is more consequential than golden #2; the dataset simply has no golden for it.Weaknesses: five findings is a light haul for a feature touching the detector pipeline, and the two LOW test-fidelity items (mock
value=6vs packet value 10, the stale TODO comment) are nit-level. The review is also mildly self-contradictory — 'test coverage is good' in the summary, then a coverage gap in the checklist — and the checklist's 'Partially covered' verdict on the dedupe goal reads more like a design objection than a PR defect.Honest bottom line: the 100% recall scores the two dataset goldens correctly, and the TOCTOU finding shows depth beyond the dataset. Small PRs with well-curated goldens are where FR looks best, provided the perfect score is not read as perfection.