diff --git a/src/sentry/sentry_apps/tasks/sentry_apps.py b/src/sentry/sentry_apps/tasks/sentry_apps.py index eb4988c41a523f..c238b958efa0da 100644 --- a/src/sentry/sentry_apps/tasks/sentry_apps.py +++ b/src/sentry/sentry_apps/tasks/sentry_apps.py @@ -751,18 +751,18 @@ def send_webhooks(installation: RpcSentryAppInstallation, event: str, **kwargs: servicehook: ServiceHook | None = _load_service_hook( installation.organization_id, installation.id ) + lifecycle.add_extras( + { + "installation_uuid": installation.uuid, + "installation_id": installation.id, + "organization": installation.organization_id, + "sentry_app": installation.sentry_app.id, + "events": installation.sentry_app.events, + "webhook_url": installation.sentry_app.webhook_url or "", + } + ) + if not servicehook: - lifecycle.add_extra("events", installation.sentry_app.events) - lifecycle.add_extras( - { - "installation_uuid": installation.uuid, - "installation_id": installation.id, - "organization": installation.organization_id, - "sentry_app": installation.sentry_app.id, - "events": installation.sentry_app.events, - "webhook_url": installation.sentry_app.webhook_url or "", - } - ) raise SentryAppSentryError(message=SentryAppWebhookFailureReason.MISSING_SERVICEHOOK) if event not in servicehook.events: raise SentryAppSentryError( @@ -799,6 +799,37 @@ def send_webhooks(installation: RpcSentryAppInstallation, event: str, **kwargs: ) +@instrumented_task( + "sentry.sentry_apps.tasks.sentry_apps.regenerate_service_hook_for_installation", + taskworker_config=TaskworkerConfig( + namespace=sentryapp_tasks, retry=Retry(times=3), processing_deadline_duration=60 + ), + **TASK_OPTIONS, +) +def regenerate_service_hook_for_installation(installation_id: int) -> None: + installation = app_service.installation_by_id(id=installation_id) + if installation is None: + logger.info( + "regenerate_service_hook_for_installation.could_not_find_installation", + extra={"installation_id": installation_id}, + ) + return + + create_or_update_service_hooks_for_installation( + installation=installation, + events=installation.sentry_app.events, + webhook_url=installation.sentry_app.webhook_url, + ) + + logger.info( + "regenerate_service_hook_for_installation", + extra={ + "installation_id": installation_id, + "sentry_app": installation.sentry_app.id, + }, + ) + + @instrumented_task( "sentry.sentry_apps.tasks.sentry_apps.create_or_update_service_hooks_for_sentry_app", taskworker_config=TaskworkerConfig( diff --git a/src/sentry/utils/sentry_apps/service_hook_manager.py b/src/sentry/utils/sentry_apps/service_hook_manager.py index 58af41a48b1b41..2e07e405ef83bf 100644 --- a/src/sentry/utils/sentry_apps/service_hook_manager.py +++ b/src/sentry/utils/sentry_apps/service_hook_manager.py @@ -1,9 +1,12 @@ from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation +from sentry.sentry_apps.services.app.model import RpcSentryAppInstallation from sentry.sentry_apps.services.hook import hook_service def create_or_update_service_hooks_for_installation( - installation: SentryAppInstallation, webhook_url: str | None, events: list[str] + installation: SentryAppInstallation | RpcSentryAppInstallation, + webhook_url: str | None, + events: list[str], ) -> None: """ This function creates or updates service hooks for a given Sentry app installation. diff --git a/tests/sentry/sentry_apps/tasks/test_sentry_apps.py b/tests/sentry/sentry_apps/tasks/test_sentry_apps.py index 6f72943cc8e4a7..2ff61cf5433152 100644 --- a/tests/sentry/sentry_apps/tasks/test_sentry_apps.py +++ b/tests/sentry/sentry_apps/tasks/test_sentry_apps.py @@ -26,12 +26,14 @@ installation_webhook, notify_sentry_app, process_resource_change_bound, + regenerate_service_hook_for_installation, send_alert_webhook_v2, send_webhooks, workflow_notification, ) from sentry.sentry_apps.utils.errors import SentryAppSentryError from sentry.shared_integrations.exceptions import ClientError +from sentry.silo.base import SiloMode from sentry.tasks.post_process import post_process_group from sentry.testutils.asserts import ( assert_count_of_metric, @@ -44,7 +46,7 @@ from sentry.testutils.helpers import with_feature from sentry.testutils.helpers.datetime import before_now from sentry.testutils.helpers.eventprocessing import write_event_to_cache -from sentry.testutils.silo import assume_test_silo_mode_of, control_silo_test +from sentry.testutils.silo import assume_test_silo_mode, assume_test_silo_mode_of, control_silo_test from sentry.testutils.skips import requires_snuba from sentry.types.activity import ActivityType from sentry.types.rules import RuleFuture @@ -1659,3 +1661,53 @@ def test_saves_error_event_id_if_in_header(self, safe_urlopen): assert first_request["organization_id"] == self.install.organization_id assert first_request["error_id"] == "d5111da2c28645c5889d072017e3445d" assert first_request["project_id"] == "1" + + +class TestBackfillServiceHooksEvents(TestCase): + def setUp(self) -> None: + self.sentry_app = self.create_sentry_app( + name="Test App", + webhook_url="https://example.com", + organization=self.organization, + events=["issue.created", "issue.resolved", "error.created"], + ) + self.install = self.create_sentry_app_installation( + organization=self.organization, slug=self.sentry_app.slug + ) + + def test_regenerate_service_hook_for_installation_success(self): + with assume_test_silo_mode(SiloMode.REGION): + hook = ServiceHook.objects.get(installation_id=self.install.id) + hook.events = ["issue.resolved", "error.created"] + hook.save() + + with self.tasks(), assume_test_silo_mode(SiloMode.REGION): + regenerate_service_hook_for_installation(installation_id=self.install.id) + + with assume_test_silo_mode(SiloMode.REGION): + hook.refresh_from_db() + assert set(hook.events) == {"issue.created", "issue.resolved", "error.created"} + + def test_regenerate_service_hook_for_installation_event_not_in_app_events(self): + with self.tasks(), assume_test_silo_mode(SiloMode.REGION): + regenerate_service_hook_for_installation(installation_id=self.install.id) + + with assume_test_silo_mode(SiloMode.REGION): + hook = ServiceHook.objects.get(installation_id=self.install.id) + assert set(hook.events) == {"issue.created", "issue.resolved", "error.created"} + + def test_regenerate_service_hook_for_installation_with_empty_app_events(self): + with assume_test_silo_mode(SiloMode.CONTROL): + self.sentry_app.update(events=[]) + assert self.sentry_app.events == [] + + with assume_test_silo_mode(SiloMode.REGION): + hook = ServiceHook.objects.get(installation_id=self.install.id) + assert hook.events != [] + + with self.tasks(), assume_test_silo_mode(SiloMode.REGION): + regenerate_service_hook_for_installation(installation_id=self.install.id) + + with assume_test_silo_mode(SiloMode.REGION): + hook.refresh_from_db() + assert hook.events == []