diff --git a/src/sentry/api/endpoints/project_rule_details.py b/src/sentry/api/endpoints/project_rule_details.py index 0559c04d52fe0f..f1950b2c85a94c 100644 --- a/src/sentry/api/endpoints/project_rule_details.py +++ b/src/sentry/api/endpoints/project_rule_details.py @@ -33,8 +33,8 @@ from sentry.integrations.slack.utils.rule_status import RedisRuleStatus from sentry.models.rule import NeglectedRule, RuleActivity, RuleActivityType from sentry.projects.project_rules.updater import ProjectRuleUpdater -from sentry.rules.actions import trigger_sentry_app_action_creators_for_issues from sentry.rules.actions.utils import get_changed_data, get_updated_rule_data +from sentry.sentry_apps.utils.alert_rule_action import create_sentry_app_alert_rule_issues_component from sentry.signals import alert_rule_edited from sentry.types.actor import Actor from sentry.utils import metrics @@ -285,7 +285,9 @@ def put(self, request: Request, project, rule) -> Response: context = {"uuid": client.uuid} return Response(context, status=202) - trigger_sentry_app_action_creators_for_issues(actions=kwargs["actions"]) + result = create_sentry_app_alert_rule_issues_component(actions=kwargs["actions"]) + if isinstance(result, Response): + return result updated_rule = ProjectRuleUpdater( rule=rule, diff --git a/src/sentry/api/endpoints/project_rules.py b/src/sentry/api/endpoints/project_rules.py index a7b86baf086b1f..a7a2a7072b541f 100644 --- a/src/sentry/api/endpoints/project_rules.py +++ b/src/sentry/api/endpoints/project_rules.py @@ -29,9 +29,9 @@ from sentry.integrations.slack.utils.rule_status import RedisRuleStatus from sentry.models.rule import Rule, RuleActivity, RuleActivityType from sentry.projects.project_rules.creator import ProjectRuleCreator -from sentry.rules.actions import trigger_sentry_app_action_creators_for_issues from sentry.rules.actions.base import instantiate_action from sentry.rules.processing.processor import is_condition_slow +from sentry.sentry_apps.utils.alert_rule_action import create_sentry_app_alert_rule_issues_component from sentry.signals import alert_rule_created from sentry.utils import metrics @@ -841,9 +841,12 @@ def post(self, request: Request, project) -> Response: find_channel_id_for_rule.apply_async(kwargs=kwargs) return Response(uuid_context, status=202) - created_alert_rule_ui_component = trigger_sentry_app_action_creators_for_issues( + created_alert_rule_ui_component = create_sentry_app_alert_rule_issues_component( kwargs["actions"] ) + if isinstance(created_alert_rule_ui_component, Response): + return created_alert_rule_ui_component + rule = ProjectRuleCreator( name=kwargs["name"], project=project, diff --git a/src/sentry/incidents/endpoints/organization_alert_rule_details.py b/src/sentry/incidents/endpoints/organization_alert_rule_details.py index 0aeea3a1fb0df0..5e956fbd1d939b 100644 --- a/src/sentry/incidents/endpoints/organization_alert_rule_details.py +++ b/src/sentry/incidents/endpoints/organization_alert_rule_details.py @@ -29,13 +29,15 @@ get_slack_actions_with_async_lookups, ) from sentry.incidents.serializers import AlertRuleSerializer as DrfAlertRuleSerializer -from sentry.incidents.utils.sentry_apps import trigger_sentry_app_action_creators_for_incidents from sentry.integrations.slack.tasks.find_channel_id_for_alert_rule import ( find_channel_id_for_alert_rule, ) from sentry.integrations.slack.utils.rule_status import RedisRuleStatus from sentry.models.rulesnooze import RuleSnooze from sentry.sentry_apps.services.app import app_service +from sentry.sentry_apps.utils.alert_rule_action import ( + create_sentry_app_alert_rule_component_for_incidents, +) from sentry.users.services.user.service import user_service @@ -82,7 +84,12 @@ def update_alert_rule(request: Request, organization, alert_rule): partial=True, ) if serializer.is_valid(): - trigger_sentry_app_action_creators_for_incidents(serializer.validated_data) + raised_error = create_sentry_app_alert_rule_component_for_incidents( + serializer.validated_data + ) + if raised_error: + return raised_error + if get_slack_actions_with_async_lookups(organization, request.user, data): # need to kick off an async job for Slack client = RedisRuleStatus() diff --git a/src/sentry/incidents/endpoints/organization_alert_rule_index.py b/src/sentry/incidents/endpoints/organization_alert_rule_index.py index db45347717d093..eb512447984899 100644 --- a/src/sentry/incidents/endpoints/organization_alert_rule_index.py +++ b/src/sentry/incidents/endpoints/organization_alert_rule_index.py @@ -41,7 +41,6 @@ from sentry.incidents.models.alert_rule import AlertRule from sentry.incidents.models.incident import Incident, IncidentStatus from sentry.incidents.serializers import AlertRuleSerializer as DrfAlertRuleSerializer -from sentry.incidents.utils.sentry_apps import trigger_sentry_app_action_creators_for_incidents from sentry.integrations.slack.tasks.find_channel_id_for_alert_rule import ( find_channel_id_for_alert_rule, ) @@ -52,6 +51,9 @@ from sentry.models.rule import Rule, RuleSource from sentry.models.team import Team from sentry.sentry_apps.services.app import app_service +from sentry.sentry_apps.utils.alert_rule_action import ( + create_sentry_app_alert_rule_component_for_incidents, +) from sentry.snuba.dataset import Dataset from sentry.snuba.models import SnubaQuery from sentry.uptime.models import ( @@ -120,7 +122,12 @@ def create_metric_alert( if not serializer.is_valid(): raise ValidationError(serializer.errors) - trigger_sentry_app_action_creators_for_incidents(serializer.validated_data) + raised_error = create_sentry_app_alert_rule_component_for_incidents( + serializer.validated_data + ) + if raised_error: + return raised_error + if get_slack_actions_with_async_lookups(organization, request.user, request.data): # need to kick off an async job for Slack client = RedisRuleStatus() diff --git a/src/sentry/incidents/utils/sentry_apps.py b/src/sentry/incidents/utils/sentry_apps.py deleted file mode 100644 index e7fd63be4f094f..00000000000000 --- a/src/sentry/incidents/utils/sentry_apps.py +++ /dev/null @@ -1,36 +0,0 @@ -from collections.abc import Mapping -from typing import Any - -from rest_framework import serializers - -from sentry.auth.access import NoAccess -from sentry.incidents.logic import get_filtered_actions -from sentry.incidents.models.alert_rule import AlertRuleTriggerAction -from sentry.incidents.serializers import AlertRuleTriggerActionSerializer -from sentry.sentry_apps.services.app import app_service - - -def trigger_sentry_app_action_creators_for_incidents(alert_rule_data: Mapping[str, Any]) -> None: - sentry_app_actions = get_filtered_actions( - alert_rule_data=alert_rule_data, - action_type=AlertRuleTriggerAction.Type.SENTRY_APP, - ) - # We're doing this so that Sentry Apps without alert-rule-action schemas still get saved - sentry_app_actions_with_components = list( - filter(lambda x: x.get("sentry_app_config"), sentry_app_actions) - ) - - for action in sentry_app_actions_with_components: - action_serializer = AlertRuleTriggerActionSerializer( - context={"access": NoAccess()}, - data=action, - ) - if not action_serializer.is_valid(): - raise serializers.ValidationError(action_serializer.errors) - - result = app_service.trigger_sentry_app_action_creators( - fields=action.get("sentry_app_config"), - install_uuid=action.get("sentry_app_installation_uuid"), - ) - if not result.success: - raise serializers.ValidationError({"sentry_app": result.message}) diff --git a/src/sentry/rules/actions/__init__.py b/src/sentry/rules/actions/__init__.py index 7cee5b8db4a817..2f567f43ef91d1 100644 --- a/src/sentry/rules/actions/__init__.py +++ b/src/sentry/rules/actions/__init__.py @@ -4,12 +4,10 @@ IntegrationNotifyServiceForm, TicketEventAction, ) -from sentry.rules.actions.sentry_apps import trigger_sentry_app_action_creators_for_issues __all__ = ( "EventAction", "IntegrationEventAction", "IntegrationNotifyServiceForm", "TicketEventAction", - "trigger_sentry_app_action_creators_for_issues", ) diff --git a/src/sentry/rules/actions/sentry_apps/__init__.py b/src/sentry/rules/actions/sentry_apps/__init__.py index aaa63e984888dd..97adb9e9201ced 100644 --- a/src/sentry/rules/actions/sentry_apps/__init__.py +++ b/src/sentry/rules/actions/sentry_apps/__init__.py @@ -1,9 +1,7 @@ from .base import SentryAppEventAction from .notify_event import NotifyEventSentryAppAction -from .utils import trigger_sentry_app_action_creators_for_issues __all__ = ( "NotifyEventSentryAppAction", "SentryAppEventAction", - "trigger_sentry_app_action_creators_for_issues", ) diff --git a/src/sentry/rules/actions/sentry_apps/utils.py b/src/sentry/rules/actions/sentry_apps/utils.py deleted file mode 100644 index fa23dda81fad38..00000000000000 --- a/src/sentry/rules/actions/sentry_apps/utils.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -from collections.abc import Mapping, Sequence -from typing import Any - -from rest_framework import serializers - -from sentry.constants import SENTRY_APP_ACTIONS -from sentry.sentry_apps.services.app import app_service - - -def trigger_sentry_app_action_creators_for_issues( - actions: Sequence[Mapping[str, Any]] -) -> str | None: - created = None - for action in actions: - # Only call creator for Sentry Apps with UI Components for alert rules. - if not action.get("id") in SENTRY_APP_ACTIONS: - continue - - result = app_service.trigger_sentry_app_action_creators( - fields=action["settings"], install_uuid=action.get("sentryAppInstallationUuid") - ) - # Bubble up errors from Sentry App to the UI - if not result.success: - raise serializers.ValidationError({"actions": [result.message]}) - created = "alert-rule-action" - return created diff --git a/src/sentry/sentry_apps/alert_rule_action_creator.py b/src/sentry/sentry_apps/alert_rule_action_creator.py index cd7e1632d297ec..9652294bc2181e 100644 --- a/src/sentry/sentry_apps/alert_rule_action_creator.py +++ b/src/sentry/sentry_apps/alert_rule_action_creator.py @@ -10,8 +10,10 @@ AlertRuleActionRequester, AlertRuleActionResult, ) +from sentry.sentry_apps.models.sentry_app import SentryApp from sentry.sentry_apps.models.sentry_app_component import SentryAppComponent from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation +from sentry.sentry_apps.utils.errors import SentryAppIntegratorError @dataclass @@ -25,16 +27,17 @@ def run(self) -> AlertRuleActionResult: response = self._make_external_request(uri) return response - def _fetch_sentry_app_uri(self): + def _fetch_sentry_app_uri(self) -> str: component = SentryAppComponent.objects.get( type="alert-rule-action", sentry_app=self.sentry_app ) settings = component.schema.get("settings", {}) - return settings.get("uri") + uri = settings.get("uri", None) + if not uri: + raise SentryAppIntegratorError(APIError("Sentry App request url not found on schema")) + return uri - def _make_external_request(self, uri=None): - if uri is None: - raise APIError("Sentry App request url not found") + def _make_external_request(self, uri: str) -> AlertRuleActionResult: response = AlertRuleActionRequester( install=self.install, uri=uri, @@ -44,5 +47,5 @@ def _make_external_request(self, uri=None): return response @cached_property - def sentry_app(self): + def sentry_app(self) -> SentryApp: return self.install.sentry_app diff --git a/src/sentry/sentry_apps/external_requests/alert_rule_action_requester.py b/src/sentry/sentry_apps/external_requests/alert_rule_action_requester.py index a9249af4849f63..82f73c1729bbb2 100644 --- a/src/sentry/sentry_apps/external_requests/alert_rule_action_requester.py +++ b/src/sentry/sentry_apps/external_requests/alert_rule_action_requester.py @@ -6,16 +6,17 @@ from uuid import uuid4 from django.utils.functional import cached_property -from requests import RequestException +from requests.exceptions import RequestException from requests.models import Response from sentry.sentry_apps.external_requests.utils import send_and_save_sentry_app_request from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation from sentry.sentry_apps.services.app.model import RpcSentryAppInstallation +from sentry.sentry_apps.utils.errors import SentryAppErrorType, SentryAppIntegratorError from sentry.utils import json DEFAULT_SUCCESS_MESSAGE = "Success!" -DEFAULT_ERROR_MESSAGE = "Something went wrong!" +DEFAULT_ERROR_MESSAGE = "Something went wrong while setting up alert for" logger = logging.getLogger("sentry.sentry_apps.external_requests") @@ -23,6 +24,7 @@ class AlertRuleActionResult(TypedDict): success: bool message: str + error_type: SentryAppErrorType | None @dataclass @@ -43,23 +45,23 @@ def run(self) -> AlertRuleActionResult: method=self.http_method, data=self.body, ) - except RequestException as e: - logger.info( - "alert_rule_action.error", - extra={ - "sentry_app_slug": self.sentry_app.slug, - "install_uuid": self.install.uuid, - "uri": self.uri, - "error_message": str(e), - }, - ) + self._log_exceptions(e) + raise SentryAppIntegratorError( + self._get_response_message( + e.response, f"{DEFAULT_ERROR_MESSAGE} {self.sentry_app.slug}" + ) + ) from e + + except Exception as e: + self._log_exceptions(e) + e.args = (f"{DEFAULT_ERROR_MESSAGE} {self.sentry_app.slug}",) + raise - return AlertRuleActionResult( - success=False, message=self._get_response_message(e.response, DEFAULT_ERROR_MESSAGE) - ) return AlertRuleActionResult( - success=True, message=self._get_response_message(response, DEFAULT_SUCCESS_MESSAGE) + success=True, + error_type=None, + message=self._get_response_message(response, DEFAULT_SUCCESS_MESSAGE), ) def _build_url(self) -> str: @@ -92,6 +94,17 @@ def _get_response_message(self, response: Response | None, default_message: str) return f"{self.sentry_app.name}: {message}" + def _log_exceptions(self, error: Exception) -> None: + logger.info( + "alert_rule_action.error", + extra={ + "sentry_app_slug": self.sentry_app.slug, + "install_uuid": self.install.uuid, + "uri": self.uri, + "error_message": str(error), + }, + ) + @cached_property def body(self): return json.dumps( diff --git a/src/sentry/sentry_apps/models/sentry_app_installation.py b/src/sentry/sentry_apps/models/sentry_app_installation.py index 62cfb87dac734f..55a04c45b4de96 100644 --- a/src/sentry/sentry_apps/models/sentry_app_installation.py +++ b/src/sentry/sentry_apps/models/sentry_app_installation.py @@ -19,6 +19,7 @@ from sentry.hybridcloud.outbox.category import OutboxCategory from sentry.projects.services.project import RpcProject from sentry.sentry_apps.services.app.model import RpcSentryAppComponent, RpcSentryAppInstallation +from sentry.sentry_apps.utils.errors import SentryAppError, SentryAppIntegratorError from sentry.types.region import find_regions_for_orgs if TYPE_CHECKING: @@ -240,6 +241,6 @@ def prepare_ui_component( component=component, install=installation, project_slug=project_slug, values=values ).run() return component - except (APIError, ValidationError): + except (APIError, ValidationError, SentryAppError, SentryAppIntegratorError): # TODO(nisanthan): For now, skip showing the UI Component if the API requests fail return None diff --git a/src/sentry/sentry_apps/services/app/impl.py b/src/sentry/sentry_apps/services/app/impl.py index e266283516b35e..443939243897d9 100644 --- a/src/sentry/sentry_apps/services/app/impl.py +++ b/src/sentry/sentry_apps/services/app/impl.py @@ -24,7 +24,6 @@ from sentry.sentry_apps.models.sentry_app_installation_token import SentryAppInstallationToken from sentry.sentry_apps.services.app import ( AppService, - RpcAlertRuleActionResult, RpcSentryApp, RpcSentryAppComponent, RpcSentryAppComponentContext, @@ -33,11 +32,17 @@ RpcSentryAppService, SentryAppInstallationFilterArgs, ) +from sentry.sentry_apps.services.app.model import RpcAlertRuleActionResult from sentry.sentry_apps.services.app.serial import ( serialize_sentry_app, serialize_sentry_app_component, serialize_sentry_app_installation, ) +from sentry.sentry_apps.utils.errors import ( + SentryAppError, + SentryAppErrorType, + SentryAppIntegratorError, +) from sentry.users.models.user import User from sentry.users.services.user import RpcUser @@ -253,10 +258,19 @@ def trigger_sentry_app_action_creators( ) -> RpcAlertRuleActionResult: try: install = SentryAppInstallation.objects.get(uuid=install_uuid) - except SentryAppInstallation.DoesNotExist: - return RpcAlertRuleActionResult(success=False, message="Installation does not exist") - result = AlertRuleActionCreator(install=install, fields=fields).run() - return RpcAlertRuleActionResult(success=result["success"], message=result["message"]) + result = AlertRuleActionCreator(install=install, fields=fields).run() + except (SentryAppError, SentryAppIntegratorError) as e: + return RpcAlertRuleActionResult(success=False, error_type=e.error_type, message=str(e)) + except Exception as e: + return RpcAlertRuleActionResult( + success=False, + error_type=SentryAppErrorType.SENTRY, + message=str(e), + ) + + return RpcAlertRuleActionResult( + success=result["success"], error_type=None, message=result["message"] + ) def find_service_hook_sentry_app(self, *, api_application_id: int) -> RpcSentryApp | None: try: diff --git a/src/sentry/sentry_apps/services/app/model.py b/src/sentry/sentry_apps/services/app/model.py index 20609618b50841..3a873a083c5558 100644 --- a/src/sentry/sentry_apps/services/app/model.py +++ b/src/sentry/sentry_apps/services/app/model.py @@ -98,8 +98,11 @@ class RpcSentryAppComponentContext(RpcModel): class RpcAlertRuleActionResult(RpcModel): + from sentry.sentry_apps.utils.errors import SentryAppErrorType + success: bool message: str + error_type: SentryAppErrorType | None class SentryAppEventDataInterface(Protocol): diff --git a/src/sentry/sentry_apps/services/app/service.py b/src/sentry/sentry_apps/services/app/service.py index 7d8d8f466fcc60..757e0ca41094fa 100644 --- a/src/sentry/sentry_apps/services/app/service.py +++ b/src/sentry/sentry_apps/services/app/service.py @@ -11,16 +11,16 @@ from sentry.hybridcloud.rpc.caching.service import back_with_silo_cache, back_with_silo_cache_list from sentry.hybridcloud.rpc.filter_query import OpaqueSerializedResponse from sentry.hybridcloud.rpc.service import RpcService, rpc_method -from sentry.sentry_apps.services.app import ( +from sentry.sentry_apps.services.app.model import ( RpcAlertRuleActionResult, RpcSentryApp, RpcSentryAppComponent, + RpcSentryAppComponentContext, RpcSentryAppEventData, RpcSentryAppInstallation, RpcSentryAppService, SentryAppInstallationFilterArgs, ) -from sentry.sentry_apps.services.app.model import RpcSentryAppComponentContext from sentry.silo.base import SiloMode from sentry.users.services.user import RpcUser diff --git a/src/sentry/sentry_apps/utils/alert_rule_action.py b/src/sentry/sentry_apps/utils/alert_rule_action.py new file mode 100644 index 00000000000000..9aeecce7c224fd --- /dev/null +++ b/src/sentry/sentry_apps/utils/alert_rule_action.py @@ -0,0 +1,117 @@ +from collections.abc import Mapping, Sequence +from typing import Any + +import sentry_sdk +from rest_framework import serializers, status +from rest_framework.response import Response + +from sentry.auth.access import NoAccess +from sentry.constants import SENTRY_APP_ACTIONS +from sentry.incidents.logic import get_filtered_actions +from sentry.incidents.models.alert_rule import AlertRuleTriggerAction +from sentry.incidents.serializers import AlertRuleTriggerActionSerializer +from sentry.sentry_apps.services.app import app_service +from sentry.sentry_apps.services.app.model import RpcAlertRuleActionResult +from sentry.sentry_apps.utils.errors import ( + SentryAppError, + SentryAppErrorType, + SentryAppIntegratorError, +) + + +def raise_alert_rule_action_result_errors(result: RpcAlertRuleActionResult) -> None: + if result.error_type is None: + return None + + error_type = SentryAppErrorType(result.error_type) + match error_type: + case SentryAppErrorType.INTEGRATOR: + raise SentryAppIntegratorError(result.message) + case SentryAppErrorType.CLIENT: + raise SentryAppError(result.message) + case SentryAppErrorType.SENTRY: + raise Exception(result.message) + + +def create_sentry_app_alert_rule_component_for_incidents( + serialized_data: Mapping[str, Any] +) -> Response | None: + try: + trigger_sentry_app_action_creators_for_incidents(serialized_data) + except (SentryAppError, SentryAppIntegratorError) as e: + return Response( + str(e), + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + error_id = sentry_sdk.capture_exception(e) + return Response( + f"Something went wrong while trying to create alert rule action. Sentry error ID: {error_id}", + status=500, + ) + return None + + +def create_sentry_app_alert_rule_issues_component( + actions: Sequence[Mapping[str, Any]] +) -> str | Response | None: + try: + created = trigger_sentry_app_action_creators_for_issues(actions) + + except (SentryAppError, SentryAppIntegratorError) as e: + return Response( + {"actions": [str(e)]}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + error_id = sentry_sdk.capture_exception(e) + return Response( + { + "actions": [ + f"Something went wrong while trying to create alert rule action. Sentry error ID: {error_id}" + ] + }, + status=500, + ) + return created + + +def trigger_sentry_app_action_creators_for_incidents(alert_rule_data: Mapping[str, Any]) -> None: + sentry_app_actions = get_filtered_actions( + alert_rule_data=alert_rule_data, + action_type=AlertRuleTriggerAction.Type.SENTRY_APP, + ) + # We're doing this so that Sentry Apps without alert-rule-action schemas still get saved + sentry_app_actions_with_components = list( + filter(lambda x: x.get("sentry_app_config"), sentry_app_actions) + ) + + for action in sentry_app_actions_with_components: + action_serializer = AlertRuleTriggerActionSerializer( + context={"access": NoAccess()}, + data=action, + ) + if not action_serializer.is_valid(): + raise serializers.ValidationError(action_serializer.errors) + result = app_service.trigger_sentry_app_action_creators( + fields=action.get("sentry_app_config"), + install_uuid=action.get("sentry_app_installation_uuid"), + ) + raise_alert_rule_action_result_errors(result) + + +def trigger_sentry_app_action_creators_for_issues( + actions: Sequence[Mapping[str, Any]] +) -> str | None: + created = None + for action in actions: + # Only call creator for Sentry Apps with UI Components for alert rules. + if not action.get("id") in SENTRY_APP_ACTIONS: + continue + + result = app_service.trigger_sentry_app_action_creators( + fields=action["settings"], install_uuid=action.get("sentryAppInstallationUuid") + ) + raise_alert_rule_action_result_errors(result=result) + created = "alert-rule-action" + return created diff --git a/src/sentry/sentry_apps/utils/errors.py b/src/sentry/sentry_apps/utils/errors.py new file mode 100644 index 00000000000000..68aba2dcaba5b1 --- /dev/null +++ b/src/sentry/sentry_apps/utils/errors.py @@ -0,0 +1,17 @@ +from enum import Enum + + +class SentryAppErrorType(Enum): + CLIENT = "client" + INTEGRATOR = "integrator" + SENTRY = "sentry" + + +# Represents a user/client error that occured during a Sentry App process +class SentryAppError(Exception): + error_type = SentryAppErrorType.CLIENT + + +# Represents an error caused by a 3p integrator during a Sentry App process +class SentryAppIntegratorError(Exception): + error_type = SentryAppErrorType.INTEGRATOR diff --git a/tests/sentry/api/endpoints/test_project_rules.py b/tests/sentry/api/endpoints/test_project_rules.py index 3f3772445a2018..b126d885ffa63e 100644 --- a/tests/sentry/api/endpoints/test_project_rules.py +++ b/tests/sentry/api/endpoints/test_project_rules.py @@ -1013,6 +1013,43 @@ def test_create_sentry_app_action_failure(self): assert len(responses.calls) == 1 assert error_message in response.json().get("actions")[0] + @patch("sentry_sdk.capture_exception") + @patch( + "sentry.sentry_apps.utils.alert_rule_action.trigger_sentry_app_action_creators_for_issues" + ) + def test_create_sentry_app_action_failure_500(self, error, exc_id): + actions = [ + { + "id": "sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction", + "settings": self.sentry_app_settings_payload, + "sentryAppInstallationUuid": self.sentry_app_installation.uuid, + "hasSchemaFormConfig": True, + }, + ] + payload = { + "name": "my super cool rule", + "owner": f"user:{self.user.id}", + "conditions": [], + "filters": [], + "actions": actions, + "filterMatch": "any", + "actionMatch": "any", + "frequency": 30, + } + exc_id.return_value = 1 + error.side_effect = Exception("ZOINKS") + response = self.get_error_response( + self.organization.slug, + self.project.slug, + **payload, + status_code=500, + ) + + assert ( + f"Something went wrong while trying to create alert rule action. Sentry error ID: {exc_id.return_value}" + in response.json().get("actions")[0] + ) + def test_post_rule_256_char_name(self): char_256_name = "wOOFmsWY80o0RPrlsrrqDp2Ylpr5K2unBWbsrqvuNb4Fy3vzawkNAyFJdqeFLlXNWF2kMfgMT9EQmFF3u3MqW3CTI7L2SLsmS9uSDQtcinjlZrr8BT4v8Q6ySrVY5HmiFO97w3awe4lA8uyVikeaSwPjt8MD5WSjdTI0RRXYeK3qnHTpVswBe9AIcQVMLKQXHgjulpsrxHc0DI0Vb8hKA4BhmzQXhYmAvKK26ZwCSjJurAODJB6mgIdlV7tigsFO" response = self.get_success_response( diff --git a/tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py b/tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py index edf682e7591127..c4f60376189422 100644 --- a/tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py +++ b/tests/sentry/incidents/endpoints/test_organization_alert_rule_details.py @@ -50,6 +50,10 @@ from sentry.seer.anomaly_detection.store_data import seer_anomaly_detection_connection_pool from sentry.seer.anomaly_detection.types import StoreDataResponse from sentry.sentry_apps.services.app import app_service +from sentry.sentry_apps.utils.alert_rule_action import ( + trigger_sentry_app_action_creators_for_incidents, +) +from sentry.sentry_apps.utils.errors import SentryAppIntegratorError from sentry.silo.base import SiloMode from sentry.testutils.abstract import Abstract from sentry.testutils.helpers.features import with_feature @@ -1692,7 +1696,132 @@ def test_error_response_from_sentry_app(self): resp = self.get_response(self.organization.slug, self.alert_rule.id, **test_params) assert resp.status_code == 400 - assert error_message in resp.data["sentry_app"] + assert error_message in resp.data + + @responses.activate + def test_trigger_sentry_app_action_creators_for_incidents(self): + self.create_member( + user=self.user, organization=self.organization, role="owner", teams=[self.team] + ) + self.login_as(self.user) + error_message = "Everything is broken!" + responses.add( + method=responses.POST, + url="https://example.com/sentry/alert-rule", + status=500, + json={"message": error_message}, + ) + + sentry_app = self.create_sentry_app( + name="foo", + organization=self.organization, + schema={ + "elements": [ + self.create_alert_rule_action_schema(), + ] + }, + ) + install = self.create_sentry_app_installation( + slug="foo", organization=self.organization, user=self.user + ) + + sentry_app_settings = [ + {"name": "title", "value": "test title"}, + {"name": "description", "value": "test description"}, + ] + + test_params = self.valid_params.copy() + test_params["triggers"] = [ + { + "actions": [ + { + "type": "sentry_app", + "targetType": "sentry_app", + "targetIdentifier": sentry_app.id, + "hasSchemaFormConfig": True, + "sentryAppId": sentry_app.id, + "sentryAppInstallationUuid": install.uuid, + "settings": sentry_app_settings, + } + ], + "alertThreshold": 300, + "label": "critical", + } + ] + + serializer = AlertRuleSerializer( + context={ + "organization": self.organization, + "access": OrganizationGlobalAccess(self.organization, settings.SENTRY_SCOPES), + "user": self.user, + "installations": app_service.installations_for_organization( + organization_id=self.organization.id + ), + }, + data=test_params, + ) + assert serializer.is_valid() + + with pytest.raises(SentryAppIntegratorError) as e: + trigger_sentry_app_action_creators_for_incidents(serializer.validated_data) + assert str(e) == f"{sentry_app.slug}: {error_message}" + + @patch("sentry_sdk.capture_exception") + @patch( + "sentry.sentry_apps.utils.alert_rule_action.trigger_sentry_app_action_creators_for_incidents" + ) + def test_error_response_from_sentry_app_500(self, error, sentry): + self.create_member( + user=self.user, organization=self.organization, role="owner", teams=[self.team] + ) + self.login_as(self.user) + sentry_app = self.create_sentry_app( + name="foo", + organization=self.organization, + schema={ + "elements": [ + self.create_alert_rule_action_schema(), + ] + }, + ) + install = self.create_sentry_app_installation( + slug="foo", organization=self.organization, user=self.user + ) + + sentry_app_settings = [ + {"name": "title", "value": "test title"}, + {"name": "description", "value": "test description"}, + ] + + test_params = self.valid_params.copy() + test_params["triggers"] = [ + { + "actions": [ + { + "type": "sentry_app", + "targetType": "sentry_app", + "targetIdentifier": sentry_app.id, + "hasSchemaFormConfig": True, + "sentryAppId": sentry_app.id, + "sentryAppInstallationUuid": install.uuid, + "settings": sentry_app_settings, + } + ], + "alertThreshold": 300, + "label": "critical", + } + ] + + error.side_effect = Exception("wowzers") + sentry.return_value = 1 + with self.feature(["organizations:incidents", "organizations:performance-view"]): + resp = self.get_response(self.organization.slug, self.alert_rule.id, **test_params) + + assert resp.status_code == 500 + assert ( + resp.data + == f"Something went wrong while trying to create alert rule action. Sentry error ID: {sentry.return_value}" + ) class AlertRuleDetailsDeleteEndpointTest(AlertRuleDetailsBase): diff --git a/tests/sentry/incidents/endpoints/test_organization_alert_rule_index.py b/tests/sentry/incidents/endpoints/test_organization_alert_rule_index.py index 444d9bb6648b0c..f3de456356e7a8 100644 --- a/tests/sentry/incidents/endpoints/test_organization_alert_rule_index.py +++ b/tests/sentry/incidents/endpoints/test_organization_alert_rule_index.py @@ -807,7 +807,7 @@ def test_error_response_from_sentry_app(self): resp = self.get_response(self.organization.slug, **alert_rule) assert resp.status_code == 400 - assert error_message in resp.data["sentry_app"] + assert error_message in resp.data def test_no_label(self): rule_one_trigger_no_label = { diff --git a/tests/sentry/sentry_apps/external_requests/test_alert_rule_action_requester.py b/tests/sentry/sentry_apps/external_requests/test_alert_rule_action_requester.py index b728a3d78309b1..c381c781f30408 100644 --- a/tests/sentry/sentry_apps/external_requests/test_alert_rule_action_requester.py +++ b/tests/sentry/sentry_apps/external_requests/test_alert_rule_action_requester.py @@ -1,5 +1,6 @@ from collections.abc import Mapping +import pytest import responses from sentry.sentry_apps.external_requests.alert_rule_action_requester import ( @@ -7,6 +8,7 @@ DEFAULT_SUCCESS_MESSAGE, AlertRuleActionRequester, ) +from sentry.sentry_apps.utils.errors import SentryAppIntegratorError from sentry.testutils.cases import TestCase from sentry.testutils.silo import control_silo_test from sentry.utils import json @@ -122,13 +124,14 @@ def test_makes_failed_request(self): status=401, ) - result = AlertRuleActionRequester( - install=self.install, - uri="/sentry/alert-rule", - fields=self.fields, - ).run() - assert not result["success"] - assert result["message"] == f"{self.sentry_app.name}: {DEFAULT_ERROR_MESSAGE}" + with pytest.raises(SentryAppIntegratorError) as e: + AlertRuleActionRequester( + install=self.install, + uri="/sentry/alert-rule", + fields=self.fields, + ).run() + assert str(e) == f"{DEFAULT_ERROR_MESSAGE} {self.sentry_app.slug}" + request = responses.calls[0].request data = { @@ -164,13 +167,14 @@ def test_makes_failed_request_with_message(self): status=401, json={"message": self.error_message}, ) - result = AlertRuleActionRequester( - install=self.install, - uri="/sentry/alert-rule", - fields=self.fields, - ).run() - assert not result["success"] - assert result["message"] == f"{self.sentry_app.name}: {self.error_message}" + with pytest.raises(SentryAppIntegratorError) as e: + AlertRuleActionRequester( + install=self.install, + uri="/sentry/alert-rule", + fields=self.fields, + ).run() + + assert str(e) == f"{self.sentry_app.name}: {self.error_message}" @responses.activate def test_makes_failed_request_with_malformed_message(self): @@ -180,10 +184,11 @@ def test_makes_failed_request_with_malformed_message(self): status=401, body=self.error_message.encode(), ) - result = AlertRuleActionRequester( - install=self.install, - uri="/sentry/alert-rule", - fields=self.fields, - ).run() - assert not result["success"] - assert result["message"] == f"{self.sentry_app.name}: {DEFAULT_ERROR_MESSAGE}" + + with pytest.raises(SentryAppIntegratorError) as e: + AlertRuleActionRequester( + install=self.install, + uri="/sentry/alert-rule", + fields=self.fields, + ).run() + assert str(e) == f"{DEFAULT_ERROR_MESSAGE} {self.sentry_app.slug}"