Skip to content

Commit a949cd4

Browse files
yuvmenclaude
andcommitted
feat(seer): Add lightweight RCA clustering endpoint integration
Call Seer's new /v0/issues/supergroups/cluster-lightweight endpoint on new issue creation, gated per-org via sentry-options. This sends issue event data to Seer for lightweight root cause analysis and clustering into supergroups. Also renames the existing explorer-based lightweight RCA files to explorer_lightweight_rca to avoid confusion with the new direct endpoint-based clustering approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a8a8009 commit a949cd4

File tree

10 files changed

+251
-30
lines changed

10 files changed

+251
-30
lines changed

src/sentry/options/defaults.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,19 @@
13431343
flags=FLAG_MODIFIABLE_RATE | FLAG_AUTOMATOR_MODIFIABLE,
13441344
)
13451345

1346+
# Supergroups / Lightweight RCA
1347+
register(
1348+
"supergroups.active-rca-source",
1349+
default="explorer",
1350+
flags=FLAG_AUTOMATOR_MODIFIABLE,
1351+
)
1352+
register(
1353+
"supergroups.lightweight-enabled-orgs",
1354+
type=Sequence,
1355+
default=[],
1356+
flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
1357+
)
1358+
13461359
# ## sentry.killswitches
13471360
#
13481361
# The following options are documented in sentry.killswitches in more detail

src/sentry/seer/autofix/issue_summary.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
make_signed_seer_api_request,
4646
make_summarize_issue_request,
4747
)
48-
from sentry.seer.supergroups.lightweight_rca import trigger_lightweight_rca
48+
from sentry.seer.supergroups.explorer_lightweight_rca import trigger_explorer_lightweight_rca
4949
from sentry.services import eventstore
5050
from sentry.services.eventstore.models import Event, GroupEvent
5151
from sentry.tasks.base import instrumented_task
@@ -226,10 +226,10 @@ def _trigger_autofix_task(
226226
stopping_point=stopping_point,
227227
)
228228
try:
229-
trigger_lightweight_rca(group)
229+
trigger_explorer_lightweight_rca(group)
230230
except Exception:
231231
logger.exception(
232-
"lightweight_rca.trigger_error_in_trigger_autofix_task",
232+
"explorer_lightweight_rca.trigger_error_in_trigger_autofix_task",
233233
extra={"group_id": group_id},
234234
)
235235
else:

src/sentry/seer/signed_seer_api.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,14 @@ class SupergroupsEmbeddingRequest(TypedDict):
259259
artifact_data: dict[str, Any]
260260

261261

262+
class LightweightRCAClusterRequest(TypedDict):
263+
group_id: int
264+
issue: dict[str, Any]
265+
organization_slug: str
266+
organization_id: int
267+
project_id: int
268+
269+
262270
class SupergroupsListRequest(TypedDict):
263271
organization_id: int
264272
offset: NotRequired[int | None]
@@ -375,6 +383,20 @@ def make_supergroups_embedding_request(
375383
)
376384

377385

386+
def make_lightweight_rca_cluster_request(
387+
body: LightweightRCAClusterRequest,
388+
timeout: int | float | None = None,
389+
viewer_context: SeerViewerContext | None = None,
390+
) -> BaseHTTPResponse:
391+
return make_signed_seer_api_request(
392+
seer_autofix_default_connection_pool,
393+
"/v0/issues/supergroups/cluster-lightweight",
394+
body=orjson.dumps(body),
395+
timeout=timeout,
396+
viewer_context=viewer_context,
397+
)
398+
399+
378400
def make_supergroups_list_request(
379401
body: SupergroupsListRequest,
380402
viewer_context: SeerViewerContext,

src/sentry/seer/supergroups/lightweight_rca.py renamed to src/sentry/seer/supergroups/explorer_lightweight_rca.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
logger = logging.getLogger(__name__)
1111

1212

13-
def trigger_lightweight_rca(group: Group) -> int | None:
13+
def trigger_explorer_lightweight_rca(group: Group) -> int | None:
1414
"""
1515
Trigger a lightweight Explorer RCA run for the given group.
1616
@@ -26,7 +26,7 @@ def trigger_lightweight_rca(group: Group) -> int | None:
2626
"""
2727
has_feature = features.has("projects:supergroup-lightweight-rca", group.project)
2828
logger.info(
29-
"lightweight_rca.feature_flag_check",
29+
"explorer_lightweight_rca.feature_flag_check",
3030
extra={
3131
"group_id": group.id,
3232
"project_id": group.project.id,
@@ -66,7 +66,7 @@ def trigger_lightweight_rca(group: Group) -> int | None:
6666
)
6767

6868
logger.info(
69-
"lightweight_rca.starting_run",
69+
"explorer_lightweight_rca.starting_run",
7070
extra={
7171
"group_id": group.id,
7272
"project_id": group.project.id,
@@ -83,7 +83,7 @@ def trigger_lightweight_rca(group: Group) -> int | None:
8383
)
8484

8585
logger.info(
86-
"lightweight_rca.run_started",
86+
"explorer_lightweight_rca.run_started",
8787
extra={
8888
"group_id": group.id,
8989
"project_id": group.project.id,
@@ -94,7 +94,7 @@ def trigger_lightweight_rca(group: Group) -> int | None:
9494
return run_id
9595
except Exception:
9696
logger.exception(
97-
"lightweight_rca.trigger_failed",
97+
"explorer_lightweight_rca.trigger_failed",
9898
extra={
9999
"group_id": group.id,
100100
"organization_id": group.organization.id,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
from sentry import options
6+
from sentry.api.serializers import EventSerializer, serialize
7+
from sentry.eventstore import backend as eventstore
8+
from sentry.models.group import Group
9+
from sentry.seer.models import SeerApiError
10+
from sentry.seer.signed_seer_api import (
11+
LightweightRCAClusterRequest,
12+
SeerViewerContext,
13+
make_lightweight_rca_cluster_request,
14+
)
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
def trigger_lightweight_rca_cluster(group: Group) -> None:
20+
"""
21+
Call Seer's lightweight RCA clustering endpoint for the given group.
22+
23+
Sends issue event data to Seer, which generates a lightweight root cause analysis
24+
and clusters the issue into supergroups based on embedding similarity.
25+
"""
26+
enabled_orgs: list[int] = options.get("supergroups.lightweight-enabled-orgs")
27+
if group.organization.id not in enabled_orgs:
28+
return
29+
30+
event = group.get_recommended_event_for_environments()
31+
if not event:
32+
event = group.get_latest_event()
33+
34+
if not event:
35+
logger.info(
36+
"lightweight_rca_cluster.no_event",
37+
extra={"group_id": group.id},
38+
)
39+
return
40+
41+
ready_event = eventstore.get_event_by_id(group.project.id, event.event_id, group_id=group.id)
42+
if not ready_event:
43+
logger.info(
44+
"lightweight_rca_cluster.event_not_ready",
45+
extra={"group_id": group.id, "event_id": event.event_id},
46+
)
47+
return
48+
49+
serialized_event = serialize(ready_event, None, EventSerializer())
50+
51+
body = LightweightRCAClusterRequest(
52+
group_id=group.id,
53+
issue={
54+
"id": group.id,
55+
"title": group.title,
56+
"short_id": group.qualified_short_id,
57+
"events": [serialized_event],
58+
},
59+
organization_slug=group.organization.slug,
60+
organization_id=group.organization.id,
61+
project_id=group.project.id,
62+
)
63+
viewer_context = SeerViewerContext(organization_id=group.organization.id)
64+
65+
response = make_lightweight_rca_cluster_request(body, timeout=30, viewer_context=viewer_context)
66+
if response.status >= 400:
67+
raise SeerApiError("Lightweight RCA cluster request failed", response.status)
68+
69+
logger.info(
70+
"lightweight_rca_cluster.success",
71+
extra={
72+
"group_id": group.id,
73+
"project_id": group.project.id,
74+
"organization_id": group.organization.id,
75+
},
76+
)

src/sentry/tasks/post_process.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,22 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
15781578
)
15791579

15801580

1581+
def kick_off_lightweight_rca_cluster(job: PostProcessJob) -> None:
1582+
from sentry.tasks.seer.lightweight_rca_cluster import trigger_lightweight_rca_cluster_task
1583+
1584+
if not job["group_state"]["is_new"]:
1585+
return
1586+
1587+
event = job["event"]
1588+
group = event.group
1589+
1590+
enabled_orgs: list[int] = options.get("supergroups.lightweight-enabled-orgs")
1591+
if group.organization.id not in enabled_orgs:
1592+
return
1593+
1594+
trigger_lightweight_rca_cluster_task.delay(group.id)
1595+
1596+
15811597
GROUP_CATEGORY_POST_PROCESS_PIPELINE = {
15821598
GroupCategory.ERROR: [
15831599
_capture_group_stats,
@@ -1588,6 +1604,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
15881604
handle_owner_assignment,
15891605
handle_auto_assignment,
15901606
kick_off_seer_automation,
1607+
kick_off_lightweight_rca_cluster,
15911608
process_workflow_engine_issue_alerts,
15921609
process_resource_change_bounds,
15931610
process_data_forwarding,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import logging
2+
3+
from sentry.models.group import Group
4+
from sentry.tasks.base import instrumented_task
5+
from sentry.taskworker.namespaces import issues_tasks
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
@instrumented_task(
11+
name="sentry.tasks.seer.lightweight_rca_cluster.trigger_lightweight_rca_cluster_task",
12+
queue="default",
13+
max_retries=0,
14+
taskworker_namespace=issues_tasks,
15+
)
16+
def trigger_lightweight_rca_cluster_task(group_id: int, **kwargs) -> None:
17+
from sentry.seer.supergroups.lightweight_rca_cluster import trigger_lightweight_rca_cluster
18+
19+
try:
20+
group = Group.objects.get(id=group_id)
21+
except Group.DoesNotExist:
22+
logger.info(
23+
"lightweight_rca_cluster_task.group_not_found",
24+
extra={"group_id": group_id},
25+
)
26+
return
27+
28+
try:
29+
trigger_lightweight_rca_cluster(group)
30+
except Exception:
31+
logger.exception(
32+
"lightweight_rca_cluster_task.failed",
33+
extra={"group_id": group_id},
34+
)

tests/sentry/seer/autofix/test_issue_summary.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -960,14 +960,14 @@ def setUp(self) -> None:
960960
event_data = load_data("python")
961961
self.event = self.store_event(data=event_data, project_id=self.project.id)
962962

963-
@patch("sentry.seer.autofix.issue_summary.trigger_lightweight_rca")
963+
@patch("sentry.seer.autofix.issue_summary.trigger_explorer_lightweight_rca")
964964
@patch("sentry.seer.autofix.issue_summary.trigger_autofix_explorer", return_value=42)
965965
def test_lightweight_rca_called_on_explorer_path(
966966
self,
967967
mock_explorer,
968-
mock_lightweight_rca,
968+
mock_explorer_lightweight_rca,
969969
):
970-
"""trigger_lightweight_rca is called when the explorer path is taken"""
970+
"""trigger_explorer_lightweight_rca is called when the explorer path is taken"""
971971
_trigger_autofix_task(
972972
group_id=self.group.id,
973973
event_id=self.event.event_id,
@@ -976,18 +976,18 @@ def test_lightweight_rca_called_on_explorer_path(
976976
)
977977

978978
mock_explorer.assert_called_once()
979-
mock_lightweight_rca.assert_called_once_with(self.group)
979+
mock_explorer_lightweight_rca.assert_called_once_with(self.group)
980980

981-
@patch("sentry.seer.autofix.issue_summary.trigger_lightweight_rca")
981+
@patch("sentry.seer.autofix.issue_summary.trigger_explorer_lightweight_rca")
982982
@patch(
983983
"sentry.seer.autofix.issue_summary.trigger_autofix", return_value=Mock(data={"run_id": 42})
984984
)
985985
def test_lightweight_rca_not_called_on_legacy_path(
986986
self,
987987
mock_autofix,
988-
mock_lightweight_rca,
988+
mock_explorer_lightweight_rca,
989989
):
990-
"""trigger_lightweight_rca is NOT called on the legacy autofix path"""
990+
"""trigger_explorer_lightweight_rca is NOT called on the legacy autofix path"""
991991
with self.feature(
992992
{
993993
"organizations:seer-explorer": False,
@@ -1002,17 +1002,17 @@ def test_lightweight_rca_not_called_on_legacy_path(
10021002
)
10031003

10041004
mock_autofix.assert_called_once()
1005-
mock_lightweight_rca.assert_not_called()
1005+
mock_explorer_lightweight_rca.assert_not_called()
10061006

1007-
@patch("sentry.seer.autofix.issue_summary.trigger_lightweight_rca")
1007+
@patch("sentry.seer.autofix.issue_summary.trigger_explorer_lightweight_rca")
10081008
@patch("sentry.seer.autofix.issue_summary.trigger_autofix_explorer", return_value=42)
10091009
def test_lightweight_rca_failure_does_not_block_explorer(
10101010
self,
10111011
mock_explorer,
1012-
mock_lightweight_rca,
1012+
mock_explorer_lightweight_rca,
10131013
):
1014-
"""Failure in trigger_lightweight_rca doesn't prevent the explorer autofix from completing"""
1015-
mock_lightweight_rca.side_effect = Exception("lightweight RCA failed")
1014+
"""Failure in trigger_explorer_lightweight_rca doesn't prevent the explorer autofix from completing"""
1015+
mock_explorer_lightweight_rca.side_effect = Exception("lightweight RCA failed")
10161016

10171017
_trigger_autofix_task(
10181018
group_id=self.group.id,
@@ -1022,7 +1022,7 @@ def test_lightweight_rca_failure_does_not_block_explorer(
10221022
)
10231023

10241024
mock_explorer.assert_called_once()
1025-
mock_lightweight_rca.assert_called_once_with(self.group)
1025+
mock_explorer_lightweight_rca.assert_called_once_with(self.group)
10261026

10271027

10281028
class TestFetchUserPreference:

0 commit comments

Comments
 (0)