Skip to content
Draft
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
108 changes: 68 additions & 40 deletions src/sentry/seer/autofix/autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
import orjson
import sentry_sdk
from django.contrib.auth.models import AnonymousUser
from django.db import router, transaction
from django.utils import timezone
from django.utils.timezone import now
from rest_framework.response import Response

from sentry import features, quotas, tagstore
from sentry.api.endpoints.organization_trace import OrganizationTraceEndpoint
from sentry.api.serializers import EventSerializer, serialize
from sentry.constants import ENABLE_SEER_CODING_DEFAULT, DataCategory, ObjectStatus
from sentry.hybridcloud.models.outbox import CellOutbox, outbox_context
from sentry.hybridcloud.outbox.category import OutboxCategory, OutboxScope
from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig
from sentry.issues.auto_source_code_config.code_mapping import (
convert_stacktrace_frame_path_to_source_path,
Expand All @@ -41,6 +45,7 @@
read_preference_from_sentry_db,
)
from sentry.seer.models import SeerProjectPreference
from sentry.seer.models.run import SeerRun, SeerRunMirrorStatus, SeerRunType
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.seer.utils import get_github_username_for_user
from sentry.services import eventstore
Expand Down Expand Up @@ -473,53 +478,76 @@ def _call_autofix(
stopping_point: AutofixStoppingPoint | None = None,
github_username: str | None = None,
):
body = orjson.dumps(
{
"organization_id": group.organization.id,
"project_id": group.project.id,
"preference": preference.dict(),
"repos": [repo.dict() for repo in preference.repositories],
"issue": {
"id": group.id,
"title": group.title,
"short_id": group.qualified_short_id,
"first_seen": group.first_seen.isoformat(),
"events": [serialized_event],
},
"profile": profile,
"trace_tree": trace_tree,
"logs": logs,
"tags_overview": tags_overview,
"instruction": instruction,
"timeout_secs": timeout_secs,
"last_updated": datetime.now().isoformat(),
"invoking_user": (
{
"id": user.id,
"display_name": user.get_display_name(),
"github_username": github_username,
}
if not isinstance(user, AnonymousUser)
else None
body_dict = {
"organization_id": group.organization.id,
"project_id": group.project.id,
"preference": preference.dict(),
"repos": [repo.dict() for repo in preference.repositories],
"issue": {
"id": group.id,
"title": group.title,
"short_id": group.qualified_short_id,
"first_seen": group.first_seen.isoformat(),
"events": [serialized_event],
},
"profile": profile,
"trace_tree": trace_tree,
"logs": logs,
"tags_overview": tags_overview,
"instruction": instruction,
"timeout_secs": timeout_secs,
"last_updated": datetime.now().isoformat(),
"invoking_user": (
{
"id": user.id,
"display_name": user.get_display_name(),
"github_username": github_username,
}
if not isinstance(user, AnonymousUser)
else None
),
"options": {
"comment_on_pr_with_url": pr_to_comment_on_url,
"auto_run_source": auto_run_source,
"referrer": referrer.value,
"disable_coding_step": not group.organization.get_option(
"sentry:enable_seer_coding", default=ENABLE_SEER_CODING_DEFAULT
),
"options": {
"comment_on_pr_with_url": pr_to_comment_on_url,
"auto_run_source": auto_run_source,
"referrer": referrer.value,
"disable_coding_step": not group.organization.get_option(
"sentry:enable_seer_coding", default=ENABLE_SEER_CODING_DEFAULT
),
"stopping_point": stopping_point.value if stopping_point else None,
},
"stopping_point": stopping_point.value if stopping_point else None,
},
option=orjson.OPT_NON_STR_KEYS,
)
}

viewer_context = SeerViewerContext(organization_id=group.organization.id)
if not isinstance(user, AnonymousUser):
viewer_context["user_id"] = user.id

response = make_autofix_start_request(body, viewer_context=viewer_context)
if features.has("organizations:seer-run-mirror", group.organization):
with outbox_context(transaction.atomic(using=router.db_for_write(SeerRun)), flush=True):
run = SeerRun.objects.create(
organization=group.organization,
user_id=user.id if not isinstance(user, AnonymousUser) else None,
type=SeerRunType.AUTOFIX,
last_triggered_at=now(),
)
CellOutbox(
shard_scope=OutboxScope.ORGANIZATION_SCOPE,
shard_identifier=group.organization.id,
category=OutboxCategory.SEER_RUN_CREATE,
object_identifier=run.id,
payload={
"body": body_dict,
"viewer_context": dict(viewer_context),
},
).save()
run.refresh_from_db()
if run.mirror_status != SeerRunMirrorStatus.LIVE or run.seer_run_state_id is None:
raise Exception("Seer run mirror failed to materialize")
return run.seer_run_state_id

response = make_autofix_start_request(
orjson.dumps(body_dict, option=orjson.OPT_NON_STR_KEYS),
viewer_context=viewer_context,
)

if response.status >= 400:
raise Exception(f"Seer request failed with status {response.status}")
Expand Down
41 changes: 41 additions & 0 deletions tests/sentry/seer/autofix/test_autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,47 @@ def test_call_autofix(self, mock_request, mock_get_username) -> None:
)
assert body["options"]["disable_coding_step"] is False

@patch("sentry.receivers.outbox.cell.make_autofix_start_request")
def test_outbox_path_creates_run_and_returns_run_id(self, mock_request: Mock) -> None:
mock_request.return_value = Mock(status=200, json=Mock(return_value={"run_id": 42}))

group = self.create_group()
preference = SeerProjectPreference(
organization_id=group.organization.id,
project_id=group.project.id,
repositories=[],
)

with self.feature("organizations:seer-run-mirror"):
run_id = _call_autofix(
user=self.user,
group=group,
preference=preference,
serialized_event={"event_id": "test-event"},
profile=None,
trace_tree=None,
logs=None,
tags_overview=None,
referrer=AutofixReferrer.GROUP_AUTOFIX_ENDPOINT,
)

assert run_id == 42

from sentry.seer.models.run import SeerRun, SeerRunMirrorStatus, SeerRunType

run = SeerRun.objects.get(organization_id=group.organization.id)
assert run.type == SeerRunType.AUTOFIX
assert run.mirror_status == SeerRunMirrorStatus.LIVE
assert run.seer_run_state_id == 42
assert run.user_id == self.user.id

sent_body = mock_request.call_args[0][0]
body = orjson.loads(sent_body)
assert body["organization_id"] == group.organization.id
assert body["project_id"] == group.project.id
assert "external_idempotency_key" in body
assert body["external_idempotency_key"] == str(run.uuid)


class TestGetGithubUsernameForUser(TestCase):
def test_get_github_username_for_user_with_github(self) -> None:
Expand Down
Loading