From 186718ea95f9cc28b6de76f01e836de15903cb59 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 19 Nov 2025 08:57:57 +0100 Subject: [PATCH 01/13] Add trace_lifecycle experimental opt --- sentry_sdk/_types.py | 2 ++ sentry_sdk/consts.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 66ed7df4f7..b4cb31c314 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -336,3 +336,5 @@ class SDKInfo(TypedDict): ) HttpStatusCodeRange = Union[int, Container[int]] + + TraceLifecycleMode = Literal["static", "stream"] diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index f74ea4eba4..7987708ed5 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -56,6 +56,7 @@ class CompressionAlgo(Enum): Metric, ProfilerMode, TracesSampler, + TraceLifecycleMode, TransactionProcessor, ) @@ -81,6 +82,7 @@ class CompressionAlgo(Enum): "before_send_log": Optional[Callable[[Log, Hint], Optional[Log]]], "enable_metrics": Optional[bool], "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], + "trace_lifecycle": Optional[TraceLifecycleMode], }, total=False, ) From 93c83e23ab48013707afe647700a25c5a7f2fcb2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 19 Nov 2025 16:35:37 +0100 Subject: [PATCH 02/13] . --- sentry_sdk/_types.py | 29 +++++++++++++++++++++++++++++ sentry_sdk/client.py | 9 +++++++++ sentry_sdk/scope.py | 8 +++++--- sentry_sdk/tracing.py | 28 +++++++++++++++++++++++----- sentry_sdk/tracing_utils.py | 8 ++++++++ sentry_sdk/utils.py | 17 +++++++++++++++++ 6 files changed, 91 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index b4cb31c314..5298663602 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -1,3 +1,4 @@ +from consts import SPANSTATUS from typing import TYPE_CHECKING, TypeVar, Union @@ -222,6 +223,17 @@ class SDKInfo(TypedDict): # TODO: Make a proper type definition for this (PRs welcome!) Hint = Dict[str, Any] + # TODO: Consolidate attribute types. E.g. metrics use the same attr structure. + # I'm keeping span attrs separate for now so that the span first stuff is + # isolated until it's not experimental. + SpanAttributeValue = TypedDict( + "SpanAttributeValue", + { + "type": Literal["string", "boolean", "double", "integer"], + "value": str | bool | float | int, + }, + ) + Log = TypedDict( "Log", { @@ -260,6 +272,23 @@ class SDKInfo(TypedDict): MetricProcessor = Callable[[Metric, Hint], Optional[Metric]] + # This is the V2 span format + # https://develop.sentry.dev/sdk/telemetry/spans/span-protocol/ + SpanV2 = TypedDict( + "SpanV2", + { + "trace_id": str, + "span_id": str, + "parent_span_id": Optional[str], + "name": str, + "status": Literal[SPANSTATUS.OK, SPANSTATUS.ERROR], + "is_segment": bool, + "start_timestamp": float, + "end_timestamp": float, + "attributes": Optional[dict[str, SpanAttributeValue]], + }, + ) + # TODO: Make a proper type definition for this (PRs welcome!) Breadcrumb = Dict[str, Any] diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 2c245297bd..96a5a87fa1 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -11,6 +11,7 @@ import sentry_sdk from sentry_sdk._compat import PY37, check_uwsgi_thread_support from sentry_sdk._metrics_batcher import MetricsBatcher +from sentry_sdk._span_batcher import SpanBatcher from sentry_sdk.utils import ( AnnotatedValue, ContextVar, @@ -187,6 +188,7 @@ def __init__(self, options=None): self.monitor = None # type: Optional[Monitor] self.log_batcher = None # type: Optional[LogBatcher] self.metrics_batcher = None # type: Optional[MetricsBatcher] + self._span_batcher = None # type: Optional[SpanBatcher] def __getstate__(self, *args, **kwargs): # type: (*Any, **Any) -> Any @@ -398,6 +400,13 @@ def _record_lost_event( record_lost_func=_record_lost_event, ) + self._span_batcher = None + if self.options["_experiments"].get("trace_lifecycle", None) == "stream": + self._span_batcher = SpanBatcher( + capture_func=_capture_envelope, + record_lost_func=_record_lost_event, + ) + max_request_body_size = ("always", "never", "small", "medium") if self.options["max_request_body_size"] not in max_request_body_size: raise ValueError( diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 8e55add770..68b5174632 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -22,6 +22,7 @@ from sentry_sdk.session import Session from sentry_sdk.tracing_utils import ( Baggage, + has_streaming_enabled, has_tracing_enabled, normalize_incoming_data, PropagationContext, @@ -1124,9 +1125,10 @@ def start_transaction( transaction.set_profiler_id(get_profiler_id()) # we don't bother to keep spans if we already know we're not going to - # send the transaction - max_spans = (client.options["_experiments"].get("max_spans")) or 1000 - transaction.init_span_recorder(maxlen=max_spans) + # send the transaction or if we're in streaming mode + if not has_streaming_enabled(client.options): + max_spans = (client.options["_experiments"].get("max_spans")) or 1000 + transaction.init_span_recorder(maxlen=max_spans) return transaction diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0d652e490a..6458fe7daa 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -711,6 +711,14 @@ def finish(self, scope=None, end_timestamp=None): scope = scope or sentry_sdk.get_current_scope() maybe_create_breadcrumbs_from_span(scope, self) + client = sentry_sdk.get_client() + if client.is_active(): + if ( + has_span_streaming_enabled(client.options) + and self.containing_transaction.sampled + ): + client._span_batcher.add(self) + return None def to_json(self): @@ -857,6 +865,12 @@ def __init__( # type: ignore[misc] else: self._sample_rand = _generate_sample_rand(self.trace_id) + self._mode = "static" + client = sentry_sdk.get_client() + if client.is_active(): + if has_span_streaming_enabled(client.options): + self._mode = "stream" + def __repr__(self): # type: () -> str return ( @@ -882,9 +896,11 @@ def _possibly_started(self): with sentry_sdk.start_transaction, and therefore the transaction will be discarded. """ - - # We must explicitly check self.sampled is False since self.sampled can be None - return self._span_recorder is not None or self.sampled is False + if self._mode == "static": + # We must explicitly check self.sampled is False since self.sampled can be None + return self._span_recorder is not None or self.sampled is False + else: + return True def __enter__(self): # type: () -> Transaction @@ -972,7 +988,8 @@ def finish( ): # type: (...) -> Optional[str] """Finishes the transaction and sends it to Sentry. - All finished spans in the transaction will also be sent to Sentry. + If we're in non-streaming mode, all finished spans in the transaction + will also be sent to Sentry at this point. :param scope: The Scope to use for this transaction. If not provided, the current Scope will be used. @@ -1000,7 +1017,7 @@ def finish( # We have no active client and therefore nowhere to send this transaction. return None - if self._span_recorder is None: + if self._mode == "static" and self._span_recorder is None: # Explicit check against False needed because self.sampled might be None if self.sampled is False: logger.debug("Discarding transaction because sampled = False") @@ -1482,5 +1499,6 @@ def calculate_interest_rate(amount, rate, years): extract_sentrytrace_data, _generate_sample_rand, has_tracing_enabled, + has_span_streaming_enabled, maybe_create_breadcrumbs_from_span, ) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 6506cca266..6e9d5bc1a2 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -110,6 +110,14 @@ def has_tracing_enabled(options): ) +def has_span_streaming_enabled(options): + # type: (Optional[Dict[str, Any]]) -> bool + if options is None: + return False + + return options.get("_experiments").get("trace_lifecycle") == "stream" + + @contextlib.contextmanager def record_sql_queries( cursor, # type: Any diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index eae6156b13..fb4e1733c6 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -2076,3 +2076,20 @@ def get_before_send_metric(options): return options.get("before_send_metric") or options["_experiments"].get( "before_send_metric" ) + + +def format_attribute_value(value): + # type: (Any) -> dict[str, bool | str | int | float] + if isinstance(value, bool): + return {"value": value, "type": "boolean"} + + if isinstance(value, int): + return {"value": value, "type": "integer"} + + if isinstance(value, float): + return {"value": value, "type": "double"} + + if isinstance(value, str): + return {"value": value, "type": "string"} + + return {"value": safe_repr(value), "type": "string"} From ba91268828eb8e839f68c06ea20af40dded58f32 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 19 Nov 2025 16:39:18 +0100 Subject: [PATCH 03/13] track batcher file --- sentry_sdk/_span_batcher.py | 189 ++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 sentry_sdk/_span_batcher.py diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py new file mode 100644 index 0000000000..65ec1ada91 --- /dev/null +++ b/sentry_sdk/_span_batcher.py @@ -0,0 +1,189 @@ +# This file is experimental and its contents may change without notice. This is +# a simple POC buffer implementation. Eventually, we should switch to a telemetry +# buffer: https://develop.sentry.dev/sdk/telemetry/telemetry-buffer/ + +import os +import random +import threading +from collections import defaultdict +from datetime import datetime, timezone +from typing import Optional, List, Callable, TYPE_CHECKING, Any + +from sentry_sdk.consts import SPANSTATUS +from sentry_sdk.utils import format_attribute_value, format_timestamp, safe_repr +from sentry_sdk.envelope import Envelope, Item, PayloadRef +from sentry_sdk.tracing import Transaction + +if TYPE_CHECKING: + from sentry_sdk.tracing import Span + from sentry_sdk._types import SpanV2 + + +class SpanBatcher: + # TODO[span-first]: Adjust limits. However, there's still a restriction of + # at most 1000 spans per envelope. + MAX_SPANS_BEFORE_FLUSH = 1_000 + MAX_SPANS_BEFORE_DROP = 2_000 + FLUSH_WAIT_TIME = 5.0 + + def __init__( + self, + capture_func, # type: Callable[[Envelope], None] + record_lost_func, # type: Callable[..., None] + ): + # type: (...) -> None + # Spans from different traces cannot be emitted in the same envelope + # since the envelope contains a shared trace header. That's why we bucket + # by trace_id, so that we can then send the buckets each in its own + # envelope. + # trace_id -> span buffer + self._span_buffer = defaultdict(list) # type: dict[str, list[Span]] + self._capture_func = capture_func + self._record_lost_func = record_lost_func + self._running = True + self._lock = threading.Lock() + + self._flush_event = threading.Event() # type: threading.Event + + self._flusher = None # type: Optional[threading.Thread] + self._flusher_pid = None # type: Optional[int] + + def _ensure_thread(self): + # type: (...) -> bool + """For forking processes we might need to restart this thread. + This ensures that our process actually has that thread running. + """ + if not self._running: + return False + + pid = os.getpid() + if self._flusher_pid == pid: + return True + + with self._lock: + # Recheck to make sure another thread didn't get here and start the + # the flusher in the meantime + if self._flusher_pid == pid: + return True + + self._flusher_pid = pid + + self._flusher = threading.Thread(target=self._flush_loop) + self._flusher.daemon = True + + try: + self._flusher.start() + except RuntimeError: + # Unfortunately at this point the interpreter is in a state that no + # longer allows us to spawn a thread and we have to bail. + self._running = False + return False + + return True + + def _flush_loop(self): + # type: (...) -> None + while self._running: + self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random()) + self._flush_event.clear() + self._flush() + + def get_size(self): + # type: () -> int + # caller is responsible for locking before checking this + return sum(len(buffer) for buffer in self._span_buffer.values()) + + def add(self, span): + # type: (Span) -> None + if not self._ensure_thread() or self._flusher is None: + return None + + with self._lock: + if self.get_size() >= self.MAX_SPANS_BEFORE_DROP: + self._record_lost_func( + reason="queue_overflow", + data_category="span", + quantity=1, + ) + return None + + self._span_buffer[span.trace_id].append(span) + if ( + self.get_size() >= self.MAX_SPANS_BEFORE_FLUSH + ): # TODO[span-first] should this be per bucket? + self._flush_event.set() + + def kill(self): + # type: (...) -> None + if self._flusher is None: + return + + self._running = False + self._flush_event.set() + self._flusher = None + + def flush(self): + # type: (...) -> None + self._flush() + + @staticmethod + def _span_to_transport_format(span): + # type: (Span) -> SpanV2 + res = { + "trace_id": span.trace_id, + "span_id": span.span_id, + "name": span.name, + "status": SPANSTATUS.OK + if span.status in (SPANSTATUS.OK, SPANSTATUS.UNSET) + else SPANSTATUS.ERROR, + "is_segment": span.containing_transaction == span, + "start_timestamp": span.start_timestamp, + "end_timestamp": span.timestamp, + } + + if span.parent_span_id: + res["parent_span_id"] = span.parent_span_id + + if span["attributes"]: + res["attributes"] = { + k: format_attribute_value(v) for (k, v) in span["attributes"].items() + } + + return res + + def _flush(self): + # type: (...) -> Optional[Envelope] + with self._lock: + if len(self._span_buffer) == 0: + return None + + for trace_id, spans in self._span_buffer: + envelope = Envelope( + headers={ + "sent_at": format_timestamp(datetime.now(timezone.utc)), + } + # TODO[span-first] more headers + ) + + envelope.add_item( + Item( + type="span", + content_type="application/vnd.sentry.items.span.v2+json", + headers={ + "item_count": len(spans), + }, + payload=PayloadRef( + json={ + "items": [ + self._span_to_transport_format(span) + for span in spans + ] + } + ), + ) + ) + + self._span_buffer.clear() + + self._capture_func(envelope) + return envelope From 0824d4220d47dd8da36977874482df5c8e47eb0c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 19 Nov 2025 16:56:27 +0100 Subject: [PATCH 04/13] small fixes --- sentry_sdk/_types.py | 2 +- sentry_sdk/scope.py | 4 ++-- sentry_sdk/tracing.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 5298663602..6659f3b444 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -1,6 +1,6 @@ -from consts import SPANSTATUS from typing import TYPE_CHECKING, TypeVar, Union +from sentry_sdk.consts import SPANSTATUS # Re-exported for compat, since code out there in the wild might use this variable. MYPY = TYPE_CHECKING diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 68b5174632..f4cfb74ce7 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -22,7 +22,7 @@ from sentry_sdk.session import Session from sentry_sdk.tracing_utils import ( Baggage, - has_streaming_enabled, + has_span_streaming_enabled, has_tracing_enabled, normalize_incoming_data, PropagationContext, @@ -1126,7 +1126,7 @@ def start_transaction( # we don't bother to keep spans if we already know we're not going to # send the transaction or if we're in streaming mode - if not has_streaming_enabled(client.options): + if not has_span_streaming_enabled(client.options): max_spans = (client.options["_experiments"].get("max_spans")) or 1000 transaction.init_span_recorder(maxlen=max_spans) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 6458fe7daa..597638c744 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -281,6 +281,7 @@ class Span: "name", "_flags", "_flags_capacity", + "_mode", ) def __init__( From 7066f6922c75c19f1d8eb22de8130e85c97468d9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 20 Nov 2025 11:24:16 +0100 Subject: [PATCH 05/13] safeguard against experiments not being there --- sentry_sdk/tracing_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 6e9d5bc1a2..96346db7c8 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -115,7 +115,7 @@ def has_span_streaming_enabled(options): if options is None: return False - return options.get("_experiments").get("trace_lifecycle") == "stream" + return (options.get("_experiments") or {}).get("trace_lifecycle") == "stream" @contextlib.contextmanager From 445bf76c6343965fd70efa245c1702a674b73141 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 24 Nov 2025 09:50:10 +0100 Subject: [PATCH 06/13] debug etc --- sentry_sdk/_span_batcher.py | 11 ++++---- sentry_sdk/tracing.py | 50 ++++++++++++++++++++++++------------- sentry_sdk/transport.py | 23 ++++++++++++----- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index 65ec1ada91..97eb0c3a07 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -112,6 +112,7 @@ def add(self, span): self.get_size() >= self.MAX_SPANS_BEFORE_FLUSH ): # TODO[span-first] should this be per bucket? self._flush_event.set() + print(self._span_buffer) def kill(self): # type: (...) -> None @@ -132,19 +133,19 @@ def _span_to_transport_format(span): res = { "trace_id": span.trace_id, "span_id": span.span_id, - "name": span.name, + "name": span.description, # TODO[span-first] "status": SPANSTATUS.OK if span.status in (SPANSTATUS.OK, SPANSTATUS.UNSET) else SPANSTATUS.ERROR, "is_segment": span.containing_transaction == span, - "start_timestamp": span.start_timestamp, - "end_timestamp": span.timestamp, + "start_timestamp": span.start_timestamp.timestamp(), # TODO[span-first] + "end_timestamp": span.timestamp.timestamp(), } if span.parent_span_id: res["parent_span_id"] = span.parent_span_id - if span["attributes"]: + if span.attributes: res["attributes"] = { k: format_attribute_value(v) for (k, v) in span["attributes"].items() } @@ -157,7 +158,7 @@ def _flush(self): if len(self._span_buffer) == 0: return None - for trace_id, spans in self._span_buffer: + for trace_id, spans in self._span_buffer.items(): envelope = Envelope( headers={ "sent_at": format_timestamp(datetime.now(timezone.utc)), diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 597638c744..354f94f576 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -282,6 +282,7 @@ class Span: "_flags", "_flags_capacity", "_mode", + "attributes", ) def __init__( @@ -300,6 +301,7 @@ def __init__( scope=None, # type: Optional[sentry_sdk.Scope] origin="manual", # type: str name=None, # type: Optional[str] + attributes=None, # type: Optional[dict] ): # type: (...) -> None self._trace_id = trace_id @@ -319,6 +321,8 @@ def __init__( self._containing_transaction = containing_transaction self._flags = {} # type: Dict[str, bool] self._flags_capacity = 10 + self.attributes = attributes or {} + # TODO[span-first]: fill attributes if hub is not None: warnings.warn( @@ -676,22 +680,7 @@ def is_success(self): # type: () -> bool return self.status == "ok" - def finish(self, scope=None, end_timestamp=None): - # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] - """ - Sets the end timestamp of the span. - - Additionally it also creates a breadcrumb from the span, - if the span represents a database or HTTP request. - - :param scope: The scope to use for this transaction. - If not provided, the current scope will be used. - :param end_timestamp: Optional timestamp that should - be used as timestamp instead of the current time. - - :return: Always ``None``. The type is ``Optional[str]`` to match - the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. - """ + def _finish(self, scope=None, end_timestamp=None): if self.timestamp is not None: # This span is already finished, ignore. return None @@ -712,12 +701,31 @@ def finish(self, scope=None, end_timestamp=None): scope = scope or sentry_sdk.get_current_scope() maybe_create_breadcrumbs_from_span(scope, self) + def finish(self, scope=None, end_timestamp=None): + # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str] + """ + Sets the end timestamp of the span. + + Additionally it also creates a breadcrumb from the span, + if the span represents a database or HTTP request. + + :param scope: The scope to use for this transaction. + If not provided, the current scope will be used. + :param end_timestamp: Optional timestamp that should + be used as timestamp instead of the current time. + + :return: Always ``None``. The type is ``Optional[str]`` to match + the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`. + """ + self._finish(scope, end_timestamp) + client = sentry_sdk.get_client() if client.is_active(): if ( has_span_streaming_enabled(client.options) and self.containing_transaction.sampled ): + logger.debug(f"[Tracing] Adding span {self.span_id} to buffer") client._span_batcher.add(self) return None @@ -1048,11 +1056,12 @@ def finish( ) self.name = "" - super().finish(scope, end_timestamp) + super()._finish(scope, end_timestamp) status_code = self._data.get(SPANDATA.HTTP_STATUS_CODE) if ( - status_code is not None + self._mode == "static" + and status_code is not None and status_code in client.options["trace_ignore_status_codes"] ): logger.debug( @@ -1084,6 +1093,11 @@ def finish( return None + if self._mode == "stream" and self.containing_transaction.sampled: + logger.debug(f"[Tracing] Adding span {self.span_id} to buffer") + client._span_batcher.add(self) + return + finished_spans = [ span.to_json() for span in self._span_recorder.spans diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 645bfead19..fb64826ab7 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -471,12 +471,23 @@ def _send_envelope(self, envelope): if content_encoding: headers["Content-Encoding"] = content_encoding - self._send_request( - body.getvalue(), - headers=headers, - endpoint_type=EndpointType.ENVELOPE, - envelope=envelope, - ) + print("ENVELOPE") + print(envelope.headers) + for i, item in enumerate(envelope.items): + print("Item", i, item.type) + print(item.headers) + print(item.payload.json) + print() + + print("-----------------------------------") + print() + + # self._send_request( + # body.getvalue(), + # headers=headers, + # endpoint_type=EndpointType.ENVELOPE, + # envelope=envelope, + # ) return None def _serialize_envelope(self, envelope): From ef83cb388608e3be2170a5777b7ca3569664683d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 25 Nov 2025 13:31:56 +0100 Subject: [PATCH 07/13] add some attrs --- sentry_sdk/_span_batcher.py | 11 +++-- sentry_sdk/_types.py | 28 +++++------ sentry_sdk/client.py | 3 ++ sentry_sdk/tracing.py | 34 +++++++++++-- sentry_sdk/transport.py | 6 +++ sentry_sdk/utils.py | 97 ++++++++++++++++++++++++++++++------- 6 files changed, 138 insertions(+), 41 deletions(-) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index 97eb0c3a07..9fbceabf0c 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -10,7 +10,6 @@ from typing import Optional, List, Callable, TYPE_CHECKING, Any from sentry_sdk.consts import SPANSTATUS -from sentry_sdk.utils import format_attribute_value, format_timestamp, safe_repr from sentry_sdk.envelope import Envelope, Item, PayloadRef from sentry_sdk.tracing import Transaction @@ -112,7 +111,6 @@ def add(self, span): self.get_size() >= self.MAX_SPANS_BEFORE_FLUSH ): # TODO[span-first] should this be per bucket? self._flush_event.set() - print(self._span_buffer) def kill(self): # type: (...) -> None @@ -130,6 +128,8 @@ def flush(self): @staticmethod def _span_to_transport_format(span): # type: (Span) -> SpanV2 + from sentry_sdk.utils import attribute_value_to_transport_format, safe_repr + res = { "trace_id": span.trace_id, "span_id": span.span_id, @@ -145,15 +145,18 @@ def _span_to_transport_format(span): if span.parent_span_id: res["parent_span_id"] = span.parent_span_id - if span.attributes: + if span._attributes: res["attributes"] = { - k: format_attribute_value(v) for (k, v) in span["attributes"].items() + k: attribute_value_to_transport_format(v) + for (k, v) in span._attributes.items() } return res def _flush(self): # type: (...) -> Optional[Envelope] + from sentry_sdk.utils import format_timestamp + with self._lock: if len(self._span_buffer) == 0: return None diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 6659f3b444..69a3fe8ce9 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -223,14 +223,16 @@ class SDKInfo(TypedDict): # TODO: Make a proper type definition for this (PRs welcome!) Hint = Dict[str, Any] - # TODO: Consolidate attribute types. E.g. metrics use the same attr structure. - # I'm keeping span attrs separate for now so that the span first stuff is - # isolated until it's not experimental. - SpanAttributeValue = TypedDict( - "SpanAttributeValue", + AttributeValue = ( + str | bool | float | int | list[str] | list[bool] | list[float] | list[int] + ) + Attributes = dict[str, AttributeValue] + + SerializedAttributeValue = TypedDict( + "SerializedAttributeValue", { "type": Literal["string", "boolean", "double", "integer"], - "value": str | bool | float | int, + "value": AttributeValue, }, ) @@ -240,7 +242,7 @@ class SDKInfo(TypedDict): "severity_text": str, "severity_number": int, "body": str, - "attributes": dict[str, str | bool | float | int], + "attributes": Attributes, "time_unix_nano": int, "trace_id": Optional[str], }, @@ -248,14 +250,6 @@ class SDKInfo(TypedDict): MetricType = Literal["counter", "gauge", "distribution"] - MetricAttributeValue = TypedDict( - "MetricAttributeValue", - { - "value": Union[str, bool, float, int], - "type": Literal["string", "boolean", "double", "integer"], - }, - ) - Metric = TypedDict( "Metric", { @@ -266,7 +260,7 @@ class SDKInfo(TypedDict): "type": MetricType, "value": float, "unit": Optional[str], - "attributes": dict[str, str | bool | float | int], + "attributes": Attributes, }, ) @@ -285,7 +279,7 @@ class SDKInfo(TypedDict): "is_segment": bool, "start_timestamp": float, "end_timestamp": float, - "attributes": Optional[dict[str, SpanAttributeValue]], + "attributes": Attributes, }, ) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 4475d432f2..d249baff70 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -12,6 +12,7 @@ from sentry_sdk._compat import PY37, check_uwsgi_thread_support from sentry_sdk._metrics_batcher import MetricsBatcher from sentry_sdk._span_batcher import SpanBatcher +from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( AnnotatedValue, ContextVar, @@ -938,6 +939,7 @@ def capture_event( def _capture_log(self, log): # type: (Optional[Log]) -> None + # TODO[ivana]: Use get_default_attributes here if not has_logs_enabled(self.options) or log is None: return @@ -1006,6 +1008,7 @@ def _capture_log(self, log): def _capture_metric(self, metric): # type: (Optional[Metric]) -> None + # TODO[ivana]: Use get_default_attributes here if not has_metrics_enabled(self.options) or metric is None: return diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 354f94f576..de731771c3 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -13,6 +13,8 @@ logger, nanosecond_time, should_be_treated_as_error, + serialize_attribute, + get_default_attributes, ) from typing import TYPE_CHECKING @@ -44,6 +46,8 @@ MeasurementUnit, SamplingContext, MeasurementValue, + Attributes, + AttributeValue, ) class SpanKwargs(TypedDict, total=False): @@ -282,7 +286,7 @@ class Span: "_flags", "_flags_capacity", "_mode", - "attributes", + "_attributes", ) def __init__( @@ -321,8 +325,7 @@ def __init__( self._containing_transaction = containing_transaction self._flags = {} # type: Dict[str, bool] self._flags_capacity = 10 - self.attributes = attributes or {} - # TODO[span-first]: fill attributes + self._attributes = attributes or {} # type: Attributes if hub is not None: warnings.warn( @@ -352,6 +355,16 @@ def __init__( self.update_active_thread() self.set_profiler_id(get_profiler_id()) + self._set_initial_attributes() + + def _set_initial_attributes(self): + attributes = get_default_attributes() + self._attributes = attributes | self._attributes + + self._attributes["sentry.segment.id"] = self.containing_transaction.span_id + if hasattr(self.containing_transaction, "name"): + # TODO[span-first]: fix this properly + self._attributes["sentry.segment.name"] = self.containing_transaction.name # TODO this should really live on the Transaction class rather than the Span # class @@ -631,6 +644,21 @@ def update_data(self, data): # type: (Dict[str, Any]) -> None self._data.update(data) + def get_attributes(self): + # type: () -> Attributes + return self._attributes + + def set_attribute(self, attribute, value): + # type: (str, AttributeValue) -> None + self._attributes[attribute] = serialize_attribute(value) + + def remove_attribute(self, attribute): + # type: (str) -> None + try: + del self._attributes[attribute] + except KeyError: + pass + def set_flag(self, flag, result): # type: (str, bool) -> None if len(self._flags) < self._flags_capacity: diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index fb64826ab7..5823ea2dac 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -475,7 +475,13 @@ def _send_envelope(self, envelope): print(envelope.headers) for i, item in enumerate(envelope.items): print("Item", i, item.type) + print("Headers") print(item.headers) + print("Attributes") + for i in item.payload.json["items"]: + for attribute, value in i["attributes"].items(): + print(attribute, value) + print("Payload") print(item.payload.json) print() diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 90db494426..8f4d069a5a 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -30,6 +30,7 @@ DEFAULT_ADD_FULL_STACK, DEFAULT_MAX_STACK_FRAMES, DEFAULT_MAX_VALUE_LENGTH, + SPANDATA, EndpointType, ) from sentry_sdk._types import Annotated, AnnotatedValue, SENSITIVE_DATA_SUBSTITUTE @@ -1726,6 +1727,85 @@ def is_sentry_url(client, url): ) +def serialize_attribute(value): + # type: (Any) -> AttributeValue + # check for allowed primitives + if isinstance(value, (int, str, float, bool)): + return value + + # lists are allowed too, as long as they don't mix types + if isinstance(value, (list, tuple)): + for type_ in (int, str, float, bool): + if all(isinstance(item, type_) for item in value): + return list(value) + + return safe_repr(value) + + +def attribute_value_to_transport_format(value): + # type: (Any) -> dict[str, bool | str | int | float] + if isinstance(value, bool): + return {"value": value, "type": "boolean"} + + if isinstance(value, int): + return {"value": value, "type": "integer"} + + if isinstance(value, float): + return {"value": value, "type": "double"} + + if isinstance(value, str): + return {"value": value, "type": "string"} + + return {"value": safe_repr(value), "type": "string"} + + +def get_default_attributes(): + # type: () -> Attributes + # TODO[ivana]: standardize attr names into an enum/take from sentry convs + from sentry_sdk.client import SDK_INFO + from sentry_sdk.scope import should_send_default_pii + + attributes = {} + + attributes["sentry.sdk.name"] = SDK_INFO["name"] + attributes["sentry.sdk.version"] = SDK_INFO["version"] + + options = sentry_sdk.get_client().options + + server_name = options.get("server_name") + if server_name is not None: + attributes[SPANDATA.SERVER_ADDRESS] = server_name + + environment = options.get("environment") + if environment is not None: + attributes["sentry.environment"] = environment + + release = options.get("release") + if release is not None: + attributes["sentry.release"] = release + + thread_id, thread_name = get_current_thread_meta() + if thread_id is not None: + attributes["thread.id"] = thread_id + if thread_name is not None: + attributes["thread.name"] = thread_name + + # The user, if present, is always set on the isolation scope. + # TODO[ivana]: gate behing PII? + if should_send_default_pii(): + isolation_scope = sentry_sdk.get_isolation_scope() + if isolation_scope._user is not None: + for attribute, user_attribute in ( + ("user.id", "id"), + ("user.name", "username"), + ("user.email", "email"), + ): + if attribute in isolation_scope._user: + attributes[attribute] = isolation_scope._user[user_attribute] + + return attributes + + def _generate_installed_modules(): # type: () -> Iterator[Tuple[str, str]] try: @@ -2076,20 +2156,3 @@ def get_before_send_metric(options): return options.get("before_send_metric") or options["_experiments"].get( "before_send_metric" ) - - -def format_attribute_value(value): - # type: (Any) -> dict[str, bool | str | int | float] - if isinstance(value, bool): - return {"value": value, "type": "boolean"} - - if isinstance(value, int): - return {"value": value, "type": "integer"} - - if isinstance(value, float): - return {"value": value, "type": "double"} - - if isinstance(value, str): - return {"value": value, "type": "string"} - - return {"value": safe_repr(value), "type": "string"} From 6c6fd25d8e1da7b7ae00d3c2fdb32944a67cf22e Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 25 Nov 2025 13:43:25 +0100 Subject: [PATCH 08/13] shuffle around --- sentry_sdk/tracing.py | 8 ++++---- sentry_sdk/utils.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index de731771c3..7eb3d5a562 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -362,9 +362,8 @@ def _set_initial_attributes(self): self._attributes = attributes | self._attributes self._attributes["sentry.segment.id"] = self.containing_transaction.span_id - if hasattr(self.containing_transaction, "name"): - # TODO[span-first]: fix this properly - self._attributes["sentry.segment.name"] = self.containing_transaction.name + # TODO[span-first]: This might need to be updated if the segment name is updated + self._attributes["sentry.segment.name"] = self.containing_transaction.name # TODO this should really live on the Transaction class rather than the Span # class @@ -882,9 +881,10 @@ def __init__( # type: ignore[misc] **kwargs, # type: Unpack[SpanKwargs] ): # type: (...) -> None + self.name = name + super().__init__(**kwargs) - self.name = name self.source = source self.sample_rate = None # type: Optional[float] self.parent_sampled = parent_sampled diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 8f4d069a5a..cc273c02d8 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1790,9 +1790,8 @@ def get_default_attributes(): if thread_name is not None: attributes["thread.name"] = thread_name - # The user, if present, is always set on the isolation scope. - # TODO[ivana]: gate behing PII? if should_send_default_pii(): + # The user, if present, is always set on the isolation scope. isolation_scope = sentry_sdk.get_isolation_scope() if isolation_scope._user is not None: for attribute, user_attribute in ( From bf3f3d138d36c4949a2e1842990b13230f0cb8bd Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 25 Nov 2025 15:48:46 +0100 Subject: [PATCH 09/13] go via client --- sentry_sdk/_span_batcher.py | 17 +++++++++++------ sentry_sdk/client.py | 8 ++++++++ sentry_sdk/tracing.py | 14 +++++--------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index 9fbceabf0c..94e42792a9 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -19,8 +19,8 @@ class SpanBatcher: - # TODO[span-first]: Adjust limits. However, there's still a restriction of - # at most 1000 spans per envelope. + # TODO[span-first]: Adjust limits. Protocol dictates at most 1000 spans + # in an envelope. MAX_SPANS_BEFORE_FLUSH = 1_000 MAX_SPANS_BEFORE_DROP = 2_000 FLUSH_WAIT_TIME = 5.0 @@ -107,9 +107,7 @@ def add(self, span): return None self._span_buffer[span.trace_id].append(span) - if ( - self.get_size() >= self.MAX_SPANS_BEFORE_FLUSH - ): # TODO[span-first] should this be per bucket? + if self.get_size() >= self.MAX_SPANS_BEFORE_FLUSH: self._flush_event.set() def kill(self): @@ -130,10 +128,12 @@ def _span_to_transport_format(span): # type: (Span) -> SpanV2 from sentry_sdk.utils import attribute_value_to_transport_format, safe_repr + is_segment = isinstance(span, Transaction) + res = { "trace_id": span.trace_id, "span_id": span.span_id, - "name": span.description, # TODO[span-first] + "name": span.name if is_segment else span.description, "status": SPANSTATUS.OK if span.status in (SPANSTATUS.OK, SPANSTATUS.UNSET) else SPANSTATUS.ERROR, @@ -151,6 +151,11 @@ def _span_to_transport_format(span): for (k, v) in span._attributes.items() } + # We set these as late as possible to increase the odds of the span + # getting a good segment name + res["attributes"]["sentry.segment.id"] = span.containing_transaction.span_id + res["attributes"]["sentry.segment.name"] = span.containing_transaction.name + return res def _flush(self): diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index d249baff70..849000d218 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -937,6 +937,14 @@ def capture_event( return return_value + def _capture_span(self, span): + # type: (Span) -> None + # Used for span streaming (trace_lifecycle == "stream"). + if not has_span_streaming_enabled(self.options): + return + + self._span_batcher.add(span) + def _capture_log(self, log): # type: (Optional[Log]) -> None # TODO[ivana]: Use get_default_attributes here diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 7eb3d5a562..0dde6b9927 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -361,10 +361,6 @@ def _set_initial_attributes(self): attributes = get_default_attributes() self._attributes = attributes | self._attributes - self._attributes["sentry.segment.id"] = self.containing_transaction.span_id - # TODO[span-first]: This might need to be updated if the segment name is updated - self._attributes["sentry.segment.name"] = self.containing_transaction.name - # TODO this should really live on the Transaction class rather than the Span # class def init_span_recorder(self, maxlen): @@ -753,7 +749,7 @@ def finish(self, scope=None, end_timestamp=None): and self.containing_transaction.sampled ): logger.debug(f"[Tracing] Adding span {self.span_id} to buffer") - client._span_batcher.add(self) + client._capture_span(self) return None @@ -1121,9 +1117,10 @@ def finish( return None - if self._mode == "stream" and self.containing_transaction.sampled: - logger.debug(f"[Tracing] Adding span {self.span_id} to buffer") - client._span_batcher.add(self) + if self._mode == "stream": + if self.containing_transaction.sampled: + logger.debug(f"[Tracing] Adding span {self.span_id} to batcher") + client._capture_span(self) return finished_spans = [ @@ -1167,7 +1164,6 @@ def finish( self._profile = None event["measurements"] = self._measurements - return scope.capture_event(event) def set_measurement(self, name, value, unit=""): From 2fa3338195731f646d96eef6fa80e0ace5099cff Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 25 Nov 2025 15:55:14 +0100 Subject: [PATCH 10/13] set attrs later --- sentry_sdk/_span_batcher.py | 5 ----- sentry_sdk/client.py | 7 +++++++ sentry_sdk/tracing.py | 6 ------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index 94e42792a9..a6dd3168d0 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -151,11 +151,6 @@ def _span_to_transport_format(span): for (k, v) in span._attributes.items() } - # We set these as late as possible to increase the odds of the span - # getting a good segment name - res["attributes"]["sentry.segment.id"] = span.containing_transaction.span_id - res["attributes"]["sentry.segment.name"] = span.containing_transaction.name - return res def _flush(self): diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 849000d218..e26b1866ab 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -28,6 +28,7 @@ logger, get_before_send_log, get_before_send_metric, + get_default_attributes, has_logs_enabled, has_metrics_enabled, ) @@ -943,6 +944,12 @@ def _capture_span(self, span): if not has_span_streaming_enabled(self.options): return + attributes = get_default_attributes() + span._attributes = attributes | span._attributes + + span._attributes["sentry.segment.id"] = span.containing_transaction.span_id + span._attributes["sentry.segment.name"] = span.containing_transaction.name + self._span_batcher.add(span) def _capture_log(self, log): diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 0dde6b9927..65bf7b8f26 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -14,7 +14,6 @@ nanosecond_time, should_be_treated_as_error, serialize_attribute, - get_default_attributes, ) from typing import TYPE_CHECKING @@ -355,11 +354,6 @@ def __init__( self.update_active_thread() self.set_profiler_id(get_profiler_id()) - self._set_initial_attributes() - - def _set_initial_attributes(self): - attributes = get_default_attributes() - self._attributes = attributes | self._attributes # TODO this should really live on the Transaction class rather than the Span # class From edaf05d996ff70368f6bf0630fe1a2eb853a5d06 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 25 Nov 2025 16:58:41 +0100 Subject: [PATCH 11/13] dsc --- sentry_sdk/_span_batcher.py | 47 ++++++++++++++++++++----------------- sentry_sdk/client.py | 11 ++++++--- sentry_sdk/tracing.py | 2 -- sentry_sdk/transport.py | 7 +++--- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index a6dd3168d0..a402ba5979 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -162,30 +162,35 @@ def _flush(self): return None for trace_id, spans in self._span_buffer.items(): - envelope = Envelope( - headers={ - "sent_at": format_timestamp(datetime.now(timezone.utc)), - } - # TODO[span-first] more headers - ) + if spans: + trace_context = spans[0].get_trace_context() + dsc = trace_context.get("dynamic_sampling_context") + # XXX[span-first]: empty dsc? - envelope.add_item( - Item( - type="span", - content_type="application/vnd.sentry.items.span.v2+json", + envelope = Envelope( headers={ - "item_count": len(spans), - }, - payload=PayloadRef( - json={ - "items": [ - self._span_to_transport_format(span) - for span in spans - ] - } - ), + "sent_at": format_timestamp(datetime.now(timezone.utc)), + "trace": dsc, + } + ) + + envelope.add_item( + Item( + type="span", + content_type="application/vnd.sentry.items.span.v2+json", + headers={ + "item_count": len(spans), + }, + payload=PayloadRef( + json={ + "items": [ + self._span_to_transport_format(span) + for span in spans + ] + } + ), + ) ) - ) self._span_buffer.clear() diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e26b1866ab..9d55be6656 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -947,10 +947,15 @@ def _capture_span(self, span): attributes = get_default_attributes() span._attributes = attributes | span._attributes - span._attributes["sentry.segment.id"] = span.containing_transaction.span_id - span._attributes["sentry.segment.name"] = span.containing_transaction.name + segment = span.containing_transaction + span._attributes["sentry.segment.id"] = segment.span_id + span._attributes["sentry.segment.name"] = segment.name - self._span_batcher.add(span) + if self._span_batcher: + logger.debug( + f"[Tracing] Adding span {span.span_id} of segment {segment.span_id} to batcher" + ) + self._span_batcher.add(span) def _capture_log(self, log): # type: (Optional[Log]) -> None diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 65bf7b8f26..339e7e1e19 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -742,7 +742,6 @@ def finish(self, scope=None, end_timestamp=None): has_span_streaming_enabled(client.options) and self.containing_transaction.sampled ): - logger.debug(f"[Tracing] Adding span {self.span_id} to buffer") client._capture_span(self) return None @@ -1113,7 +1112,6 @@ def finish( if self._mode == "stream": if self.containing_transaction.sampled: - logger.debug(f"[Tracing] Adding span {self.span_id} to batcher") client._capture_span(self) return diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 5823ea2dac..3fe27b4dc5 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -478,9 +478,10 @@ def _send_envelope(self, envelope): print("Headers") print(item.headers) print("Attributes") - for i in item.payload.json["items"]: - for attribute, value in i["attributes"].items(): - print(attribute, value) + if item.payload.json.get("items"): + for i in item.payload.json["items"]: + for attribute, value in i["attributes"].items(): + print(attribute, value) print("Payload") print(item.payload.json) print() From e618c0b11e679e48a04f18f86b137075137c4679 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 26 Nov 2025 08:51:57 +0100 Subject: [PATCH 12/13] is_segment, remove prints --- sentry_sdk/_span_batcher.py | 4 ++-- sentry_sdk/transport.py | 30 ++++++------------------------ 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index a402ba5979..7430314760 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -128,7 +128,7 @@ def _span_to_transport_format(span): # type: (Span) -> SpanV2 from sentry_sdk.utils import attribute_value_to_transport_format, safe_repr - is_segment = isinstance(span, Transaction) + is_segment = span.containing_transaction == span res = { "trace_id": span.trace_id, @@ -137,7 +137,7 @@ def _span_to_transport_format(span): "status": SPANSTATUS.OK if span.status in (SPANSTATUS.OK, SPANSTATUS.UNSET) else SPANSTATUS.ERROR, - "is_segment": span.containing_transaction == span, + "is_segment": is_segment, "start_timestamp": span.start_timestamp.timestamp(), # TODO[span-first] "end_timestamp": span.timestamp.timestamp(), } diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 3fe27b4dc5..645bfead19 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -471,30 +471,12 @@ def _send_envelope(self, envelope): if content_encoding: headers["Content-Encoding"] = content_encoding - print("ENVELOPE") - print(envelope.headers) - for i, item in enumerate(envelope.items): - print("Item", i, item.type) - print("Headers") - print(item.headers) - print("Attributes") - if item.payload.json.get("items"): - for i in item.payload.json["items"]: - for attribute, value in i["attributes"].items(): - print(attribute, value) - print("Payload") - print(item.payload.json) - print() - - print("-----------------------------------") - print() - - # self._send_request( - # body.getvalue(), - # headers=headers, - # endpoint_type=EndpointType.ENVELOPE, - # envelope=envelope, - # ) + self._send_request( + body.getvalue(), + headers=headers, + endpoint_type=EndpointType.ENVELOPE, + envelope=envelope, + ) return None def _serialize_envelope(self, envelope): From 2651342e90ebb56d954007e3d65401dac961e6ab Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 27 Nov 2025 13:53:42 +0100 Subject: [PATCH 13/13] adapt to spanstatus change, add ignorespans signature --- sentry_sdk/_span_batcher.py | 4 ++-- sentry_sdk/consts.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/_span_batcher.py b/sentry_sdk/_span_batcher.py index 7430314760..e99d495f50 100644 --- a/sentry_sdk/_span_batcher.py +++ b/sentry_sdk/_span_batcher.py @@ -135,8 +135,8 @@ def _span_to_transport_format(span): "span_id": span.span_id, "name": span.name if is_segment else span.description, "status": SPANSTATUS.OK - if span.status in (SPANSTATUS.OK, SPANSTATUS.UNSET) - else SPANSTATUS.ERROR, + if span.status == SPANSTATUS.OK + else SPANSTATUS.INTERNAL_ERROR, "is_segment": is_segment, "start_timestamp": span.start_timestamp.timestamp(), # TODO[span-first] "end_timestamp": span.timestamp.timestamp(), diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index c70d13f52d..e11832c63c 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -42,10 +42,12 @@ class CompressionAlgo(Enum): from typing import Sequence from typing import Tuple from typing import AbstractSet + from typing import Pattern from typing_extensions import Literal from typing_extensions import TypedDict from sentry_sdk._types import ( + Attributes, BreadcrumbProcessor, ContinuousProfilerMode, Event, @@ -83,6 +85,12 @@ class CompressionAlgo(Enum): "enable_metrics": Optional[bool], "before_send_metric": Optional[Callable[[Metric, Hint], Optional[Metric]]], "trace_lifecycle": Optional[TraceLifecycleMode], + "ignore_spans": Optional[ + list[ + Union[str, Pattern], + dict[Union[Literal["name", "attributes"], Union[str, Attributes]],], + ] + ], }, total=False, )