From 135a08bda8196e5e41a2a44f426574fac3e59d48 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Tue, 24 Mar 2026 18:32:29 -0400 Subject: [PATCH 1/7] feat(autofix): Use new seer explorer autofix last triggered column This uses the new seer_explorer_autofix_last_triggered column to handle the new explorer autofix so we can handle the migration cleanly. --- src/sentry/api/serializers/models/group.py | 9 +++++++-- src/sentry/search/snuba/backend.py | 12 +++++++++--- src/sentry/seer/autofix/autofix_agent.py | 2 +- src/sentry/tasks/post_process.py | 5 +++++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/sentry/api/serializers/models/group.py b/src/sentry/api/serializers/models/group.py index 41c6be7d9fe191..3dfd38883817e5 100644 --- a/src/sentry/api/serializers/models/group.py +++ b/src/sentry/api/serializers/models/group.py @@ -13,7 +13,7 @@ from django.contrib.auth.models import AnonymousUser from django.db.models import Min, prefetch_related_objects -from sentry import tagstore +from sentry import features, tagstore from sentry.api.serializers import Serializer, register, serialize from sentry.api.serializers.models.actor import ActorSerializer, ActorSerializerResponse from sentry.api.serializers.models.plugin import is_plugin_deprecated @@ -363,6 +363,7 @@ def serialize( share_id = attrs["share_id"] priority_label = PriorityLevel(obj.priority).to_str() if obj.priority else None issue_category = obj.issue_category_v2.name.lower() + group_dict: BaseGroupSerializerResponse = { "id": str(obj.id), "shareId": share_id, @@ -392,7 +393,11 @@ def serialize( "priority": priority_label, "priorityLockedAt": obj.priority_locked_at, "seerFixabilityScore": obj.seer_fixability_score, - "seerAutofixLastTriggered": obj.seer_autofix_last_triggered, + "seerAutofixLastTriggered": ( + obj.seer_explorer_autofix_last_triggered + if features.has("organization:autofix-on-explorer", obj.organization) + else obj.seer_autofix_last_triggered + ), } # This attribute is currently feature gated diff --git a/src/sentry/search/snuba/backend.py b/src/sentry/search/snuba/backend.py index 981883e306ffc1..bcf4258c03e808 100644 --- a/src/sentry/search/snuba/backend.py +++ b/src/sentry/search/snuba/backend.py @@ -12,7 +12,7 @@ from django.utils import timezone from django.utils.functional import SimpleLazyObject -from sentry import quotas +from sentry import features, quotas from sentry.api.event_search import SearchFilter from sentry.db.models.manager.base_query_set import BaseQuerySet from sentry.exceptions import InvalidSearchQuery @@ -555,6 +555,8 @@ def _get_queryset_conditions( environments: Sequence[Environment] | None, search_filters: Sequence[SearchFilter], ) -> Mapping[str, Condition]: + organization = projects[0].organization + queryset_conditions: dict[str, Condition] = { "status": QCallbackCondition(lambda statuses: Q(status__in=statuses)), "substatus": QCallbackCondition(lambda substatuses: Q(substatus__in=substatuses)), @@ -590,7 +592,11 @@ def _get_queryset_conditions( "issue.type": QCallbackCondition(lambda types: Q(type__in=types)), "issue.priority": QCallbackCondition(lambda priorities: Q(priority__in=priorities)), "issue.seer_actionability": QCallbackCondition(seer_actionability_filter), - "issue.seer_last_run": ScalarCondition("seer_autofix_last_triggered"), + "issue.seer_last_run": ScalarCondition( + "seer_explorer_autofix_last_triggered" + if features.has("organization:autofix-on-explorer", organization) + else "seer_autofix_last_triggered" + ), "issue.id": QCallbackCondition( lambda ids: Q(id__in=[int(v) for v in (ids if isinstance(ids, list) else [ids])]) ), @@ -605,7 +611,7 @@ def _get_queryset_conditions( # if environment(s) are selected, we just filter on the group # environment's first_release attribute. id__in=GroupEnvironment.objects.filter( - first_release__organization_id=projects[0].organization_id, + first_release__organization_id=organization.id, first_release__version__in=versions, environment_id__in=environment_ids, ).values_list("group_id"), diff --git a/src/sentry/seer/autofix/autofix_agent.py b/src/sentry/seer/autofix/autofix_agent.py index 0b7c97fc7c13a9..860cc17e880c23 100644 --- a/src/sentry/seer/autofix/autofix_agent.py +++ b/src/sentry/seer/autofix/autofix_agent.py @@ -246,7 +246,7 @@ def trigger_autofix_explorer( artifact_schema=artifact_schema, ) - group.update(seer_autofix_last_triggered=timezone.now()) + group.update(seer_explorer_autofix_last_triggered=timezone.now()) payload = { "run_id": run_id, diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index 54656f5aaeb061..78799a784c1757 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1582,10 +1582,15 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: generate_issue_summary_only.delay(group.id) else: # Event count >= 10: run automation + # Long-term check to avoid re-running if group.seer_autofix_last_triggered is not None: return + if group.seer_explorer_autofix_last_triggered is not None: + if features.has("organizations:autofix-on-explorer", group.organization): + return + # Don't run automation on old issues if group.first_seen < (timezone.now() - timedelta(days=14)): return From cfa95ac2101c51af301dfd814a6878011dad673c Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Tue, 24 Mar 2026 18:49:17 -0400 Subject: [PATCH 2/7] add tests --- src/sentry/api/serializers/models/group.py | 2 +- src/sentry/search/snuba/backend.py | 2 +- tests/sentry/api/serializers/test_group.py | 23 ++++++++++ .../test_organization_group_index.py | 30 +++++++++++++ .../sentry/seer/autofix/test_autofix_agent.py | 23 ++++++++++ tests/sentry/tasks/test_post_process.py | 45 +++++++++++++++++++ 6 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/sentry/api/serializers/models/group.py b/src/sentry/api/serializers/models/group.py index 3dfd38883817e5..8d7b1cfeee5674 100644 --- a/src/sentry/api/serializers/models/group.py +++ b/src/sentry/api/serializers/models/group.py @@ -395,7 +395,7 @@ def serialize( "seerFixabilityScore": obj.seer_fixability_score, "seerAutofixLastTriggered": ( obj.seer_explorer_autofix_last_triggered - if features.has("organization:autofix-on-explorer", obj.organization) + if features.has("organizations:autofix-on-explorer", obj.organization) else obj.seer_autofix_last_triggered ), } diff --git a/src/sentry/search/snuba/backend.py b/src/sentry/search/snuba/backend.py index bcf4258c03e808..879873b717e0d8 100644 --- a/src/sentry/search/snuba/backend.py +++ b/src/sentry/search/snuba/backend.py @@ -594,7 +594,7 @@ def _get_queryset_conditions( "issue.seer_actionability": QCallbackCondition(seer_actionability_filter), "issue.seer_last_run": ScalarCondition( "seer_explorer_autofix_last_triggered" - if features.has("organization:autofix-on-explorer", organization) + if features.has("organizations:autofix-on-explorer", organization) else "seer_autofix_last_triggered" ), "issue.id": QCallbackCondition( diff --git a/tests/sentry/api/serializers/test_group.py b/tests/sentry/api/serializers/test_group.py index aba28deab45aa5..f3fb8fbf84a573 100644 --- a/tests/sentry/api/serializers/test_group.py +++ b/tests/sentry/api/serializers/test_group.py @@ -463,6 +463,29 @@ def test_perf_issue(self) -> None: assert serialized["issueCategory"] == "db_query" assert serialized["issueType"] == "performance_n_plus_one_db_queries" + def test_seer_autofix_last_triggered_without_explorer_flag(self) -> None: + user = self.create_user() + group = self.create_group() + now = timezone.now() + group.update(seer_autofix_last_triggered=now) + + result = serialize(group, user) + assert result["seerAutofixLastTriggered"] == now + + def test_seer_autofix_last_triggered_with_explorer_flag(self) -> None: + user = self.create_user() + group = self.create_group() + old_time = timezone.now() - timedelta(hours=1) + new_time = timezone.now() + group.update( + seer_autofix_last_triggered=old_time, + seer_explorer_autofix_last_triggered=new_time, + ) + + with self.feature("organizations:autofix-on-explorer"): + result = serialize(group, user) + assert result["seerAutofixLastTriggered"] == new_time + class SimpleGroupSerializerTest(TestCase): def test_simple_group_serializer(self) -> None: diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py index 3be938d45e708a..48ee75043f2bdc 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_index.py +++ b/tests/sentry/issues/endpoints/test_organization_group_index.py @@ -559,6 +559,36 @@ def test_has_seer_last_run(self) -> None: assert len(response.data) == 1 assert response.data[0]["id"] == str(group_without_seer.id) + def test_has_seer_last_run_with_explorer_flag(self) -> None: + """Test filtering issues by seer_explorer_autofix_last_triggered when feature flag is on.""" + event1 = self.store_event( + data={ + "fingerprint": ["explorer-seer-group"], + "timestamp": before_now(seconds=1).isoformat(), + }, + project_id=self.project.id, + ) + group_with_explorer = event1.group + group_with_explorer.update(seer_explorer_autofix_last_triggered=timezone.now()) + event2 = self.store_event( + data={ + "fingerprint": ["no-explorer-seer-group"], + "timestamp": before_now(seconds=1).isoformat(), + }, + project_id=self.project.id, + ) + group_without_explorer = event2.group + + self.login_as(user=self.user) + with self.feature("organizations:autofix-on-explorer"): + response = self.get_success_response(query="has:issue.seer_last_run") + assert len(response.data) == 1 + assert response.data[0]["id"] == str(group_with_explorer.id) + + response = self.get_success_response(query="!has:issue.seer_last_run") + assert len(response.data) == 1 + assert response.data[0]["id"] == str(group_without_explorer.id) + def test_lookup_by_event_id(self) -> None: project = self.project project.update_option("sentry:resolve_age", 1) diff --git a/tests/sentry/seer/autofix/test_autofix_agent.py b/tests/sentry/seer/autofix/test_autofix_agent.py index 0ee1e6557878f3..e1a58ef6041954 100644 --- a/tests/sentry/seer/autofix/test_autofix_agent.py +++ b/tests/sentry/seer/autofix/test_autofix_agent.py @@ -354,6 +354,29 @@ def test_trigger_autofix_explorer_passes_group_id_in_metadata( call_kwargs = mock_client.start_run.call_args.kwargs assert call_kwargs["metadata"] == {"group_id": self.group.id, "referrer": "unknown"} + @patch("sentry.seer.autofix.autofix_agent.broadcast_webhooks_for_organization.delay") + @patch("sentry.seer.autofix.autofix_agent.SeerExplorerClient") + def test_trigger_autofix_explorer_updates_explorer_last_triggered( + self, mock_client_class, mock_broadcast + ): + """trigger_autofix_explorer sets seer_explorer_autofix_last_triggered on the group.""" + mock_client = MagicMock() + mock_client_class.return_value = mock_client + mock_client.start_run.return_value = 123 + + assert self.group.seer_explorer_autofix_last_triggered is None + + trigger_autofix_explorer( + group=self.group, + step=AutofixStep.ROOT_CAUSE, + referrer=AutofixReferrer.UNKNOWN, + run_id=None, + ) + + self.group.refresh_from_db() + assert self.group.seer_explorer_autofix_last_triggered is not None + assert self.group.seer_autofix_last_triggered is None + class TestTriggerCodingAgentHandoff(TestCase): """Tests for trigger_coding_agent_handoff function.""" diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 686d92f62f0c0e..d0be0afdbbf5f5 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -3306,6 +3306,51 @@ def mock_buffer_get(model, columns, filters): # Should not call automation since seer_autofix_last_triggered is set mock_run_automation.assert_not_called() + @patch("sentry.tasks.seer.autofix.run_automation_only_task.delay") + @with_feature( + {"organizations:gen-ai-features": True, "organizations:autofix-on-explorer": True} + ) + def test_triage_signals_event_count_gte_10_skips_with_explorer_last_triggered( + self, mock_run_automation, mock_seat_based_tier + ): + """Test that with event count >= 10 and seer_explorer_autofix_last_triggered set + feature flag, we skip automation.""" + self.project.update_option("sentry:seer_scanner_automation", True) + event = self.create_event( + data={"message": "testing"}, + project_id=self.project.id, + ) + + # Update group times_seen and seer_explorer_autofix_last_triggered + group = event.group + group.times_seen = 1 + group.seer_explorer_autofix_last_triggered = timezone.now() + group.save() + event.group.times_seen = 1 + event.group.seer_explorer_autofix_last_triggered = ( + group.seer_explorer_autofix_last_triggered + ) + + from sentry import buffer + + def mock_buffer_get(model, columns, filters): + return {"times_seen": 9} + + with patch.object(buffer.backend, "get", side_effect=mock_buffer_get): + from sentry.seer.autofix.issue_summary import get_issue_summary_cache_key + + cache_key = get_issue_summary_cache_key(group.id) + cache.set(cache_key, {"summary": "test summary"}, 3600) + + self.call_post_process_group( + is_new=False, + is_regression=False, + is_new_group_environment=False, + event=event, + ) + + # Should not call automation since seer_explorer_autofix_last_triggered is set with flag + mock_run_automation.assert_not_called() + @patch("sentry.tasks.seer.autofix.run_automation_only_task.delay") @with_feature({"organizations:gen-ai-features": True}) def test_triage_signals_event_count_gte_10_skips_with_existing_fixability_score( From e4b889a6f11a703bf2dca7f6ba31ba415ea277ed Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Tue, 24 Mar 2026 18:53:17 -0400 Subject: [PATCH 3/7] add tests --- tests/sentry/seer/autofix/test_autofix_agent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/sentry/seer/autofix/test_autofix_agent.py b/tests/sentry/seer/autofix/test_autofix_agent.py index e1a58ef6041954..4261d4679352bf 100644 --- a/tests/sentry/seer/autofix/test_autofix_agent.py +++ b/tests/sentry/seer/autofix/test_autofix_agent.py @@ -374,6 +374,7 @@ def test_trigger_autofix_explorer_updates_explorer_last_triggered( ) self.group.refresh_from_db() + assert self.group.seer_explorer_autofix_last_triggered is not None assert self.group.seer_autofix_last_triggered is None From 51bead959ba63bb34f8b31649e417c9bbc6a2341 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 25 Mar 2026 11:03:41 -0400 Subject: [PATCH 4/7] fix typing --- src/sentry/api/serializers/models/group.py | 10 ++++------ tests/sentry/api/serializers/test_group.py | 8 +++++--- tests/sentry/seer/autofix/test_autofix_agent.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sentry/api/serializers/models/group.py b/src/sentry/api/serializers/models/group.py index 8d7b1cfeee5674..d1aa310ad4e17a 100644 --- a/src/sentry/api/serializers/models/group.py +++ b/src/sentry/api/serializers/models/group.py @@ -13,7 +13,7 @@ from django.contrib.auth.models import AnonymousUser from django.db.models import Min, prefetch_related_objects -from sentry import features, tagstore +from sentry import tagstore from sentry.api.serializers import Serializer, register, serialize from sentry.api.serializers.models.actor import ActorSerializer, ActorSerializerResponse from sentry.api.serializers.models.plugin import is_plugin_deprecated @@ -127,6 +127,7 @@ class BaseGroupSerializerResponse(BaseGroupResponseOptional): priorityLockedAt: datetime | None seerFixabilityScore: float | None seerAutofixLastTriggered: datetime | None + seerExplorerAutofixLastTriggered: datetime | None project: GroupProjectResponse type: str issueType: str @@ -393,11 +394,8 @@ def serialize( "priority": priority_label, "priorityLockedAt": obj.priority_locked_at, "seerFixabilityScore": obj.seer_fixability_score, - "seerAutofixLastTriggered": ( - obj.seer_explorer_autofix_last_triggered - if features.has("organizations:autofix-on-explorer", obj.organization) - else obj.seer_autofix_last_triggered - ), + "seerAutofixLastTriggered": obj.seer_autofix_last_triggered, + "seerExplorerAutofixLastTriggered": obj.seer_explorer_autofix_last_triggered, } # This attribute is currently feature gated diff --git a/tests/sentry/api/serializers/test_group.py b/tests/sentry/api/serializers/test_group.py index f3fb8fbf84a573..b97298656d764a 100644 --- a/tests/sentry/api/serializers/test_group.py +++ b/tests/sentry/api/serializers/test_group.py @@ -463,7 +463,7 @@ def test_perf_issue(self) -> None: assert serialized["issueCategory"] == "db_query" assert serialized["issueType"] == "performance_n_plus_one_db_queries" - def test_seer_autofix_last_triggered_without_explorer_flag(self) -> None: + def test_seer_autofix_last_triggered_without_explorer(self) -> None: user = self.create_user() group = self.create_group() now = timezone.now() @@ -471,8 +471,9 @@ def test_seer_autofix_last_triggered_without_explorer_flag(self) -> None: result = serialize(group, user) assert result["seerAutofixLastTriggered"] == now + assert result["seerExplorerAutofixLastTriggered"] is None - def test_seer_autofix_last_triggered_with_explorer_flag(self) -> None: + def test_seer_autofix_last_triggered_with_explorer(self) -> None: user = self.create_user() group = self.create_group() old_time = timezone.now() - timedelta(hours=1) @@ -484,7 +485,8 @@ def test_seer_autofix_last_triggered_with_explorer_flag(self) -> None: with self.feature("organizations:autofix-on-explorer"): result = serialize(group, user) - assert result["seerAutofixLastTriggered"] == new_time + assert result["seerAutofixLastTriggered"] == old_time + assert result["seerExplorerAutofixLastTriggered"] == new_time class SimpleGroupSerializerTest(TestCase): diff --git a/tests/sentry/seer/autofix/test_autofix_agent.py b/tests/sentry/seer/autofix/test_autofix_agent.py index 4261d4679352bf..5a2543f84d63b3 100644 --- a/tests/sentry/seer/autofix/test_autofix_agent.py +++ b/tests/sentry/seer/autofix/test_autofix_agent.py @@ -375,8 +375,8 @@ def test_trigger_autofix_explorer_updates_explorer_last_triggered( self.group.refresh_from_db() - assert self.group.seer_explorer_autofix_last_triggered is not None assert self.group.seer_autofix_last_triggered is None + assert self.group.seer_explorer_autofix_last_triggered is not None class TestTriggerCodingAgentHandoff(TestCase): From 1890d343543a97975c49f65b91f2f61f5cc0eedd Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 25 Mar 2026 11:25:37 -0400 Subject: [PATCH 5/7] fix tests --- tests/sentry/api/serializers/test_group.py | 3 +- .../test_organization_group_index.py | 59 +++++++++---------- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/tests/sentry/api/serializers/test_group.py b/tests/sentry/api/serializers/test_group.py index b97298656d764a..25751ab1aef239 100644 --- a/tests/sentry/api/serializers/test_group.py +++ b/tests/sentry/api/serializers/test_group.py @@ -483,8 +483,7 @@ def test_seer_autofix_last_triggered_with_explorer(self) -> None: seer_explorer_autofix_last_triggered=new_time, ) - with self.feature("organizations:autofix-on-explorer"): - result = serialize(group, user) + result = serialize(group, user) assert result["seerAutofixLastTriggered"] == old_time assert result["seerExplorerAutofixLastTriggered"] == new_time diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py index 48ee75043f2bdc..bfa9530d55bc32 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_index.py +++ b/tests/sentry/issues/endpoints/test_organization_group_index.py @@ -532,62 +532,57 @@ def test_has_seer_last_run(self) -> None: """Test filtering issues by whether they have seer_autofix_last_triggered set.""" event1 = self.store_event( data={ - "fingerprint": ["seer-group"], + "fingerprint": ["no-seer-group"], "timestamp": before_now(seconds=1).isoformat(), }, project_id=self.project.id, ) - group_with_seer = event1.group - group_with_seer.update(seer_autofix_last_triggered=timezone.now()) + group_without_seer = event1.group + event2 = self.store_event( data={ - "fingerprint": ["no-seer-group"], + "fingerprint": ["legacy-seer-group"], "timestamp": before_now(seconds=1).isoformat(), }, project_id=self.project.id, ) - group_without_seer = event2.group - - self.login_as(user=self.user) - # Query for issues that have seer_autofix_last_triggered set - response = self.get_success_response(query="has:issue.seer_last_run") - assert len(response.data) == 1 - assert response.data[0]["id"] == str(group_with_seer.id) + group_with_legacy_seer = event2.group + group_with_legacy_seer.update(seer_autofix_last_triggered=timezone.now()) - # Query for issues that do NOT have seer_autofix_last_triggered set - response = self.get_success_response(query="!has:issue.seer_last_run") - assert len(response.data) == 1 - assert response.data[0]["id"] == str(group_without_seer.id) - - def test_has_seer_last_run_with_explorer_flag(self) -> None: - """Test filtering issues by seer_explorer_autofix_last_triggered when feature flag is on.""" - event1 = self.store_event( + event3 = self.store_event( data={ "fingerprint": ["explorer-seer-group"], "timestamp": before_now(seconds=1).isoformat(), }, project_id=self.project.id, ) - group_with_explorer = event1.group - group_with_explorer.update(seer_explorer_autofix_last_triggered=timezone.now()) - event2 = self.store_event( - data={ - "fingerprint": ["no-explorer-seer-group"], - "timestamp": before_now(seconds=1).isoformat(), - }, - project_id=self.project.id, - ) - group_without_explorer = event2.group + group_with_explorer_seer = event3.group + group_with_explorer_seer.update(seer_explorer_autofix_last_triggered=timezone.now()) self.login_as(user=self.user) + + # Query for issues that have seer_autofix_last_triggered set + response = self.get_success_response(query="has:issue.seer_last_run") + assert len(response.data) == 1 + assert response.data[0]["id"] == str(group_with_legacy_seer.id) + + # Query for issues that do NOT have seer_autofix_last_triggered set + response = self.get_success_response(query="!has:issue.seer_last_run") + assert len(response.data) == 2 + assert response.data[0]["id"] == str(group_with_explorer_seer.id) + assert response.data[1]["id"] == str(group_without_seer.id) + + # Query for issues that have seer_explorer_autofix_last_triggered set with self.feature("organizations:autofix-on-explorer"): response = self.get_success_response(query="has:issue.seer_last_run") assert len(response.data) == 1 - assert response.data[0]["id"] == str(group_with_explorer.id) + assert response.data[0]["id"] == str(group_with_explorer_seer.id) + # Query for issues that do NOT have seer_explorer_autofix_last_triggered set response = self.get_success_response(query="!has:issue.seer_last_run") - assert len(response.data) == 1 - assert response.data[0]["id"] == str(group_without_explorer.id) + assert len(response.data) == 2 + assert response.data[0]["id"] == str(group_with_legacy_seer.id) + assert response.data[1]["id"] == str(group_without_seer.id) def test_lookup_by_event_id(self) -> None: project = self.project From 2629296512e88b3564fc0e121eef54614a8c54f6 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 25 Mar 2026 11:37:52 -0400 Subject: [PATCH 6/7] fix tests --- src/sentry/api/serializers/models/group_stream.py | 1 + src/sentry/apidocs/examples/issue_examples.py | 1 + src/sentry/issues/endpoints/organization_shortid.py | 1 + 3 files changed, 3 insertions(+) diff --git a/src/sentry/api/serializers/models/group_stream.py b/src/sentry/api/serializers/models/group_stream.py index 3cb01b82d2ea84..7600fe8536cad2 100644 --- a/src/sentry/api/serializers/models/group_stream.py +++ b/src/sentry/api/serializers/models/group_stream.py @@ -297,6 +297,7 @@ class StreamGroupSerializerSnubaResponse(TypedDict): priorityLockedAt: NotRequired[datetime | None] seerFixabilityScore: NotRequired[float | None] seerAutofixLastTriggered: NotRequired[datetime | None] + seerExplorerAutofixLastTriggered: NotRequired[datetime | None] project: NotRequired[GroupProjectResponse] type: NotRequired[str] issueType: NotRequired[str] diff --git a/src/sentry/apidocs/examples/issue_examples.py b/src/sentry/apidocs/examples/issue_examples.py index 47e8e8a256868c..a7d9b09f887e73 100644 --- a/src/sentry/apidocs/examples/issue_examples.py +++ b/src/sentry/apidocs/examples/issue_examples.py @@ -74,6 +74,7 @@ "lastSeen": datetime.fromisoformat("2018-12-06T21:19:55Z"), }, "seerAutofixLastTriggered": None, + "seerExplorerAutofixLastTriggered": None, "seerFixabilityScore": None, "status": "ignored", "substatus": "archived_until_condition_met", diff --git a/src/sentry/issues/endpoints/organization_shortid.py b/src/sentry/issues/endpoints/organization_shortid.py index fde7f77614a2dd..bac93f2a1fd3ee 100644 --- a/src/sentry/issues/endpoints/organization_shortid.py +++ b/src/sentry/issues/endpoints/organization_shortid.py @@ -106,6 +106,7 @@ class ShortIdLookupEndpoint(GroupEndpoint): "priorityLockedAt": None, "seerFixabilityScore": 0.5, "seerAutofixLastTriggered": None, + "seerExplorerAutofixLastTriggered": None, "substatus": "ongoing", }, "groupId": "1", From c72c5b0e741f04afff232f9e46a0d739c73f192a Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Wed, 25 Mar 2026 16:31:54 -0400 Subject: [PATCH 7/7] dont use the feature flag --- src/sentry/tasks/post_process.py | 3 +-- src/sentry/tasks/seer/autofix.py | 1 + tests/sentry/tasks/test_post_process.py | 25 +++++++++++-------------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index 46a547a1ae34e6..107eae47a09f10 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1590,8 +1590,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: return if group.seer_explorer_autofix_last_triggered is not None: - if features.has("organizations:autofix-on-explorer", group.organization): - return + return # Don't run automation on old issues if group.first_seen < (timezone.now() - timedelta(days=14)): diff --git a/src/sentry/tasks/seer/autofix.py b/src/sentry/tasks/seer/autofix.py index 8b6c833d2e89ea..8671d98bee3281 100644 --- a/src/sentry/tasks/seer/autofix.py +++ b/src/sentry/tasks/seer/autofix.py @@ -183,6 +183,7 @@ def run_automation_only_task(group_id: int) -> None: # Track issue age when running automation issue_age_days = int((timezone.now() - group.first_seen).total_seconds() / (60 * 60 * 24)) + metrics.distribution( "seer.automation.issue_age_since_first_seen", issue_age_days, unit="day", sample_rate=1.0 ) diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index d0be0afdbbf5f5..c1aec43072b791 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -50,6 +50,11 @@ from sentry.models.userreport import UserReport from sentry.replays.lib import kafka as replays_kafka from sentry.replays.lib.kafka import clear_replay_publisher +from sentry.seer.autofix.constants import ( + AUTOFIX_AUTOMATION_OCCURRENCE_THRESHOLD, + FixabilityScoreThresholds, +) +from sentry.seer.autofix.issue_summary import get_issue_summary_cache_key from sentry.services.eventstore.models import Event from sentry.services.eventstore.processing import event_processing_store from sentry.silo.base import SiloMode @@ -3307,10 +3312,8 @@ def mock_buffer_get(model, columns, filters): mock_run_automation.assert_not_called() @patch("sentry.tasks.seer.autofix.run_automation_only_task.delay") - @with_feature( - {"organizations:gen-ai-features": True, "organizations:autofix-on-explorer": True} - ) - def test_triage_signals_event_count_gte_10_skips_with_explorer_last_triggered( + @with_feature({"organizations:gen-ai-features": True}) + def test_triage_signals_skips_with_explorer_last_triggered( self, mock_run_automation, mock_seat_based_tier ): """Test that with event count >= 10 and seer_explorer_autofix_last_triggered set + feature flag, we skip automation.""" @@ -3322,24 +3325,18 @@ def test_triage_signals_event_count_gte_10_skips_with_explorer_last_triggered( # Update group times_seen and seer_explorer_autofix_last_triggered group = event.group - group.times_seen = 1 + group.times_seen = AUTOFIX_AUTOMATION_OCCURRENCE_THRESHOLD group.seer_explorer_autofix_last_triggered = timezone.now() + group.seer_fixability_score = FixabilityScoreThresholds.MEDIUM.value group.save() - event.group.times_seen = 1 - event.group.seer_explorer_autofix_last_triggered = ( - group.seer_explorer_autofix_last_triggered - ) - - from sentry import buffer def mock_buffer_get(model, columns, filters): return {"times_seen": 9} with patch.object(buffer.backend, "get", side_effect=mock_buffer_get): - from sentry.seer.autofix.issue_summary import get_issue_summary_cache_key - cache_key = get_issue_summary_cache_key(group.id) cache.set(cache_key, {"summary": "test summary"}, 3600) + cache.set(f"seer-project-has-repos:{self.organization.id}:{self.project.id}", True) self.call_post_process_group( is_new=False, @@ -3348,7 +3345,7 @@ def mock_buffer_get(model, columns, filters): event=event, ) - # Should not call automation since seer_explorer_autofix_last_triggered is set with flag + # Should not call automation since seer_explorer_autofix_last_triggered is set mock_run_automation.assert_not_called() @patch("sentry.tasks.seer.autofix.run_automation_only_task.delay")