From dd89e688969d8c1e0769e7f09e74f67290bfc6f4 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 4 Feb 2026 13:50:59 +0100 Subject: [PATCH 1/5] feat(span-streaming): Add more span properties --- sentry_sdk/_span_batcher.py | 3 +- sentry_sdk/traces.py | 108 +++++++++++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index 38ecc8da51..39d92a7301 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -71,7 +71,8 @@ def add(self, span: "StreamedSpan") -> None: def _to_transport_format(item: "StreamedSpan") -> "Any": # TODO[span-first] res: "dict[str, Any]" = { - "name": item.name, + "name": item.get_name(), + "status": item.get_status(), } if item._attributes: diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index a63a9bebb0..39e1eadd8a 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -6,15 +6,61 @@ """ import uuid +from enum import Enum from typing import TYPE_CHECKING +from sentry_sdk.consts import SPANDATA from sentry_sdk.utils import format_attribute if TYPE_CHECKING: - from typing import Optional + from typing import Optional, Union from sentry_sdk._types import Attributes, AttributeValue +FLAGS_CAPACITY = 10 + + +class SpanStatus(str, Enum): + OK = "ok" + ERROR = "error" + + def __str__(self) -> str: + return self.value + + +# Segment source, see +# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource +class SegmentSource(str, Enum): + COMPONENT = "component" + CUSTOM = "custom" + ROUTE = "route" + TASK = "task" + URL = "url" + VIEW = "view" + + def __str__(self) -> str: + return self.value + + +# These are typically high cardinality and the server hates them +LOW_QUALITY_SEGMENT_SOURCES = [ + SegmentSource.URL, +] + + +SOURCE_FOR_STYLE = { + "endpoint": SegmentSource.COMPONENT, + "function_name": SegmentSource.COMPONENT, + "handler_name": SegmentSource.COMPONENT, + "method_and_path_pattern": SegmentSource.ROUTE, + "path": SegmentSource.URL, + "route_name": SegmentSource.COMPONENT, + "route_pattern": SegmentSource.ROUTE, + "uri_template": SegmentSource.ROUTE, + "url": SegmentSource.ROUTE, +} + + class StreamedSpan: """ A span holds timing information of a block of code. @@ -26,9 +72,11 @@ class StreamedSpan: """ __slots__ = ( - "name", + "_name", "_attributes", "_trace_id", + "_status", + "_flags", ) def __init__( @@ -38,7 +86,7 @@ def __init__( attributes: "Optional[Attributes]" = None, trace_id: "Optional[str]" = None, ): - self.name: str = name + self._name: str = name self._attributes: "Attributes" = {} if attributes: for attribute, value in attributes.items(): @@ -46,6 +94,11 @@ def __init__( self._trace_id = trace_id + self.set_status(SpanStatus.OK) + self.set_source(SegmentSource.CUSTOM) + + self._flags: dict[str, bool] = {} + def get_attributes(self) -> "Attributes": return self._attributes @@ -62,6 +115,55 @@ def remove_attribute(self, key: str) -> None: except KeyError: pass + def get_status(self) -> "Union[SpanStatus, str]": + if self._status in SpanStatus: + return SpanStatus(self._status) + + return self._status + + def set_status(self, status: "Union[SpanStatus, str]") -> None: + if isinstance(status, Enum): + status = status.value + + self._status = status + + def set_http_status(self, http_status: int) -> None: + self.set_attribute(SPANDATA.HTTP_STATUS_CODE, http_status) + + if http_status >= 400: + self.set_status(SpanStatus.ERROR) + else: + self.set_status(SpanStatus.OK) + + def get_name(self) -> str: + return self._name + + def set_name(self, name: str) -> None: + self._name = name + + def set_flag(self, flag: str, result: bool) -> None: + if len(self._flags) < FLAGS_CAPACITY: + self._flags[flag] = result + + def set_op(self, op: str) -> None: + self.set_attribute("sentry.op", op) + + def set_origin(self, origin: str) -> None: + self.set_attribute("sentry.origin", origin) + + def set_source(self, source: "Union[str, SegmentSource]") -> None: + if isinstance(source, Enum): + source = source.value + + self.set_attribute("sentry.span.source", source) + + @property + def span_id(self) -> str: + if not self._span_id: + self._span_id = uuid.uuid4().hex[16:] + + return self._span_id + @property def trace_id(self) -> str: if not self._trace_id: From 014e2cc5135b53edd37fe215a8517261f6fc03af Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 4 Feb 2026 13:54:30 +0100 Subject: [PATCH 2/5] use attrs directly in batcher --- sentry_sdk/_span_batcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index 39d92a7301..993d51e800 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -71,8 +71,8 @@ def add(self, span: "StreamedSpan") -> None: def _to_transport_format(item: "StreamedSpan") -> "Any": # TODO[span-first] res: "dict[str, Any]" = { - "name": item.get_name(), - "status": item.get_status(), + "name": item._name, + "status": item._status, } if item._attributes: From ba0478e6e7f507373e5ce67672b6506ba80ca751 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 4 Feb 2026 14:01:24 +0100 Subject: [PATCH 3/5] fix span_id --- sentry_sdk/traces.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 39e1eadd8a..48533e2b48 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -74,6 +74,7 @@ class StreamedSpan: __slots__ = ( "_name", "_attributes", + "_span_id", "_trace_id", "_status", "_flags", @@ -92,7 +93,8 @@ def __init__( for attribute, value in attributes.items(): self.set_attribute(attribute, value) - self._trace_id = trace_id + self._span_id: "Optional[str]" = None + self._trace_id: "Optional[str]" = trace_id self.set_status(SpanStatus.OK) self.set_source(SegmentSource.CUSTOM) From 4768e99022d3eb419ac0a345a18394dd0cba25ce Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 4 Feb 2026 14:02:23 +0100 Subject: [PATCH 4/5] add span_Id to batcher --- sentry_sdk/_span_batcher.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index 993d51e800..947eca3806 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -71,6 +71,7 @@ def add(self, span: "StreamedSpan") -> None: def _to_transport_format(item: "StreamedSpan") -> "Any": # TODO[span-first] res: "dict[str, Any]" = { + "span_id": item.span_id, "name": item._name, "status": item._status, } From bae1f671d3d9ce82027f5e09d50e3766a1b5cf9e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 4 Feb 2026 14:27:34 +0100 Subject: [PATCH 5/5] compat --- sentry_sdk/traces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py index 48533e2b48..531a06b1fd 100644 --- a/sentry_sdk/traces.py +++ b/sentry_sdk/traces.py @@ -118,7 +118,7 @@ def remove_attribute(self, key: str) -> None: pass def get_status(self) -> "Union[SpanStatus, str]": - if self._status in SpanStatus: + if self._status in {s.value for s in SpanStatus}: return SpanStatus(self._status) return self._status