Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(autofix): Log auto run & don't autorun if autofix exists #87299

Merged
merged 2 commits into from
Mar 18, 2025
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
28 changes: 19 additions & 9 deletions src/sentry/api/endpoints/group_ai_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from sentry.api.bases.group import GroupEndpoint
from sentry.api.serializers import EventSerializer, serialize
from sentry.api.serializers.rest_framework.base import convert_dict_key_case, snake_to_camel_case
from sentry.autofix.utils import get_autofix_state
from sentry.constants import ObjectStatus
from sentry.eventstore.models import Event, GroupEvent
from sentry.models.group import Group
Expand Down Expand Up @@ -240,15 +241,24 @@ def post(self, request: Request, group: Group) -> Response:
issue_summary = self._generate_fixability_score(group.id)

if issue_summary.scores.is_fixable:
with sentry_sdk.start_span(op="ai_summary.trigger_autofix"):
response = trigger_autofix(
group=group, event_id=event.event_id, user=request.user
)

if response.status_code != 202:
# If autofix trigger fails, we don't cache to let it error and we can run again
# This is only temporary for when we're testing this internally.
return response
with sentry_sdk.start_span(op="ai_summary.get_autofix_state"):
autofix_state = get_autofix_state(group_id=group.id)

if (
not autofix_state
): # Only trigger autofix if we don't have an autofix on this issue already.
with sentry_sdk.start_span(op="ai_summary.trigger_autofix"):
response = trigger_autofix(
group=group,
event_id=event.event_id,
user=request.user,
auto_run_source="issue_summary_fixability",
)

if response.status_code != 202:
# If autofix trigger fails, we don't cache to let it error and we can run again
# This is only temporary for when we're testing this internally.
return response

summary_dict = issue_summary.dict()
summary_dict["event_id"] = event.event_id
Expand Down
23 changes: 14 additions & 9 deletions src/sentry/seer/autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ def _respond_with_error(reason: str, status: int):


def _call_autofix(
*,
user: User | AnonymousUser,
group: Group,
repos: list[dict],
Expand All @@ -495,6 +496,7 @@ def _call_autofix(
instruction: str | None = None,
timeout_secs: int = TIMEOUT_SECONDS,
pr_to_comment_on_url: str | None = None,
auto_run_source: str | None = None,
):
path = "/v1/automation/autofix/start"
body = orjson.dumps(
Expand Down Expand Up @@ -523,6 +525,7 @@ def _call_autofix(
),
"options": {
"comment_on_pr_with_url": pr_to_comment_on_url,
"auto_run_source": auto_run_source,
},
},
option=orjson.OPT_NON_STR_KEYS,
Expand All @@ -549,6 +552,7 @@ def trigger_autofix(
user: User | AnonymousUser,
instruction: str | None = None,
pr_to_comment_on_url: str | None = None,
auto_run_source: str | None = None,
):
if event_id is None:
event: Event | GroupEvent | None = group.get_recommended_event_for_environments()
Expand Down Expand Up @@ -602,15 +606,16 @@ def trigger_autofix(

try:
run_id = _call_autofix(
user,
group,
repos,
serialized_event,
profile,
trace_tree,
instruction,
TIMEOUT_SECONDS,
pr_to_comment_on_url,
user=user,
group=group,
repos=repos,
serialized_event=serialized_event,
profile=profile,
trace_tree=trace_tree,
instruction=instruction,
timeout_secs=TIMEOUT_SECONDS,
pr_to_comment_on_url=pr_to_comment_on_url,
auto_run_source=auto_run_source,
)
except Exception:
logger.exception("Failed to send autofix to seer")
Expand Down
48 changes: 24 additions & 24 deletions tests/sentry/api/endpoints/test_group_ai_autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ def test_ai_autofix_post_endpoint(
mock_call.assert_called_once()

# Check individual parameters that we care about
call_args = mock_call.call_args[0]
assert call_args[1].id == group.id # Check that the group object matches
call_kwargs = mock_call.call_args.kwargs
assert call_kwargs["group"].id == group.id # Check that the group object matches

# Check that the repos parameter contains the expected data
expected_repo = {
Expand All @@ -324,16 +324,16 @@ def test_ai_autofix_post_endpoint(
"name": "sentry",
"external_id": "123",
}
assert expected_repo in call_args[2]
assert expected_repo in call_kwargs["repos"]

# Check that the instruction was passed correctly
assert call_args[6] == "Yes"
assert call_kwargs["instruction"] == "Yes"

# Check other parameters
assert call_args[7] == TIMEOUT_SECONDS
assert call_kwargs["timeout_secs"] == TIMEOUT_SECONDS

# Verify that the serialized event has an exception entry
serialized_event_arg = call_args[3]
serialized_event_arg = call_kwargs["serialized_event"]
assert any(
[entry.get("type") == "exception" for entry in serialized_event_arg.get("entries", [])]
)
Expand Down Expand Up @@ -387,20 +387,20 @@ def test_ai_autofix_post_without_code_mappings(
mock_call.assert_called_once()

# Check individual parameters that we care about
call_args = mock_call.call_args[0]
assert call_args[1].id == group.id # Check that the group object matches
call_kwargs = mock_call.call_args.kwargs
assert call_kwargs["group"].id == group.id # Check that the group object matches

# Check that the repos parameter is an empty list (no code mappings)
assert call_args[2] == []
assert call_kwargs["repos"] == []

# Check that the instruction was passed correctly
assert call_args[6] == "Yes"
assert call_kwargs["instruction"] == "Yes"

# Check other parameters
assert call_args[7] == TIMEOUT_SECONDS
assert call_kwargs["timeout_secs"] == TIMEOUT_SECONDS

# Verify that the serialized event has an exception entry
serialized_event_arg = call_args[3]
serialized_event_arg = call_kwargs["serialized_event"]
assert any(
[entry.get("type") == "exception" for entry in serialized_event_arg.get("entries", [])]
)
Expand Down Expand Up @@ -460,8 +460,8 @@ def test_ai_autofix_post_without_event_id(
mock_call.assert_called_once()

# Check individual parameters that we care about
call_args = mock_call.call_args[0]
assert call_args[1].id == group.id # Check that the group object matches
call_kwargs = mock_call.call_args.kwargs
assert call_kwargs["group"].id == group.id # Check that the group object matches

# Check that the repos parameter contains the expected data
expected_repo = {
Expand All @@ -470,16 +470,16 @@ def test_ai_autofix_post_without_event_id(
"name": "sentry",
"external_id": "123",
}
assert expected_repo in call_args[2]
assert expected_repo in call_kwargs["repos"]

# Check that the instruction was passed correctly
assert call_args[6] == "Yes"
assert call_kwargs["instruction"] == "Yes"

# Check other parameters
assert call_args[7] == TIMEOUT_SECONDS
assert call_kwargs["timeout_secs"] == TIMEOUT_SECONDS

# Verify that the serialized event has an exception entry
serialized_event_arg = call_args[3]
serialized_event_arg = call_kwargs["serialized_event"]
assert any(
[entry.get("type") == "exception" for entry in serialized_event_arg.get("entries", [])]
)
Expand Down Expand Up @@ -539,8 +539,8 @@ def test_ai_autofix_post_without_event_id_no_recommended_event(
mock_call.assert_called_once()

# Check individual parameters that we care about
call_args = mock_call.call_args[0]
assert call_args[1].id == group.id # Check that the group object matches
call_kwargs = mock_call.call_args.kwargs
assert call_kwargs["group"].id == group.id # Check that the group object matches

# Check that the repos parameter contains the expected data
expected_repo = {
Expand All @@ -549,16 +549,16 @@ def test_ai_autofix_post_without_event_id_no_recommended_event(
"name": "sentry",
"external_id": "123",
}
assert expected_repo in call_args[2]
assert expected_repo in call_kwargs["repos"]

# Check that the instruction was passed correctly
assert call_args[6] == "Yes"
assert call_kwargs["instruction"] == "Yes"

# Check other parameters
assert call_args[7] == TIMEOUT_SECONDS
assert call_kwargs["timeout_secs"] == TIMEOUT_SECONDS

# Verify that the serialized event has an exception entry
serialized_event_arg = call_args[3]
serialized_event_arg = call_kwargs["serialized_event"]
assert any(
[entry.get("type") == "exception" for entry in serialized_event_arg.get("entries", [])]
)
Expand Down
36 changes: 18 additions & 18 deletions tests/sentry/seer/test_autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1008,14 +1008,14 @@ def test_trigger_autofix_with_event_id(

# Verify the function calls
mock_call.assert_called_once()
call_args = mock_call.call_args[0]
assert call_args[0] == test_user # user
assert call_args[1] == group # group
assert call_args[4] == {"profile_data": "test"} # profile
assert call_args[5] == {"trace_data": "test"} # trace tree
assert call_args[6] == "Test instruction" # instruction
assert call_args[7] == TIMEOUT_SECONDS # timeout
assert call_args[8] == "https://github.com/getsentry/sentry/pull/123" # PR URL
call_kwargs = mock_call.call_args.kwargs
assert call_kwargs["user"] == test_user
assert call_kwargs["group"] == group
assert call_kwargs["profile"] == {"profile_data": "test"}
assert call_kwargs["trace_tree"] == {"trace_data": "test"}
assert call_kwargs["instruction"] == "Test instruction"
assert call_kwargs["timeout_secs"] == TIMEOUT_SECONDS
assert call_kwargs["pr_to_comment_on_url"] == "https://github.com/getsentry/sentry/pull/123"

# Verify check_autofix_status was scheduled
mock_check_autofix_status.assert_called_once_with(
Expand Down Expand Up @@ -1093,17 +1093,17 @@ def test_call_autofix(self, mock_sign, mock_post):
trace_tree = {"trace_data": "test"}
instruction = "Test instruction"

# Call the function
# Call the function with keyword arguments
run_id = _call_autofix(
user,
group,
repos,
serialized_event,
profile,
trace_tree,
instruction,
TIMEOUT_SECONDS,
"https://github.com/getsentry/sentry/pull/123",
user=user,
group=group,
repos=repos,
serialized_event=serialized_event,
profile=profile,
trace_tree=trace_tree,
instruction=instruction,
timeout_secs=TIMEOUT_SECONDS,
pr_to_comment_on_url="https://github.com/getsentry/sentry/pull/123",
)

# Verify the result
Expand Down
Loading