From 42b9053802834b44df046cd8d59d04a09bcfe25d Mon Sep 17 00:00:00 2001 From: srest2021 Date: Thu, 26 Mar 2026 16:28:56 -0700 Subject: [PATCH 01/31] feat(seer): Add org-level default stopping point and wire coding agent defaults into project creation Register new org option `sentry:default_stopping_point` and wire all org-level Seer defaults (stopping point, coding agent, auto_open_prs) into project creation and existing-org migration. When auto_open_prs is true, stopping point is forced to open_pr. External agents (Cursor/Claude) now get automation_handoff set on new projects. Also adds input validation for defaultCodingAgent with alias mapping (cursor -> cursor_background_agent, claude_code -> claude_code_agent) and ChoiceField validation for defaultAutomatedRunStoppingPoint. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- .../api/serializers/models/organization.py | 6 +++ .../apidocs/examples/organization_examples.py | 1 + src/sentry/constants.py | 1 + .../core/endpoints/organization_details.py | 26 +++++++++- src/sentry/seer/similarity/utils.py | 51 ++++++++++++++++--- src/sentry/tasks/seer/autofix.py | 38 ++++++++++++-- 6 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index b06d6aa591ff27..1906cea0617707 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -58,6 +58,7 @@ ROLLBACK_ENABLED_DEFAULT, SAMPLING_MODE_DEFAULT, SCRAPE_JAVASCRIPT_DEFAULT, + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, SEER_DEFAULT_CODING_AGENT_DEFAULT, TARGET_SAMPLE_RATE_DEFAULT, ObjectStatus, @@ -560,6 +561,7 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp enableSeerCoding: bool defaultCodingAgent: str | None defaultCodingAgentIntegrationId: int | None + defaultAutomatedRunStoppingPoint: str autoEnableCodeReview: bool autoOpenPrs: bool defaultCodeReviewTriggers: list[str] @@ -741,6 +743,10 @@ def serialize( # type: ignore[override] "sentry:seer_default_coding_agent_integration_id", None, ), + "defaultAutomatedRunStoppingPoint": obj.get_option( + "sentry:default_automated_run_stopping_point", + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + ), "autoOpenPrs": bool( obj.get_option( "sentry:auto_open_prs", diff --git a/src/sentry/apidocs/examples/organization_examples.py b/src/sentry/apidocs/examples/organization_examples.py index 67acd600769107..f84eab2739982c 100644 --- a/src/sentry/apidocs/examples/organization_examples.py +++ b/src/sentry/apidocs/examples/organization_examples.py @@ -306,6 +306,7 @@ class OrganizationExamples: "autoOpenPrs": False, "defaultCodingAgent": None, "defaultCodingAgentIntegrationId": None, + "defaultAutomatedRunStoppingPoint": "open_pr", "issueAlertsThreadFlag": True, "metricAlertsThreadFlag": True, "trustedRelays": [], diff --git a/src/sentry/constants.py b/src/sentry/constants.py index f2aff76cadff2d..8f87a68360ca4f 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -730,6 +730,7 @@ class InsightModules(Enum): "on_new_commit", ] SEER_DEFAULT_CODING_AGENT_DEFAULT = "seer" +SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes" SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes" ENABLED_CONSOLE_PLATFORMS_DEFAULT: list[str] = [] CONSOLE_SDK_INVITE_QUOTA_DEFAULT = 0 diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index 5eecb3461606ec..fe985eb87c9c39 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -68,6 +68,7 @@ ROLLBACK_ENABLED_DEFAULT, SAMPLING_MODE_DEFAULT, SCRAPE_JAVASCRIPT_DEFAULT, + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, SEER_DEFAULT_CODING_AGENT_DEFAULT, TARGET_SAMPLE_RATE_DEFAULT, ObjectStatus, @@ -260,6 +261,12 @@ bool, AUTO_OPEN_PRS_DEFAULT, ), + ( + "defaultAutomatedRunStoppingPoint", + "sentry:default_automated_run_stopping_point", + str, + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + ), ( "autoEnableCodeReview", "sentry:auto_enable_code_review", @@ -371,8 +378,16 @@ class OrganizationSerializer(BaseOrganizationSerializer): dashboardsAsyncQueueParallelLimit = serializers.IntegerField(required=False, min_value=1) enableSeerEnhancedAlerts = serializers.BooleanField(required=False) enableSeerCoding = serializers.BooleanField(required=False) - defaultCodingAgent = serializers.CharField(required=False, allow_null=True) + defaultCodingAgent = serializers.ChoiceField( + choices=["seer", "cursor", "claude_code", "cursor_background_agent", "claude_code_agent"], + required=False, + allow_null=True, + ) defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True) + defaultAutomatedRunStoppingPoint = serializers.ChoiceField( + choices=["root_cause", "solution", "code_changes", "open_pr"], + required=False, + ) autoOpenPrs = serializers.BooleanField(required=False) autoEnableCodeReview = serializers.BooleanField(required=False) defaultCodeReviewTriggers = serializers.ListField( @@ -401,6 +416,15 @@ def validate_relayPiiConfig(self, value): organization = self.context["organization"] return validate_pii_config_update(organization, value) + def validate_defaultCodingAgent(self, value: str | None) -> str: + coding_agent_aliases: dict[str, str] = { + "cursor": "cursor_background_agent", + "claude_code": "claude_code_agent", + } + if value is None: + return SEER_DEFAULT_CODING_AGENT_DEFAULT + return coding_agent_aliases.get(value, value) + def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | None: if value is None: return None diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index 448ab305a0e13f..b090801b9bd894 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -9,7 +9,12 @@ from tokenizers import Tokenizer from sentry import features, options -from sentry.constants import DATA_ROOT +from sentry.constants import ( + AUTO_OPEN_PRS_DEFAULT, + DATA_ROOT, + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_DEFAULT_CODING_AGENT_DEFAULT, +) from sentry.grouping.api import get_contributing_variant_and_component from sentry.grouping.grouping_info import get_grouping_info_from_variants_legacy from sentry.grouping.variants import BaseVariant @@ -23,7 +28,11 @@ set_project_seer_preference, write_preference_to_sentry_db, ) -from sentry.seer.models import SeerProjectPreference +from sentry.seer.models import ( + AutofixHandoffPoint, + SeerAutomationHandoffConfiguration, + SeerProjectPreference, +) from sentry.seer.similarity.types import GroupingVersion from sentry.services.eventstore.models import Event, GroupEvent from sentry.utils import metrics @@ -564,21 +573,51 @@ def set_default_project_seer_scanner_automation( def set_default_project_auto_open_prs(organization: Organization, project: Project) -> None: - """Called once at project creation time to set the initial auto open PRs.""" + """Called once at project creation time to set the initial automated run stopping + point and automation handoff. + + Reads org options (default_automated_run_stopping_point, auto_open_prs, default_coding_agent, + default_coding_agent_integration_id) and writes the corresponding project-level + options (stopping point, handoff config). + + When auto_open_prs is True, stopping_point is forced to open_pr regardless of + default_stopping_point. + """ if not is_seer_seat_based_tier_enabled(organization): return - stopping_point = AutofixStoppingPoint.CODE_CHANGES - if organization.get_option("sentry:auto_open_prs"): + auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)) + if auto_open_prs: stopping_point = AutofixStoppingPoint.OPEN_PR + else: + stopping_point = organization.get_option( + "sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) + + coding_agent = organization.get_option( + "sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT + ) + coding_agent_integration_id = organization.get_option( + "sentry:seer_default_coding_agent_integration_id", None + ) + + automation_handoff: SeerAutomationHandoffConfiguration | None = None + if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: + automation_handoff = SeerAutomationHandoffConfiguration( + handoff_point=AutofixHandoffPoint.ROOT_CAUSE, + target=coding_agent, + integration_id=coding_agent_integration_id, + auto_create_pr=auto_open_prs, + ) - # We need to make an API call to Seer to set this preference preference = SeerProjectPreference( organization_id=organization.id, project_id=project.id, repositories=[], automated_run_stopping_point=stopping_point, + automation_handoff=automation_handoff, ) + try: set_project_seer_preference(preference) except Exception as e: diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 8671d98bee3281..f8ffa1e93d8451 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -1,5 +1,6 @@ import logging from datetime import datetime, timedelta +from typing import Any import sentry_sdk from django.utils import timezone @@ -8,7 +9,12 @@ from sentry import analytics, features from sentry.analytics.events.autofix_automation_events import AiAutofixAutomationEvent -from sentry.constants import ObjectStatus +from sentry.constants import ( + AUTO_OPEN_PRS_DEFAULT, + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_DEFAULT_CODING_AGENT_DEFAULT, + ObjectStatus, +) from sentry.models.group import Group from sentry.models.organization import Organization from sentry.models.project import Project @@ -238,6 +244,30 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM ) + auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)) + if auto_open_prs: + default_stopping_point = "open_pr" + else: + default_stopping_point = organization.get_option( + "sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) + + coding_agent = organization.get_option( + "sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT + ) + coding_agent_integration_id = organization.get_option( + "sentry:seer_default_coding_agent_integration_id", None + ) + + default_handoff: dict[str, Any] | None = None + if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: + default_handoff = { + "handoff_point": "root_cause", + "target": coding_agent, + "integration_id": coding_agent_integration_id, + "auto_create_pr": auto_open_prs, + } + preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) # Determine which projects need updates @@ -262,9 +292,11 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "organization_id": organization_id, "project_id": project_id, "repositories": repositories or [], - "automated_run_stopping_point": "code_changes", + "automated_run_stopping_point": default_stopping_point, "automation_handoff": ( - existing_pref.get("automation_handoff") if existing_pref else None + existing_pref.get("automation_handoff") + if existing_pref and existing_pref.get("automation_handoff") + else default_handoff ), } ) From 007a027000bc3bae3d96a11cb4fb4933eaae3d5d Mon Sep 17 00:00:00 2001 From: srest2021 Date: Thu, 26 Mar 2026 16:37:58 -0700 Subject: [PATCH 02/31] test(seer): Add tests for set_default_project_auto_open_prs Cover all org-default-to-project mapping cases: Seer agent with default and custom stopping points, auto_open_prs forcing open_pr, external agents (Cursor/Claude) with handoff config, missing integration ID edge case, API error handling, and dual-write feature flag gating. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- tests/sentry/seer/similarity/test_utils.py | 138 +++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/tests/sentry/seer/similarity/test_utils.py b/tests/sentry/seer/similarity/test_utils.py index 2ae2a6f77bca3f..6d8253f488bc7a 100644 --- a/tests/sentry/seer/similarity/test_utils.py +++ b/tests/sentry/seer/similarity/test_utils.py @@ -15,6 +15,7 @@ filter_null_from_string, get_stacktrace_string, get_token_count, + set_default_project_auto_open_prs, stacktrace_exceeds_limits, ) from sentry.services.eventstore.models import Event @@ -1220,3 +1221,140 @@ def test_generates_stacktrace_string_from_variants(self) -> None: assert token_count > 0 # Verify we get the expected token count for this specific stacktrace assert token_count == 33 + + +class TestSetDefaultProjectAutoOpenPrs(TestCase): + """Tests for set_default_project_auto_open_prs which wires org-level Seer + defaults (stopping point, coding agent, auto_open_prs) into project-level + preferences at project creation time. + """ + + def setUp(self): + super().setUp() + self.project = self.create_project(organization=self.organization) + + @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") + @patch("sentry.seer.similarity.utils.set_project_seer_preference") + @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=False) + def test_skips_when_tier_not_enabled( + self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock + ): + set_default_project_auto_open_prs(self.organization, self.project) + mock_set_pref.assert_not_called() + mock_dual_write.assert_not_called() + + @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") + @patch("sentry.seer.similarity.utils.set_project_seer_preference") + @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + def test_seer_agent_default_stopping_point( + self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock + ): + """Seer agent, no auto_open_prs, default stopping point (code_changes).""" + set_default_project_auto_open_prs(self.organization, self.project) + + pref = mock_set_pref.call_args[0][0] + assert pref.automated_run_stopping_point == "code_changes" + assert pref.automation_handoff is None + + @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") + @patch("sentry.seer.similarity.utils.set_project_seer_preference") + @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + def test_seer_agent_custom_stopping_point( + self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock + ): + """Seer agent, no auto_open_prs, custom stopping point (root_cause).""" + self.organization.update_option("sentry:default_stopping_point", "root_cause") + + set_default_project_auto_open_prs(self.organization, self.project) + + pref = mock_set_pref.call_args[0][0] + assert pref.automated_run_stopping_point == "root_cause" + assert pref.automation_handoff is None + + @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") + @patch("sentry.seer.similarity.utils.set_project_seer_preference") + @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + def test_seer_agent_with_auto_open_prs( + self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock + ): + """auto_open_prs=True forces stopping point to open_pr regardless of default.""" + self.organization.update_option("sentry:auto_open_prs", True) + self.organization.update_option("sentry:default_stopping_point", "root_cause") + + set_default_project_auto_open_prs(self.organization, self.project) + + pref = mock_set_pref.call_args[0][0] + assert pref.automated_run_stopping_point == "open_pr" + assert pref.automation_handoff is None + + @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") + @patch("sentry.seer.similarity.utils.set_project_seer_preference") + @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + def test_external_agent_no_auto_open_prs( + self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock + ): + agents = [ + ("cursor_background_agent", 1234), + ("claude_code_agent", 5678), + ] + for agent, integration_id in agents: + with self.subTest(agent=agent): + mock_set_pref.reset_mock() + self.organization.update_option("sentry:seer_default_coding_agent", agent) + self.organization.update_option( + "sentry:seer_default_coding_agent_integration_id", integration_id + ) + + set_default_project_auto_open_prs(self.organization, self.project) + + pref = mock_set_pref.call_args[0][0] + assert pref.automated_run_stopping_point == "code_changes" + assert pref.automation_handoff is not None + assert pref.automation_handoff.handoff_point == "root_cause" + assert pref.automation_handoff.target == agent + assert pref.automation_handoff.integration_id == integration_id + assert pref.automation_handoff.auto_create_pr is False + + @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") + @patch("sentry.seer.similarity.utils.set_project_seer_preference") + @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + def test_external_agent_with_auto_open_prs( + self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock + ): + """auto_open_prs=True forces open_pr and sets auto_create_pr on handoff.""" + self.organization.update_option("sentry:auto_open_prs", True) + agents = [ + ("cursor_background_agent", 1234), + ("claude_code_agent", 5678), + ] + for agent, integration_id in agents: + with self.subTest(agent=agent): + mock_set_pref.reset_mock() + self.organization.update_option("sentry:seer_default_coding_agent", agent) + self.organization.update_option( + "sentry:seer_default_coding_agent_integration_id", integration_id + ) + + set_default_project_auto_open_prs(self.organization, self.project) + + pref = mock_set_pref.call_args[0][0] + assert pref.automated_run_stopping_point == "open_pr" + assert pref.automation_handoff is not None + assert pref.automation_handoff.target == agent + assert pref.automation_handoff.integration_id == integration_id + assert pref.automation_handoff.auto_create_pr is True + + @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") + @patch("sentry.seer.similarity.utils.set_project_seer_preference") + @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + def test_external_agent_without_integration_id_skips_handoff( + self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock + ): + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) + + set_default_project_auto_open_prs(self.organization, self.project) + + pref = mock_set_pref.call_args[0][0] + assert pref.automation_handoff is None From 52fb592f06b9412af399f3871192e3b6cd55cc81 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 27 Mar 2026 11:00:30 -0700 Subject: [PATCH 03/31] preserve original stopping point --- src/sentry/seer/similarity/utils.py | 13 +++---------- src/sentry/tasks/seer/autofix.py | 9 +++------ tests/sentry/seer/similarity/test_utils.py | 15 ++++++++------- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index b090801b9bd894..3b617230097420 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -23,7 +23,6 @@ from sentry.models.project import Project from sentry.seer.autofix.constants import AutofixAutomationTuningSettings from sentry.seer.autofix.utils import ( - AutofixStoppingPoint, is_seer_seat_based_tier_enabled, set_project_seer_preference, write_preference_to_sentry_db, @@ -579,20 +578,14 @@ def set_default_project_auto_open_prs(organization: Organization, project: Proje Reads org options (default_automated_run_stopping_point, auto_open_prs, default_coding_agent, default_coding_agent_integration_id) and writes the corresponding project-level options (stopping point, handoff config). - - When auto_open_prs is True, stopping_point is forced to open_pr regardless of - default_stopping_point. """ if not is_seer_seat_based_tier_enabled(organization): return auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)) - if auto_open_prs: - stopping_point = AutofixStoppingPoint.OPEN_PR - else: - stopping_point = organization.get_option( - "sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT - ) + stopping_point = organization.get_option( + "sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) coding_agent = organization.get_option( "sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index f8ffa1e93d8451..62c9059d402337 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -245,12 +245,9 @@ def configure_seer_for_existing_org(organization_id: int) -> None: ) auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)) - if auto_open_prs: - default_stopping_point = "open_pr" - else: - default_stopping_point = organization.get_option( - "sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT - ) + default_stopping_point = organization.get_option( + "sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) coding_agent = organization.get_option( "sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT diff --git a/tests/sentry/seer/similarity/test_utils.py b/tests/sentry/seer/similarity/test_utils.py index 6d8253f488bc7a..0418c3c87a5cba 100644 --- a/tests/sentry/seer/similarity/test_utils.py +++ b/tests/sentry/seer/similarity/test_utils.py @@ -1246,7 +1246,7 @@ def test_skips_when_tier_not_enabled( @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") @patch("sentry.seer.similarity.utils.set_project_seer_preference") @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_seer_agent_default_stopping_point( + def test_seer_agent_default( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): """Seer agent, no auto_open_prs, default stopping point (code_changes).""" @@ -1259,7 +1259,7 @@ def test_seer_agent_default_stopping_point( @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") @patch("sentry.seer.similarity.utils.set_project_seer_preference") @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_seer_agent_custom_stopping_point( + def test_seer_agent_with_custom_stopping_point( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): """Seer agent, no auto_open_prs, custom stopping point (root_cause).""" @@ -1277,20 +1277,20 @@ def test_seer_agent_custom_stopping_point( def test_seer_agent_with_auto_open_prs( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): - """auto_open_prs=True forces stopping point to open_pr regardless of default.""" + """auto_open_prs does not override stopping point.""" self.organization.update_option("sentry:auto_open_prs", True) self.organization.update_option("sentry:default_stopping_point", "root_cause") set_default_project_auto_open_prs(self.organization, self.project) pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "open_pr" + assert pref.automated_run_stopping_point == "root_cause" assert pref.automation_handoff is None @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") @patch("sentry.seer.similarity.utils.set_project_seer_preference") @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_external_agent_no_auto_open_prs( + def test_external_agent_default( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): agents = [ @@ -1321,8 +1321,9 @@ def test_external_agent_no_auto_open_prs( def test_external_agent_with_auto_open_prs( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): - """auto_open_prs=True forces open_pr and sets auto_create_pr on handoff.""" + """auto_open_prs sets auto_create_pr on handoff but does not override stopping point.""" self.organization.update_option("sentry:auto_open_prs", True) + self.organization.update_option("sentry:default_stopping_point", "root_cause") agents = [ ("cursor_background_agent", 1234), ("claude_code_agent", 5678), @@ -1338,7 +1339,7 @@ def test_external_agent_with_auto_open_prs( set_default_project_auto_open_prs(self.organization, self.project) pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "open_pr" + assert pref.automated_run_stopping_point == "root_cause" assert pref.automation_handoff is not None assert pref.automation_handoff.target == agent assert pref.automation_handoff.integration_id == integration_id From 91b53a80047fab4cd3e28c3589bfc2b9ee2b3f8e Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 27 Mar 2026 11:19:20 -0700 Subject: [PATCH 04/31] fixes --- src/sentry/seer/similarity/utils.py | 11 ++++++----- src/sentry/tasks/seer/autofix.py | 3 ++- tests/sentry/seer/similarity/test_utils.py | 9 +++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index 3b617230097420..f552ba0e2ce0e6 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -582,11 +582,6 @@ def set_default_project_auto_open_prs(organization: Organization, project: Proje if not is_seer_seat_based_tier_enabled(organization): return - auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)) - stopping_point = organization.get_option( - "sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT - ) - coding_agent = organization.get_option( "sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT ) @@ -594,6 +589,12 @@ def set_default_project_auto_open_prs(organization: Organization, project: Proje "sentry:seer_default_coding_agent_integration_id", None ) + auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)) + stopping_point = organization.get_option( + "sentry:default_automated_run_stopping_point", + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + ) + automation_handoff: SeerAutomationHandoffConfiguration | None = None if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: automation_handoff = SeerAutomationHandoffConfiguration( diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 62c9059d402337..224c763d30cee8 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -246,7 +246,8 @@ def configure_seer_for_existing_org(organization_id: int) -> None: auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)) default_stopping_point = organization.get_option( - "sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + "sentry:default_automated_run_stopping_point", + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ) coding_agent = organization.get_option( diff --git a/tests/sentry/seer/similarity/test_utils.py b/tests/sentry/seer/similarity/test_utils.py index 0418c3c87a5cba..acb027609c193a 100644 --- a/tests/sentry/seer/similarity/test_utils.py +++ b/tests/sentry/seer/similarity/test_utils.py @@ -1263,7 +1263,7 @@ def test_seer_agent_with_custom_stopping_point( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): """Seer agent, no auto_open_prs, custom stopping point (root_cause).""" - self.organization.update_option("sentry:default_stopping_point", "root_cause") + self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") set_default_project_auto_open_prs(self.organization, self.project) @@ -1277,9 +1277,9 @@ def test_seer_agent_with_custom_stopping_point( def test_seer_agent_with_auto_open_prs( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): - """auto_open_prs does not override stopping point.""" + """auto_open_prs does not override contradictory stopping point.""" self.organization.update_option("sentry:auto_open_prs", True) - self.organization.update_option("sentry:default_stopping_point", "root_cause") + self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") set_default_project_auto_open_prs(self.organization, self.project) @@ -1293,6 +1293,7 @@ def test_seer_agent_with_auto_open_prs( def test_external_agent_default( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): + """external agent, no auto_open_prs, default stopping point and handoff.""" agents = [ ("cursor_background_agent", 1234), ("claude_code_agent", 5678), @@ -1323,7 +1324,7 @@ def test_external_agent_with_auto_open_prs( ): """auto_open_prs sets auto_create_pr on handoff but does not override stopping point.""" self.organization.update_option("sentry:auto_open_prs", True) - self.organization.update_option("sentry:default_stopping_point", "root_cause") + self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") agents = [ ("cursor_background_agent", 1234), ("claude_code_agent", 5678), From 83f31ca69c480250d76a9af3690f336327a03530 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 27 Mar 2026 11:34:23 -0700 Subject: [PATCH 05/31] org details tests --- .../core/endpoints/organization_details.py | 6 +-- src/sentry/seer/similarity/utils.py | 18 +++---- src/sentry/tasks/seer/autofix.py | 18 +++---- .../endpoints/test_organization_details.py | 50 +++++++++++++++++++ 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index fe985eb87c9c39..3939ae34300ed0 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -416,13 +416,13 @@ def validate_relayPiiConfig(self, value): organization = self.context["organization"] return validate_pii_config_update(organization, value) - def validate_defaultCodingAgent(self, value: str | None) -> str: + def validate_defaultCodingAgent(self, value: str | None) -> str | None: + if value is None: + return None coding_agent_aliases: dict[str, str] = { "cursor": "cursor_background_agent", "claude_code": "claude_code_agent", } - if value is None: - return SEER_DEFAULT_CODING_AGENT_DEFAULT return coding_agent_aliases.get(value, value) def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | None: diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index f552ba0e2ce0e6..eb8e65e213352a 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -588,27 +588,25 @@ def set_default_project_auto_open_prs(organization: Organization, project: Proje coding_agent_integration_id = organization.get_option( "sentry:seer_default_coding_agent_integration_id", None ) - - auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)) - stopping_point = organization.get_option( - "sentry:default_automated_run_stopping_point", - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - ) - automation_handoff: SeerAutomationHandoffConfiguration | None = None - if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: + if coding_agent and coding_agent_integration_id is not None: automation_handoff = SeerAutomationHandoffConfiguration( handoff_point=AutofixHandoffPoint.ROOT_CAUSE, target=coding_agent, integration_id=coding_agent_integration_id, - auto_create_pr=auto_open_prs, + auto_create_pr=bool( + organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) + ), ) preference = SeerProjectPreference( organization_id=organization.id, project_id=project.id, repositories=[], - automated_run_stopping_point=stopping_point, + automated_run_stopping_point=organization.get_option( + "sentry:default_automated_run_stopping_point", + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + ), automation_handoff=automation_handoff, ) diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 224c763d30cee8..40492e4a8f28e0 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -244,26 +244,21 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM ) - auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)) - default_stopping_point = organization.get_option( - "sentry:default_automated_run_stopping_point", - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - ) - coding_agent = organization.get_option( "sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT ) coding_agent_integration_id = organization.get_option( "sentry:seer_default_coding_agent_integration_id", None ) - default_handoff: dict[str, Any] | None = None - if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: + if coding_agent and coding_agent_integration_id is not None: default_handoff = { "handoff_point": "root_cause", "target": coding_agent, "integration_id": coding_agent_integration_id, - "auto_create_pr": auto_open_prs, + "auto_create_pr": bool( + organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) + ), } preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) @@ -290,7 +285,10 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "organization_id": organization_id, "project_id": project_id, "repositories": repositories or [], - "automated_run_stopping_point": default_stopping_point, + "automated_run_stopping_point": organization.get_option( + "sentry:default_automated_run_stopping_point", + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + ), "automation_handoff": ( existing_pref.get("automation_handoff") if existing_pref and existing_pref.get("automation_handoff") diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index c6c29cde2c2a73..d497216ed3325d 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -21,6 +21,7 @@ from sentry.auth.authenticators.totp import TotpInterface from sentry.constants import ( RESERVED_ORGANIZATION_SLUGS, + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, SEER_DEFAULT_CODING_AGENT_DEFAULT, ObjectStatus, ) @@ -1588,6 +1589,55 @@ def test_default_coding_agent_can_be_cleared(self) -> None: assert self.organization.get_option("sentry:seer_default_coding_agent") is None assert response.data["defaultCodingAgent"] is None + def test_default_coding_agent_cursor_values(self) -> None: + for value in ("cursor", "cursor_background_agent"): + data = {"defaultCodingAgent": value} + response = self.get_success_response(self.organization.slug, **data) + assert ( + self.organization.get_option("sentry:seer_default_coding_agent") + == "cursor_background_agent" + ) + assert response.data["defaultCodingAgent"] == "cursor_background_agent" + + def test_default_coding_agent_claude_values(self) -> None: + for value in ("claude_code", "claude_code_agent"): + data = {"defaultCodingAgent": value} + response = self.get_success_response(self.organization.slug, **data) + assert ( + self.organization.get_option("sentry:seer_default_coding_agent") + == "claude_code_agent" + ) + assert response.data["defaultCodingAgent"] == "claude_code_agent" + + def test_default_coding_agent_rejects_invalid_choice(self) -> None: + data = {"defaultCodingAgent": "invalid_agent"} + self.get_error_response(self.organization.slug, status_code=400, **data) + + def test_default_automated_run_stopping_point_default(self) -> None: + response = self.get_success_response(self.organization.slug) + assert ( + response.data["defaultAutomatedRunStoppingPoint"] + == SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) + + def test_default_automated_run_stopping_point_can_be_set(self) -> None: + data = {"defaultAutomatedRunStoppingPoint": "open_pr"} + response = self.get_success_response(self.organization.slug, **data) + assert ( + self.organization.get_option("sentry:default_automated_run_stopping_point") == "open_pr" + ) + assert response.data["defaultAutomatedRunStoppingPoint"] == "open_pr" + + def test_default_automated_run_stopping_point_all_choices(self) -> None: + for choice in ("root_cause", "solution", "code_changes", "open_pr"): + data = {"defaultAutomatedRunStoppingPoint": choice} + response = self.get_success_response(self.organization.slug, **data) + assert response.data["defaultAutomatedRunStoppingPoint"] == choice + + def test_default_automated_run_stopping_point_rejects_invalid(self) -> None: + data = {"defaultAutomatedRunStoppingPoint": "invalid_point"} + self.get_error_response(self.organization.slug, status_code=400, **data) + def test_default_coding_agent_integration_id_can_be_cleared(self) -> None: self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 123) data = {"defaultCodingAgentIntegrationId": None} From 3564251c18440cc126e7bd7ba29f40e58e853c82 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 27 Mar 2026 11:44:43 -0700 Subject: [PATCH 06/31] coding agent none default --- src/sentry/api/serializers/models/organization.py | 9 ++------- src/sentry/constants.py | 1 - src/sentry/core/endpoints/organization_details.py | 8 +------- src/sentry/seer/similarity/utils.py | 8 +++----- src/sentry/tasks/seer/autofix.py | 7 ++----- .../sentry/core/endpoints/test_organization_details.py | 10 +++------- 6 files changed, 11 insertions(+), 32 deletions(-) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index 1906cea0617707..87098cb9a64e95 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -59,7 +59,6 @@ SAMPLING_MODE_DEFAULT, SCRAPE_JAVASCRIPT_DEFAULT, SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - SEER_DEFAULT_CODING_AGENT_DEFAULT, TARGET_SAMPLE_RATE_DEFAULT, ObjectStatus, ) @@ -735,13 +734,9 @@ def serialize( # type: ignore[override] ENABLE_SEER_CODING_DEFAULT, ) ), - "defaultCodingAgent": obj.get_option( - "sentry:seer_default_coding_agent", - SEER_DEFAULT_CODING_AGENT_DEFAULT, - ), + "defaultCodingAgent": obj.get_option("sentry:seer_default_coding_agent", None), "defaultCodingAgentIntegrationId": obj.get_option( - "sentry:seer_default_coding_agent_integration_id", - None, + "sentry:seer_default_coding_agent_integration_id", None ), "defaultAutomatedRunStoppingPoint": obj.get_option( "sentry:default_automated_run_stopping_point", diff --git a/src/sentry/constants.py b/src/sentry/constants.py index 8f87a68360ca4f..da02ebaf21dc41 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -729,7 +729,6 @@ class InsightModules(Enum): "on_ready_for_review", "on_new_commit", ] -SEER_DEFAULT_CODING_AGENT_DEFAULT = "seer" SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes" SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes" ENABLED_CONSOLE_PLATFORMS_DEFAULT: list[str] = [] diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index 3939ae34300ed0..7a12fe59e6d462 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -69,7 +69,6 @@ SAMPLING_MODE_DEFAULT, SCRAPE_JAVASCRIPT_DEFAULT, SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - SEER_DEFAULT_CODING_AGENT_DEFAULT, TARGET_SAMPLE_RATE_DEFAULT, ObjectStatus, ) @@ -242,12 +241,7 @@ bool, ENABLE_SEER_CODING_DEFAULT, ), - ( - "defaultCodingAgent", - "sentry:seer_default_coding_agent", - str, - SEER_DEFAULT_CODING_AGENT_DEFAULT, - ), + ("defaultCodingAgent", "sentry:seer_default_coding_agent", str | None, None), ( "defaultCodingAgentIntegrationId", "sentry:seer_default_coding_agent_integration_id", diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index eb8e65e213352a..be1b6c3dafc603 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -13,7 +13,6 @@ AUTO_OPEN_PRS_DEFAULT, DATA_ROOT, SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - SEER_DEFAULT_CODING_AGENT_DEFAULT, ) from sentry.grouping.api import get_contributing_variant_and_component from sentry.grouping.grouping_info import get_grouping_info_from_variants_legacy @@ -582,11 +581,9 @@ def set_default_project_auto_open_prs(organization: Organization, project: Proje if not is_seer_seat_based_tier_enabled(organization): return - coding_agent = organization.get_option( - "sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT - ) + coding_agent = organization.get_option("sentry:seer_default_coding_agent") coding_agent_integration_id = organization.get_option( - "sentry:seer_default_coding_agent_integration_id", None + "sentry:seer_default_coding_agent_integration_id" ) automation_handoff: SeerAutomationHandoffConfiguration | None = None if coding_agent and coding_agent_integration_id is not None: @@ -599,6 +596,7 @@ def set_default_project_auto_open_prs(organization: Organization, project: Proje ), ) + # We need to make an API call to Seer to set this preference preference = SeerProjectPreference( organization_id=organization.id, project_id=project.id, diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 40492e4a8f28e0..98c9afbb593f63 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -12,7 +12,6 @@ from sentry.constants import ( AUTO_OPEN_PRS_DEFAULT, SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - SEER_DEFAULT_CODING_AGENT_DEFAULT, ObjectStatus, ) from sentry.models.group import Group @@ -244,11 +243,9 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM ) - coding_agent = organization.get_option( - "sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT - ) + coding_agent = organization.get_option("sentry:seer_default_coding_agent") coding_agent_integration_id = organization.get_option( - "sentry:seer_default_coding_agent_integration_id", None + "sentry:seer_default_coding_agent_integration_id" ) default_handoff: dict[str, Any] | None = None if coding_agent and coding_agent_integration_id is not None: diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index d497216ed3325d..b00cafd44bdaee 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -22,7 +22,6 @@ from sentry.constants import ( RESERVED_ORGANIZATION_SLUGS, SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - SEER_DEFAULT_CODING_AGENT_DEFAULT, ObjectStatus, ) from sentry.core.endpoints.organization_details import ERR_NO_2FA, ERR_SSO_ENABLED @@ -1498,7 +1497,7 @@ def test_enable_seer_coding_cannot_be_enabled_when_flag_enabled(self) -> None: def test_default_coding_agent_default(self) -> None: response = self.get_success_response(self.organization.slug) - assert response.data["defaultCodingAgent"] == SEER_DEFAULT_CODING_AGENT_DEFAULT + assert response.data["defaultCodingAgent"] is None def test_default_coding_agent_can_be_set(self) -> None: data = {"defaultCodingAgent": "seer"} @@ -1522,13 +1521,10 @@ def test_default_coding_agent_writing_default_value_stores_but_skips_audit_log( with assume_test_silo_mode_of(AuditLogEntry): AuditLogEntry.objects.filter(organization_id=self.organization.id).delete() - data = {"defaultCodingAgent": SEER_DEFAULT_CODING_AGENT_DEFAULT} + data = {"defaultCodingAgent": None} self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:seer_default_coding_agent") - == SEER_DEFAULT_CODING_AGENT_DEFAULT - ) + assert self.organization.get_option("sentry:seer_default_coding_agent") is None with assume_test_silo_mode_of(AuditLogEntry): assert not AuditLogEntry.objects.filter(organization_id=self.organization.id).exists() From b14d15438273d86f08c6383f7ce2b95d606ac6e9 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 27 Mar 2026 11:50:59 -0700 Subject: [PATCH 07/31] handle seer coding agent --- .../core/endpoints/organization_details.py | 2 +- .../endpoints/test_organization_details.py | 52 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index 7a12fe59e6d462..b8bcb0ef718143 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -411,7 +411,7 @@ def validate_relayPiiConfig(self, value): return validate_pii_config_update(organization, value) def validate_defaultCodingAgent(self, value: str | None) -> str | None: - if value is None: + if value == "seer" or value is None: return None coding_agent_aliases: dict[str, str] = { "cursor": "cursor_background_agent", diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index b00cafd44bdaee..ef3e010e12c9f5 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -1500,10 +1500,38 @@ def test_default_coding_agent_default(self) -> None: assert response.data["defaultCodingAgent"] is None def test_default_coding_agent_can_be_set(self) -> None: + data = {"defaultCodingAgent": "claude_code_agent"} + response = self.get_success_response(self.organization.slug, **data) + assert ( + self.organization.get_option("sentry:seer_default_coding_agent") == "claude_code_agent" + ) + assert response.data["defaultCodingAgent"] == "claude_code_agent" + + def test_default_coding_agent_seer(self) -> None: data = {"defaultCodingAgent": "seer"} response = self.get_success_response(self.organization.slug, **data) - assert self.organization.get_option("sentry:seer_default_coding_agent") == "seer" - assert response.data["defaultCodingAgent"] == "seer" + assert self.organization.get_option("sentry:seer_default_coding_agent") is None + assert response.data["defaultCodingAgent"] is None + + def test_default_coding_agent_cursor(self) -> None: + for value in ("cursor", "cursor_background_agent"): + data = {"defaultCodingAgent": value} + response = self.get_success_response(self.organization.slug, **data) + assert ( + self.organization.get_option("sentry:seer_default_coding_agent") + == "cursor_background_agent" + ) + assert response.data["defaultCodingAgent"] == "cursor_background_agent" + + def test_default_coding_agent_claude(self) -> None: + for value in ("claude_code", "claude_code_agent"): + data = {"defaultCodingAgent": value} + response = self.get_success_response(self.organization.slug, **data) + assert ( + self.organization.get_option("sentry:seer_default_coding_agent") + == "claude_code_agent" + ) + assert response.data["defaultCodingAgent"] == "claude_code_agent" def test_default_coding_agent_null_on_first_write_create_path(self) -> None: # Tests the create path (no OrganizationOption row exists yet): sending null @@ -1585,26 +1613,6 @@ def test_default_coding_agent_can_be_cleared(self) -> None: assert self.organization.get_option("sentry:seer_default_coding_agent") is None assert response.data["defaultCodingAgent"] is None - def test_default_coding_agent_cursor_values(self) -> None: - for value in ("cursor", "cursor_background_agent"): - data = {"defaultCodingAgent": value} - response = self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:seer_default_coding_agent") - == "cursor_background_agent" - ) - assert response.data["defaultCodingAgent"] == "cursor_background_agent" - - def test_default_coding_agent_claude_values(self) -> None: - for value in ("claude_code", "claude_code_agent"): - data = {"defaultCodingAgent": value} - response = self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:seer_default_coding_agent") - == "claude_code_agent" - ) - assert response.data["defaultCodingAgent"] == "claude_code_agent" - def test_default_coding_agent_rejects_invalid_choice(self) -> None: data = {"defaultCodingAgent": "invalid_agent"} self.get_error_response(self.organization.slug, status_code=400, **data) From 22f98fe33e43ec7a00c4e50904ea2a8822705391 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Fri, 27 Mar 2026 12:13:16 -0700 Subject: [PATCH 08/31] fix typing --- src/sentry/core/endpoints/organization_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index b8bcb0ef718143..23ce2405cc3891 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -241,7 +241,7 @@ bool, ENABLE_SEER_CODING_DEFAULT, ), - ("defaultCodingAgent", "sentry:seer_default_coding_agent", str | None, None), + ("defaultCodingAgent", "sentry:seer_default_coding_agent", str, None), ( "defaultCodingAgentIntegrationId", "sentry:seer_default_coding_agent_integration_id", From 7a0759fbe908942d919f4fb68754e498a0cda491 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 10:13:29 -0700 Subject: [PATCH 09/31] set default coding agent to seer, and cast null to seer --- .../api/serializers/models/organization.py | 5 ++- src/sentry/constants.py | 1 + .../core/endpoints/organization_details.py | 7 ++-- src/sentry/seer/similarity/utils.py | 2 +- src/sentry/tasks/seer/autofix.py | 2 +- .../endpoints/test_organization_details.py | 32 ++++++++++--------- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index 87098cb9a64e95..f6a68be384c60f 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -59,6 +59,7 @@ SAMPLING_MODE_DEFAULT, SCRAPE_JAVASCRIPT_DEFAULT, SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_DEFAULT_CODING_AGENT_DEFAULT, TARGET_SAMPLE_RATE_DEFAULT, ObjectStatus, ) @@ -734,7 +735,9 @@ def serialize( # type: ignore[override] ENABLE_SEER_CODING_DEFAULT, ) ), - "defaultCodingAgent": obj.get_option("sentry:seer_default_coding_agent", None), + "defaultCodingAgent": obj.get_option( + "sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT + ), "defaultCodingAgentIntegrationId": obj.get_option( "sentry:seer_default_coding_agent_integration_id", None ), diff --git a/src/sentry/constants.py b/src/sentry/constants.py index da02ebaf21dc41..8f87a68360ca4f 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -729,6 +729,7 @@ class InsightModules(Enum): "on_ready_for_review", "on_new_commit", ] +SEER_DEFAULT_CODING_AGENT_DEFAULT = "seer" SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes" SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes" ENABLED_CONSOLE_PLATFORMS_DEFAULT: list[str] = [] diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index 23ce2405cc3891..a4c0c45e91d94b 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -69,6 +69,7 @@ SAMPLING_MODE_DEFAULT, SCRAPE_JAVASCRIPT_DEFAULT, SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_DEFAULT_CODING_AGENT_DEFAULT, TARGET_SAMPLE_RATE_DEFAULT, ObjectStatus, ) @@ -410,9 +411,9 @@ def validate_relayPiiConfig(self, value): organization = self.context["organization"] return validate_pii_config_update(organization, value) - def validate_defaultCodingAgent(self, value: str | None) -> str | None: - if value == "seer" or value is None: - return None + def validate_defaultCodingAgent(self, value: str | None) -> str: + if value is None: + return SEER_DEFAULT_CODING_AGENT_DEFAULT coding_agent_aliases: dict[str, str] = { "cursor": "cursor_background_agent", "claude_code": "claude_code_agent", diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index be1b6c3dafc603..7762ca2c4629f6 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -586,7 +586,7 @@ def set_default_project_auto_open_prs(organization: Organization, project: Proje "sentry:seer_default_coding_agent_integration_id" ) automation_handoff: SeerAutomationHandoffConfiguration | None = None - if coding_agent and coding_agent_integration_id is not None: + if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: automation_handoff = SeerAutomationHandoffConfiguration( handoff_point=AutofixHandoffPoint.ROOT_CAUSE, target=coding_agent, diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 98c9afbb593f63..2918aa549754a5 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -248,7 +248,7 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "sentry:seer_default_coding_agent_integration_id" ) default_handoff: dict[str, Any] | None = None - if coding_agent and coding_agent_integration_id is not None: + if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: default_handoff = { "handoff_point": "root_cause", "target": coding_agent, diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index ef3e010e12c9f5..bbf409aa589206 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -22,6 +22,7 @@ from sentry.constants import ( RESERVED_ORGANIZATION_SLUGS, SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_DEFAULT_CODING_AGENT_DEFAULT, ObjectStatus, ) from sentry.core.endpoints.organization_details import ERR_NO_2FA, ERR_SSO_ENABLED @@ -1510,8 +1511,8 @@ def test_default_coding_agent_can_be_set(self) -> None: def test_default_coding_agent_seer(self) -> None: data = {"defaultCodingAgent": "seer"} response = self.get_success_response(self.organization.slug, **data) - assert self.organization.get_option("sentry:seer_default_coding_agent") is None - assert response.data["defaultCodingAgent"] is None + assert self.organization.get_option("sentry:seer_default_coding_agent") == "seer" + assert response.data["defaultCodingAgent"] == "seer" def test_default_coding_agent_cursor(self) -> None: for value in ("cursor", "cursor_background_agent"): @@ -1533,26 +1534,25 @@ def test_default_coding_agent_claude(self) -> None: ) assert response.data["defaultCodingAgent"] == "claude_code_agent" - def test_default_coding_agent_null_on_first_write_create_path(self) -> None: - # Tests the create path (no OrganizationOption row exists yet): sending null - # must store null rather than the string "None" via str(None). + def test_default_coding_agent_null_casts_to_seer(self) -> None: data = {"defaultCodingAgent": None} response = self.get_success_response(self.organization.slug, **data) - assert self.organization.get_option("sentry:seer_default_coding_agent") is None - assert response.data["defaultCodingAgent"] is None + assert self.organization.get_option("sentry:seer_default_coding_agent") == "seer" + assert response.data["defaultCodingAgent"] == "seer" def test_default_coding_agent_writing_default_value_stores_but_skips_audit_log( self, ) -> None: - # Sending the default value does not produce an audit log entry (by design: - # the ORG_OPTIONS loop only audits writes that differ from the default). with assume_test_silo_mode_of(AuditLogEntry): AuditLogEntry.objects.filter(organization_id=self.organization.id).delete() - data = {"defaultCodingAgent": None} + data = {"defaultCodingAgent": SEER_DEFAULT_CODING_AGENT_DEFAULT} self.get_success_response(self.organization.slug, **data) - assert self.organization.get_option("sentry:seer_default_coding_agent") is None + assert ( + self.organization.get_option("sentry:seer_default_coding_agent") + == SEER_DEFAULT_CODING_AGENT_DEFAULT + ) with assume_test_silo_mode_of(AuditLogEntry): assert not AuditLogEntry.objects.filter(organization_id=self.organization.id).exists() @@ -1606,12 +1606,14 @@ def test_default_coding_agent_integration_id_null_on_first_write_create_path(sel ) assert response.data["defaultCodingAgentIntegrationId"] is None - def test_default_coding_agent_can_be_cleared(self) -> None: - self.organization.update_option("sentry:seer_default_coding_agent", "seer") + def test_default_coding_agent_null_resets_to_seer(self) -> None: + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) data = {"defaultCodingAgent": None} response = self.get_success_response(self.organization.slug, **data) - assert self.organization.get_option("sentry:seer_default_coding_agent") is None - assert response.data["defaultCodingAgent"] is None + assert self.organization.get_option("sentry:seer_default_coding_agent") == "seer" + assert response.data["defaultCodingAgent"] == "seer" def test_default_coding_agent_rejects_invalid_choice(self) -> None: data = {"defaultCodingAgent": "invalid_agent"} From 684ac29c4bf4ee17cdc222fe5a809c929c3aa0fc Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 10:22:55 -0700 Subject: [PATCH 10/31] other spots --- src/sentry/apidocs/examples/organization_examples.py | 2 +- src/sentry/core/endpoints/organization_details.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sentry/apidocs/examples/organization_examples.py b/src/sentry/apidocs/examples/organization_examples.py index f84eab2739982c..4788b17d521bd8 100644 --- a/src/sentry/apidocs/examples/organization_examples.py +++ b/src/sentry/apidocs/examples/organization_examples.py @@ -304,7 +304,7 @@ class OrganizationExamples: "enableSeerCoding": True, "enableSeerEnhancedAlerts": True, "autoOpenPrs": False, - "defaultCodingAgent": None, + "defaultCodingAgent": "seer", "defaultCodingAgentIntegrationId": None, "defaultAutomatedRunStoppingPoint": "open_pr", "issueAlertsThreadFlag": True, diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index a4c0c45e91d94b..cd27251dd73330 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -242,7 +242,12 @@ bool, ENABLE_SEER_CODING_DEFAULT, ), - ("defaultCodingAgent", "sentry:seer_default_coding_agent", str, None), + ( + "defaultCodingAgent", + "sentry:seer_default_coding_agent", + str, + SEER_DEFAULT_CODING_AGENT_DEFAULT, + ), ( "defaultCodingAgentIntegrationId", "sentry:seer_default_coding_agent_integration_id", From 88d41065b85fcb5db4d504a64b9b54db0830243c Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 10:37:47 -0700 Subject: [PATCH 11/31] fix failing test --- tests/sentry/core/endpoints/test_organization_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index bbf409aa589206..1efba3a5789323 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -1498,7 +1498,7 @@ def test_enable_seer_coding_cannot_be_enabled_when_flag_enabled(self) -> None: def test_default_coding_agent_default(self) -> None: response = self.get_success_response(self.organization.slug) - assert response.data["defaultCodingAgent"] is None + assert response.data["defaultCodingAgent"] == SEER_DEFAULT_CODING_AGENT_DEFAULT def test_default_coding_agent_can_be_set(self) -> None: data = {"defaultCodingAgent": "claude_code_agent"} From df1f39ea30756b6cc92f51e09702f5da9b3b6b39 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 10:59:38 -0700 Subject: [PATCH 12/31] fix bug for projects with existing stopping point --- src/sentry/seer/similarity/utils.py | 4 ---- src/sentry/tasks/seer/autofix.py | 28 +++++++++++++++------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index 7762ca2c4629f6..fe6e76149a7180 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -573,10 +573,6 @@ def set_default_project_seer_scanner_automation( def set_default_project_auto_open_prs(organization: Organization, project: Project) -> None: """Called once at project creation time to set the initial automated run stopping point and automation handoff. - - Reads org options (default_automated_run_stopping_point, auto_open_prs, default_coding_agent, - default_coding_agent_integration_id) and writes the corresponding project-level - options (stopping point, handoff config). """ if not is_seer_seat_based_tier_enabled(organization): return diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 2918aa549754a5..80c5fd52d5203f 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -258,39 +258,41 @@ def configure_seer_for_existing_org(organization_id: int) -> None: ), } + default_stopping_point = organization.get_option( + "sentry:default_automated_run_stopping_point", + SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + ) + preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) # Determine which projects need updates preferences_to_set = [] projects_by_id = {p.id: p for p in projects} for project_id in project_ids: + stopping_point = default_stopping_point + handoff = default_handoff + existing_pref = preferences_by_id.get(str(project_id)) if not existing_pref: # No existing preferences, get repositories from code mappings repositories = get_autofix_repos_from_project_code_mappings(projects_by_id[project_id]) else: - # Skip projects that already have an acceptable stopping point configured - if existing_pref.get("automated_run_stopping_point") in ("open_pr", "code_changes"): - continue repositories = existing_pref.get("repositories") or [] + if existing_pref.get("automated_run_stopping_point") in ("open_pr", "code_changes"): + stopping_point = existing_pref["automated_run_stopping_point"] + + if existing_pref.get("automation_handoff"): + handoff = existing_pref["automation_handoff"] repositories = deduplicate_repositories(repositories) - # Preserve existing repositories and automation_handoff, only update the stopping point preferences_to_set.append( { "organization_id": organization_id, "project_id": project_id, "repositories": repositories or [], - "automated_run_stopping_point": organization.get_option( - "sentry:default_automated_run_stopping_point", - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - ), - "automation_handoff": ( - existing_pref.get("automation_handoff") - if existing_pref and existing_pref.get("automation_handoff") - else default_handoff - ), + "automated_run_stopping_point": stopping_point, + "automation_handoff": handoff, } ) From 99b0f5f7e48d5d75410de3c786fdf2e8305c80a1 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 11:04:13 -0700 Subject: [PATCH 13/31] more tests --- tests/sentry/tasks/seer/test_autofix.py | 84 +++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/tests/sentry/tasks/seer/test_autofix.py b/tests/sentry/tasks/seer/test_autofix.py index 20566413060f75..c8e0870d8b4bf6 100644 --- a/tests/sentry/tasks/seer/test_autofix.py +++ b/tests/sentry/tasks/seer/test_autofix.py @@ -197,10 +197,10 @@ def test_overrides_autofix_off_to_medium( @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") - def test_skips_projects_with_existing_stopping_point( + def test_preserves_existing_stopping_point( self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock ) -> None: - """Test that projects with open_pr or code_changes stopping point are skipped.""" + """Test that projects with existing stopping points keep them rather than getting the org default.""" project1 = self.create_project(organization=self.organization) project2 = self.create_project(organization=self.organization) @@ -211,8 +211,84 @@ def test_skips_projects_with_existing_stopping_point( configure_seer_for_existing_org(organization_id=self.organization.id) - # bulk_set should not be called since both projects are skipped - mock_bulk_set.assert_not_called() + mock_bulk_set.assert_called_once() + preferences = mock_bulk_set.call_args[0][1] + prefs_by_project = {p["project_id"]: p for p in preferences} + assert prefs_by_project[project1.id]["automated_run_stopping_point"] == "open_pr" + assert prefs_by_project[project2.id]["automated_run_stopping_point"] == "code_changes" + + @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") + def test_applies_org_default_handoff_to_projects_without_handoff( + self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock + ) -> None: + """Test that org-level external agent config is applied to projects that lack handoff.""" + project = self.create_project(organization=self.organization) + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) + self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 42) + self.organization.update_option("sentry:auto_open_prs", True) + + mock_bulk_get.return_value = { + str(project.id): {"automated_run_stopping_point": "code_changes"}, + } + + configure_seer_for_existing_org(organization_id=self.organization.id) + + mock_bulk_set.assert_called_once() + prefs = mock_bulk_set.call_args[0][1][0] + assert prefs["automation_handoff"] == { + "handoff_point": "root_cause", + "target": "cursor_background_agent", + "integration_id": 42, + "auto_create_pr": True, + } + + @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") + def test_preserves_existing_handoff( + self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock + ) -> None: + """Test that existing project-level handoff is preserved over org default.""" + project = self.create_project(organization=self.organization) + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) + self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 42) + + existing_handoff = { + "handoff_point": "root_cause", + "target": "claude_code_agent", + "integration_id": 99, + "auto_create_pr": False, + } + mock_bulk_get.return_value = { + str(project.id): {"automation_handoff": existing_handoff}, + } + + configure_seer_for_existing_org(organization_id=self.organization.id) + + mock_bulk_set.assert_called_once() + prefs = mock_bulk_set.call_args[0][1][0] + assert prefs["automation_handoff"] == existing_handoff + + @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") + def test_no_handoff_for_seer_agent( + self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock + ) -> None: + """Test that no handoff is created when the default coding agent is seer.""" + self.create_project(organization=self.organization) + self.organization.update_option("sentry:seer_default_coding_agent", "seer") + + mock_bulk_get.return_value = {} + + configure_seer_for_existing_org(organization_id=self.organization.id) + + mock_bulk_set.assert_called_once() + prefs = mock_bulk_set.call_args[0][1][0] + assert prefs["automation_handoff"] is None @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") def test_raises_on_bulk_get_api_failure(self, mock_bulk_get: MagicMock) -> None: From 33bc6b210b56522ab751e4b54669b6ca8707c4af Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 11:15:22 -0700 Subject: [PATCH 14/31] clean up tests --- .../endpoints/test_organization_details.py | 52 +++++++------------ 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index 1efba3a5789323..c48c835925d6d4 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -1500,21 +1500,13 @@ def test_default_coding_agent_default(self) -> None: response = self.get_success_response(self.organization.slug) assert response.data["defaultCodingAgent"] == SEER_DEFAULT_CODING_AGENT_DEFAULT - def test_default_coding_agent_can_be_set(self) -> None: - data = {"defaultCodingAgent": "claude_code_agent"} - response = self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:seer_default_coding_agent") == "claude_code_agent" - ) - assert response.data["defaultCodingAgent"] == "claude_code_agent" - - def test_default_coding_agent_seer(self) -> None: + def test_default_coding_agent_can_be_set_to_seer(self) -> None: data = {"defaultCodingAgent": "seer"} response = self.get_success_response(self.organization.slug, **data) assert self.organization.get_option("sentry:seer_default_coding_agent") == "seer" assert response.data["defaultCodingAgent"] == "seer" - def test_default_coding_agent_cursor(self) -> None: + def test_default_coding_agent_can_be_set_to_cursor(self) -> None: for value in ("cursor", "cursor_background_agent"): data = {"defaultCodingAgent": value} response = self.get_success_response(self.organization.slug, **data) @@ -1524,7 +1516,7 @@ def test_default_coding_agent_cursor(self) -> None: ) assert response.data["defaultCodingAgent"] == "cursor_background_agent" - def test_default_coding_agent_claude(self) -> None: + def test_default_coding_agent_can_be_set_to_claude(self) -> None: for value in ("claude_code", "claude_code_agent"): data = {"defaultCodingAgent": value} response = self.get_success_response(self.organization.slug, **data) @@ -1534,15 +1526,30 @@ def test_default_coding_agent_claude(self) -> None: ) assert response.data["defaultCodingAgent"] == "claude_code_agent" - def test_default_coding_agent_null_casts_to_seer(self) -> None: + def test_default_coding_agent_none_casts_to_seer(self) -> None: + data = {"defaultCodingAgent": None} + response = self.get_success_response(self.organization.slug, **data) + assert self.organization.get_option("sentry:seer_default_coding_agent") == "seer" + assert response.data["defaultCodingAgent"] == "seer" + + def test_default_coding_agent_none_resets_to_seer(self) -> None: + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) data = {"defaultCodingAgent": None} response = self.get_success_response(self.organization.slug, **data) assert self.organization.get_option("sentry:seer_default_coding_agent") == "seer" assert response.data["defaultCodingAgent"] == "seer" + def test_default_coding_agent_rejects_invalid_choice(self) -> None: + data = {"defaultCodingAgent": "invalid_agent"} + self.get_error_response(self.organization.slug, status_code=400, **data) + def test_default_coding_agent_writing_default_value_stores_but_skips_audit_log( self, ) -> None: + # Sending the default value does not produce an audit log entry (by design: + # the ORG_OPTIONS loop only audits writes that differ from the default). with assume_test_silo_mode_of(AuditLogEntry): AuditLogEntry.objects.filter(organization_id=self.organization.id).delete() @@ -1606,19 +1613,6 @@ def test_default_coding_agent_integration_id_null_on_first_write_create_path(sel ) assert response.data["defaultCodingAgentIntegrationId"] is None - def test_default_coding_agent_null_resets_to_seer(self) -> None: - self.organization.update_option( - "sentry:seer_default_coding_agent", "cursor_background_agent" - ) - data = {"defaultCodingAgent": None} - response = self.get_success_response(self.organization.slug, **data) - assert self.organization.get_option("sentry:seer_default_coding_agent") == "seer" - assert response.data["defaultCodingAgent"] == "seer" - - def test_default_coding_agent_rejects_invalid_choice(self) -> None: - data = {"defaultCodingAgent": "invalid_agent"} - self.get_error_response(self.organization.slug, status_code=400, **data) - def test_default_automated_run_stopping_point_default(self) -> None: response = self.get_success_response(self.organization.slug) assert ( @@ -1627,14 +1621,6 @@ def test_default_automated_run_stopping_point_default(self) -> None: ) def test_default_automated_run_stopping_point_can_be_set(self) -> None: - data = {"defaultAutomatedRunStoppingPoint": "open_pr"} - response = self.get_success_response(self.organization.slug, **data) - assert ( - self.organization.get_option("sentry:default_automated_run_stopping_point") == "open_pr" - ) - assert response.data["defaultAutomatedRunStoppingPoint"] == "open_pr" - - def test_default_automated_run_stopping_point_all_choices(self) -> None: for choice in ("root_cause", "solution", "code_changes", "open_pr"): data = {"defaultAutomatedRunStoppingPoint": choice} response = self.get_success_response(self.organization.slug, **data) From f745023461bd9c49abc4924053ab8bd99135e2c6 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 11:30:14 -0700 Subject: [PATCH 15/31] only allow code changes and open pr as valid default stopping points --- src/sentry/core/endpoints/organization_details.py | 2 +- .../core/endpoints/test_organization_details.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index cd27251dd73330..f4091e7da5f7e4 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -385,7 +385,7 @@ class OrganizationSerializer(BaseOrganizationSerializer): ) defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True) defaultAutomatedRunStoppingPoint = serializers.ChoiceField( - choices=["root_cause", "solution", "code_changes", "open_pr"], + choices=["code_changes", "open_pr"], required=False, ) autoOpenPrs = serializers.BooleanField(required=False) diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index c48c835925d6d4..1800e279e8a066 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -1621,14 +1621,17 @@ def test_default_automated_run_stopping_point_default(self) -> None: ) def test_default_automated_run_stopping_point_can_be_set(self) -> None: - for choice in ("root_cause", "solution", "code_changes", "open_pr"): - data = {"defaultAutomatedRunStoppingPoint": choice} - response = self.get_success_response(self.organization.slug, **data) - assert response.data["defaultAutomatedRunStoppingPoint"] == choice + for choice in ("code_changes", "open_pr"): + with self.subTest(choice=choice): + data = {"defaultAutomatedRunStoppingPoint": choice} + response = self.get_success_response(self.organization.slug, **data) + assert response.data["defaultAutomatedRunStoppingPoint"] == choice def test_default_automated_run_stopping_point_rejects_invalid(self) -> None: - data = {"defaultAutomatedRunStoppingPoint": "invalid_point"} - self.get_error_response(self.organization.slug, status_code=400, **data) + for invalid in ("root_cause", "solution", "invalid_point"): + with self.subTest(value=invalid): + data = {"defaultAutomatedRunStoppingPoint": invalid} + self.get_error_response(self.organization.slug, status_code=400, **data) def test_default_coding_agent_integration_id_can_be_cleared(self) -> None: self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 123) From 5bb23fde15995bfa85d87361228538e83b904479 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 11:46:16 -0700 Subject: [PATCH 16/31] ref(seer): Clean up project preferences and restrict stopping point choices Rename set_default_project_auto_open_prs to set_default_project_seer_preferences to reflect its expanded scope (stopping point + handoff). Restrict defaultAutomatedRunStoppingPoint to only accept "code_changes" and "open_pr" since "root_cause" and "solution" are internal runtime decisions. Fix API docs example to show the actual default value. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- .../apidocs/examples/organization_examples.py | 2 +- src/sentry/core/endpoints/team_projects.py | 4 +- src/sentry/seer/similarity/utils.py | 2 +- tests/sentry/seer/similarity/test_utils.py | 42 ++++++++++--------- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/sentry/apidocs/examples/organization_examples.py b/src/sentry/apidocs/examples/organization_examples.py index 4788b17d521bd8..fd238fc93f95ce 100644 --- a/src/sentry/apidocs/examples/organization_examples.py +++ b/src/sentry/apidocs/examples/organization_examples.py @@ -306,7 +306,7 @@ class OrganizationExamples: "autoOpenPrs": False, "defaultCodingAgent": "seer", "defaultCodingAgentIntegrationId": None, - "defaultAutomatedRunStoppingPoint": "open_pr", + "defaultAutomatedRunStoppingPoint": "code_changes", "issueAlertsThreadFlag": True, "metricAlertsThreadFlag": True, "trustedRelays": [], diff --git a/src/sentry/core/endpoints/team_projects.py b/src/sentry/core/endpoints/team_projects.py index ae2d9b8b254cc8..b82545bfdbb5d4 100644 --- a/src/sentry/core/endpoints/team_projects.py +++ b/src/sentry/core/endpoints/team_projects.py @@ -31,8 +31,8 @@ from sentry.models.team import Team from sentry.seer.similarity.utils import ( project_is_seer_eligible, - set_default_project_auto_open_prs, set_default_project_autofix_automation_tuning, + set_default_project_seer_preferences, set_default_project_seer_scanner_automation, ) from sentry.signals import project_created @@ -56,7 +56,7 @@ def apply_default_project_settings(organization: Organization, project: Project) set_default_project_autofix_automation_tuning(organization, project) set_default_project_seer_scanner_automation(organization, project) - set_default_project_auto_open_prs(organization, project) + set_default_project_seer_preferences(organization, project) class ProjectPostSerializer(serializers.Serializer): diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index fe6e76149a7180..d95fc61966fe00 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -570,7 +570,7 @@ def set_default_project_seer_scanner_automation( project.update_option("sentry:seer_scanner_automation", org_default) -def set_default_project_auto_open_prs(organization: Organization, project: Project) -> None: +def set_default_project_seer_preferences(organization: Organization, project: Project) -> None: """Called once at project creation time to set the initial automated run stopping point and automation handoff. """ diff --git a/tests/sentry/seer/similarity/test_utils.py b/tests/sentry/seer/similarity/test_utils.py index acb027609c193a..5a9de64e86b315 100644 --- a/tests/sentry/seer/similarity/test_utils.py +++ b/tests/sentry/seer/similarity/test_utils.py @@ -15,7 +15,7 @@ filter_null_from_string, get_stacktrace_string, get_token_count, - set_default_project_auto_open_prs, + set_default_project_seer_preferences, stacktrace_exceeds_limits, ) from sentry.services.eventstore.models import Event @@ -1223,8 +1223,8 @@ def test_generates_stacktrace_string_from_variants(self) -> None: assert token_count == 33 -class TestSetDefaultProjectAutoOpenPrs(TestCase): - """Tests for set_default_project_auto_open_prs which wires org-level Seer +class TestSetDefaultProjectSeerPreferences(TestCase): + """Tests for set_default_project_seer_preferences which wires org-level Seer defaults (stopping point, coding agent, auto_open_prs) into project-level preferences at project creation time. """ @@ -1239,7 +1239,7 @@ def setUp(self): def test_skips_when_tier_not_enabled( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): - set_default_project_auto_open_prs(self.organization, self.project) + set_default_project_seer_preferences(self.organization, self.project) mock_set_pref.assert_not_called() mock_dual_write.assert_not_called() @@ -1250,7 +1250,7 @@ def test_seer_agent_default( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): """Seer agent, no auto_open_prs, default stopping point (code_changes).""" - set_default_project_auto_open_prs(self.organization, self.project) + set_default_project_seer_preferences(self.organization, self.project) pref = mock_set_pref.call_args[0][0] assert pref.automated_run_stopping_point == "code_changes" @@ -1262,29 +1262,31 @@ def test_seer_agent_default( def test_seer_agent_with_custom_stopping_point( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): - """Seer agent, no auto_open_prs, custom stopping point (root_cause).""" - self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") + """Seer agent, no auto_open_prs, non-default stopping point.""" + self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") - set_default_project_auto_open_prs(self.organization, self.project) + set_default_project_seer_preferences(self.organization, self.project) pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "root_cause" + assert pref.automated_run_stopping_point == "open_pr" assert pref.automation_handoff is None @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") @patch("sentry.seer.similarity.utils.set_project_seer_preference") @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_seer_agent_with_auto_open_prs( + def test_seer_agent_with_auto_open_prs_preserves_stopping_point( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): - """auto_open_prs does not override contradictory stopping point.""" + """auto_open_prs does not override the configured stopping point.""" self.organization.update_option("sentry:auto_open_prs", True) - self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") + self.organization.update_option( + "sentry:default_automated_run_stopping_point", "code_changes" + ) - set_default_project_auto_open_prs(self.organization, self.project) + set_default_project_seer_preferences(self.organization, self.project) pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "root_cause" + assert pref.automated_run_stopping_point == "code_changes" assert pref.automation_handoff is None @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") @@ -1306,7 +1308,7 @@ def test_external_agent_default( "sentry:seer_default_coding_agent_integration_id", integration_id ) - set_default_project_auto_open_prs(self.organization, self.project) + set_default_project_seer_preferences(self.organization, self.project) pref = mock_set_pref.call_args[0][0] assert pref.automated_run_stopping_point == "code_changes" @@ -1324,7 +1326,9 @@ def test_external_agent_with_auto_open_prs( ): """auto_open_prs sets auto_create_pr on handoff but does not override stopping point.""" self.organization.update_option("sentry:auto_open_prs", True) - self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") + self.organization.update_option( + "sentry:default_automated_run_stopping_point", "code_changes" + ) agents = [ ("cursor_background_agent", 1234), ("claude_code_agent", 5678), @@ -1337,10 +1341,10 @@ def test_external_agent_with_auto_open_prs( "sentry:seer_default_coding_agent_integration_id", integration_id ) - set_default_project_auto_open_prs(self.organization, self.project) + set_default_project_seer_preferences(self.organization, self.project) pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "root_cause" + assert pref.automated_run_stopping_point == "code_changes" assert pref.automation_handoff is not None assert pref.automation_handoff.target == agent assert pref.automation_handoff.integration_id == integration_id @@ -1356,7 +1360,7 @@ def test_external_agent_without_integration_id_skips_handoff( "sentry:seer_default_coding_agent", "cursor_background_agent" ) - set_default_project_auto_open_prs(self.organization, self.project) + set_default_project_seer_preferences(self.organization, self.project) pref = mock_set_pref.call_args[0][0] assert pref.automation_handoff is None From e5508b90d4b1ccc1f327970aa629c8886779ed70 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 12:03:06 -0700 Subject: [PATCH 17/31] small fix --- src/sentry/api/serializers/models/organization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index f6a68be384c60f..343f1e1c3a54d5 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -559,7 +559,7 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp defaultSeerScannerAutomation: bool enableSeerEnhancedAlerts: bool enableSeerCoding: bool - defaultCodingAgent: str | None + defaultCodingAgent: str defaultCodingAgentIntegrationId: int | None defaultAutomatedRunStoppingPoint: str autoEnableCodeReview: bool From e9a9725206d1c34de04937721a2e75dcca94bf36 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 14:06:26 -0700 Subject: [PATCH 18/31] consolidate two stopping point defaults --- src/sentry/api/serializers/models/organization.py | 4 ++-- src/sentry/constants.py | 1 - src/sentry/core/endpoints/organization_details.py | 7 +++---- src/sentry/seer/similarity/utils.py | 4 ++-- src/sentry/tasks/seer/autofix.py | 4 ++-- tests/sentry/core/endpoints/test_organization_details.py | 4 ++-- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py index 343f1e1c3a54d5..1f68aa0fc4f336 100644 --- a/src/sentry/api/serializers/models/organization.py +++ b/src/sentry/api/serializers/models/organization.py @@ -58,7 +58,7 @@ ROLLBACK_ENABLED_DEFAULT, SAMPLING_MODE_DEFAULT, SCRAPE_JAVASCRIPT_DEFAULT, - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, SEER_DEFAULT_CODING_AGENT_DEFAULT, TARGET_SAMPLE_RATE_DEFAULT, ObjectStatus, @@ -743,7 +743,7 @@ def serialize( # type: ignore[override] ), "defaultAutomatedRunStoppingPoint": obj.get_option( "sentry:default_automated_run_stopping_point", - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ), "autoOpenPrs": bool( obj.get_option( diff --git a/src/sentry/constants.py b/src/sentry/constants.py index 8f87a68360ca4f..f2aff76cadff2d 100644 --- a/src/sentry/constants.py +++ b/src/sentry/constants.py @@ -730,7 +730,6 @@ class InsightModules(Enum): "on_new_commit", ] SEER_DEFAULT_CODING_AGENT_DEFAULT = "seer" -SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes" SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes" ENABLED_CONSOLE_PLATFORMS_DEFAULT: list[str] = [] CONSOLE_SDK_INVITE_QUOTA_DEFAULT = 0 diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index f4091e7da5f7e4..8f728e5df62e3b 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -68,7 +68,7 @@ ROLLBACK_ENABLED_DEFAULT, SAMPLING_MODE_DEFAULT, SCRAPE_JAVASCRIPT_DEFAULT, - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, SEER_DEFAULT_CODING_AGENT_DEFAULT, TARGET_SAMPLE_RATE_DEFAULT, ObjectStatus, @@ -265,7 +265,7 @@ "defaultAutomatedRunStoppingPoint", "sentry:default_automated_run_stopping_point", str, - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ), ( "autoEnableCodeReview", @@ -385,8 +385,7 @@ class OrganizationSerializer(BaseOrganizationSerializer): ) defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True) defaultAutomatedRunStoppingPoint = serializers.ChoiceField( - choices=["code_changes", "open_pr"], - required=False, + choices=["code_changes", "open_pr"], required=False ) autoOpenPrs = serializers.BooleanField(required=False) autoEnableCodeReview = serializers.BooleanField(required=False) diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index d95fc61966fe00..fa2223f1e747e6 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -12,7 +12,7 @@ from sentry.constants import ( AUTO_OPEN_PRS_DEFAULT, DATA_ROOT, - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ) from sentry.grouping.api import get_contributing_variant_and_component from sentry.grouping.grouping_info import get_grouping_info_from_variants_legacy @@ -599,7 +599,7 @@ def set_default_project_seer_preferences(organization: Organization, project: Pr repositories=[], automated_run_stopping_point=organization.get_option( "sentry:default_automated_run_stopping_point", - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ), automation_handoff=automation_handoff, ) diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 80c5fd52d5203f..6547d67f57aff7 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -11,7 +11,7 @@ from sentry.analytics.events.autofix_automation_events import AiAutofixAutomationEvent from sentry.constants import ( AUTO_OPEN_PRS_DEFAULT, - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ObjectStatus, ) from sentry.models.group import Group @@ -260,7 +260,7 @@ def configure_seer_for_existing_org(organization_id: int) -> None: default_stopping_point = organization.get_option( "sentry:default_automated_run_stopping_point", - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ) preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index 1800e279e8a066..c931c3e8269e5f 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -21,7 +21,7 @@ from sentry.auth.authenticators.totp import TotpInterface from sentry.constants import ( RESERVED_ORGANIZATION_SLUGS, - SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, + SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, SEER_DEFAULT_CODING_AGENT_DEFAULT, ObjectStatus, ) @@ -1617,7 +1617,7 @@ def test_default_automated_run_stopping_point_default(self) -> None: response = self.get_success_response(self.organization.slug) assert ( response.data["defaultAutomatedRunStoppingPoint"] - == SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + == SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT ) def test_default_automated_run_stopping_point_can_be_set(self) -> None: From 82b453ac4825c1bff33f6cafe7cd0163d3c05388 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Mon, 30 Mar 2026 16:01:34 -0700 Subject: [PATCH 19/31] test(seer): Add tests for configure_seer_for_existing_org skip logic Cover the skip/no-skip behavior in the migration task: projects with valid stopping points and existing handoff are skipped, while projects missing handoff or with invalid stopping points get org defaults applied. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- src/sentry/tasks/seer/autofix.py | 20 ++-- tests/sentry/tasks/seer/test_autofix.py | 136 +++++++++++++++++------- 2 files changed, 114 insertions(+), 42 deletions(-) diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 6547d67f57aff7..be4cf5cdd0e6b8 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -279,18 +279,26 @@ def configure_seer_for_existing_org(organization_id: int) -> None: else: repositories = existing_pref.get("repositories") or [] - if existing_pref.get("automated_run_stopping_point") in ("open_pr", "code_changes"): - stopping_point = existing_pref["automated_run_stopping_point"] + existing_stopping_point = existing_pref.get("automated_run_stopping_point") + existing_handoff = existing_pref.get("automation_handoff") - if existing_pref.get("automation_handoff"): - handoff = existing_pref["automation_handoff"] - repositories = deduplicate_repositories(repositories) + # Skip projects that a) already have an acceptable stopping point configured + # AND b) already have a handoff configured or no org default handoff. + if existing_stopping_point in ("open_pr", "code_changes") and ( + existing_handoff or default_handoff is None + ): + continue + + if existing_stopping_point in ("open_pr", "code_changes"): + stopping_point = existing_stopping_point + if existing_handoff: + handoff = existing_handoff preferences_to_set.append( { "organization_id": organization_id, "project_id": project_id, - "repositories": repositories or [], + "repositories": deduplicate_repositories(repositories) or [], "automated_run_stopping_point": stopping_point, "automation_handoff": handoff, } diff --git a/tests/sentry/tasks/seer/test_autofix.py b/tests/sentry/tasks/seer/test_autofix.py index c8e0870d8b4bf6..31ef5373a3d254 100644 --- a/tests/sentry/tasks/seer/test_autofix.py +++ b/tests/sentry/tasks/seer/test_autofix.py @@ -4,6 +4,7 @@ import pytest from django.test import TestCase +from sentry.constants import SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT from sentry.models.repository import Repository from sentry.seer.autofix.constants import AutofixStatus, SeerAutomationSource from sentry.seer.autofix.utils import AutofixState, get_seer_seat_based_tier_cache_key @@ -197,32 +198,29 @@ def test_overrides_autofix_off_to_medium( @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") - def test_preserves_existing_stopping_point( + def test_new_project_gets_stopping_point_and_no_handoff_from_org_defaults( self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock ) -> None: - """Test that projects with existing stopping points keep them rather than getting the org default.""" - project1 = self.create_project(organization=self.organization) - project2 = self.create_project(organization=self.organization) + """Project with no existing prefs gets stopping point and no handoff (seer coding agent) from org defaults.""" + project = self.create_project(organization=self.organization) + self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") - mock_bulk_get.return_value = { - str(project1.id): {"automated_run_stopping_point": "open_pr"}, - str(project2.id): {"automated_run_stopping_point": "code_changes"}, - } + mock_bulk_get.return_value = {} configure_seer_for_existing_org(organization_id=self.organization.id) mock_bulk_set.assert_called_once() - preferences = mock_bulk_set.call_args[0][1] - prefs_by_project = {p["project_id"]: p for p in preferences} - assert prefs_by_project[project1.id]["automated_run_stopping_point"] == "open_pr" - assert prefs_by_project[project2.id]["automated_run_stopping_point"] == "code_changes" + prefs = mock_bulk_set.call_args[0][1] + prefs_by_project = {p["project_id"]: p for p in prefs} + assert prefs_by_project[project.id]["automated_run_stopping_point"] == "open_pr" + assert prefs_by_project[project.id]["automation_handoff"] is None @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") - def test_applies_org_default_handoff_to_projects_without_handoff( + def test_new_project_gets_stopping_point_and_handoff_from_org_defaults( self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock ) -> None: - """Test that org-level external agent config is applied to projects that lack handoff.""" + """Project with no existing prefs gets stopping point and external agent handoff from org defaults.""" project = self.create_project(organization=self.organization) self.organization.update_option( "sentry:seer_default_coding_agent", "cursor_background_agent" @@ -230,15 +228,14 @@ def test_applies_org_default_handoff_to_projects_without_handoff( self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 42) self.organization.update_option("sentry:auto_open_prs", True) - mock_bulk_get.return_value = { - str(project.id): {"automated_run_stopping_point": "code_changes"}, - } + mock_bulk_get.return_value = {} configure_seer_for_existing_org(organization_id=self.organization.id) mock_bulk_set.assert_called_once() - prefs = mock_bulk_set.call_args[0][1][0] - assert prefs["automation_handoff"] == { + prefs = mock_bulk_set.call_args[0][1] + prefs_by_project = {p["project_id"]: p for p in prefs} + assert prefs_by_project[project.id]["automation_handoff"] == { "handoff_point": "root_cause", "target": "cursor_background_agent", "integration_id": 42, @@ -247,48 +244,115 @@ def test_applies_org_default_handoff_to_projects_without_handoff( @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") - def test_preserves_existing_handoff( + def test_skips_project_with_valid_stopping_point_and_no_default_handoff( + self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock + ) -> None: + """Project is skipped when it has a valid stopping point and the org has no default handoff (seer agent).""" + project = self.create_project(organization=self.organization) + + mock_bulk_get.return_value = { + str(project.id): {"automated_run_stopping_point": "open_pr"}, + } + + configure_seer_for_existing_org(organization_id=self.organization.id) + + mock_bulk_set.assert_not_called() + + @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") + def test_skips_project_with_valid_stopping_point_and_existing_handoff( self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock ) -> None: - """Test that existing project-level handoff is preserved over org default.""" + """Project is skipped when it has a valid stopping point and an existing handoff configured.""" project = self.create_project(organization=self.organization) self.organization.update_option( "sentry:seer_default_coding_agent", "cursor_background_agent" ) self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 42) - existing_handoff = { - "handoff_point": "root_cause", - "target": "claude_code_agent", - "integration_id": 99, - "auto_create_pr": False, + mock_bulk_get.return_value = { + str(project.id): { + "automated_run_stopping_point": "code_changes", + "automation_handoff": { + "handoff_point": "root_cause", + "target": "claude_code_agent", + "integration_id": 99, + "auto_create_pr": False, + }, + }, } + + configure_seer_for_existing_org(organization_id=self.organization.id) + + mock_bulk_set.assert_not_called() + + @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") + def test_project_with_valid_stopping_point_gets_handoff_from_org_defaults( + self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock + ) -> None: + """Project with valid stopping point but no handoff gets org default handoff applied. + Existing stopping point is preserved.""" + project = self.create_project(organization=self.organization) + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) + self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 42) + self.organization.update_option("sentry:auto_open_prs", True) + mock_bulk_get.return_value = { - str(project.id): {"automation_handoff": existing_handoff}, + str(project.id): {"automated_run_stopping_point": "open_pr"}, } configure_seer_for_existing_org(organization_id=self.organization.id) mock_bulk_set.assert_called_once() - prefs = mock_bulk_set.call_args[0][1][0] - assert prefs["automation_handoff"] == existing_handoff + prefs = mock_bulk_set.call_args[0][1] + prefs_by_project = {p["project_id"]: p for p in prefs} + assert prefs_by_project[project.id]["automated_run_stopping_point"] == "open_pr" + assert prefs_by_project[project.id]["automation_handoff"] == { + "handoff_point": "root_cause", + "target": "cursor_background_agent", + "integration_id": 42, + "auto_create_pr": True, + } @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") - def test_no_handoff_for_seer_agent( + def test_project_with_invalid_stopping_point_gets_org_default_stopping_point( self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock ) -> None: - """Test that no handoff is created when the default coding agent is seer.""" - self.create_project(organization=self.organization) - self.organization.update_option("sentry:seer_default_coding_agent", "seer") + """Project with unrecognized stopping point gets org default stopping point applied. + Existing handoff (if any) is preserved.""" + project = self.create_project(organization=self.organization) + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) + self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 42) - mock_bulk_get.return_value = {} + existing_handoff = { + "handoff_point": "root_cause", + "target": "claude_code_agent", + "integration_id": 99, + "auto_create_pr": False, + } + mock_bulk_get.return_value = { + str(project.id): { + "automated_run_stopping_point": "root_cause", + "automation_handoff": existing_handoff, + }, + } configure_seer_for_existing_org(organization_id=self.organization.id) mock_bulk_set.assert_called_once() - prefs = mock_bulk_set.call_args[0][1][0] - assert prefs["automation_handoff"] is None + prefs = mock_bulk_set.call_args[0][1] + prefs_by_project = {p["project_id"]: p for p in prefs} + assert ( + prefs_by_project[project.id]["automated_run_stopping_point"] + == SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) + assert prefs_by_project[project.id]["automation_handoff"] == existing_handoff @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") def test_raises_on_bulk_get_api_failure(self, mock_bulk_get: MagicMock) -> None: From 8138f6c40c79eca275229edc7bad8e0e9cbea6ca Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 10:28:46 -0700 Subject: [PATCH 20/31] remove obsolete comment --- src/sentry/core/endpoints/organization_details.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index 8f728e5df62e3b..a3c7fd1f39b75a 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -255,7 +255,6 @@ None, ), ( - # Informs UI default for automated_run_stopping_point in project preferences "autoOpenPrs", "sentry:auto_open_prs", bool, From 11502936a60786cbabcaa6a1836a74526e8d283e Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 11:16:26 -0700 Subject: [PATCH 21/31] feat(seer): Add feature flag for root_cause stopping point and fix auto_open_prs logic Gate root_cause as a valid stopping point behind seer-overview-project-creation feature flag in the migration task. For project creation, fix stopping point logic for Seer agent: auto_open_prs=True forces open_pr, auto_open_prs=False caps open_pr down to code_changes. External agents use the org default stopping point and pass auto_open_prs through to auto_create_pr on handoff. Co-Authored-By: Claude Sonnet 4 Made-with: Cursor --- src/sentry/features/temporary.py | 2 ++ src/sentry/seer/similarity/utils.py | 22 ++++++++++++------- src/sentry/tasks/seer/autofix.py | 7 ++++-- tests/sentry/seer/similarity/test_utils.py | 25 ++++++++++++++++++---- tests/sentry/tasks/seer/test_autofix.py | 18 ++++++++++++++++ 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 1122038668af8e..112b9f8b7a7022 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -296,6 +296,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:seer-explorer-chat-coding", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the Seer Overview sections for Seat-Based users manager.add("organizations:seer-overview", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Use org-level default stopping point and handoff for new project creation and migration + manager.add("organizations:seer-overview-project-creation", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable the Seer Wizard and related prompts/links/banners manager.add("organizations:seer-wizard", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the Seer issues view diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index fa2223f1e747e6..0a9937e4d0ec35 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -577,30 +577,36 @@ def set_default_project_seer_preferences(organization: Organization, project: Pr if not is_seer_seat_based_tier_enabled(organization): return + stopping_point = organization.get_option( + "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) + auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) + + automation_handoff: SeerAutomationHandoffConfiguration | None = None coding_agent = organization.get_option("sentry:seer_default_coding_agent") coding_agent_integration_id = organization.get_option( "sentry:seer_default_coding_agent_integration_id" ) - automation_handoff: SeerAutomationHandoffConfiguration | None = None if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: automation_handoff = SeerAutomationHandoffConfiguration( handoff_point=AutofixHandoffPoint.ROOT_CAUSE, target=coding_agent, integration_id=coding_agent_integration_id, - auto_create_pr=bool( - organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) - ), + auto_create_pr=auto_open_prs, ) + # If Seer agent and auto open PRs, we can run up to open_pr. + elif auto_open_prs: + stopping_point = "open_pr" + # If Seer agent and no auto open PRs, we shouldn't go past code_changes. + elif stopping_point == "open_pr": + stopping_point = "code_changes" # We need to make an API call to Seer to set this preference preference = SeerProjectPreference( organization_id=organization.id, project_id=project.id, repositories=[], - automated_run_stopping_point=organization.get_option( - "sentry:default_automated_run_stopping_point", - SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - ), + automated_run_stopping_point=stopping_point, automation_handoff=automation_handoff, ) diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index be4cf5cdd0e6b8..0ad8814296aee4 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -262,6 +262,9 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ) + valid_stopping_points = {"open_pr", "code_changes"} + if features.has("organizations:seer-overview-project-creation", organization): + valid_stopping_points.add("root_cause") preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) @@ -284,12 +287,12 @@ def configure_seer_for_existing_org(organization_id: int) -> None: # Skip projects that a) already have an acceptable stopping point configured # AND b) already have a handoff configured or no org default handoff. - if existing_stopping_point in ("open_pr", "code_changes") and ( + if existing_stopping_point in valid_stopping_points and ( existing_handoff or default_handoff is None ): continue - if existing_stopping_point in ("open_pr", "code_changes"): + if existing_stopping_point in valid_stopping_points: stopping_point = existing_stopping_point if existing_handoff: handoff = existing_handoff diff --git a/tests/sentry/seer/similarity/test_utils.py b/tests/sentry/seer/similarity/test_utils.py index 5a9de64e86b315..5b72d385a1163a 100644 --- a/tests/sentry/seer/similarity/test_utils.py +++ b/tests/sentry/seer/similarity/test_utils.py @@ -1274,10 +1274,10 @@ def test_seer_agent_with_custom_stopping_point( @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") @patch("sentry.seer.similarity.utils.set_project_seer_preference") @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_seer_agent_with_auto_open_prs_preserves_stopping_point( + def test_seer_agent_auto_open_prs_overrides_stopping_point( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): - """auto_open_prs does not override the configured stopping point.""" + """auto_open_prs=True forces stopping point to open_pr regardless of org default.""" self.organization.update_option("sentry:auto_open_prs", True) self.organization.update_option( "sentry:default_automated_run_stopping_point", "code_changes" @@ -1285,6 +1285,21 @@ def test_seer_agent_with_auto_open_prs_preserves_stopping_point( set_default_project_seer_preferences(self.organization, self.project) + pref = mock_set_pref.call_args[0][0] + assert pref.automated_run_stopping_point == "open_pr" + assert pref.automation_handoff is None + + @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") + @patch("sentry.seer.similarity.utils.set_project_seer_preference") + @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) + def test_seer_agent_no_auto_open_prs_caps_stopping_point( + self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock + ): + """Without auto_open_prs, org default of open_pr is capped to code_changes.""" + self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") + + set_default_project_seer_preferences(self.organization, self.project) + pref = mock_set_pref.call_args[0][0] assert pref.automated_run_stopping_point == "code_changes" assert pref.automation_handoff is None @@ -1324,7 +1339,7 @@ def test_external_agent_default( def test_external_agent_with_auto_open_prs( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): - """auto_open_prs sets auto_create_pr on handoff but does not override stopping point.""" + """auto_open_prs sets auto_create_pr on handoff; stopping point comes from org option.""" self.organization.update_option("sentry:auto_open_prs", True) self.organization.update_option( "sentry:default_automated_run_stopping_point", "code_changes" @@ -1353,14 +1368,16 @@ def test_external_agent_with_auto_open_prs( @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") @patch("sentry.seer.similarity.utils.set_project_seer_preference") @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_external_agent_without_integration_id_skips_handoff( + def test_external_agent_without_integration_id_falls_back_to_seer_agent( self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock ): self.organization.update_option( "sentry:seer_default_coding_agent", "cursor_background_agent" ) + self.organization.update_option("sentry:auto_open_prs", True) set_default_project_seer_preferences(self.organization, self.project) pref = mock_set_pref.call_args[0][0] + assert pref.automated_run_stopping_point == "open_pr" assert pref.automation_handoff is None diff --git a/tests/sentry/tasks/seer/test_autofix.py b/tests/sentry/tasks/seer/test_autofix.py index 31ef5373a3d254..901112e9fae930 100644 --- a/tests/sentry/tasks/seer/test_autofix.py +++ b/tests/sentry/tasks/seer/test_autofix.py @@ -16,6 +16,7 @@ generate_issue_summary_only, ) from sentry.testutils.cases import TestCase as SentryTestCase +from sentry.testutils.helpers.features import with_feature from sentry.utils.cache import cache @@ -354,6 +355,23 @@ def test_project_with_invalid_stopping_point_gets_org_default_stopping_point( ) assert prefs_by_project[project.id]["automation_handoff"] == existing_handoff + @with_feature("organizations:seer-overview-project-creation") + @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") + def test_root_cause_is_valid_stopping_point_with_flag( + self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock + ) -> None: + """With the feature flag, root_cause is a valid stopping point and the project is skipped.""" + project = self.create_project(organization=self.organization) + + mock_bulk_get.return_value = { + str(project.id): {"automated_run_stopping_point": "root_cause"}, + } + + configure_seer_for_existing_org(organization_id=self.organization.id) + + mock_bulk_set.assert_not_called() + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") def test_raises_on_bulk_get_api_failure(self, mock_bulk_get: MagicMock) -> None: """Test that task raises on bulk GET API failure to trigger retry.""" From 2e5a9370fd94c7420caf7035bb7735907434efe5 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 11:25:53 -0700 Subject: [PATCH 22/31] allow root cause default stopping point if feature flag --- src/sentry/core/endpoints/organization_details.py | 10 +++++++++- .../core/endpoints/test_organization_details.py | 6 ++++++ tests/sentry/seer/similarity/test_utils.py | 15 --------------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index a3c7fd1f39b75a..374d2d9658f786 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -384,7 +384,7 @@ class OrganizationSerializer(BaseOrganizationSerializer): ) defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True) defaultAutomatedRunStoppingPoint = serializers.ChoiceField( - choices=["code_changes", "open_pr"], required=False + choices=["code_changes", "open_pr", "root_cause"], required=False ) autoOpenPrs = serializers.BooleanField(required=False) autoEnableCodeReview = serializers.BooleanField(required=False) @@ -433,6 +433,14 @@ def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | N raise serializers.ValidationError("Integration does not exist.") return value + def validate_defaultAutomatedRunStoppingPoint(self, value: str) -> str: + organization = self.context["organization"] + if value == "root_cause" and not features.has( + "organizations:seer-overview-project-creation", organization + ): + raise serializers.ValidationError("Invalid default stopping point") + return value + def validate_sensitiveFields(self, value): if value and not all(value): raise serializers.ValidationError("Empty values are not allowed.") diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index c931c3e8269e5f..4421695bbdc23d 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -1627,6 +1627,12 @@ def test_default_automated_run_stopping_point_can_be_set(self) -> None: response = self.get_success_response(self.organization.slug, **data) assert response.data["defaultAutomatedRunStoppingPoint"] == choice + @with_feature("organizations:seer-overview-project-creation") + def test_default_automated_run_stopping_point_root_cause_with_flag(self) -> None: + data = {"defaultAutomatedRunStoppingPoint": "root_cause"} + response = self.get_success_response(self.organization.slug, **data) + assert response.data["defaultAutomatedRunStoppingPoint"] == "root_cause" + def test_default_automated_run_stopping_point_rejects_invalid(self) -> None: for invalid in ("root_cause", "solution", "invalid_point"): with self.subTest(value=invalid): diff --git a/tests/sentry/seer/similarity/test_utils.py b/tests/sentry/seer/similarity/test_utils.py index 5b72d385a1163a..367cf869f0d555 100644 --- a/tests/sentry/seer/similarity/test_utils.py +++ b/tests/sentry/seer/similarity/test_utils.py @@ -1256,21 +1256,6 @@ def test_seer_agent_default( assert pref.automated_run_stopping_point == "code_changes" assert pref.automation_handoff is None - @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") - @patch("sentry.seer.similarity.utils.set_project_seer_preference") - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_seer_agent_with_custom_stopping_point( - self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock - ): - """Seer agent, no auto_open_prs, non-default stopping point.""" - self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") - - set_default_project_seer_preferences(self.organization, self.project) - - pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "open_pr" - assert pref.automation_handoff is None - @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") @patch("sentry.seer.similarity.utils.set_project_seer_preference") @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) From ab215d08394365711ba73d2e151042cf58321b27 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 11:28:55 -0700 Subject: [PATCH 23/31] use existing feature flag --- src/sentry/core/endpoints/organization_details.py | 4 +--- src/sentry/features/temporary.py | 2 -- src/sentry/tasks/seer/autofix.py | 2 +- tests/sentry/core/endpoints/test_organization_details.py | 2 +- tests/sentry/tasks/seer/test_autofix.py | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index 374d2d9658f786..f936ee32723adf 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -435,9 +435,7 @@ def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | N def validate_defaultAutomatedRunStoppingPoint(self, value: str) -> str: organization = self.context["organization"] - if value == "root_cause" and not features.has( - "organizations:seer-overview-project-creation", organization - ): + if value == "root_cause" and not features.has("organizations:seer-overview", organization): raise serializers.ValidationError("Invalid default stopping point") return value diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 112b9f8b7a7022..1122038668af8e 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -296,8 +296,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:seer-explorer-chat-coding", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the Seer Overview sections for Seat-Based users manager.add("organizations:seer-overview", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Use org-level default stopping point and handoff for new project creation and migration - manager.add("organizations:seer-overview-project-creation", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable the Seer Wizard and related prompts/links/banners manager.add("organizations:seer-wizard", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the Seer issues view diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 0ad8814296aee4..1238fb6cb35f2c 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -263,7 +263,7 @@ def configure_seer_for_existing_org(organization_id: int) -> None: SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ) valid_stopping_points = {"open_pr", "code_changes"} - if features.has("organizations:seer-overview-project-creation", organization): + if features.has("organizations:seer-overview", organization): valid_stopping_points.add("root_cause") preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index 4421695bbdc23d..d5e4e1786de7cb 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -1627,7 +1627,7 @@ def test_default_automated_run_stopping_point_can_be_set(self) -> None: response = self.get_success_response(self.organization.slug, **data) assert response.data["defaultAutomatedRunStoppingPoint"] == choice - @with_feature("organizations:seer-overview-project-creation") + @with_feature("organizations:seer-overview") def test_default_automated_run_stopping_point_root_cause_with_flag(self) -> None: data = {"defaultAutomatedRunStoppingPoint": "root_cause"} response = self.get_success_response(self.organization.slug, **data) diff --git a/tests/sentry/tasks/seer/test_autofix.py b/tests/sentry/tasks/seer/test_autofix.py index 901112e9fae930..0c0f7834b5aeb6 100644 --- a/tests/sentry/tasks/seer/test_autofix.py +++ b/tests/sentry/tasks/seer/test_autofix.py @@ -355,7 +355,7 @@ def test_project_with_invalid_stopping_point_gets_org_default_stopping_point( ) assert prefs_by_project[project.id]["automation_handoff"] == existing_handoff - @with_feature("organizations:seer-overview-project-creation") + @with_feature("organizations:seer-overview") @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") def test_root_cause_is_valid_stopping_point_with_flag( From 53c106c1c36b3c77176f1cbdfa49b68441306ca4 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 12:04:01 -0700 Subject: [PATCH 24/31] fix auto_open_prs logic in configure_seer_for_existing_org --- src/sentry/tasks/seer/autofix.py | 17 +++++--- tests/sentry/tasks/seer/test_autofix.py | 52 ++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 1238fb6cb35f2c..3bc0edd128d8a6 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -243,11 +243,16 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM ) + default_stopping_point = organization.get_option( + "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) + auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) + + default_handoff: dict[str, Any] | None = None coding_agent = organization.get_option("sentry:seer_default_coding_agent") coding_agent_integration_id = organization.get_option( "sentry:seer_default_coding_agent_integration_id" ) - default_handoff: dict[str, Any] | None = None if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: default_handoff = { "handoff_point": "root_cause", @@ -257,11 +262,13 @@ def configure_seer_for_existing_org(organization_id: int) -> None: organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) ), } + # If Seer agent and auto open PRs, we can run up to open_pr. + elif auto_open_prs: + default_stopping_point = "open_pr" + # If Seer agent and no auto open PRs, we shouldn't go past code_changes. + elif default_stopping_point == "open_pr": + default_stopping_point = "code_changes" - default_stopping_point = organization.get_option( - "sentry:default_automated_run_stopping_point", - SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, - ) valid_stopping_points = {"open_pr", "code_changes"} if features.has("organizations:seer-overview", organization): valid_stopping_points.add("root_cause") diff --git a/tests/sentry/tasks/seer/test_autofix.py b/tests/sentry/tasks/seer/test_autofix.py index 0c0f7834b5aeb6..a0fbf5b21f57cc 100644 --- a/tests/sentry/tasks/seer/test_autofix.py +++ b/tests/sentry/tasks/seer/test_autofix.py @@ -204,7 +204,6 @@ def test_new_project_gets_stopping_point_and_no_handoff_from_org_defaults( ) -> None: """Project with no existing prefs gets stopping point and no handoff (seer coding agent) from org defaults.""" project = self.create_project(organization=self.organization) - self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") mock_bulk_get.return_value = {} @@ -213,7 +212,7 @@ def test_new_project_gets_stopping_point_and_no_handoff_from_org_defaults( mock_bulk_set.assert_called_once() prefs = mock_bulk_set.call_args[0][1] prefs_by_project = {p["project_id"]: p for p in prefs} - assert prefs_by_project[project.id]["automated_run_stopping_point"] == "open_pr" + assert prefs_by_project[project.id]["automated_run_stopping_point"] == "code_changes" assert prefs_by_project[project.id]["automation_handoff"] is None @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") @@ -242,6 +241,11 @@ def test_new_project_gets_stopping_point_and_handoff_from_org_defaults( "integration_id": 42, "auto_create_pr": True, } + # auto_open_prs should NOT override stopping point for external agents + assert ( + prefs_by_project[project.id]["automated_run_stopping_point"] + == SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") @@ -372,6 +376,50 @@ def test_root_cause_is_valid_stopping_point_with_flag( mock_bulk_set.assert_not_called() + @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") + def test_seer_agent_auto_open_prs_overrides_stopping_point_to_open_pr( + self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock + ) -> None: + """When Seer agent (no external handoff) and auto_open_prs=True, + stopping point is forced to open_pr regardless of org default.""" + project = self.create_project(organization=self.organization) + self.organization.update_option( + "sentry:default_automated_run_stopping_point", "code_changes" + ) + self.organization.update_option("sentry:auto_open_prs", True) + + mock_bulk_get.return_value = {} + + configure_seer_for_existing_org(organization_id=self.organization.id) + + mock_bulk_set.assert_called_once() + prefs = mock_bulk_set.call_args[0][1] + prefs_by_project = {p["project_id"]: p for p in prefs} + assert prefs_by_project[project.id]["automated_run_stopping_point"] == "open_pr" + assert prefs_by_project[project.id]["automation_handoff"] is None + + @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") + def test_seer_agent_no_auto_open_prs_caps_open_pr_to_code_changes( + self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock + ) -> None: + """When Seer agent (no external handoff) and auto_open_prs=False, + org default of open_pr is capped to code_changes.""" + project = self.create_project(organization=self.organization) + self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") + self.organization.update_option("sentry:auto_open_prs", False) + + mock_bulk_get.return_value = {} + + configure_seer_for_existing_org(organization_id=self.organization.id) + + mock_bulk_set.assert_called_once() + prefs = mock_bulk_set.call_args[0][1] + prefs_by_project = {p["project_id"]: p for p in prefs} + assert prefs_by_project[project.id]["automated_run_stopping_point"] == "code_changes" + assert prefs_by_project[project.id]["automation_handoff"] is None + @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") def test_raises_on_bulk_get_api_failure(self, mock_bulk_get: MagicMock) -> None: """Test that task raises on bulk GET API failure to trigger retry.""" From d7e7df508b0c09edb147be2afa6824794b4cb37c Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 13:15:16 -0700 Subject: [PATCH 25/31] small fix --- src/sentry/tasks/seer/autofix.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 3bc0edd128d8a6..e36d9ba864b2de 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -258,9 +258,7 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "handoff_point": "root_cause", "target": coding_agent, "integration_id": coding_agent_integration_id, - "auto_create_pr": bool( - organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) - ), + "auto_create_pr": auto_open_prs, } # If Seer agent and auto open PRs, we can run up to open_pr. elif auto_open_prs: From adeb0c8345fe727b5d3493aba5118ce22d604334 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 13:31:01 -0700 Subject: [PATCH 26/31] fall back to default stoppin gpont if root cause and not feature flagged --- src/sentry/seer/similarity/utils.py | 5 +++++ src/sentry/tasks/seer/autofix.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index 0a9937e4d0ec35..577293e9298574 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -580,6 +580,11 @@ def set_default_project_seer_preferences(organization: Organization, project: Pr stopping_point = organization.get_option( "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT ) + if stopping_point == "root_cause" and not features.has( + "organizations:seer-overview", organization + ): + stopping_point = SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) automation_handoff: SeerAutomationHandoffConfiguration | None = None diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index e36d9ba864b2de..4b8cd61c0e81e6 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -246,6 +246,11 @@ def configure_seer_for_existing_org(organization_id: int) -> None: default_stopping_point = organization.get_option( "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT ) + if default_stopping_point == "root_cause" and not features.has( + "organizations:seer-overview", organization + ): + default_stopping_point = SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) default_handoff: dict[str, Any] | None = None From e236a23c3920e465e4c04c185e866c7b29d9dc9f Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 13:54:12 -0700 Subject: [PATCH 27/31] make helper and move tests around --- src/sentry/seer/autofix/utils.py | 45 +++++- src/sentry/seer/similarity/utils.py | 34 +--- src/sentry/tasks/seer/autofix.py | 34 +--- .../sentry/seer/autofix/test_autofix_utils.py | 105 +++++++++++++ tests/sentry/seer/similarity/test_utils.py | 146 ------------------ tests/sentry/tasks/seer/test_autofix.py | 44 ------ 6 files changed, 154 insertions(+), 254 deletions(-) diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index 12e4d5feb2092a..2fa778e0220ce6 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -15,6 +15,7 @@ from sentry import features, options, ratelimits from sentry.constants import ( + AUTO_OPEN_PRS_DEFAULT, SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, DataCategory, ObjectStatus, @@ -42,6 +43,10 @@ SeerProjectRepository, SeerProjectRepositoryBranchOverride, ) +from sentry.seer.models.seer_api_models import ( + AutofixHandoffPoint, + SeerAutomationHandoffConfiguration, +) from sentry.seer.signed_seer_api import SeerViewerContext, make_signed_seer_api_request from sentry.utils.cache import cache from sentry.utils.outcomes import Outcome, track_outcome @@ -388,11 +393,49 @@ def default_seer_project_preference(project: Project) -> SeerProjectPreference: organization_id=project.organization.id, project_id=project.id, repositories=[], - automated_run_stopping_point=AutofixStoppingPoint.CODE_CHANGES.value, + automated_run_stopping_point=project.organization.get_option( + "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ), automation_handoff=None, ) +def get_org_default_seer_automation_handoff( + organization: Organization, +) -> tuple[str, SeerAutomationHandoffConfiguration | None]: + """Get the default stopping point and automation handoff for an organization.""" + stopping_point = organization.get_option( + "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + ) + if stopping_point == "root_cause" and not features.has( + "organizations:seer-overview", organization + ): + stopping_point = SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT + + auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) + + automation_handoff: SeerAutomationHandoffConfiguration | None = None + coding_agent = organization.get_option("sentry:seer_default_coding_agent") + coding_agent_integration_id = organization.get_option( + "sentry:seer_default_coding_agent_integration_id" + ) + if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: + automation_handoff = SeerAutomationHandoffConfiguration( + handoff_point=AutofixHandoffPoint.ROOT_CAUSE, + target=coding_agent, + integration_id=coding_agent_integration_id, + auto_create_pr=auto_open_prs, + ) + # If Seer agent and auto open PRs, we can run up to open_pr. + elif auto_open_prs: + stopping_point = "open_pr" + # If Seer agent and no auto open PRs, we shouldn't go past code_changes. + elif stopping_point == "open_pr": + stopping_point = "code_changes" + + return stopping_point, automation_handoff + + def get_project_seer_preferences(project_id: int) -> SeerRawPreferenceResponse: """ Fetch Seer project preferences from the Seer API. diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index 577293e9298574..ff530ed98c12dd 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -10,9 +10,7 @@ from sentry import features, options from sentry.constants import ( - AUTO_OPEN_PRS_DEFAULT, DATA_ROOT, - SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ) from sentry.grouping.api import get_contributing_variant_and_component from sentry.grouping.grouping_info import get_grouping_info_from_variants_legacy @@ -22,13 +20,12 @@ from sentry.models.project import Project from sentry.seer.autofix.constants import AutofixAutomationTuningSettings from sentry.seer.autofix.utils import ( + get_org_default_seer_automation_handoff, is_seer_seat_based_tier_enabled, set_project_seer_preference, write_preference_to_sentry_db, ) from sentry.seer.models import ( - AutofixHandoffPoint, - SeerAutomationHandoffConfiguration, SeerProjectPreference, ) from sentry.seer.similarity.types import GroupingVersion @@ -577,34 +574,7 @@ def set_default_project_seer_preferences(organization: Organization, project: Pr if not is_seer_seat_based_tier_enabled(organization): return - stopping_point = organization.get_option( - "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT - ) - if stopping_point == "root_cause" and not features.has( - "organizations:seer-overview", organization - ): - stopping_point = SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT - - auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) - - automation_handoff: SeerAutomationHandoffConfiguration | None = None - coding_agent = organization.get_option("sentry:seer_default_coding_agent") - coding_agent_integration_id = organization.get_option( - "sentry:seer_default_coding_agent_integration_id" - ) - if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: - automation_handoff = SeerAutomationHandoffConfiguration( - handoff_point=AutofixHandoffPoint.ROOT_CAUSE, - target=coding_agent, - integration_id=coding_agent_integration_id, - auto_create_pr=auto_open_prs, - ) - # If Seer agent and auto open PRs, we can run up to open_pr. - elif auto_open_prs: - stopping_point = "open_pr" - # If Seer agent and no auto open PRs, we shouldn't go past code_changes. - elif stopping_point == "open_pr": - stopping_point = "code_changes" + stopping_point, automation_handoff = get_org_default_seer_automation_handoff(organization) # We need to make an API call to Seer to set this preference preference = SeerProjectPreference( diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 4b8cd61c0e81e6..369aec88e54f84 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -1,6 +1,5 @@ import logging from datetime import datetime, timedelta -from typing import Any import sentry_sdk from django.utils import timezone @@ -10,8 +9,6 @@ from sentry import analytics, features from sentry.analytics.events.autofix_automation_events import AiAutofixAutomationEvent from sentry.constants import ( - AUTO_OPEN_PRS_DEFAULT, - SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, ObjectStatus, ) from sentry.models.group import Group @@ -29,6 +26,7 @@ deduplicate_repositories, get_autofix_repos_from_project_code_mappings, get_autofix_state, + get_org_default_seer_automation_handoff, get_seer_seat_based_tier_cache_key, resolve_repository_ids, ) @@ -243,34 +241,8 @@ def configure_seer_for_existing_org(organization_id: int) -> None: "sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM ) - default_stopping_point = organization.get_option( - "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT - ) - if default_stopping_point == "root_cause" and not features.has( - "organizations:seer-overview", organization - ): - default_stopping_point = SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT - - auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) - - default_handoff: dict[str, Any] | None = None - coding_agent = organization.get_option("sentry:seer_default_coding_agent") - coding_agent_integration_id = organization.get_option( - "sentry:seer_default_coding_agent_integration_id" - ) - if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None: - default_handoff = { - "handoff_point": "root_cause", - "target": coding_agent, - "integration_id": coding_agent_integration_id, - "auto_create_pr": auto_open_prs, - } - # If Seer agent and auto open PRs, we can run up to open_pr. - elif auto_open_prs: - default_stopping_point = "open_pr" - # If Seer agent and no auto open PRs, we shouldn't go past code_changes. - elif default_stopping_point == "open_pr": - default_stopping_point = "code_changes" + default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization) + default_handoff = default_handoff.dict() if default_handoff else None valid_stopping_points = {"open_pr", "code_changes"} if features.has("organizations:seer-overview", organization): diff --git a/tests/sentry/seer/autofix/test_autofix_utils.py b/tests/sentry/seer/autofix/test_autofix_utils.py index d125288ea479b6..3f3a6ffdc407be 100644 --- a/tests/sentry/seer/autofix/test_autofix_utils.py +++ b/tests/sentry/seer/autofix/test_autofix_utils.py @@ -15,6 +15,7 @@ deduplicate_repositories, get_autofix_prompt, get_coding_agent_prompt, + get_org_default_seer_automation_handoff, has_project_connected_repos, is_seer_seat_based_tier_enabled, resolve_repository_ids, @@ -33,6 +34,7 @@ SeerProjectRepositoryBranchOverride, ) from sentry.testutils.cases import TestCase +from sentry.testutils.helpers.features import with_feature from sentry.utils.cache import cache @@ -1218,3 +1220,106 @@ def test_bulk_write_replaces_per_project(self): assert p1_repo.branch_name == "new-branch" p2_repo = SeerProjectRepository.objects.get(project=project2) assert p2_repo.branch_name == "project-2-branch" + + +class TestGetOrgDefaultSeerAutomationHandoff(TestCase): + def test_defaults(self): + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "code_changes" + assert handoff is None + + def test_respects_org_stopping_point_option(self): + self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") + self.organization.update_option("sentry:auto_open_prs", True) + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "open_pr" + assert handoff is None + + @with_feature("organizations:seer-overview") + def test_root_cause_stopping_point_allowed_with_flag(self): + self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "root_cause" + assert handoff is None + + def test_root_cause_stopping_point_falls_back_without_seer_overview_flag(self): + self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "code_changes" + assert handoff is None + + def test_seer_agent_auto_open_prs_forces_open_pr(self): + self.organization.update_option( + "sentry:default_automated_run_stopping_point", "code_changes" + ) + self.organization.update_option("sentry:auto_open_prs", True) + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "open_pr" + assert handoff is None + + def test_seer_agent_no_auto_open_prs_caps_open_pr_to_code_changes(self): + self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "code_changes" + assert handoff is None + + def test_external_agent_returns_handoff_config(self): + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) + self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 42) + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "code_changes" + assert handoff is not None + assert handoff.handoff_point == "root_cause" + assert handoff.target == "cursor_background_agent" + assert handoff.integration_id == 42 + assert handoff.auto_create_pr is False + + def test_external_agent_auto_open_prs_sets_auto_create_pr(self): + self.organization.update_option("sentry:seer_default_coding_agent", "claude_code_agent") + self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 99) + self.organization.update_option("sentry:auto_open_prs", True) + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert handoff is not None + assert handoff.auto_create_pr is True + + def test_external_agent_auto_open_prs_does_not_override_stopping_point(self): + self.organization.update_option( + "sentry:default_automated_run_stopping_point", "code_changes" + ) + self.organization.update_option("sentry:auto_open_prs", True) + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) + self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 42) + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "code_changes" + assert handoff is not None + + def test_external_agent_without_integration_id_falls_back_to_seer(self): + self.organization.update_option( + "sentry:seer_default_coding_agent", "cursor_background_agent" + ) + self.organization.update_option("sentry:auto_open_prs", True) + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "open_pr" + assert handoff is None + + def test_seer_coding_agent_treated_as_no_external_agent(self): + self.organization.update_option("sentry:seer_default_coding_agent", "seer") + self.organization.update_option("sentry:seer_default_coding_agent_integration_id", 42) + self.organization.update_option("sentry:auto_open_prs", True) + + stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) + assert stopping_point == "open_pr" + assert handoff is None diff --git a/tests/sentry/seer/similarity/test_utils.py b/tests/sentry/seer/similarity/test_utils.py index 367cf869f0d555..2ae2a6f77bca3f 100644 --- a/tests/sentry/seer/similarity/test_utils.py +++ b/tests/sentry/seer/similarity/test_utils.py @@ -15,7 +15,6 @@ filter_null_from_string, get_stacktrace_string, get_token_count, - set_default_project_seer_preferences, stacktrace_exceeds_limits, ) from sentry.services.eventstore.models import Event @@ -1221,148 +1220,3 @@ def test_generates_stacktrace_string_from_variants(self) -> None: assert token_count > 0 # Verify we get the expected token count for this specific stacktrace assert token_count == 33 - - -class TestSetDefaultProjectSeerPreferences(TestCase): - """Tests for set_default_project_seer_preferences which wires org-level Seer - defaults (stopping point, coding agent, auto_open_prs) into project-level - preferences at project creation time. - """ - - def setUp(self): - super().setUp() - self.project = self.create_project(organization=self.organization) - - @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") - @patch("sentry.seer.similarity.utils.set_project_seer_preference") - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=False) - def test_skips_when_tier_not_enabled( - self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock - ): - set_default_project_seer_preferences(self.organization, self.project) - mock_set_pref.assert_not_called() - mock_dual_write.assert_not_called() - - @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") - @patch("sentry.seer.similarity.utils.set_project_seer_preference") - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_seer_agent_default( - self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock - ): - """Seer agent, no auto_open_prs, default stopping point (code_changes).""" - set_default_project_seer_preferences(self.organization, self.project) - - pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "code_changes" - assert pref.automation_handoff is None - - @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") - @patch("sentry.seer.similarity.utils.set_project_seer_preference") - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_seer_agent_auto_open_prs_overrides_stopping_point( - self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock - ): - """auto_open_prs=True forces stopping point to open_pr regardless of org default.""" - self.organization.update_option("sentry:auto_open_prs", True) - self.organization.update_option( - "sentry:default_automated_run_stopping_point", "code_changes" - ) - - set_default_project_seer_preferences(self.organization, self.project) - - pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "open_pr" - assert pref.automation_handoff is None - - @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") - @patch("sentry.seer.similarity.utils.set_project_seer_preference") - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_seer_agent_no_auto_open_prs_caps_stopping_point( - self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock - ): - """Without auto_open_prs, org default of open_pr is capped to code_changes.""" - self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") - - set_default_project_seer_preferences(self.organization, self.project) - - pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "code_changes" - assert pref.automation_handoff is None - - @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") - @patch("sentry.seer.similarity.utils.set_project_seer_preference") - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_external_agent_default( - self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock - ): - """external agent, no auto_open_prs, default stopping point and handoff.""" - agents = [ - ("cursor_background_agent", 1234), - ("claude_code_agent", 5678), - ] - for agent, integration_id in agents: - with self.subTest(agent=agent): - mock_set_pref.reset_mock() - self.organization.update_option("sentry:seer_default_coding_agent", agent) - self.organization.update_option( - "sentry:seer_default_coding_agent_integration_id", integration_id - ) - - set_default_project_seer_preferences(self.organization, self.project) - - pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "code_changes" - assert pref.automation_handoff is not None - assert pref.automation_handoff.handoff_point == "root_cause" - assert pref.automation_handoff.target == agent - assert pref.automation_handoff.integration_id == integration_id - assert pref.automation_handoff.auto_create_pr is False - - @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") - @patch("sentry.seer.similarity.utils.set_project_seer_preference") - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_external_agent_with_auto_open_prs( - self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock - ): - """auto_open_prs sets auto_create_pr on handoff; stopping point comes from org option.""" - self.organization.update_option("sentry:auto_open_prs", True) - self.organization.update_option( - "sentry:default_automated_run_stopping_point", "code_changes" - ) - agents = [ - ("cursor_background_agent", 1234), - ("claude_code_agent", 5678), - ] - for agent, integration_id in agents: - with self.subTest(agent=agent): - mock_set_pref.reset_mock() - self.organization.update_option("sentry:seer_default_coding_agent", agent) - self.organization.update_option( - "sentry:seer_default_coding_agent_integration_id", integration_id - ) - - set_default_project_seer_preferences(self.organization, self.project) - - pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "code_changes" - assert pref.automation_handoff is not None - assert pref.automation_handoff.target == agent - assert pref.automation_handoff.integration_id == integration_id - assert pref.automation_handoff.auto_create_pr is True - - @patch("sentry.seer.similarity.utils.write_preference_to_sentry_db") - @patch("sentry.seer.similarity.utils.set_project_seer_preference") - @patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True) - def test_external_agent_without_integration_id_falls_back_to_seer_agent( - self, mock_tier: MagicMock, mock_set_pref: MagicMock, mock_dual_write: MagicMock - ): - self.organization.update_option( - "sentry:seer_default_coding_agent", "cursor_background_agent" - ) - self.organization.update_option("sentry:auto_open_prs", True) - - set_default_project_seer_preferences(self.organization, self.project) - - pref = mock_set_pref.call_args[0][0] - assert pref.automated_run_stopping_point == "open_pr" - assert pref.automation_handoff is None diff --git a/tests/sentry/tasks/seer/test_autofix.py b/tests/sentry/tasks/seer/test_autofix.py index a0fbf5b21f57cc..90f5665b1eef95 100644 --- a/tests/sentry/tasks/seer/test_autofix.py +++ b/tests/sentry/tasks/seer/test_autofix.py @@ -376,50 +376,6 @@ def test_root_cause_is_valid_stopping_point_with_flag( mock_bulk_set.assert_not_called() - @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") - @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") - def test_seer_agent_auto_open_prs_overrides_stopping_point_to_open_pr( - self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock - ) -> None: - """When Seer agent (no external handoff) and auto_open_prs=True, - stopping point is forced to open_pr regardless of org default.""" - project = self.create_project(organization=self.organization) - self.organization.update_option( - "sentry:default_automated_run_stopping_point", "code_changes" - ) - self.organization.update_option("sentry:auto_open_prs", True) - - mock_bulk_get.return_value = {} - - configure_seer_for_existing_org(organization_id=self.organization.id) - - mock_bulk_set.assert_called_once() - prefs = mock_bulk_set.call_args[0][1] - prefs_by_project = {p["project_id"]: p for p in prefs} - assert prefs_by_project[project.id]["automated_run_stopping_point"] == "open_pr" - assert prefs_by_project[project.id]["automation_handoff"] is None - - @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") - @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") - def test_seer_agent_no_auto_open_prs_caps_open_pr_to_code_changes( - self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock - ) -> None: - """When Seer agent (no external handoff) and auto_open_prs=False, - org default of open_pr is capped to code_changes.""" - project = self.create_project(organization=self.organization) - self.organization.update_option("sentry:default_automated_run_stopping_point", "open_pr") - self.organization.update_option("sentry:auto_open_prs", False) - - mock_bulk_get.return_value = {} - - configure_seer_for_existing_org(organization_id=self.organization.id) - - mock_bulk_set.assert_called_once() - prefs = mock_bulk_set.call_args[0][1] - prefs_by_project = {p["project_id"]: p for p in prefs} - assert prefs_by_project[project.id]["automated_run_stopping_point"] == "code_changes" - assert prefs_by_project[project.id]["automation_handoff"] is None - @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") def test_raises_on_bulk_get_api_failure(self, mock_bulk_get: MagicMock) -> None: """Test that task raises on bulk GET API failure to trigger retry.""" From 6c3c62dd3ecf3cd8bb0c90c7855c91fcab1ee0c6 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 13:56:39 -0700 Subject: [PATCH 28/31] fix mypy --- src/sentry/tasks/seer/autofix.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 369aec88e54f84..8e6ef48e79f5fd 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -242,7 +242,7 @@ def configure_seer_for_existing_org(organization_id: int) -> None: ) default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization) - default_handoff = default_handoff.dict() if default_handoff else None + default_handoff_dict = default_handoff.dict() if default_handoff else None valid_stopping_points = {"open_pr", "code_changes"} if features.has("organizations:seer-overview", organization): @@ -255,7 +255,7 @@ def configure_seer_for_existing_org(organization_id: int) -> None: projects_by_id = {p.id: p for p in projects} for project_id in project_ids: stopping_point = default_stopping_point - handoff = default_handoff + handoff = default_handoff_dict existing_pref = preferences_by_id.get(str(project_id)) if not existing_pref: @@ -270,7 +270,7 @@ def configure_seer_for_existing_org(organization_id: int) -> None: # Skip projects that a) already have an acceptable stopping point configured # AND b) already have a handoff configured or no org default handoff. if existing_stopping_point in valid_stopping_points and ( - existing_handoff or default_handoff is None + existing_handoff or default_handoff_dict is None ): continue From ac7e0daba761e8f6d69b8dcedfd64f581ee18ef3 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 14:10:23 -0700 Subject: [PATCH 29/31] rm root cause feature flag --- .../core/endpoints/organization_details.py | 8 +------- src/sentry/seer/autofix/utils.py | 8 +------- src/sentry/tasks/seer/autofix.py | 2 -- .../endpoints/test_organization_details.py | 6 ------ .../sentry/seer/autofix/test_autofix_utils.py | 11 +---------- tests/sentry/tasks/seer/test_autofix.py | 18 ------------------ 6 files changed, 3 insertions(+), 50 deletions(-) diff --git a/src/sentry/core/endpoints/organization_details.py b/src/sentry/core/endpoints/organization_details.py index f936ee32723adf..a3c7fd1f39b75a 100644 --- a/src/sentry/core/endpoints/organization_details.py +++ b/src/sentry/core/endpoints/organization_details.py @@ -384,7 +384,7 @@ class OrganizationSerializer(BaseOrganizationSerializer): ) defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True) defaultAutomatedRunStoppingPoint = serializers.ChoiceField( - choices=["code_changes", "open_pr", "root_cause"], required=False + choices=["code_changes", "open_pr"], required=False ) autoOpenPrs = serializers.BooleanField(required=False) autoEnableCodeReview = serializers.BooleanField(required=False) @@ -433,12 +433,6 @@ def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | N raise serializers.ValidationError("Integration does not exist.") return value - def validate_defaultAutomatedRunStoppingPoint(self, value: str) -> str: - organization = self.context["organization"] - if value == "root_cause" and not features.has("organizations:seer-overview", organization): - raise serializers.ValidationError("Invalid default stopping point") - return value - def validate_sensitiveFields(self, value): if value and not all(value): raise serializers.ValidationError("Empty values are not allowed.") diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index 2fa778e0220ce6..fba791d6413418 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -393,9 +393,7 @@ def default_seer_project_preference(project: Project) -> SeerProjectPreference: organization_id=project.organization.id, project_id=project.id, repositories=[], - automated_run_stopping_point=project.organization.get_option( - "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT - ), + automated_run_stopping_point=AutofixStoppingPoint.CODE_CHANGES.value, automation_handoff=None, ) @@ -407,10 +405,6 @@ def get_org_default_seer_automation_handoff( stopping_point = organization.get_option( "sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT ) - if stopping_point == "root_cause" and not features.has( - "organizations:seer-overview", organization - ): - stopping_point = SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT) diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 8e6ef48e79f5fd..1d4ac97d8abcb2 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -245,8 +245,6 @@ def configure_seer_for_existing_org(organization_id: int) -> None: default_handoff_dict = default_handoff.dict() if default_handoff else None valid_stopping_points = {"open_pr", "code_changes"} - if features.has("organizations:seer-overview", organization): - valid_stopping_points.add("root_cause") preferences_by_id = bulk_get_project_preferences(organization_id, project_ids) diff --git a/tests/sentry/core/endpoints/test_organization_details.py b/tests/sentry/core/endpoints/test_organization_details.py index d5e4e1786de7cb..c931c3e8269e5f 100644 --- a/tests/sentry/core/endpoints/test_organization_details.py +++ b/tests/sentry/core/endpoints/test_organization_details.py @@ -1627,12 +1627,6 @@ def test_default_automated_run_stopping_point_can_be_set(self) -> None: response = self.get_success_response(self.organization.slug, **data) assert response.data["defaultAutomatedRunStoppingPoint"] == choice - @with_feature("organizations:seer-overview") - def test_default_automated_run_stopping_point_root_cause_with_flag(self) -> None: - data = {"defaultAutomatedRunStoppingPoint": "root_cause"} - response = self.get_success_response(self.organization.slug, **data) - assert response.data["defaultAutomatedRunStoppingPoint"] == "root_cause" - def test_default_automated_run_stopping_point_rejects_invalid(self) -> None: for invalid in ("root_cause", "solution", "invalid_point"): with self.subTest(value=invalid): diff --git a/tests/sentry/seer/autofix/test_autofix_utils.py b/tests/sentry/seer/autofix/test_autofix_utils.py index 3f3a6ffdc407be..9f2ed346712140 100644 --- a/tests/sentry/seer/autofix/test_autofix_utils.py +++ b/tests/sentry/seer/autofix/test_autofix_utils.py @@ -34,7 +34,6 @@ SeerProjectRepositoryBranchOverride, ) from sentry.testutils.cases import TestCase -from sentry.testutils.helpers.features import with_feature from sentry.utils.cache import cache @@ -1236,15 +1235,7 @@ def test_respects_org_stopping_point_option(self): assert stopping_point == "open_pr" assert handoff is None - @with_feature("organizations:seer-overview") - def test_root_cause_stopping_point_allowed_with_flag(self): - self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") - - stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) - assert stopping_point == "root_cause" - assert handoff is None - - def test_root_cause_stopping_point_falls_back_without_seer_overview_flag(self): + def test_invalid_stopping_point_falls_back_to_default(self): self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) diff --git a/tests/sentry/tasks/seer/test_autofix.py b/tests/sentry/tasks/seer/test_autofix.py index 90f5665b1eef95..ed92f065fb2c17 100644 --- a/tests/sentry/tasks/seer/test_autofix.py +++ b/tests/sentry/tasks/seer/test_autofix.py @@ -16,7 +16,6 @@ generate_issue_summary_only, ) from sentry.testutils.cases import TestCase as SentryTestCase -from sentry.testutils.helpers.features import with_feature from sentry.utils.cache import cache @@ -359,23 +358,6 @@ def test_project_with_invalid_stopping_point_gets_org_default_stopping_point( ) assert prefs_by_project[project.id]["automation_handoff"] == existing_handoff - @with_feature("organizations:seer-overview") - @patch("sentry.tasks.seer.autofix.bulk_set_project_preferences") - @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") - def test_root_cause_is_valid_stopping_point_with_flag( - self, mock_bulk_get: MagicMock, mock_bulk_set: MagicMock - ) -> None: - """With the feature flag, root_cause is a valid stopping point and the project is skipped.""" - project = self.create_project(organization=self.organization) - - mock_bulk_get.return_value = { - str(project.id): {"automated_run_stopping_point": "root_cause"}, - } - - configure_seer_for_existing_org(organization_id=self.organization.id) - - mock_bulk_set.assert_not_called() - @patch("sentry.tasks.seer.autofix.bulk_get_project_preferences") def test_raises_on_bulk_get_api_failure(self, mock_bulk_get: MagicMock) -> None: """Test that task raises on bulk GET API failure to trigger retry.""" From e210c941ab56cb379c6876e238282167deff83b2 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 14:16:33 -0700 Subject: [PATCH 30/31] fix test --- tests/sentry/seer/autofix/test_autofix_utils.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/sentry/seer/autofix/test_autofix_utils.py b/tests/sentry/seer/autofix/test_autofix_utils.py index 9f2ed346712140..4ac7effa8548d1 100644 --- a/tests/sentry/seer/autofix/test_autofix_utils.py +++ b/tests/sentry/seer/autofix/test_autofix_utils.py @@ -1235,13 +1235,6 @@ def test_respects_org_stopping_point_option(self): assert stopping_point == "open_pr" assert handoff is None - def test_invalid_stopping_point_falls_back_to_default(self): - self.organization.update_option("sentry:default_automated_run_stopping_point", "root_cause") - - stopping_point, handoff = get_org_default_seer_automation_handoff(self.organization) - assert stopping_point == "code_changes" - assert handoff is None - def test_seer_agent_auto_open_prs_forces_open_pr(self): self.organization.update_option( "sentry:default_automated_run_stopping_point", "code_changes" From d6bddbe116fdae7c6b936f50a8bd5d02dc81a3e5 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Tue, 31 Mar 2026 14:33:04 -0700 Subject: [PATCH 31/31] update default seer pref --- src/sentry/seer/autofix/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py index fba791d6413418..8ac05e2efa1eba 100644 --- a/src/sentry/seer/autofix/utils.py +++ b/src/sentry/seer/autofix/utils.py @@ -389,12 +389,13 @@ def validate(self, data): def default_seer_project_preference(project: Project) -> SeerProjectPreference: + stopping_point, handoff = get_org_default_seer_automation_handoff(project.organization) return SeerProjectPreference( organization_id=project.organization.id, project_id=project.id, repositories=[], - automated_run_stopping_point=AutofixStoppingPoint.CODE_CHANGES.value, - automation_handoff=None, + automated_run_stopping_point=stopping_point, + automation_handoff=handoff, )