From 27eab2a2ac1ee5093829e3324c1c3d8fb3fb2885 Mon Sep 17 00:00:00 2001 From: William Mak Date: Wed, 27 May 2026 13:25:11 -0400 Subject: [PATCH 1/3] fix(trace-item-details): allow timestamp - This endpoint was defaulting to all time so queries were commonly timing out, add our usual snuba params style timestamps along with a trace style timestamp parameter with a buffer --- .../endpoints/project_trace_item_details.py | 35 ++++-- .../test_project_trace_item_details.py | 104 +++++++++++++++++- 2 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/sentry/api/endpoints/project_trace_item_details.py b/src/sentry/api/endpoints/project_trace_item_details.py index dc62ea7520b7..09d27474e8c0 100644 --- a/src/sentry/api/endpoints/project_trace_item_details.py +++ b/src/sentry/api/endpoints/project_trace_item_details.py @@ -1,5 +1,6 @@ import time import uuid +from datetime import timedelta from typing import Any, Literal import sentry_sdk @@ -17,6 +18,7 @@ from sentry.api.base import cell_silo_endpoint from sentry.api.bases.project import ProjectEndpoint from sentry.api.exceptions import BadRequest +from sentry.api.utils import get_date_range_from_params from sentry.auth.staff import is_active_staff from sentry.auth.superuser import is_active_superuser from sentry.models.project import Project @@ -36,8 +38,10 @@ translate_search_type_for_internal_column, translate_to_sentry_conventions, ) +from sentry.search.utils import parse_datetime_string from sentry.snuba.referrer import Referrer -from sentry.utils import json, snuba_rpc +from sentry.utils import json +from sentry.utils.snuba_rpc import trace_item_details_rpc _NUMERIC_COERCIONS: dict[str, type] = {"valFloat": float, "valDouble": float} _VAL_TYPE_TO_COLUMN_TYPE: dict[str, ColumnType] = { @@ -362,6 +366,21 @@ def get(request: Request, project: Project, item_id: str) -> Response: if not serializer.is_valid(): return Response(serializer.errors, status=400) + start, end = get_date_range_from_params(request.GET, optional=True) + if "timestamp" in request.GET: + example_timestamp = parse_datetime_string(request.GET["timestamp"]) + time_buffer = 1.5 + example_start = example_timestamp - timedelta(days=time_buffer) + example_end = example_timestamp + timedelta(days=time_buffer) + if start is not None: + start = max(start, example_start) + else: + start = example_start + if end is not None: + end = min(end, example_end) + else: + end = example_end + serialized = serializer.validated_data trace_id = serialized.get("trace_id") item_type = serialized.get("item_type") @@ -377,12 +396,14 @@ def get(request: Request, project: Project, item_id: str) -> Response: raise BadRequest(detail=f"Unknown trace item type: {item_type}") start_timestamp_proto = ProtoTimestamp() - start_timestamp_proto.FromSeconds(0) - end_timestamp_proto = ProtoTimestamp() - - # due to clock drift, the end time can be in the future - add a week to be safe - end_timestamp_proto.FromSeconds(int(time.time()) + 60 * 60 * 24 * 7) + if start is not None and end is not None: + start_timestamp_proto.FromDatetime(start) + end_timestamp_proto.FromDatetime(end) + else: + start_timestamp_proto.FromSeconds(0) + # due to clock drift, the end time can be in the future - add a week to be safe + end_timestamp_proto.FromSeconds(int(time.time()) + 60 * 60 * 24 * 7) trace_id = request.GET.get("trace_id") if not trace_id: @@ -403,7 +424,7 @@ def get(request: Request, project: Project, item_id: str) -> Response: trace_id=trace_id, ) - resp = MessageToDict(snuba_rpc.trace_item_details_rpc(req)) + resp = MessageToDict(trace_item_details_rpc(req)) use_sentry_conventions = features.has( "organizations:performance-sentry-conventions-fields", diff --git a/tests/snuba/api/endpoints/test_project_trace_item_details.py b/tests/snuba/api/endpoints/test_project_trace_item_details.py index 7694cabfbad3..6d4926c98b41 100644 --- a/tests/snuba/api/endpoints/test_project_trace_item_details.py +++ b/tests/snuba/api/endpoints/test_project_trace_item_details.py @@ -1,4 +1,5 @@ import uuid +from datetime import timedelta from unittest import mock import pytest @@ -32,7 +33,7 @@ def setUp(self) -> None: self.one_min_ago = before_now(minutes=1) self.trace_uuid = str(uuid.uuid4()).replace("-", "") - def do_request(self, event_type: str, item_id: str, features=None): + def do_request(self, event_type: str, item_id: str, extra_data=None, features=None): item_details_url = reverse( "sentry-api-0-project-trace-item-details", kwargs={ @@ -43,13 +44,16 @@ def do_request(self, event_type: str, item_id: str, features=None): ) if features is None: features = self.features + data = { + "item_type": event_type, + "trace_id": self.trace_uuid, + } + if extra_data is not None: + data.update(extra_data) with self.feature(features): return self.client.get( item_details_url, - { - "item_type": event_type, - "trace_id": self.trace_uuid, - }, + data, ) def test_simple(self) -> None: @@ -639,3 +643,93 @@ def test_attachment(self) -> None: "meta": {}, "timestamp": mock.ANY, } + + def test_with_timestamp(self) -> None: + log = self.create_ourlog( + { + "body": "foo", + "trace_id": self.trace_uuid, + }, + attributes={ + "str_attr": { + "string_value": "1", + }, + "int_attr": {"int_value": 2}, + "float_attr": { + "double_value": 3.0, + }, + "bool_attr": { + "bool_value": True, + }, + }, + timestamp=self.one_min_ago, + ) + self.store_eap_items([log]) + item_id = log.item_id.hex() + + for extra_data in [ + {"timestamp": self.one_min_ago.isoformat()}, + {"statsPeriod": "24h"}, + ]: + trace_details_response = self.do_request("logs", item_id, extra_data=extra_data) + + assert trace_details_response.status_code == 200, trace_details_response.content + + timestamp_nanos = int(self.one_min_ago.timestamp() * 1_000_000_000) + assert trace_details_response.data["attributes"] == [ + {"name": "tags[bool_attr,boolean]", "type": "bool", "value": True}, + {"name": "tags[float_attr,number]", "type": "float", "value": 3.0}, + { + "name": "observed_timestamp", + "type": "int", + "value": str(timestamp_nanos), + }, + {"name": "project_id", "type": "int", "value": str(self.project.id)}, + {"name": "severity_number", "type": "int", "value": "0"}, + {"name": "tags[int_attr,number]", "type": "int", "value": "2"}, + { + "name": "timestamp_precise", + "type": "int", + "value": str(timestamp_nanos), + }, + {"name": "message", "type": "str", "value": "foo"}, + {"name": "severity", "type": "str", "value": "INFO"}, + {"name": "str_attr", "type": "str", "value": "1"}, + {"name": "trace", "type": "str", "value": self.trace_uuid}, + ] + assert trace_details_response.data["itemId"] == item_id + assert ( + trace_details_response.data["timestamp"] + == self.one_min_ago.replace(microsecond=0, tzinfo=None).isoformat() + "Z" + ) + + def test_with_incorrect_timestamp(self) -> None: + log = self.create_ourlog( + { + "body": "foo", + "trace_id": self.trace_uuid, + }, + attributes={ + "str_attr": { + "string_value": "1", + }, + "int_attr": {"int_value": 2}, + "float_attr": { + "double_value": 3.0, + }, + "bool_attr": { + "bool_value": True, + }, + }, + timestamp=self.one_min_ago, + ) + self.store_eap_items([log]) + item_id = log.item_id.hex() + + for extra_data in [ + {"timestamp": (self.one_min_ago - timedelta(days=30)).isoformat()}, + {"statsPeriodEnd": "24h", "statsPeriodStart": "48h"}, + ]: + trace_details_response = self.do_request("logs", item_id, extra_data=extra_data) + + assert trace_details_response.status_code == 404, trace_details_response.content From 130b9d5de157b51ff2d0b075382c0e2659414462 Mon Sep 17 00:00:00 2001 From: William Mak Date: Wed, 27 May 2026 13:39:21 -0400 Subject: [PATCH 2/3] fix --- .../endpoints/project_trace_item_details.py | 13 ++++++-- .../test_project_trace_item_details.py | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/sentry/api/endpoints/project_trace_item_details.py b/src/sentry/api/endpoints/project_trace_item_details.py index 09d27474e8c0..d14074f32db9 100644 --- a/src/sentry/api/endpoints/project_trace_item_details.py +++ b/src/sentry/api/endpoints/project_trace_item_details.py @@ -21,6 +21,7 @@ from sentry.api.utils import get_date_range_from_params from sentry.auth.staff import is_active_staff from sentry.auth.superuser import is_active_superuser +from sentry.exceptions import InvalidParams from sentry.models.project import Project from sentry.search.eap import constants from sentry.search.eap.types import ( @@ -38,7 +39,7 @@ translate_search_type_for_internal_column, translate_to_sentry_conventions, ) -from sentry.search.utils import parse_datetime_string +from sentry.search.utils import InvalidQuery, parse_datetime_string from sentry.snuba.referrer import Referrer from sentry.utils import json from sentry.utils.snuba_rpc import trace_item_details_rpc @@ -366,9 +367,15 @@ def get(request: Request, project: Project, item_id: str) -> Response: if not serializer.is_valid(): return Response(serializer.errors, status=400) - start, end = get_date_range_from_params(request.GET, optional=True) + try: + start, end = get_date_range_from_params(request.GET, optional=True) + except InvalidParams: + return Response("date range parameters invalid", status=400) if "timestamp" in request.GET: - example_timestamp = parse_datetime_string(request.GET["timestamp"]) + try: + example_timestamp = parse_datetime_string(request.GET["timestamp"]) + except InvalidQuery: + return Response("timestamp parameter invalid", status=400) time_buffer = 1.5 example_start = example_timestamp - timedelta(days=time_buffer) example_end = example_timestamp + timedelta(days=time_buffer) diff --git a/tests/snuba/api/endpoints/test_project_trace_item_details.py b/tests/snuba/api/endpoints/test_project_trace_item_details.py index 6d4926c98b41..dc29c5c6f079 100644 --- a/tests/snuba/api/endpoints/test_project_trace_item_details.py +++ b/tests/snuba/api/endpoints/test_project_trace_item_details.py @@ -733,3 +733,34 @@ def test_with_incorrect_timestamp(self) -> None: trace_details_response = self.do_request("logs", item_id, extra_data=extra_data) assert trace_details_response.status_code == 404, trace_details_response.content + + def test_with_invalid_timestamp(self) -> None: + log = self.create_ourlog( + { + "body": "foo", + "trace_id": self.trace_uuid, + }, + attributes={ + "str_attr": { + "string_value": "1", + }, + "int_attr": {"int_value": 2}, + "float_attr": { + "double_value": 3.0, + }, + "bool_attr": { + "bool_value": True, + }, + }, + timestamp=self.one_min_ago, + ) + self.store_eap_items([log]) + item_id = log.item_id.hex() + + for extra_data in [ + {"timestamp": "beepboop"}, + {"statsPeriod": "hello"}, + ]: + trace_details_response = self.do_request("logs", item_id, extra_data=extra_data) + + assert trace_details_response.status_code == 400, trace_details_response.content From 228016a51d64a923d274de8787874dc953978243 Mon Sep 17 00:00:00 2001 From: William Mak Date: Wed, 27 May 2026 13:48:09 -0400 Subject: [PATCH 3/3] add tag --- src/sentry/api/endpoints/project_trace_item_details.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/api/endpoints/project_trace_item_details.py b/src/sentry/api/endpoints/project_trace_item_details.py index d14074f32db9..6efbba3419db 100644 --- a/src/sentry/api/endpoints/project_trace_item_details.py +++ b/src/sentry/api/endpoints/project_trace_item_details.py @@ -391,6 +391,7 @@ def get(request: Request, project: Project, item_id: str) -> Response: serialized = serializer.validated_data trace_id = serialized.get("trace_id") item_type = serialized.get("item_type") + sentry_sdk.set_tag("trace_item_details.item_type", item_type) referrer = serialized.get("referrer", Referrer.API_ORGANIZATION_TRACE_ITEM_DETAILS.value) trace_item_type = None