From 0a48ce3fad679c447e79d4ed288d37e2d2b38592 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Fri, 24 Jul 2026 19:02:29 -0400 Subject: [PATCH] fix(autofix): Gate automated runs on Seer access Prevent automatic Autofix runs when the completion handler lacks Seer access, and record skipped completion processing. Refs CW-1721 --- src/sentry/seer/autofix/issue_summary.py | 4 +++ src/sentry/seer/autofix/on_completion_hook.py | 13 +++++++ .../test_autofix_on_completion_hook.py | 34 +++++++++++++++++++ .../sentry/seer/autofix/test_issue_summary.py | 7 ++++ 4 files changed, 58 insertions(+) diff --git a/src/sentry/seer/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index 9a0cf474d6ed..6aa5e151f973 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -42,6 +42,7 @@ from sentry.seer.entrypoints.operator import SeerAutofixOperator from sentry.seer.models import SummarizeIssueResponse from sentry.seer.models.run import SeerRun +from sentry.seer.seer_setup import has_seer_access from sentry.seer.signed_seer_api import ( SeerViewerContext, SummarizeIssueRequest, @@ -425,6 +426,9 @@ def is_group_triggering_automation(group: Group) -> bool: Checks if a group is going to be picked up for automation. Does not check for existing run. Checks project options (fixability tuning, preferences), billing quota, and rate limiting. """ + if not has_seer_access(group.organization): + return False + fixability_score = get_and_update_group_fixability_score(group) if ( diff --git a/src/sentry/seer/autofix/on_completion_hook.py b/src/sentry/seer/autofix/on_completion_hook.py index a7d8842908ad..4479b88caae0 100644 --- a/src/sentry/seer/autofix/on_completion_hook.py +++ b/src/sentry/seer/autofix/on_completion_hook.py @@ -475,6 +475,19 @@ def _send_step_webhook( "organization_id": organization.id, } ) + else: + metrics.incr( + "autofix.on_completion_hook.process_autofix_updates.skipped", + tags={"reason": "no_operator_access", "event_type": str(event_type)}, + ) + logger.info( + "autofix.on_completion_hook.process_autofix_updates.skipped", + extra={ + "organization_id": organization.id, + "event_type": str(event_type), + "reason": "no_operator_access", + }, + ) except ValueError: logger.exception( "autofix.on_completion_hook.webhook_invalid_event_type", diff --git a/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py b/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py index 3c6de55e07aa..d4b7bc4657de 100644 --- a/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py +++ b/tests/sentry/seer/autofix/test_autofix_on_completion_hook.py @@ -552,6 +552,40 @@ def test_send_step_webhook_pr_iteration( == AutofixReferrer.GROUP_AUTOFIX_ENDPOINT.value ) + @patch("sentry.seer.autofix.on_completion_hook.logger.info") + @patch("sentry.seer.autofix.on_completion_hook.metrics.incr") + @patch("sentry.seer.autofix.on_completion_hook.process_autofix_updates.apply_async") + @patch( + "sentry.seer.autofix.on_completion_hook.SeerAutofixOperator.has_access", return_value=False + ) + @patch("sentry.seer.autofix.on_completion_hook.broadcast_webhooks_for_organization.delay") + def test_records_skipped_processing_without_operator_access( + self, mock_broadcast, mock_has_access, mock_process_autofix_updates, mock_incr, mock_info + ): + run_id = 123 + self.create_seer_run(organization=self.organization, seer_run_state_id=run_id) + + AutofixOnCompletionHook._send_step_webhook( + self.organization, + run_id, + run_state(blocks=[root_cause_memory_block()]), + self.group, + ) + + mock_process_autofix_updates.assert_not_called() + mock_incr.assert_any_call( + "autofix.on_completion_hook.process_autofix_updates.skipped", + tags={"reason": "no_operator_access", "event_type": "seer.root_cause_completed"}, + ) + mock_info.assert_called_once_with( + "autofix.on_completion_hook.process_autofix_updates.skipped", + extra={ + "organization_id": self.organization.id, + "event_type": "seer.root_cause_completed", + "reason": "no_operator_access", + }, + ) + @patch("sentry.seer.autofix.on_completion_hook.analytics.record") @patch("sentry.seer.autofix.on_completion_hook.broadcast_webhooks_for_organization.delay") def test_send_step_webhook_pr_iteration_does_not_emit_pr_created( diff --git a/tests/sentry/seer/autofix/test_issue_summary.py b/tests/sentry/seer/autofix/test_issue_summary.py index 8581266cff8f..eed4ae74f3db 100644 --- a/tests/sentry/seer/autofix/test_issue_summary.py +++ b/tests/sentry/seer/autofix/test_issue_summary.py @@ -1244,6 +1244,13 @@ def test_returns_true_when_all_checks_pass(self, mock_fixability, mock_quota, mo assert is_group_triggering_automation(self.group) is True + @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") + def test_returns_false_without_seer_access(self, mock_fixability): + with self.feature({"organizations:gen-ai-features": False}): + assert is_group_triggering_automation(self.group) is False + + mock_fixability.assert_not_called() + @patch("sentry.seer.autofix.issue_summary.get_and_update_group_fixability_score") def test_returns_false_when_not_fixable(self, mock_fixability): mock_fixability.return_value = 0.20