From 7b309e401d8e61d371cfb6e6143faf822b5fe991 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Tue, 7 Apr 2026 16:52:57 -0700 Subject: [PATCH 1/3] feat(seer): Add rca_source to supergroup queries and use feature flag Add RCASource enum and rca_source field to supergroup query requests so Seer knows which embedding space to query. The source is determined by the organizations:supergroups-lightweight-rca-clustering feature flag. Replace the supergroups.lightweight-enabled-orgs sentry-option with the feature flag for both the write path (post_process task dispatch) and read path (supergroup query endpoints), consistent with how all other supergroup features are gated. --- src/sentry/features/temporary.py | 2 + src/sentry/options/defaults.py | 8 --- src/sentry/seer/signed_seer_api.py | 8 +++ .../organization_supergroup_details.py | 14 +++- .../organization_supergroups_by_group.py | 9 ++- src/sentry/tasks/post_process.py | 3 +- .../test_organization_supergroup_details.py | 67 +++++++++++++++++++ .../test_organization_supergroups_by_group.py | 35 ++++++++++ tests/sentry/tasks/test_post_process.py | 17 +++-- 9 files changed, 141 insertions(+), 22 deletions(-) create mode 100644 tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index f48a566b091fee..c589432368cc03 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -571,6 +571,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("projects:supergroup-embeddings-explorer", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable lightweight Explorer RCA runs for supergroup quality evaluation manager.add("projects:supergroup-lightweight-rca", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + # Use lightweight RCA source for supergroup clustering and queries + manager.add("organizations:supergroups-lightweight-rca-clustering", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) manager.add("projects:workflow-engine-performance-detectors", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 8725c6ad5c15d0..c58843c709136d 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -1385,14 +1385,6 @@ flags=FLAG_AUTOMATOR_MODIFIABLE, ) -# Supergroups / Lightweight RCA -register( - "supergroups.lightweight-enabled-orgs", - type=Sequence, - default=[], - flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE, -) - # ## sentry.killswitches # # The following options are documented in sentry.killswitches in more detail diff --git a/src/sentry/seer/signed_seer_api.py b/src/sentry/seer/signed_seer_api.py index 33752a24de8b8b..83f8e4f4489271 100644 --- a/src/sentry/seer/signed_seer_api.py +++ b/src/sentry/seer/signed_seer_api.py @@ -1,6 +1,7 @@ import hashlib import hmac import logging +from enum import StrEnum from typing import Any, NotRequired, TypedDict from urllib.parse import urlparse @@ -369,6 +370,11 @@ class SummarizeIssueRequest(TypedDict): experiment_variant: NotRequired[str | None] +class RCASource(StrEnum): + EXPLORER = "explorer" + LIGHTWEIGHT = "lightweight" + + class SupergroupsEmbeddingRequest(TypedDict): organization_id: int group_id: int @@ -387,11 +393,13 @@ class LightweightRCAClusterRequest(TypedDict): class SupergroupsGetRequest(TypedDict): organization_id: int supergroup_id: int + rca_source: str class SupergroupsGetByGroupIdsRequest(TypedDict): organization_id: int group_ids: list[int] + rca_source: str class SupergroupDetailData(TypedDict): diff --git a/src/sentry/seer/supergroups/endpoints/organization_supergroup_details.py b/src/sentry/seer/supergroups/endpoints/organization_supergroup_details.py index ab03c3fe13099a..e809056257abab 100644 --- a/src/sentry/seer/supergroups/endpoints/organization_supergroup_details.py +++ b/src/sentry/seer/supergroups/endpoints/organization_supergroup_details.py @@ -12,7 +12,7 @@ from sentry.api.base import cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.models.organization import Organization -from sentry.seer.signed_seer_api import SeerViewerContext, make_supergroups_get_request +from sentry.seer.signed_seer_api import RCASource, SeerViewerContext, make_supergroups_get_request logger = logging.getLogger(__name__) @@ -35,8 +35,18 @@ def get(self, request: Request, organization: Organization, supergroup_id: int) if not features.has("organizations:top-issues-ui", organization, actor=request.user): return Response({"detail": "Feature not available"}, status=403) + rca_source = ( + RCASource.LIGHTWEIGHT + if features.has("organizations:supergroups-lightweight-rca-clustering", organization) + else RCASource.EXPLORER + ) + response = make_supergroups_get_request( - {"organization_id": organization.id, "supergroup_id": supergroup_id}, + { + "organization_id": organization.id, + "supergroup_id": supergroup_id, + "rca_source": rca_source, + }, SeerViewerContext(organization_id=organization.id, user_id=request.user.id), timeout=10, ) diff --git a/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py b/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py index fc2c50de7a59be..6505be0774c0b4 100644 --- a/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py +++ b/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py @@ -15,6 +15,7 @@ from sentry.models.group import STATUS_QUERY_CHOICES, Group from sentry.models.organization import Organization from sentry.seer.signed_seer_api import ( + RCASource, SeerViewerContext, SupergroupsByGroupIdsResponse, make_supergroups_get_by_group_ids_request, @@ -77,8 +78,14 @@ def get(self, request: Request, organization: Organization) -> Response: status=status_codes.HTTP_404_NOT_FOUND, ) + rca_source = ( + RCASource.LIGHTWEIGHT + if features.has("organizations:supergroups-lightweight-rca-clustering", organization) + else RCASource.EXPLORER + ) + response = make_supergroups_get_by_group_ids_request( - {"organization_id": organization.id, "group_ids": group_ids}, + {"organization_id": organization.id, "group_ids": group_ids, "rca_source": rca_source}, SeerViewerContext(organization_id=organization.id, user_id=request.user.id), timeout=10, ) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index a7d5ac582ad224..4650dccb7f9062 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1593,8 +1593,7 @@ def kick_off_lightweight_rca_cluster(job: PostProcessJob) -> None: event = job["event"] group = event.group - enabled_orgs: list[int] = options.get("supergroups.lightweight-enabled-orgs") - if group.organization.id not in enabled_orgs: + if not features.has("organizations:supergroups-lightweight-rca-clustering", group.organization): return trigger_lightweight_rca_cluster_task.delay(group.id) diff --git a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py new file mode 100644 index 00000000000000..e94f10f83c3852 --- /dev/null +++ b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock, patch + +import orjson + +from sentry.testutils.cases import APITestCase + + +def mock_seer_response(data: dict[str, Any]) -> MagicMock: + response = MagicMock() + response.status = 200 + response.data = orjson.dumps(data) + return response + + +class OrganizationSupergroupDetailsEndpointTest(APITestCase): + endpoint = "sentry-api-0-organization-supergroup-details" + + def setUp(self): + super().setUp() + self.login_as(self.user) + + @patch( + "sentry.seer.supergroups.endpoints.organization_supergroup_details.make_supergroups_get_request" + ) + def test_get_supergroup_details(self, mock_seer): + mock_seer.return_value = mock_seer_response( + {"id": 1, "title": "NullPointerException in auth", "group_ids": [10, 20]} + ) + + with self.feature("organizations:top-issues-ui"): + response = self.get_success_response(self.organization.slug, "1") + + assert response.data["id"] == 1 + assert response.data["title"] == "NullPointerException in auth" + assert response.data["group_ids"] == [10, 20] + + @patch( + "sentry.seer.supergroups.endpoints.organization_supergroup_details.make_supergroups_get_request" + ) + def test_rca_source_defaults_to_explorer(self, mock_seer): + mock_seer.return_value = mock_seer_response({"id": 1, "title": "test"}) + + with self.feature("organizations:top-issues-ui"): + self.get_success_response(self.organization.slug, "1") + + body = mock_seer.call_args.args[0] + assert body["rca_source"] == "explorer" + + @patch( + "sentry.seer.supergroups.endpoints.organization_supergroup_details.make_supergroups_get_request" + ) + def test_rca_source_lightweight_when_flag_enabled(self, mock_seer): + mock_seer.return_value = mock_seer_response({"id": 1, "title": "test"}) + + with self.feature( + { + "organizations:top-issues-ui": True, + "organizations:supergroups-lightweight-rca-clustering": True, + } + ): + self.get_success_response(self.organization.slug, "1") + + body = mock_seer.call_args.args[0] + assert body["rca_source"] == "lightweight" diff --git a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py index 4419d824602847..d6ce658e370303 100644 --- a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py +++ b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py @@ -74,3 +74,38 @@ def test_status_filter_invalid(self): status="bogus", status_code=400, ) + + @patch( + "sentry.seer.supergroups.endpoints.organization_supergroups_by_group.make_supergroups_get_by_group_ids_request" + ) + def test_rca_source_defaults_to_explorer(self, mock_seer): + mock_seer.return_value = mock_seer_response({"data": []}) + + with self.feature("organizations:top-issues-ui"): + self.get_success_response( + self.organization.slug, + group_id=[self.unresolved_group.id], + ) + + body = mock_seer.call_args.args[0] + assert body["rca_source"] == "explorer" + + @patch( + "sentry.seer.supergroups.endpoints.organization_supergroups_by_group.make_supergroups_get_by_group_ids_request" + ) + def test_rca_source_lightweight_when_flag_enabled(self, mock_seer): + mock_seer.return_value = mock_seer_response({"data": []}) + + with self.feature( + { + "organizations:top-issues-ui": True, + "organizations:supergroups-lightweight-rca-clustering": True, + } + ): + self.get_success_response( + self.organization.slug, + group_id=[self.unresolved_group.id], + ) + + body = mock_seer.call_args.args[0] + assert body["rca_source"] == "lightweight" diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 9ee29b5d7fc7b1..0f0555be84c4dd 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -3078,7 +3078,7 @@ def test_kick_off_lightweight_rca_cluster_when_enabled(self, mock_task): project_id=self.project.id, ) - with self.options({"supergroups.lightweight-enabled-orgs": [self.project.organization.id]}): + with self.feature("organizations:supergroups-lightweight-rca-clustering"): self.call_post_process_group( is_new=True, is_regression=False, @@ -3095,13 +3095,12 @@ def test_kick_off_lightweight_rca_cluster_skips_when_not_enabled(self, mock_task project_id=self.project.id, ) - with self.options({"supergroups.lightweight-enabled-orgs": []}): - self.call_post_process_group( - is_new=True, - is_regression=False, - is_new_group_environment=True, - event=event, - ) + self.call_post_process_group( + is_new=True, + is_regression=False, + is_new_group_environment=True, + event=event, + ) mock_task.assert_not_called() @@ -3112,7 +3111,7 @@ def test_kick_off_lightweight_rca_cluster_skips_when_not_new(self, mock_task): project_id=self.project.id, ) - with self.options({"supergroups.lightweight-enabled-orgs": [self.project.organization.id]}): + with self.feature("organizations:supergroups-lightweight-rca-clustering"): self.call_post_process_group( is_new=False, is_regression=False, From b0ab7ad8e914b4f5e2633efe0c619214da473b79 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Wed, 8 Apr 2026 12:14:45 -0700 Subject: [PATCH 2/3] ref(seer): Split lightweight RCA clustering into separate read/write flags - Write: organizations:supergroups-lightweight-rca-clustering-write (controls post_process task dispatch for generating embeddings) - Read: organizations:supergroups-lightweight-rca-clustering-read (controls which rca_source supergroup query endpoints use) Allows enabling write ahead of read to accumulate embeddings before switching what users see. --- src/sentry/features/temporary.py | 6 ++++-- .../endpoints/organization_supergroup_details.py | 4 +++- .../endpoints/organization_supergroups_by_group.py | 4 +++- src/sentry/tasks/post_process.py | 4 +++- .../endpoints/test_organization_supergroup_details.py | 2 +- .../endpoints/test_organization_supergroups_by_group.py | 2 +- tests/sentry/tasks/test_post_process.py | 4 ++-- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index c589432368cc03..5667065e8255c6 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -571,8 +571,10 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("projects:supergroup-embeddings-explorer", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable lightweight Explorer RCA runs for supergroup quality evaluation manager.add("projects:supergroup-lightweight-rca", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) - # Use lightweight RCA source for supergroup clustering and queries - manager.add("organizations:supergroups-lightweight-rca-clustering", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + # Enable lightweight RCA clustering write path (generate embeddings on new issues) + manager.add("organizations:supergroups-lightweight-rca-clustering-write", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + # Enable lightweight RCA clustering read path (query lightweight embeddings in supergroup APIs) + manager.add("organizations:supergroups-lightweight-rca-clustering-read", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) manager.add("projects:workflow-engine-performance-detectors", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) diff --git a/src/sentry/seer/supergroups/endpoints/organization_supergroup_details.py b/src/sentry/seer/supergroups/endpoints/organization_supergroup_details.py index e809056257abab..ce4746ffd032f8 100644 --- a/src/sentry/seer/supergroups/endpoints/organization_supergroup_details.py +++ b/src/sentry/seer/supergroups/endpoints/organization_supergroup_details.py @@ -37,7 +37,9 @@ def get(self, request: Request, organization: Organization, supergroup_id: int) rca_source = ( RCASource.LIGHTWEIGHT - if features.has("organizations:supergroups-lightweight-rca-clustering", organization) + if features.has( + "organizations:supergroups-lightweight-rca-clustering-read", organization + ) else RCASource.EXPLORER ) diff --git a/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py b/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py index 6505be0774c0b4..ef92c5df0c2974 100644 --- a/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py +++ b/src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py @@ -80,7 +80,9 @@ def get(self, request: Request, organization: Organization) -> Response: rca_source = ( RCASource.LIGHTWEIGHT - if features.has("organizations:supergroups-lightweight-rca-clustering", organization) + if features.has( + "organizations:supergroups-lightweight-rca-clustering-read", organization + ) else RCASource.EXPLORER ) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index 4650dccb7f9062..f0efd0f7d7759a 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1593,7 +1593,9 @@ def kick_off_lightweight_rca_cluster(job: PostProcessJob) -> None: event = job["event"] group = event.group - if not features.has("organizations:supergroups-lightweight-rca-clustering", group.organization): + if not features.has( + "organizations:supergroups-lightweight-rca-clustering-write", group.organization + ): return trigger_lightweight_rca_cluster_task.delay(group.id) diff --git a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py index e94f10f83c3852..8c27ea7d51cad2 100644 --- a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py +++ b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py @@ -58,7 +58,7 @@ def test_rca_source_lightweight_when_flag_enabled(self, mock_seer): with self.feature( { "organizations:top-issues-ui": True, - "organizations:supergroups-lightweight-rca-clustering": True, + "organizations:supergroups-lightweight-rca-clustering-read": True, } ): self.get_success_response(self.organization.slug, "1") diff --git a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py index d6ce658e370303..20b42732b81999 100644 --- a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py +++ b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py @@ -99,7 +99,7 @@ def test_rca_source_lightweight_when_flag_enabled(self, mock_seer): with self.feature( { "organizations:top-issues-ui": True, - "organizations:supergroups-lightweight-rca-clustering": True, + "organizations:supergroups-lightweight-rca-clustering-read": True, } ): self.get_success_response( diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index 0f0555be84c4dd..7506b70d38ac07 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -3078,7 +3078,7 @@ def test_kick_off_lightweight_rca_cluster_when_enabled(self, mock_task): project_id=self.project.id, ) - with self.feature("organizations:supergroups-lightweight-rca-clustering"): + with self.feature("organizations:supergroups-lightweight-rca-clustering-write"): self.call_post_process_group( is_new=True, is_regression=False, @@ -3111,7 +3111,7 @@ def test_kick_off_lightweight_rca_cluster_skips_when_not_new(self, mock_task): project_id=self.project.id, ) - with self.feature("organizations:supergroups-lightweight-rca-clustering"): + with self.feature("organizations:supergroups-lightweight-rca-clustering-write"): self.call_post_process_group( is_new=False, is_regression=False, From 1e73216dc3daad179ac8226bea23da01ceb0984c Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Wed, 8 Apr 2026 12:22:37 -0700 Subject: [PATCH 3/3] ref(seer): Use uppercase values for RCASource enum --- src/sentry/seer/signed_seer_api.py | 4 ++-- .../endpoints/test_organization_supergroup_details.py | 4 ++-- .../endpoints/test_organization_supergroups_by_group.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sentry/seer/signed_seer_api.py b/src/sentry/seer/signed_seer_api.py index 83f8e4f4489271..14967631caa763 100644 --- a/src/sentry/seer/signed_seer_api.py +++ b/src/sentry/seer/signed_seer_api.py @@ -371,8 +371,8 @@ class SummarizeIssueRequest(TypedDict): class RCASource(StrEnum): - EXPLORER = "explorer" - LIGHTWEIGHT = "lightweight" + EXPLORER = "EXPLORER" + LIGHTWEIGHT = "LIGHTWEIGHT" class SupergroupsEmbeddingRequest(TypedDict): diff --git a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py index 8c27ea7d51cad2..d146d79126ccfa 100644 --- a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py +++ b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroup_details.py @@ -47,7 +47,7 @@ def test_rca_source_defaults_to_explorer(self, mock_seer): self.get_success_response(self.organization.slug, "1") body = mock_seer.call_args.args[0] - assert body["rca_source"] == "explorer" + assert body["rca_source"] == "EXPLORER" @patch( "sentry.seer.supergroups.endpoints.organization_supergroup_details.make_supergroups_get_request" @@ -64,4 +64,4 @@ def test_rca_source_lightweight_when_flag_enabled(self, mock_seer): self.get_success_response(self.organization.slug, "1") body = mock_seer.call_args.args[0] - assert body["rca_source"] == "lightweight" + assert body["rca_source"] == "LIGHTWEIGHT" diff --git a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py index 20b42732b81999..1c89779ffef777 100644 --- a/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py +++ b/tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py @@ -88,7 +88,7 @@ def test_rca_source_defaults_to_explorer(self, mock_seer): ) body = mock_seer.call_args.args[0] - assert body["rca_source"] == "explorer" + assert body["rca_source"] == "EXPLORER" @patch( "sentry.seer.supergroups.endpoints.organization_supergroups_by_group.make_supergroups_get_by_group_ids_request" @@ -108,4 +108,4 @@ def test_rca_source_lightweight_when_flag_enabled(self, mock_seer): ) body = mock_seer.call_args.args[0] - assert body["rca_source"] == "lightweight" + assert body["rca_source"] == "LIGHTWEIGHT"