diff --git a/src/sentry/api/base.py b/src/sentry/api/base.py index c44cc6fdc177..cc323b01c590 100644 --- a/src/sentry/api/base.py +++ b/src/sentry/api/base.py @@ -112,6 +112,12 @@ ONE_HOUR = ONE_MINUTE * 60 ONE_DAY = ONE_HOUR * 24 +# A reasonable `Endpoint.max_acceptable_duration` (in seconds) for endpoints with no known reason +# to be slow. Endpoints have no budget unless they set one; use this when opting in without a +# more specific value. This value may change, so use an explicit number when the budget was +# chosen for that endpoint. +DEFAULT_DURATION_BUDGET = 3.0 + CURSOR_LINK_HEADER = ( '<{uri}&cursor={cursor}>; rel="{name}"; results="{has_results}"; cursor="{cursor}"' ) @@ -284,6 +290,9 @@ class Endpoint(APIView): cursor_name = "cursor" owner: ApiOwner = ApiOwner.UNOWNED + # Request duration budgets are expressed in seconds. A mapping applies budgets only to the + # listed HTTP methods; missing methods are not budgeted. See DEFAULT_DURATION_BUDGET. + max_acceptable_duration: dict[HTTP_METHOD_NAME, float] | float | None = None publish_status: dict[HTTP_METHOD_NAME, ApiPublishStatus] = {} # Non-safe methods that a client may resend after a timeout or 5xx without # changing the outcome. Published as `x-sentry-idempotent`; see apidocs/README.md. diff --git a/src/sentry/issues/endpoints/actionable_items.py b/src/sentry/issues/endpoints/actionable_items.py index 058ff0c274bb..e67e43836851 100644 --- a/src/sentry/issues/endpoints/actionable_items.py +++ b/src/sentry/issues/endpoints/actionable_items.py @@ -5,7 +5,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.helpers.actionable_items_helper import ( ActionPriority, @@ -31,6 +31,7 @@ class ActionableItemsEndpoint(ProjectEndpoint): "GET": ApiPublishStatus.PRIVATE, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET def get(self, request: Request, project: Project, event_id: str) -> Response: # Retrieve information about actionable items (source maps, event errors, etc.) for a given event. diff --git a/src/sentry/issues/endpoints/debug_group_derived_data.py b/src/sentry/issues/endpoints/debug_group_derived_data.py index e618ee19f313..4b7c25c1d3a1 100644 --- a/src/sentry/issues/endpoints/debug_group_derived_data.py +++ b/src/sentry/issues/endpoints/debug_group_derived_data.py @@ -5,7 +5,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.issues.derived.framework import Pipeline, State from sentry.issues.derived.processing import PIPELINE from sentry.issues.derived.store import GroupDerivedDataStore @@ -25,6 +25,7 @@ def _state_to_dict(pipeline: Pipeline[Any], state: State) -> dict[str, Any]: @cell_silo_endpoint class DebugGroupDerivedDataEndpoint(GroupEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/event_grouping_info.py b/src/sentry/issues/endpoints/event_grouping_info.py index 2921f347e81d..90ad32522f81 100644 --- a/src/sentry/issues/endpoints/event_grouping_info.py +++ b/src/sentry/issues/endpoints/event_grouping_info.py @@ -4,7 +4,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.grouping.api import load_grouping_config @@ -19,6 +19,7 @@ @cell_silo_endpoint class EventGroupingInfoEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/event_owners.py b/src/sentry/issues/endpoints/event_owners.py index 9e9f3f97cf47..7ef2cc9d3ac5 100644 --- a/src/sentry/issues/endpoints/event_owners.py +++ b/src/sentry/issues/endpoints/event_owners.py @@ -6,7 +6,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import serialize from sentry.api.serializers.models.actor import ActorSerializer @@ -19,6 +19,7 @@ @cell_silo_endpoint class EventOwnersEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_activities.py b/src/sentry/issues/endpoints/group_activities.py index 1dc4e3d649eb..90a64e4b01de 100644 --- a/src/sentry/issues/endpoints/group_activities.py +++ b/src/sentry/issues/endpoints/group_activities.py @@ -4,7 +4,7 @@ from rest_framework.response import Response from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.serializers import serialize from sentry.api.serializers.models.groupactionlogentry import get_serialized_activity_items @@ -19,6 +19,8 @@ @cell_silo_endpoint class GroupActivitiesEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_attachments.py b/src/sentry/issues/endpoints/group_attachments.py index 275610c44aaa..cfd41afdc3f6 100644 --- a/src/sentry/issues/endpoints/group_attachments.py +++ b/src/sentry/issues/endpoints/group_attachments.py @@ -6,7 +6,7 @@ from sentry import features from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.helpers.deprecation import deprecated from sentry.api.helpers.environments import get_environments @@ -68,6 +68,8 @@ def get_event_ids_from_filters( @cell_silo_endpoint class GroupAttachmentsEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_current_release.py b/src/sentry/issues/endpoints/group_current_release.py index 101be95c3312..be70500d2805 100644 --- a/src/sentry/issues/endpoints/group_current_release.py +++ b/src/sentry/issues/endpoints/group_current_release.py @@ -6,7 +6,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.helpers.environments import get_environments from sentry.api.serializers import serialize @@ -22,6 +22,8 @@ @cell_silo_endpoint class GroupCurrentReleaseEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + owner = ApiOwner.COMMUNITY publish_status = { "GET": ApiPublishStatus.PRIVATE, diff --git a/src/sentry/issues/endpoints/group_details.py b/src/sentry/issues/endpoints/group_details.py index 95a4192aac0e..3457fbd5716c 100644 --- a/src/sentry/issues/endpoints/group_details.py +++ b/src/sentry/issues/endpoints/group_details.py @@ -14,7 +14,7 @@ from sentry.analytics.events.issue_viewed import IssueViewedEvent from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.client import client from sentry.api.helpers.deprecation import deprecated from sentry.api.helpers.environments import get_environment_func, get_environments @@ -101,6 +101,7 @@ class GroupDetailsPermission(GroupPermission): @cell_silo_endpoint class GroupDetailsEndpoint(GroupEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET permission_classes = (GroupDetailsPermission,) publish_status = { "DELETE": ApiPublishStatus.PUBLIC, diff --git a/src/sentry/issues/endpoints/group_event_details.py b/src/sentry/issues/endpoints/group_event_details.py index 776a9a0ab344..714a4c095aed 100644 --- a/src/sentry/issues/endpoints/group_event_details.py +++ b/src/sentry/issues/endpoints/group_event_details.py @@ -17,7 +17,7 @@ from sentry import features from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.event_search import ( ParenExpression, SearchBoolean, @@ -168,6 +168,8 @@ class GroupEventDetailsFormattedResponse(GroupEventDetailsResponse, total=False) @extend_schema(tags=["Events"]) @cell_silo_endpoint class GroupEventDetailsEndpoint(FormattableResponseMixin, GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + publish_status = { "GET": ApiPublishStatus.PUBLIC, } diff --git a/src/sentry/issues/endpoints/group_events.py b/src/sentry/issues/endpoints/group_events.py index 68236aa462e4..8a69a87c576c 100644 --- a/src/sentry/issues/endpoints/group_events.py +++ b/src/sentry/issues/endpoints/group_events.py @@ -68,6 +68,7 @@ class GroupEventsEndpoint(GroupEndpoint): "GET": ApiPublishStatus.PUBLIC, } owner = ApiOwner.ISSUES + max_acceptable_duration = 10.0 def get_per_page( self, request: Request, default_per_page: int | None = None, max_per_page: int | None = None diff --git a/src/sentry/issues/endpoints/group_first_last_release.py b/src/sentry/issues/endpoints/group_first_last_release.py index 8a5b94d6de7b..44a34c761bac 100644 --- a/src/sentry/issues/endpoints/group_first_last_release.py +++ b/src/sentry/issues/endpoints/group_first_last_release.py @@ -3,7 +3,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.environments import get_environments from sentry.api.helpers.group_index import get_first_last_release from sentry.issues.endpoints.bases.group import GroupEndpoint @@ -14,6 +14,8 @@ @cell_silo_endpoint class GroupFirstLastReleaseEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + owner = ApiOwner.COMMUNITY publish_status = { "GET": ApiPublishStatus.PRIVATE, diff --git a/src/sentry/issues/endpoints/group_hashes.py b/src/sentry/issues/endpoints/group_hashes.py index 6491739e3a4a..2676c3de9aad 100644 --- a/src/sentry/issues/endpoints/group_hashes.py +++ b/src/sentry/issues/endpoints/group_hashes.py @@ -10,7 +10,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.paginator import GenericOffsetPaginator from sentry.api.serializers import EventSerializer, SimpleEventSerializer, serialize @@ -50,6 +50,7 @@ class GroupHashesResult(TypedDict): @cell_silo_endpoint class GroupHashesEndpoint(GroupEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "PUT": ApiPublishStatus.PRIVATE, "GET": ApiPublishStatus.PUBLIC, diff --git a/src/sentry/issues/endpoints/group_integration_details.py b/src/sentry/issues/endpoints/group_integration_details.py index 8f6a96c15c9a..e3153b7c73b8 100644 --- a/src/sentry/issues/endpoints/group_integration_details.py +++ b/src/sentry/issues/endpoints/group_integration_details.py @@ -137,6 +137,8 @@ def serialize( @extend_schema(tags=["Integration"]) @cell_silo_endpoint class GroupIntegrationDetailsEndpoint(GroupEndpoint): + max_acceptable_duration = 10.0 + owner = ApiOwner.INTEGRATION_PLATFORM publish_status = { "GET": ApiPublishStatus.PUBLIC, diff --git a/src/sentry/issues/endpoints/group_integrations.py b/src/sentry/issues/endpoints/group_integrations.py index de41319c3c94..4ac2330fef4b 100644 --- a/src/sentry/issues/endpoints/group_integrations.py +++ b/src/sentry/issues/endpoints/group_integrations.py @@ -12,7 +12,7 @@ from sentry import features, options from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.serializers import Serializer, serialize from sentry.apidocs.parameters import CursorQueryParam, GlobalParams, IssueParams @@ -108,6 +108,8 @@ def serialize( @extend_schema(tags=["Integration"]) @cell_silo_endpoint class GroupIntegrationsEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + owner = ApiOwner.INTEGRATION_PLATFORM publish_status = { "GET": ApiPublishStatus.PUBLIC_EXPERIMENTAL, diff --git a/src/sentry/issues/endpoints/group_notes.py b/src/sentry/issues/endpoints/group_notes.py index faa455d8e5b6..bdc770f35736 100644 --- a/src/sentry/issues/endpoints/group_notes.py +++ b/src/sentry/issues/endpoints/group_notes.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.paginator import DateTimePaginator from sentry.api.serializers.models.activity import ( @@ -30,6 +30,8 @@ @cell_silo_endpoint class GroupNotesEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + publish_status = { "GET": ApiPublishStatus.PRIVATE, "POST": ApiPublishStatus.PRIVATE, diff --git a/src/sentry/issues/endpoints/group_notes_details.py b/src/sentry/issues/endpoints/group_notes_details.py index 3468b494cf41..c98d6b9f3c6a 100644 --- a/src/sentry/issues/endpoints/group_notes_details.py +++ b/src/sentry/issues/endpoints/group_notes_details.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.helpers.deprecation import deprecated from sentry.api.serializers.models.activity import ( @@ -32,6 +32,8 @@ @cell_silo_endpoint class GroupNotesDetailsEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + publish_status = { "DELETE": ApiPublishStatus.PRIVATE, "PUT": ApiPublishStatus.PRIVATE, diff --git a/src/sentry/issues/endpoints/group_pull_requests.py b/src/sentry/issues/endpoints/group_pull_requests.py index f5997712ebef..f62eb3e6ba9f 100644 --- a/src/sentry/issues/endpoints/group_pull_requests.py +++ b/src/sentry/issues/endpoints/group_pull_requests.py @@ -12,7 +12,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.serializers import serialize from sentry.api.serializers.models.pullrequest import ( LinkedPullRequestResponse, @@ -190,6 +190,7 @@ def _get_pull_request_status( @cell_silo_endpoint class GroupPullRequestsEndpoint(GroupEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_reprocessing.py b/src/sentry/issues/endpoints/group_reprocessing.py index 4ae115895ad4..ec982db6fe64 100644 --- a/src/sentry/issues/endpoints/group_reprocessing.py +++ b/src/sentry/issues/endpoints/group_reprocessing.py @@ -2,7 +2,7 @@ from rest_framework.response import Response from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.constants import CELL_API_DEPRECATION_DATE from sentry.issues.endpoints.bases.group import GroupEndpoint @@ -12,6 +12,8 @@ @cell_silo_endpoint class GroupReprocessingEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + publish_status = { "POST": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_similar_issues.py b/src/sentry/issues/endpoints/group_similar_issues.py index 667d8fe3fc64..ab0786825594 100644 --- a/src/sentry/issues/endpoints/group_similar_issues.py +++ b/src/sentry/issues/endpoints/group_similar_issues.py @@ -5,7 +5,7 @@ from sentry import similarity from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.serializers import serialize from sentry.constants import CELL_API_DEPRECATION_DATE @@ -23,6 +23,8 @@ def _fix_label(label: tuple[str, ...] | str) -> str: @cell_silo_endpoint class GroupSimilarIssuesEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + """ This endpoint uses the legacy MinHash similarity system which has been replaced by embeddings-based grouping for SaaS. diff --git a/src/sentry/issues/endpoints/group_similar_issues_embeddings.py b/src/sentry/issues/endpoints/group_similar_issues_embeddings.py index d345f68bd551..94b8d778746b 100644 --- a/src/sentry/issues/endpoints/group_similar_issues_embeddings.py +++ b/src/sentry/issues/endpoints/group_similar_issues_embeddings.py @@ -10,7 +10,7 @@ from sentry.api.analytics import GroupSimilarIssuesEmbeddingsCountEvent from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.serializers import serialize from sentry.auth.principal import acting_user_id @@ -44,6 +44,7 @@ class FormattedSimilarIssuesEmbeddingsData(TypedDict): @cell_silo_endpoint class GroupSimilarIssuesEmbeddingsEndpoint(GroupEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_stats.py b/src/sentry/issues/endpoints/group_stats.py index c4348cf72605..411ee9afc589 100644 --- a/src/sentry/issues/endpoints/group_stats.py +++ b/src/sentry/issues/endpoints/group_stats.py @@ -3,7 +3,7 @@ from sentry import tsdb from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import StatsMixin, cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, StatsMixin, cell_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.helpers.deprecation import deprecated from sentry.api.helpers.environments import get_environment_id @@ -16,6 +16,8 @@ @cell_silo_endpoint class GroupStatsEndpoint(GroupEndpoint, StatsMixin): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_tagkey_details.py b/src/sentry/issues/endpoints/group_tagkey_details.py index 613835f26bd6..c9e9fd720291 100644 --- a/src/sentry/issues/endpoints/group_tagkey_details.py +++ b/src/sentry/issues/endpoints/group_tagkey_details.py @@ -5,7 +5,7 @@ from sentry import tagstore from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.helpers.deprecation import deprecated from sentry.api.helpers.environments import get_environment_id @@ -35,6 +35,7 @@ class GroupTagKeyDetailsEndpoint(GroupEndpoint): "GET": ApiPublishStatus.PUBLIC, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET enforce_rate_limit = True rate_limits = RateLimitConfig( diff --git a/src/sentry/issues/endpoints/group_tagkey_values.py b/src/sentry/issues/endpoints/group_tagkey_values.py index 237cc54db262..60497c542a3f 100644 --- a/src/sentry/issues/endpoints/group_tagkey_values.py +++ b/src/sentry/issues/endpoints/group_tagkey_values.py @@ -6,7 +6,7 @@ from sentry.analytics.events.eventuser_endpoint_request import EventUserEndpointRequest from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.helpers.deprecation import deprecated from sentry.api.helpers.environments import get_environments @@ -36,6 +36,7 @@ class GroupTagKeyValuesEndpoint(GroupEndpoint): "GET": ApiPublishStatus.PUBLIC, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET enforce_rate_limit = True rate_limits = RateLimitConfig( diff --git a/src/sentry/issues/endpoints/group_tags.py b/src/sentry/issues/endpoints/group_tags.py index 803f95fa0614..d95ebf96b51e 100644 --- a/src/sentry/issues/endpoints/group_tags.py +++ b/src/sentry/issues/endpoints/group_tags.py @@ -7,7 +7,7 @@ from sentry import tagstore from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.helpers.environments import get_environments from sentry.api.helpers.mobile import get_readable_device_name @@ -25,6 +25,8 @@ @cell_silo_endpoint class GroupTagsEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_tombstone.py b/src/sentry/issues/endpoints/group_tombstone.py index ef411afacae7..8803c765385c 100644 --- a/src/sentry/issues/endpoints/group_tombstone.py +++ b/src/sentry/issues/endpoints/group_tombstone.py @@ -3,7 +3,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.paginator import OffsetPaginator from sentry.api.serializers import serialize @@ -14,6 +14,7 @@ @cell_silo_endpoint class GroupTombstoneEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_tombstone_details.py b/src/sentry/issues/endpoints/group_tombstone_details.py index 07f8f59ef549..64f9e57ead07 100644 --- a/src/sentry/issues/endpoints/group_tombstone_details.py +++ b/src/sentry/issues/endpoints/group_tombstone_details.py @@ -3,7 +3,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.utils import to_valid_int_id @@ -15,6 +15,7 @@ @cell_silo_endpoint class GroupTombstoneDetailsEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "DELETE": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/group_user_reports.py b/src/sentry/issues/endpoints/group_user_reports.py index 7f4a31842356..8d5ac3fa5a61 100644 --- a/src/sentry/issues/endpoints/group_user_reports.py +++ b/src/sentry/issues/endpoints/group_user_reports.py @@ -2,7 +2,7 @@ from rest_framework.response import Response from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.helpers.environments import get_environment from sentry.api.paginator import DateTimePaginator @@ -16,6 +16,8 @@ @cell_silo_endpoint class GroupUserReportsEndpoint(GroupEndpoint): + max_acceptable_duration = DEFAULT_DURATION_BUDGET + publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/organization_code_mapping_repo_prefixes.py b/src/sentry/issues/endpoints/organization_code_mapping_repo_prefixes.py index 5ac8b9ba4997..756c6bff9a94 100644 --- a/src/sentry/issues/endpoints/organization_code_mapping_repo_prefixes.py +++ b/src/sentry/issues/endpoints/organization_code_mapping_repo_prefixes.py @@ -9,7 +9,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import ( OrganizationEndpoint, OrganizationIntegrationsLoosePermission, @@ -111,6 +111,7 @@ def _get_repo_prefixes( @cell_silo_endpoint class OrganizationCodeMappingRepoPrefixesEndpoint(OrganizationEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = {"GET": ApiPublishStatus.EXPERIMENTAL} permission_classes = (OrganizationIntegrationsLoosePermission,) diff --git a/src/sentry/issues/endpoints/organization_codeowners_associations.py b/src/sentry/issues/endpoints/organization_codeowners_associations.py index af722a7a07ed..584688d1f23a 100644 --- a/src/sentry/issues/endpoints/organization_codeowners_associations.py +++ b/src/sentry/issues/endpoints/organization_codeowners_associations.py @@ -7,7 +7,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import ( OrganizationEndpoint, OrganizationIntegrationsLoosePermission, @@ -30,6 +30,7 @@ class CodeOwnersProjectAssociationsResponse(TypedDict): @cell_silo_endpoint class OrganizationCodeOwnersAssociationsEndpoint(OrganizationEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PUBLIC, } diff --git a/src/sentry/issues/endpoints/organization_derive_code_mappings.py b/src/sentry/issues/endpoints/organization_derive_code_mappings.py index f997c30f4e51..e46417f46f99 100644 --- a/src/sentry/issues/endpoints/organization_derive_code_mappings.py +++ b/src/sentry/issues/endpoints/organization_derive_code_mappings.py @@ -39,6 +39,7 @@ class OrganizationDeriveCodeMappingsEndpoint(OrganizationEndpoint): """ owner = ApiOwner.ISSUES + max_acceptable_duration = 10.0 publish_status = { "GET": ApiPublishStatus.EXPERIMENTAL, "POST": ApiPublishStatus.EXPERIMENTAL, diff --git a/src/sentry/issues/endpoints/organization_eventid.py b/src/sentry/issues/endpoints/organization_eventid.py index a4fedcfe64fa..da145a5da09d 100644 --- a/src/sentry/issues/endpoints/organization_eventid.py +++ b/src/sentry/issues/endpoints/organization_eventid.py @@ -9,7 +9,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize @@ -40,6 +40,7 @@ class EventIdLookupResponse(TypedDict): @extend_schema(tags=["Organizations"]) class EventIdLookupEndpoint(OrganizationEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PUBLIC, } diff --git a/src/sentry/issues/endpoints/organization_group_index.py b/src/sentry/issues/endpoints/organization_group_index.py index 02bfb9c0eb78..c6c1c9b632e0 100644 --- a/src/sentry/issues/endpoints/organization_group_index.py +++ b/src/sentry/issues/endpoints/organization_group_index.py @@ -16,7 +16,7 @@ from sentry.analytics.events.issue_search_endpoint_queried import IssueSearchEndpointQueriedEvent from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases import OrganizationEventPermission from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.event_search import SearchFilter @@ -273,6 +273,7 @@ class OrganizationGroupIndexEndpoint(OrganizationEndpoint): } idempotent_methods = frozenset() owner = ApiOwner.ISSUES + max_acceptable_duration = {"GET": DEFAULT_DURATION_BUDGET, "PUT": 10.0, "DELETE": 10.0} permission_classes = (OrganizationEventPermission,) enforce_rate_limit = True diff --git a/src/sentry/issues/endpoints/organization_group_index_stats.py b/src/sentry/issues/endpoints/organization_group_index_stats.py index 39a2151835c1..fb407c9c5190 100644 --- a/src/sentry/issues/endpoints/organization_group_index_stats.py +++ b/src/sentry/issues/endpoints/organization_group_index_stats.py @@ -4,7 +4,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases import OrganizationEventPermission from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.helpers.group_index import build_query_params_from_request, calculate_stats_period @@ -27,6 +27,7 @@ class OrganizationGroupIndexStatsEndpoint(OrganizationEndpoint): permission_classes = (OrganizationEventPermission,) enforce_rate_limit = True owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET rate_limits = RateLimitConfig( limit_overrides={ diff --git a/src/sentry/issues/endpoints/organization_group_search_view_details.py b/src/sentry/issues/endpoints/organization_group_search_view_details.py index 5bd145226447..4c4fe54c0a9c 100644 --- a/src/sentry/issues/endpoints/organization_group_search_view_details.py +++ b/src/sentry/issues/endpoints/organization_group_search_view_details.py @@ -7,7 +7,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.serializers.base import serialize from sentry.api.serializers.models.groupsearchview import GroupSearchViewSerializer @@ -40,6 +40,7 @@ class OrganizationGroupSearchViewDetailsEndpoint(OrganizationEndpoint): "DELETE": ApiPublishStatus.EXPERIMENTAL, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET permission_classes = (GroupSearchViewPermission,) def get(self, request: Request, organization: Organization, view_id: str) -> Response: diff --git a/src/sentry/issues/endpoints/organization_group_search_view_details_starred.py b/src/sentry/issues/endpoints/organization_group_search_view_details_starred.py index 97bb82139828..53b6689d26ed 100644 --- a/src/sentry/issues/endpoints/organization_group_search_view_details_starred.py +++ b/src/sentry/issues/endpoints/organization_group_search_view_details_starred.py @@ -4,7 +4,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.utils import to_valid_int_id from sentry.models.groupsearchview import GroupSearchView, GroupSearchViewVisibility @@ -34,6 +34,7 @@ class OrganizationGroupSearchViewDetailsStarredEndpoint(OrganizationEndpoint): "POST": ApiPublishStatus.EXPERIMENTAL, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET permission_classes = (MemberPermission,) def post(self, request: Request, organization: Organization, view_id: str) -> Response: diff --git a/src/sentry/issues/endpoints/organization_group_search_view_starred_order.py b/src/sentry/issues/endpoints/organization_group_search_view_starred_order.py index 87e57e3858ac..357ae496a07d 100644 --- a/src/sentry/issues/endpoints/organization_group_search_view_starred_order.py +++ b/src/sentry/issues/endpoints/organization_group_search_view_starred_order.py @@ -5,7 +5,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.models.groupsearchviewstarred import GroupSearchViewStarred from sentry.models.organization import Organization @@ -31,6 +31,7 @@ def validate_view_ids(self, view_ids: list[int]) -> list[int]: class OrganizationGroupSearchViewStarredOrderEndpoint(OrganizationEndpoint): publish_status = {"PUT": ApiPublishStatus.EXPERIMENTAL} owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET permission_classes = (MemberPermission,) def put(self, request: Request, organization: Organization) -> Response: diff --git a/src/sentry/issues/endpoints/organization_group_search_view_visit.py b/src/sentry/issues/endpoints/organization_group_search_view_visit.py index 53428443006e..745f3746a877 100644 --- a/src/sentry/issues/endpoints/organization_group_search_view_visit.py +++ b/src/sentry/issues/endpoints/organization_group_search_view_visit.py @@ -5,7 +5,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.utils import to_valid_int_id from sentry.models.groupsearchview import GroupSearchView @@ -25,6 +25,7 @@ class OrganizationGroupSearchViewVisitEndpoint(OrganizationEndpoint): "POST": ApiPublishStatus.PRIVATE, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET permission_classes = (MemberPermission,) def post(self, request: Request, organization: Organization, view_id: str) -> Response: diff --git a/src/sentry/issues/endpoints/organization_group_search_views.py b/src/sentry/issues/endpoints/organization_group_search_views.py index 689d3184fd47..ae1a3b823067 100644 --- a/src/sentry/issues/endpoints/organization_group_search_views.py +++ b/src/sentry/issues/endpoints/organization_group_search_views.py @@ -11,7 +11,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.paginator import ChainPaginator from sentry.api.serializers import serialize @@ -100,6 +100,7 @@ class OrganizationGroupSearchViewsEndpoint(OrganizationEndpoint): } idempotent_methods = frozenset() owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET permission_classes = (MemberPermission,) @extend_schema( diff --git a/src/sentry/issues/endpoints/organization_group_search_views_starred.py b/src/sentry/issues/endpoints/organization_group_search_views_starred.py index f74e3f63dae0..821ddc5b85c8 100644 --- a/src/sentry/issues/endpoints/organization_group_search_views_starred.py +++ b/src/sentry/issues/endpoints/organization_group_search_views_starred.py @@ -3,7 +3,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.serializers import serialize from sentry.api.serializers.models.groupsearchviewstarred import GroupSearchViewStarredSerializer @@ -23,6 +23,7 @@ class OrganizationGroupSearchViewsStarredEndpoint(OrganizationEndpoint): "GET": ApiPublishStatus.EXPERIMENTAL, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET permission_classes = (MemberPermission,) def get(self, request: Request, organization: Organization) -> Response: diff --git a/src/sentry/issues/endpoints/organization_issues_count.py b/src/sentry/issues/endpoints/organization_issues_count.py index d82c1b265ada..4aec14431499 100644 --- a/src/sentry/issues/endpoints/organization_issues_count.py +++ b/src/sentry/issues/endpoints/organization_issues_count.py @@ -8,7 +8,7 @@ from sentry import search from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.helpers.group_index import validate_search_filter_permissions from sentry.api.helpers.group_index.validators import ValidationError @@ -35,6 +35,7 @@ class OrganizationIssuesCountEndpoint(OrganizationEndpoint): "GET": ApiPublishStatus.PRIVATE, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET enforce_rate_limit = True rate_limits = RateLimitConfig( limit_overrides={ diff --git a/src/sentry/issues/endpoints/organization_issues_resolved_in_release.py b/src/sentry/issues/endpoints/organization_issues_resolved_in_release.py index 1454bf19fc28..8a0f1e326071 100644 --- a/src/sentry/issues/endpoints/organization_issues_resolved_in_release.py +++ b/src/sentry/issues/endpoints/organization_issues_resolved_in_release.py @@ -3,7 +3,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.helpers.releases import get_group_ids_resolved_in_release from sentry.api.serializers import serialize @@ -16,6 +16,7 @@ @cell_silo_endpoint class OrganizationIssuesResolvedInReleaseEndpoint(OrganizationEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/organization_issues_with_supergroups.py b/src/sentry/issues/endpoints/organization_issues_with_supergroups.py index b598bcc36fee..8126801912d5 100644 --- a/src/sentry/issues/endpoints/organization_issues_with_supergroups.py +++ b/src/sentry/issues/endpoints/organization_issues_with_supergroups.py @@ -10,7 +10,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases import OrganizationEventPermission from sentry.api.bases.organization import OrganizationEndpoint from sentry.api.helpers.group_index import calculate_stats_period @@ -34,6 +34,7 @@ class OrganizationIssuesWithSupergroupsEndpoint(OrganizationEndpoint): "GET": ApiPublishStatus.EXPERIMENTAL, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET permission_classes = (OrganizationEventPermission,) enforce_rate_limit = True diff --git a/src/sentry/issues/endpoints/organization_release_previous_commits.py b/src/sentry/issues/endpoints/organization_release_previous_commits.py index 8f64ce34cd40..c130e61ac919 100644 --- a/src/sentry/issues/endpoints/organization_release_previous_commits.py +++ b/src/sentry/issues/endpoints/organization_release_previous_commits.py @@ -6,7 +6,7 @@ from sentry.analytics.events.release_get_previous_commits import ReleaseGetPreviousCommitsEvent from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.organization import NoProjects, OrganizationReleasesBaseEndpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize @@ -23,6 +23,7 @@ class OrganizationReleasePreviousCommitsEndpoint(OrganizationReleasesBaseEndpoin "GET": ApiPublishStatus.PRIVATE, } owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET rate_limits = RateLimitConfig(group="CLI") def get(self, request: Request, organization: Organization, version: str) -> Response: diff --git a/src/sentry/issues/endpoints/organization_shortid.py b/src/sentry/issues/endpoints/organization_shortid.py index 7814580c3e2a..0b877b2afc6a 100644 --- a/src/sentry/issues/endpoints/organization_shortid.py +++ b/src/sentry/issues/endpoints/organization_shortid.py @@ -6,7 +6,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.serializers import serialize from sentry.api.serializers.models.group import BaseGroupSerializerResponse from sentry.apidocs.constants import ( @@ -33,6 +33,7 @@ class ShortIdLookupResponse(TypedDict): @cell_silo_endpoint class ShortIdLookupEndpoint(GroupEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PUBLIC, } diff --git a/src/sentry/issues/endpoints/project_codeowners_details.py b/src/sentry/issues/endpoints/project_codeowners_details.py index 515156e83052..fd8a0d14c5f0 100644 --- a/src/sentry/issues/endpoints/project_codeowners_details.py +++ b/src/sentry/issues/endpoints/project_codeowners_details.py @@ -13,7 +13,7 @@ from sentry.analytics.events.codeowners_updated import CodeOwnersUpdated from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize from sentry.api.serializers.models.projectcodeowners import ( @@ -56,6 +56,7 @@ @extend_schema(tags=["Projects"]) class ProjectCodeOwnersDetailsEndpoint(ProjectCodeOwnersBase): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "DELETE": ApiPublishStatus.PUBLIC, "GET": ApiPublishStatus.PUBLIC, diff --git a/src/sentry/issues/endpoints/project_codeowners_index.py b/src/sentry/issues/endpoints/project_codeowners_index.py index f5d73b6a3045..73c793defed7 100644 --- a/src/sentry/issues/endpoints/project_codeowners_index.py +++ b/src/sentry/issues/endpoints/project_codeowners_index.py @@ -9,7 +9,7 @@ from sentry.analytics.events.codeowners_created import CodeOwnersCreated from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.serializers import serialize from sentry.api.serializers.models.projectcodeowners import ( DEFAULT_CODEOWNERS_EXPAND, @@ -39,6 +39,7 @@ @extend_schema(tags=["Projects"]) class ProjectCodeOwnersEndpoint(ProjectCodeOwnersBase): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PUBLIC, "POST": ApiPublishStatus.PUBLIC, diff --git a/src/sentry/issues/endpoints/project_event_details.py b/src/sentry/issues/endpoints/project_event_details.py index 369bb633b78e..87a822576e30 100644 --- a/src/sentry/issues/endpoints/project_event_details.py +++ b/src/sentry/issues/endpoints/project_event_details.py @@ -11,7 +11,7 @@ from sentry import options from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.serializers import EventSerializer, IssueEventSerializer, serialize from sentry.api.serializers.models.event import EventSerializerResponse, GroupEventDetailsResponse @@ -113,6 +113,7 @@ def wrap_event_response( @cell_silo_endpoint class ProjectEventDetailsEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PUBLIC, } @@ -211,6 +212,7 @@ def get( @cell_silo_endpoint class EventJsonEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.EXPERIMENTAL, } diff --git a/src/sentry/issues/endpoints/project_events.py b/src/sentry/issues/endpoints/project_events.py index bd7d02b0eee1..30fed1e60b33 100644 --- a/src/sentry/issues/endpoints/project_events.py +++ b/src/sentry/issues/endpoints/project_events.py @@ -33,6 +33,7 @@ @cell_silo_endpoint class ProjectEventsEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = 10.0 publish_status = { "GET": ApiPublishStatus.PUBLIC, } diff --git a/src/sentry/issues/endpoints/project_group_index.py b/src/sentry/issues/endpoints/project_group_index.py index 794a0f565727..2eaf20ee6d51 100644 --- a/src/sentry/issues/endpoints/project_group_index.py +++ b/src/sentry/issues/endpoints/project_group_index.py @@ -10,7 +10,7 @@ from sentry.analytics.events.project_issue_searched import ProjectIssueSearchEvent from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectEventPermission from sentry.api.helpers.environments import get_environment_func from sentry.api.helpers.group_index import ( @@ -50,6 +50,7 @@ @cell_silo_endpoint class ProjectGroupIndexEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "DELETE": ApiPublishStatus.EXPERIMENTAL, "GET": ApiPublishStatus.EXPERIMENTAL, diff --git a/src/sentry/issues/endpoints/project_group_stats.py b/src/sentry/issues/endpoints/project_group_stats.py index 91b6e683d317..add451b80736 100644 --- a/src/sentry/issues/endpoints/project_group_stats.py +++ b/src/sentry/issues/endpoints/project_group_stats.py @@ -4,7 +4,7 @@ from sentry import tsdb from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import StatsMixin, cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, StatsMixin, cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.helpers.environments import get_environment_id @@ -19,6 +19,7 @@ @cell_silo_endpoint class ProjectGroupStatsEndpoint(ProjectEndpoint, StatsMixin): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/project_issues_resolved_in_release.py b/src/sentry/issues/endpoints/project_issues_resolved_in_release.py index b74f8b33bc77..0eb2301a0df6 100644 --- a/src/sentry/issues/endpoints/project_issues_resolved_in_release.py +++ b/src/sentry/issues/endpoints/project_issues_resolved_in_release.py @@ -3,7 +3,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectPermission from sentry.api.helpers.environments import get_environment_func from sentry.api.helpers.releases import get_group_ids_resolved_in_release @@ -16,6 +16,7 @@ @cell_silo_endpoint class ProjectIssuesResolvedInReleaseEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.EXPERIMENTAL, } diff --git a/src/sentry/issues/endpoints/project_ownership.py b/src/sentry/issues/endpoints/project_ownership.py index a94508aa06c5..c643900cf0f3 100644 --- a/src/sentry/issues/endpoints/project_ownership.py +++ b/src/sentry/issues/endpoints/project_ownership.py @@ -11,7 +11,7 @@ from sentry import audit_log, features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint, ProjectOwnershipPermission from sentry.api.serializers import serialize from sentry.api.serializers.models.projectownership import ( @@ -237,6 +237,7 @@ def __modify_auto_assignment(self, ownership: ProjectOwnership) -> bool: @extend_schema(tags=["Projects"]) class ProjectOwnershipEndpoint(ProjectEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = {"GET": DEFAULT_DURATION_BUDGET, "PUT": 10.0} publish_status = { "GET": ApiPublishStatus.PUBLIC, "PUT": ApiPublishStatus.PUBLIC, diff --git a/src/sentry/issues/endpoints/project_stacktrace_link.py b/src/sentry/issues/endpoints/project_stacktrace_link.py index 78ba1c6baf6e..201066cf5f11 100644 --- a/src/sentry/issues/endpoints/project_stacktrace_link.py +++ b/src/sentry/issues/endpoints/project_stacktrace_link.py @@ -183,6 +183,7 @@ class ProjectStacktraceLinkEndpoint(ProjectEndpoint): """ owner = ApiOwner.ISSUES + max_acceptable_duration = 10.0 @extend_schema( operation_id="getProjectStacktraceLink", diff --git a/src/sentry/issues/endpoints/project_stacktrace_source_context.py b/src/sentry/issues/endpoints/project_stacktrace_source_context.py index 395cb9d4d43c..5a38d04160f2 100644 --- a/src/sentry/issues/endpoints/project_stacktrace_source_context.py +++ b/src/sentry/issues/endpoints/project_stacktrace_source_context.py @@ -37,6 +37,7 @@ class ProjectStacktraceSourceContextEndpoint(ProjectEndpoint): """ owner = ApiOwner.ISSUES + max_acceptable_duration = 10.0 def get(self, request: Request, project: Project) -> Response: if not project.get_option("sentry:scm_source_context_enabled", False): diff --git a/src/sentry/issues/endpoints/related_issues.py b/src/sentry/issues/endpoints/related_issues.py index bd85aa27e0f2..3c2cc015995f 100644 --- a/src/sentry/issues/endpoints/related_issues.py +++ b/src/sentry/issues/endpoints/related_issues.py @@ -4,7 +4,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.helpers.deprecation import deprecated from sentry.api.helpers.projects import filter_projects_by_permissions from sentry.constants import CELL_API_DEPRECATION_DATE, ObjectStatus @@ -26,6 +26,7 @@ class RequestSerializer(serializers.Serializer[None]): @cell_silo_endpoint class RelatedIssuesEndpoint(GroupEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = {"GET": ApiPublishStatus.EXPERIMENTAL} enforce_rate_limit = True rate_limits = RateLimitConfig( diff --git a/src/sentry/issues/endpoints/shared_group_details.py b/src/sentry/issues/endpoints/shared_group_details.py index 0fd6699d5561..e8b7b1f357b8 100644 --- a/src/sentry/issues/endpoints/shared_group_details.py +++ b/src/sentry/issues/endpoints/shared_group_details.py @@ -5,7 +5,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import Endpoint, cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, Endpoint, cell_silo_endpoint from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.helpers.environments import get_environment_func from sentry.api.serializers import SharedGroupSerializer, serialize @@ -15,6 +15,7 @@ @cell_silo_endpoint class SharedGroupDetailsEndpoint(Endpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/team_all_unresolved_issues.py b/src/sentry/issues/endpoints/team_all_unresolved_issues.py index 18f8af8eebb4..b2689f482004 100644 --- a/src/sentry/issues/endpoints/team_all_unresolved_issues.py +++ b/src/sentry/issues/endpoints/team_all_unresolved_issues.py @@ -13,7 +13,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.helpers.environments import get_environments from sentry.api.utils import get_date_range_from_params @@ -166,6 +166,7 @@ def calculate_unresolved_counts( @cell_silo_endpoint class TeamAllUnresolvedIssuesEndpoint(TeamEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/team_groups_old.py b/src/sentry/issues/endpoints/team_groups_old.py index 68f02e5ecd08..ad0fd6d3c6b4 100644 --- a/src/sentry/issues/endpoints/team_groups_old.py +++ b/src/sentry/issues/endpoints/team_groups_old.py @@ -6,7 +6,7 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.helpers.environments import get_environment_func, get_environments from sentry.api.serializers import GroupSerializer, serialize @@ -17,6 +17,7 @@ @cell_silo_endpoint class TeamGroupsOldEndpoint(TeamEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/issues/endpoints/team_issue_breakdown.py b/src/sentry/issues/endpoints/team_issue_breakdown.py index 9041618c51df..470783b30f7f 100644 --- a/src/sentry/issues/endpoints/team_issue_breakdown.py +++ b/src/sentry/issues/endpoints/team_issue_breakdown.py @@ -10,7 +10,7 @@ from sentry import features from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint +from sentry.api.base import DEFAULT_DURATION_BUDGET, cell_silo_endpoint from sentry.api.bases.team import TeamEndpoint from sentry.api.helpers.environments import get_environments from sentry.api.utils import get_date_range_from_params @@ -29,6 +29,7 @@ @cell_silo_endpoint class TeamIssueBreakdownEndpoint(TeamEndpoint): owner = ApiOwner.ISSUES + max_acceptable_duration = DEFAULT_DURATION_BUDGET publish_status = { "GET": ApiPublishStatus.PRIVATE, } diff --git a/src/sentry/middleware/stats.py b/src/sentry/middleware/stats.py index d3f827aa8493..03ddd08cacf9 100644 --- a/src/sentry/middleware/stats.py +++ b/src/sentry/middleware/stats.py @@ -44,6 +44,14 @@ def process_view( if path and path.startswith(self.allowed_paths): setattr(request, "_view_path", path) setattr(request, "_start_time", time.monotonic()) + + view_class = getattr(view_func, "view_class", None) + max_acceptable_duration = getattr(view_class, "max_acceptable_duration", None) + if isinstance(max_acceptable_duration, dict): + max_acceptable_duration = max_acceptable_duration.get(request.method) + + setattr(request, "_max_acceptable_duration", max_acceptable_duration) + setattr(request, "_view_owner", getattr(view_class, "owner", None)) return None def process_response(self, request: Request, response: Response) -> Response: @@ -83,7 +91,8 @@ def _record_time(request: Request, status_code: int) -> None: if start_time is None: return - ms = int((time.monotonic() - start_time) * 1000) + duration = time.monotonic() - start_time + ms = int(duration * 1000) metrics.distribution( "view.duration", ms, @@ -91,3 +100,19 @@ def _record_time(request: Request, status_code: int) -> None: tags={"method": request.method}, unit="millisecond", ) + + max_acceptable_duration = getattr(request, "_max_acceptable_duration", None) + if max_acceptable_duration is not None and duration > max_acceptable_duration: + owner = getattr(request, "_view_owner", None) + metrics.distribution( + "view.duration_over_budget", + (duration - max_acceptable_duration) * 1000, + instance=view_path, + tags={ + "method": request.method, + "owner": getattr(owner, "value", owner), + "ui_request": tags["ui_request"], + }, + sample_rate=1.0, + unit="millisecond", + ) diff --git a/tests/sentry/middleware/test_stats.py b/tests/sentry/middleware/test_stats.py index 493ab16b7445..32b925c2f9b0 100644 --- a/tests/sentry/middleware/test_stats.py +++ b/tests/sentry/middleware/test_stats.py @@ -1,11 +1,12 @@ from functools import cached_property from typing import Any -from unittest.mock import MagicMock, Mock, patch, sentinel +from unittest.mock import MagicMock, Mock, call, patch, sentinel from django.test import RequestFactory, override_settings from django.urls.base import resolve from rest_framework.permissions import AllowAny +from sentry.api.api_owners import ApiOwner from sentry.api.base import Endpoint from sentry.middleware.ratelimit import RatelimitMiddleware from sentry.middleware.stats import RequestTimingMiddleware @@ -26,6 +27,15 @@ def get(self) -> None: raise NotImplementedError +class BudgetedEndpoint(Endpoint): + owner = ApiOwner.ISSUES + max_acceptable_duration = 3.0 + + +class MethodBudgetedEndpoint(Endpoint): + max_acceptable_duration = {"GET": 3.0, "POST": 5.0} + + class RequestTimingMiddlewareTest(TestCase): middleware = cached_property(RequestTimingMiddleware) @@ -105,3 +115,73 @@ def test_records_ui_request(self, incr: MagicMock) -> None: }, skip_internal=False, ) + + @patch("sentry.utils.metrics.distribution") + @patch("sentry.middleware.stats.time.monotonic", side_effect=[10.0, 14.0]) + def test_records_duration_over_budget( + self, mock_time: MagicMock, distribution: MagicMock + ) -> None: + view = BudgetedEndpoint.as_view() + request = self.factory.get("/") + request.resolver_match = resolve("/") + response = Mock(status_code=500) + + with patch.object(RequestTimingMiddleware, "allowed_paths", (view.__module__,)): + self.middleware.process_view(request, view, [], {}) + self.middleware.process_response(request, response) + + assert mock_time.call_count == 2 + distribution.assert_has_calls( + [ + call( + "view.duration", + 4000, + instance=request._view_path, + tags={"method": "GET"}, + unit="millisecond", + ), + call( + "view.duration_over_budget", + 1000.0, + instance=request._view_path, + tags={"method": "GET", "owner": "issues-feed", "ui_request": False}, + sample_rate=1.0, + unit="millisecond", + ), + ] + ) + + @patch("sentry.utils.metrics.distribution") + @patch("sentry.middleware.stats.time.monotonic", side_effect=[10.0, 13.0]) + def test_does_not_record_duration_at_budget( + self, mock_time: MagicMock, distribution: MagicMock + ) -> None: + view = BudgetedEndpoint.as_view() + request = self.factory.get("/") + request.resolver_match = resolve("/") + response = Mock(status_code=200) + + with patch.object(RequestTimingMiddleware, "allowed_paths", (view.__module__,)): + self.middleware.process_view(request, view, [], {}) + self.middleware.process_response(request, response) + + assert mock_time.call_count == 2 + distribution.assert_called_once_with( + "view.duration", + 3000, + instance=request._view_path, + tags={"method": "GET"}, + unit="millisecond", + ) + + def test_resolves_method_specific_duration_budget(self) -> None: + view = MethodBudgetedEndpoint.as_view() + get_request = self.factory.get("/") + delete_request = self.factory.delete("/") + + with patch.object(RequestTimingMiddleware, "allowed_paths", (view.__module__,)): + self.middleware.process_view(get_request, view, [], {}) + self.middleware.process_view(delete_request, view, [], {}) + + assert get_request._max_acceptable_duration == 3.0 + assert delete_request._max_acceptable_duration is None