From 13244c80fbf6d1a76d33b3964a9ea36a8e3d4f35 Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:50:22 -0700 Subject: [PATCH 1/7] add catching for integration not found error --- src/sentry/seer/autofix/on_completion_hook.py | 23 +++++- src/sentry/seer/endpoints/seer_rpc.py | 14 +++- .../seer/autofix/test_on_completion_hook.py | 72 +++++++++++++++++++ tests/sentry/seer/endpoints/test_seer_rpc.py | 17 +++++ 4 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 tests/sentry/seer/autofix/test_on_completion_hook.py diff --git a/src/sentry/seer/autofix/on_completion_hook.py b/src/sentry/seer/autofix/on_completion_hook.py index 6c10e988fa5867..131ebdad6255f8 100644 --- a/src/sentry/seer/autofix/on_completion_hook.py +++ b/src/sentry/seer/autofix/on_completion_hook.py @@ -3,6 +3,8 @@ import logging from typing import TYPE_CHECKING +from rest_framework.exceptions import NotFound + from sentry import features from sentry.models.group import Group from sentry.models.organization import Organization @@ -12,7 +14,11 @@ trigger_coding_agent_handoff, ) from sentry.seer.autofix.constants import AutofixReferrer -from sentry.seer.autofix.utils import AutofixStoppingPoint, get_project_seer_preferences +from sentry.seer.autofix.utils import ( + AutofixStoppingPoint, + get_project_seer_preferences, + set_project_seer_preference, +) from sentry.seer.entrypoints.operator import SeerAutofixOperator, process_autofix_updates from sentry.seer.explorer.client import SeerExplorerClient from sentry.seer.explorer.client_models import Artifact @@ -516,6 +522,21 @@ def _trigger_coding_agent_handoff( "group_id": group_id, }, ) + except NotFound: + logger.exception( + "autofix.on_completion_hook.coding_agent_handoff_integration_not_found", + extra={ + "run_id": run_id, + "organization_id": organization.id, + "integration_id": handoff_config.integration_id, + }, + ) + preference_response = get_project_seer_preferences(group.project_id) + if preference_response and preference_response.preference: + updated_preference = preference_response.preference.copy( + update={"automation_handoff": None} + ) + set_project_seer_preference(updated_preference) except Exception: logger.exception( "autofix.on_completion_hook.coding_agent_handoff_failed", diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 2795c05760c5eb..4e112139a8ec1a 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -579,7 +579,7 @@ def trigger_coding_agent_launch( trigger_source: Either "root_cause" or "solution" (default: "solution") Returns: - dict: {"success": bool} + dict: {"success": bool, "error_code": str | None} """ try: launch_coding_agents_for_run( @@ -589,7 +589,17 @@ def trigger_coding_agent_launch( trigger_source=AutofixTriggerSource(trigger_source), ) return {"success": True} - except (NotFound, PermissionDenied, ValidationError, APIException): + except NotFound: + logger.exception( + "coding_agent.rpc_launch_error", + extra={ + "organization_id": organization_id, + "integration_id": integration_id, + "run_id": run_id, + }, + ) + return {"success": False, "error_code": "integration_not_found"} + except (PermissionDenied, ValidationError, APIException): logger.exception( "coding_agent.rpc_launch_error", extra={ diff --git a/tests/sentry/seer/autofix/test_on_completion_hook.py b/tests/sentry/seer/autofix/test_on_completion_hook.py new file mode 100644 index 00000000000000..58128669f3018f --- /dev/null +++ b/tests/sentry/seer/autofix/test_on_completion_hook.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from rest_framework.exceptions import NotFound + +from sentry.seer.autofix.utils import CodingAgentProviderType +from sentry.seer.models.seer_api_models import SeerAutomationHandoffConfiguration +from sentry.testutils.cases import TestCase + + +class TestTriggerCodingAgentHandoff(TestCase): + def setUp(self) -> None: + super().setUp() + self.organization = self.create_organization() + self.project = self.create_project(organization=self.organization) + self.group = self.create_group(project=self.project) + + def _make_handoff_config(self, integration_id: int = 789) -> SeerAutomationHandoffConfiguration: + return SeerAutomationHandoffConfiguration( + handoff_point="root_cause", + target=CodingAgentProviderType.CURSOR_BACKGROUND_AGENT, + integration_id=integration_id, + ) + + @patch("sentry.seer.autofix.on_completion_hook.set_project_seer_preference") + @patch("sentry.seer.autofix.on_completion_hook.get_project_seer_preferences") + @patch("sentry.seer.autofix.on_completion_hook.trigger_coding_agent_handoff") + def test_not_found_clears_automation_handoff( + self, mock_trigger, mock_get_prefs, mock_set_pref + ) -> None: + from sentry.seer.autofix.on_completion_hook import AutofixOnCompletionHook + + mock_trigger.side_effect = NotFound("Integration not found") + + mock_pref = MagicMock() + mock_pref.automation_handoff = self._make_handoff_config() + mock_pref.copy.return_value = mock_pref + mock_get_prefs.return_value = MagicMock(preference=mock_pref) + + handoff_config = self._make_handoff_config() + + AutofixOnCompletionHook._trigger_coding_agent_handoff( + organization=self.organization, + run_id=1, + group_id=self.group.id, + handoff_config=handoff_config, + ) + + mock_get_prefs.assert_called_once_with(self.group.project_id) + mock_pref.copy.assert_called_once_with(update={"automation_handoff": None}) + mock_set_pref.assert_called_once_with(mock_pref) + + @patch("sentry.seer.autofix.on_completion_hook.set_project_seer_preference") + @patch("sentry.seer.autofix.on_completion_hook.get_project_seer_preferences") + @patch("sentry.seer.autofix.on_completion_hook.trigger_coding_agent_handoff") + def test_not_found_no_preference_response_does_not_call_set( + self, mock_trigger, mock_get_prefs, mock_set_pref + ) -> None: + from sentry.seer.autofix.on_completion_hook import AutofixOnCompletionHook + + mock_trigger.side_effect = NotFound("Integration not found") + mock_get_prefs.return_value = None + + AutofixOnCompletionHook._trigger_coding_agent_handoff( + organization=self.organization, + run_id=1, + group_id=self.group.id, + handoff_config=self._make_handoff_config(), + ) + + mock_set_pref.assert_not_called() diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 0610b39ed643bd..7d8bbe178e304c 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -21,6 +21,7 @@ get_attributes_for_span, get_github_enterprise_integration_config, has_repo_code_mappings, + trigger_coding_agent_launch, validate_repo, ) from sentry.seer.explorer.tools import get_trace_item_attributes @@ -1317,3 +1318,19 @@ def test_validate_repo_github_enterprise(self) -> None: ) assert result == {"valid": True, "integration_id": integration.id} + + +class TestTriggerCodingAgentLaunch: + @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") + def test_not_found_returns_integration_not_found_error_code(self, mock_launch): + from rest_framework.exceptions import NotFound + + mock_launch.side_effect = NotFound("Integration not found") + + result = trigger_coding_agent_launch( + organization_id=1, + integration_id=2, + run_id=3, + ) + + assert result == {"success": False, "error_code": "integration_not_found"} From 07d8d17db59b563651320b2140587518afa920a1 Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:25:10 -0700 Subject: [PATCH 2/7] add inits and exception handling --- src/sentry/seer/autofix/on_completion_hook.py | 23 ++++++--- src/sentry/seer/endpoints/seer_rpc.py | 6 ++- tests/sentry/seer/autofix/__init__.py | 0 .../test_autofix_on_completion_hook.py | 49 +++++++++++++++++++ tests/sentry/seer/endpoints/test_seer_rpc.py | 30 ++++++++++++ tests/sentry/seer/explorer/__init__.py | 0 6 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 tests/sentry/seer/autofix/__init__.py create mode 100644 tests/sentry/seer/explorer/__init__.py diff --git a/src/sentry/seer/autofix/on_completion_hook.py b/src/sentry/seer/autofix/on_completion_hook.py index 131ebdad6255f8..3d10ac35ad87d2 100644 --- a/src/sentry/seer/autofix/on_completion_hook.py +++ b/src/sentry/seer/autofix/on_completion_hook.py @@ -477,6 +477,22 @@ def _get_handoff_config_if_applicable( return handoff_config + @classmethod + def _clear_handoff_preference(cls, project_id: int, run_id: int, organization_id: int) -> None: + """Clear automation_handoff from project preferences after integration is not found.""" + try: + preference_response = get_project_seer_preferences(project_id) + if preference_response and preference_response.preference: + updated_preference = preference_response.preference.copy( + update={"automation_handoff": None} + ) + set_project_seer_preference(updated_preference) + except (SeerApiError, SeerApiResponseValidationError): + logger.exception( + "autofix.on_completion_hook.clear_handoff_preference_failed", + extra={"run_id": run_id, "organization_id": organization_id}, + ) + @classmethod def _trigger_coding_agent_handoff( cls, @@ -531,12 +547,7 @@ def _trigger_coding_agent_handoff( "integration_id": handoff_config.integration_id, }, ) - preference_response = get_project_seer_preferences(group.project_id) - if preference_response and preference_response.preference: - updated_preference = preference_response.preference.copy( - update={"automation_handoff": None} - ) - set_project_seer_preference(updated_preference) + cls._clear_handoff_preference(group.project_id, run_id, organization.id) except Exception: logger.exception( "autofix.on_completion_hook.coding_agent_handoff_failed", diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 4e112139a8ec1a..22ebd137edc8bd 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -589,7 +589,7 @@ def trigger_coding_agent_launch( trigger_source=AutofixTriggerSource(trigger_source), ) return {"success": True} - except NotFound: + except NotFound as e: logger.exception( "coding_agent.rpc_launch_error", extra={ @@ -598,7 +598,9 @@ def trigger_coding_agent_launch( "run_id": run_id, }, ) - return {"success": False, "error_code": "integration_not_found"} + if str(e.detail) == "Integration not found": + return {"success": False, "error_code": "integration_not_found"} + return {"success": False} except (PermissionDenied, ValidationError, APIException): logger.exception( "coding_agent.rpc_launch_error", diff --git a/tests/sentry/seer/autofix/__init__.py b/tests/sentry/seer/autofix/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py b/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py index 0e540a9ceff578..6b97597eb645ee 100644 --- a/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py +++ b/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py @@ -560,6 +560,55 @@ def test_maybe_continue_pipeline_triggers_handoff_when_configured( mock_trigger_handoff.assert_called_once() + @patch("sentry.seer.autofix.on_completion_hook.set_project_seer_preference") + @patch("sentry.seer.autofix.on_completion_hook.get_project_seer_preferences") + @patch("sentry.seer.autofix.on_completion_hook.trigger_coding_agent_handoff") + def test_trigger_coding_agent_handoff_clears_preference_on_not_found( + self, mock_trigger, mock_get_prefs, mock_set_pref + ): + """When NotFound is raised, automation_handoff is cleared from preferences.""" + from rest_framework.exceptions import NotFound + + mock_trigger.side_effect = NotFound() + handoff_config = self._make_handoff_config() + mock_get_prefs.return_value = self._make_preference_response(handoff_config=handoff_config) + + AutofixOnCompletionHook._trigger_coding_agent_handoff( + organization=self.organization, + run_id=123, + group_id=self.group.id, + handoff_config=handoff_config, + ) + + mock_set_pref.assert_called_once() + updated = mock_set_pref.call_args.args[0] + assert updated.automation_handoff is None + + @patch("sentry.seer.autofix.on_completion_hook.set_project_seer_preference") + @patch("sentry.seer.autofix.on_completion_hook.get_project_seer_preferences") + @patch("sentry.seer.autofix.on_completion_hook.trigger_coding_agent_handoff") + def test_trigger_coding_agent_handoff_not_found_seer_api_error_does_not_raise( + self, mock_trigger, mock_get_prefs, mock_set_pref + ): + """A SeerApiError during preference-clearing after NotFound should not propagate.""" + from rest_framework.exceptions import NotFound + + from sentry.seer.models import SeerApiError + + mock_trigger.side_effect = NotFound() + mock_get_prefs.side_effect = SeerApiError("seer unavailable", 503) + handoff_config = self._make_handoff_config() + + # Should not raise + AutofixOnCompletionHook._trigger_coding_agent_handoff( + organization=self.organization, + run_id=123, + group_id=self.group.id, + handoff_config=handoff_config, + ) + + mock_set_pref.assert_not_called() + @patch("sentry.seer.autofix.on_completion_hook.trigger_coding_agent_handoff") def test_trigger_coding_agent_handoff_calls_function(self, mock_trigger): """Test _trigger_coding_agent_handoff calls the trigger function correctly.""" diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 7d8bbe178e304c..7967d846b40b05 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -1334,3 +1334,33 @@ def test_not_found_returns_integration_not_found_error_code(self, mock_launch): ) assert result == {"success": False, "error_code": "integration_not_found"} + + @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") + def test_organization_not_found_does_not_return_integration_error_code(self, mock_launch): + from rest_framework.exceptions import NotFound + + mock_launch.side_effect = NotFound("Organization not found") + + result = trigger_coding_agent_launch( + organization_id=1, + integration_id=2, + run_id=3, + ) + + assert result == {"success": False} + assert result.get("error_code") != "integration_not_found" + + @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") + def test_autofix_state_not_found_does_not_return_integration_error_code(self, mock_launch): + from rest_framework.exceptions import NotFound + + mock_launch.side_effect = NotFound("Autofix state not found") + + result = trigger_coding_agent_launch( + organization_id=1, + integration_id=2, + run_id=3, + ) + + assert result == {"success": False} + assert result.get("error_code") != "integration_not_found" diff --git a/tests/sentry/seer/explorer/__init__.py b/tests/sentry/seer/explorer/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 From 21286fcc8d801d22a3aee63131b7f9cddda7063c Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:59:29 -0700 Subject: [PATCH 3/7] group_id -> group --- tests/sentry/seer/autofix/test_autofix_on_completion_hook.py | 4 ++-- tests/sentry/seer/autofix/test_on_completion_hook.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py b/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py index 68732ac9342249..e01dde77104925 100644 --- a/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py +++ b/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py @@ -624,7 +624,7 @@ def test_trigger_coding_agent_handoff_clears_preference_on_not_found( AutofixOnCompletionHook._trigger_coding_agent_handoff( organization=self.organization, run_id=123, - group_id=self.group.id, + group=self.group, handoff_config=handoff_config, ) @@ -651,7 +651,7 @@ def test_trigger_coding_agent_handoff_not_found_seer_api_error_does_not_raise( AutofixOnCompletionHook._trigger_coding_agent_handoff( organization=self.organization, run_id=123, - group_id=self.group.id, + group=self.group, handoff_config=handoff_config, ) diff --git a/tests/sentry/seer/autofix/test_on_completion_hook.py b/tests/sentry/seer/autofix/test_on_completion_hook.py index 58128669f3018f..721570338f9dd1 100644 --- a/tests/sentry/seer/autofix/test_on_completion_hook.py +++ b/tests/sentry/seer/autofix/test_on_completion_hook.py @@ -43,7 +43,7 @@ def test_not_found_clears_automation_handoff( AutofixOnCompletionHook._trigger_coding_agent_handoff( organization=self.organization, run_id=1, - group_id=self.group.id, + group=self.group, handoff_config=handoff_config, ) @@ -65,7 +65,7 @@ def test_not_found_no_preference_response_does_not_call_set( AutofixOnCompletionHook._trigger_coding_agent_handoff( organization=self.organization, run_id=1, - group_id=self.group.id, + group=self.group, handoff_config=self._make_handoff_config(), ) From 4565b3423cba2a5d060e720cc2c3c213f458d7e3 Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:23:12 -0700 Subject: [PATCH 4/7] specify error + merge conflict + dual write --- src/sentry/seer/autofix/coding_agent.py | 28 ++++++++++++++---- src/sentry/seer/autofix/on_completion_hook.py | 29 +++++++++++++++---- src/sentry/seer/endpoints/seer_rpc.py | 23 +++++++++++---- .../seer/autofix/test_on_completion_hook.py | 7 ++--- tests/sentry/seer/endpoints/test_seer_rpc.py | 12 ++++---- 5 files changed, 72 insertions(+), 27 deletions(-) diff --git a/src/sentry/seer/autofix/coding_agent.py b/src/sentry/seer/autofix/coding_agent.py index ad9e9fd3cca7db..5d61bdb065e38e 100644 --- a/src/sentry/seer/autofix/coding_agent.py +++ b/src/sentry/seer/autofix/coding_agent.py @@ -12,6 +12,24 @@ from rest_framework.exceptions import APIException, NotFound, PermissionDenied, ValidationError from sentry import features + + +class IntegrationNotFound(NotFound): + pass + + +class OrganizationNotFound(NotFound): + pass + + +class AutofixStateNotFound(NotFound): + pass + + +class StateReposNotFound(NotFound): + pass + + from sentry.constants import ENABLE_SEER_CODING_DEFAULT, ObjectStatus from sentry.integrations.claude_code.integration import ( ClaudeCodeIntegrationMetadata, @@ -118,7 +136,7 @@ def _validate_and_get_integration(organization, integration_id: int): ) if not org_integration or org_integration.status != ObjectStatus.ACTIVE: - raise NotFound("Integration not found") + raise IntegrationNotFound("Integration not found") integration = integration_service.get_integration( organization_integration_id=org_integration.id, @@ -126,7 +144,7 @@ def _validate_and_get_integration(organization, integration_id: int): ) if not integration: - raise NotFound("Integration not found") + raise IntegrationNotFound("Integration not found") # Verify it's a coding agent integration if integration.provider not in get_coding_agent_providers(): @@ -252,7 +270,7 @@ def _launch_agents_for_repos( repos_to_launch = validated_repos or autofix_state_repos if not repos_to_launch: - raise NotFound( + raise StateReposNotFound( "There are no repos in the Seer state to launch coding agents with, make sure you have repos connected to Seer and rerun this Issue Fix." ) @@ -426,7 +444,7 @@ def launch_coding_agents_for_run( try: organization = Organization.objects.get(id=organization_id) except Organization.DoesNotExist: - raise NotFound("Organization not found") + raise OrganizationNotFound("Organization not found") if not organization.get_option("sentry:enable_seer_coding", default=ENABLE_SEER_CODING_DEFAULT): raise PermissionDenied("Code generation is disabled for this organization") @@ -457,7 +475,7 @@ def launch_coding_agents_for_run( autofix_state = _get_autofix_state(run_id, organization) if autofix_state is None: - raise NotFound("Autofix state not found") + raise AutofixStateNotFound("Autofix state not found") logger.info( "coding_agent.launch_request", diff --git a/src/sentry/seer/autofix/on_completion_hook.py b/src/sentry/seer/autofix/on_completion_hook.py index cf1e333d3ba14f..0f86910bb20719 100644 --- a/src/sentry/seer/autofix/on_completion_hook.py +++ b/src/sentry/seer/autofix/on_completion_hook.py @@ -4,22 +4,25 @@ from typing import TYPE_CHECKING from django.utils import timezone -from rest_framework.exceptions import NotFound from sentry import features from sentry.models.group import Group from sentry.models.organization import Organization +from sentry.models.project import Project from sentry.seer.autofix.autofix_agent import ( AutofixStep, trigger_autofix_explorer, trigger_coding_agent_handoff, trigger_push_changes, ) +from sentry.seer.autofix.coding_agent import IntegrationNotFound from sentry.seer.autofix.constants import AutofixReferrer from sentry.seer.autofix.utils import ( AutofixStoppingPoint, get_project_seer_preferences, + resolve_repository_ids, set_project_seer_preference, + write_preference_to_sentry_db, ) from sentry.seer.entrypoints.operator import SeerAutofixOperator, process_autofix_updates from sentry.seer.explorer.client_models import Artifact @@ -30,6 +33,7 @@ SeerApiResponseValidationError, SeerAutomationHandoffConfiguration, ) +from sentry.seer.models.seer_api_models import SeerProjectPreference from sentry.seer.supergroups.embeddings import trigger_supergroups_embedding from sentry.sentry_apps.metrics import SentryAppEventType from sentry.sentry_apps.tasks.sentry_apps import broadcast_webhooks_for_organization @@ -478,19 +482,32 @@ def _get_handoff_config_if_applicable( return handoff_config @classmethod - def _clear_handoff_preference(cls, project_id: int, run_id: int, organization_id: int) -> None: + def _clear_handoff_preference( + cls, project: Project, run_id: int, organization: Organization + ) -> None: """Clear automation_handoff from project preferences after integration is not found.""" try: - preference_response = get_project_seer_preferences(project_id) + preference_response = get_project_seer_preferences(project.id) if preference_response and preference_response.preference: updated_preference = preference_response.preference.copy( update={"automation_handoff": None} ) set_project_seer_preference(updated_preference) + + if features.has("organizations:seer-project-settings-dual-write", organization): + try: + validated_pref = SeerProjectPreference.validate(updated_preference) + resolved_pref = resolve_repository_ids(organization.id, [validated_pref]) + write_preference_to_sentry_db(project, resolved_pref[0]) + except Exception: + logger.exception( + "seer.write_preferences.failed", + extra={"project_id": project.id, "organization_id": organization.id}, + ) except (SeerApiError, SeerApiResponseValidationError): logger.exception( "autofix.on_completion_hook.clear_handoff_preference_failed", - extra={"run_id": run_id, "organization_id": organization_id}, + extra={"run_id": run_id, "organization_id": organization.id}, ) @classmethod @@ -529,7 +546,7 @@ def _trigger_coding_agent_handoff( "failures": len(result.get("failures", [])), }, ) - except NotFound: + except IntegrationNotFound: logger.exception( "autofix.on_completion_hook.coding_agent_handoff_integration_not_found", extra={ @@ -538,7 +555,7 @@ def _trigger_coding_agent_handoff( "integration_id": handoff_config.integration_id, }, ) - cls._clear_handoff_preference(group.project_id, run_id, organization.id) + cls._clear_handoff_preference(group.project, run_id, organization) except Exception: logger.exception( "autofix.on_completion_hook.coding_agent_handoff_failed", diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index aa018ed52164af..f592985b49ddba 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -73,7 +73,13 @@ get_attribute_values_with_substring, ) from sentry.seer.autofix.autofix_tools import get_error_event_details, get_profile_details -from sentry.seer.autofix.coding_agent import launch_coding_agents_for_run +from sentry.seer.autofix.coding_agent import ( + AutofixStateNotFound, + IntegrationNotFound, + OrganizationNotFound, + StateReposNotFound, + launch_coding_agents_for_run, +) from sentry.seer.autofix.utils import AutofixTriggerSource from sentry.seer.constants import SEER_SUPPORTED_SCM_PROVIDERS, SeerSCMProvider from sentry.seer.entrypoints.operator import SeerAutofixOperator, process_autofix_updates @@ -589,7 +595,7 @@ def trigger_coding_agent_launch( trigger_source=AutofixTriggerSource(trigger_source), ) return {"success": True} - except NotFound as e: + except IntegrationNotFound: logger.exception( "coding_agent.rpc_launch_error", extra={ @@ -598,10 +604,15 @@ def trigger_coding_agent_launch( "run_id": run_id, }, ) - if str(e.detail) == "Integration not found": - return {"success": False, "error_code": "integration_not_found"} - return {"success": False} - except (PermissionDenied, ValidationError, APIException): + return {"success": False, "error_code": "integration_not_found"} + except ( + OrganizationNotFound, + AutofixStateNotFound, + StateReposNotFound, + PermissionDenied, + ValidationError, + APIException, + ): logger.exception( "coding_agent.rpc_launch_error", extra={ diff --git a/tests/sentry/seer/autofix/test_on_completion_hook.py b/tests/sentry/seer/autofix/test_on_completion_hook.py index 721570338f9dd1..7fc51b0eb87c91 100644 --- a/tests/sentry/seer/autofix/test_on_completion_hook.py +++ b/tests/sentry/seer/autofix/test_on_completion_hook.py @@ -2,8 +2,7 @@ from unittest.mock import MagicMock, patch -from rest_framework.exceptions import NotFound - +from sentry.seer.autofix.coding_agent import IntegrationNotFound from sentry.seer.autofix.utils import CodingAgentProviderType from sentry.seer.models.seer_api_models import SeerAutomationHandoffConfiguration from sentry.testutils.cases import TestCase @@ -31,7 +30,7 @@ def test_not_found_clears_automation_handoff( ) -> None: from sentry.seer.autofix.on_completion_hook import AutofixOnCompletionHook - mock_trigger.side_effect = NotFound("Integration not found") + mock_trigger.side_effect = IntegrationNotFound("Integration not found") mock_pref = MagicMock() mock_pref.automation_handoff = self._make_handoff_config() @@ -59,7 +58,7 @@ def test_not_found_no_preference_response_does_not_call_set( ) -> None: from sentry.seer.autofix.on_completion_hook import AutofixOnCompletionHook - mock_trigger.side_effect = NotFound("Integration not found") + mock_trigger.side_effect = IntegrationNotFound("Integration not found") mock_get_prefs.return_value = None AutofixOnCompletionHook._trigger_coding_agent_handoff( diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index b99a80bb00244d..1e216b601823ad 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -1485,9 +1485,9 @@ def test_get_repo_installation_id_integration_not_found(self) -> None: class TestTriggerCodingAgentLaunch: @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") def test_not_found_returns_integration_not_found_error_code(self, mock_launch): - from rest_framework.exceptions import NotFound + from sentry.seer.autofix.coding_agent import IntegrationNotFound - mock_launch.side_effect = NotFound("Integration not found") + mock_launch.side_effect = IntegrationNotFound() result = trigger_coding_agent_launch( organization_id=1, @@ -1499,9 +1499,9 @@ def test_not_found_returns_integration_not_found_error_code(self, mock_launch): @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") def test_organization_not_found_does_not_return_integration_error_code(self, mock_launch): - from rest_framework.exceptions import NotFound + from sentry.seer.autofix.coding_agent import OrganizationNotFound - mock_launch.side_effect = NotFound("Organization not found") + mock_launch.side_effect = OrganizationNotFound() result = trigger_coding_agent_launch( organization_id=1, @@ -1514,9 +1514,9 @@ def test_organization_not_found_does_not_return_integration_error_code(self, moc @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") def test_autofix_state_not_found_does_not_return_integration_error_code(self, mock_launch): - from rest_framework.exceptions import NotFound + from sentry.seer.autofix.coding_agent import AutofixStateNotFound - mock_launch.side_effect = NotFound("Autofix state not found") + mock_launch.side_effect = AutofixStateNotFound() result = trigger_coding_agent_launch( organization_id=1, From 284eabb273aa5638ff0d79efa158bf46f52862be Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:40:41 -0700 Subject: [PATCH 5/7] update tests --- .../seer/autofix/test_autofix_on_completion_hook.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py b/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py index e01dde77104925..327669d82ec04c 100644 --- a/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py +++ b/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py @@ -614,10 +614,10 @@ def test_maybe_continue_pipeline_triggers_handoff_when_configured( def test_trigger_coding_agent_handoff_clears_preference_on_not_found( self, mock_trigger, mock_get_prefs, mock_set_pref ): - """When NotFound is raised, automation_handoff is cleared from preferences.""" - from rest_framework.exceptions import NotFound + """When IntegrationNotFound is raised, automation_handoff is cleared from preferences.""" + from sentry.seer.autofix.coding_agent import IntegrationNotFound - mock_trigger.side_effect = NotFound() + mock_trigger.side_effect = IntegrationNotFound() handoff_config = self._make_handoff_config() mock_get_prefs.return_value = self._make_preference_response(handoff_config=handoff_config) @@ -638,12 +638,11 @@ def test_trigger_coding_agent_handoff_clears_preference_on_not_found( def test_trigger_coding_agent_handoff_not_found_seer_api_error_does_not_raise( self, mock_trigger, mock_get_prefs, mock_set_pref ): - """A SeerApiError during preference-clearing after NotFound should not propagate.""" - from rest_framework.exceptions import NotFound - + """A SeerApiError during preference-clearing after IntegrationNotFound should not propagate.""" + from sentry.seer.autofix.coding_agent import IntegrationNotFound from sentry.seer.models import SeerApiError - mock_trigger.side_effect = NotFound() + mock_trigger.side_effect = IntegrationNotFound() mock_get_prefs.side_effect = SeerApiError("seer unavailable", 503) handoff_config = self._make_handoff_config() From 1b40bf98a1517af139bf03d15fc2f297a62156de Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:48:26 -0700 Subject: [PATCH 6/7] clear integration on sentry side --- src/sentry/seer/endpoints/seer_rpc.py | 19 ++++++++ tests/sentry/seer/endpoints/test_seer_rpc.py | 50 ++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index f592985b49ddba..28cb1587b37746 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -36,6 +36,7 @@ from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue, StrArray from sentry_protos.snuba.v1.trace_item_filter_pb2 import ComparisonFilter, TraceItemFilter +from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.authentication import AuthenticationSiloLimit, StandardAuthentication @@ -571,6 +572,7 @@ def send_seer_webhook(*, event_name: str, organization_id: int, payload: dict) - def trigger_coding_agent_launch( *, organization_id: int, + project_id: int, integration_id: int, run_id: int, trigger_source: str = "solution", @@ -604,6 +606,23 @@ def trigger_coding_agent_launch( "run_id": run_id, }, ) + try: + project = Project.objects.get_from_cache(id=project_id) + organization = Organization.objects.get_from_cache(id=organization_id) + if features.has("organizations:seer-project-settings-dual-write", organization): + project.delete_option("sentry:seer_automation_handoff_point") + project.delete_option("sentry:seer_automation_handoff_target") + project.delete_option("sentry:seer_automation_handoff_integration_id") + project.delete_option("sentry:seer_automation_handoff_auto_create_pr") + except Exception: + logger.exception( + "coding_agent.clear_handoff_preference_failed", + extra={ + "project_id": project_id, + "organization_id": organization_id, + "run_id": run_id, + }, + ) return {"success": False, "error_code": "integration_not_found"} except ( OrganizationNotFound, diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 1e216b601823ad..76e664fac8fc74 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -1491,6 +1491,7 @@ def test_not_found_returns_integration_not_found_error_code(self, mock_launch): result = trigger_coding_agent_launch( organization_id=1, + project_id=4, integration_id=2, run_id=3, ) @@ -1505,6 +1506,7 @@ def test_organization_not_found_does_not_return_integration_error_code(self, moc result = trigger_coding_agent_launch( organization_id=1, + project_id=4, integration_id=2, run_id=3, ) @@ -1520,9 +1522,57 @@ def test_autofix_state_not_found_does_not_return_integration_error_code(self, mo result = trigger_coding_agent_launch( organization_id=1, + project_id=4, integration_id=2, run_id=3, ) assert result == {"success": False} assert result.get("error_code") != "integration_not_found" + + +class TestTriggerCodingAgentLaunchClearsHandoff(APITestCase): + @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") + def test_integration_not_found_clears_handoff_project_options(self, mock_launch): + from sentry.seer.autofix.coding_agent import IntegrationNotFound + + mock_launch.side_effect = IntegrationNotFound() + + self.project.update_option("sentry:seer_automation_handoff_point", "root_cause") + self.project.update_option( + "sentry:seer_automation_handoff_target", "cursor_background_agent" + ) + self.project.update_option("sentry:seer_automation_handoff_integration_id", 42) + self.project.update_option("sentry:seer_automation_handoff_auto_create_pr", True) + + with self.feature("organizations:seer-project-settings-dual-write"): + result = trigger_coding_agent_launch( + organization_id=self.organization.id, + project_id=self.project.id, + integration_id=42, + run_id=99, + ) + + assert result == {"success": False, "error_code": "integration_not_found"} + assert self.project.get_option("sentry:seer_automation_handoff_point") is None + assert self.project.get_option("sentry:seer_automation_handoff_target") is None + assert self.project.get_option("sentry:seer_automation_handoff_integration_id") is None + assert self.project.get_option("sentry:seer_automation_handoff_auto_create_pr") is False + + @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") + def test_integration_not_found_skips_clear_without_feature_flag(self, mock_launch): + from sentry.seer.autofix.coding_agent import IntegrationNotFound + + mock_launch.side_effect = IntegrationNotFound() + + self.project.update_option("sentry:seer_automation_handoff_point", "root_cause") + + result = trigger_coding_agent_launch( + organization_id=self.organization.id, + project_id=self.project.id, + integration_id=42, + run_id=99, + ) + + assert result == {"success": False, "error_code": "integration_not_found"} + assert self.project.get_option("sentry:seer_automation_handoff_point") == "root_cause" From cc2fc5024be58c890de7056a9a6bee698e95a02e Mon Sep 17 00:00:00 2001 From: sehr-m <58871345+sehr-m@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:21:05 -0700 Subject: [PATCH 7/7] match calls --- src/sentry/seer/endpoints/seer_rpc.py | 22 +++++++++---- tests/sentry/seer/endpoints/test_seer_rpc.py | 34 ++++++++++++++++++-- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 28cb1587b37746..cdabc2867d0bef 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -81,7 +81,12 @@ StateReposNotFound, launch_coding_agents_for_run, ) -from sentry.seer.autofix.utils import AutofixTriggerSource +from sentry.seer.autofix.utils import ( + AutofixTriggerSource, + get_project_seer_preferences, + resolve_repository_ids, + write_preference_to_sentry_db, +) from sentry.seer.constants import SEER_SUPPORTED_SCM_PROVIDERS, SeerSCMProvider from sentry.seer.entrypoints.operator import SeerAutofixOperator, process_autofix_updates from sentry.seer.explorer.custom_tool_utils import call_custom_tool @@ -111,6 +116,7 @@ ) from sentry.seer.fetch_issues import by_error_type, by_function_name, by_text_query, utils from sentry.seer.issue_detection import create_issue_occurrence +from sentry.seer.models.seer_api_models import SeerProjectPreference from sentry.seer.utils import filter_repo_by_provider from sentry.sentry_apps.metrics import SentryAppEventType from sentry.sentry_apps.tasks.sentry_apps import broadcast_webhooks_for_organization @@ -572,7 +578,7 @@ def send_seer_webhook(*, event_name: str, organization_id: int, payload: dict) - def trigger_coding_agent_launch( *, organization_id: int, - project_id: int, + project_id: int | None = None, integration_id: int, run_id: int, trigger_source: str = "solution", @@ -610,10 +616,14 @@ def trigger_coding_agent_launch( project = Project.objects.get_from_cache(id=project_id) organization = Organization.objects.get_from_cache(id=organization_id) if features.has("organizations:seer-project-settings-dual-write", organization): - project.delete_option("sentry:seer_automation_handoff_point") - project.delete_option("sentry:seer_automation_handoff_target") - project.delete_option("sentry:seer_automation_handoff_integration_id") - project.delete_option("sentry:seer_automation_handoff_auto_create_pr") + preference_response = get_project_seer_preferences(project.id) + if preference_response and preference_response.preference: + updated_preference = preference_response.preference.copy( + update={"automation_handoff": None} + ) + validated_pref = SeerProjectPreference.validate(updated_preference) + resolved_pref = resolve_repository_ids(organization.id, [validated_pref]) + write_preference_to_sentry_db(project, resolved_pref[0]) except Exception: logger.exception( "coding_agent.clear_handoff_preference_failed", diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 76e664fac8fc74..80e6e3e9c369a5 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -1532,11 +1532,36 @@ def test_autofix_state_not_found_does_not_return_integration_error_code(self, mo class TestTriggerCodingAgentLaunchClearsHandoff(APITestCase): + def _make_preference_response(self): + from sentry.seer.models.seer_api_models import ( + AutofixHandoffPoint, + SeerAutomationHandoffConfiguration, + SeerProjectPreference, + SeerRawPreferenceResponse, + ) + + return SeerRawPreferenceResponse( + preference=SeerProjectPreference( + organization_id=self.organization.id, + project_id=self.project.id, + repositories=[], + automation_handoff=SeerAutomationHandoffConfiguration( + handoff_point=AutofixHandoffPoint.ROOT_CAUSE, + target="cursor_background_agent", + integration_id=42, + ), + ) + ) + + @patch("sentry.seer.endpoints.seer_rpc.get_project_seer_preferences") @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") - def test_integration_not_found_clears_handoff_project_options(self, mock_launch): + def test_integration_not_found_clears_handoff_project_options( + self, mock_launch, mock_get_prefs + ): from sentry.seer.autofix.coding_agent import IntegrationNotFound mock_launch.side_effect = IntegrationNotFound() + mock_get_prefs.return_value = self._make_preference_response() self.project.update_option("sentry:seer_automation_handoff_point", "root_cause") self.project.update_option( @@ -1559,11 +1584,15 @@ def test_integration_not_found_clears_handoff_project_options(self, mock_launch) assert self.project.get_option("sentry:seer_automation_handoff_integration_id") is None assert self.project.get_option("sentry:seer_automation_handoff_auto_create_pr") is False + @patch("sentry.seer.endpoints.seer_rpc.get_project_seer_preferences") @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") - def test_integration_not_found_skips_clear_without_feature_flag(self, mock_launch): + def test_integration_not_found_skips_clear_without_feature_flag( + self, mock_launch, mock_get_prefs + ): from sentry.seer.autofix.coding_agent import IntegrationNotFound mock_launch.side_effect = IntegrationNotFound() + mock_get_prefs.return_value = self._make_preference_response() self.project.update_option("sentry:seer_automation_handoff_point", "root_cause") @@ -1576,3 +1605,4 @@ def test_integration_not_found_skips_clear_without_feature_flag(self, mock_launc assert result == {"success": False, "error_code": "integration_not_found"} assert self.project.get_option("sentry:seer_automation_handoff_point") == "root_cause" + mock_get_prefs.assert_not_called()