Skip to content

Commit d088e3b

Browse files
authored
fix(alerts): Skip validation for EAP columns; EXP-301 (#92276)
There's some really strict validation which does not need to happen for EAP alerts. Skipping column validation, we still validate aggregate.
1 parent 2173475 commit d088e3b

File tree

4 files changed

+69
-39
lines changed

4 files changed

+69
-39
lines changed

src/sentry/incidents/endpoints/serializers/alert_rule.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from sentry.sentry_apps.models.sentry_app_installation import prepare_ui_component
2828
from sentry.sentry_apps.services.app import app_service
2929
from sentry.sentry_apps.services.app.model import RpcSentryAppComponentContext
30+
from sentry.snuba.dataset import Dataset
3031
from sentry.snuba.models import SnubaQueryEventType
3132
from sentry.uptime.models import ProjectUptimeSubscription
3233
from sentry.users.models.user import User
@@ -262,7 +263,10 @@ def serialize(
262263
)
263264
# Temporary: Translate aggregate back here from `tags[sentry:user]` to `user` for the frontend.
264265
aggregate = translate_aggregate_field(
265-
obj.snuba_query.aggregate, reverse=True, allow_mri=allow_mri
266+
obj.snuba_query.aggregate,
267+
reverse=True,
268+
allow_mri=allow_mri,
269+
allow_eap=obj.snuba_query.dataset == Dataset.EventsAnalyticsPlatform.value,
266270
)
267271

268272
data: AlertRuleSerializerResponse = {

src/sentry/incidents/logic.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
METRICS_LAYER_UNSUPPORTED_TRANSACTION_METRICS_FUNCTIONS,
6161
SPANS_METRICS_FUNCTIONS,
6262
)
63-
from sentry.search.events.fields import is_function, is_typed_numeric_tag, resolve_field
63+
from sentry.search.events.fields import is_function, resolve_field
6464
from sentry.search.events.types import SnubaParams
6565
from sentry.seer.anomaly_detection.delete_rule import delete_rule_in_seer
6666
from sentry.seer.anomaly_detection.store_data import send_new_rule_data, update_rule_data
@@ -1753,6 +1753,7 @@ def get_opsgenie_teams(organization_id: int, integration_id: int) -> list[tuple[
17531753
]
17541754
EAP_FUNCTIONS = [
17551755
"count",
1756+
"count_unique",
17561757
"avg",
17571758
"p50",
17581759
"p75",
@@ -1766,7 +1767,9 @@ def get_opsgenie_teams(organization_id: int, integration_id: int) -> list[tuple[
17661767
]
17671768

17681769

1769-
def get_column_from_aggregate(aggregate: str, allow_mri: bool) -> str | None:
1770+
def get_column_from_aggregate(
1771+
aggregate: str, allow_mri: bool, allow_eap: bool = False
1772+
) -> str | None:
17701773
# These functions exist as SnQLFunction definitions and are not supported in the older
17711774
# logic for resolving functions. We parse these using `fields.is_function`, otherwise
17721775
# they will fail using the old resolve_field logic.
@@ -1778,7 +1781,7 @@ def get_column_from_aggregate(aggregate: str, allow_mri: bool) -> str | None:
17781781
return None if match.group("columns") == "" else match.group("columns")
17791782

17801783
# Skip additional validation for EAP queries. They don't exist in the old logic.
1781-
if match and match.group("function") in EAP_FUNCTIONS and match.group("columns") in EAP_COLUMNS:
1784+
if match and match.group("function") in EAP_FUNCTIONS and allow_eap:
17821785
return match.group("columns")
17831786

17841787
if allow_mri:
@@ -1817,7 +1820,7 @@ def check_aggregate_column_support(
18171820
aggregate: str, allow_mri: bool = False, allow_eap: bool = False
18181821
) -> bool:
18191822
# TODO(ddm): remove `allow_mri` once the experimental feature flag is removed.
1820-
column = get_column_from_aggregate(aggregate, allow_mri)
1823+
column = get_column_from_aggregate(aggregate, allow_mri, allow_eap)
18211824
match = is_function(aggregate)
18221825
function = match.group("function") if match else None
18231826
return (
@@ -1830,15 +1833,14 @@ def check_aggregate_column_support(
18301833
isinstance(function, str)
18311834
and column in INSIGHTS_FUNCTION_VALID_ARGS_MAP.get(function, [])
18321835
)
1833-
or (column in EAP_COLUMNS and allow_eap)
1834-
or (is_typed_numeric_tag(column) and allow_eap)
1836+
or allow_eap
18351837
)
18361838

18371839

18381840
def translate_aggregate_field(
1839-
aggregate: str, reverse: bool = False, allow_mri: bool = False
1841+
aggregate: str, reverse: bool = False, allow_mri: bool = False, allow_eap: bool = False
18401842
) -> str:
1841-
column = get_column_from_aggregate(aggregate, allow_mri)
1843+
column = get_column_from_aggregate(aggregate, allow_mri, allow_eap)
18421844
if not reverse:
18431845
if column in TRANSLATABLE_COLUMNS:
18441846
return aggregate.replace(column, TRANSLATABLE_COLUMNS[column])

src/sentry/snuba/snuba_query_validator.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import timedelta
44

55
from django.utils import timezone
6+
from django.utils.translation import gettext_lazy as _
67
from rest_framework import serializers
78
from snuba_sdk import Column, Condition, Entity, Limit, Op
89

@@ -116,35 +117,6 @@ def validate_query(self, query: str):
116117
)
117118
return query
118119

119-
def validate_aggregate(self, value: str):
120-
allow_mri = features.has(
121-
"organizations:custom-metrics",
122-
self.context["organization"],
123-
actor=self.context.get("user", None),
124-
) or features.has(
125-
"organizations:insights-alerts",
126-
self.context["organization"],
127-
actor=self.context.get("user", None),
128-
)
129-
allow_eap = features.has(
130-
"organizations:visibility-explore-view",
131-
self.context["organization"],
132-
actor=self.context.get("user", None),
133-
)
134-
try:
135-
if not check_aggregate_column_support(
136-
value,
137-
allow_mri=allow_mri,
138-
allow_eap=allow_eap,
139-
):
140-
raise serializers.ValidationError(
141-
"Invalid Metric: We do not currently support this field."
142-
)
143-
except InvalidSearchQuery as e:
144-
raise serializers.ValidationError(f"Invalid Metric: {e}")
145-
146-
return translate_aggregate_field(value, allow_mri=allow_mri)
147-
148120
def validate_event_types(self, value: Sequence[str]) -> list[SnubaQueryEventType.EventType]:
149121
try:
150122
return [SnubaQueryEventType.EventType[event_type.upper()] for event_type in value]
@@ -156,6 +128,7 @@ def validate_event_types(self, value: Sequence[str]) -> list[SnubaQueryEventType
156128

157129
def validate(self, data):
158130
data = super().validate(data)
131+
self._validate_aggregate(data)
159132
self._validate_query(data)
160133

161134
query_type = data["query_type"]
@@ -172,6 +145,35 @@ def validate(self, data):
172145

173146
return data
174147

148+
def _validate_aggregate(self, data):
149+
dataset = data.setdefault("dataset", Dataset.Events)
150+
aggregate = data.get("aggregate")
151+
allow_mri = features.has(
152+
"organizations:custom-metrics",
153+
self.context["organization"],
154+
actor=self.context.get("user", None),
155+
) or features.has(
156+
"organizations:insights-alerts",
157+
self.context["organization"],
158+
actor=self.context.get("user", None),
159+
)
160+
allow_eap = dataset == Dataset.EventsAnalyticsPlatform
161+
try:
162+
if not check_aggregate_column_support(
163+
aggregate,
164+
allow_mri=allow_mri,
165+
allow_eap=allow_eap,
166+
):
167+
raise serializers.ValidationError(
168+
{"aggregate": _("Invalid Metric: We do not currently support this field.")}
169+
)
170+
except InvalidSearchQuery as e:
171+
raise serializers.ValidationError({"aggregate": _(f"Invalid Metric: {e}")})
172+
173+
data["aggregate"] = translate_aggregate_field(
174+
aggregate, allow_mri=allow_mri, allow_eap=allow_eap
175+
)
176+
175177
def _validate_query(self, data):
176178
dataset = data.setdefault("dataset", Dataset.Events)
177179

@@ -184,7 +186,11 @@ def _validate_query(self, data):
184186
self.context["organization"],
185187
actor=self.context.get("user", None),
186188
):
187-
column = get_column_from_aggregate(data["aggregate"], allow_mri=True)
189+
column = get_column_from_aggregate(
190+
data["aggregate"],
191+
allow_mri=True,
192+
allow_eap=dataset == Dataset.EventsAnalyticsPlatform,
193+
)
188194
if is_mri(column) and dataset != Dataset.PerformanceMetrics:
189195
raise serializers.ValidationError(
190196
"You can use an MRI only on alerts on performance metrics"

tests/sentry/incidents/endpoints/test_organization_alert_rule_index.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,24 @@ def test_anomaly_detection_alert_eap(self, mock_seer_request):
408408
assert alert_rule.sensitivity == resp.data.get("sensitivity")
409409
assert mock_seer_request.call_count == 1
410410

411+
def test_create_alert_rule_eap(self):
412+
data = deepcopy(self.alert_rule_dict)
413+
data["dataset"] = "events_analytics_platform"
414+
data["alertType"] = "eap_metrics"
415+
data["aggregate"] = "count_unique(org)"
416+
with (
417+
outbox_runner(),
418+
self.feature(["organizations:incidents", "organizations:performance-view"]),
419+
):
420+
resp = self.get_success_response(
421+
self.organization.slug,
422+
status_code=201,
423+
**data,
424+
)
425+
assert "id" in resp.data
426+
alert_rule = AlertRule.objects.get(id=resp.data["id"])
427+
assert resp.data == serialize(alert_rule, self.user)
428+
411429
@with_feature("organizations:anomaly-detection-alerts")
412430
@with_feature("organizations:anomaly-detection-rollout")
413431
@with_feature("organizations:incidents")

0 commit comments

Comments
 (0)