diff --git a/pyproject.toml b/pyproject.toml index a79bfad134bd..cdfc178d319c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -503,7 +503,6 @@ module = [ "sentry.api.endpoints.organization_details", "sentry.api.endpoints.organization_events", "sentry.api.endpoints.organization_events_facets", - "sentry.api.endpoints.organization_events_facets_performance", "sentry.api.endpoints.organization_events_histogram", "sentry.api.endpoints.organization_events_meta", "sentry.api.endpoints.organization_events_stats", diff --git a/src/sentry/api/endpoints/organization_events_facets_performance.py b/src/sentry/api/endpoints/organization_events_facets_performance.py deleted file mode 100644 index 1b6ee17ce1b9..000000000000 --- a/src/sentry/api/endpoints/organization_events_facets_performance.py +++ /dev/null @@ -1,523 +0,0 @@ -import math -from collections.abc import Mapping -from typing import Any - -from django.http import Http404 -from rest_framework.exceptions import ParseError -from rest_framework.request import Request -from rest_framework.response import Response -from sentry_sdk import traces -from snuba_sdk import Column, Condition, Function, Op - -from sentry import features, tagstore -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.paginator import GenericOffsetPaginator -from sentry.api.utils import handle_query_errors -from sentry.models.organization import Organization -from sentry.search.events.builder.discover import DiscoverQueryBuilder -from sentry.search.events.types import EventsResponse, SnubaParams -from sentry.snuba import discover -from sentry.snuba.dataset import Dataset -from sentry.utils.cursors import Cursor, CursorResult - -ALLOWED_AGGREGATE_COLUMNS = { - "transaction.duration", - "measurements.lcp", - "measurements.cls", - "measurements.fcp", - "measurements.fid", - "measurements.inp", - "spans.browser", - "spans.http", - "spans.db", - "spans.resource", -} - -TAG_ALIASES = {"release": "sentry:release", "dist": "sentry:dist", "user": "sentry:user"} -DEFAULT_TAG_KEY_LIMIT = 5 - - -class OrganizationEventsFacetsPerformanceEndpointBase(OrganizationEventsEndpointBase): - publish_status = { - "GET": ApiPublishStatus.PRIVATE, - } - - def has_feature(self, organization, request): - return features.has("organizations:performance-view", organization, actor=request.user) - - # NOTE: This used to be called setup, but since Django 2.2 it's a View method. - # We don't fit its semantics, but I couldn't think of a better name, and - # it's only used in child classes. - def _setup(self, request: Request, organization): - if not self.has_feature(organization, request): - raise Http404 - - snuba_params = self.get_snuba_params(request, organization) - - filter_query = request.GET.get("query") - aggregate_column = request.GET.get("aggregateColumn") - - if not aggregate_column: - raise ParseError(detail="'aggregateColumn' must be provided.") - - if aggregate_column not in ALLOWED_AGGREGATE_COLUMNS: - raise ParseError(detail=f"'{aggregate_column}' is not a supported tags column.") - - if len(snuba_params.project_ids) > 1: - raise ParseError(detail="You cannot view facet performance for multiple projects.") - - return snuba_params, aggregate_column, filter_query - - -@cell_silo_endpoint -class OrganizationEventsFacetsPerformanceEndpoint(OrganizationEventsFacetsPerformanceEndpointBase): - def get(self, request: Request, organization: Organization) -> Response: - try: - snuba_params, aggregate_column, filter_query = self._setup(request, organization) - except NoProjects: - return Response([]) - - all_tag_keys = bool(request.GET.get("allTagKeys")) - tag_key = request.GET.get("tagKey") - - if tag_key in TAG_ALIASES: - tag_key = TAG_ALIASES.get(tag_key) - - def data_fn(offset, limit: int): - with traces.start_span( - name="discover_query", attributes={"sentry.op": "discover.endpoint"} - ): - referrer = "api.organization-events-facets-performance.top-tags" - tag_data = query_tag_data( - filter_query=filter_query, - aggregate_column=aggregate_column, - referrer=referrer, - snuba_params=snuba_params, - ) - - if not tag_data: - return {"data": []} - - results = query_facet_performance( - tag_data=tag_data, - filter_query=filter_query, - aggregate_column=aggregate_column, - referrer=referrer, - orderby=self.get_orderby(request), - limit=limit, - offset=offset, - snuba_params=snuba_params, - all_tag_keys=all_tag_keys, - tag_key=tag_key, - ) - - for row in results["data"]: - row["tags_value"] = tagstore.backend.get_tag_value_label( - row["tags_key"], row["tags_value"] - ) - row["tags_key"] = tagstore.backend.get_standardized_key(row["tags_key"]) - - return results - - with handle_query_errors(): - return self.paginate( - request=request, - paginator=GenericOffsetPaginator(data_fn=data_fn), - on_results=lambda results: self.handle_results_with_meta( - request, organization, snuba_params.project_ids, results - ), - default_per_page=5, - max_per_page=20, - ) - - -@cell_silo_endpoint -class OrganizationEventsFacetsPerformanceHistogramEndpoint( - OrganizationEventsFacetsPerformanceEndpointBase -): - publish_status = { - "GET": ApiPublishStatus.PRIVATE, - } - - def get(self, request: Request, organization: Organization) -> Response: - try: - snuba_params, aggregate_column, filter_query = self._setup(request, organization) - except NoProjects: - return Response([]) - - tag_key = request.GET.get("tagKey") - num_buckets_per_key_s = request.GET.get("numBucketsPerKey") - per_page_s = request.GET.get("per_page", DEFAULT_TAG_KEY_LIMIT) - - if not num_buckets_per_key_s: - raise ParseError( - detail="'numBucketsPerKey' must be provided for the performance histogram." - ) - try: - per_page = int(per_page_s) - num_buckets_per_key = int(num_buckets_per_key_s) - except ValueError: - raise ParseError(detail="Bucket and tag key per_pages must be numeric.") - - if per_page * num_buckets_per_key > 500: - raise ParseError( - detail="The number of total buckets ('per_page' * 'numBucketsPerKey') cannot exceed 500" - ) - - if not tag_key: - raise ParseError(detail="'tagKey' must be provided when using histograms.") - - if tag_key in TAG_ALIASES: - tag_key = TAG_ALIASES[tag_key] - - def data_fn(offset, limit, raw_limit): - with traces.start_span( - name="discover_query", attributes={"sentry.op": "discover.endpoint"} - ): - referrer = "api.organization-events-facets-performance-histogram" - top_tags = query_top_tags( - tag_key=tag_key, - limit=limit, - filter_query=filter_query, - aggregate_column=aggregate_column, - snuba_params=snuba_params, - orderby=self.get_orderby(request), - offset=offset, - referrer=referrer, - ) - - if not top_tags: - return {"tags": [], "histogram": {"data": []}} - - # Only pass exactly the number of tags so histogram fetches correct number of rows - histogram_top_tags = top_tags[0:raw_limit] - - histogram = query_facet_performance_key_histogram( - top_tags=histogram_top_tags, - tag_key=tag_key, - filter_query=filter_query, - aggregate_column=aggregate_column, - referrer=referrer, - snuba_params=snuba_params, - limit=raw_limit, - num_buckets_per_key=num_buckets_per_key, - ) - - if not histogram: - return {"tags": top_tags, "histogram": {"data": []}} - - for row in histogram["data"]: - row["tags_key"] = tagstore.backend.get_standardized_key(row["tags_key"]) - - return {"tags": top_tags, "histogram": histogram} - - def on_results(data): - return { - "tags": self.handle_results_with_meta( - request, organization, snuba_params.project_ids, {"data": data["tags"]} - ), - "histogram": self.handle_results_with_meta( - request, organization, snuba_params.project_ids, data["histogram"] - ), - } - - with handle_query_errors(): - return self.paginate( - request=request, - paginator=HistogramPaginator(data_fn=data_fn), - on_results=on_results, - default_per_page=DEFAULT_TAG_KEY_LIMIT, - max_per_page=50, - ) - - -class HistogramPaginator(GenericOffsetPaginator): - def get_result(self, limit, cursor=None): - assert limit > 0 - offset = cursor.offset if cursor is not None else 0 - # Request 1 more than limit so we can tell if there is another page - # Use raw_limit for the histogram itself so bucket calculations are correct - data = self.data_fn(offset=offset, limit=limit + 1, raw_limit=limit) - - if isinstance(data["tags"], list): - has_more = len(data["tags"]) == limit + 1 - if has_more: - data["tags"].pop() - else: - raise NotImplementedError - - return CursorResult( - data, - prev=Cursor(0, max(0, offset - limit), True, offset > 0), - next=Cursor(0, max(0, offset + limit), False, has_more), - ) - - -def query_tag_data( - snuba_params: SnubaParams, - referrer: str, - filter_query: str | None = None, - aggregate_column: str | None = None, -) -> dict | None: - """ - Fetch general data about all the transactions with this transaction name to feed into the facet query - :return: Returns the row with aggregate and count if the query was successful - Returns None if query was not successful which causes the endpoint to return early - """ - with traces.start_span( - name="facets.filter_transform", - attributes={ - "sentry.op": "discover.discover", - }, - ) as span: - if filter_query is not None: - span.set_attribute("query", filter_query) - tag_query = DiscoverQueryBuilder( - dataset=Dataset.Discover, - params={}, - snuba_params=snuba_params, - query=filter_query, - selected_columns=[ - "count()", - f"avg({aggregate_column}) as aggregate", - f"max({aggregate_column}) as max", - f"min({aggregate_column}) as min", - ], - ) - tag_query.where.append( - Condition(tag_query.resolve_column(aggregate_column), Op.IS_NOT_NULL) - ) - - with traces.start_span( - name="facets.frequent_tags", attributes={"sentry.op": "discover.discover"} - ): - # Get the average and count to use to filter the next request to facets - tag_data = tag_query.run_query(f"{referrer}.all_transactions") - - if len(tag_data["data"]) != 1: - return None - - counts = [r["count"] for r in tag_data["data"]] - aggregates = [r["aggregate"] for r in tag_data["data"]] - - # Return early to avoid doing more queries with 0 count transactions or aggregates for columns that don't exist - if counts[0] == 0 or aggregates[0] is None: - return None - if not tag_data["data"][0]: - return None - return tag_data["data"][0] - - -def query_top_tags( - snuba_params: SnubaParams, - tag_key: str, - limit: int, - referrer: str, - orderby: list[str] | None, - offset: int | None = None, - aggregate_column: str | None = None, - *, - filter_query: str, -) -> list[Any] | None: - """ - Fetch counts by tag value, finding the top tag values for a tag key by a limit. - :return: Returns the row with the value, the aggregate and the count if the query was successful - Returns None if query was not successful which causes the endpoint to return early - """ - translated_aggregate_column = discover.resolve_discover_column(aggregate_column) - - with traces.start_span(name="facets.top_tags", attributes={"sentry.op": "discover.discover"}): - if not orderby: - orderby = ["-count"] - - for i, sort in enumerate(orderby): - if "frequency" in sort: - # Replacing frequency as it's the same underlying data dimension, this way we don't have to modify the existing histogram query. - orderby[i] = sort.replace("frequency", "count") - - if "tags_value" not in orderby: - orderby = orderby + ["tags_value"] - - # Get the average and count to use to filter the next request to facets - tag_data = discover.query( - selected_columns=[ - "count()", - f"avg({aggregate_column}) as aggregate", - "array_join(tags.value) as tags_value", - ], - query=filter_query, - snuba_params=snuba_params, - orderby=orderby, - conditions=[ - Condition(Column(translated_aggregate_column), Op.IS_NOT_NULL), - Condition(Column("tags_key"), Op.EQ, tag_key), - ], - functions_acl=["array_join"], - referrer=f"{referrer}.top_tags", - limit=limit, - offset=offset, - ) - - if len(tag_data["data"]) <= 0: - return None - - counts = [r["count"] for r in tag_data["data"]] - - # Return early to avoid doing more queries with 0 count transactions or aggregates for columns that don't exist - if counts[0] == 0: - return None - if not tag_data["data"]: - return None - return tag_data["data"] - - -def query_facet_performance( - snuba_params: SnubaParams, - tag_data: Mapping[str, Any], - referrer: str, - aggregate_column: str | None = None, - filter_query: str | None = None, - orderby: list[str] | None = None, - offset: int | None = None, - all_tag_keys: bool | None = None, - tag_key: str | None = None, - *, - limit: int, -) -> EventsResponse: - # Dynamically sample so at least 50000 transactions are selected - sample_start_count = 50000 - transaction_count = tag_data["count"] - sampling_enabled = transaction_count > sample_start_count - - # log-e growth starting at 50,000 - target_sample = max( - sample_start_count * (math.log(transaction_count) - (math.log(sample_start_count) - 1)), - transaction_count, - ) - - dynamic_sample_rate = 0 if transaction_count <= 0 else (target_sample / transaction_count) - sample_rate = min(max(dynamic_sample_rate, 0), 1) if sampling_enabled else None - frequency_sample_rate = sample_rate if sample_rate else 1 - - tag_key_limit = limit if tag_key else 1 - - with traces.start_span( - name="facets.filter_transform", - attributes={ - "sentry.op": "discover.discover", - }, - ) as span: - if filter_query is not None: - span.set_attribute("query", filter_query) - tag_query = DiscoverQueryBuilder( - dataset=Dataset.Discover, - params={}, - snuba_params=snuba_params, - query=filter_query, - selected_columns=["count()", "tags_key", "tags_value"], - sample_rate=sample_rate, - turbo=sample_rate is not None, - limit=limit, - offset=offset, - limitby=("tags_key", tag_key_limit) if not tag_key else None, - ) - translated_aggregate_column = tag_query.resolve_column(aggregate_column) - - # Aggregate (avg) and count of all transactions for this query - transaction_aggregate = tag_data["aggregate"] - - # Exclude tags that have high cardinality are generally unrelated to performance - excluded_tags = Condition( - Column("tags_key"), - Op.NOT_IN, - ["trace", "trace.ctx", "trace.span", "project", "browser", "celery_task_id", "url"], - ) - - with traces.start_span( - name="facets.aggregate_tags", - attributes={"sentry.op": "discover.discover", "target_sample": target_sample}, - ) as span: - if sample_rate is not None: - span.set_attribute("sample_rate", sample_rate) - aggregate_comparison = transaction_aggregate * 1.005 if transaction_aggregate else 0 - aggregate_column = Function("avg", [translated_aggregate_column], "aggregate") - tag_query.where.append(excluded_tags) - if not all_tag_keys and not tag_key: - tag_query.having.append(Condition(aggregate_column, Op.GT, aggregate_comparison)) - - tag_query.where.append(Condition(translated_aggregate_column, Op.IS_NOT_NULL)) - - if tag_key: - tag_query.where.append(Condition(Column("tags_key"), Op.IN, [tag_key])) - - tag_query.columns.extend( - [ - Function( - "divide", - [ - Function( - "sum", - [ - Function( - "minus", [translated_aggregate_column, transaction_aggregate] - ) - ], - ), - frequency_sample_rate, - ], - "sumdelta", - ), - Function( - "divide", - [ - Function("divide", [Function("count", [], "count"), frequency_sample_rate]), - transaction_count, - ], - "frequency", - ), - Function("divide", [aggregate_column, transaction_aggregate], "comparison"), - aggregate_column, - ] - ) - - # Need to wait for the custom functions to be added first since they can be orderby options - tag_query.orderby = tag_query.resolve_orderby([*(orderby or []), "tags_key", "tags_value"]) - - results = tag_query.process_results(tag_query.run_query(f"{referrer}.tag_values")) - - return results - - -def query_facet_performance_key_histogram( - snuba_params: SnubaParams, - top_tags: list[Any], - tag_key: str, - num_buckets_per_key: int, - limit: int, - referrer: str, - aggregate_column: str, - *, - filter_query: str, -) -> dict[str, Any] | EventsResponse: - precision = 0 - - tag_values = [x["tags_value"] for x in top_tags] - - results = discover.histogram_query( - fields=[aggregate_column], - user_query=filter_query, - snuba_params=snuba_params, - num_buckets=num_buckets_per_key, - precision=precision, - group_by=["tags_value", "tags_key"], - extra_conditions=[ - Condition(Column("tags_key"), Op.EQ, tag_key), - Condition(Column("tags_value"), Op.IN, tag_values), - ], - histogram_rows=limit, - referrer="api.organization-events-facets-performance-histogram", - normalize_results=False, - ) - return results diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 1da1993cea69..393ad3fea41d 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -803,10 +803,6 @@ from .endpoints.organization_events import OrganizationEventsEndpoint from .endpoints.organization_events_dropped import OrganizationEventsDroppedEndpoint from .endpoints.organization_events_facets import OrganizationEventsFacetsEndpoint -from .endpoints.organization_events_facets_performance import ( - OrganizationEventsFacetsPerformanceEndpoint, - OrganizationEventsFacetsPerformanceHistogramEndpoint, -) from .endpoints.organization_events_heatmap import OrganizationEventsHeatmapEndpoint from .endpoints.organization_events_histogram import OrganizationEventsHistogramEndpoint from .endpoints.organization_events_meta import ( @@ -1920,16 +1916,6 @@ def create_group_urls( OrganizationEventsFacetsEndpoint.as_view(), name="sentry-api-0-organization-events-facets", ), - re_path( - r"^(?P[^/]+)/events-facets-performance/$", - OrganizationEventsFacetsPerformanceEndpoint.as_view(), - name="sentry-api-0-organization-events-facets-performance", - ), - re_path( - r"^(?P[^/]+)/events-facets-performance-histogram/$", - OrganizationEventsFacetsPerformanceHistogramEndpoint.as_view(), - name="sentry-api-0-organization-events-facets-performance-histogram", - ), re_path( r"^(?P[^/]+)/events-meta/$", OrganizationEventsMetaEndpoint.as_view(), diff --git a/src/sentry/snuba/discover.py b/src/sentry/snuba/discover.py index 0d67b35ce24c..4e9e12e143c8 100644 --- a/src/sentry/snuba/discover.py +++ b/src/sentry/snuba/discover.py @@ -45,7 +45,6 @@ is_measurement, is_span_op_breakdown, naiveify_datetime, - resolve_column, to_naive_timestamp, ) @@ -72,8 +71,6 @@ HistogramResults = dict[str, list[dict[str, Any]]] -resolve_discover_column = resolve_column(Dataset.Discover) - OTHER_KEY = "Other" TOP_KEYS_DEFAULT_LIMIT = 10 diff --git a/src/sentry/snuba/referrer.py b/src/sentry/snuba/referrer.py index 9dbd0a799094..fd1309d18fa1 100644 --- a/src/sentry/snuba/referrer.py +++ b/src/sentry/snuba/referrer.py @@ -453,18 +453,6 @@ class Referrer(StrEnum): API_ORGANIZATION_EVENT_STATS_FIND_TOPN = "api.organization-event-stats.find-topn" API_ORGANIZATION_EVENT_STATS_METRICS_ENHANCED = "api.organization-event-stats.metrics-enhanced" API_ORGANIZATION_EVENT_STATS = "api.organization-event-stats" - API_ORGANIZATION_EVENTS_FACETS_PERFORMANCE_HISTOGRAM_TOP_TAGS = ( - "api.organization-events-facets-performance-histogram.top_tags" - ) - API_ORGANIZATION_EVENTS_FACETS_PERFORMANCE_HISTOGRAM = ( - "api.organization-events-facets-performance-histogram" - ) - API_ORGANIZATION_EVENTS_FACETS_PERFORMANCE_TOP_TAGS_ALL_TRANSACTIONS = ( - "api.organization-events-facets-performance.top-tags.all_transactions" - ) - API_ORGANIZATION_EVENTS_FACETS_PERFORMANCE_TOP_TAGS_TAG_VALUES = ( - "api.organization-events-facets-performance.top-tags.tag_values" - ) API_ORGANIZATION_EVENTS_FACETS_TOP_TAGS = "api.organization-events-facets.top-tags" API_ORGANIZATION_EVENTS_HISTOGRAM_MIN_MAX_METRICS_ENHANCED_PRIMARY = ( "api.organization-events-histogram-min-max.metrics-enhanced.primary" diff --git a/static/app/utils/api/knownSentryApiUrls.generated.ts b/static/app/utils/api/knownSentryApiUrls.generated.ts index b5f65b3330af..6d2b2a51ea53 100644 --- a/static/app/utils/api/knownSentryApiUrls.generated.ts +++ b/static/app/utils/api/knownSentryApiUrls.generated.ts @@ -140,8 +140,6 @@ export type KnownSentryApiUrls = | '/organizations/$organizationIdOrSlug/environments/' | '/organizations/$organizationIdOrSlug/eventids/$eventId/' | '/organizations/$organizationIdOrSlug/events-dropped/' - | '/organizations/$organizationIdOrSlug/events-facets-performance-histogram/' - | '/organizations/$organizationIdOrSlug/events-facets-performance/' | '/organizations/$organizationIdOrSlug/events-facets/' | '/organizations/$organizationIdOrSlug/events-heatmap/' | '/organizations/$organizationIdOrSlug/events-histogram/' diff --git a/tests/snuba/api/endpoints/test_organization_events_facets_performance.py b/tests/snuba/api/endpoints/test_organization_events_facets_performance.py deleted file mode 100644 index 8a807df41b34..000000000000 --- a/tests/snuba/api/endpoints/test_organization_events_facets_performance.py +++ /dev/null @@ -1,304 +0,0 @@ -from datetime import timedelta - -from django.urls import reverse - -from sentry.testutils.cases import APITestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now -from sentry.utils.samples import load_data - - -class BaseOrganizationEventsFacetsPerformanceEndpointTest(SnubaTestCase, APITestCase): - url: str - feature_list = ( - "organizations:discover-basic", - "organizations:performance-view", - ) - - def setUp(self) -> None: - super().setUp() - self.min_ago = before_now(minutes=1).replace(microsecond=0) - self.two_mins_ago = before_now(minutes=2).replace(microsecond=0) - self.day_ago = before_now(days=1).replace(microsecond=0) - - self.login_as(user=self.user) - self.project = self.create_project() - self.project2 = self.create_project() - - def do_request(self, query=None, features=None): - query = query if query is not None else {"aggregateColumn": "transaction.duration"} - query["project"] = query["project"] if "project" in query else [self.project.id] - - feature_dict = {feature: True for feature in self.feature_list} - feature_dict.update(features or {}) - with self.feature(feature_dict): - return self.client.get(self.url, query, format="json") - - -class OrganizationEventsFacetsPerformanceEndpointTest( - BaseOrganizationEventsFacetsPerformanceEndpointTest -): - def setUp(self) -> None: - super().setUp() - - self._transaction_count = 0 - - for i in range(5): - self.store_transaction( - tags=[["color", "blue"], ["many", "yes"]], duration=4000, lcp=3000 - ) - for i in range(14): - self.store_transaction(tags=[["color", "red"], ["many", "yes"]], duration=1000, lcp=500) - for i in range(1): - self.store_transaction( - tags=[["color", "green"], ["many", "no"]], duration=5000, lcp=4000 - ) - - self.url = reverse( - "sentry-api-0-organization-events-facets-performance", - kwargs={"organization_id_or_slug": self.project.organization.slug}, - ) - - def store_transaction( - self, name="exampleTransaction", duration=100, project_id=None, lcp=None, *, tags - ): - if project_id is None: - project_id = self.project.id - event = load_data("transaction") - event["tags"].extend(tags) - event.update( - { - "transaction": name, - "event_id": f"{self._transaction_count:02x}".rjust(32, "0"), - "start_timestamp": (self.two_mins_ago - timedelta(seconds=duration)).isoformat(), - "timestamp": self.two_mins_ago.isoformat(), - } - ) - - if lcp: - event["measurements"]["lcp"]["value"] = lcp - else: - del event["measurements"]["lcp"] - - self._transaction_count += 1 - self.store_event(data=event, project_id=project_id) - - def test_basic_request(self) -> None: - response = self.do_request() - assert response.status_code == 200, response.content - - data = response.data["data"] - assert len(data) == 2 - assert data[0] == { - "aggregate": 4000000.0, - "comparison": 2.051282051282051, - "count": 5, - "frequency": 0.25, - "sumdelta": 10250000.0, - "tags_key": "color", - "tags_value": "blue", - } - - def test_sort_frequency(self) -> None: - # Descending - response = self.do_request( - { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "per_page": 20, - "statsPeriod": "14d", - } - ) - assert response.status_code == 200, response.content - - data = response.data["data"] - assert len(data) == 2 - - # The first set of generated is the most frequent since the 14 transactions are excluded because of 1000 duration - assert data[0]["count"] == 5 - assert data[0]["tags_key"] == "color" - assert data[0]["tags_value"] == "blue" - - # The 14 transactions with many=yes are excluded because of 1000 duration - assert data[1]["count"] == 1 - assert data[1]["tags_key"] == "many" - assert data[1]["tags_value"] == "no" - - # Ascending - response = self.do_request( - { - "aggregateColumn": "transaction.duration", - "sort": "frequency", - "per_page": 5, - "statsPeriod": "14d", - } - ) - - data = response.data["data"] - assert len(data) == 2 - assert data[0]["count"] == 1 - assert data[0]["tags_key"] == "color" - assert data[0]["tags_value"] == "green" - - assert data[1]["count"] == 1 - assert data[1]["tags_key"] == "many" - assert data[1]["tags_value"] == "no" - - def test_basic_query(self) -> None: - response = self.do_request( - { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "per_page": 5, - "statsPeriod": "14d", - "query": "(color:red or color:blue)", - } - ) - - assert response.status_code == 200, response.content - data = response.data["data"] - assert len(data) == 1 - assert data[0]["count"] == 5 - assert data[0]["tags_key"] == "color" - assert data[0]["tags_value"] == "blue" - - def test_multiple_projects_not_allowed(self) -> None: - response = self.do_request( - { - "aggregateColumn": "transaction.duration", - "project": [self.project.id, self.project2.id], - } - ) - assert response.status_code == 400, response.content - assert response.data == { - "detail": "You cannot view facet performance for multiple projects." - } - - def test_missing_tags_column(self) -> None: - response = self.do_request({}) - assert response.status_code == 400, response.content - assert response.data == {"detail": "'aggregateColumn' must be provided."} - - def test_invalid_tags_column(self) -> None: - response = self.do_request({"aggregateColumn": "abc"}) - - assert response.status_code == 400, response.content - assert response.data == {"detail": "'abc' is not a supported tags column."} - - def test_all_tag_keys(self) -> None: - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "per_page": 5, - "statsPeriod": "14d", - "query": "(color:red or color:blue)", - "allTagKeys": True, - } - - # With feature access - response = self.do_request(request) - assert response.status_code == 200, response.content - data = response.data["data"] - assert len(data) == 5 - assert data[0]["count"] == 19 - assert data[0]["tags_key"] == "application" - assert data[0]["tags_value"] == "countries" - - def test_tag_frequency(self) -> None: - # LCP-less transaction should be ignored in total counts for frequency. - self.store_transaction(tags=[["color", "orange"], ["many", "maybe"]], lcp=None) - - request = { - "aggregateColumn": "measurements.lcp", - "sort": "-frequency", - "per_page": 5, - "statsPeriod": "14d", - "query": "(color:red or color:blue)", - "allTagKeys": True, - } - - response = self.do_request(request) - assert response.status_code == 200, response.content - - data = response.data["data"] - assert len(data) == 5 - assert data[0]["count"] == 19 - assert data[0]["tags_key"] == "application" - assert data[0]["tags_value"] == "countries" - - # Only transactions with lcp should be considered - assert data[0]["frequency"] == 1 - - def test_tag_key_values(self) -> None: - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "per_page": 5, - "statsPeriod": "14d", - "tagKey": "color", - } - # With feature access - response = self.do_request(request) - assert response.status_code == 200, response.content - data = response.data["data"] - assert len(data) == 3 - assert data[0]["count"] == 14 - assert data[0]["tags_key"] == "color" - assert data[0]["tags_value"] == "red" - - def test_aggregate_zero(self) -> None: - # LCP-less transaction should be ignored in total counts for frequency. - self.store_transaction(tags=[["color", "purple"]], duration=0) - - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "per_page": 5, - "statsPeriod": "14d", - "tagKey": "color", - "query": "(color:purple)", - } - - response = self.do_request(request) - assert response.status_code == 200, response.content - data = response.data["data"] - assert len(data) == 1 - assert data[0]["count"] == 1 - assert data[0]["comparison"] == 0 - assert data[0]["tags_key"] == "color" - assert data[0]["tags_value"] == "purple" - - def test_cursor(self) -> None: - self.store_transaction(tags=[["third_tag", "good"]], duration=1000) - self.store_transaction(tags=[["third_tag", "bad"]], duration=10000) - - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "per_page": 2, - "cursor": "0:0:0", - } - - response = self.do_request(request) - assert response.status_code == 200, response.content - data = response.data["data"] - assert len(data) == 2 - assert data[0]["tags_key"] == "color" - assert data[0]["count"] == 5 - assert data[1]["tags_key"] == "many" - assert data[1]["count"] == 1 - - request["cursor"] = "0:2:0" - response = self.do_request(request) - assert response.status_code == 200, response.content - data = response.data["data"] - # Only 1 key in this page - assert len(data) == 1 - assert data[0]["tags_key"] == "third_tag" - assert data[0]["count"] == 1 - - request["cursor"] = "0:4:0" - response = self.do_request(request) - assert response.status_code == 200, response.content - data = response.data["data"] - # 0 keys, past all 3 tag keys stored. - assert len(data) == 0 diff --git a/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py b/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py deleted file mode 100644 index c64649dbf475..000000000000 --- a/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py +++ /dev/null @@ -1,383 +0,0 @@ -from datetime import timedelta - -from django.urls import reverse - -from sentry.utils.cursors import Cursor -from sentry.utils.samples import load_data -from tests.snuba.api.endpoints.test_organization_events_facets_performance import ( - BaseOrganizationEventsFacetsPerformanceEndpointTest, -) - - -class OrganizationEventsFacetsPerformanceHistogramEndpointTest( - BaseOrganizationEventsFacetsPerformanceEndpointTest -): - feature_list = ( - "organizations:discover-basic", - "organizations:performance-view", - ) - - def setUp(self) -> None: - super().setUp() - - self._transaction_count = 0 - - self.url = reverse( - "sentry-api-0-organization-events-facets-performance-histogram", - kwargs={"organization_id_or_slug": self.project.organization.slug}, - ) - - # Function to set up some transactions for most tests - def setup_transactions(self): - for i in range(5): - self.store_transaction( - tags=[["color", "blue"], ["many", "yes"]], duration=4000, lcp=4000 - ) - - # LCP-less transaction - self.store_transaction(tags=[["color", "orange"], ["many", "maybe"]], lcp=None) - - for i in range(14): - self.store_transaction( - tags=[["color", "red"], ["many", "yes"]], duration=1000, lcp=1000 - ) - for i in range(1): - self.store_transaction( - tags=[["color", "green"], ["many", "no"]], duration=5000, lcp=5000 - ) - - def store_transaction( - self, - name="exampleTransaction", - duration=100, - project_id=None, - lcp=None, - user_id=None, - *, - tags, - ): - if project_id is None: - project_id = self.project.id - event = load_data("transaction") - event["tags"].extend(tags) - event.update( - { - "transaction": name, - "event_id": f"{self._transaction_count:02x}".rjust(32, "0"), - "start_timestamp": (self.two_mins_ago - timedelta(seconds=duration)).isoformat(), - "timestamp": self.two_mins_ago.isoformat(), - } - ) - if user_id: - event["user"] = { - "email": "foo@example.com", - "id": user_id, - "ip_address": "127.0.0.1", - "username": "foo", - } - - if lcp: - event["measurements"]["lcp"]["value"] = lcp - else: - del event["measurements"]["lcp"] - - self._transaction_count += 1 - self.store_event(data=event, project_id=project_id) - - def test_multiple_projects_not_allowed(self) -> None: - response = self.do_request( - { - "aggregateColumn": "transaction.duration", - "project": [self.project.id, self.project2.id], - } - ) - assert response.status_code == 400, response.content - assert response.data == { - "detail": "You cannot view facet performance for multiple projects." - } - - def test_missing_tags_column(self) -> None: - response = self.do_request({}) - assert response.status_code == 400, response.content - assert response.data == {"detail": "'aggregateColumn' must be provided."} - - def test_no_access(self) -> None: - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "per_page": 5, - "statsPeriod": "14d", - "query": "(color:red or color:blue)", - } - error_response = self.do_request( - request, - { - "organizations:performance-view": False, - }, - ) - assert error_response.status_code == 404 - - def test_num_buckets_error(self) -> None: - self.setup_transactions() - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "statsPeriod": "14d", - "query": "(color:red or color:blue)", - "per_page": 5, - } - # With feature access, no tag key - error_response = self.do_request(request) - - assert error_response.status_code == 400, error_response.content - assert error_response.data == { - "detail": "'numBucketsPerKey' must be provided for the performance histogram." - } - - def test_tag_key_histograms(self) -> None: - self.setup_transactions() - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "statsPeriod": "14d", - "per_page": 10, - "numBucketsPerKey": 10, - "query": "(color:red or color:blue)", - } - # With feature access, no tag key - error_response = self.do_request(request) - - assert error_response.status_code == 400, error_response.content - assert error_response.data == {"detail": "'tagKey' must be provided when using histograms."} - - # With feature access and tag key - request["tagKey"] = "color" - data_response = self.do_request(request) - - histogram_data = data_response.data["histogram"]["data"] - assert len(histogram_data) == 2 - assert histogram_data[0]["count"] == 14 - - assert histogram_data[0]["histogram_transaction_duration_500000_1000000_1"] == 1000000.0 - assert histogram_data[0]["tags_value"] == "red" - assert histogram_data[0]["tags_key"] == "color" - assert histogram_data[1]["count"] == 5 - assert histogram_data[1]["histogram_transaction_duration_500000_1000000_1"] == 4000000.0 - assert histogram_data[1]["tags_value"] == "blue" - assert histogram_data[1]["tags_key"] == "color" - - tag_data = data_response.data["tags"]["data"] - assert len(tag_data) == 2 - assert tag_data[0]["tags_value"] == "red" - assert tag_data[1]["tags_value"] == "blue" - - def test_no_top_tags(self) -> None: - self.setup_transactions() - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "statsPeriod": "14d", - "per_page": 10, - "numBucketsPerKey": 10, - "tagKey": "color", - "query": "(color:teal or color:oak)", - } - - data_response = self.do_request(request) - - histogram_data = data_response.data["histogram"]["data"] - assert histogram_data == [] - - tag_data = data_response.data["tags"]["data"] - assert tag_data == [] - - def test_tag_key_histogram_buckets(self) -> None: - self.setup_transactions() - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "statsPeriod": "14d", - "per_page": 1, - "numBucketsPerKey": 2, - "tagKey": "color", - "query": "(color:red or color:blue or color:green)", - } - - data_response = self.do_request(request) - - histogram_data = data_response.data["histogram"]["data"] - assert len(histogram_data) == 1 - assert histogram_data[0]["count"] == 14 - assert histogram_data[0]["histogram_transaction_duration_2500000_0_1"] == 0.0 - assert histogram_data[0]["tags_value"] == "red" - assert histogram_data[0]["tags_key"] == "color" - - request["per_page"] = 3 - data_response = self.do_request(request) - - histogram_data = data_response.data["histogram"]["data"] - assert len(histogram_data) == 3 - - assert histogram_data[0]["count"] == 14 - assert histogram_data[0]["histogram_transaction_duration_2500000_0_1"] == 0.0 - assert histogram_data[0]["tags_value"] == "red" - assert histogram_data[0]["tags_key"] == "color" - - assert histogram_data[1]["count"] == 5 - assert histogram_data[1]["histogram_transaction_duration_2500000_0_1"] == 2500000.0 - assert histogram_data[1]["tags_value"] == "blue" - assert histogram_data[1]["tags_key"] == "color" - - assert histogram_data[2]["count"] == 1 - assert histogram_data[2]["histogram_transaction_duration_2500000_0_1"] == 5000000.0 - assert histogram_data[2]["tags_value"] == "green" - assert histogram_data[2]["tags_key"] == "color" - - def test_histograms_omit_empty_measurements(self) -> None: - self.setup_transactions() - request = { - "aggregateColumn": "transaction.duration", - "sort": "-frequency", - "statsPeriod": "14d", - "per_page": 3, - "numBucketsPerKey": 2, - "tagKey": "color", - "query": "(color:red or color:blue or color:green or color:orange)", - } - - data_response = self.do_request(request) - - assert data_response.data["tags"]["data"][2]["tags_value"] == "green" - - request["aggregateColumn"] = "measurements.lcp" - - data_response = self.do_request(request) - - tags_data = data_response.data["tags"]["data"] - assert len(tags_data) == 3 - assert tags_data[2]["tags_value"] == "green" - - histogram_data = data_response.data["histogram"]["data"] - assert len(histogram_data) == 3 - assert histogram_data[0]["count"] == 14 - assert histogram_data[0]["histogram_measurements_lcp_2500_0_1"] == 0.0 - assert histogram_data[0]["tags_value"] == "red" - assert histogram_data[0]["tags_key"] == "color" - - assert histogram_data[1]["count"] == 5 - assert histogram_data[1]["histogram_measurements_lcp_2500_0_1"] == 2500.0 - assert histogram_data[1]["tags_value"] == "blue" - assert histogram_data[1]["tags_key"] == "color" - - assert histogram_data[2]["count"] == 1 - assert histogram_data[2]["histogram_measurements_lcp_2500_0_1"] == 5000.0 - assert histogram_data[2]["tags_value"] == "green" - assert histogram_data[2]["tags_key"] == "color" - - def test_histogram_user_field(self) -> None: - self.setup_transactions() - self.store_transaction( - tags=[["color", "blue"], ["many", "yes"]], duration=4000, user_id=555 - ) - - request = { - "aggregateColumn": "transaction.duration", - "per_page": 1, - "numBucketsPerKey": 2, - "tagKey": "user", - "query": "(user.id:555)", - } - - data_response = self.do_request(request) - - histogram_data = data_response.data["histogram"]["data"] - assert histogram_data[0]["count"] == 1 - assert histogram_data[0]["tags_value"] == "id:555" - assert histogram_data[0]["tags_key"] == "user" - - tag_data = data_response.data["tags"]["data"] - assert tag_data[0]["count"] == 1 - assert tag_data[0]["tags_value"] == "id:555" - - def test_histogram_pagination(self) -> None: - self.setup_transactions() - request = { - "aggregateColumn": "transaction.duration", - "per_page": 3, - "numBucketsPerKey": 2, - "tagKey": "color", - } - - data_response = self.do_request(request) - - tag_data = data_response.data["tags"]["data"] - assert len(tag_data) == 3 - - request["cursor"] = Cursor(0, 3) - - data_response = self.do_request(request) - - tag_data = data_response.data["tags"]["data"] - assert len(tag_data) == 1 - - def test_histogram_sorting(self) -> None: - self.setup_transactions() - request = { - "aggregateColumn": "transaction.duration", - "per_page": 1, - "sort": "-frequency", - "numBucketsPerKey": 2, - "tagKey": "color", - } - - data_response = self.do_request(request) - - tag_data = data_response.data["tags"]["data"] - assert len(tag_data) == 1 - assert tag_data[0]["tags_value"] == "red" - assert tag_data[0]["count"] == 14 - - request["sort"] = "-aggregate" - - data_response = self.do_request(request) - - tag_data = data_response.data["tags"]["data"] - assert len(tag_data) == 1 - assert tag_data[0]["tags_value"] == "green" - assert tag_data[0]["count"] == 1 - - def test_histogram_high_buckets(self) -> None: - for i in range(10): - self.store_transaction(tags=[["fruit", "apple"]], duration=i * 100 + 50) - self.store_transaction(tags=[["fruit", "orange"]], duration=i * 100 + 1000 + 50) - - request = { - "aggregateColumn": "transaction.duration", - "per_page": 2, - "sort": "-frequency", - "numBucketsPerKey": 20, - "tagKey": "fruit", - } - - data_response = self.do_request(request) - - histogram_data = data_response.data["histogram"]["data"] - assert len(histogram_data) == 20 - - for i, d in enumerate(histogram_data): - assert d["count"] == 1 - assert d["histogram_transaction_duration_100000_0_1"] == i * 100000.0 - if i < 10: - assert d["tags_value"] == "apple" - else: - assert d["tags_value"] == "orange" - - tag_data = data_response.data["tags"]["data"] - assert len(tag_data) == 2 - assert tag_data[0]["tags_value"] == "apple" - assert tag_data[0]["count"] == 10 - assert tag_data[0]["aggregate"] == 500000.0 - - assert tag_data[1]["tags_value"] == "orange" - assert tag_data[1]["count"] == 10 - assert tag_data[1]["aggregate"] == 1500000.0