From b89a513cc4dbe004f5ac1d06d32ddc3da0f44d0c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:44:25 +0000 Subject: [PATCH 1/2] feat(apidocs): Publish trace-logs endpoint Publish GET /organizations/{org}/trace-logs/ in the public API schema so it reaches @sentry/api. Type each log row and describe every response field, and fix the example's timestamp_precise, which the endpoint returns as an integer. Refs #130351 Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- .../api/endpoints/organization_trace_logs.py | 77 ++++++++++++++++--- .../examples/trace_item_attribute_examples.py | 2 +- .../events/test_organization_trace_logs.py | 31 ++++++++ 3 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 tests/apidocs/endpoints/events/test_organization_trace_logs.py diff --git a/src/sentry/api/endpoints/organization_trace_logs.py b/src/sentry/api/endpoints/organization_trace_logs.py index 69695f5bf0f0..37b82d675d28 100644 --- a/src/sentry/api/endpoints/organization_trace_logs.py +++ b/src/sentry/api/endpoints/organization_trace_logs.py @@ -1,4 +1,4 @@ -from typing import Any, TypedDict +from typing import Annotated, NotRequired, TypedDict from django.http import HttpResponse from drf_spectacular.utils import OpenApiParameter, extend_schema @@ -18,32 +18,87 @@ RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED, ) +from sentry.apidocs.descriptions import ApiDescription from sentry.apidocs.examples.trace_item_attribute_examples import TraceItemAttributeExamples +from sentry.apidocs.formats import IsoDateTime from sentry.apidocs.parameters import CursorQueryParam, GlobalParams from sentry.apidocs.utils import inline_sentry_response_serializer from sentry.models.organization import Organization from sentry.models.project import Project from sentry.organizations.services.organization import RpcOrganization from sentry.search.eap import constants -from sentry.search.eap.types import SearchResolverConfig +from sentry.search.eap.types import Confidence, SearchResolverConfig from sentry.search.events.types import EventsResponse, SnubaParams from sentry.snuba.ourlogs import OurLogs from sentry.snuba.referrer import Referrer from sentry.utils.validators import INVALID_ID_DETAILS, is_event_id -class TraceLogsMeta(TypedDict, total=False): - fields: dict[str, str] - full_scan: bool - bytes_scanned: int | None - routingHint: str +class TraceLogsMeta(TypedDict): + fields: Annotated[ + dict[str, str], ApiDescription("Type of each returned field, keyed by field name") + ] + full_scan: NotRequired[ + Annotated[ + bool, + ApiDescription( + "Whether the query scanned the full data set rather than a downsampled sample" + ), + ] + ] + bytes_scanned: NotRequired[ + Annotated[ + int | None, + ApiDescription("Number of bytes the query scanned, or null if it wasn't reported"), + ] + ] + routingHint: NotRequired[ + Annotated[ + str, + ApiDescription( + "Opaque routing hint of the query. Pass it unchanged as routing_hint when " + "fetching the details of a log" + ), + ] + ] + + +# Functional syntax because `project.id` isn't a valid identifier. +TraceLog = TypedDict( + "TraceLog", + { + "id": Annotated[str, ApiDescription("The ID of the log")], + "project.id": Annotated[int, ApiDescription("The ID of the project the log belongs to")], + "trace": Annotated[str, ApiDescription("The ID of the trace the log belongs to")], + "severity_number": Annotated[ + int, + ApiDescription( + "The OpenTelemetry severity number of the log, from 1 to 24, or 0 if unset" + ), + ], + "severity": Annotated[ + str, ApiDescription("The severity text of the log, such as `info` or `error`") + ], + "timestamp": Annotated[IsoDateTime, ApiDescription("When the log was recorded")], + "timestamp_precise": Annotated[ + int, ApiDescription("When the log was recorded, in nanoseconds since the Unix epoch") + ], + "message": Annotated[str, ApiDescription("The body of the log")], + }, +) # Only used for api docs class TraceLogsResponse(TypedDict): - data: list[dict[str, Any]] - meta: TraceLogsMeta - confidence: list[dict[str, Any]] + data: Annotated[list[TraceLog], ApiDescription("The logs, sorted by the `sort` parameter")] + meta: Annotated[TraceLogsMeta, ApiDescription("Metadata about the query and its fields")] + confidence: Annotated[ + list[dict[str, Confidence]], + ApiDescription( + "Sampling confidence of each row in `data`, keyed by field. Logs are not " + "aggregated, so each entry is an empty object" + ), + ] # The only columns trace logs can be sorted by; each may be prefixed with `-`. @@ -65,7 +120,7 @@ class OrganizationTraceLogsEndpoint(OrganizationEventsEndpointBase): """Replaces a call to events that isn't possible for team plans because of projects restrictions""" publish_status = { - "GET": ApiPublishStatus.PRIVATE, + "GET": ApiPublishStatus.PUBLIC, } def get_projects( diff --git a/src/sentry/apidocs/examples/trace_item_attribute_examples.py b/src/sentry/apidocs/examples/trace_item_attribute_examples.py index 5703f88056a3..b3a8117446be 100644 --- a/src/sentry/apidocs/examples/trace_item_attribute_examples.py +++ b/src/sentry/apidocs/examples/trace_item_attribute_examples.py @@ -93,7 +93,7 @@ "severity_number": 9, "severity": "INFO", "timestamp": "2026-09-01T12:00:00+00:00", - "timestamp_precise": 1.756728000123e18, + "timestamp_precise": 1756728000123000000, "message": "Payment processed", } ], diff --git a/tests/apidocs/endpoints/events/test_organization_trace_logs.py b/tests/apidocs/endpoints/events/test_organization_trace_logs.py new file mode 100644 index 000000000000..9e24bd5a3c13 --- /dev/null +++ b/tests/apidocs/endpoints/events/test_organization_trace_logs.py @@ -0,0 +1,31 @@ +from django.test.client import RequestFactory +from django.urls import reverse + +from fixtures.apidocs_test_case import APIDocsTestCase +from sentry.testutils.cases import OurLogTestCase, SnubaTestCase +from sentry.testutils.helpers.datetime import before_now + + +class OrganizationTraceLogsDocs(APIDocsTestCase, SnubaTestCase, OurLogTestCase): + def setUp(self) -> None: + self.login_as(user=self.user) + self.trace_id = "a" * 32 + self.store_eap_items( + [ + self.create_ourlog( + {"body": "Payment processed", "trace_id": self.trace_id}, + timestamp=before_now(minutes=10), + ), + ] + ) + self.url = reverse( + "sentry-api-0-organization-trace-logs", + kwargs={"organization_id_or_slug": self.organization.slug}, + ) + + def test_get(self) -> None: + response = self.client.get(self.url, {"traceId": self.trace_id, "statsPeriod": "1h"}) + assert len(response.data["data"]) == 1 + request = RequestFactory().get(self.url) + + self.validate_schema(request, response) From 58145e1c969770dc645886a905e6a66fbbd91d5e Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:04:07 +0000 Subject: [PATCH 2/2] test(seer): Pass a trace ID to trace-logs in the public GET matrix Publishing the trace-logs endpoint adds it to the agent token public GET matrix. The endpoint returns 400 without a traceId or replayId, so give it one like the other endpoints that need query params. --- tests/sentry/seer/endpoints/test_organization_agent_token.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/sentry/seer/endpoints/test_organization_agent_token.py b/tests/sentry/seer/endpoints/test_organization_agent_token.py index 8ec6f2d60a21..d394316f9cf6 100644 --- a/tests/sentry/seer/endpoints/test_organization_agent_token.py +++ b/tests/sentry/seer/endpoints/test_organization_agent_token.py @@ -1690,6 +1690,8 @@ def _query_params(self, endpoint: PublicGetEndpoint | PublicMutationEndpoint) -> return {"service_span": "permission-matrix-span", "project_id": self.project.id} if endpoint.endpoint_name == "OrganizationTraceItemAttributesEndpoint": return {"dataset": "spans", "project": [self.project.id]} + if endpoint.endpoint_name == "OrganizationTraceLogsEndpoint": + return {"traceId": uuid4().hex, "statsPeriod": "1h"} if endpoint.endpoint_name == "OrganizationTraceItemStatsEndpoint": return { "statsType": ["attributeDistributions"],