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
3 changes: 3 additions & 0 deletions src/sentry/api/serializers/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
MAX_SQL_FORMAT_OPS = 20
MAX_SQL_FORMAT_LENGTH = 1500

# Page-size ceiling for endpoints that serialize a full event body per row (full=true).
FULL_PAYLOAD_MAX_PER_PAGE = 10


class EventTagOptional(TypedDict, total=False):
query: str
Expand Down
8 changes: 4 additions & 4 deletions src/sentry/issues/endpoints/group_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
from sentry.api.helpers.events import get_direct_hit_response, run_group_events_query
from sentry.api.paginator import GenericOffsetPaginator
from sentry.api.serializers import EventSerializer, SimpleEventSerializer, serialize
from sentry.api.serializers.models.event import SimpleEventSerializerResponse
from sentry.api.serializers.models.event import (
FULL_PAYLOAD_MAX_PER_PAGE,
SimpleEventSerializerResponse,
)
from sentry.api.utils import get_date_range_from_params, handle_query_errors
from sentry.apidocs.constants import (
RESPONSE_BAD_REQUEST,
Expand Down Expand Up @@ -58,9 +61,6 @@ class GroupEventsError(Exception):
pass


FULL_PAYLOAD_MAX_PER_PAGE = 10


@extend_schema(tags=["Events"])
@cell_silo_endpoint
class GroupEventsEndpoint(GroupEndpoint):
Expand Down
9 changes: 9 additions & 0 deletions src/sentry/issues/endpoints/group_hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sentry.api.paginator import GenericOffsetPaginator
from sentry.api.serializers import EventSerializer, SimpleEventSerializer, serialize
from sentry.api.serializers.models.event import (
FULL_PAYLOAD_MAX_PER_PAGE,
EventSerializerResponse,
SimpleEventSerializerResponse,
)
Expand Down Expand Up @@ -54,6 +55,14 @@ class GroupHashesEndpoint(GroupEndpoint):
"GET": ApiPublishStatus.PUBLIC,
}

def get_per_page(
self, request: Request, default_per_page: int | None = None, max_per_page: int | None = None
) -> int:
per_page = super().get_per_page(request, default_per_page, max_per_page)
if request.GET.get("full") not in ("0", "false"):
return min(per_page, FULL_PAYLOAD_MAX_PER_PAGE)
return per_page

@extend_schema(
operation_id="listOrganizationIssueHashes",
summary="List an Issue's Hashes",
Expand Down
13 changes: 12 additions & 1 deletion src/sentry/issues/endpoints/project_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
from sentry.api.base import cell_silo_endpoint
from sentry.api.bases.project import ProjectEndpoint
from sentry.api.serializers import EventSerializer, SimpleEventSerializer, serialize
from sentry.api.serializers.models.event import SimpleEventSerializerResponse
from sentry.api.serializers.models.event import (
FULL_PAYLOAD_MAX_PER_PAGE,
SimpleEventSerializerResponse,
)
from sentry.api.utils import get_date_range_from_params
from sentry.apidocs.constants import RESPONSE_FORBIDDEN, RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED
from sentry.apidocs.examples.event_examples import EventExamples
Expand Down Expand Up @@ -44,6 +47,14 @@ class ProjectEventsEndpoint(ProjectEndpoint):
}
)

def get_per_page(
self, request: Request, default_per_page: int | None = None, max_per_page: int | None = None
) -> int:
per_page = super().get_per_page(request, default_per_page, max_per_page)
if request.GET.get("full") in ("1", "true"):
return min(per_page, FULL_PAYLOAD_MAX_PER_PAGE)
return per_page

@extend_schema(
operation_id="listProjectEvents",
summary="List a Project's Error Events",
Expand Down
2 changes: 1 addition & 1 deletion tests/sentry/issues/endpoints/test_group_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from urllib3.connectionpool import ConnectionPool
from urllib3.exceptions import ReadTimeoutError

from sentry.issues.endpoints.group_events import FULL_PAYLOAD_MAX_PER_PAGE
from sentry.api.serializers.models.event import FULL_PAYLOAD_MAX_PER_PAGE
from sentry.issues.grouptype import ProfileFileIOGroupType
from sentry.models.group import Group
from sentry.search.eap.occurrences.rollout_utils import EAP_OCCURRENCES_SHOULD_RUN_EXPERIMENT_OPTION
Expand Down
18 changes: 18 additions & 0 deletions tests/sentry/issues/endpoints/test_group_hashes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from unittest.mock import patch
from urllib.parse import urlencode

from django.test import RequestFactory

from sentry.api.serializers.models.event import FULL_PAYLOAD_MAX_PER_PAGE
from sentry.eventstream.snuba import SnubaEventStream
from sentry.issues.endpoints.group_hashes import GroupHashesEndpoint
from sentry.models.grouphash import GroupHash
from sentry.testutils.cases import APITestCase, SnubaTestCase
from sentry.testutils.helpers.datetime import before_now
Expand Down Expand Up @@ -175,6 +179,20 @@ def test_full_param(self) -> None:
assert response.status_code == 200, response.content
assert "entries" not in response.data[0]["latestEvent"]

def test_per_page_clamped_when_full(self) -> None:
endpoint = GroupHashesEndpoint()

# full defaults to true on this endpoint, so an unqualified request clamps
assert endpoint.get_per_page(RequestFactory().get("/")) == FULL_PAYLOAD_MAX_PER_PAGE
assert (
endpoint.get_per_page(RequestFactory().get("/?per_page=100"))
== FULL_PAYLOAD_MAX_PER_PAGE
)
# a smaller explicit page is honored
assert endpoint.get_per_page(RequestFactory().get("/?per_page=5")) == 5
# opting out of full removes the clamp
assert endpoint.get_per_page(RequestFactory().get("/?full=false&per_page=100")) == 100

def test_unmerge(self) -> None:
self.login_as(user=self.user)

Expand Down
52 changes: 52 additions & 0 deletions tests/snuba/api/endpoints/test_project_events.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.urls import reverse

from sentry.api.serializers.models.event import FULL_PAYLOAD_MAX_PER_PAGE
from sentry.testutils.cases import APITestCase, SnubaTestCase
from sentry.testutils.helpers.datetime import before_now

Expand Down Expand Up @@ -259,6 +260,57 @@ def test_full_param(self) -> None:
assert response.status_code == 200, response.content
assert "entries" not in response.data[0]

def test_full_clamps_per_page(self) -> None:
self.login_as(user=self.user)

project = self.create_project()
for i in range(FULL_PAYLOAD_MAX_PER_PAGE + 5):
self.store_event(
data={"event_id": f"{i:032x}", "timestamp": before_now(minutes=1).isoformat()},
project_id=project.id,
)

url = reverse(
"sentry-api-0-project-events",
kwargs={
"organization_id_or_slug": project.organization.slug,
"project_id_or_slug": project.slug,
},
)

response = self.client.get(url, {"full": "true", "per_page": "100"}, format="json")
assert response.status_code == 200, response.content
assert len(response.data) == FULL_PAYLOAD_MAX_PER_PAGE
assert "entries" in response.data[0]

# a large per_page is clamped, not rejected
assert (
self.client.get(url, {"full": "1", "per_page": "100"}, format="json").status_code == 200
)

def test_per_page_not_clamped_without_full(self) -> None:
self.login_as(user=self.user)

project = self.create_project()
total = FULL_PAYLOAD_MAX_PER_PAGE + 5
for i in range(total):
self.store_event(
data={"event_id": f"{i:032x}", "timestamp": before_now(minutes=1).isoformat()},
project_id=project.id,
)

url = reverse(
"sentry-api-0-project-events",
kwargs={
"organization_id_or_slug": project.organization.slug,
"project_id_or_slug": project.slug,
},
)

response = self.client.get(url, {"per_page": "100"}, format="json")
assert response.status_code == 200, response.content
assert len(response.data) == total

def test_sample(self) -> None:
self.login_as(user=self.user)

Expand Down
Loading