Skip to content

Commit 0851330

Browse files
yuvmengeorge-sentry
authored andcommitted
feat(seer): Add rca_source to supergroup queries with feature flag gating (#112436)
Add `rca_source` parameter to supergroup query APIs so Seer knows which embedding space (explorer vs lightweight) to query from. The source is determined by the `organizations:supergroups-lightweight-rca-clustering` feature flag. Also replaces the `supergroups.lightweight-enabled-orgs` sentry-option with this feature flag for both: - **Write path**: post_process task dispatch for lightweight RCA clustering - **Read path**: supergroup query endpoints (details + by-group) This is consistent with how all other supergroup features are gated (via feature flags, not options). Depends on #112229 (merged).
1 parent 3c1865e commit 0851330

File tree

9 files changed

+149
-22
lines changed

9 files changed

+149
-22
lines changed

src/sentry/features/temporary.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,10 @@ def register_temporary_features(manager: FeatureManager) -> None:
571571
manager.add("projects:supergroup-embeddings-explorer", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
572572
# Enable lightweight Explorer RCA runs for supergroup quality evaluation
573573
manager.add("projects:supergroup-lightweight-rca", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
574+
# Enable lightweight RCA clustering write path (generate embeddings on new issues)
575+
manager.add("organizations:supergroups-lightweight-rca-clustering-write", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
576+
# Enable lightweight RCA clustering read path (query lightweight embeddings in supergroup APIs)
577+
manager.add("organizations:supergroups-lightweight-rca-clustering-read", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
574578

575579
manager.add("projects:workflow-engine-performance-detectors", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
576580

src/sentry/options/defaults.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,14 +1385,6 @@
13851385
flags=FLAG_AUTOMATOR_MODIFIABLE,
13861386
)
13871387

1388-
# Supergroups / Lightweight RCA
1389-
register(
1390-
"supergroups.lightweight-enabled-orgs",
1391-
type=Sequence,
1392-
default=[],
1393-
flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
1394-
)
1395-
13961388
# ## sentry.killswitches
13971389
#
13981390
# The following options are documented in sentry.killswitches in more detail

src/sentry/seer/signed_seer_api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import hashlib
22
import hmac
33
import logging
4+
from enum import StrEnum
45
from typing import Any, NotRequired, TypedDict
56
from urllib.parse import urlparse
67

@@ -369,6 +370,11 @@ class SummarizeIssueRequest(TypedDict):
369370
experiment_variant: NotRequired[str | None]
370371

371372

373+
class RCASource(StrEnum):
374+
EXPLORER = "EXPLORER"
375+
LIGHTWEIGHT = "LIGHTWEIGHT"
376+
377+
372378
class SupergroupsEmbeddingRequest(TypedDict):
373379
organization_id: int
374380
group_id: int
@@ -387,11 +393,13 @@ class LightweightRCAClusterRequest(TypedDict):
387393
class SupergroupsGetRequest(TypedDict):
388394
organization_id: int
389395
supergroup_id: int
396+
rca_source: str
390397

391398

392399
class SupergroupsGetByGroupIdsRequest(TypedDict):
393400
organization_id: int
394401
group_ids: list[int]
402+
rca_source: str
395403

396404

397405
class SupergroupDetailData(TypedDict):

src/sentry/seer/supergroups/endpoints/organization_supergroup_details.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from sentry.api.base import cell_silo_endpoint
1313
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
1414
from sentry.models.organization import Organization
15-
from sentry.seer.signed_seer_api import SeerViewerContext, make_supergroups_get_request
15+
from sentry.seer.signed_seer_api import RCASource, SeerViewerContext, make_supergroups_get_request
1616

1717
logger = logging.getLogger(__name__)
1818

@@ -35,8 +35,20 @@ def get(self, request: Request, organization: Organization, supergroup_id: int)
3535
if not features.has("organizations:top-issues-ui", organization, actor=request.user):
3636
return Response({"detail": "Feature not available"}, status=403)
3737

38+
rca_source = (
39+
RCASource.LIGHTWEIGHT
40+
if features.has(
41+
"organizations:supergroups-lightweight-rca-clustering-read", organization
42+
)
43+
else RCASource.EXPLORER
44+
)
45+
3846
response = make_supergroups_get_request(
39-
{"organization_id": organization.id, "supergroup_id": supergroup_id},
47+
{
48+
"organization_id": organization.id,
49+
"supergroup_id": supergroup_id,
50+
"rca_source": rca_source,
51+
},
4052
SeerViewerContext(organization_id=organization.id, user_id=request.user.id),
4153
timeout=10,
4254
)

src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from sentry.models.group import STATUS_QUERY_CHOICES, Group
1616
from sentry.models.organization import Organization
1717
from sentry.seer.signed_seer_api import (
18+
RCASource,
1819
SeerViewerContext,
1920
SupergroupsByGroupIdsResponse,
2021
make_supergroups_get_by_group_ids_request,
@@ -77,8 +78,16 @@ def get(self, request: Request, organization: Organization) -> Response:
7778
status=status_codes.HTTP_404_NOT_FOUND,
7879
)
7980

81+
rca_source = (
82+
RCASource.LIGHTWEIGHT
83+
if features.has(
84+
"organizations:supergroups-lightweight-rca-clustering-read", organization
85+
)
86+
else RCASource.EXPLORER
87+
)
88+
8089
response = make_supergroups_get_by_group_ids_request(
81-
{"organization_id": organization.id, "group_ids": group_ids},
90+
{"organization_id": organization.id, "group_ids": group_ids, "rca_source": rca_source},
8291
SeerViewerContext(organization_id=organization.id, user_id=request.user.id),
8392
timeout=10,
8493
)

src/sentry/tasks/post_process.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,8 +1595,9 @@ def kick_off_lightweight_rca_cluster(job: PostProcessJob) -> None:
15951595
event = job["event"]
15961596
group = event.group
15971597

1598-
enabled_orgs: list[int] = options.get("supergroups.lightweight-enabled-orgs")
1599-
if group.organization.id not in enabled_orgs:
1598+
if not features.has(
1599+
"organizations:supergroups-lightweight-rca-clustering-write", group.organization
1600+
):
16001601
return
16011602

16021603
trigger_lightweight_rca_cluster_task.delay(group.id)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
from unittest.mock import MagicMock, patch
5+
6+
import orjson
7+
8+
from sentry.testutils.cases import APITestCase
9+
10+
11+
def mock_seer_response(data: dict[str, Any]) -> MagicMock:
12+
response = MagicMock()
13+
response.status = 200
14+
response.data = orjson.dumps(data)
15+
return response
16+
17+
18+
class OrganizationSupergroupDetailsEndpointTest(APITestCase):
19+
endpoint = "sentry-api-0-organization-supergroup-details"
20+
21+
def setUp(self):
22+
super().setUp()
23+
self.login_as(self.user)
24+
25+
@patch(
26+
"sentry.seer.supergroups.endpoints.organization_supergroup_details.make_supergroups_get_request"
27+
)
28+
def test_get_supergroup_details(self, mock_seer):
29+
mock_seer.return_value = mock_seer_response(
30+
{"id": 1, "title": "NullPointerException in auth", "group_ids": [10, 20]}
31+
)
32+
33+
with self.feature("organizations:top-issues-ui"):
34+
response = self.get_success_response(self.organization.slug, "1")
35+
36+
assert response.data["id"] == 1
37+
assert response.data["title"] == "NullPointerException in auth"
38+
assert response.data["group_ids"] == [10, 20]
39+
40+
@patch(
41+
"sentry.seer.supergroups.endpoints.organization_supergroup_details.make_supergroups_get_request"
42+
)
43+
def test_rca_source_defaults_to_explorer(self, mock_seer):
44+
mock_seer.return_value = mock_seer_response({"id": 1, "title": "test"})
45+
46+
with self.feature("organizations:top-issues-ui"):
47+
self.get_success_response(self.organization.slug, "1")
48+
49+
body = mock_seer.call_args.args[0]
50+
assert body["rca_source"] == "EXPLORER"
51+
52+
@patch(
53+
"sentry.seer.supergroups.endpoints.organization_supergroup_details.make_supergroups_get_request"
54+
)
55+
def test_rca_source_lightweight_when_flag_enabled(self, mock_seer):
56+
mock_seer.return_value = mock_seer_response({"id": 1, "title": "test"})
57+
58+
with self.feature(
59+
{
60+
"organizations:top-issues-ui": True,
61+
"organizations:supergroups-lightweight-rca-clustering-read": True,
62+
}
63+
):
64+
self.get_success_response(self.organization.slug, "1")
65+
66+
body = mock_seer.call_args.args[0]
67+
assert body["rca_source"] == "LIGHTWEIGHT"

tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,38 @@ def test_status_filter_invalid(self):
7474
status="bogus",
7575
status_code=400,
7676
)
77+
78+
@patch(
79+
"sentry.seer.supergroups.endpoints.organization_supergroups_by_group.make_supergroups_get_by_group_ids_request"
80+
)
81+
def test_rca_source_defaults_to_explorer(self, mock_seer):
82+
mock_seer.return_value = mock_seer_response({"data": []})
83+
84+
with self.feature("organizations:top-issues-ui"):
85+
self.get_success_response(
86+
self.organization.slug,
87+
group_id=[self.unresolved_group.id],
88+
)
89+
90+
body = mock_seer.call_args.args[0]
91+
assert body["rca_source"] == "EXPLORER"
92+
93+
@patch(
94+
"sentry.seer.supergroups.endpoints.organization_supergroups_by_group.make_supergroups_get_by_group_ids_request"
95+
)
96+
def test_rca_source_lightweight_when_flag_enabled(self, mock_seer):
97+
mock_seer.return_value = mock_seer_response({"data": []})
98+
99+
with self.feature(
100+
{
101+
"organizations:top-issues-ui": True,
102+
"organizations:supergroups-lightweight-rca-clustering-read": True,
103+
}
104+
):
105+
self.get_success_response(
106+
self.organization.slug,
107+
group_id=[self.unresolved_group.id],
108+
)
109+
110+
body = mock_seer.call_args.args[0]
111+
assert body["rca_source"] == "LIGHTWEIGHT"

tests/sentry/tasks/test_post_process.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3078,7 +3078,7 @@ def test_kick_off_lightweight_rca_cluster_when_enabled(self, mock_task):
30783078
project_id=self.project.id,
30793079
)
30803080

3081-
with self.options({"supergroups.lightweight-enabled-orgs": [self.project.organization.id]}):
3081+
with self.feature("organizations:supergroups-lightweight-rca-clustering-write"):
30823082
self.call_post_process_group(
30833083
is_new=True,
30843084
is_regression=False,
@@ -3095,13 +3095,12 @@ def test_kick_off_lightweight_rca_cluster_skips_when_not_enabled(self, mock_task
30953095
project_id=self.project.id,
30963096
)
30973097

3098-
with self.options({"supergroups.lightweight-enabled-orgs": []}):
3099-
self.call_post_process_group(
3100-
is_new=True,
3101-
is_regression=False,
3102-
is_new_group_environment=True,
3103-
event=event,
3104-
)
3098+
self.call_post_process_group(
3099+
is_new=True,
3100+
is_regression=False,
3101+
is_new_group_environment=True,
3102+
event=event,
3103+
)
31053104

31063105
mock_task.assert_not_called()
31073106

@@ -3112,7 +3111,7 @@ def test_kick_off_lightweight_rca_cluster_skips_when_not_new(self, mock_task):
31123111
project_id=self.project.id,
31133112
)
31143113

3115-
with self.options({"supergroups.lightweight-enabled-orgs": [self.project.organization.id]}):
3114+
with self.feature("organizations:supergroups-lightweight-rca-clustering-write"):
31163115
self.call_post_process_group(
31173116
is_new=False,
31183117
is_regression=False,

0 commit comments

Comments
 (0)