Skip to content

Commit f0d2fbe

Browse files
authored
fix(eap): Has parent span filter (#90348)
This fixes the has:parent_span and !has:parent_span filters on eap spans.
1 parent aed0b68 commit f0d2fbe

File tree

4 files changed

+73
-7
lines changed

4 files changed

+73
-7
lines changed

src/sentry/search/eap/columns.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import Callable
1+
from collections.abc import Callable, Iterable
22
from dataclasses import dataclass, field
33
from datetime import datetime
44
from typing import Any, Literal, TypeAlias, TypedDict
@@ -44,7 +44,7 @@ class ResolvedColumn:
4444
# Processor is the function run in the post process step to transform a row into the final result
4545
processor: Callable[[Any], Any] | None = None
4646
# Validator to check if the value in a query is correct
47-
validator: Callable[[Any], bool] | None = None
47+
validator: Callable[[Any], bool] | list[Callable[[Any], bool]] | None = None
4848
# Indicates this attribute is a secondary alias for the attribute.
4949
# It exists for compatibility or convenience reasons and should NOT be preferred.
5050
secondary_alias: bool = False
@@ -56,9 +56,16 @@ def process_column(self, value: Any) -> Any:
5656
return value
5757

5858
def validate(self, value: Any) -> None:
59-
if self.validator is not None:
60-
if not self.validator(value):
61-
raise InvalidSearchQuery(f"{value} is an invalid value for {self.public_alias}")
59+
if callable(self.validator):
60+
if self.validator(value):
61+
return
62+
raise InvalidSearchQuery(f"{value} is an invalid value for {self.public_alias}")
63+
64+
elif isinstance(self.validator, Iterable):
65+
for validator in self.validator:
66+
if validator(value):
67+
return
68+
raise InvalidSearchQuery(f"{value} is an invalid value for {self.public_alias}")
6269

6370
@property
6471
def proto_type(self) -> AttributeKey.Type.ValueType:

src/sentry/search/eap/spans/attributes.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
)
2222
from sentry.search.events.types import SnubaParams
2323
from sentry.search.utils import DEVICE_CLASS
24-
from sentry.utils.validators import is_event_id, is_span_id
24+
from sentry.utils.validators import is_empty_string, is_event_id, is_span_id
2525

2626

2727
def validate_event_id(value: str | list[str]) -> bool:
@@ -45,7 +45,7 @@ def validate_event_id(value: str | list[str]) -> bool:
4545
public_alias="parent_span",
4646
internal_name="sentry.parent_span_id",
4747
search_type="string",
48-
validator=is_span_id,
48+
validator=[is_empty_string, is_span_id],
4949
),
5050
ResolvedAttribute(
5151
public_alias="span.action",

src/sentry/utils/validators.py

+4
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ def is_event_id(value):
2727

2828
def is_span_id(value):
2929
return bool(HEXADECIMAL_16_DIGITS.search(force_str(value)))
30+
31+
32+
def is_empty_string(value):
33+
return force_str(value) == ""

tests/snuba/api/endpoints/test_organization_events_span_indexed.py

+55
Original file line numberDiff line numberDiff line change
@@ -4271,3 +4271,58 @@ def test_empty_string_negation(self):
42714271
},
42724272
]
42734273
assert meta["dataset"] == self.dataset
4274+
4275+
def test_has_parent_span_filter(self):
4276+
spans = [
4277+
self.create_span(
4278+
{"parent_span_id": None},
4279+
start_ts=self.ten_mins_ago,
4280+
),
4281+
self.create_span(
4282+
{},
4283+
start_ts=self.ten_mins_ago,
4284+
),
4285+
]
4286+
self.store_spans(spans, is_eap=self.is_eap)
4287+
4288+
response = self.do_request(
4289+
{
4290+
"field": ["parent_span"],
4291+
"query": "!has:parent_span",
4292+
"project": self.project.id,
4293+
"dataset": self.dataset,
4294+
}
4295+
)
4296+
assert response.status_code == 200, response.content
4297+
data = response.data["data"]
4298+
meta = response.data["meta"]
4299+
assert len(data) == 1
4300+
assert data == [
4301+
{
4302+
"id": spans[0]["span_id"],
4303+
"parent_span": None,
4304+
"project.name": self.project.slug,
4305+
}
4306+
]
4307+
assert meta["dataset"] == self.dataset
4308+
4309+
response = self.do_request(
4310+
{
4311+
"field": ["parent_span"],
4312+
"query": "has:parent_span",
4313+
"project": self.project.id,
4314+
"dataset": self.dataset,
4315+
}
4316+
)
4317+
assert response.status_code == 200, response.content
4318+
data = response.data["data"]
4319+
meta = response.data["meta"]
4320+
assert len(data) == 1
4321+
assert data == [
4322+
{
4323+
"id": spans[1]["span_id"],
4324+
"parent_span": spans[1]["parent_span_id"],
4325+
"project.name": self.project.slug,
4326+
}
4327+
]
4328+
assert meta["dataset"] == self.dataset

0 commit comments

Comments
 (0)