Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(sentry apps): Improve Errors for Alert Rule Creation for Sentry Apps #82067

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
6 changes: 4 additions & 2 deletions src/sentry/api/endpoints/project_rule_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"])
Copy link
Member

Choose a reason for hiding this comment

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

[2] Then wrap this in try catch and then in the catch, catch the exception and call a method like return IntegrationResponse.from_exception() where this method is a util that does all the work you're doing to turn exceptions into response.

If from_exception() needs to know the caller for some reason to parse the errors better you can pass that as a param.

if isinstance(result, Response):
Copy link
Member

Choose a reason for hiding this comment

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

It's not good practice to have different typed returned from a method and then decide what to do with it based on type. I have a suggestion below on how to improve this. Please read the comments annotated with [n] in the order on n.

return result

updated_rule = ProjectRuleUpdater(
rule=rule,
Expand Down
7 changes: 5 additions & 2 deletions src/sentry/api/endpoints/project_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

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

All comments applies to this method as well.

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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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 (
Expand Down Expand Up @@ -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()
Expand Down
36 changes: 0 additions & 36 deletions src/sentry/incidents/utils/sentry_apps.py

This file was deleted.

2 changes: 0 additions & 2 deletions src/sentry/rules/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
2 changes: 0 additions & 2 deletions src/sentry/rules/actions/sentry_apps/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
)
28 changes: 0 additions & 28 deletions src/sentry/rules/actions/sentry_apps/utils.py

This file was deleted.

15 changes: 9 additions & 6 deletions src/sentry/sentry_apps/alert_rule_action_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@
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")


class AlertRuleActionResult(TypedDict):
success: bool
message: str
error_type: SentryAppErrorType | None


@dataclass
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/sentry_apps/models/sentry_app_installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
24 changes: 19 additions & 5 deletions src/sentry/sentry_apps/services/app/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading