From 675fbe86b45ec784c4f980096b48997235a142d8 Mon Sep 17 00:00:00 2001 From: Josh Callender <1569818+saponifi3d@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:34:40 -0700 Subject: [PATCH 1/6] We have two ways to maintain if the condition is a trigger or not, this simplifies it back down to 1 --- .../validators/base/data_condition_group.py | 15 +++++++++++++-- .../workflow_engine/models/data_condition.py | 7 ------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py b/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py index bd79711c474a..9b589134b777 100644 --- a/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py +++ b/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py @@ -4,13 +4,16 @@ from rest_framework import serializers from sentry.api.serializers.rest_framework import CamelSnakeSerializer +from sentry.utils import registry from sentry.workflow_engine.endpoints.validators.base.data_condition import ( BaseDataConditionValidator, DataConditionInput, ) from sentry.workflow_engine.endpoints.validators.utils import remove_items_by_api_input from sentry.workflow_engine.models import DataConditionGroup -from sentry.workflow_engine.models.data_condition import TRIGGER_CONDITIONS, DataCondition +from sentry.workflow_engine.models.data_condition import Condition, DataCondition +from sentry.workflow_engine.registry import condition_handler_registry +from sentry.workflow_engine.types import DataConditionHandler class DataConditionGroupInput(TypedDict): @@ -40,7 +43,15 @@ def _validate_logic_type(self, condition_data: list[dict[str, Any]], logic_type: break upon updating. """ for condition in condition_data: - if (condition.get("type") in TRIGGER_CONDITIONS) and ( + try: + condition_type = Condition(condition.get("type")) + condition_handler = condition_handler_registry.get(condition_type) + except (registry.NoRegistrationExistsError, ValueError): + raise serializers.ValidationError( + f"Invalid condition type, '{condition.get('type')}'" + ) + + if (condition_handler.group == DataConditionHandler.Group.WORKFLOW_TRIGGER) and ( logic_type != DataConditionGroup.Type.ANY_SHORT_CIRCUIT.value ): raise serializers.ValidationError("Triggers' logic type must be 'any-short'") diff --git a/src/sentry/workflow_engine/models/data_condition.py b/src/sentry/workflow_engine/models/data_condition.py index c8e06e4b7cbe..cc57072537b3 100644 --- a/src/sentry/workflow_engine/models/data_condition.py +++ b/src/sentry/workflow_engine/models/data_condition.py @@ -70,13 +70,6 @@ class Condition(StrEnum): SEER_ACTIVITY_TRIGGER = "seer_activity_trigger" -TRIGGER_CONDITIONS = [ - Condition.FIRST_SEEN_EVENT, - Condition.ISSUE_RESOLVED_TRIGGER, - Condition.REAPPEARED_EVENT, - Condition.REGRESSION_EVENT, -] - CONDITION_OPS = { Condition.EQUAL: operator.eq, Condition.GREATER_OR_EQUAL: operator.ge, From 32ed09c8303f7822e1bf911b2193e6d46d987dc8 Mon Sep 17 00:00:00 2001 From: Josh Callender <1569818+saponifi3d@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:51:27 -0700 Subject: [PATCH 2/6] bot was right, this would have been not ideal. --- .../validators/base/data_condition_group.py | 12 +++++++----- .../workflow_engine/models/data_condition.py | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py b/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py index 9b589134b777..a7cb2b72f5e1 100644 --- a/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py +++ b/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py @@ -4,15 +4,17 @@ from rest_framework import serializers from sentry.api.serializers.rest_framework import CamelSnakeSerializer -from sentry.utils import registry from sentry.workflow_engine.endpoints.validators.base.data_condition import ( BaseDataConditionValidator, DataConditionInput, ) from sentry.workflow_engine.endpoints.validators.utils import remove_items_by_api_input from sentry.workflow_engine.models import DataConditionGroup -from sentry.workflow_engine.models.data_condition import Condition, DataCondition -from sentry.workflow_engine.registry import condition_handler_registry +from sentry.workflow_engine.models.data_condition import ( + Condition, + DataCondition, + get_condition_handler, +) from sentry.workflow_engine.types import DataConditionHandler @@ -45,12 +47,12 @@ def _validate_logic_type(self, condition_data: list[dict[str, Any]], logic_type: for condition in condition_data: try: condition_type = Condition(condition.get("type")) - condition_handler = condition_handler_registry.get(condition_type) - except (registry.NoRegistrationExistsError, ValueError): + except ValueError: raise serializers.ValidationError( f"Invalid condition type, '{condition.get('type')}'" ) + condition_handler = get_condition_handler(condition_type) if (condition_handler.group == DataConditionHandler.Group.WORKFLOW_TRIGGER) and ( logic_type != DataConditionGroup.Type.ANY_SHORT_CIRCUIT.value ): diff --git a/src/sentry/workflow_engine/models/data_condition.py b/src/sentry/workflow_engine/models/data_condition.py index cc57072537b3..a4f067ee25c0 100644 --- a/src/sentry/workflow_engine/models/data_condition.py +++ b/src/sentry/workflow_engine/models/data_condition.py @@ -14,7 +14,12 @@ DataConditionEvaluationException, ) from sentry.workflow_engine.registry import condition_handler_registry -from sentry.workflow_engine.types import ConditionError, DataConditionResult, DetectorPriorityLevel +from sentry.workflow_engine.types import ( + ConditionError, + DataConditionHandler, + DataConditionResult, + DetectorPriorityLevel, +) from sentry.workflow_engine.utils import scopedstats logger = logging.getLogger(__name__) @@ -253,6 +258,16 @@ def evaluate_value(self, value: T) -> DataConditionEvaluation: ) +def get_condition_handler(condition_type: Condition) -> DataConditionHandler: + if condition_type not in CONDITION_OPS: + try: + return condition_handler_registry.get(condition_type) + except registry.NoRegistrationExistsError: + pass + + return None + + def is_slow_condition(condition: DataCondition) -> bool: return Condition(condition.type) in SLOW_CONDITIONS From 15a3c09a885238f7ace1c48711950e021c2d275b Mon Sep 17 00:00:00 2001 From: Josh Callender <1569818+saponifi3d@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:54:58 -0700 Subject: [PATCH 3/6] ensure the handler isn't none --- .../endpoints/validators/base/data_condition_group.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py b/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py index a7cb2b72f5e1..bac60beff5a9 100644 --- a/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py +++ b/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py @@ -53,9 +53,11 @@ def _validate_logic_type(self, condition_data: list[dict[str, Any]], logic_type: ) condition_handler = get_condition_handler(condition_type) - if (condition_handler.group == DataConditionHandler.Group.WORKFLOW_TRIGGER) and ( - logic_type != DataConditionGroup.Type.ANY_SHORT_CIRCUIT.value - ): + + if ( + condition_handler is not None + and condition_handler.group == DataConditionHandler.Group.WORKFLOW_TRIGGER + ) and (logic_type != DataConditionGroup.Type.ANY_SHORT_CIRCUIT.value): raise serializers.ValidationError("Triggers' logic type must be 'any-short'") def update_or_create_condition( From d9c9c263bb2001dde585e0b5ab09b9b390de494a Mon Sep 17 00:00:00 2001 From: Josh Callender <1569818+saponifi3d@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:00:09 -0700 Subject: [PATCH 4/6] now with proper types --- src/sentry/workflow_engine/models/data_condition.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sentry/workflow_engine/models/data_condition.py b/src/sentry/workflow_engine/models/data_condition.py index a4f067ee25c0..e243396ac830 100644 --- a/src/sentry/workflow_engine/models/data_condition.py +++ b/src/sentry/workflow_engine/models/data_condition.py @@ -258,7 +258,9 @@ def evaluate_value(self, value: T) -> DataConditionEvaluation: ) -def get_condition_handler(condition_type: Condition) -> DataConditionHandler: +def get_condition_handler( + condition_type: Condition, +) -> type[DataConditionHandler[Any]] | None: if condition_type not in CONDITION_OPS: try: return condition_handler_registry.get(condition_type) From 6d05c9f68929b0da41f074e6e4c780b41379074a Mon Sep 17 00:00:00 2001 From: Josh Callender <1569818+saponifi3d@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:19:49 -0700 Subject: [PATCH 5/6] Add a couple of tests to verify everything --- .../test_base_data_condition_group.py | 42 ++++++++++++++++++- .../models/test_data_condition.py | 28 +++++++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/tests/sentry/workflow_engine/endpoints/validators/test_base_data_condition_group.py b/tests/sentry/workflow_engine/endpoints/validators/test_base_data_condition_group.py index 20c5f5b4765b..cde4c44afffb 100644 --- a/tests/sentry/workflow_engine/endpoints/validators/test_base_data_condition_group.py +++ b/tests/sentry/workflow_engine/endpoints/validators/test_base_data_condition_group.py @@ -149,6 +149,44 @@ def test_create__with_conditions(self) -> None: assert condition.comparison == 1 assert condition.condition_group == result + def test_create_operator_condition__non_short_circuit_logic_type(self) -> None: + self.valid_data["logicType"] = DataConditionGroup.Type.ALL + self.valid_data["conditions"] = [ + { + "type": Condition.EQUAL, + "comparison": 1, + "conditionResult": True, + } + ] + + validator = BaseDataConditionGroupValidator(data=self.valid_data, context=self.context) + validator.is_valid(raise_exception=True) + result = validator.create(validator.validated_data) + + assert result.logic_type == DataConditionGroup.Type.ALL + assert result.conditions.count() == 1 + + def test_create_action_filter__non_short_circuit_logic_type(self) -> None: + self.valid_data["logicType"] = DataConditionGroup.Type.ALL + self.valid_data["conditions"] = [ + { + "type": Condition.AGE_COMPARISON, + "comparison": { + "comparison_type": "older", + "value": 1, + "time": "day", + }, + "conditionResult": True, + } + ] + + validator = BaseDataConditionGroupValidator(data=self.valid_data, context=self.context) + validator.is_valid(raise_exception=True) + result = validator.create(validator.validated_data) + + assert result.logic_type == DataConditionGroup.Type.ALL + assert result.conditions.count() == 1 + def test_create_trigger__valid_logic_type(self) -> None: valid_data = { "organizationId": self.organization.id, @@ -194,7 +232,9 @@ def test_create_trigger__invalid_logic_type(self) -> None: "logicType": DataConditionGroup.Type.ALL, "conditions": [ { - "type": Condition.FIRST_SEEN_EVENT, + # This trigger was never included in the former TRIGGER_CONDITIONS list. + # Its handler metadata must be the source of truth for validation. + "type": Condition.EVERY_EVENT, "comparison": True, "conditionResult": True, } diff --git a/tests/sentry/workflow_engine/models/test_data_condition.py b/tests/sentry/workflow_engine/models/test_data_condition.py index 7d10ce5046d7..aaad3e59e8ed 100644 --- a/tests/sentry/workflow_engine/models/test_data_condition.py +++ b/tests/sentry/workflow_engine/models/test_data_condition.py @@ -1,10 +1,12 @@ from enum import IntEnum +from typing import Any, cast from unittest import mock import pytest from sentry.testutils.cases import TestCase -from sentry.workflow_engine.models.data_condition import Condition +from sentry.utils.registry import NoRegistrationExistsError +from sentry.workflow_engine.models.data_condition import Condition, get_condition_handler from sentry.workflow_engine.processors.evaluations import DataConditionEvaluationException from sentry.workflow_engine.types import ConditionError, DetectorPriorityLevel from tests.sentry.workflow_engine.test_base import BaseWorkflowTest, DataConditionHandlerMixin @@ -45,10 +47,26 @@ def test_boolean(self) -> None: assert dc.get_condition_result() is True +class GetConditionHandlerTest(TestCase): + def test_operator_condition(self) -> None: + assert get_condition_handler(Condition.EQUAL) is None + + def test_registered_condition(self) -> None: + assert get_condition_handler(Condition.EVERY_EVENT) is not None + + @mock.patch("sentry.workflow_engine.models.data_condition.condition_handler_registry.get") + def test_unregistered_condition(self, mock_get: mock.Mock) -> None: + mock_get.side_effect = NoRegistrationExistsError + + assert get_condition_handler(Condition.AGE_COMPARISON) is None + + class EvaluateValueTest(DataConditionHandlerMixin, BaseWorkflowTest): def test(self) -> None: dc = self.create_data_condition( - type=Condition.GREATER, comparison=1.0, condition_result=DetectorPriorityLevel.HIGH + type=Condition.GREATER, + comparison=cast(Any, 1.0), + condition_result=DetectorPriorityLevel.HIGH, ) evaluation = dc.evaluate_value(2) assert evaluation.triggered is True @@ -84,7 +102,9 @@ def test_bad_condition(self) -> None: with pytest.raises(ValueError): # Raises ValueError because the condition is invalid self.create_data_condition( - type="invalid", comparison=1.0, condition_result=DetectorPriorityLevel.HIGH + type=cast(Any, "invalid"), + comparison=cast(Any, 1.0), + condition_result=DetectorPriorityLevel.HIGH, ) def test_bad_comparison(self) -> None: @@ -100,7 +120,7 @@ def test_bad_comparison(self) -> None: def test_condition_result_comparison_fails(self) -> None: dc = self.create_data_condition( type=Condition.GREATER, - comparison=1.0, + comparison=cast(Any, 1.0), condition_result="wrong", ) evaluation = dc.evaluate_value(2) From 445ac3abd055fdff8d02eba4c35b7b1f51a9333c Mon Sep 17 00:00:00 2001 From: Josh Callender <1569818+saponifi3d@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:20:00 -0700 Subject: [PATCH 6/6] fix the last mypy failure --- .../endpoints/validators/base/data_condition_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py b/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py index bac60beff5a9..3119f74717a3 100644 --- a/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py +++ b/src/sentry/workflow_engine/endpoints/validators/base/data_condition_group.py @@ -46,7 +46,7 @@ def _validate_logic_type(self, condition_data: list[dict[str, Any]], logic_type: """ for condition in condition_data: try: - condition_type = Condition(condition.get("type")) + condition_type = Condition(str(condition.get("type"))) except ValueError: raise serializers.ValidationError( f"Invalid condition type, '{condition.get('type')}'"