Skip to content

Commit 9652a9e

Browse files
Mihir-Mavalankarandrewshie-sentry
authored andcommitted
feat(triage signals): Fixability based stopping point for autofix (#103076)
## PR Details + Changing the logic to set the default stopping point of Autofix based on fixability score. The UI stopping point will still override this until the Settings UI is changed. + Using this feature flag to just try out in the Seer org: https://github.com/getsentry/sentry-options-automator/blob/main/options/default/flagpole.yaml#L3207 + More details in triage signals implementation document: https://www.notion.so/sentry/Triage-Signals-V0-Technical-Implementation-Details-2a18b10e4b5d8086a7ceddaf4194849a?source=copy_link#2a48b10e4b5d8064a52ac6b519630fc0
1 parent 87a4ea3 commit 9652a9e

File tree

3 files changed

+140
-5
lines changed

3 files changed

+140
-5
lines changed

src/sentry/features/temporary.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
485485
manager.add("organizations:transaction-name-mark-scrubbed-as-sanitized", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, default=True, api_expose=False)
486486
# Normalize URL transaction names during ingestion.
487487
manager.add("organizations:transaction-name-normalize", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, default=True, api_expose=False)
488-
# Enbale Triage signals V0 for AI powered issue classifiaction in sentry
489-
manager.add("organizations:triage-signals-v0", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
490488
# Enables unlimited auto-triggered autofix runs
491489
manager.add("organizations:unlimited-auto-triggered-autofix-runs", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
492490
# Enables view hierarchy attachment scrubbing
@@ -659,6 +657,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
659657
manager.add("projects:plugins", ProjectPluginFeature, FeatureHandlerStrategy.INTERNAL, default=True, api_expose=True)
660658
# Enables experimental span v2 processing in Relay.
661659
manager.add("projects:span-v2-experimental-processing", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
660+
# Enbale Triage signals V0 for AI powered issue classifiaction in sentry
661+
manager.add("projects:triage-signals-v0", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
662662

663663
manager.add("projects:profiling-ingest-unsampled-profiles", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
664664

src/sentry/seer/autofix/issue_summary.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
FixabilityScoreThresholds,
2525
SeerAutomationSource,
2626
)
27-
from sentry.seer.autofix.utils import get_autofix_state, is_seer_autotriggered_autofix_rate_limited
27+
from sentry.seer.autofix.utils import (
28+
AutofixStoppingPoint,
29+
get_autofix_state,
30+
is_seer_autotriggered_autofix_rate_limited,
31+
)
2832
from sentry.seer.models import SummarizeIssueResponse
2933
from sentry.seer.seer_setup import get_seer_org_acknowledgement
3034
from sentry.seer.signed_seer_api import make_signed_seer_api_request, sign_with_seer_secret
@@ -48,13 +52,31 @@
4852
}
4953

5054

55+
def _get_stopping_point_from_fixability(fixability_score: float) -> AutofixStoppingPoint | None:
56+
"""
57+
Determine the autofix stopping point based on fixability score.
58+
"""
59+
if fixability_score < FixabilityScoreThresholds.MEDIUM.value:
60+
return None
61+
elif fixability_score < FixabilityScoreThresholds.HIGH.value:
62+
return AutofixStoppingPoint.SOLUTION
63+
else:
64+
return AutofixStoppingPoint.CODE_CHANGES
65+
66+
5167
@instrumented_task(
5268
name="sentry.tasks.autofix.trigger_autofix_from_issue_summary",
5369
namespace=seer_tasks,
5470
processing_deadline_duration=65,
5571
retry=Retry(times=1),
5672
)
57-
def _trigger_autofix_task(group_id: int, event_id: str, user_id: int | None, auto_run_source: str):
73+
def _trigger_autofix_task(
74+
group_id: int,
75+
event_id: str,
76+
user_id: int | None,
77+
auto_run_source: str,
78+
stopping_point: AutofixStoppingPoint | None = None,
79+
):
5880
"""
5981
Asynchronous task to trigger Autofix.
6082
"""
@@ -82,6 +104,7 @@ def _trigger_autofix_task(group_id: int, event_id: str, user_id: int | None, aut
82104
event_id=event_id,
83105
user=user,
84106
auto_run_source=auto_run_source,
107+
stopping_point=stopping_point,
85108
)
86109

87110

@@ -252,11 +275,16 @@ def _run_automation(
252275
if is_rate_limited:
253276
return
254277

278+
stopping_point = None
279+
if features.has("projects:triage-signals-v0", group.project):
280+
stopping_point = _get_stopping_point_from_fixability(issue_summary.scores.fixability_score)
281+
255282
_trigger_autofix_task.delay(
256283
group_id=group.id,
257284
event_id=event.event_id,
258285
user_id=user_id,
259286
auto_run_source=auto_run_source,
287+
stopping_point=stopping_point,
260288
)
261289

262290

tests/sentry/seer/autofix/test_issue_summary.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@
1212
from sentry.issues.ingest import save_issue_occurrence
1313
from sentry.locks import locks
1414
from sentry.seer.autofix.constants import SeerAutomationSource
15-
from sentry.seer.autofix.issue_summary import _call_seer, _get_event, get_issue_summary
15+
from sentry.seer.autofix.issue_summary import (
16+
_call_seer,
17+
_get_event,
18+
_get_stopping_point_from_fixability,
19+
_run_automation,
20+
get_issue_summary,
21+
)
22+
from sentry.seer.autofix.utils import AutofixStoppingPoint
1623
from sentry.seer.models import SummarizeIssueResponse, SummarizeIssueScores
1724
from sentry.testutils.cases import APITestCase, SnubaTestCase
1825
from sentry.testutils.helpers.datetime import before_now
@@ -682,3 +689,103 @@ def test_get_issue_summary_handles_trace_tree_errors(
682689

683690
assert status_code == 200
684691
mock_call_seer.assert_called_once_with(self.group, serialized_event, None)
692+
693+
694+
class TestGetStoppingPointFromFixability:
695+
@pytest.mark.parametrize(
696+
"score,expected",
697+
[
698+
(0.0, None),
699+
(0.39, None),
700+
(0.40, AutofixStoppingPoint.SOLUTION),
701+
(0.65, AutofixStoppingPoint.SOLUTION),
702+
(0.66, AutofixStoppingPoint.CODE_CHANGES),
703+
(1.0, AutofixStoppingPoint.CODE_CHANGES),
704+
],
705+
)
706+
def test_stopping_point_mapping(self, score, expected):
707+
assert _get_stopping_point_from_fixability(score) == expected
708+
709+
710+
@with_feature({"organizations:gen-ai-features": True, "projects:triage-signals-v0": True})
711+
class TestRunAutomationStoppingPoint(APITestCase, SnubaTestCase):
712+
def setUp(self) -> None:
713+
super().setUp()
714+
self.group = self.create_group()
715+
event_data = load_data("python")
716+
self.event = self.store_event(data=event_data, project_id=self.project.id)
717+
718+
@patch("sentry.seer.autofix.issue_summary._trigger_autofix_task.delay")
719+
@patch(
720+
"sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited",
721+
return_value=False,
722+
)
723+
@patch("sentry.seer.autofix.issue_summary.get_autofix_state", return_value=None)
724+
@patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True)
725+
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
726+
def test_high_fixability_code_changes(
727+
self, mock_gen, mock_budget, mock_state, mock_rate, mock_trigger
728+
):
729+
self.project.update_option("sentry:autofix_automation_tuning", "always")
730+
mock_gen.return_value = SummarizeIssueResponse(
731+
group_id=str(self.group.id),
732+
headline="h",
733+
whats_wrong="w",
734+
trace="t",
735+
possible_cause="c",
736+
scores=SummarizeIssueScores(fixability_score=0.80),
737+
)
738+
_run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT)
739+
mock_trigger.assert_called_once()
740+
assert mock_trigger.call_args[1]["stopping_point"] == AutofixStoppingPoint.CODE_CHANGES
741+
742+
@patch("sentry.seer.autofix.issue_summary._trigger_autofix_task.delay")
743+
@patch(
744+
"sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited",
745+
return_value=False,
746+
)
747+
@patch("sentry.seer.autofix.issue_summary.get_autofix_state", return_value=None)
748+
@patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True)
749+
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
750+
def test_medium_fixability_solution(
751+
self, mock_gen, mock_budget, mock_state, mock_rate, mock_trigger
752+
):
753+
self.project.update_option("sentry:autofix_automation_tuning", "always")
754+
mock_gen.return_value = SummarizeIssueResponse(
755+
group_id=str(self.group.id),
756+
headline="h",
757+
whats_wrong="w",
758+
trace="t",
759+
possible_cause="c",
760+
scores=SummarizeIssueScores(fixability_score=0.50),
761+
)
762+
_run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT)
763+
mock_trigger.assert_called_once()
764+
assert mock_trigger.call_args[1]["stopping_point"] == AutofixStoppingPoint.SOLUTION
765+
766+
@patch("sentry.seer.autofix.issue_summary._trigger_autofix_task.delay")
767+
@patch(
768+
"sentry.seer.autofix.issue_summary.is_seer_autotriggered_autofix_rate_limited",
769+
return_value=False,
770+
)
771+
@patch("sentry.seer.autofix.issue_summary.get_autofix_state", return_value=None)
772+
@patch("sentry.quotas.backend.has_available_reserved_budget", return_value=True)
773+
@patch("sentry.seer.autofix.issue_summary._generate_fixability_score")
774+
def test_without_feature_flag(self, mock_gen, mock_budget, mock_state, mock_rate, mock_trigger):
775+
self.project.update_option("sentry:autofix_automation_tuning", "always")
776+
mock_gen.return_value = SummarizeIssueResponse(
777+
group_id=str(self.group.id),
778+
headline="h",
779+
whats_wrong="w",
780+
trace="t",
781+
possible_cause="c",
782+
scores=SummarizeIssueScores(fixability_score=0.80),
783+
)
784+
785+
with self.feature(
786+
{"organizations:gen-ai-features": True, "projects:triage-signals-v0": False}
787+
):
788+
_run_automation(self.group, self.user, self.event, SeerAutomationSource.ALERT)
789+
790+
mock_trigger.assert_called_once()
791+
assert mock_trigger.call_args[1]["stopping_point"] is None

0 commit comments

Comments
 (0)