Skip to content
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
1 change: 1 addition & 0 deletions src/sentry/search/events/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ class ThresholdDict(TypedDict):
"timestamp",
"timestamp.to_hour",
"timestamp.to_day",
"error.received",
}
NON_FAILURE_STATUS = {"ok", "cancelled", "unknown"}
HTTP_SERVER_ERROR_STATUS = {
Expand Down
9 changes: 3 additions & 6 deletions src/sentry/search/events/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
SEMVER_PACKAGE_ALIAS,
SEMVER_WILDCARDS,
TEAM_KEY_TRANSACTION_ALIAS,
TIMESTAMP_FIELDS,
TRANSACTION_STATUS_ALIAS,
USER_DISPLAY_ALIAS,
)
Expand Down Expand Up @@ -578,13 +579,9 @@ def convert_search_filter_to_snuba_query(
elif name in ARRAY_FIELDS and search_filter.value.raw_value == "":
return [["notEmpty", [name]], "=", 1 if search_filter.operator == "!=" else 0]
else:
# timestamp{,.to_{hour,day}} need a datetime string
# TIMESTAMP_FIELDS need a datetime string
# last_seen needs an integer
if isinstance(value, datetime) and name not in {
"timestamp",
"timestamp.to_hour",
"timestamp.to_day",
}:
if isinstance(value, datetime) and name not in TIMESTAMP_FIELDS:
value = int(value.timestamp()) * 1000

if name in {"trace.span", "trace.parent_span"}:
Expand Down
44 changes: 43 additions & 1 deletion tests/sentry/search/events/builder/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
from snuba_sdk.function import Function

from sentry.search.events.builder.errors import ErrorsQueryBuilder
from sentry.search.events.types import QueryBuilderConfig
from sentry.search.events.types import QueryBuilderConfig, SnubaParams
from sentry.snuba.dataset import Dataset
from sentry.snuba.errors import PARSER_CONFIG_OVERRIDES
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers.datetime import before_now

pytestmark = pytest.mark.sentry_metrics

Expand Down Expand Up @@ -121,3 +122,44 @@ def test_is_status_simple_query(self) -> None:
self.projects,
),
]

def test_error_received_filter_uses_datetime(self) -> None:
"""Test that error.received filter uses datetime comparison, not epoch integer."""
snuba_dataclass = SnubaParams(
start=before_now(days=1),
end=before_now(),
projects=[self.project],
organization=self.organization,
)

filter_date = before_now(hours=12)
query = ErrorsQueryBuilder(
dataset=Dataset.Events,
query=f"error.received:>{filter_date.isoformat()}",
selected_columns=["count()"],
params={
"project_id": self.projects,
},
snuba_params=snuba_dataclass,
offset=None,
limit=None,
config=QueryBuilderConfig(),
).get_snql_query()
query.validate()

# Find the error.received condition in the where clause
received_conditions = [
c
for c in query.query.where
if isinstance(c, Condition) and isinstance(c.lhs, Column) and c.lhs.name == "received"
]

assert len(received_conditions) == 1, "Should have exactly one error.received condition"
received_condition = received_conditions[0]

# Verify the condition uses datetime comparison, not epoch integer
assert received_condition.op == Op.GT
# The RHS should be a datetime or datetime-formatted Function, not an integer
assert not isinstance(
received_condition.rhs, int
), "error.received should compare against datetime, not epoch integer"
47 changes: 47 additions & 0 deletions tests/snuba/api/endpoints/test_organization_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -7339,3 +7339,50 @@ def test_remapping(self) -> None:
assert meta["fields"]["transaction.duration"] == "duration"
assert meta["units"]["span.duration"] == "millisecond"
assert meta["units"]["transaction.duration"] == "millisecond"

def test_error_received_filter(self) -> None:
"""Test that error.received filter works correctly with datetime comparison."""
# Store an event 10 minutes ago
self.store_event(
data={
"event_id": "a" * 32,
"timestamp": self.ten_mins_ago_iso,
"fingerprint": ["group1"],
"message": "older event",
},
project_id=self.project.id,
)

# Store an event 9 minutes ago
nine_mins_ago_iso = self.nine_mins_ago.replace(microsecond=0).isoformat()
self.store_event(
data={
"event_id": "b" * 32,
"timestamp": nine_mins_ago_iso,
"fingerprint": ["group2"],
"message": "newer event",
},
project_id=self.project.id,
)

# Query for events received after 10 mins ago (should only get the newer one)
query = {
"field": ["count()", "message"],
"statsPeriod": "1h",
"query": f"error.received:>{self.ten_mins_ago_iso}",
"dataset": "errors",
}
response = self.do_request(query)
assert response.status_code == 200, response.content
assert response.data["data"][0]["count()"] == 1

# Query for events received after 11 mins ago (should get both)
query = {
"field": ["count()"],
"statsPeriod": "1h",
"query": f"error.received:>{self.eleven_mins_ago_iso}",
"dataset": "errors",
}
response = self.do_request(query)
assert response.status_code == 200, response.content
assert response.data["data"][0]["count()"] == 2
Loading