Skip to content

Commit d1e184e

Browse files
authored
🐛 fix(aci): dedupe actions based on workflow_id as well (#97784)
1 parent 9725222 commit d1e184e

File tree

4 files changed

+104
-23
lines changed

4 files changed

+104
-23
lines changed

src/sentry/workflow_engine/models/action.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ def trigger(self, event_data: WorkflowEventData, detector: Detector) -> None:
110110
},
111111
)
112112

113-
def get_dedup_key(self) -> str:
113+
def get_dedup_key(self, workflow_id: int | None) -> str:
114114
key_parts = [self.type]
115+
if workflow_id is not None:
116+
key_parts.append(str(workflow_id))
115117

116118
if self.integration_id:
117119
key_parts.append(str(self.integration_id))

src/sentry/workflow_engine/processors/action.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ def deduplicate_actions(
133133
dedup_key_to_action_id: dict[str, int] = {}
134134

135135
for action in actions_queryset:
136-
dedup_key = action.get_dedup_key()
136+
# workflow_id is annotated in the queryset
137+
workflow_id = getattr(action, "workflow_id")
138+
dedup_key = action.get_dedup_key(workflow_id)
137139
dedup_key_to_action_id[dedup_key] = action.id
138140

139141
return actions_queryset.filter(id__in=dedup_key_to_action_id.values())

tests/sentry/workflow_engine/processors/test_action_deduplication.py

Lines changed: 97 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from django.db import models
2+
from django.db.models import Value
3+
14
from sentry.notifications.models.notificationaction import ActionTarget
25
from sentry.testutils.cases import TestCase
36
from sentry.testutils.silo import region_silo_test
@@ -61,7 +64,9 @@ def test_deduplicate_actions_different_types(self) -> None:
6164
},
6265
)
6366

64-
actions_queryset = Action.objects.filter(id__in=[self.slack_action.id, email_action.id])
67+
actions_queryset = Action.objects.filter(
68+
id__in=[self.slack_action.id, email_action.id]
69+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
6570

6671
result = deduplicate_actions(actions_queryset)
6772

@@ -84,7 +89,9 @@ def test_deduplicate_actions_same_slack_channels(self) -> None:
8489
},
8590
)
8691

87-
actions_queryset = Action.objects.filter(id__in=[slack_action_1.id, slack_action_2.id])
92+
actions_queryset = Action.objects.filter(
93+
id__in=[slack_action_1.id, slack_action_2.id]
94+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
8895

8996
result = deduplicate_actions(actions_queryset)
9097

@@ -111,7 +118,9 @@ def test_deduplicate_actions_different_slack_channels(self) -> None:
111118
},
112119
)
113120

114-
actions_queryset = Action.objects.filter(id__in=[slack_action_1.id, slack_action_2.id])
121+
actions_queryset = Action.objects.filter(
122+
id__in=[slack_action_1.id, slack_action_2.id]
123+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
115124

116125
result = deduplicate_actions(actions_queryset)
117126

@@ -135,7 +144,9 @@ def test_deduplicate_multiple_slack_actions_same_channel_different_name(self) ->
135144
},
136145
)
137146

138-
actions_queryset = Action.objects.filter(id__in=[slack_action_1.id, slack_action_2.id])
147+
actions_queryset = Action.objects.filter(
148+
id__in=[slack_action_1.id, slack_action_2.id]
149+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
139150

140151
result = deduplicate_actions(actions_queryset)
141152

@@ -167,7 +178,9 @@ def test_deduplicate_actions_same_slack_different_data(self) -> None:
167178
data={"notes": "second action"},
168179
)
169180

170-
actions_queryset = Action.objects.filter(id__in=[slack_action_1.id, slack_action_2.id])
181+
actions_queryset = Action.objects.filter(
182+
id__in=[slack_action_1.id, slack_action_2.id]
183+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
171184

172185
result = deduplicate_actions(actions_queryset)
173186

@@ -202,7 +215,9 @@ def test_deduplicate_actions_different_slack_integrations(self) -> None:
202215
data={"notes": "second action"},
203216
)
204217

205-
actions_queryset = Action.objects.filter(id__in=[slack_action_1.id, slack_action_2.id])
218+
actions_queryset = Action.objects.filter(
219+
id__in=[slack_action_1.id, slack_action_2.id]
220+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
206221

207222
result = deduplicate_actions(actions_queryset)
208223

@@ -231,7 +246,9 @@ def test_deduplicate_actions_email_same_target(self) -> None:
231246
},
232247
)
233248

234-
actions_queryset = Action.objects.filter(id__in=[email_action_1.id, email_action_2.id])
249+
actions_queryset = Action.objects.filter(
250+
id__in=[email_action_1.id, email_action_2.id]
251+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
235252

236253
result = deduplicate_actions(actions_queryset)
237254

@@ -257,7 +274,9 @@ def test_deduplicate_actions_email_different_target_identifier(self) -> None:
257274
},
258275
)
259276

260-
actions_queryset = Action.objects.filter(id__in=[email_action_1.id, email_action_2.id])
277+
actions_queryset = Action.objects.filter(
278+
id__in=[email_action_1.id, email_action_2.id]
279+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
261280

262281
result = deduplicate_actions(actions_queryset)
263282

@@ -285,7 +304,9 @@ def test_deduplicate_actions_email_different_target_type(self) -> None:
285304
},
286305
)
287306

288-
actions_queryset = Action.objects.filter(id__in=[email_action_1.id, email_action_2.id])
307+
actions_queryset = Action.objects.filter(
308+
id__in=[email_action_1.id, email_action_2.id]
309+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
289310

290311
result = deduplicate_actions(actions_queryset)
291312

@@ -319,7 +340,9 @@ def test_deduplicate_actions_email_different_fallthrough_type(self) -> None:
319340
},
320341
)
321342

322-
actions_queryset = Action.objects.filter(id__in=[email_action_1.id, email_action_2.id])
343+
actions_queryset = Action.objects.filter(
344+
id__in=[email_action_1.id, email_action_2.id]
345+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
323346

324347
result = deduplicate_actions(actions_queryset)
325348

@@ -352,7 +375,9 @@ def test_deduplicate_actions_email_everything_is_same(self) -> None:
352375
},
353376
)
354377

355-
actions_queryset = Action.objects.filter(id__in=[email_action_1.id, email_action_2.id])
378+
actions_queryset = Action.objects.filter(
379+
id__in=[email_action_1.id, email_action_2.id]
380+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
356381

357382
result = deduplicate_actions(actions_queryset)
358383

@@ -383,7 +408,7 @@ def test_deduplicate_actions_sentry_app_same_identifier(self) -> None:
383408

384409
actions_queryset = Action.objects.filter(
385410
id__in=[sentry_app_action_1.id, sentry_app_action_2.id]
386-
)
411+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
387412

388413
result = deduplicate_actions(actions_queryset)
389414

@@ -408,7 +433,9 @@ def test_deduplicate_actions_webhook_same_target_identifier(self) -> None:
408433
},
409434
)
410435

411-
actions_queryset = Action.objects.filter(id__in=[webhook_action_1.id, webhook_action_2.id])
436+
actions_queryset = Action.objects.filter(
437+
id__in=[webhook_action_1.id, webhook_action_2.id]
438+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
412439

413440
result = deduplicate_actions(actions_queryset)
414441

@@ -421,7 +448,9 @@ def test_deduplicate_actions_plugin_actions(self) -> None:
421448

422449
plugin_action_2 = self.create_action(type=Action.Type.PLUGIN)
423450

424-
actions_queryset = Action.objects.filter(id__in=[plugin_action_1.id, plugin_action_2.id])
451+
actions_queryset = Action.objects.filter(
452+
id__in=[plugin_action_1.id, plugin_action_2.id]
453+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
425454

426455
result = deduplicate_actions(actions_queryset)
427456

@@ -451,7 +480,9 @@ def test_deduplicate_actions_mixed_types_integration_bucket(self) -> None:
451480
},
452481
)
453482

454-
actions_queryset = Action.objects.filter(id__in=[slack_action.id, pagerduty_action.id])
483+
actions_queryset = Action.objects.filter(
484+
id__in=[slack_action.id, pagerduty_action.id]
485+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
455486

456487
result = deduplicate_actions(actions_queryset)
457488

@@ -486,7 +517,9 @@ def test_deduplicate_actions_ticketing_actions(self) -> None:
486517
},
487518
)
488519

489-
actions_queryset = Action.objects.filter(id__in=[jira_action_1.id, jira_action_2.id])
520+
actions_queryset = Action.objects.filter(
521+
id__in=[jira_action_1.id, jira_action_2.id]
522+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
490523

491524
result = deduplicate_actions(actions_queryset)
492525

@@ -520,7 +553,9 @@ def test_deduplicate_actions_ticketing_actions_same_integration_and_data(self) -
520553
},
521554
)
522555

523-
actions_queryset = Action.objects.filter(id__in=[jira_action_1.id, jira_action_2.id])
556+
actions_queryset = Action.objects.filter(
557+
id__in=[jira_action_1.id, jira_action_2.id]
558+
).annotate(workflow_id=Value(1, output_field=models.IntegerField()))
524559

525560
result = deduplicate_actions(actions_queryset)
526561

@@ -541,11 +576,55 @@ def test_deduplicate_actions_single_action(self) -> None:
541576
"""Test deduplication with single action."""
542577
single_action = self.slack_action
543578

544-
actions_queryset = Action.objects.filter(id=single_action.id)
579+
actions_queryset = Action.objects.filter(id=single_action.id).annotate(
580+
workflow_id=Value(1, output_field=models.IntegerField())
581+
)
545582

546583
result = deduplicate_actions(actions_queryset)
547584

548585
# Should return the single action
549586
result_ids = list(result.values_list("id", flat=True))
550587
assert len(result_ids) == 1
551588
assert result_ids[0] == single_action.id
589+
590+
def test_deduplicate_actions_same_actions_different_workflows(self) -> None:
591+
"""Test that identical actions from different workflows are NOT deduplicated."""
592+
# Create two identical Slack actions
593+
slack_action_1 = self.create_action(
594+
type=Action.Type.SLACK,
595+
integration_id=self.slack_integration.id,
596+
config={
597+
"target_type": ActionTarget.SPECIFIC,
598+
"target_identifier": "channel-123",
599+
"target_display": "Test Channel",
600+
},
601+
)
602+
603+
slack_action_2 = self.create_action(
604+
type=Action.Type.SLACK,
605+
integration_id=self.slack_integration.id,
606+
config={
607+
"target_type": ActionTarget.SPECIFIC,
608+
"target_identifier": "channel-123",
609+
"target_display": "Test Channel",
610+
},
611+
)
612+
613+
# Annotate with different workflow IDs
614+
actions_queryset = Action.objects.filter(
615+
id__in=[slack_action_1.id, slack_action_2.id]
616+
).annotate(
617+
workflow_id=models.Case(
618+
models.When(id=slack_action_1.id, then=models.Value(1)),
619+
models.When(id=slack_action_2.id, then=models.Value(2)),
620+
output_field=models.IntegerField(),
621+
)
622+
)
623+
624+
result = deduplicate_actions(actions_queryset)
625+
626+
# Both actions should remain since they're from different workflows
627+
result_ids = list(result.values_list("id", flat=True))
628+
assert len(result_ids) == 2
629+
assert slack_action_1.id in result_ids
630+
assert slack_action_2.id in result_ids

tests/sentry/workflow_engine/processors/test_workflow.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,7 @@ def test_workflow_fire_history_with_action_deduping(
302302
process_workflows(self.event_data)
303303

304304
assert WorkflowFireHistory.objects.count() == 3
305-
assert (
306-
mock_trigger_action.call_count == 1
307-
) # TODO: determine if this is ideal, the actions are duped but not in the same workflows
305+
assert mock_trigger_action.call_count == 3
308306

309307

310308
@mock_redis_buffer()

0 commit comments

Comments
 (0)