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 @@ -20,6 +20,7 @@
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.integrations.services.integration import RpcIntegration
from sentry.rules.actions.services import PluginService, SentryAppService
from sentry.sentry_apps.models.sentry_app_installation import prepare_ui_component
from sentry.sentry_apps.services.app import app_service
from sentry.workflow_engine.endpoints.serializers.action_handler_serializer import (
ActionHandlerSerializer,
Expand Down Expand Up @@ -77,19 +78,29 @@ def get(self, request, organization):
AvailableIntegration(integration=integration, services=services)
)

sentry_app_component_contexts = app_service.get_installation_component_contexts(
all_sentry_app_contexts = app_service.get_installation_component_contexts(
filter={"organization_id": organization.id},
component_type="alert-rule-action",
include_contexts_without_component=True,
)

# Split contexts into those with and without components
sentry_app_contexts_with_components = [
context for context in sentry_app_component_contexts if context.component
]
sentry_app_contexts_without_components = [
context for context in sentry_app_component_contexts if not context.component
]
# filter for alertable apps and split contexts into those with and without components
alertable_apps_with_components = []
alertable_apps_without_components = []
for context in all_sentry_app_contexts:
if not context.installation.sentry_app.is_alertable:
continue

if context.component:
# filter out broken apps by checking if prepare_ui_component succeeds
prepared_component = prepare_ui_component(
installation=context.installation,
component=context.component,
)
Comment on lines +96 to +99
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this component preparation code make any network requests at all?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summarizing a conversation we had in Slack:
This potentially calls the downstream Sentry App's endpoint, which can add a ton of additional latency, or fallibility.

This is ok for now, but we should consider moving this call into its own API, and letting the UI make this call on-demand to retrieve just the UI component that it needs when the user selects the Sentry app as a target and has to configure it.

Additionally, this call is repeated in the serializer below, so we're removing that other call and hoisting it here for now while @ameliahsu works on the API and UI changes.

if prepared_component is not None:
alertable_apps_with_components.append(context)
else:
alertable_apps_without_components.append(context)

actions = []
for action_type, handler in action_handler_registry.registrations.items():
Expand All @@ -112,27 +123,26 @@ def get(self, request, organization):
)

# add alertable sentry app actions
# sentry app actions are only for sentry apps with components
# sentry app actions are only for alertable sentry apps with components
elif action_type == Action.Type.SENTRY_APP:
for context in sentry_app_contexts_with_components:
if context.installation.sentry_app.is_alertable:
actions.append(
serialize(
handler,
request.user,
ActionHandlerSerializer(),
action_type=action_type,
sentry_app_context=context,
)
for context in alertable_apps_with_components:
actions.append(
serialize(
handler,
request.user,
ActionHandlerSerializer(),
action_type=action_type,
sentry_app_context=context,
)
)

# add webhook action
# service options include plugins and sentry apps without components
elif action_type == Action.Type.WEBHOOK:
plugins = get_notification_plugins_for_org(organization)
sentry_apps: list[PluginService] = [
SentryAppService(context.installation.sentry_app)
for context in sentry_app_contexts_without_components
for context in alertable_apps_without_components
]
available_services: list[PluginService] = plugins + sentry_apps

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from sentry.api.serializers import Serializer, register
from sentry.rules.actions.notify_event_service import PLUGINS_WITH_FIRST_PARTY_EQUIVALENTS
from sentry.sentry_apps.models.sentry_app_installation import prepare_ui_component
from sentry.workflow_engine.types import ActionHandler


Expand Down Expand Up @@ -76,16 +75,9 @@ def serialize(
"status": installation.sentry_app.status,
}
if component:
prepared_component = prepare_ui_component(
installation=installation,
component=component,
project_slug=None,
values=None,
)
if prepared_component:
sentry_app["settings"] = prepared_component.app_schema.get("settings", {})
if prepared_component.app_schema.get("title"):
sentry_app["title"] = prepared_component.app_schema.get("title")
sentry_app["settings"] = component.app_schema.get("settings", {})
if component.app_schema.get("title"):
sentry_app["title"] = component.app_schema.get("title")
result["sentryApp"] = sentry_app

services = kwargs.get("services")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,32 @@ class SentryAppActionHandler(ActionHandler):
slug=self.sentry_app.slug, organization=self.organization
)

# should not return sentry apps that are not alertable
self.not_alertable_sentry_app = self.create_sentry_app(
name="Not Alertable Sentry App",
organization=self.organization,
is_alertable=False,
)
self.not_alertable_sentry_app_installation = self.create_sentry_app_installation(
slug=self.not_alertable_sentry_app.slug, organization=self.organization
)

self.not_alertable_sentry_app = self.create_sentry_app(
name="Not Alertable Sentry App With Component",
organization=self.organization,
schema={
"elements": [
self.sentry_app_settings_schema,
]
},
is_alertable=False,
)
self.not_alertable_sentry_app_with_component_installation = (
self.create_sentry_app_installation(
slug=self.not_alertable_sentry_app.slug, organization=self.organization
)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Sentry App Overwrite Creates Orphan Installation

The self.not_alertable_sentry_app variable is reassigned, overwriting the first Sentry app object. This causes the initial Sentry app to be lost, leaving its associated installation (self.not_alertable_sentry_app_installation) orphaned. It looks like the setup intended to create two distinct non-alertable Sentry apps.

Fix in Cursor Fix in Web


# should not return sentry apps that are not installed
self.create_sentry_app(
name="Bad Sentry App",
Expand Down Expand Up @@ -388,6 +414,28 @@ def test_sentry_apps(self, mock_sentry_app_component_preparer: MagicMock) -> Non
},
]

@patch(
"sentry.workflow_engine.endpoints.organization_available_action_index.prepare_ui_component"
)
def test_sentry_apps_filters_failed_component_preparation(
self, mock_prepare_ui_component: MagicMock
) -> None:
"""Test that sentry apps whose components fail to prepare are filtered out"""
self.setup_sentry_apps()

# make prepare_ui_component return None to simulate a broken app
mock_prepare_ui_component.return_value = None

response = self.get_success_response(
self.organization.slug,
status_code=200,
)

# verify prepare_ui_component was called
assert mock_prepare_ui_component.called
# should return no sentry apps since component preparation failed
assert len(response.data) == 0

def test_webhooks(self) -> None:
self.setup_webhooks()

Expand Down
Loading