diff --git a/src/sentry/integrations/jira/integration.py b/src/sentry/integrations/jira/integration.py index 657bc35ab2043b..a967f523b4d400 100644 --- a/src/sentry/integrations/jira/integration.py +++ b/src/sentry/integrations/jira/integration.py @@ -36,6 +36,7 @@ from sentry.shared_integrations.exceptions import ( ApiError, ApiHostError, + ApiInvalidRequestError, ApiRateLimitedError, ApiUnauthorized, IntegrationError, @@ -1067,7 +1068,10 @@ def sync_status_outbound( logger.warning("jira.status-sync-fail", extra=log_context) return - client.transition_issue(external_issue.key, transition["id"]) + try: + client.transition_issue(external_issue.key, transition["id"]) + except ApiInvalidRequestError as e: + self.raise_error(e) def _get_done_statuses(self): client = self.get_client() diff --git a/src/sentry/integrations/tasks/sync_status_outbound.py b/src/sentry/integrations/tasks/sync_status_outbound.py index a418b0a922caff..822ae6a40ec02e 100644 --- a/src/sentry/integrations/tasks/sync_status_outbound.py +++ b/src/sentry/integrations/tasks/sync_status_outbound.py @@ -8,6 +8,7 @@ ) from sentry.integrations.services.integration import integration_service from sentry.models.group import Group, GroupStatus +from sentry.shared_integrations.exceptions import IntegrationFormError from sentry.silo.base import SiloMode from sentry.tasks.base import instrumented_task, retry, track_group_async_operation from sentry.taskworker.config import TaskworkerConfig @@ -68,9 +69,13 @@ def sync_status_outbound(group_id: int, external_issue_id: int) -> bool | None: "status": group.status, } ) - installation.sync_status_outbound( - external_issue, group.status == GroupStatus.RESOLVED, group.project_id - ) + try: + installation.sync_status_outbound( + external_issue, group.status == GroupStatus.RESOLVED, group.project_id + ) + except IntegrationFormError as e: + lifecycle.record_halt(halt_reason=e) + return None analytics.record( "integration.issue.status.synced", provider=integration.provider, diff --git a/tests/sentry/integrations/tasks/test_sync_status_outbound.py b/tests/sentry/integrations/tasks/test_sync_status_outbound.py index 2e47effb869bb4..d1b6ce5dd5b3c2 100644 --- a/tests/sentry/integrations/tasks/test_sync_status_outbound.py +++ b/tests/sentry/integrations/tasks/test_sync_status_outbound.py @@ -6,6 +6,8 @@ from sentry.integrations.models import ExternalIssue, Integration from sentry.integrations.tasks import sync_status_outbound from sentry.integrations.types import EventLifecycleOutcome +from sentry.shared_integrations.exceptions import IntegrationFormError +from sentry.testutils.asserts import assert_count_of_metric, assert_halt_metric from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode_of, region_silo_test @@ -14,6 +16,10 @@ def raise_exception(_external_issue, _is_resolved, _group_proj_id): raise Exception("Something went wrong") +def raise_integration_form_error(*args, **kwargs): + raise IntegrationFormError(field_errors={"foo": "Invalid foo provided"}) + + @region_silo_test class TestSyncStatusOutbound(TestCase): def setUp(self): @@ -103,3 +109,25 @@ def test_failed_sync(self, mock_sync_status, mock_record_failure): metric_exception = mock_record_event_args[0] assert isinstance(metric_exception, Exception) assert metric_exception.args[0] == "Something went wrong" + + @mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") + @mock.patch.object(ExampleIntegration, "sync_status_outbound") + def test_integration_form_error(self, mock_sync_status, mock_record): + mock_sync_status.side_effect = raise_integration_form_error + external_issue: ExternalIssue = self.create_integration_external_issue( + group=self.group, key="foo_integration", integration=self.example_integration + ) + + sync_status_outbound(self.group.id, external_issue_id=external_issue.id) + + # SLOs SYNC_STATUS_OUTBOUND (halt) + assert_count_of_metric( + mock_record=mock_record, outcome=EventLifecycleOutcome.STARTED, outcome_count=1 + ) + assert_count_of_metric( + mock_record=mock_record, outcome=EventLifecycleOutcome.HALTED, outcome_count=1 + ) + + assert_halt_metric( + mock_record=mock_record, error_msg=IntegrationFormError({"error": "bruh"}) + )