Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("organizations:new-replay-processing", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)

manager.add("projects:project-detail-apple-app-hang-rate", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable supergroup RCA embedding generation from autofix explorer runs
manager.add("projects:supergroup-embeddings-explorer", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
Copy link
Copy Markdown
Contributor Author

@cvxluo cvxluo Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i went with project-level granularity since we want to test on specific projects internally, rather than the entire sentry org. makes it slightly easier to track any issues that come up right now

# fmt: on

# Partner oauth
Expand Down
51 changes: 51 additions & 0 deletions src/sentry/seer/autofix/on_completion_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
from typing import TYPE_CHECKING

from sentry import features
from sentry.models.group import Group
from sentry.models.organization import Organization
from sentry.seer.autofix.autofix_agent import (
Expand All @@ -19,6 +20,7 @@
SeerApiResponseValidationError,
SeerAutomationHandoffConfiguration,
)
from sentry.seer.supergroups import trigger_supergroups_embedding
from sentry.sentry_apps.tasks.sentry_apps import broadcast_webhooks_for_organization
from sentry.sentry_apps.utils.webhooks import SeerActionType

Expand Down Expand Up @@ -75,6 +77,10 @@ def execute(cls, organization: Organization, run_id: int) -> None:
# Send webhook for the completed step
cls._send_step_webhook(organization, run_id, artifacts, state)

current_step = cls._get_current_step(artifacts, state)
if current_step == AutofixStep.ROOT_CAUSE:
cls._maybe_trigger_supergroups_embedding(organization, run_id, state, artifacts)

# Continue the automated pipeline if stopping_point hasn't been reached
cls._maybe_continue_pipeline(organization, run_id, state, artifacts)

Expand Down Expand Up @@ -153,6 +159,51 @@ def _send_step_webhook(
},
)

@classmethod
def _maybe_trigger_supergroups_embedding(
cls,
organization: Organization,
run_id: int,
state: SeerRunState,
artifacts: dict[str, Artifact],
) -> None:
"""Trigger supergroups embedding if feature flag is enabled."""
group_id = state.metadata.get("group_id") if state.metadata else None
if group_id is None:
return

try:
group = Group.objects.get(id=group_id)
except Group.DoesNotExist:
logger.warning(
"autofix.supergroup_embedding.group_not_found",
extra={"group_id": group_id},
)
return

if not features.has("projects:supergroup-embeddings-explorer", group.project):
return

root_cause_artifact = artifacts.get("root_cause")
if not root_cause_artifact or not root_cause_artifact.data:
return

try:
trigger_supergroups_embedding(
organization_id=organization.id,
group_id=group_id,
artifact_data=root_cause_artifact.data,
)
except Exception:
logger.exception(
"autofix.on_completion_hook.supergroups_embedding_failed",
extra={
"run_id": run_id,
"organization_id": organization.id,
"group_id": group_id,
},
)

@classmethod
def _get_current_step(
cls, artifacts: dict[str, Artifact], state: SeerRunState
Expand Down
33 changes: 33 additions & 0 deletions src/sentry/seer/supergroups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

import orjson
import requests
from django.conf import settings

from sentry.seer.signed_seer_api import sign_with_seer_secret


def trigger_supergroups_embedding(
organization_id: int,
group_id: int,
artifact_data: dict,
) -> None:
path = "/v0/issues/supergroups"
body = orjson.dumps(
{
"organization_id": organization_id,
"group_id": group_id,
"artifact_data": artifact_data,
}
)

response = requests.post(
f"{settings.SEER_AUTOFIX_URL}{path}",
data=body,
headers={
"content-type": "application/json;charset=utf-8",
**sign_with_seer_secret(body),
},
timeout=5,
)
response.raise_for_status()
95 changes: 95 additions & 0 deletions tests/sentry/seer/autofix/test_autofix_on_completion_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
)
from sentry.sentry_apps.utils.webhooks import SeerActionType
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers.features import with_feature


class TestAutofixOnCompletionHookHelpers(TestCase):
Expand Down Expand Up @@ -328,6 +329,100 @@ def test_send_step_webhook_no_artifacts_no_webhook(self, mock_broadcast):
mock_broadcast.assert_not_called()


class TestAutofixOnCompletionHookSupergroups(TestCase):
"""Tests for supergroups embedding trigger in AutofixOnCompletionHook."""

def setUp(self):
super().setUp()
self.organization = self.create_organization()
self.project = self.create_project(organization=self.organization)
self.group = self.create_group(project=self.project)

@with_feature("projects:supergroup-embeddings-explorer")
@patch("sentry.seer.autofix.on_completion_hook.trigger_supergroups_embedding")
def test_triggers_embedding_on_root_cause(self, mock_trigger_sg):
"""Triggers supergroups embedding when root cause completes with feature flag enabled."""
artifact_data = {"one_line_description": "Null pointer in auth module"}
artifacts = {"root_cause": Artifact(key="root_cause", data=artifact_data, reason="test")}
state = MagicMock()
state.metadata = {"group_id": self.group.id}

AutofixOnCompletionHook._maybe_trigger_supergroups_embedding(
self.organization, 123, state, artifacts
)

mock_trigger_sg.assert_called_once_with(
organization_id=self.organization.id,
group_id=self.group.id,
artifact_data=artifact_data,
)

@patch("sentry.seer.autofix.on_completion_hook.trigger_supergroups_embedding")
def test_skips_embedding_when_flag_disabled(self, mock_trigger_sg):
"""Does not trigger supergroups embedding when feature flag is disabled."""
artifacts = {
"root_cause": Artifact(
key="root_cause", data={"one_line_description": "test"}, reason="test"
)
}
state = MagicMock()
state.metadata = {"group_id": self.group.id}

AutofixOnCompletionHook._maybe_trigger_supergroups_embedding(
self.organization, 123, state, artifacts
)

mock_trigger_sg.assert_not_called()

@patch("sentry.seer.autofix.on_completion_hook.trigger_supergroups_embedding")
def test_skips_embedding_when_no_group_id(self, mock_trigger_sg):
"""Does not trigger supergroups embedding when group_id is missing from metadata."""
artifacts = {
"root_cause": Artifact(
key="root_cause", data={"one_line_description": "test"}, reason="test"
)
}
state = MagicMock()
state.metadata = {}

AutofixOnCompletionHook._maybe_trigger_supergroups_embedding(
self.organization, 123, state, artifacts
)

mock_trigger_sg.assert_not_called()

@with_feature("projects:supergroup-embeddings-explorer")
@patch("sentry.seer.autofix.on_completion_hook.trigger_supergroups_embedding")
@patch("sentry.seer.autofix.on_completion_hook.broadcast_webhooks_for_organization.delay")
@patch("sentry.seer.autofix.on_completion_hook.fetch_run_status")
def test_skips_embedding_when_current_step_is_not_root_cause(
self, mock_fetch, mock_broadcast, mock_trigger_sg
):
"""Does not trigger embedding when current step is solution, not root cause."""
state = MagicMock()
state.metadata = {"group_id": self.group.id}
state.has_code_changes.return_value = (False, True)
state.get_artifacts.return_value = {
"root_cause": Artifact(
key="root_cause", data={"one_line_description": "test"}, reason="test"
),
"solution": Artifact(key="solution", data={"steps": []}, reason="test"),
}
state.blocks = [
MemoryBlock(
id="block_sol",
message=Message(message="test", role="tool_use"),
timestamp="2024-01-01T00:01:00Z",
artifacts=[Artifact(key="solution", data={"steps": []}, reason="test")],
),
]
mock_fetch.return_value = state

AutofixOnCompletionHook.execute(self.organization, 123)

mock_trigger_sg.assert_not_called()


class TestAutofixOnCompletionHookHandoff(TestCase):
"""Tests for coding agent handoff logic in AutofixOnCompletionHook."""

Expand Down
30 changes: 30 additions & 0 deletions tests/sentry/seer/test_supergroups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from unittest.mock import patch

import orjson
from django.conf import settings

from sentry.seer.supergroups import trigger_supergroups_embedding
from sentry.testutils.cases import TestCase


class TriggerSupergroupsEmbeddingTest(TestCase):
@patch("sentry.seer.supergroups.requests.post")
@patch("sentry.seer.supergroups.sign_with_seer_secret", return_value={})
def test_calls_seer_with_correct_payload(self, mock_sign, mock_post):
mock_post.return_value.raise_for_status.return_value = None

trigger_supergroups_embedding(
organization_id=1,
group_id=123,
artifact_data={"one_line_description": "Null pointer in auth module"},
)

mock_post.assert_called_once()
assert mock_post.call_args.args[0] == f"{settings.SEER_AUTOFIX_URL}/v0/issues/supergroups"
assert mock_post.call_args.kwargs["timeout"] == 5

mock_sign.assert_called_once()
payload = orjson.loads(mock_sign.call_args.args[0])
assert payload["organization_id"] == 1
assert payload["group_id"] == 123
assert payload["artifact_data"] == {"one_line_description": "Null pointer in auth module"}
Loading