diff --git a/pyproject.toml b/pyproject.toml index a79bfad134bd..15bf09306e0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -508,7 +508,6 @@ module = [ "sentry.api.endpoints.organization_events_meta", "sentry.api.endpoints.organization_events_stats", "sentry.api.endpoints.organization_events_timeseries", - "sentry.api.endpoints.organization_events_vitals", "sentry.api.endpoints.organization_fork", "sentry.api.endpoints.organization_insights_tree", "sentry.api.endpoints.organization_intercom_jwt", diff --git a/src/sentry/api/endpoints/organization_events_vitals.py b/src/sentry/api/endpoints/organization_events_vitals.py deleted file mode 100644 index 9b7fee2435f3..000000000000 --- a/src/sentry/api/endpoints/organization_events_vitals.py +++ /dev/null @@ -1,90 +0,0 @@ -from rest_framework.exceptions import ParseError -from rest_framework.request import Request -from rest_framework.response import Response -from sentry_sdk import traces - -from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint -from sentry.api.bases import NoProjects, OrganizationEventsEndpointBase -from sentry.api.utils import handle_query_errors -from sentry.models.organization import Organization -from sentry.search.events.fields import get_function_alias -from sentry.snuba import discover - - -@cell_silo_endpoint -class OrganizationEventsVitalsEndpoint(OrganizationEventsEndpointBase): - publish_status = { - "GET": ApiPublishStatus.PRIVATE, - } - VITALS = { - "measurements.lcp": {"thresholds": [0, 2500, 4000]}, - "measurements.fid": {"thresholds": [0, 100, 300]}, - "measurements.cls": {"thresholds": [0, 0.1, 0.25]}, - "measurements.fcp": {"thresholds": [0, 1000, 3000]}, - "measurements.fp": {"thresholds": [0, 1000, 3000]}, - } - - def get(self, request: Request, organization: Organization) -> Response: - if not self.has_feature(organization, request): - return Response(status=404) - - with traces.start_span(name="parse params", attributes={"sentry.op": "discover.endpoint"}): - try: - snuba_params = self.get_snuba_params(request, organization) - except NoProjects: - return Response([]) - - vitals = [vital.lower() for vital in request.GET.getlist("vital", [])] - if len(vitals) == 0: - raise ParseError(detail="Need to pass at least one vital") - - allow_metric_aggregates = request.GET.get("preventMetricAggregates") != "1" - - selected_columns = [] - for vital in vitals: - if vital not in self.VITALS: - raise ParseError(detail=f"{vital} is not a valid vital") - selected_columns.extend( - [ - f"p75({vital})", - f"count_web_vitals({vital}, good)", - f"count_web_vitals({vital}, meh)", - f"count_web_vitals({vital}, poor)", - f"count_web_vitals({vital}, any)", - ] - ) - - with handle_query_errors(): - events_results = discover.query( - selected_columns=selected_columns, - query=request.GET.get("query", ""), - snuba_params=snuba_params, - # Results should only ever have 1 result - limit=1, - referrer="api.events.vitals", - auto_fields=True, - auto_aggregations=False, - use_aggregate_conditions=False, - allow_metric_aggregates=allow_metric_aggregates, - transform_alias_to_input_format=False, - ) - - results = {} - if len(events_results["data"]) == 1: - event_data = events_results["data"][0] - for vital in vitals: - results[vital] = { - "p75": event_data.get(get_function_alias(f"p75({vital})")), - "total": event_data.get(get_function_alias(f"count_web_vitals({vital}, any)")) - or 0, - "good": event_data.get(get_function_alias(f"count_web_vitals({vital}, good)")) - or 0, - "meh": event_data.get(get_function_alias(f"count_web_vitals({vital}, meh)")) - or 0, - "poor": event_data.get(get_function_alias(f"count_web_vitals({vital}, poor)")) - or 0, - } - results["meta"] = {"isMetricsData": events_results["meta"].get("isMetricsData", False)} - - return Response(results) diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 1da1993cea69..ce25bda5a2ac 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -817,7 +817,6 @@ from .endpoints.organization_events_stats import OrganizationEventsStatsEndpoint from .endpoints.organization_events_timeseries import OrganizationEventsTimeseriesEndpoint from .endpoints.organization_events_validate import OrganizationEventsValidateEndpoint -from .endpoints.organization_events_vitals import OrganizationEventsVitalsEndpoint from .endpoints.organization_measurements_meta import OrganizationMeasurementsMeta from .endpoints.organization_metrics_meta import ( OrganizationMetricsCompatibility, @@ -1955,11 +1954,6 @@ def create_group_urls( OrganizationEventsHistogramEndpoint.as_view(), name="sentry-api-0-organization-events-histogram", ), - re_path( - r"^(?P[^/]+)/events-vitals/$", - OrganizationEventsVitalsEndpoint.as_view(), - name="sentry-api-0-organization-events-vitals", - ), re_path( r"^(?P[^/]+)/trace/(?P(?:\d+|[A-Fa-f0-9-]{32,36}))/$", OrganizationTraceEndpoint.as_view(), diff --git a/src/sentry/conf/api_pagination_allowlist_do_not_modify.py b/src/sentry/conf/api_pagination_allowlist_do_not_modify.py index 8340268a0c2d..6ad8d31dcc81 100644 --- a/src/sentry/conf/api_pagination_allowlist_do_not_modify.py +++ b/src/sentry/conf/api_pagination_allowlist_do_not_modify.py @@ -35,7 +35,6 @@ "OrganizationEnvironmentsEndpoint", "OrganizationEventsFacetsEndpoint", "OrganizationEventsRelatedIssuesEndpoint", - "OrganizationEventsVitalsEndpoint", "OrganizationGroupIndexEndpoint", "OrganizationGroupIndexStatsEndpoint", "OrganizationIndexEndpoint", diff --git a/static/app/utils/api/knownSentryApiUrls.generated.ts b/static/app/utils/api/knownSentryApiUrls.generated.ts index b5f65b3330af..a20e2215d17d 100644 --- a/static/app/utils/api/knownSentryApiUrls.generated.ts +++ b/static/app/utils/api/knownSentryApiUrls.generated.ts @@ -149,7 +149,6 @@ export type KnownSentryApiUrls = | '/organizations/$organizationIdOrSlug/events-sql/' | '/organizations/$organizationIdOrSlug/events-stats/' | '/organizations/$organizationIdOrSlug/events-timeseries/' - | '/organizations/$organizationIdOrSlug/events-vitals/' | '/organizations/$organizationIdOrSlug/events/' | '/organizations/$organizationIdOrSlug/events/$projectIdOrSlug:$eventId/' | '/organizations/$organizationIdOrSlug/events/anomalies/' diff --git a/tests/snuba/api/endpoints/test_organization_events_vitals.py b/tests/snuba/api/endpoints/test_organization_events_vitals.py deleted file mode 100644 index e2332d54710c..000000000000 --- a/tests/snuba/api/endpoints/test_organization_events_vitals.py +++ /dev/null @@ -1,296 +0,0 @@ -from datetime import timedelta - -import pytest -from django.urls import reverse - -from sentry.models.transaction_threshold import ProjectTransactionThreshold, TransactionMetric -from sentry.testutils.cases import APITestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now -from sentry.utils.samples import load_data - -pytestmark = pytest.mark.sentry_metrics - - -class OrganizationEventsVitalsEndpointTest(APITestCase, SnubaTestCase): - def setUp(self) -> None: - super().setUp() - self.start = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0) - self.end = self.start + timedelta(hours=6) - - self.transaction_data = load_data("transaction", timestamp=self.start) - self.query: dict[str, str | list[str]] = { - "start": self.start.isoformat(), - "end": self.end.isoformat(), - } - self.features: dict[str, bool] = {} - - def store_event(self, data, measurements=None, **kwargs): - if measurements: - for vital, value in measurements.items(): - data["measurements"][vital]["value"] = value - - return super().store_event( - data.copy(), - project_id=self.project.id, - ) - - def do_request(self, query=None, features=None): - if features is None: - features = {"organizations:discover-basic": True} - features.update(self.features) - if query is None: - query = self.query - - self.login_as(user=self.user) - url = reverse( - "sentry-api-0-organization-events-vitals", - kwargs={"organization_id_or_slug": self.organization.slug}, - ) - - with self.feature(features): - return self.client.get(url, query, format="json") - - def test_no_projects(self) -> None: - response = self.do_request() - assert response.status_code == 200, response.content - assert len(response.data) == 0 - - def test_no_vitals(self) -> None: - self.store_event( - self.transaction_data, - project_id=self.project.id, - ) - - self.query.update({"vital": []}) - response = self.do_request() - assert response.status_code == 400, response.content - assert "Need to pass at least one vital" == response.data["detail"] - - def test_bad_vital(self) -> None: - self.store_event( - self.transaction_data, - project_id=self.project.id, - ) - - self.query.update({"vital": ["foobar"]}) - response = self.do_request() - assert response.status_code == 400, response.content - assert "foobar is not a valid vital" == response.data["detail"] - - def test_simple(self) -> None: - data = self.transaction_data.copy() - for lcp in [2000, 3000, 5000]: - self.store_event( - data, - {"lcp": lcp}, - project_id=self.project.id, - ) - - self.query.update({"vital": ["measurements.lcp"]}) - response = self.do_request() - assert response.status_code == 200, response.content - assert not response.data["meta"]["isMetricsData"] - assert response.data["measurements.lcp"] == { - "good": 1, - "meh": 1, - "poor": 1, - "total": 3, - "p75": 4000, - } - - def test_simple_with_refining_user_misery_filter(self) -> None: - project1 = self.create_project(organization=self.organization) - project2 = self.create_project(organization=self.organization) - ProjectTransactionThreshold.objects.create( - project=project1, - organization=project1.organization, - threshold=100, - metric=TransactionMetric.LCP.value, - ) - - ProjectTransactionThreshold.objects.create( - project=project2, - organization=project2.organization, - threshold=1000, - metric=TransactionMetric.LCP.value, - ) - - data = self.transaction_data.copy() - - for project in [project1, project2]: - for lcp in [2000, 3000, 5000]: - self.store_event( - data, - {"lcp": lcp}, - project_id=project.id, - ) - - self.query.update({"vital": ["measurements.lcp"]}) - response = self.do_request(features={"organizations:discover-basic": True}) - - assert response.status_code == 200, response.content - assert not response.data["meta"]["isMetricsData"] - assert response.data["measurements.lcp"] == { - "good": 0, - "meh": 1, - "poor": 1, - "total": 2, - "p75": 4500, - } - - self.query.update({"query": "user_misery():<0.04"}) - response = self.do_request(features={"organizations:discover-basic": True}) - - assert response.status_code == 200, response.content - assert len(response.data) == 2 - assert not response.data["meta"]["isMetricsData"] - assert response.data["measurements.lcp"] == { - "good": 0, - "meh": 1, - "poor": 1, - "total": 2, - "p75": 4500, - } - - def test_grouping(self) -> None: - counts = [ - (100, 2), - (3000, 3), - (4500, 1), - ] - for duration, count in counts: - for _ in range(count): - self.store_event( - load_data("transaction", timestamp=self.start), - {"lcp": duration}, - project_id=self.project.id, - ) - - self.query.update({"vital": ["measurements.lcp"]}) - response = self.do_request() - assert response.status_code == 200 - assert not response.data["meta"]["isMetricsData"] - assert response.data["measurements.lcp"] == { - "good": 2, - "meh": 3, - "poor": 1, - "total": 6, - "p75": 3000, - } - - def test_multiple_vitals(self) -> None: - vitals = {"lcp": 3000, "fid": 50, "cls": 0.15, "fcp": 5000, "fp": 4000} - self.store_event( - load_data("transaction", timestamp=self.start), - vitals, - project_id=self.project.id, - ) - - self.query.update( - { - "vital": [ - "measurements.lcp", - "measurements.fid", - "measurements.cls", - "measurements.fcp", - "measurements.fp", - ] - } - ) - response = self.do_request() - assert response.status_code == 200 - assert not response.data["meta"]["isMetricsData"] - assert response.data["measurements.lcp"] == { - "good": 0, - "meh": 1, - "poor": 0, - "total": 1, - "p75": 3000, - } - assert response.data["measurements.fid"] == { - "good": 1, - "meh": 0, - "poor": 0, - "total": 1, - "p75": 50, - } - assert response.data["measurements.cls"] == { - "good": 0, - "meh": 1, - "poor": 0, - "total": 1, - "p75": 0.15, - } - assert response.data["measurements.fcp"] == { - "good": 0, - "meh": 0, - "poor": 1, - "total": 1, - "p75": 5000, - } - assert response.data["measurements.fp"] == { - "good": 0, - "meh": 0, - "poor": 1, - "total": 1, - "p75": 4000, - } - - def test_transactions_without_vitals(self) -> None: - del self.transaction_data["measurements"] - self.store_event( - self.transaction_data, - project_id=self.project.id, - ) - - self.query.update({"vital": ["measurements.lcp", "measurements.fcp"]}) - response = self.do_request() - assert response.status_code == 200, response.data - assert not response.data["meta"]["isMetricsData"] - assert response.data["measurements.lcp"] == { - "good": 0, - "meh": 0, - "poor": 0, - "total": 0, - "p75": None, - } - assert response.data["measurements.fcp"] == { - "good": 0, - "meh": 0, - "poor": 0, - "total": 0, - "p75": None, - } - - def test_edges_of_vital_thresholds(self) -> None: - self.store_event( - load_data("transaction", timestamp=self.start), - {"lcp": 4000, "fp": 1000, "fcp": 0}, - project_id=self.project.id, - ) - - self.query.update({"vital": ["measurements.lcp", "measurements.fp", "measurements.fcp"]}) - response = self.do_request() - assert response.status_code == 200, response.data - assert not response.data["meta"]["isMetricsData"] - assert response.data["measurements.lcp"] == { - "good": 0, - "meh": 0, - "poor": 1, - "total": 1, - "p75": 4000, - } - assert response.data["measurements.fp"] == { - "good": 0, - "meh": 1, - "poor": 0, - "total": 1, - "p75": 1000, - } - assert response.data["measurements.fcp"] == { - "good": 1, - "meh": 0, - "poor": 0, - "total": 1, - "p75": 0, - }