Skip to content

fix(eap): Has parent span filter #90348

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/sentry/search/eap/columns.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/search/eap/spans/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/sentry/utils/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) == ""
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading