From 9ec008154a85c0c3f299fb2ed4d16374d74a3a02 Mon Sep 17 00:00:00 2001 From: Cathy Teng Date: Fri, 3 Oct 2025 15:27:48 -0700 Subject: [PATCH 1/5] async deletions for sentry app installations --- src/sentry/sentry_apps/api/endpoints/sentry_app_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/sentry_apps/api/endpoints/sentry_app_details.py b/src/sentry/sentry_apps/api/endpoints/sentry_app_details.py index 022fd980192fed..1b9d7a92d46f56 100644 --- a/src/sentry/sentry_apps/api/endpoints/sentry_app_details.py +++ b/src/sentry/sentry_apps/api/endpoints/sentry_app_details.py @@ -8,7 +8,7 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import analytics, audit_log, deletions, features +from sentry import analytics, audit_log, features from sentry.analytics.events.sentry_app_deleted import SentryAppDeletedEvent from sentry.analytics.events.sentry_app_schema_validation_error import ( SentryAppSchemaValidationError, @@ -230,7 +230,7 @@ def delete(self, request: Request, sentry_app) -> Response: SentryAppInstallationNotifier( sentry_app_installation=install, user=request.user, action="deleted" ).run() - deletions.exec_sync(install) + ScheduledDeletion.schedule(install, days=0, actor=request.user) except RequestException as exc: sentry_sdk.capture_exception(exc) From e1a68b2c7daccf9163acf0f096595a2e2a23d62a Mon Sep 17 00:00:00 2001 From: Cathy Teng Date: Fri, 3 Oct 2025 15:38:58 -0700 Subject: [PATCH 2/5] remove redundant deletion lol --- src/sentry/sentry_apps/api/endpoints/sentry_app_details.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry/sentry_apps/api/endpoints/sentry_app_details.py b/src/sentry/sentry_apps/api/endpoints/sentry_app_details.py index 1b9d7a92d46f56..00bf4031d90ced 100644 --- a/src/sentry/sentry_apps/api/endpoints/sentry_app_details.py +++ b/src/sentry/sentry_apps/api/endpoints/sentry_app_details.py @@ -230,7 +230,6 @@ def delete(self, request: Request, sentry_app) -> Response: SentryAppInstallationNotifier( sentry_app_installation=install, user=request.user, action="deleted" ).run() - ScheduledDeletion.schedule(install, days=0, actor=request.user) except RequestException as exc: sentry_sdk.capture_exception(exc) From ecbe47855e52241b6452306ddfbd1ac03a6ae778 Mon Sep 17 00:00:00 2001 From: Cathy Teng Date: Fri, 3 Oct 2025 15:52:50 -0700 Subject: [PATCH 3/5] add async deletion for deleting sentry app install directly --- .../sentry_apps/api/endpoints/installation_details.py | 5 +++-- .../test_organization_sentry_app_installation_details.py | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/sentry/sentry_apps/api/endpoints/installation_details.py b/src/sentry/sentry_apps/api/endpoints/installation_details.py index 013342e5fbac0f..657f5c29508458 100644 --- a/src/sentry/sentry_apps/api/endpoints/installation_details.py +++ b/src/sentry/sentry_apps/api/endpoints/installation_details.py @@ -4,12 +4,13 @@ from rest_framework.request import Request from rest_framework.response import Response -from sentry import analytics, audit_log, deletions +from sentry import analytics, audit_log from sentry.analytics.events.sentry_app_uninstalled import SentryAppUninstalledEvent from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.serializers import serialize +from sentry.deletions.models.scheduleddeletion import ScheduledDeletion from sentry.sentry_apps.api.bases.sentryapps import SentryAppInstallationBaseEndpoint from sentry.sentry_apps.api.parsers.sentry_app_installation import SentryAppInstallationParser from sentry.sentry_apps.api.serializers.sentry_app_installation import ( @@ -56,7 +57,7 @@ def delete(self, request: Request, installation) -> Response: # if the error is from a request exception, log the error and continue except RequestException as exc: sentry_sdk.capture_exception(exc) - deletions.exec_sync(sentry_app_installation) + ScheduledDeletion.schedule(sentry_app_installation, days=0, actor=request.user) create_audit_entry( request=request, organization_id=sentry_app_installation.organization_id, diff --git a/tests/sentry/sentry_apps/api/endpoints/test_organization_sentry_app_installation_details.py b/tests/sentry/sentry_apps/api/endpoints/test_organization_sentry_app_installation_details.py index 127ef4f874adaf..2ab3093073c185 100644 --- a/tests/sentry/sentry_apps/api/endpoints/test_organization_sentry_app_installation_details.py +++ b/tests/sentry/sentry_apps/api/endpoints/test_organization_sentry_app_installation_details.py @@ -9,7 +9,9 @@ ) from sentry.analytics.events.sentry_app_uninstalled import SentryAppUninstalledEvent from sentry.constants import SentryAppInstallationStatus +from sentry.deletions.tasks.scheduled import run_scheduled_deletions_control from sentry.models.auditlogentry import AuditLogEntry +from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation from sentry.sentry_apps.token_exchange.grant_exchanger import GrantExchanger from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.analytics import assert_last_analytics_event @@ -120,6 +122,11 @@ def test_delete_install(self, record: MagicMock) -> None: ), ) + with self.tasks(): + run_scheduled_deletions_control() + + assert not SentryAppInstallation.objects.filter(id=self.installation2.id).exists() + response_body = json.loads(responses.calls[0].request.body) assert response_body.get("installation").get("uuid") == self.installation2.uuid From d5c5fd2cb024743201ef4b54d6df9fde48061e9a Mon Sep 17 00:00:00 2001 From: Cathy Teng Date: Tue, 7 Oct 2025 14:09:01 -0700 Subject: [PATCH 4/5] run deletion before checking status --- .../acceptance/test_organization_sentry_app_detailed_view.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/acceptance/test_organization_sentry_app_detailed_view.py b/tests/acceptance/test_organization_sentry_app_detailed_view.py index 49c1bf778843d7..0b1a20c2deb4bd 100644 --- a/tests/acceptance/test_organization_sentry_app_detailed_view.py +++ b/tests/acceptance/test_organization_sentry_app_detailed_view.py @@ -1,6 +1,7 @@ from fixtures.page_objects.organization_integration_settings import ( OrganizationSentryAppDetailViewPage, ) +from sentry.deletions.tasks.scheduled import run_scheduled_deletions_control from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation from sentry.testutils.cases import AcceptanceTestCase from sentry.testutils.silo import no_silo_test @@ -49,6 +50,10 @@ def test_uninstallation(self) -> None: detail_view_page.uninstall() self.browser.wait_until('[data-test-id="toast-success"]') + + with self.tasks(): + run_scheduled_deletions_control() + assert not SentryAppInstallation.objects.filter( organization_id=self.organization.id, sentry_app=self.sentry_app ) From 06a79015379d2f56aa64ea1a277a45cd8a98878d Mon Sep 17 00:00:00 2001 From: Cathy Teng Date: Tue, 7 Oct 2025 14:12:28 -0700 Subject: [PATCH 5/5] update status to pending deletion --- src/sentry/constants.py | 5 +++++ src/sentry/sentry_apps/api/endpoints/installation_details.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/sentry/constants.py b/src/sentry/constants.py index fa82179a20bf19..e72109f9196a5e 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -595,14 +595,17 @@ def as_int_choices(cls) -> Sequence[int]: class SentryAppInstallationStatus: PENDING = 0 INSTALLED = 1 + PENDING_DELETION = 2 PENDING_STR = "pending" INSTALLED_STR = "installed" + PENDING_DELETION_STR = "pending_deletion" @classmethod def as_choices(cls) -> Sequence[tuple[int, str]]: return ( (cls.PENDING, cls.PENDING_STR), (cls.INSTALLED, cls.INSTALLED_STR), + (cls.PENDING_DELETION, cls.PENDING_DELETION_STR), ) @classmethod @@ -611,6 +614,8 @@ def as_str(cls, status: int) -> str: return cls.PENDING_STR elif status == cls.INSTALLED: return cls.INSTALLED_STR + elif status == cls.PENDING_DELETION: + return cls.PENDING_DELETION_STR else: raise ValueError(f"Not a SentryAppInstallationStatus int: {status!r}") diff --git a/src/sentry/sentry_apps/api/endpoints/installation_details.py b/src/sentry/sentry_apps/api/endpoints/installation_details.py index 657f5c29508458..f1cb0dd852de76 100644 --- a/src/sentry/sentry_apps/api/endpoints/installation_details.py +++ b/src/sentry/sentry_apps/api/endpoints/installation_details.py @@ -10,6 +10,7 @@ from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import control_silo_endpoint from sentry.api.serializers import serialize +from sentry.constants import SentryAppInstallationStatus from sentry.deletions.models.scheduleddeletion import ScheduledDeletion from sentry.sentry_apps.api.bases.sentryapps import SentryAppInstallationBaseEndpoint from sentry.sentry_apps.api.parsers.sentry_app_installation import SentryAppInstallationParser @@ -57,6 +58,7 @@ def delete(self, request: Request, installation) -> Response: # if the error is from a request exception, log the error and continue except RequestException as exc: sentry_sdk.capture_exception(exc) + sentry_app_installation.update(status=SentryAppInstallationStatus.PENDING_DELETION) ScheduledDeletion.schedule(sentry_app_installation, days=0, actor=request.user) create_audit_entry( request=request,