diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index ff85730ad8f193..a662410935a09d 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -2506,3 +2506,12 @@ register( "api.organization-activity.brownout-duration", default="PT1M", flags=FLAG_AUTOMATOR_MODIFIABLE ) + +# Enable percentile operations in the metrics/meta endpoint in the Metrics API for the orgs in the list. This is used to +# also hide those expensive operations from view in the Metrics UI for everyone except the whitelist. +register( + "sentry-metrics.metrics-api.enable-percentile-operations-for-orgs", + type=Sequence, + default=[], + flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE, +) diff --git a/src/sentry/sentry_metrics/querying/metadata/metrics.py b/src/sentry/sentry_metrics/querying/metadata/metrics.py index 21d4ec5f0fbb47..7c3703def7c437 100644 --- a/src/sentry/sentry_metrics/querying/metadata/metrics.py +++ b/src/sentry/sentry_metrics/querying/metadata/metrics.py @@ -1,15 +1,22 @@ from collections import defaultdict from collections.abc import Sequence +from typing import cast +from sentry import options from sentry.models.organization import Organization from sentry.models.project import Project +from sentry.sentry_metrics.querying.metadata.utils import METRICS_API_HIDDEN_OPERATIONS from sentry.sentry_metrics.use_case_id_registry import UseCaseID from sentry.snuba.metrics import parse_mri -from sentry.snuba.metrics.datasource import ( - _build_metric_meta, - get_metrics_blocking_state_of_projects, +from sentry.snuba.metrics.datasource import get_metrics_blocking_state_of_projects +from sentry.snuba.metrics.naming_layer.mri import ParsedMRI, get_available_operations +from sentry.snuba.metrics.utils import ( + BlockedMetric, + MetricMeta, + MetricOperationType, + MetricType, + MetricUnit, ) -from sentry.snuba.metrics.utils import BlockedMetric, MetricMeta from sentry.snuba.metrics_layer.query import fetch_metric_mris @@ -49,7 +56,9 @@ def get_metrics_meta( # not stored. del metrics_blocking_state[metric_mri] - metrics_metas.append(_build_metric_meta(parsed_mri, project_ids, blocking_status)) + metrics_metas.append( + _build_metric_meta(organization, parsed_mri, project_ids, blocking_status) + ) for metric_mri, metric_blocking in metrics_blocking_state.items(): parsed_mri = parse_mri(metric_mri) @@ -58,6 +67,7 @@ def get_metrics_meta( metrics_metas.append( _build_metric_meta( + organization, parsed_mri, [], [ @@ -95,3 +105,31 @@ def _convert_to_mris_to_project_ids_mapping(project_id_to_mris: dict[int, list[s mris_to_project_ids.setdefault(mri, []).append(project_id) return mris_to_project_ids + + +def _build_metric_meta( + organization: Organization, + parsed_mri: ParsedMRI, + project_ids: Sequence[int], + blocking_status: Sequence[BlockedMetric], +) -> MetricMeta: + available_operations = get_available_operations(parsed_mri) + + if organization.id not in options.get( + "sentry-metrics.metrics-api.enable-percentile-operations-for-orgs" + ): + available_operations = [ + operation + for operation in available_operations + if operation not in METRICS_API_HIDDEN_OPERATIONS + ] + + return MetricMeta( + type=cast(MetricType, parsed_mri.entity), + name=parsed_mri.name, + unit=cast(MetricUnit, parsed_mri.unit), + mri=parsed_mri.mri_string, + operations=cast(Sequence[MetricOperationType], available_operations), + projectIds=project_ids, + blockingStatus=blocking_status, + ) diff --git a/src/sentry/sentry_metrics/querying/metadata/utils.py b/src/sentry/sentry_metrics/querying/metadata/utils.py index cc7df54f7e8b8e..71e259e5e47327 100644 --- a/src/sentry/sentry_metrics/querying/metadata/utils.py +++ b/src/sentry/sentry_metrics/querying/metadata/utils.py @@ -1,6 +1,8 @@ from sentry.snuba.metrics import get_mri from sentry.snuba.metrics.naming_layer.mri import is_mri +METRICS_API_HIDDEN_OPERATIONS = ["p50", "p75", "p90", "p95", "p99"] + def convert_metric_names_to_mris(metric_names: list[str]) -> list[str]: mris: list[str] = [] diff --git a/src/sentry/snuba/metrics/datasource.py b/src/sentry/snuba/metrics/datasource.py index 7659fab695811b..c9492a5697160f 100644 --- a/src/sentry/snuba/metrics/datasource.py +++ b/src/sentry/snuba/metrics/datasource.py @@ -13,7 +13,6 @@ """ __all__ = ( - "get_metrics_meta", "get_all_tags", "get_tag_values", "get_series", @@ -27,7 +26,7 @@ from dataclasses import dataclass, replace from datetime import datetime from operator import itemgetter -from typing import Any, cast +from typing import Any from snuba_sdk import And, Column, Condition, Function, Op, Or, Query, Request from snuba_sdk.conditions import ConditionGroup @@ -54,13 +53,7 @@ org_id_from_projects, ) from sentry.snuba.metrics.naming_layer.mapping import get_mri -from sentry.snuba.metrics.naming_layer.mri import ( - ParsedMRI, - get_available_operations, - is_custom_measurement, - is_mri, - parse_mri, -) +from sentry.snuba.metrics.naming_layer.mri import is_custom_measurement, is_mri, parse_mri from sentry.snuba.metrics.query import Groupable, MetricField, MetricsQuery from sentry.snuba.metrics.query_builder import ( SnubaQueryBuilder, @@ -73,14 +66,11 @@ CUSTOM_MEASUREMENT_DATASETS, METRIC_TYPE_TO_ENTITY, UNALLOWED_TAGS, - BlockedMetric, DerivedMetricParseException, MetricDoesNotExistInIndexer, MetricMeta, MetricMetaWithTagKeys, - MetricOperationType, MetricType, - MetricUnit, NotSupportedOverCompositeEntityException, Tag, TagValue, @@ -309,78 +299,6 @@ def get_metrics_blocking_state_of_projects( return metrics_blocking_state_by_mri -def _build_metric_meta( - parsed_mri: ParsedMRI, project_ids: Sequence[int], blocking_status: Sequence[BlockedMetric] -) -> MetricMeta: - return MetricMeta( - type=parsed_mri.entity, - name=parsed_mri.name, - unit=cast(MetricUnit, parsed_mri.unit), - mri=parsed_mri.mri_string, - operations=cast(Sequence[MetricOperationType], get_available_operations(parsed_mri)), - projectIds=project_ids, - blockingStatus=blocking_status, - ) - - -def get_metrics_meta( - projects: Sequence[Project], - use_case_ids: Sequence[UseCaseID], - start: datetime | None = None, - end: datetime | None = None, -) -> Sequence[MetricMeta]: - if not projects: - return [] - - stored_metrics = get_stored_metrics_of_projects(projects, use_case_ids, start, end) - metrics_blocking_state = ( - get_metrics_blocking_state_of_projects(projects) if UseCaseID.CUSTOM in use_case_ids else {} - ) - - metrics_metas = [] - for metric_mri, project_ids in stored_metrics.items(): - parsed_mri = parse_mri(metric_mri) - if parsed_mri is None: - with sentry_sdk.push_scope() as scope: - scope.set_extra("project_ids", project_ids) - scope.set_extra("metric_mri", metric_mri) - sentry_sdk.capture_message("Invalid metric MRI detected") - - continue - - blocking_status = [] - if (metric_blocking := metrics_blocking_state.get(metric_mri)) is not None: - blocking_status = [ - BlockedMetric(isBlocked=is_blocked, blockedTags=blocked_tags, projectId=project_id) - for is_blocked, blocked_tags, project_id in metric_blocking - ] - # We delete the metric so that in the next steps we can just merge the remaining blocked metrics that are - # not stored. - del metrics_blocking_state[metric_mri] - - metrics_metas.append(_build_metric_meta(parsed_mri, project_ids, blocking_status)) - - for metric_mri, metric_blocking in metrics_blocking_state.items(): - parsed_mri = parse_mri(metric_mri) - if parsed_mri is None: - continue - - metrics_metas.append( - _build_metric_meta( - parsed_mri, - [], - [ - BlockedMetric( - isBlocked=is_blocked, blockedTags=blocked_tags, projectId=project_id - ) - for is_blocked, blocked_tags, project_id in metric_blocking - ], - ) - ) - - return metrics_metas - - def get_stored_metrics_of_projects( projects: Sequence[Project], use_case_ids: Sequence[UseCaseID], diff --git a/tests/sentry/api/endpoints/test_organization_metrics_details.py b/tests/sentry/api/endpoints/test_organization_metrics_details.py index ad19524fbcafc3..3e6c7b8661d8cc 100644 --- a/tests/sentry/api/endpoints/test_organization_metrics_details.py +++ b/tests/sentry/api/endpoints/test_organization_metrics_details.py @@ -9,6 +9,7 @@ ) from sentry.sentry_metrics.visibility import block_metric, block_tags_of_metric from sentry.testutils.cases import MetricsAPIBaseTestCase, OrganizationMetricsIntegrationTestCase +from sentry.testutils.helpers import override_options from sentry.testutils.skips import requires_snuba pytestmark = [pytest.mark.sentry_metrics, requires_snuba] @@ -220,3 +221,40 @@ def test_metrics_details_for_custom_metrics(self): assert data[2]["blockingStatus"] == [ {"isBlocked": True, "blockedTags": [], "projectId": project_1.id} ] + assert sorted(data[1]["operations"]) == [ + "avg", + "count", + "histogram", + "max", + "max_timestamp", + "min", + "min_timestamp", + "sum", + ] + + with override_options( + { + "sentry-metrics.metrics-api.enable-percentile-operations-for-orgs": [ + self.organization.id + ] + }, + ): + response = self.get_success_response( + self.organization.slug, project=[project_1.id, project_2.id], useCase="custom" + ) + data = sorted(response.data, key=lambda d: d["mri"]) + assert sorted(data[1]["operations"]) == [ + "avg", + "count", + "histogram", + "max", + "max_timestamp", + "min", + "min_timestamp", + "p50", + "p75", + "p90", + "p95", + "p99", + "sum", + ]