Skip to content
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
66 changes: 22 additions & 44 deletions src/sentry/seer/endpoints/organization_seer_explorer_runs.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from __future__ import annotations

import logging
from typing import Any

import orjson
from django.conf import settings
from rest_framework import serializers
from rest_framework.request import Request
from rest_framework.response import Response

Expand All @@ -13,6 +13,7 @@
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import region_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
from sentry.api.paginator import GenericOffsetPaginator
from sentry.models.organization import Organization
from sentry.net.http import connection_from_url
from sentry.seer.seer_setup import has_seer_access_with_detail
Expand All @@ -29,21 +30,6 @@ class OrganizationSeerExplorerRunsPermission(OrganizationPermission):
}


class ExplorerRunsRequestSerializer(serializers.Serializer):
offset = serializers.IntegerField(required=False, allow_null=False)
limit = serializers.IntegerField(required=False, allow_null=False)

def validate_offset(self, value: int) -> int:
if value < 0:
raise serializers.ValidationError("Offset must be non-negative")
return value

def validate_limit(self, value: int) -> int:
if value < 1:
raise serializers.ValidationError("Limit must be greater than 0")
return value


@region_silo_endpoint
class OrganizationSeerExplorerRunsEndpoint(OrganizationEndpoint):
publish_status = {
Expand Down Expand Up @@ -72,34 +58,26 @@ def get(self, request: Request, organization: Organization) -> Response:
status=403,
)

serializer = ExplorerRunsRequestSerializer(data=request.GET)
if not serializer.is_valid():
return Response(serializer.errors, status=400)

offset: int | None = serializer.validated_data.get("offset")
limit: int | None = serializer.validated_data.get("limit")

path = "/v1/automation/explorer/runs"
body = orjson.dumps(
{
"organization_id": organization.id,
"user_id": request.user.id,
"offset": offset,
"limit": limit,
},
option=orjson.OPT_NON_STR_KEYS,
)

response = make_signed_seer_api_request(autofix_connection_pool, path, body)
if response.status < 200 or response.status >= 300:
logger.error(
"Seer explorer runs endpoint failed",
extra={
"path": path,
"status_code": response.status,
"response_data": response.data,
def make_seer_runs_request(offset: int, limit: int) -> dict[str, Any]:
path = "/v1/automation/explorer/runs"
body = orjson.dumps(
{
"organization_id": organization.id,
"user_id": request.user.id,
"offset": offset,
"limit": limit,
},
option=orjson.OPT_NON_STR_KEYS,
)
return Response({"detail": "Internal Server Error"}, status=502)

return Response(response.json(), status=response.status)
response = make_signed_seer_api_request(autofix_connection_pool, path, body)
if response.status < 200 or response.status >= 300:
raise Exception(f"Seer explorer runs endpoint failed with status {response.status}")

return response.json()

return self.paginate(
request=request,
paginator=GenericOffsetPaginator(data_fn=make_seer_runs_request),
default_per_page=100,
)
56 changes: 34 additions & 22 deletions tests/sentry/seer/endpoints/test_organization_seer_explorer_runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers.features import with_feature
from sentry.utils.cursors import Cursor


@with_feature("organizations:seer-explorer")
Expand Down Expand Up @@ -35,11 +36,11 @@ def tearDown(self) -> None:
def test_get_simple(self) -> None:
self.make_seer_request.return_value.status = 200
self.make_seer_request.return_value.json.return_value = {
"hello": "world",
"data": [{"run_id": "1"}, {"run_id": "2"}],
}
response = self.client.get(self.url)
assert response.status_code == 200
assert response.json() == {"hello": "world"}
assert response.json()["data"] == [{"run_id": "1"}, {"run_id": "2"}]

self.make_seer_request.assert_called_once()
call_args = self.make_seer_request.call_args
Expand All @@ -48,18 +49,21 @@ def test_get_simple(self) -> None:
assert body_json == {
"organization_id": self.organization.id,
"user_id": self.user.id,
"limit": None,
"offset": None,
"limit": 101, # Default per_page of 100 + 1 for has_more
"offset": 0,
}

def test_get_with_offset_and_limit(self) -> None:
def test_get_cursor_pagination(self) -> None:
self.make_seer_request.return_value.status = 200
# Mock seer response for offset 0, limit 3.
self.make_seer_request.return_value.json.return_value = {
"hello": "world",
"data": [{"run_id": "1"}, {"run_id": "2"}, {"run_id": "3"}],
}
response = self.client.get(self.url + "?offset=1&limit=11")
cursor = str(Cursor(0, 0))
response = self.client.get(self.url + f"?per_page=2&cursor={cursor}")
assert response.status_code == 200
assert response.json() == {"hello": "world"}
assert response.json()["data"] == [{"run_id": "1"}, {"run_id": "2"}]
assert 'rel="next"; results="true"' in response.headers["Link"]

self.make_seer_request.assert_called_once()
call_args = self.make_seer_request.call_args
Expand All @@ -68,26 +72,34 @@ def test_get_with_offset_and_limit(self) -> None:
assert body_json == {
"organization_id": self.organization.id,
"user_id": self.user.id,
"limit": 11,
"offset": 1,
"limit": 3, # +1 for has_more
"offset": 0,
}

def test_get_with_invalid_limit(self) -> None:
for value in ["invalid", "-1", "0"]:
response = self.client.get(self.url + f"?limit={value}")
assert response.status_code == 400
assert "limit" in response.json()
# Second page - mock seer response for offset 2, limit 3.
self.make_seer_request.return_value.json.return_value = {
"data": [{"run_id": "3"}, {"run_id": "4"}],
}
cursor = str(Cursor(0, 2))
response = self.client.get(self.url + f"?per_page=2&cursor={cursor}")
assert response.status_code == 200
assert response.json()["data"] == [{"run_id": "3"}, {"run_id": "4"}]
assert 'rel="next"; results="false"' in response.headers["Link"]

def test_get_with_invalid_offset(self) -> None:
for value in ["invalid", "-1"]:
response = self.client.get(self.url + f"?offset={value}")
assert response.status_code == 400
assert "offset" in response.json()
call_args = self.make_seer_request.call_args
assert call_args[0][1] == "/v1/automation/explorer/runs"
body_json = orjson.loads(call_args[0][2])
assert body_json == {
"organization_id": self.organization.id,
"user_id": self.user.id,
"limit": 3, # +1 for has_more
"offset": 2,
}

def test_get_with_seer_error(self) -> None:
self.make_seer_request.return_value.status = 500
self.make_seer_request.return_value.status = 404
response = self.client.get(self.url)
assert response.status_code == 502
assert response.status_code == 500


class TestOrganizationSeerExplorerRunsEndpointFeatureFlags(APITestCase):
Expand Down
Loading