From 1b7c0828527feed6972ca40f71cc165d95ff3a95 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Fri, 3 Apr 2026 13:21:48 -0700 Subject: [PATCH 01/11] 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 --- src/sentry/options/defaults.py | 13 ++++ src/sentry/seer/autofix/issue_summary.py | 6 +- src/sentry/seer/signed_seer_api.py | 22 ++++++ ...ght_rca.py => explorer_lightweight_rca.py} | 10 +-- .../supergroups/lightweight_rca_cluster.py | 76 +++++++++++++++++++ src/sentry/tasks/post_process.py | 19 +++++ .../tasks/seer/lightweight_rca_cluster.py | 34 +++++++++ .../sentry/seer/autofix/test_issue_summary.py | 26 +++---- ...ca.py => test_explorer_lightweight_rca.py} | 18 ++--- .../test_lightweight_rca_cluster.py | 59 ++++++++++++++ 10 files changed, 253 insertions(+), 30 deletions(-) rename src/sentry/seer/supergroups/{lightweight_rca.py => explorer_lightweight_rca.py} (91%) create mode 100644 src/sentry/seer/supergroups/lightweight_rca_cluster.py create mode 100644 src/sentry/tasks/seer/lightweight_rca_cluster.py rename tests/sentry/seer/supergroups/{test_lightweight_rca.py => test_explorer_lightweight_rca.py} (76%) create mode 100644 tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 028c7f36986a52..0304465149b2b2 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -1343,6 +1343,19 @@ flags=FLAG_MODIFIABLE_RATE | FLAG_AUTOMATOR_MODIFIABLE, ) +# Supergroups / Lightweight RCA +register( + "supergroups.active-rca-source", + default="explorer", + flags=FLAG_AUTOMATOR_MODIFIABLE, +) +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/autofix/issue_summary.py b/src/sentry/seer/autofix/issue_summary.py index 367131d81def82..0a69745e31a502 100644 --- a/src/sentry/seer/autofix/issue_summary.py +++ b/src/sentry/seer/autofix/issue_summary.py @@ -45,7 +45,7 @@ make_signed_seer_api_request, make_summarize_issue_request, ) -from sentry.seer.supergroups.lightweight_rca import trigger_lightweight_rca +from sentry.seer.supergroups.explorer_lightweight_rca import trigger_explorer_lightweight_rca from sentry.services import eventstore from sentry.services.eventstore.models import Event, GroupEvent from sentry.tasks.base import instrumented_task @@ -226,10 +226,10 @@ def _trigger_autofix_task( stopping_point=stopping_point, ) try: - trigger_lightweight_rca(group) + trigger_explorer_lightweight_rca(group) except Exception: logger.exception( - "lightweight_rca.trigger_error_in_trigger_autofix_task", + "explorer_lightweight_rca.trigger_error_in_trigger_autofix_task", extra={"group_id": group_id}, ) else: diff --git a/src/sentry/seer/signed_seer_api.py b/src/sentry/seer/signed_seer_api.py index 20cf09976c871b..1323205b2c47e2 100644 --- a/src/sentry/seer/signed_seer_api.py +++ b/src/sentry/seer/signed_seer_api.py @@ -259,6 +259,14 @@ class SupergroupsEmbeddingRequest(TypedDict): artifact_data: dict[str, Any] +class LightweightRCAClusterRequest(TypedDict): + group_id: int + issue: dict[str, Any] + organization_slug: str + organization_id: int + project_id: int + + class SupergroupsListRequest(TypedDict): organization_id: int offset: NotRequired[int | None] @@ -375,6 +383,20 @@ def make_supergroups_embedding_request( ) +def make_lightweight_rca_cluster_request( + body: LightweightRCAClusterRequest, + timeout: int | float | None = None, + viewer_context: SeerViewerContext | None = None, +) -> BaseHTTPResponse: + return make_signed_seer_api_request( + seer_autofix_default_connection_pool, + "/v0/issues/supergroups/cluster-lightweight", + body=orjson.dumps(body), + timeout=timeout, + viewer_context=viewer_context, + ) + + def make_supergroups_list_request( body: SupergroupsListRequest, viewer_context: SeerViewerContext, diff --git a/src/sentry/seer/supergroups/lightweight_rca.py b/src/sentry/seer/supergroups/explorer_lightweight_rca.py similarity index 91% rename from src/sentry/seer/supergroups/lightweight_rca.py rename to src/sentry/seer/supergroups/explorer_lightweight_rca.py index 26cf86b9715aa7..b7459db4394d6e 100644 --- a/src/sentry/seer/supergroups/lightweight_rca.py +++ b/src/sentry/seer/supergroups/explorer_lightweight_rca.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -def trigger_lightweight_rca(group: Group) -> int | None: +def trigger_explorer_lightweight_rca(group: Group) -> int | None: """ Trigger a lightweight Explorer RCA run for the given group. @@ -26,7 +26,7 @@ def trigger_lightweight_rca(group: Group) -> int | None: """ has_feature = features.has("projects:supergroup-lightweight-rca", group.project) logger.info( - "lightweight_rca.feature_flag_check", + "explorer_lightweight_rca.feature_flag_check", extra={ "group_id": group.id, "project_id": group.project.id, @@ -66,7 +66,7 @@ def trigger_lightweight_rca(group: Group) -> int | None: ) logger.info( - "lightweight_rca.starting_run", + "explorer_lightweight_rca.starting_run", extra={ "group_id": group.id, "project_id": group.project.id, @@ -83,7 +83,7 @@ def trigger_lightweight_rca(group: Group) -> int | None: ) logger.info( - "lightweight_rca.run_started", + "explorer_lightweight_rca.run_started", extra={ "group_id": group.id, "project_id": group.project.id, @@ -94,7 +94,7 @@ def trigger_lightweight_rca(group: Group) -> int | None: return run_id except Exception: logger.exception( - "lightweight_rca.trigger_failed", + "explorer_lightweight_rca.trigger_failed", extra={ "group_id": group.id, "organization_id": group.organization.id, diff --git a/src/sentry/seer/supergroups/lightweight_rca_cluster.py b/src/sentry/seer/supergroups/lightweight_rca_cluster.py new file mode 100644 index 00000000000000..8014c29244d011 --- /dev/null +++ b/src/sentry/seer/supergroups/lightweight_rca_cluster.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import logging + +from sentry import options +from sentry.api.serializers import EventSerializer, serialize +from sentry.eventstore import backend as eventstore +from sentry.models.group import Group +from sentry.seer.models import SeerApiError +from sentry.seer.signed_seer_api import ( + LightweightRCAClusterRequest, + SeerViewerContext, + make_lightweight_rca_cluster_request, +) + +logger = logging.getLogger(__name__) + + +def trigger_lightweight_rca_cluster(group: Group) -> None: + """ + Call Seer's lightweight RCA clustering endpoint for the given group. + + Sends issue event data to Seer, which generates a lightweight root cause analysis + and clusters the issue into supergroups based on embedding similarity. + """ + enabled_orgs: list[int] = options.get("supergroups.lightweight-enabled-orgs") + if group.organization.id not in enabled_orgs: + return + + event = group.get_recommended_event_for_environments() + if not event: + event = group.get_latest_event() + + if not event: + logger.info( + "lightweight_rca_cluster.no_event", + extra={"group_id": group.id}, + ) + return + + ready_event = eventstore.get_event_by_id(group.project.id, event.event_id, group_id=group.id) + if not ready_event: + logger.info( + "lightweight_rca_cluster.event_not_ready", + extra={"group_id": group.id, "event_id": event.event_id}, + ) + return + + serialized_event = serialize(ready_event, None, EventSerializer()) + + body = LightweightRCAClusterRequest( + group_id=group.id, + issue={ + "id": group.id, + "title": group.title, + "short_id": group.qualified_short_id, + "events": [serialized_event], + }, + organization_slug=group.organization.slug, + organization_id=group.organization.id, + project_id=group.project.id, + ) + viewer_context = SeerViewerContext(organization_id=group.organization.id) + + response = make_lightweight_rca_cluster_request(body, timeout=30, viewer_context=viewer_context) + if response.status >= 400: + raise SeerApiError("Lightweight RCA cluster request failed", response.status) + + logger.info( + "lightweight_rca_cluster.success", + extra={ + "group_id": group.id, + "project_id": group.project.id, + "organization_id": group.organization.id, + }, + ) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index f4d8390e65c6be..3e373cc7717c1d 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1584,6 +1584,24 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: ) +def kick_off_lightweight_rca_cluster(job: PostProcessJob) -> None: + from sentry.tasks.seer.lightweight_rca_cluster import trigger_lightweight_rca_cluster_task + + if not job["group_state"]["is_new"]: + return + + event = job["event"] + group = event.group + if group is None: + return + + enabled_orgs: list[int] = options.get("supergroups.lightweight-enabled-orgs") + if group.organization.id not in enabled_orgs: + return + + trigger_lightweight_rca_cluster_task.delay(group.id) + + GROUP_CATEGORY_POST_PROCESS_PIPELINE: dict[ GroupCategory, list[Callable[[PostProcessJob], None]] ] = { @@ -1596,6 +1614,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None: handle_owner_assignment, handle_auto_assignment, kick_off_seer_automation, + kick_off_lightweight_rca_cluster, process_workflow_engine_issue_alerts, process_resource_change_bounds, process_data_forwarding, diff --git a/src/sentry/tasks/seer/lightweight_rca_cluster.py b/src/sentry/tasks/seer/lightweight_rca_cluster.py new file mode 100644 index 00000000000000..8dc3043963987e --- /dev/null +++ b/src/sentry/tasks/seer/lightweight_rca_cluster.py @@ -0,0 +1,34 @@ +import logging + +from sentry.models.group import Group +from sentry.tasks.base import instrumented_task +from sentry.taskworker.namespaces import issues_tasks + +logger = logging.getLogger(__name__) + + +@instrumented_task( + name="sentry.tasks.seer.lightweight_rca_cluster.trigger_lightweight_rca_cluster_task", + queue="default", + max_retries=0, + taskworker_namespace=issues_tasks, +) +def trigger_lightweight_rca_cluster_task(group_id: int, **kwargs) -> None: + from sentry.seer.supergroups.lightweight_rca_cluster import trigger_lightweight_rca_cluster + + try: + group = Group.objects.get(id=group_id) + except Group.DoesNotExist: + logger.info( + "lightweight_rca_cluster_task.group_not_found", + extra={"group_id": group_id}, + ) + return + + try: + trigger_lightweight_rca_cluster(group) + except Exception: + logger.exception( + "lightweight_rca_cluster_task.failed", + extra={"group_id": group_id}, + ) diff --git a/tests/sentry/seer/autofix/test_issue_summary.py b/tests/sentry/seer/autofix/test_issue_summary.py index 05ea097c4e69a7..548b83ebe7c106 100644 --- a/tests/sentry/seer/autofix/test_issue_summary.py +++ b/tests/sentry/seer/autofix/test_issue_summary.py @@ -960,14 +960,14 @@ def setUp(self) -> None: event_data = load_data("python") self.event = self.store_event(data=event_data, project_id=self.project.id) - @patch("sentry.seer.autofix.issue_summary.trigger_lightweight_rca") + @patch("sentry.seer.autofix.issue_summary.trigger_explorer_lightweight_rca") @patch("sentry.seer.autofix.issue_summary.trigger_autofix_explorer", return_value=42) def test_lightweight_rca_called_on_explorer_path( self, mock_explorer, - mock_lightweight_rca, + mock_explorer_lightweight_rca, ): - """trigger_lightweight_rca is called when the explorer path is taken""" + """trigger_explorer_lightweight_rca is called when the explorer path is taken""" _trigger_autofix_task( group_id=self.group.id, event_id=self.event.event_id, @@ -976,18 +976,18 @@ def test_lightweight_rca_called_on_explorer_path( ) mock_explorer.assert_called_once() - mock_lightweight_rca.assert_called_once_with(self.group) + mock_explorer_lightweight_rca.assert_called_once_with(self.group) - @patch("sentry.seer.autofix.issue_summary.trigger_lightweight_rca") + @patch("sentry.seer.autofix.issue_summary.trigger_explorer_lightweight_rca") @patch( "sentry.seer.autofix.issue_summary.trigger_autofix", return_value=Mock(data={"run_id": 42}) ) def test_lightweight_rca_not_called_on_legacy_path( self, mock_autofix, - mock_lightweight_rca, + mock_explorer_lightweight_rca, ): - """trigger_lightweight_rca is NOT called on the legacy autofix path""" + """trigger_explorer_lightweight_rca is NOT called on the legacy autofix path""" with self.feature( { "organizations:seer-explorer": False, @@ -1002,17 +1002,17 @@ def test_lightweight_rca_not_called_on_legacy_path( ) mock_autofix.assert_called_once() - mock_lightweight_rca.assert_not_called() + mock_explorer_lightweight_rca.assert_not_called() - @patch("sentry.seer.autofix.issue_summary.trigger_lightweight_rca") + @patch("sentry.seer.autofix.issue_summary.trigger_explorer_lightweight_rca") @patch("sentry.seer.autofix.issue_summary.trigger_autofix_explorer", return_value=42) def test_lightweight_rca_failure_does_not_block_explorer( self, mock_explorer, - mock_lightweight_rca, + mock_explorer_lightweight_rca, ): - """Failure in trigger_lightweight_rca doesn't prevent the explorer autofix from completing""" - mock_lightweight_rca.side_effect = Exception("lightweight RCA failed") + """Failure in trigger_explorer_lightweight_rca doesn't prevent the explorer autofix from completing""" + mock_explorer_lightweight_rca.side_effect = Exception("lightweight RCA failed") _trigger_autofix_task( group_id=self.group.id, @@ -1022,7 +1022,7 @@ def test_lightweight_rca_failure_does_not_block_explorer( ) mock_explorer.assert_called_once() - mock_lightweight_rca.assert_called_once_with(self.group) + mock_explorer_lightweight_rca.assert_called_once_with(self.group) class TestFetchUserPreference: diff --git a/tests/sentry/seer/supergroups/test_lightweight_rca.py b/tests/sentry/seer/supergroups/test_explorer_lightweight_rca.py similarity index 76% rename from tests/sentry/seer/supergroups/test_lightweight_rca.py rename to tests/sentry/seer/supergroups/test_explorer_lightweight_rca.py index 9d5033ae8ea960..e3c0544684062d 100644 --- a/tests/sentry/seer/supergroups/test_lightweight_rca.py +++ b/tests/sentry/seer/supergroups/test_explorer_lightweight_rca.py @@ -1,10 +1,10 @@ from unittest.mock import MagicMock, patch -from sentry.seer.supergroups.lightweight_rca import trigger_lightweight_rca +from sentry.seer.supergroups.explorer_lightweight_rca import trigger_explorer_lightweight_rca from sentry.testutils.cases import TestCase -class TestTriggerLightweightRca(TestCase): +class TestTriggerExplorerLightweightRca(TestCase): def setUp(self) -> None: super().setUp() self.user = self.create_user() @@ -13,18 +13,18 @@ def setUp(self) -> None: self.group = self.create_group(project=self.project) def test_returns_none_when_feature_flag_off(self) -> None: - run_id = trigger_lightweight_rca(self.group) + run_id = trigger_explorer_lightweight_rca(self.group) assert run_id is None - @patch("sentry.seer.supergroups.lightweight_rca.SeerExplorerClient") + @patch("sentry.seer.supergroups.explorer_lightweight_rca.SeerExplorerClient") def test_creates_client_with_correct_params(self, mock_client_cls): mock_client = MagicMock() mock_client.start_run.return_value = 42 mock_client_cls.return_value = mock_client with self.feature("projects:supergroup-lightweight-rca"): - run_id = trigger_lightweight_rca(self.group) + run_id = trigger_explorer_lightweight_rca(self.group) assert run_id == 42 mock_client_cls.assert_called_once() @@ -38,14 +38,14 @@ def test_creates_client_with_correct_params(self, mock_client_cls): assert kwargs["category_key"] == "lightweight_rca" assert kwargs["category_value"] == str(self.group.id) - @patch("sentry.seer.supergroups.lightweight_rca.SeerExplorerClient") + @patch("sentry.seer.supergroups.explorer_lightweight_rca.SeerExplorerClient") def test_start_run_called_with_correct_params(self, mock_client_cls): mock_client = MagicMock() mock_client.start_run.return_value = 42 mock_client_cls.return_value = mock_client with self.feature("projects:supergroup-lightweight-rca"): - trigger_lightweight_rca(self.group) + trigger_explorer_lightweight_rca(self.group) mock_client.start_run.assert_called_once() kwargs = mock_client.start_run.call_args[1] @@ -54,11 +54,11 @@ def test_start_run_called_with_correct_params(self, mock_client_cls): assert kwargs["metadata"] == {"group_id": self.group.id} assert "root cause" in kwargs["prompt"].lower() - @patch("sentry.seer.supergroups.lightweight_rca.SeerExplorerClient") + @patch("sentry.seer.supergroups.explorer_lightweight_rca.SeerExplorerClient") def test_returns_none_on_error(self, mock_client_cls): mock_client_cls.side_effect = Exception("connection failed") with self.feature("projects:supergroup-lightweight-rca"): - run_id = trigger_lightweight_rca(self.group) + run_id = trigger_explorer_lightweight_rca(self.group) assert run_id is None diff --git a/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py b/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py new file mode 100644 index 00000000000000..c19ee92fcc358d --- /dev/null +++ b/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py @@ -0,0 +1,59 @@ +from unittest.mock import patch + +import pytest + +from sentry.seer.supergroups.lightweight_rca_cluster import trigger_lightweight_rca_cluster +from sentry.testutils.cases import TestCase + + +class TriggerLightweightRCAClusterTest(TestCase): + def setUp(self): + super().setUp() + self.event = self.store_event( + data={"message": "test error", "level": "error"}, + project_id=self.project.id, + ) + self.group = self.event.group + + @patch("sentry.seer.supergroups.lightweight_rca_cluster.make_lightweight_rca_cluster_request") + def test_calls_seer_with_correct_payload(self, mock_request): + mock_request.return_value.status = 200 + + with self.options({"supergroups.lightweight-enabled-orgs": [self.group.organization.id]}): + trigger_lightweight_rca_cluster(self.group) + + mock_request.assert_called_once() + body = mock_request.call_args.args[0] + assert body["group_id"] == self.group.id + assert body["organization_id"] == self.group.organization.id + assert body["organization_slug"] == self.group.organization.slug + assert body["project_id"] == self.group.project.id + assert body["issue"]["id"] == self.group.id + assert body["issue"]["title"] == self.group.title + assert "events" in body["issue"] + assert len(body["issue"]["events"]) == 1 + + @patch("sentry.seer.supergroups.lightweight_rca_cluster.make_lightweight_rca_cluster_request") + def test_skips_when_org_not_enabled(self, mock_request): + with self.options({"supergroups.lightweight-enabled-orgs": []}): + trigger_lightweight_rca_cluster(self.group) + + mock_request.assert_not_called() + + @patch("sentry.seer.supergroups.lightweight_rca_cluster.make_lightweight_rca_cluster_request") + def test_raises_on_seer_error(self, mock_request): + mock_request.return_value.status = 500 + + with self.options({"supergroups.lightweight-enabled-orgs": [self.group.organization.id]}): + with pytest.raises(Exception): + trigger_lightweight_rca_cluster(self.group) + + @patch("sentry.seer.supergroups.lightweight_rca_cluster.make_lightweight_rca_cluster_request") + def test_passes_viewer_context(self, mock_request): + mock_request.return_value.status = 200 + + with self.options({"supergroups.lightweight-enabled-orgs": [self.group.organization.id]}): + trigger_lightweight_rca_cluster(self.group) + + call_kwargs = mock_request.call_args.kwargs + assert call_kwargs["viewer_context"]["organization_id"] == self.group.organization.id From 98a5f1695f4f19835d4be8ad47c1d97f9634e929 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Fri, 3 Apr 2026 14:05:23 -0700 Subject: [PATCH 02/11] fix(seer): Add OPT_NON_STR_KEYS to lightweight RCA cluster request Serialized event data from EventSerializer can contain non-string dict keys (integer keys in _meta.entries). Without this option orjson.dumps raises TypeError. --- src/sentry/seer/signed_seer_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/seer/signed_seer_api.py b/src/sentry/seer/signed_seer_api.py index 1323205b2c47e2..ce4dee6760494d 100644 --- a/src/sentry/seer/signed_seer_api.py +++ b/src/sentry/seer/signed_seer_api.py @@ -391,7 +391,7 @@ def make_lightweight_rca_cluster_request( return make_signed_seer_api_request( seer_autofix_default_connection_pool, "/v0/issues/supergroups/cluster-lightweight", - body=orjson.dumps(body), + body=orjson.dumps(body, option=orjson.OPT_NON_STR_KEYS), timeout=timeout, viewer_context=viewer_context, ) From a3bbef27e38cc2ea04e4fec945647fbd8a79ee5a Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Fri, 3 Apr 2026 14:13:36 -0700 Subject: [PATCH 03/11] fix(seer): Register lightweight RCA cluster task in TASKWORKER_IMPORTS Without this registration the task won't be discovered by the taskworker in production. --- src/sentry/conf/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 12f13726a2d31d..846c0fc5c9aa38 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -992,6 +992,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "sentry.workflow_engine.tasks.cleanup", "sentry.tasks.seer.explorer_index", "sentry.tasks.seer.context_engine_index", + "sentry.tasks.seer.lightweight_rca_cluster", # Used for tests "sentry.taskworker.tasks.examples", ) From 6f9d2d149ce0f447280bcff18c21c051cf2fccda Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Fri, 3 Apr 2026 14:55:04 -0700 Subject: [PATCH 04/11] ref(seer): Remove unused supergroups.active-rca-source option This option is used on the Seer side, not in Sentry. Remove it until it's actually needed here. --- src/sentry/options/defaults.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 0304465149b2b2..e553bc6943af57 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -1344,11 +1344,6 @@ ) # Supergroups / Lightweight RCA -register( - "supergroups.active-rca-source", - default="explorer", - flags=FLAG_AUTOMATOR_MODIFIABLE, -) register( "supergroups.lightweight-enabled-orgs", type=Sequence, From 4905a860fc867c35158208a562417f73b13c7d93 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Fri, 3 Apr 2026 15:13:16 -0700 Subject: [PATCH 05/11] fix(seer): Use correct namespace param for instrumented_task The instrumented_task decorator requires `namespace` not `taskworker_namespace`, and doesn't accept `queue` or `max_retries`. --- src/sentry/tasks/seer/lightweight_rca_cluster.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sentry/tasks/seer/lightweight_rca_cluster.py b/src/sentry/tasks/seer/lightweight_rca_cluster.py index 8dc3043963987e..81c52250dca360 100644 --- a/src/sentry/tasks/seer/lightweight_rca_cluster.py +++ b/src/sentry/tasks/seer/lightweight_rca_cluster.py @@ -9,9 +9,7 @@ @instrumented_task( name="sentry.tasks.seer.lightweight_rca_cluster.trigger_lightweight_rca_cluster_task", - queue="default", - max_retries=0, - taskworker_namespace=issues_tasks, + namespace=issues_tasks, ) def trigger_lightweight_rca_cluster_task(group_id: int, **kwargs) -> None: from sentry.seer.supergroups.lightweight_rca_cluster import trigger_lightweight_rca_cluster From c984fcfb0d72104b6f078973d199abd8adbacf51 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Fri, 3 Apr 2026 15:48:14 -0700 Subject: [PATCH 06/11] fix(seer): Remove unreachable None check on GroupEvent.group GroupEvent.group is typed as non-optional, so the None check is unreachable and mypy flags it. --- src/sentry/tasks/post_process.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index 3e373cc7717c1d..a7d5ac582ad224 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -1592,8 +1592,6 @@ def kick_off_lightweight_rca_cluster(job: PostProcessJob) -> None: event = job["event"] group = event.group - if group is None: - return enabled_orgs: list[int] = options.get("supergroups.lightweight-enabled-orgs") if group.organization.id not in enabled_orgs: From 2b845299fca094e0b3c13c9ee2c5e3bab9b7d1ad Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Mon, 6 Apr 2026 11:10:36 -0700 Subject: [PATCH 07/11] ref(seer): Remove duplicate org check from trigger_lightweight_rca_cluster The org eligibility check is already done in the pipeline step before scheduling the task, so there's no need to check again in the function itself. --- .../supergroups/lightweight_rca_cluster.py | 5 ----- .../test_lightweight_rca_cluster.py | 18 ++++-------------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/sentry/seer/supergroups/lightweight_rca_cluster.py b/src/sentry/seer/supergroups/lightweight_rca_cluster.py index 8014c29244d011..a1d36e810c99bc 100644 --- a/src/sentry/seer/supergroups/lightweight_rca_cluster.py +++ b/src/sentry/seer/supergroups/lightweight_rca_cluster.py @@ -2,7 +2,6 @@ import logging -from sentry import options from sentry.api.serializers import EventSerializer, serialize from sentry.eventstore import backend as eventstore from sentry.models.group import Group @@ -23,10 +22,6 @@ def trigger_lightweight_rca_cluster(group: Group) -> None: Sends issue event data to Seer, which generates a lightweight root cause analysis and clusters the issue into supergroups based on embedding similarity. """ - enabled_orgs: list[int] = options.get("supergroups.lightweight-enabled-orgs") - if group.organization.id not in enabled_orgs: - return - event = group.get_recommended_event_for_environments() if not event: event = group.get_latest_event() diff --git a/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py b/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py index c19ee92fcc358d..ff285aaeff0db8 100644 --- a/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py +++ b/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py @@ -19,8 +19,7 @@ def setUp(self): def test_calls_seer_with_correct_payload(self, mock_request): mock_request.return_value.status = 200 - with self.options({"supergroups.lightweight-enabled-orgs": [self.group.organization.id]}): - trigger_lightweight_rca_cluster(self.group) + trigger_lightweight_rca_cluster(self.group) mock_request.assert_called_once() body = mock_request.call_args.args[0] @@ -33,27 +32,18 @@ def test_calls_seer_with_correct_payload(self, mock_request): assert "events" in body["issue"] assert len(body["issue"]["events"]) == 1 - @patch("sentry.seer.supergroups.lightweight_rca_cluster.make_lightweight_rca_cluster_request") - def test_skips_when_org_not_enabled(self, mock_request): - with self.options({"supergroups.lightweight-enabled-orgs": []}): - trigger_lightweight_rca_cluster(self.group) - - mock_request.assert_not_called() - @patch("sentry.seer.supergroups.lightweight_rca_cluster.make_lightweight_rca_cluster_request") def test_raises_on_seer_error(self, mock_request): mock_request.return_value.status = 500 - with self.options({"supergroups.lightweight-enabled-orgs": [self.group.organization.id]}): - with pytest.raises(Exception): - trigger_lightweight_rca_cluster(self.group) + with pytest.raises(Exception): + trigger_lightweight_rca_cluster(self.group) @patch("sentry.seer.supergroups.lightweight_rca_cluster.make_lightweight_rca_cluster_request") def test_passes_viewer_context(self, mock_request): mock_request.return_value.status = 200 - with self.options({"supergroups.lightweight-enabled-orgs": [self.group.organization.id]}): - trigger_lightweight_rca_cluster(self.group) + trigger_lightweight_rca_cluster(self.group) call_kwargs = mock_request.call_args.kwargs assert call_kwargs["viewer_context"]["organization_id"] == self.group.organization.id From 2b9b85522f9df2febe8f94439575a89112356432 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Mon, 6 Apr 2026 17:05:46 -0700 Subject: [PATCH 08/11] ref(seer): Address PR review feedback for lightweight RCA cluster - Remove viewer_context test (not tested on other seer endpoints) - Switch to get_latest_event() since this runs on new issue creation - Change task namespace to ingest_errors_postprocess_tasks - Add post_process pipeline tests verifying task dispatch gating: dispatched when org enabled + new issue, skipped otherwise --- .../supergroups/lightweight_rca_cluster.py | 5 +- .../tasks/seer/lightweight_rca_cluster.py | 4 +- .../test_lightweight_rca_cluster.py | 9 ---- tests/sentry/tasks/test_post_process.py | 54 +++++++++++++++++++ 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/sentry/seer/supergroups/lightweight_rca_cluster.py b/src/sentry/seer/supergroups/lightweight_rca_cluster.py index a1d36e810c99bc..e8920463d9985b 100644 --- a/src/sentry/seer/supergroups/lightweight_rca_cluster.py +++ b/src/sentry/seer/supergroups/lightweight_rca_cluster.py @@ -22,10 +22,7 @@ def trigger_lightweight_rca_cluster(group: Group) -> None: Sends issue event data to Seer, which generates a lightweight root cause analysis and clusters the issue into supergroups based on embedding similarity. """ - event = group.get_recommended_event_for_environments() - if not event: - event = group.get_latest_event() - + event = group.get_latest_event() if not event: logger.info( "lightweight_rca_cluster.no_event", diff --git a/src/sentry/tasks/seer/lightweight_rca_cluster.py b/src/sentry/tasks/seer/lightweight_rca_cluster.py index 81c52250dca360..5c6e030b784a45 100644 --- a/src/sentry/tasks/seer/lightweight_rca_cluster.py +++ b/src/sentry/tasks/seer/lightweight_rca_cluster.py @@ -2,14 +2,14 @@ from sentry.models.group import Group from sentry.tasks.base import instrumented_task -from sentry.taskworker.namespaces import issues_tasks +from sentry.taskworker.namespaces import ingest_errors_postprocess_tasks logger = logging.getLogger(__name__) @instrumented_task( name="sentry.tasks.seer.lightweight_rca_cluster.trigger_lightweight_rca_cluster_task", - namespace=issues_tasks, + namespace=ingest_errors_postprocess_tasks, ) def trigger_lightweight_rca_cluster_task(group_id: int, **kwargs) -> None: from sentry.seer.supergroups.lightweight_rca_cluster import trigger_lightweight_rca_cluster diff --git a/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py b/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py index ff285aaeff0db8..956c3df75ff7ac 100644 --- a/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py +++ b/tests/sentry/seer/supergroups/test_lightweight_rca_cluster.py @@ -38,12 +38,3 @@ def test_raises_on_seer_error(self, mock_request): with pytest.raises(Exception): trigger_lightweight_rca_cluster(self.group) - - @patch("sentry.seer.supergroups.lightweight_rca_cluster.make_lightweight_rca_cluster_request") - def test_passes_viewer_context(self, mock_request): - mock_request.return_value.status = 200 - - trigger_lightweight_rca_cluster(self.group) - - call_kwargs = mock_request.call_args.kwargs - assert call_kwargs["viewer_context"]["organization_id"] == self.group.organization.id diff --git a/tests/sentry/tasks/test_post_process.py b/tests/sentry/tasks/test_post_process.py index ef745e87d12cae..9ee29b5d7fc7b1 100644 --- a/tests/sentry/tasks/test_post_process.py +++ b/tests/sentry/tasks/test_post_process.py @@ -3070,6 +3070,59 @@ def test_kick_off_seer_automation_with_hide_ai_features_enabled( mock_generate_summary_and_run_automation.assert_not_called() +class KickOffLightweightRCAClusterTestMixin(BasePostProcessGroupMixin): + @patch("sentry.tasks.seer.lightweight_rca_cluster.trigger_lightweight_rca_cluster_task.delay") + def test_kick_off_lightweight_rca_cluster_when_enabled(self, mock_task): + event = self.create_event( + data={"message": "testing"}, + project_id=self.project.id, + ) + + with self.options({"supergroups.lightweight-enabled-orgs": [self.project.organization.id]}): + self.call_post_process_group( + is_new=True, + is_regression=False, + is_new_group_environment=True, + event=event, + ) + + mock_task.assert_called_once_with(event.group.id) + + @patch("sentry.tasks.seer.lightweight_rca_cluster.trigger_lightweight_rca_cluster_task.delay") + def test_kick_off_lightweight_rca_cluster_skips_when_not_enabled(self, mock_task): + event = self.create_event( + data={"message": "testing"}, + 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, + ) + + mock_task.assert_not_called() + + @patch("sentry.tasks.seer.lightweight_rca_cluster.trigger_lightweight_rca_cluster_task.delay") + def test_kick_off_lightweight_rca_cluster_skips_when_not_new(self, mock_task): + event = self.create_event( + data={"message": "testing"}, + project_id=self.project.id, + ) + + with self.options({"supergroups.lightweight-enabled-orgs": [self.project.organization.id]}): + self.call_post_process_group( + is_new=False, + is_regression=False, + is_new_group_environment=False, + event=event, + ) + + mock_task.assert_not_called() + + @patch("sentry.seer.autofix.utils.is_seer_seat_based_tier_enabled", return_value=True) class TriageSignalsV0TestMixin(BasePostProcessGroupMixin): """Tests for the triage signals V0 flow.""" @@ -3497,6 +3550,7 @@ class PostProcessGroupErrorTest( InboxTestMixin, ResourceChangeBoundsTestMixin, KickOffSeerAutomationTestMixin, + KickOffLightweightRCAClusterTestMixin, TriageSignalsV0TestMixin, SeerAutomationHelperFunctionsTestMixin, WorkflowEngineTestMixin, From 87ecf749a3245e9658a45619b21ca1059ced5a28 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Tue, 7 Apr 2026 10:10:28 -0700 Subject: [PATCH 09/11] ref(seer): Move import to top level in lightweight RCA cluster task No circular dependency exists, so the function-level import is unnecessary. --- src/sentry/tasks/seer/lightweight_rca_cluster.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sentry/tasks/seer/lightweight_rca_cluster.py b/src/sentry/tasks/seer/lightweight_rca_cluster.py index 5c6e030b784a45..2154560db800f8 100644 --- a/src/sentry/tasks/seer/lightweight_rca_cluster.py +++ b/src/sentry/tasks/seer/lightweight_rca_cluster.py @@ -1,6 +1,7 @@ import logging from sentry.models.group import Group +from sentry.seer.supergroups.lightweight_rca_cluster import trigger_lightweight_rca_cluster from sentry.tasks.base import instrumented_task from sentry.taskworker.namespaces import ingest_errors_postprocess_tasks @@ -12,8 +13,6 @@ namespace=ingest_errors_postprocess_tasks, ) def trigger_lightweight_rca_cluster_task(group_id: int, **kwargs) -> None: - from sentry.seer.supergroups.lightweight_rca_cluster import trigger_lightweight_rca_cluster - try: group = Group.objects.get(id=group_id) except Group.DoesNotExist: From ce103c26aabd9c233c076612ef80c23ff5ed2c74 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Tue, 7 Apr 2026 11:13:09 -0700 Subject: [PATCH 10/11] fix(seer): Re-raise exceptions in lightweight RCA cluster task Log the exception for Sentry visibility, then re-raise so the task is marked as failed in monitoring. --- src/sentry/tasks/seer/lightweight_rca_cluster.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/tasks/seer/lightweight_rca_cluster.py b/src/sentry/tasks/seer/lightweight_rca_cluster.py index 2154560db800f8..49bdd249f99cb0 100644 --- a/src/sentry/tasks/seer/lightweight_rca_cluster.py +++ b/src/sentry/tasks/seer/lightweight_rca_cluster.py @@ -29,3 +29,4 @@ def trigger_lightweight_rca_cluster_task(group_id: int, **kwargs) -> None: "lightweight_rca_cluster_task.failed", extra={"group_id": group_id}, ) + raise From 4270ced26a68442d38a00b871bc5670e25341353 Mon Sep 17 00:00:00 2001 From: Yuval Mandelboum Date: Tue, 7 Apr 2026 15:04:51 -0700 Subject: [PATCH 11/11] ref(seer): Use ingest_errors_tasks namespace for lightweight RCA task This task makes an external API call with a 30s timeout, so it shouldn't run on the postprocess worker pool. Use ingest_errors_tasks to match generate_summary_and_run_automation which has the same dispatch pattern. --- src/sentry/tasks/seer/lightweight_rca_cluster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/tasks/seer/lightweight_rca_cluster.py b/src/sentry/tasks/seer/lightweight_rca_cluster.py index 49bdd249f99cb0..9d8abcbb4dc75a 100644 --- a/src/sentry/tasks/seer/lightweight_rca_cluster.py +++ b/src/sentry/tasks/seer/lightweight_rca_cluster.py @@ -3,14 +3,14 @@ from sentry.models.group import Group from sentry.seer.supergroups.lightweight_rca_cluster import trigger_lightweight_rca_cluster from sentry.tasks.base import instrumented_task -from sentry.taskworker.namespaces import ingest_errors_postprocess_tasks +from sentry.taskworker.namespaces import ingest_errors_tasks logger = logging.getLogger(__name__) @instrumented_task( name="sentry.tasks.seer.lightweight_rca_cluster.trigger_lightweight_rca_cluster_task", - namespace=ingest_errors_postprocess_tasks, + namespace=ingest_errors_tasks, ) def trigger_lightweight_rca_cluster_task(group_id: int, **kwargs) -> None: try: