Skip to content

Commit 9e9d090

Browse files
authored
feat(autofix): Log auto run & don't autorun if autofix exists (#87299)
1. Adds logging to seer when a run is automatically triggered 2. Doesn't auto run when an autofix already exists for an issue
1 parent b296710 commit 9e9d090

File tree

4 files changed

+75
-60
lines changed

4 files changed

+75
-60
lines changed

src/sentry/api/endpoints/group_ai_summary.py

+19-9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from sentry.api.bases.group import GroupEndpoint
1919
from sentry.api.serializers import EventSerializer, serialize
2020
from sentry.api.serializers.rest_framework.base import convert_dict_key_case, snake_to_camel_case
21+
from sentry.autofix.utils import get_autofix_state
2122
from sentry.constants import ObjectStatus
2223
from sentry.eventstore.models import Event, GroupEvent
2324
from sentry.models.group import Group
@@ -240,15 +241,24 @@ def post(self, request: Request, group: Group) -> Response:
240241
issue_summary = self._generate_fixability_score(group.id)
241242

242243
if issue_summary.scores.is_fixable:
243-
with sentry_sdk.start_span(op="ai_summary.trigger_autofix"):
244-
response = trigger_autofix(
245-
group=group, event_id=event.event_id, user=request.user
246-
)
247-
248-
if response.status_code != 202:
249-
# If autofix trigger fails, we don't cache to let it error and we can run again
250-
# This is only temporary for when we're testing this internally.
251-
return response
244+
with sentry_sdk.start_span(op="ai_summary.get_autofix_state"):
245+
autofix_state = get_autofix_state(group_id=group.id)
246+
247+
if (
248+
not autofix_state
249+
): # Only trigger autofix if we don't have an autofix on this issue already.
250+
with sentry_sdk.start_span(op="ai_summary.trigger_autofix"):
251+
response = trigger_autofix(
252+
group=group,
253+
event_id=event.event_id,
254+
user=request.user,
255+
auto_run_source="issue_summary_fixability",
256+
)
257+
258+
if response.status_code != 202:
259+
# If autofix trigger fails, we don't cache to let it error and we can run again
260+
# This is only temporary for when we're testing this internally.
261+
return response
252262

253263
summary_dict = issue_summary.dict()
254264
summary_dict["event_id"] = event.event_id

src/sentry/seer/autofix.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ def _respond_with_error(reason: str, status: int):
486486

487487

488488
def _call_autofix(
489+
*,
489490
user: User | AnonymousUser,
490491
group: Group,
491492
repos: list[dict],
@@ -495,6 +496,7 @@ def _call_autofix(
495496
instruction: str | None = None,
496497
timeout_secs: int = TIMEOUT_SECONDS,
497498
pr_to_comment_on_url: str | None = None,
499+
auto_run_source: str | None = None,
498500
):
499501
path = "/v1/automation/autofix/start"
500502
body = orjson.dumps(
@@ -523,6 +525,7 @@ def _call_autofix(
523525
),
524526
"options": {
525527
"comment_on_pr_with_url": pr_to_comment_on_url,
528+
"auto_run_source": auto_run_source,
526529
},
527530
},
528531
option=orjson.OPT_NON_STR_KEYS,
@@ -549,6 +552,7 @@ def trigger_autofix(
549552
user: User | AnonymousUser,
550553
instruction: str | None = None,
551554
pr_to_comment_on_url: str | None = None,
555+
auto_run_source: str | None = None,
552556
):
553557
if event_id is None:
554558
event: Event | GroupEvent | None = group.get_recommended_event_for_environments()
@@ -602,15 +606,16 @@ def trigger_autofix(
602606

603607
try:
604608
run_id = _call_autofix(
605-
user,
606-
group,
607-
repos,
608-
serialized_event,
609-
profile,
610-
trace_tree,
611-
instruction,
612-
TIMEOUT_SECONDS,
613-
pr_to_comment_on_url,
609+
user=user,
610+
group=group,
611+
repos=repos,
612+
serialized_event=serialized_event,
613+
profile=profile,
614+
trace_tree=trace_tree,
615+
instruction=instruction,
616+
timeout_secs=TIMEOUT_SECONDS,
617+
pr_to_comment_on_url=pr_to_comment_on_url,
618+
auto_run_source=auto_run_source,
614619
)
615620
except Exception:
616621
logger.exception("Failed to send autofix to seer")

tests/sentry/api/endpoints/test_group_ai_autofix.py

+24-24
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@ def test_ai_autofix_post_endpoint(
314314
mock_call.assert_called_once()
315315

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

320320
# Check that the repos parameter contains the expected data
321321
expected_repo = {
@@ -324,16 +324,16 @@ def test_ai_autofix_post_endpoint(
324324
"name": "sentry",
325325
"external_id": "123",
326326
}
327-
assert expected_repo in call_args[2]
327+
assert expected_repo in call_kwargs["repos"]
328328

329329
# Check that the instruction was passed correctly
330-
assert call_args[6] == "Yes"
330+
assert call_kwargs["instruction"] == "Yes"
331331

332332
# Check other parameters
333-
assert call_args[7] == TIMEOUT_SECONDS
333+
assert call_kwargs["timeout_secs"] == TIMEOUT_SECONDS
334334

335335
# Verify that the serialized event has an exception entry
336-
serialized_event_arg = call_args[3]
336+
serialized_event_arg = call_kwargs["serialized_event"]
337337
assert any(
338338
[entry.get("type") == "exception" for entry in serialized_event_arg.get("entries", [])]
339339
)
@@ -387,20 +387,20 @@ def test_ai_autofix_post_without_code_mappings(
387387
mock_call.assert_called_once()
388388

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

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

396396
# Check that the instruction was passed correctly
397-
assert call_args[6] == "Yes"
397+
assert call_kwargs["instruction"] == "Yes"
398398

399399
# Check other parameters
400-
assert call_args[7] == TIMEOUT_SECONDS
400+
assert call_kwargs["timeout_secs"] == TIMEOUT_SECONDS
401401

402402
# Verify that the serialized event has an exception entry
403-
serialized_event_arg = call_args[3]
403+
serialized_event_arg = call_kwargs["serialized_event"]
404404
assert any(
405405
[entry.get("type") == "exception" for entry in serialized_event_arg.get("entries", [])]
406406
)
@@ -460,8 +460,8 @@ def test_ai_autofix_post_without_event_id(
460460
mock_call.assert_called_once()
461461

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

466466
# Check that the repos parameter contains the expected data
467467
expected_repo = {
@@ -470,16 +470,16 @@ def test_ai_autofix_post_without_event_id(
470470
"name": "sentry",
471471
"external_id": "123",
472472
}
473-
assert expected_repo in call_args[2]
473+
assert expected_repo in call_kwargs["repos"]
474474

475475
# Check that the instruction was passed correctly
476-
assert call_args[6] == "Yes"
476+
assert call_kwargs["instruction"] == "Yes"
477477

478478
# Check other parameters
479-
assert call_args[7] == TIMEOUT_SECONDS
479+
assert call_kwargs["timeout_secs"] == TIMEOUT_SECONDS
480480

481481
# Verify that the serialized event has an exception entry
482-
serialized_event_arg = call_args[3]
482+
serialized_event_arg = call_kwargs["serialized_event"]
483483
assert any(
484484
[entry.get("type") == "exception" for entry in serialized_event_arg.get("entries", [])]
485485
)
@@ -539,8 +539,8 @@ def test_ai_autofix_post_without_event_id_no_recommended_event(
539539
mock_call.assert_called_once()
540540

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

545545
# Check that the repos parameter contains the expected data
546546
expected_repo = {
@@ -549,16 +549,16 @@ def test_ai_autofix_post_without_event_id_no_recommended_event(
549549
"name": "sentry",
550550
"external_id": "123",
551551
}
552-
assert expected_repo in call_args[2]
552+
assert expected_repo in call_kwargs["repos"]
553553

554554
# Check that the instruction was passed correctly
555-
assert call_args[6] == "Yes"
555+
assert call_kwargs["instruction"] == "Yes"
556556

557557
# Check other parameters
558-
assert call_args[7] == TIMEOUT_SECONDS
558+
assert call_kwargs["timeout_secs"] == TIMEOUT_SECONDS
559559

560560
# Verify that the serialized event has an exception entry
561-
serialized_event_arg = call_args[3]
561+
serialized_event_arg = call_kwargs["serialized_event"]
562562
assert any(
563563
[entry.get("type") == "exception" for entry in serialized_event_arg.get("entries", [])]
564564
)

tests/sentry/seer/test_autofix.py

+18-18
Original file line numberDiff line numberDiff line change
@@ -1008,14 +1008,14 @@ def test_trigger_autofix_with_event_id(
10081008

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

10201020
# Verify check_autofix_status was scheduled
10211021
mock_check_autofix_status.assert_called_once_with(
@@ -1093,17 +1093,17 @@ def test_call_autofix(self, mock_sign, mock_post):
10931093
trace_tree = {"trace_data": "test"}
10941094
instruction = "Test instruction"
10951095

1096-
# Call the function
1096+
# Call the function with keyword arguments
10971097
run_id = _call_autofix(
1098-
user,
1099-
group,
1100-
repos,
1101-
serialized_event,
1102-
profile,
1103-
trace_tree,
1104-
instruction,
1105-
TIMEOUT_SECONDS,
1106-
"https://github.com/getsentry/sentry/pull/123",
1098+
user=user,
1099+
group=group,
1100+
repos=repos,
1101+
serialized_event=serialized_event,
1102+
profile=profile,
1103+
trace_tree=trace_tree,
1104+
instruction=instruction,
1105+
timeout_secs=TIMEOUT_SECONDS,
1106+
pr_to_comment_on_url="https://github.com/getsentry/sentry/pull/123",
11071107
)
11081108

11091109
# Verify the result

0 commit comments

Comments
 (0)