From 80aea58dda736506646c7ec4e5b6b449f317f414 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Thu, 24 Apr 2025 18:21:24 -0400 Subject: [PATCH] fix(eap): Has parent span filter This fixes the has:parent_span and !has:parent_span filters on eap spans. --- src/sentry/search/eap/columns.py | 17 ++++-- src/sentry/search/eap/spans/attributes.py | 4 +- src/sentry/utils/validators.py | 4 ++ .../test_organization_events_span_indexed.py | 55 +++++++++++++++++++ 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/sentry/search/eap/columns.py b/src/sentry/search/eap/columns.py index 37e91b53b5f445..42c29f242726af 100644 --- a/src/sentry/search/eap/columns.py +++ b/src/sentry/search/eap/columns.py @@ -1,4 +1,4 @@ -from collections.abc import Callable +from collections.abc import Callable, Iterable from dataclasses import dataclass, field from datetime import datetime from typing import Any, Literal, TypeAlias, TypedDict @@ -44,7 +44,7 @@ class ResolvedColumn: # Processor is the function run in the post process step to transform a row into the final result processor: Callable[[Any], Any] | None = None # Validator to check if the value in a query is correct - validator: Callable[[Any], bool] | None = None + validator: Callable[[Any], bool] | list[Callable[[Any], bool]] | None = None # Indicates this attribute is a secondary alias for the attribute. # It exists for compatibility or convenience reasons and should NOT be preferred. secondary_alias: bool = False @@ -56,9 +56,16 @@ def process_column(self, value: Any) -> Any: return value def validate(self, value: Any) -> None: - if self.validator is not None: - if not self.validator(value): - raise InvalidSearchQuery(f"{value} is an invalid value for {self.public_alias}") + if callable(self.validator): + if self.validator(value): + return + raise InvalidSearchQuery(f"{value} is an invalid value for {self.public_alias}") + + elif isinstance(self.validator, Iterable): + for validator in self.validator: + if validator(value): + return + raise InvalidSearchQuery(f"{value} is an invalid value for {self.public_alias}") @property def proto_type(self) -> AttributeKey.Type.ValueType: diff --git a/src/sentry/search/eap/spans/attributes.py b/src/sentry/search/eap/spans/attributes.py index 24bec50e206a32..bae822c4e793f1 100644 --- a/src/sentry/search/eap/spans/attributes.py +++ b/src/sentry/search/eap/spans/attributes.py @@ -21,7 +21,7 @@ ) from sentry.search.events.types import SnubaParams from sentry.search.utils import DEVICE_CLASS -from sentry.utils.validators import is_event_id, is_span_id +from sentry.utils.validators import is_empty_string, is_event_id, is_span_id def validate_event_id(value: str | list[str]) -> bool: @@ -45,7 +45,7 @@ def validate_event_id(value: str | list[str]) -> bool: public_alias="parent_span", internal_name="sentry.parent_span_id", search_type="string", - validator=is_span_id, + validator=[is_empty_string, is_span_id], ), ResolvedAttribute( public_alias="span.action", diff --git a/src/sentry/utils/validators.py b/src/sentry/utils/validators.py index ce70a6d9567cff..736f6745f5fdd0 100644 --- a/src/sentry/utils/validators.py +++ b/src/sentry/utils/validators.py @@ -27,3 +27,7 @@ def is_event_id(value): def is_span_id(value): return bool(HEXADECIMAL_16_DIGITS.search(force_str(value))) + + +def is_empty_string(value): + return force_str(value) == "" diff --git a/tests/snuba/api/endpoints/test_organization_events_span_indexed.py b/tests/snuba/api/endpoints/test_organization_events_span_indexed.py index 6b0e7b4f89bd12..845a8b2cf4e9a4 100644 --- a/tests/snuba/api/endpoints/test_organization_events_span_indexed.py +++ b/tests/snuba/api/endpoints/test_organization_events_span_indexed.py @@ -4271,3 +4271,58 @@ def test_empty_string_negation(self): }, ] assert meta["dataset"] == self.dataset + + def test_has_parent_span_filter(self): + spans = [ + self.create_span( + {"parent_span_id": None}, + start_ts=self.ten_mins_ago, + ), + self.create_span( + {}, + start_ts=self.ten_mins_ago, + ), + ] + self.store_spans(spans, is_eap=self.is_eap) + + response = self.do_request( + { + "field": ["parent_span"], + "query": "!has:parent_span", + "project": self.project.id, + "dataset": self.dataset, + } + ) + assert response.status_code == 200, response.content + data = response.data["data"] + meta = response.data["meta"] + assert len(data) == 1 + assert data == [ + { + "id": spans[0]["span_id"], + "parent_span": None, + "project.name": self.project.slug, + } + ] + assert meta["dataset"] == self.dataset + + response = self.do_request( + { + "field": ["parent_span"], + "query": "has:parent_span", + "project": self.project.id, + "dataset": self.dataset, + } + ) + assert response.status_code == 200, response.content + data = response.data["data"] + meta = response.data["meta"] + assert len(data) == 1 + assert data == [ + { + "id": spans[1]["span_id"], + "parent_span": spans[1]["parent_span_id"], + "project.name": self.project.slug, + } + ] + assert meta["dataset"] == self.dataset