Skip to content

Commit

Permalink
feat(autofix): timeout with autofix, query state (#66241)
Browse files Browse the repository at this point in the history
  • Loading branch information
corps authored and aliu39 committed Mar 6, 2024
1 parent 9d26087 commit 35a45b3
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 4 deletions.
12 changes: 10 additions & 2 deletions src/sentry/api/endpoints/group_ai_autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def _call_autofix(
repos: list[dict],
event_entries: list[dict],
additional_context: str,
timeout_secs: int,
):
response = requests.post(
f"{settings.SEER_AUTOFIX_URL}/v0/automation/autofix",
Expand All @@ -123,10 +124,12 @@ def _call_autofix(
"issue": {
"id": group.id,
"title": group.title,
"short_id": group.short_id,
"short_id": group.qualified_short_id,
"events": [{"entries": event_entries}],
},
"additional_context": additional_context,
"timeout_secs": timeout_secs,
"last_updated": datetime.now().isoformat(),
"invoking_user": (
{
"id": user.id,
Expand Down Expand Up @@ -192,7 +195,12 @@ def post(self, request: Request, group: Group) -> Response:

try:
self._call_autofix(
request.user, group, repos, event_entries, data.get("additional_context", "")
request.user,
group,
repos,
event_entries,
data.get("additional_context", ""),
TIMEOUT_SECONDS,
)

# Mark the task as completed after TIMEOUT_SECONDS
Expand Down
38 changes: 37 additions & 1 deletion src/sentry/api/endpoints/seer_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.exceptions import (
AuthenticationFailed,
NotFound,
Expand All @@ -25,6 +26,7 @@
from sentry.services.hybrid_cloud.sig import SerializableFunctionValueException
from sentry.silo.base import SiloMode
from sentry.utils import json
from sentry.utils.env import in_test_environment


def compare_signature(url: str, body: bytes, signature: str) -> bool:
Expand Down Expand Up @@ -131,8 +133,13 @@ def post(self, request: Request, method_name: str) -> Response:
except SerializableFunctionValueException as e:
capture_exception()
raise ParseError from e
except ObjectDoesNotExist as e:
# Let this fall through, this is normal.
capture_exception()
raise NotFound from e
except Exception as e:
# Produce more detailed log
if in_test_environment():
raise
if settings.DEBUG:
raise Exception(f"Problem processing seer rpc endpoint {method_name}") from e
capture_exception()
Expand Down Expand Up @@ -174,7 +181,36 @@ def on_autofix_complete(*, issue_id: int, status: str, steps: list[dict], fix: d
group.save()


def get_autofix_state(*, issue_id: int) -> dict:
group: Group = Group.objects.get(id=issue_id)

metadata = group.data.get("metadata", {})
autofix_data = metadata.get("autofix", {})

return autofix_data


seer_method_registry = {
"on_autofix_step_update": on_autofix_step_update,
"on_autofix_complete": on_autofix_complete,
"get_autofix_state": get_autofix_state,
}


def generate_request_signature(url_path: str, body: bytes) -> str:
"""
Generate a signature for the request body
with the first shared secret. If there are other
shared secrets in the list they are only to be used
for verfication during key rotation.
"""
if not settings.SEER_RPC_SHARED_SECRET:
raise RpcAuthenticationSetupException("Cannot sign RPC requests without RPC_SHARED_SECRET")

signature_input = b"%s:%s" % (
url_path.encode("utf8"),
body,
)
secret = settings.SEER_RPC_SHARED_SECRET[0]
signature = hmac.new(secret.encode("utf-8"), signature_input, hashlib.sha256).hexdigest()
return f"rpc0:{signature}"
3 changes: 2 additions & 1 deletion tests/sentry/api/endpoints/test_group_ai_autofix.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from unittest.mock import ANY, patch

from sentry.api.endpoints.group_ai_autofix import GroupAiAutofixEndpoint
from sentry.api.endpoints.group_ai_autofix import TIMEOUT_SECONDS, GroupAiAutofixEndpoint
from sentry.models.group import Group
from sentry.testutils.cases import APITestCase, SnubaTestCase
from sentry.testutils.helpers.datetime import before_now
Expand Down Expand Up @@ -93,6 +93,7 @@ def test_ai_autofix_post_endpoint(self):
],
ANY,
"Yes",
TIMEOUT_SECONDS,
)

actual_group_arg = mock_call.call_args[0][1]
Expand Down
73 changes: 73 additions & 0 deletions tests/sentry/api/endpoints/test_seer_rpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from typing import Any

from django.test import override_settings
from django.urls import reverse

from sentry.api.endpoints.seer_rpc import generate_request_signature
from sentry.testutils.cases import APITestCase
from sentry.utils import json


@override_settings(SEER_RPC_SHARED_SECRET=["a-long-value-that-is-hard-to-guess"])
class TestSeerRpc(APITestCase):
@staticmethod
def _get_path(method_name: str) -> str:
return reverse(
"sentry-api-0-seer-rpc-service",
kwargs={"method_name": method_name},
)

def auth_header(self, path: str, data: dict | str) -> str:
if isinstance(data, dict):
data = json.dumps(data)
signature = generate_request_signature(path, data.encode("utf8"))

return f"rpcsignature {signature}"

def test_invalid_endpoint(self):
path = self._get_path("not_a_method")
response = self.client.post(path)
assert response.status_code == 403

def test_invalid_authentication(self):
path = self._get_path("on_autofix_step_update")
data: dict[str, Any] = {"args": {"issued_id": 1, "status": "", "steps": []}, "meta": {}}
response = self.client.post(path, data=data, HTTP_AUTHORIZATION="rpcsignature trash")
assert response.status_code == 401

def test_404(self):
path = self._get_path("get_autofix_state")
data: dict[str, Any] = {"args": {"issue_id": 1}, "meta": {}}
response = self.client.post(
path, data=data, HTTP_AUTHORIZATION=self.auth_header(path, data)
)
assert response.status_code == 404

def test_step_state_management(self):
group = self.create_group()

path = self._get_path("get_autofix_state")
data: dict[str, Any] = {"args": {"issue_id": group.id}, "meta": {}}
response = self.client.post(
path, data=data, HTTP_AUTHORIZATION=self.auth_header(path, data)
)
assert response.status_code == 200
assert response.json() == {}

path = self._get_path("on_autofix_step_update")
data = {
"args": {"issue_id": group.id, "status": "thing", "steps": [1, 2, 3]},
"meta": {},
}
response = self.client.post(
path, data=data, HTTP_AUTHORIZATION=self.auth_header(path, data)
)
assert response.status_code == 200

path = self._get_path("get_autofix_state")
data = {"args": {"issue_id": group.id}, "meta": {}}
response = self.client.post(
path, data=data, HTTP_AUTHORIZATION=self.auth_header(path, data)
)
assert response.status_code == 200
assert response.json() == {"status": "thing", "steps": [1, 2, 3]}

0 comments on commit 35a45b3

Please sign in to comment.