Skip to content

Commit 9e8cb89

Browse files
committed
WIP
1 parent bfbaa2e commit 9e8cb89

File tree

5 files changed

+329
-18
lines changed

5 files changed

+329
-18
lines changed

src/sentry/projects/project_rules/creator.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from sentry.models.rule import Rule, RuleSource
1111
from sentry.types.actor import Actor
1212
from sentry.workflow_engine.defaults.detectors import ensure_default_detectors
13-
from sentry.workflow_engine.migration_helpers.issue_alert_migration import IssueAlertMigrator
13+
from sentry.workflow_engine.defaults.workflows import ensure_default_workflows
14+
from sentry.workflow_engine.models import AlertRuleWorkflow
1415
from sentry.workflow_engine.utils.legacy_metric_tracking import report_used_legacy_models
1516

1617
logger = logging.getLogger(__name__)
@@ -34,16 +35,18 @@ def run(self) -> Rule:
3435
ensure_default_detectors(self.project)
3536

3637
with transaction.atomic(router.db_for_write(Rule)):
38+
workflows = ensure_default_workflows()
3739
self.rule = self._create_rule()
3840

39-
# uncaught errors will rollback the transaction
40-
workflow = IssueAlertMigrator(
41-
self.rule, self.request.user.id if self.request else None
42-
).run()
43-
logger.info(
44-
"workflow_engine.issue_alert.migrated",
45-
extra={"rule_id": self.rule.id, "workflow_id": workflow.id},
46-
)
41+
legacy_references = [
42+
AlertRuleWorkflow(
43+
rule_id=self.rule.id,
44+
workflow=workflow,
45+
)
46+
for workflow in workflows
47+
]
48+
49+
AlertRuleWorkflow.objects.bulk_create(legacy_references)
4750

4851
return self.rule
4952

src/sentry/receivers/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from .owners import * # noqa: F401,F403
1212
from .releases import * # noqa: F401,F403
1313
from .rule_snooze import * # noqa: F401,F403
14-
from .rules import * # noqa: F401,F403
1514
from .sentry_apps import * # noqa: F401,F403
1615
from .stats import * # noqa: F401,F403
1716
from .superuser import * # noqa: F401,F403
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from typing import Sequence
2+
3+
from django.db import router, transaction
4+
5+
from sentry.models.organization import Organization
6+
from sentry.models.project import Project
7+
from sentry.notifications.types import FallthroughChoiceType
8+
from sentry.workflow_engine.defaults.detectors import _ensure_detector
9+
from sentry.workflow_engine.models import (
10+
Action,
11+
DataCondition,
12+
DataConditionGroup,
13+
DataConditionGroupAction,
14+
DetectorWorkflow,
15+
Workflow,
16+
WorkflowDataConditionGroup,
17+
)
18+
from sentry.workflow_engine.models.data_condition import Condition
19+
from sentry.workflow_engine.typings.grouptype import IssueStreamGroupType
20+
21+
22+
def connect_workflows_to_issue_stream(
23+
project: Project,
24+
workflows: list[Workflow],
25+
) -> Sequence[DetectorWorkflow]:
26+
# Because we don't know if this signal is handled already or not...
27+
issue_stream_detector = _ensure_detector(project, IssueStreamGroupType.slug)
28+
29+
connections = [
30+
DetectorWorkflow(
31+
workflow=workflow,
32+
detector=issue_stream_detector,
33+
)
34+
for workflow in workflows
35+
]
36+
return DetectorWorkflow.objects.bulk_create(connections)
37+
38+
39+
def create_priority_workflow(org: Organization) -> Workflow:
40+
with transaction.atomic(router.db_for_write(Workflow)):
41+
workflow, is_created = Workflow.objects.get_or_create(
42+
organization=org,
43+
name="Send a notification for high priority issues",
44+
)
45+
46+
if not is_created:
47+
# if it exists, assume it was created correctly
48+
return workflow
49+
50+
# Create the workflow trigger conditions
51+
workflow.when_condition_group = DataConditionGroup.objects.create(
52+
logic_type=DataConditionGroup.Type.ANY_SHORT_CIRCUIT,
53+
)
54+
55+
conditions: list[DataCondition] = []
56+
conditions.append(
57+
DataCondition(
58+
type=Condition.NEW_HIGH_PRIORITY_ISSUE,
59+
condition_group=workflow.when_condition_group,
60+
comparison=True,
61+
condition_result=True,
62+
)
63+
)
64+
conditions.append(
65+
DataCondition(
66+
type=Condition.EXISTING_HIGH_PRIORITY_ISSUE,
67+
condition_group=workflow.when_condition_group,
68+
comparison=True,
69+
condition_result=True,
70+
)
71+
)
72+
DataCondition.objects.bulk_create(conditions)
73+
74+
# Create the Action
75+
action_filter = DataConditionGroup.objects.create(
76+
logic_type=DataConditionGroup.Type.ANY_SHORT_CIRCUIT,
77+
)
78+
79+
action = Action.objects.create(
80+
type=Action.Type.EMAIL,
81+
config={
82+
"target_type": "IssueOwners",
83+
"target_identifier": None,
84+
"fallthrough_type": FallthroughChoiceType.ACTIVE_MEMBERS.value,
85+
},
86+
)
87+
DataConditionGroupAction.objects.create(
88+
action=action,
89+
condition_group=action_filter,
90+
)
91+
92+
WorkflowDataConditionGroup.objects.create(
93+
workflow=workflow,
94+
condition_group=action_filter,
95+
)
96+
97+
98+
def ensure_default_workflows(project: Project) -> list[Workflow]:
99+
workflows: list[Workflow] = []
100+
101+
workflows.append(create_priority_workflow(project.organization))
102+
103+
connect_workflows_to_issue_stream(project, workflows)
104+
return workflows

src/sentry/receivers/rules.py renamed to src/sentry/workflow_engine/receivers/project_workflows.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from sentry.notifications.types import FallthroughChoiceType
88
from sentry.signals import alert_rule_created, project_created
99
from sentry.users.services.user.model import RpcUser
10-
from sentry.workflow_engine.migration_helpers.issue_alert_migration import IssueAlertMigrator
10+
from sentry.workflow_engine.defaults.workflows import ensure_default_workflows
11+
from sentry.workflow_engine.models import AlertRuleWorkflow
1112

1213
logger = logging.getLogger("sentry")
1314

@@ -34,20 +35,30 @@
3435
PLATFORMS_WITH_PRIORITY_ALERTS = ["python", "javascript"]
3536

3637

37-
def create_default_rules(project: Project, default_rules=True, RuleModel=Rule, **kwargs):
38+
# TODO - invert this so it's create_default_workflows
39+
def create_default_rules(
40+
project: Project,
41+
default_rules=True,
42+
RuleModel=Rule,
43+
):
3844
if not default_rules:
3945
return
4046

4147
rule_data = DEFAULT_RULE_DATA
4248

4349
with transaction.atomic(router.db_for_write(RuleModel)):
4450
rule = RuleModel.objects.create(project=project, label=DEFAULT_RULE_LABEL, data=rule_data)
51+
workflows = ensure_default_workflows(project)
4552

46-
workflow = IssueAlertMigrator(rule).run()
47-
logger.info(
48-
"workflow_engine.default_issue_alert.migrated",
49-
extra={"rule_id": rule.id, "workflow_id": workflow.id},
50-
)
53+
legacy_references = [
54+
AlertRuleWorkflow(
55+
rule_id=rule.id,
56+
workflow=workflow,
57+
)
58+
for workflow in workflows
59+
]
60+
61+
AlertRuleWorkflow.objects.bulk_create(legacy_references)
5162

5263
try:
5364
user: RpcUser = project.organization.get_default_owner()
@@ -71,4 +82,8 @@ def create_default_rules(project: Project, default_rules=True, RuleModel=Rule, *
7182
)
7283

7384

74-
project_created.connect(create_default_rules, dispatch_uid="create_default_rules", weak=False)
85+
project_created.connect(
86+
create_default_rules,
87+
dispatch_uid="create_default_rules",
88+
weak=False,
89+
)
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
from sentry.notifications.types import FallthroughChoiceType
2+
from sentry.testutils.cases import TestCase
3+
from sentry.workflow_engine.defaults.detectors import ensure_default_detectors
4+
from sentry.workflow_engine.defaults.workflows import (
5+
connect_workflows_to_issue_stream,
6+
create_priority_workflow,
7+
ensure_default_workflows,
8+
)
9+
from sentry.workflow_engine.models import (
10+
Action,
11+
DataCondition,
12+
DataConditionGroup,
13+
DataConditionGroupAction,
14+
Detector,
15+
DetectorWorkflow,
16+
Workflow,
17+
WorkflowDataConditionGroup,
18+
)
19+
from sentry.workflow_engine.models.data_condition import Condition
20+
from sentry.workflow_engine.typings.grouptype import IssueStreamGroupType
21+
22+
23+
class TestConnectWorkflowsToIssueStream(TestCase):
24+
def test_creates_detector_workflow_connections(self) -> None:
25+
project = self.create_project()
26+
workflow1 = Workflow.objects.create(
27+
organization=project.organization,
28+
name="Test Workflow 1",
29+
)
30+
workflow2 = Workflow.objects.create(
31+
organization=project.organization,
32+
name="Test Workflow 2",
33+
)
34+
35+
connections = connect_workflows_to_issue_stream(project, [workflow1, workflow2])
36+
37+
assert len(connections) == 2
38+
assert DetectorWorkflow.objects.filter(workflow=workflow1).exists()
39+
assert DetectorWorkflow.objects.filter(workflow=workflow2).exists()
40+
41+
# Verify all workflows are connected to the same issue stream detector
42+
detector_ids = {c.detector_id for c in connections}
43+
assert len(detector_ids) == 1
44+
detector = Detector.objects.get(id=detector_ids.pop())
45+
assert detector.type == IssueStreamGroupType.slug
46+
47+
def test_uses_issue_stream_detector(self) -> None:
48+
project = self.create_project()
49+
workflow = Workflow.objects.create(
50+
organization=project.organization,
51+
name="Test Workflow",
52+
)
53+
54+
connections = connect_workflows_to_issue_stream(project, [workflow])
55+
56+
connection = connections[0]
57+
assert connection.detector.type == IssueStreamGroupType.slug
58+
assert connection.detector.project_id == project.id
59+
60+
# Verify only one issue stream detector exists
61+
issue_stream_detectors = Detector.objects.filter(
62+
project=project, type=IssueStreamGroupType.slug
63+
)
64+
assert issue_stream_detectors.count() == 1
65+
66+
def test_uses_preexisting_issue_stream_detector(self) -> None:
67+
"""Integration test: verifies that if an issue stream detector already exists, it reuses it."""
68+
project = self.create_project()
69+
70+
# Create the default detectors first (simulating project setup signal)
71+
default_detectors = ensure_default_detectors(project)
72+
existing_detector = default_detectors[IssueStreamGroupType.slug]
73+
74+
# Now connect workflows - should use the existing detector
75+
workflow = Workflow.objects.create(
76+
organization=project.organization,
77+
name="Test Workflow",
78+
)
79+
connections = connect_workflows_to_issue_stream(project, [workflow])
80+
81+
# Verify it used the pre-existing detector
82+
assert connections[0].detector_id == existing_detector.id
83+
84+
# Verify still only one issue stream detector exists
85+
issue_stream_detectors = Detector.objects.filter(
86+
project=project, type=IssueStreamGroupType.slug
87+
)
88+
assert issue_stream_detectors.count() == 1
89+
90+
91+
class TestCreatePriorityWorkflow(TestCase):
92+
def test_creates_workflow_with_correct_name(self) -> None:
93+
org = self.create_organization()
94+
95+
workflow = create_priority_workflow(org)
96+
97+
assert workflow.name == "Send a notification for high priority issues"
98+
assert workflow.organization_id == org.id
99+
100+
def test_creates_when_condition_group(self) -> None:
101+
org = self.create_organization()
102+
103+
workflow = create_priority_workflow(org)
104+
105+
assert workflow.when_condition_group is not None
106+
assert workflow.when_condition_group.logic_type == DataConditionGroup.Type.ANY_SHORT_CIRCUIT
107+
108+
def test_creates_data_conditions(self) -> None:
109+
org = self.create_organization()
110+
111+
workflow = create_priority_workflow(org)
112+
113+
conditions = DataCondition.objects.filter(condition_group=workflow.when_condition_group)
114+
assert conditions.count() == 2
115+
116+
condition_types = {c.type for c in conditions}
117+
assert Condition.NEW_HIGH_PRIORITY_ISSUE in condition_types
118+
assert Condition.EXISTING_HIGH_PRIORITY_ISSUE in condition_types
119+
120+
for condition in conditions:
121+
assert condition.comparison is True
122+
assert condition.condition_result is True
123+
124+
def test_creates_email_action(self) -> None:
125+
org = self.create_organization()
126+
127+
create_priority_workflow(org)
128+
129+
action = Action.objects.get(type=Action.Type.EMAIL)
130+
assert action.config == {
131+
"target_type": "IssueOwners",
132+
"target_identifier": None,
133+
"fallthrough_type": FallthroughChoiceType.ACTIVE_MEMBERS.value,
134+
}
135+
136+
def test_creates_action_filter_and_links(self) -> None:
137+
org = self.create_organization()
138+
139+
workflow = create_priority_workflow(org)
140+
141+
# Verify WorkflowDataConditionGroup exists
142+
workflow_dcg = WorkflowDataConditionGroup.objects.get(workflow=workflow)
143+
action_filter = workflow_dcg.condition_group
144+
145+
# Verify action is linked to the filter
146+
action = Action.objects.get(type=Action.Type.EMAIL)
147+
dcg_action = DataConditionGroupAction.objects.get(action=action)
148+
assert dcg_action.condition_group == action_filter
149+
150+
# Verify action filter has correct logic type
151+
assert action_filter.logic_type == DataConditionGroup.Type.ANY_SHORT_CIRCUIT
152+
153+
def test_idempotent_returns_existing_workflow(self) -> None:
154+
org = self.create_organization()
155+
156+
workflow1 = create_priority_workflow(org)
157+
workflow2 = create_priority_workflow(org)
158+
159+
assert workflow1.id == workflow2.id
160+
# Should only have one workflow
161+
assert (
162+
Workflow.objects.filter(
163+
organization=org, name="Send a notification for high priority issues"
164+
).count()
165+
== 1
166+
)
167+
168+
169+
class TestEnsureDefaultWorkflows(TestCase):
170+
def test_creates_and_connects_workflows(self) -> None:
171+
project = self.create_project()
172+
173+
workflows = ensure_default_workflows(project)
174+
175+
assert len(workflows) == 1
176+
workflow = workflows[0]
177+
assert workflow.name == "Send a notification for high priority issues"
178+
179+
# Verify connection to issue stream detector
180+
connection = DetectorWorkflow.objects.get(workflow=workflow)
181+
assert connection.detector.type == IssueStreamGroupType.slug
182+
assert connection.detector.project_id == project.id
183+
184+
def test_returns_workflows_list(self) -> None:
185+
project = self.create_project()
186+
187+
workflows = ensure_default_workflows(project)
188+
189+
assert isinstance(workflows, list)
190+
assert all(isinstance(w, Workflow) for w in workflows)

0 commit comments

Comments
 (0)