Skip to content

Commit 699b63a

Browse files
feat(triage signals): Remove fixability from run_automation and put into _generate_summary (#103485)
## PR Details + Removing fixability from run_automation to enable calling run_automation directly as discussed in meeting with @kddubey @roaga . + Updated the diagram too: https://miro.com/app/board/uXjVJqn1-fQ=/?focusWidget=3458764648325594380 + Currently automation flow is unchanged. Fixability is still called before run_automation and after issue summary. --------- Co-authored-by: Rohan Agarwal <[email protected]>
1 parent 498dbfc commit 699b63a

File tree

2 files changed

+158
-83
lines changed

2 files changed

+158
-83
lines changed

src/sentry/seer/autofix/issue_summary.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -314,18 +314,13 @@ def _run_automation(
314314
}
315315
)
316316

317-
with sentry_sdk.start_span(op="ai_summary.generate_fixability_score"):
318-
issue_summary = _generate_fixability_score(group)
319-
320-
if not issue_summary.scores:
321-
raise ValueError("Issue summary scores is None or empty.")
322-
if issue_summary.scores.fixability_score is None:
323-
raise ValueError("Issue summary fixability score is None.")
324-
325-
group.update(seer_fixability_score=issue_summary.scores.fixability_score)
317+
fixability_score = group.seer_fixability_score
318+
if fixability_score is None:
319+
logger.error("Fixability score is not available for group %s", group.id)
320+
return
326321

327322
if (
328-
not _is_issue_fixable(group, issue_summary.scores.fixability_score)
323+
not _is_issue_fixable(group, fixability_score)
329324
and not group.issue_type.always_trigger_seer_automation
330325
):
331326
return
@@ -347,9 +342,7 @@ def _run_automation(
347342

348343
stopping_point = None
349344
if features.has("projects:triage-signals-v0", group.project):
350-
fixability_stopping_point = _get_stopping_point_from_fixability(
351-
issue_summary.scores.fixability_score
352-
)
345+
fixability_stopping_point = _get_stopping_point_from_fixability(fixability_score)
353346
logger.info("Fixability-based stopping point: %s", fixability_stopping_point)
354347

355348
# Fetch user preference and apply as upper bound
@@ -401,12 +394,35 @@ def _generate_summary(
401394
trace_tree,
402395
)
403396

404-
if should_run_automation:
397+
if source != SeerAutomationSource.ISSUE_DETAILS and group.seer_fixability_score is None:
405398
try:
406-
_run_automation(group, user, event, source)
399+
with sentry_sdk.start_span(op="ai_summary.generate_fixability_score"):
400+
fixability_response = _generate_fixability_score(group)
401+
402+
if not fixability_response.scores:
403+
raise ValueError("Issue summary scores is None or empty.")
404+
if fixability_response.scores.fixability_score is None:
405+
raise ValueError("Issue summary fixability score is None.")
406+
407+
group.update(seer_fixability_score=fixability_response.scores.fixability_score)
407408
except Exception:
408409
logger.exception(
409-
"Error auto-triggering autofix from issue summary", extra={"group_id": group.id}
410+
"Error generating fixability score in summary", extra={"group_id": group.id}
411+
)
412+
413+
if should_run_automation:
414+
if group.seer_fixability_score is not None:
415+
try:
416+
_run_automation(group, user, event, source)
417+
except Exception:
418+
logger.exception(
419+
"Error auto-triggering autofix from issue summary", extra={"group_id": group.id}
420+
)
421+
else:
422+
logger.error(
423+
"Skipping automation: fixability score unavailable for group %s",
424+
group.id,
425+
extra={"group_id": group.id},
410426
)
411427

412428
summary_dict = issue_summary.dict()

tests/sentry/seer/autofix/test_issue_summary.py

Lines changed: 126 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -641,11 +641,16 @@ def test_get_issue_summary_continues_when_automation_fails(
641641
)
642642
mock_call_seer.return_value = mock_summary
643643

644+
# Set fixability score so _run_automation will be called
645+
self.group.update(seer_fixability_score=0.75)
646+
644647
# Make _run_automation raise an exception
645648
mock_run_automation.side_effect = Exception("Automation failed")
646649

647-
# Call get_issue_summary and verify it still returns successfully
648-
summary_data, status_code = get_issue_summary(self.group, self.user)
650+
# Call get_issue_summary with a source that triggers automation
651+
summary_data, status_code = get_issue_summary(
652+
self.group, self.user, source=SeerAutomationSource.POST_PROCESS
653+
)
649654

650655
assert status_code == 200
651656
expected_response = mock_summary.dict()
@@ -750,6 +755,105 @@ def test_get_issue_summary_with_should_run_automation_false(
750755
cached_summary = cache.get(f"ai-group-summary-v2:{self.group.id}")
751756
assert cached_summary == expected_response_summary
752757

758+
@patch("sentry.seer.autofix.issue_summary.get_seer_org_acknowledgement")
759+
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
760+
@patch("sentry.seer.autofix.issue_summary._get_trace_tree_for_event")
761+
@patch("sentry.seer.autofix.issue_summary._call_seer")
762+
@patch("sentry.seer.autofix.issue_summary._get_event")
763+
def test_generate_summary_fixability_generation(
764+
self,
765+
mock_get_event,
766+
mock_call_seer,
767+
mock_get_trace_tree,
768+
mock_generate_fixability,
769+
mock_get_acknowledgement,
770+
):
771+
"""Test fixability generation: creates when missing, skips when exists."""
772+
mock_get_acknowledgement.return_value = True
773+
event = Mock(event_id="test_event_id", datetime=datetime.datetime.now())
774+
serialized_event = {"event_id": "test_event_id", "data": "test_event_data"}
775+
mock_get_event.return_value = [serialized_event, event]
776+
mock_summary = SummarizeIssueResponse(
777+
group_id=str(self.group.id),
778+
headline="Test headline",
779+
whats_wrong="Test whats wrong",
780+
trace="Test trace",
781+
possible_cause="Test possible cause",
782+
)
783+
mock_call_seer.return_value = mock_summary
784+
mock_get_trace_tree.return_value = None
785+
mock_generate_fixability.return_value = SummarizeIssueResponse(
786+
group_id=str(self.group.id),
787+
headline="h",
788+
whats_wrong="w",
789+
trace="t",
790+
possible_cause="c",
791+
scores=SummarizeIssueScores(fixability_score=0.75),
792+
)
793+
794+
# Test 1: Generates fixability when missing
795+
assert self.group.seer_fixability_score is None
796+
get_issue_summary(
797+
self.group,
798+
self.user,
799+
source=SeerAutomationSource.POST_PROCESS,
800+
should_run_automation=False,
801+
)
802+
mock_generate_fixability.assert_called_once_with(self.group)
803+
self.group.refresh_from_db()
804+
assert self.group.seer_fixability_score == 0.75
805+
806+
# Test 2: Skips fixability when already exists
807+
mock_generate_fixability.reset_mock()
808+
cache.delete(f"ai-group-summary-v2:{self.group.id}")
809+
get_issue_summary(
810+
self.group,
811+
self.user,
812+
source=SeerAutomationSource.POST_PROCESS,
813+
should_run_automation=False,
814+
)
815+
mock_generate_fixability.assert_not_called()
816+
817+
@patch("sentry.seer.autofix.issue_summary.get_seer_org_acknowledgement")
818+
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
819+
@patch("sentry.seer.autofix.issue_summary._get_trace_tree_for_event")
820+
@patch("sentry.seer.autofix.issue_summary._call_seer")
821+
@patch("sentry.seer.autofix.issue_summary._get_event")
822+
def test_generate_summary_continues_when_fixability_fails(
823+
self,
824+
mock_get_event,
825+
mock_call_seer,
826+
mock_get_trace_tree,
827+
mock_generate_fixability,
828+
mock_get_acknowledgement,
829+
):
830+
"""Test that summary is still cached when fixability generation fails."""
831+
mock_get_acknowledgement.return_value = True
832+
event = Mock(event_id="test_event_id", datetime=datetime.datetime.now())
833+
serialized_event = {"event_id": "test_event_id", "data": "test_event_data"}
834+
mock_get_event.return_value = [serialized_event, event]
835+
mock_summary = SummarizeIssueResponse(
836+
group_id=str(self.group.id),
837+
headline="Test headline",
838+
whats_wrong="Test whats wrong",
839+
trace="Test trace",
840+
possible_cause="Test possible cause",
841+
)
842+
mock_call_seer.return_value = mock_summary
843+
mock_get_trace_tree.return_value = None
844+
mock_generate_fixability.side_effect = Exception("Fixability service down")
845+
846+
summary_data, status_code = get_issue_summary(
847+
self.group,
848+
self.user,
849+
source=SeerAutomationSource.POST_PROCESS,
850+
should_run_automation=False,
851+
)
852+
853+
assert status_code == 200
854+
assert summary_data["headline"] == "Test headline"
855+
mock_generate_fixability.assert_called_once()
856+
753857

754858
class TestGetStoppingPointFromFixability:
755859
@pytest.mark.parametrize(
@@ -785,22 +889,12 @@ def setUp(self) -> None:
785889
)
786890
@patch("sentry.seer.autofix.issue_summary.get_autofix_state", return_value=None)
787891
@patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True)
788-
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
789-
def test_high_fixability_code_changes(
790-
self, mock_gen, mock_budget, mock_state, mock_rate, mock_trigger
791-
):
892+
def test_high_fixability_code_changes(self, mock_budget, mock_state, mock_rate, mock_trigger):
792893
self.project.update_option("sentry:autofix_automation_tuning", "always")
793-
mock_gen.return_value = SummarizeIssueResponse(
794-
group_id=str(self.group.id),
795-
headline="h",
796-
whats_wrong="w",
797-
trace="t",
798-
possible_cause="c",
799-
scores=SummarizeIssueScores(fixability_score=0.70),
800-
)
894+
self.group.update(seer_fixability_score=0.80)
801895
_run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT)
802896
mock_trigger.assert_called_once()
803-
assert mock_trigger.call_args[1]["stopping_point"] == AutofixStoppingPoint.CODE_CHANGES
897+
assert mock_trigger.call_args[1]["stopping_point"] == AutofixStoppingPoint.OPEN_PR
804898

805899
@patch("sentry.seer.autofix.issue_summary._trigger_autofix_task.delay")
806900
@patch(
@@ -809,19 +903,9 @@ def test_high_fixability_code_changes(
809903
)
810904
@patch("sentry.seer.autofix.issue_summary.get_autofix_state", return_value=None)
811905
@patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True)
812-
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
813-
def test_medium_fixability_solution(
814-
self, mock_gen, mock_budget, mock_state, mock_rate, mock_trigger
815-
):
906+
def test_medium_fixability_solution(self, mock_budget, mock_state, mock_rate, mock_trigger):
816907
self.project.update_option("sentry:autofix_automation_tuning", "always")
817-
mock_gen.return_value = SummarizeIssueResponse(
818-
group_id=str(self.group.id),
819-
headline="h",
820-
whats_wrong="w",
821-
trace="t",
822-
possible_cause="c",
823-
scores=SummarizeIssueScores(fixability_score=0.50),
824-
)
908+
self.group.update(seer_fixability_score=0.50)
825909
_run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT)
826910
mock_trigger.assert_called_once()
827911
assert mock_trigger.call_args[1]["stopping_point"] == AutofixStoppingPoint.SOLUTION
@@ -833,17 +917,9 @@ def test_medium_fixability_solution(
833917
)
834918
@patch("sentry.seer.autofix.issue_summary.get_autofix_state", return_value=None)
835919
@patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True)
836-
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
837-
def test_without_feature_flag(self, mock_gen, mock_budget, mock_state, mock_rate, mock_trigger):
920+
def test_without_feature_flag(self, mock_budget, mock_state, mock_rate, mock_trigger):
838921
self.project.update_option("sentry:autofix_automation_tuning", "always")
839-
mock_gen.return_value = SummarizeIssueResponse(
840-
group_id=str(self.group.id),
841-
headline="h",
842-
whats_wrong="w",
843-
trace="t",
844-
possible_cause="c",
845-
scores=SummarizeIssueScores(fixability_score=0.80),
846-
)
922+
self.group.update(seer_fixability_score=0.80)
847923

848924
with self.feature(
849925
{"organizations:gen-ai-features": True, "projects:triage-signals-v0": False}
@@ -853,6 +929,13 @@ def test_without_feature_flag(self, mock_gen, mock_budget, mock_state, mock_rate
853929
mock_trigger.assert_called_once()
854930
assert mock_trigger.call_args[1]["stopping_point"] is None
855931

932+
@patch("sentry.seer.autofix.issue_summary._trigger_autofix_task.delay")
933+
def test_missing_fixability_score_returns_early(self, mock_trigger):
934+
"""Test that _run_automation returns early when fixability score is None."""
935+
assert self.group.seer_fixability_score is None
936+
_run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT)
937+
mock_trigger.assert_not_called()
938+
856939

857940
class TestFetchUserPreference:
858941
@patch("sentry.seer.autofix.issue_summary.sign_with_seer_secret", return_value={})
@@ -985,20 +1068,12 @@ def setUp(self) -> None:
9851068
)
9861069
@patch("sentry.seer.autofix.issue_summary.get_autofix_state", return_value=None)
9871070
@patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True)
988-
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
9891071
def test_user_preference_limits_high_fixability(
990-
self, mock_gen, mock_budget, mock_state, mock_rate, mock_fetch, mock_trigger
1072+
self, mock_budget, mock_state, mock_rate, mock_fetch, mock_trigger
9911073
):
9921074
"""High fixability (OPEN_PR) limited by user preference (SOLUTION)"""
9931075
self.project.update_option("sentry:autofix_automation_tuning", "always")
994-
mock_gen.return_value = SummarizeIssueResponse(
995-
group_id=str(self.group.id),
996-
headline="h",
997-
whats_wrong="w",
998-
trace="t",
999-
possible_cause="c",
1000-
scores=SummarizeIssueScores(fixability_score=0.80), # High = OPEN_PR
1001-
)
1076+
self.group.update(seer_fixability_score=0.80)
10021077
mock_fetch.return_value = "solution"
10031078

10041079
_run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT)
@@ -1015,20 +1090,12 @@ def test_user_preference_limits_high_fixability(
10151090
)
10161091
@patch("sentry.seer.autofix.issue_summary.get_autofix_state", return_value=None)
10171092
@patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True)
1018-
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
10191093
def test_fixability_limits_permissive_user_preference(
1020-
self, mock_gen, mock_budget, mock_state, mock_rate, mock_fetch, mock_trigger
1094+
self, mock_budget, mock_state, mock_rate, mock_fetch, mock_trigger
10211095
):
10221096
"""Medium fixability (SOLUTION) used despite user allowing OPEN_PR"""
10231097
self.project.update_option("sentry:autofix_automation_tuning", "always")
1024-
mock_gen.return_value = SummarizeIssueResponse(
1025-
group_id=str(self.group.id),
1026-
headline="h",
1027-
whats_wrong="w",
1028-
trace="t",
1029-
possible_cause="c",
1030-
scores=SummarizeIssueScores(fixability_score=0.50), # Medium = SOLUTION
1031-
)
1098+
self.group.update(seer_fixability_score=0.50)
10321099
mock_fetch.return_value = "open_pr"
10331100

10341101
_run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT)
@@ -1045,20 +1112,12 @@ def test_fixability_limits_permissive_user_preference(
10451112
)
10461113
@patch("sentry.seer.autofix.issue_summary.get_autofix_state", return_value=None)
10471114
@patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True)
1048-
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
10491115
def test_no_user_preference_uses_fixability_only(
1050-
self, mock_gen, mock_budget, mock_state, mock_rate, mock_fetch, mock_trigger
1116+
self, mock_budget, mock_state, mock_rate, mock_fetch, mock_trigger
10511117
):
10521118
"""When user has no preference, use fixability score alone"""
10531119
self.project.update_option("sentry:autofix_automation_tuning", "always")
1054-
mock_gen.return_value = SummarizeIssueResponse(
1055-
group_id=str(self.group.id),
1056-
headline="h",
1057-
whats_wrong="w",
1058-
trace="t",
1059-
possible_cause="c",
1060-
scores=SummarizeIssueScores(fixability_score=0.80), # High = OPEN_PR
1061-
)
1120+
self.group.update(seer_fixability_score=0.80)
10621121
mock_fetch.return_value = None
10631122

10641123
_run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT)

0 commit comments

Comments
 (0)