Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,23 @@ def get_attrs(
for dcg_ids in detector_to_dcg_ids.values():
all_dcg_ids.update(dcg_ids)

# Map (condition_group_id, comparison) → action-filter DC exists in that DCG
# We need: for a given detector's DCGs + priority level → matching DCG IDs
# NOTE: Assumes DataConditions are limited to what would be dual written.
dcg_comparison_pairs: dict[int, set[int | float]] = defaultdict(set)
# Per action-filter DCG, the priority levels it gates on, and whether it gates on priority
# at all. A DCG with no priority gate fires for any priority, so its actions attach to every trigger.
# DCG_id -> priority number (e.g 75 is HIGH)
# E.g. {1: {75, 50}, 2: {75}}
dcg_priority_comparisons_mapping: dict[int, set[int | float]] = defaultdict(set)

# A set of DCG ids that have a issue priority condition
dcgs_with_priority_condition: set[int] = set()
for dc in DataCondition.objects.filter(condition_group__in=all_dcg_ids):
if dc.type != Condition.ISSUE_PRIORITY_GREATER_OR_EQUAL:
continue
dcgs_with_priority_condition.add(dc.condition_group_id)

# Only collect numeric comparison values; non-numeric values (e.g. dicts
# from anomaly detection conditions) don't match condition_result levels.
if isinstance(dc.comparison, (int, float)):
dcg_comparison_pairs[dc.condition_group_id].add(dc.comparison)
dcg_priority_comparisons_mapping[dc.condition_group_id].add(dc.comparison)

# Bulk-fetch all DCG → action mappings
dcg_to_action_ids: dict[int, list[int]] = defaultdict(list)
Expand Down Expand Up @@ -126,11 +134,12 @@ def get_attrs(
detector_id = detector.id if detector else None
trigger_dcg_ids = detector_to_dcg_ids.get(detector_id, set()) if detector_id else set()

# Find DCGs in this detector's workflows that match the trigger's priority level
# Find DCGs in this detector's workflows that match the trigger's priority level, or has no priority gate at all.
matching_dcg_ids = [
dcg_id
for dcg_id in trigger_dcg_ids
if trigger.condition_result in dcg_comparison_pairs.get(dcg_id, set())
if trigger.condition_result in dcg_priority_comparisons_mapping.get(dcg_id, set())
or dcg_id not in dcgs_with_priority_condition
]

# Collect actions from those DCGs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
migrate_metric_data_conditions,
migrate_resolve_threshold_data_condition,
)
from sentry.workflow_engine.models import DataCondition, WorkflowDataConditionGroup
from sentry.workflow_engine.models import (
Action,
DataCondition,
DataConditionGroup,
WorkflowDataConditionGroup,
)
from sentry.workflow_engine.models.data_condition import Condition
from sentry.workflow_engine.types import DetectorPriorityLevel
from tests.sentry.incidents.serializers.test_workflow_engine_base import (
Expand Down Expand Up @@ -100,6 +105,63 @@ def test_multiple_actions(self) -> None:
expected_trigger["actions"] = expected_actions
assert serialized_data_condition == expected_trigger

def test_action_filter_without_priority_condition(self) -> None:
"""
A natively-created connected alert (workflow) seeds its action filter with no conditions
(see automationBuilderContext.tsx) if they don't include an Issue Priority WHEN clause.
Priority-less action filter should fire for any priority. Previously these actions were dropped,
producing an empty `triggers[].actions` in the metric_alert webhook payload
"""
sentry_app = self.create_sentry_app(
organization=self.organization,
published=True,
verify_install=False,
name="Super Awesome App",
schema={"elements": [self.create_alert_rule_action_schema()]},
)
self.create_sentry_app_installation(
slug=sentry_app.slug, organization=self.organization, user=self.user
)
settings = [{"name": "title", "value": "An alert"}]
sentry_app_action = self.create_action(
type=Action.Type.SENTRY_APP,
config={
"target_type": AlertRuleTriggerAction.TargetType.SENTRY_APP,
"target_identifier": str(sentry_app.id),
"target_display": None,
},
data={"settings": settings},
)

# Connect a native workflow to the existing detector whose action filter has no
# priority (or any) condition, mirroring how the new Monitors UI creates connected alerts.
workflow = self.create_workflow(organization=self.organization)
self.create_detector_workflow(detector=self.detector, workflow=workflow)
action_filter = self.create_data_condition_group(
organization=self.organization, logic_type=DataConditionGroup.Type.ALL
)
self.create_workflow_data_condition_group(workflow=workflow, condition_group=action_filter)
self.create_data_condition_group_action(
action=sentry_app_action, condition_group=action_filter
)

serialized_data_condition = serialize(
self.critical_detector_trigger,
self.user,
WorkflowEngineDataConditionSerializer(),
)

serialized_sentry_app_actions = [
action
for action in serialized_data_condition["actions"]
if action["sentryAppId"] == sentry_app.id
]
assert len(serialized_sentry_app_actions) == 1
serialized_sentry_app_action = serialized_sentry_app_actions[0]
assert serialized_sentry_app_action["type"] == "sentry_app"
assert serialized_sentry_app_action["targetType"] == "sentry_app"
assert serialized_sentry_app_action["settings"] == settings

def test_comparison_delta(self) -> None:
comparison_delta_rule = self.create_alert_rule(comparison_delta=60)
comparison_delta_trigger = self.create_alert_rule_trigger(
Expand Down Expand Up @@ -205,6 +267,10 @@ def test_anomaly_detection_with_workflow_actions(self) -> None:
assert serialized["thresholdType"] == AlertRuleThresholdType.ABOVE_AND_BELOW.value
assert serialized["alertThreshold"] == 0
assert serialized["resolveThreshold"] is None
# The non-numeric ANOMALY_DETECTION condition is skipped during matching without dropping
# the DCG's action (which still matches via the priority condition) or duplicating it.
action_ids = [action["id"] for action in serialized["actions"]]
assert action_ids == [str(trigger_action.id)]

def test_multiple_rules(self) -> None:
# create another comprehensive alert rule in the DB
Expand Down
Loading