Skip to content

Commit 47c69d1

Browse files
authored
feat(tracemetrics): Enforce metrics query params behind feature flag (#102683)
1 parent 727adc5 commit 47c69d1

File tree

6 files changed

+76
-54
lines changed

6 files changed

+76
-54
lines changed

src/sentry/api/endpoints/organization_events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from sentry.ratelimits.config import RateLimitConfig
3232
from sentry.search.eap.trace_metrics.config import (
3333
TraceMetricsSearchResolverConfig,
34-
get_trace_metric_info,
34+
get_trace_metric_from_request,
3535
)
3636
from sentry.search.eap.types import AdditionalQueries, FieldsACL, SearchResolverConfig
3737
from sentry.snuba import (
@@ -531,7 +531,7 @@ def get_rpc_config():
531531
)
532532
elif scoped_dataset == TraceMetrics:
533533
# tracemetrics uses aggregate conditions
534-
metric_name, metric_type = get_trace_metric_info(request)
534+
metric_name, metric_type = get_trace_metric_from_request(request, organization)
535535

536536
return TraceMetricsSearchResolverConfig(
537537
metric_name=metric_name,

src/sentry/api/endpoints/organization_events_stats.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from sentry.models.organization import Organization
2222
from sentry.search.eap.trace_metrics.config import (
2323
TraceMetricsSearchResolverConfig,
24-
get_trace_metric_info,
24+
get_trace_metric_from_request,
2525
)
2626
from sentry.search.eap.types import SearchResolverConfig
2727
from sentry.search.events.types import SnubaParams
@@ -243,7 +243,7 @@ def get_rpc_config():
243243

244244
if scoped_dataset == TraceMetrics:
245245
# tracemetrics uses aggregate conditions
246-
metric_name, metric_type = get_trace_metric_info(request)
246+
metric_name, metric_type = get_trace_metric_from_request(request, organization)
247247

248248
return TraceMetricsSearchResolverConfig(
249249
metric_name=metric_name,

src/sentry/features/temporary.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,9 @@ def register_temporary_features(manager: FeatureManager) -> None:
590590
manager.add("organizations:tracemetrics-stats", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
591591
# Enable trace metrics in trace view UI
592592
manager.add("organizations:tracemetrics-traceview-ui", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
593+
# Enable trace metrics top level params
594+
manager.add("organizations:tracemetrics-top-level-params", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
595+
593596
# Enable feature flag to send expanded sentry apps webhooks (issue.created for all group categories)
594597
manager.add("organizations:expanded-sentry-apps-webhooks", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
595598
# Enable using paginated projects endpoint for Jira integration

src/sentry/search/eap/trace_metrics/config.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass
22
from typing import Literal, cast
33

4+
from rest_framework.exceptions import ErrorDetail, ValidationError
45
from rest_framework.request import Request
56
from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue
67
from sentry_protos.snuba.v1.trace_item_filter_pb2 import (
@@ -9,6 +10,8 @@
910
TraceItemFilter,
1011
)
1112

13+
from sentry import features
14+
from sentry.models.organization import Organization
1215
from sentry.search.eap.resolver import SearchResolver
1316
from sentry.search.eap.types import SearchResolverConfig
1417

@@ -55,33 +58,36 @@ def extra_conditions(self, search_resolver: SearchResolver) -> TraceItemFilter |
5558
ALLOWED_METRIC_TYPES: list[MetricType] = ["counter", "gauge", "distribution"]
5659

5760

58-
def get_trace_metric_info(
61+
def get_trace_metric_from_request(
5962
request: Request,
63+
organization: Organization,
6064
) -> tuple[str, MetricType]:
6165
metric_name = request.GET.get("metricName")
6266
metric_type = request.GET.get("metricType")
6367

64-
# TODO: remove this when these args are not optional anymore
65-
if not metric_name:
66-
metric_name = ""
67-
if not metric_type:
68-
metric_type = ""
69-
70-
# errors = {}
71-
72-
# if not metric_name:
73-
# errors["metricName"] = ErrorDetail("This field is required.", code="required")
74-
75-
# if not metric_type:
76-
# errors["metricType"] = ErrorDetail("This field is required.", code="required")
77-
# elif metric_type not in ALLOWED_METRIC_TYPES:
78-
# errors["metricType"] = ErrorDetail(
79-
# string=f'"{metric_type}" is not a valid choice.', code="invalid_choice"
80-
# )
68+
if not features.has(
69+
"organizations:tracemetrics-top-level-params", organization=organization, actor=request.user
70+
):
71+
if not metric_name:
72+
metric_name = ""
73+
if not metric_type:
74+
metric_type = ""
75+
else:
76+
errors = {}
77+
78+
if not metric_name:
79+
errors["metricName"] = ErrorDetail("This field is required.", code="required")
80+
81+
if not metric_type:
82+
errors["metricType"] = ErrorDetail("This field is required.", code="required")
83+
elif metric_type not in ALLOWED_METRIC_TYPES:
84+
errors["metricType"] = ErrorDetail(
85+
string=f'"{metric_type}" is not a valid choice.', code="invalid_choice"
86+
)
8187

82-
# if errors:
83-
# raise ValidationError(errors)
88+
if errors:
89+
raise ValidationError(errors)
8490

85-
# assert metric_name
91+
assert metric_name
8692

8793
return metric_name, cast(MetricType, metric_type)

src/sentry/search/eap/trace_metrics/formulas.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from sentry.exceptions import InvalidSearchQuery
1212
from sentry.search.eap.columns import (
13+
AttributeArgumentDefinition,
1314
FormulaDefinition,
1415
ResolvedArguments,
1516
ResolverSettings,
@@ -80,8 +81,13 @@ def per_minute(args: ResolvedArguments, settings: ResolverSettings) -> Column.Bi
8081
"per_second": FormulaDefinition(
8182
default_search_type="rate",
8283
arguments=[
83-
ValueArgumentDefinition(
84-
argument_types={"string"},
84+
AttributeArgumentDefinition(
85+
attribute_types={
86+
"string",
87+
"number",
88+
"integer",
89+
},
90+
default_arg="value",
8591
),
8692
ValueArgumentDefinition(
8793
argument_types={"string"},
@@ -90,12 +96,18 @@ def per_minute(args: ResolvedArguments, settings: ResolverSettings) -> Column.Bi
9096
],
9197
formula_resolver=per_second,
9298
is_aggregate=True,
99+
infer_search_type_from_arguments=False,
93100
),
94101
"per_minute": FormulaDefinition(
95102
default_search_type="rate",
96103
arguments=[
97-
ValueArgumentDefinition(
98-
argument_types={"string"},
104+
AttributeArgumentDefinition(
105+
attribute_types={
106+
"string",
107+
"number",
108+
"integer",
109+
},
110+
default_arg="value",
99111
),
100112
ValueArgumentDefinition(
101113
argument_types={"string"},
@@ -104,5 +116,6 @@ def per_minute(args: ResolvedArguments, settings: ResolverSettings) -> Column.Bi
104116
],
105117
formula_resolver=per_minute,
106118
is_aggregate=True,
119+
infer_search_type_from_arguments=False,
107120
),
108121
}

tests/snuba/api/endpoints/test_organization_events_trace_metrics.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,36 @@
99
class OrganizationEventsTraceMetricsEndpointTest(OrganizationEventsEndpointTestBase):
1010
dataset = "tracemetrics"
1111

12-
@pytest.mark.skip(reason="not implemented yet")
1312
def test_missing_metric_name_and_type(self):
14-
response = self.do_request(
15-
{
16-
"field": ["sum(value)"],
17-
"dataset": self.dataset,
18-
"project": self.project.id,
13+
with self.feature("organizations:tracemetrics-top-level-params"):
14+
response = self.do_request(
15+
{
16+
"field": ["sum(value)"],
17+
"dataset": self.dataset,
18+
"project": self.project.id,
19+
}
20+
)
21+
assert response.status_code == 400, response.content
22+
assert response.data == {
23+
"metricName": ErrorDetail("This field is required.", code="required"),
24+
"metricType": ErrorDetail("This field is required.", code="required"),
1925
}
20-
)
21-
assert response.status_code == 400, response.content
22-
assert response.data == {
23-
"metricName": ErrorDetail("This field is required.", code="required"),
24-
"metricType": ErrorDetail("This field is required.", code="required"),
25-
}
2626

27-
@pytest.mark.skip(reason="not implemented yet")
2827
def test_invalid_metric_type(self):
29-
response = self.do_request(
30-
{
31-
"metricName": "foo",
32-
"metricType": "bar",
33-
"field": ["sum(value)"],
34-
"dataset": self.dataset,
35-
"project": self.project.id,
28+
with self.feature("organizations:tracemetrics-top-level-params"):
29+
response = self.do_request(
30+
{
31+
"metricName": "foo",
32+
"metricType": "bar",
33+
"field": ["sum(value)"],
34+
"dataset": self.dataset,
35+
"project": self.project.id,
36+
}
37+
)
38+
assert response.status_code == 400, response.content
39+
assert response.data == {
40+
"metricType": ErrorDetail('"bar" is not a valid choice.', code="invalid_choice"),
3641
}
37-
)
38-
assert response.status_code == 400, response.content
39-
assert response.data == {
40-
"metricType": ErrorDetail('"bar" is not a valid choice.', code="invalid_choice"),
41-
}
4242

4343
def test_simple_deprecated(self) -> None:
4444
trace_metrics = [

0 commit comments

Comments
 (0)