Skip to content

Commit 9b30768

Browse files
authored
fix(hc): Silo fixes for alert rule actions (#58185)
Restore #57949, which was reverted by #58091. Fix SENTRY-16NH (https://sentry.sentry.io/issues/4544284664/). Adapt OrganizationAlertRuleAvailableActionIndexEndpoint and get_available_action_integrations_for_org to run in the region silo. Introduce a prepare_sentry_app_components method to AppService. Remove the method of the same name from the SentryAppInstallation model. (It previously dispatched to the same module-level function that AppService now calls. The model method was called nowhere else.) Mark OrganizationAlertRuleAvailableActionIndexEndpointTest as stable. Change setup to pass RPC models as needed.
1 parent 650777e commit 9b30768

File tree

9 files changed

+117
-52
lines changed

9 files changed

+117
-52
lines changed

src/sentry/incidents/endpoints/organization_alert_rule_available_action_index.py

+27-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from __future__ import annotations
2+
13
from collections import defaultdict
4+
from typing import Any, DefaultDict, List, Mapping
25

36
from rest_framework import status
47
from rest_framework.request import Request
@@ -9,20 +12,24 @@
912
from sentry.api.base import region_silo_endpoint
1013
from sentry.api.bases.organization import OrganizationEndpoint
1114
from sentry.api.exceptions import ResourceDoesNotExist
12-
from sentry.constants import SentryAppStatus
1315
from sentry.incidents.logic import (
1416
get_available_action_integrations_for_org,
1517
get_opsgenie_teams,
1618
get_pagerduty_services,
1719
)
1820
from sentry.incidents.models import AlertRuleTriggerAction
1921
from sentry.incidents.serializers import ACTION_TARGET_TYPE_TO_STRING
20-
from sentry.models.integrations.sentry_app_installation import SentryAppInstallation
22+
from sentry.models.organization import Organization
23+
from sentry.services.hybrid_cloud.app import RpcSentryAppInstallation, app_service
24+
from sentry.services.hybrid_cloud.integration import RpcIntegration
2125

2226

2327
def build_action_response(
24-
registered_type, integration=None, organization=None, sentry_app_installation=None
25-
):
28+
registered_type,
29+
integration: RpcIntegration | None = None,
30+
organization: Organization | None = None,
31+
sentry_app_installation: RpcSentryAppInstallation | None = None,
32+
) -> Mapping[str, Any]:
2633
"""
2734
Build the "available action" objects for the API. Each one can have different fields.
2835
@@ -45,28 +52,32 @@ def build_action_response(
4552
action_response["integrationId"] = integration.id
4653

4754
if registered_type.type == AlertRuleTriggerAction.Type.PAGERDUTY:
55+
if organization is None:
56+
raise Exception("Organization is required for PAGERDUTY actions")
4857
action_response["options"] = [
4958
{"value": id, "label": service_name}
5059
for id, service_name in get_pagerduty_services(organization.id, integration.id)
5160
]
5261
elif registered_type.type == AlertRuleTriggerAction.Type.OPSGENIE:
62+
if organization is None:
63+
raise Exception("Organization is required for OPSGENIE actions")
5364
action_response["options"] = [
5465
{"value": id, "label": team}
5566
for id, team in get_opsgenie_teams(organization.id, integration.id)
5667
]
5768

5869
elif sentry_app_installation:
5970
action_response["sentryAppName"] = sentry_app_installation.sentry_app.name
60-
action_response["sentryAppId"] = sentry_app_installation.sentry_app_id
71+
action_response["sentryAppId"] = sentry_app_installation.sentry_app.id
6172
action_response["sentryAppInstallationUuid"] = sentry_app_installation.uuid
62-
action_response["status"] = SentryAppStatus.as_str(
63-
sentry_app_installation.sentry_app.status
64-
)
73+
action_response["status"] = sentry_app_installation.sentry_app.status
6574

6675
# Sentry Apps can be alertable but not have an Alert Rule UI Component
67-
component = sentry_app_installation.prepare_sentry_app_components("alert-rule-action")
76+
component = app_service.prepare_sentry_app_components(
77+
installation_id=sentry_app_installation.id, component_type="alert-rule-action"
78+
)
6879
if component:
69-
action_response["settings"] = component.schema.get("settings", {})
80+
action_response["settings"] = component.app_schema.get("settings", {})
7081

7182
return action_response
7283

@@ -87,7 +98,7 @@ def get(self, request: Request, organization) -> Response:
8798
actions = []
8899

89100
# Cache Integration objects in this data structure to save DB calls.
90-
provider_integrations = defaultdict(list)
101+
provider_integrations: DefaultDict[str, List[RpcIntegration]] = defaultdict(list)
91102
for integration in get_available_action_integrations_for_org(organization):
92103
provider_integrations[integration.provider].append(integration)
93104

@@ -103,13 +114,13 @@ def get(self, request: Request, organization) -> Response:
103114

104115
# Add all alertable SentryApps to the list.
105116
elif registered_type.type == AlertRuleTriggerAction.Type.SENTRY_APP:
117+
installs = app_service.get_installed_for_organization(
118+
organization_id=organization.id
119+
)
106120
actions += [
107121
build_action_response(registered_type, sentry_app_installation=install)
108-
for install in SentryAppInstallation.objects.get_installed_for_organization(
109-
organization.id
110-
).filter(
111-
sentry_app__is_alertable=True,
112-
)
122+
for install in installs
123+
if install.sentry_app.is_alertable
113124
]
114125

115126
else:

src/sentry/incidents/logic.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from sentry import analytics, audit_log, features, quotas
1717
from sentry.auth.access import SystemAccess
18-
from sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS
18+
from sentry.constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS, ObjectStatus
1919
from sentry.incidents import tasks
2020
from sentry.incidents.models import (
2121
AlertRule,
@@ -38,7 +38,6 @@
3838
TriggerStatus,
3939
)
4040
from sentry.models.actor import Actor
41-
from sentry.models.integrations.integration import Integration
4241
from sentry.models.integrations.organization_integration import OrganizationIntegration
4342
from sentry.models.notificationaction import ActionService, ActionTarget
4443
from sentry.models.project import Project
@@ -1458,7 +1457,7 @@ def get_actions_for_trigger(trigger):
14581457
return AlertRuleTriggerAction.objects.filter(alert_rule_trigger=trigger)
14591458

14601459

1461-
def get_available_action_integrations_for_org(organization):
1460+
def get_available_action_integrations_for_org(organization) -> List[RpcIntegration]:
14621461
"""
14631462
Returns a list of integrations that the organization has installed. Integrations are
14641463
filtered by the list of registered providers.
@@ -1472,8 +1471,11 @@ def get_available_action_integrations_for_org(organization):
14721471
if registration.type != AlertRuleTriggerAction.Type.DISCORD
14731472
or features.has("organizations:integrations-discord-metric-alerts", organization)
14741473
]
1475-
return Integration.objects.get_active_integrations(organization.id).filter(
1476-
provider__in=providers
1474+
return integration_service.get_integrations(
1475+
status=ObjectStatus.ACTIVE,
1476+
org_integration_status=ObjectStatus.ACTIVE,
1477+
organization_id=organization.id,
1478+
providers=providers,
14771479
)
14781480

14791481

src/sentry/models/integrations/sentry_app_installation.py

+11-17
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import uuid
44
from itertools import chain
5-
from typing import TYPE_CHECKING, Any, List
5+
from typing import TYPE_CHECKING, Any, List, Mapping
66

77
from django.db import models, router, transaction
88
from django.db.models import OuterRef, QuerySet, Subquery
@@ -19,6 +19,7 @@
1919
)
2020
from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey
2121
from sentry.services.hybrid_cloud.auth import AuthenticatedToken
22+
from sentry.services.hybrid_cloud.project import RpcProject
2223
from sentry.types.region import find_regions_for_orgs
2324

2425
if TYPE_CHECKING:
@@ -195,19 +196,12 @@ def outboxes_for_update(self) -> List[ControlOutbox]:
195196
for region_name in find_regions_for_orgs([self.organization_id])
196197
]
197198

198-
def prepare_sentry_app_components(self, component_type, project=None, values=None):
199-
from sentry.models.integrations.sentry_app_component import SentryAppComponent
200-
201-
try:
202-
component = SentryAppComponent.objects.get(
203-
sentry_app_id=self.sentry_app_id, type=component_type
204-
)
205-
except SentryAppComponent.DoesNotExist:
206-
return None
207-
208-
return self.prepare_ui_component(component, project, values)
209-
210-
def prepare_ui_component(self, component, project=None, values=None):
199+
def prepare_ui_component(
200+
self,
201+
component: SentryAppComponent,
202+
project: Project | RpcProject | None = None,
203+
values: Any = None,
204+
) -> SentryAppComponent | None:
211205
return prepare_ui_component(
212206
self, component, project_slug=project.slug if project else None, values=values
213207
)
@@ -217,8 +211,8 @@ def prepare_sentry_app_components(
217211
installation: SentryAppInstallation,
218212
component_type: str,
219213
project_slug: str | None = None,
220-
values: Any = None,
221-
):
214+
values: List[Mapping[str, Any]] | None = None,
215+
) -> SentryAppComponent | None:
222216
from sentry.models.integrations.sentry_app_component import SentryAppComponent
223217

224218
try:
@@ -235,7 +229,7 @@ def prepare_ui_component(
235229
installation: SentryAppInstallation,
236230
component: SentryAppComponent,
237231
project_slug: str | None = None,
238-
values: Any = None,
232+
values: List[Mapping[str, Any]] | None = None,
239233
) -> SentryAppComponent | None:
240234
from sentry.coreapi import APIError
241235
from sentry.sentry_apps.components import SentryAppComponentPreparer

src/sentry/services/hybrid_cloud/app/impl.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
)
2929
from sentry.services.hybrid_cloud.app.serial import (
3030
serialize_sentry_app,
31+
serialize_sentry_app_component,
3132
serialize_sentry_app_installation,
3233
)
3334
from sentry.services.hybrid_cloud.auth import AuthenticationContext
@@ -55,12 +56,7 @@ def get_many(
5556

5657
def find_app_components(self, *, app_id: int) -> List[RpcSentryAppComponent]:
5758
return [
58-
RpcSentryAppComponent(
59-
uuid=str(c.uuid),
60-
sentry_app_id=c.sentry_app_id,
61-
type=c.type,
62-
app_schema=c.schema,
63-
)
59+
serialize_sentry_app_component(c)
6460
for c in SentryAppComponent.objects.filter(sentry_app_id=app_id)
6561
]
6662

@@ -263,3 +259,12 @@ def create_internal_integration_for_channel_request(
263259
installation = SentryAppInstallation.objects.get(sentry_app=sentry_app)
264260

265261
return serialize_sentry_app_installation(installation=installation, app=sentry_app)
262+
263+
def prepare_sentry_app_components(
264+
self, *, installation_id: int, component_type: str, project_slug: Optional[str] = None
265+
) -> Optional[RpcSentryAppComponent]:
266+
from sentry.models.integrations.sentry_app_installation import prepare_sentry_app_components
267+
268+
installation = SentryAppInstallation.objects.get(id=installation_id)
269+
component = prepare_sentry_app_components(installation, component_type, project_slug)
270+
return serialize_sentry_app_component(component) if component else None

src/sentry/services/hybrid_cloud/app/model.py

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class RpcSentryApp(RpcModel):
4444
uuid: str = ""
4545
events: List[str] = Field(default_factory=list)
4646
webhook_url: Optional[str] = None
47+
is_alertable: bool = False
4748
is_published: bool = False
4849
is_unpublished: bool = False
4950
is_internal: bool = True

src/sentry/services/hybrid_cloud/app/serial.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
from sentry.constants import SentryAppStatus
44
from sentry.models.apiapplication import ApiApplication
5+
from sentry.models.integrations import SentryAppComponent
56
from sentry.models.integrations.sentry_app import SentryApp
67
from sentry.models.integrations.sentry_app_installation import SentryAppInstallation
78
from sentry.services.hybrid_cloud.app import (
89
RpcApiApplication,
910
RpcSentryApp,
11+
RpcSentryAppComponent,
1012
RpcSentryAppInstallation,
1113
)
1214

@@ -34,11 +36,12 @@ def serialize_sentry_app(app: SentryApp) -> RpcSentryApp:
3436
uuid=app.uuid,
3537
events=app.events,
3638
webhook_url=app.webhook_url,
39+
is_alertable=app.is_alertable,
3740
is_published=app.status == SentryAppStatus.PUBLISHED,
3841
is_unpublished=app.status == SentryAppStatus.UNPUBLISHED,
3942
is_internal=app.status == SentryAppStatus.INTERNAL,
4043
is_publish_request_inprogress=app.status == SentryAppStatus.PUBLISH_REQUEST_INPROGRESS,
41-
status=app.status,
44+
status=SentryAppStatus.as_str(app.status),
4245
)
4346

4447

@@ -58,3 +61,12 @@ def serialize_sentry_app_installation(
5861
uuid=installation.uuid,
5962
api_token=installation.api_token.token if installation.api_token else None,
6063
)
64+
65+
66+
def serialize_sentry_app_component(component: SentryAppComponent) -> RpcSentryAppComponent:
67+
return RpcSentryAppComponent(
68+
uuid=str(component.uuid),
69+
sentry_app_id=component.sentry_app_id,
70+
type=component.type,
71+
app_schema=component.schema,
72+
)

src/sentry/services/hybrid_cloud/app/service.py

+7
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,12 @@ def create_internal_integration_for_channel_request(
145145
) -> RpcSentryAppInstallation:
146146
pass
147147

148+
@rpc_method
149+
@abc.abstractmethod
150+
def prepare_sentry_app_components(
151+
self, *, installation_id: int, component_type: str, project_slug: Optional[str] = None
152+
) -> Optional[RpcSentryAppComponent]:
153+
pass
154+
148155

149156
app_service = cast(AppService, AppService.create_delegation())

tests/sentry/incidents/endpoints/test_organization_alert_rule_available_action_index.py

+33-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
from typing import Any, Mapping
2+
13
from sentry.constants import ObjectStatus, SentryAppStatus
24
from sentry.incidents.endpoints.organization_alert_rule_available_action_index import (
35
build_action_response,
46
)
57
from sentry.incidents.models import AlertRuleTriggerAction
8+
from sentry.models.integrations import SentryAppComponent, SentryAppInstallation
69
from sentry.models.integrations.integration import Integration
710
from sentry.models.integrations.organization_integration import OrganizationIntegration
11+
from sentry.services.hybrid_cloud.app.serial import serialize_sentry_app_installation
812
from sentry.silo import SiloMode
913
from sentry.testutils.cases import APITestCase
1014
from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
@@ -25,7 +29,7 @@
2529
}
2630

2731

28-
@region_silo_test
32+
@region_silo_test(stable=True)
2933
class OrganizationAlertRuleAvailableActionIndexEndpointTest(APITestCase):
3034
endpoint = "sentry-api-0-organization-alert-rule-available-actions"
3135
email = AlertRuleTriggerAction.get_registered_type(AlertRuleTriggerAction.Type.EMAIL)
@@ -38,7 +42,7 @@ def setUp(self):
3842
super().setUp()
3943
self.login_as(self.user)
4044

41-
def install_new_sentry_app(self, name, **kwargs):
45+
def install_new_sentry_app(self, name, **kwargs) -> SentryAppInstallation:
4246
kwargs.update(
4347
name=name, organization=self.organization, is_alertable=True, verify_install=False
4448
)
@@ -109,12 +113,30 @@ def test_build_action_response_pagerduty(self):
109113
def test_build_action_response_sentry_app(self):
110114
installation = self.install_new_sentry_app("foo")
111115

112-
data = build_action_response(self.sentry_app, sentry_app_installation=installation)
116+
data = build_action_response(
117+
self.sentry_app, sentry_app_installation=serialize_sentry_app_installation(installation)
118+
)
113119

114120
assert data["type"] == "sentry_app"
115121
assert data["allowedTargetTypes"] == ["sentry_app"]
116122
assert data["status"] == SentryAppStatus.UNPUBLISHED_STR
117123

124+
def test_build_action_response_sentry_app_with_component(self):
125+
installation = self.install_new_sentry_app("foo")
126+
test_settings: Mapping[str, Any] = {"test-settings": []}
127+
with assume_test_silo_mode(SiloMode.CONTROL):
128+
SentryAppComponent.objects.create(
129+
sentry_app=installation.sentry_app,
130+
type="alert-rule-action",
131+
schema={"settings": test_settings},
132+
)
133+
134+
data = build_action_response(
135+
self.sentry_app, sentry_app_installation=serialize_sentry_app_installation(installation)
136+
)
137+
138+
assert data["settings"] == test_settings
139+
118140
def test_no_integrations(self):
119141
with self.feature("organizations:incidents"):
120142
response = self.get_success_response(self.organization.slug)
@@ -180,7 +202,10 @@ def test_sentry_apps(self):
180202
assert len(response.data) == 2
181203
assert build_action_response(self.email) in response.data
182204
assert (
183-
build_action_response(self.sentry_app, sentry_app_installation=installation)
205+
build_action_response(
206+
self.sentry_app,
207+
sentry_app_installation=serialize_sentry_app_installation(installation),
208+
)
184209
in response.data
185210
)
186211

@@ -193,7 +218,10 @@ def test_published_sentry_apps(self):
193218

194219
assert len(response.data) == 2
195220
assert (
196-
build_action_response(self.sentry_app, sentry_app_installation=installation)
221+
build_action_response(
222+
self.sentry_app,
223+
sentry_app_installation=serialize_sentry_app_installation(installation),
224+
)
197225
in response.data
198226
)
199227

0 commit comments

Comments
 (0)