Skip to content
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
946decc
ref: Remove flag storage from StreamedSpan
sentrivana Mar 5, 2026
f3ee55c
ref: Tweak StreamedSpan interface
sentrivana Mar 5, 2026
47ed910
Add missing logger
sentrivana Mar 5, 2026
5023c76
fixes
sentrivana Mar 5, 2026
6445447
ref: Add active to StreamedSpan
sentrivana Mar 5, 2026
47e6211
Add property
sentrivana Mar 5, 2026
1e7b694
ref: Add no-op streaming span class
sentrivana Mar 5, 2026
80bfe5a
Remove redundant stuff
sentrivana Mar 5, 2026
1f0ffc1
Merge branch 'master' into ivana/span-first-4-add-noop-span
sentrivana Mar 5, 2026
d773428
ref: Add experimental streaming API
sentrivana Mar 5, 2026
647fa79
reformat
sentrivana Mar 5, 2026
49bdbe6
Add a __repr__
sentrivana Mar 5, 2026
cdd8bd6
Merge branch 'master' into ivana/span-first-5-add-start-span-api
sentrivana Mar 5, 2026
54f81af
ref: Add new_trace, continue_trace to span first
sentrivana Mar 5, 2026
941863e
ref: Add streaming trace decorator
sentrivana Mar 5, 2026
4b14e8d
Remove redundant code
sentrivana Mar 5, 2026
474f8e6
simplify
sentrivana Mar 5, 2026
9996e29
Merge branch 'ivana/span-first-5-add-start-span-api' into ivana/span-…
sentrivana Mar 5, 2026
e20d4fd
Merge branch 'ivana/span-first-6-add-continue-and-new-trace' into iva…
sentrivana Mar 5, 2026
f2738ff
reorder imports
sentrivana Mar 5, 2026
7874a54
ref: Per-bucket limits, fix envelope chunking
sentrivana Mar 5, 2026
63a9396
.
sentrivana Mar 5, 2026
c974d3e
add dummy __enter__, __exit__
sentrivana Mar 5, 2026
5d8c238
Merge branch 'ivana/span-first-7-add-trace-decorator' into ivana/span…
sentrivana Mar 5, 2026
831adae
type hint
sentrivana Mar 5, 2026
656ef2e
Merge branch 'ivana/span-first-7-add-trace-decorator' into ivana/span…
sentrivana Mar 5, 2026
1dcf176
remove unused import
sentrivana Mar 5, 2026
0a7eae8
ref: Allow to start and finish StreamedSpans
sentrivana Mar 5, 2026
6888c56
Add end, finish to noop spans
sentrivana Mar 6, 2026
09e5cce
fixes
sentrivana Mar 6, 2026
ae2fd52
.
sentrivana Mar 6, 2026
f223574
Correctly detect user-set parent_span=None
sentrivana Mar 6, 2026
05a4157
Merge branch 'master' into ivana/span-first-5-add-start-span-api
sentrivana Mar 6, 2026
9e8e60e
mypy
sentrivana Mar 6, 2026
777a246
Merge branch 'ivana/span-first-5-add-start-span-api' into ivana/span-…
sentrivana Mar 6, 2026
9b1e2f3
Merge branch 'ivana/span-first-6-add-continue-and-new-trace' into iva…
sentrivana Mar 6, 2026
e589c53
Merge branch 'ivana/span-first-7-add-trace-decorator' into ivana/span…
sentrivana Mar 6, 2026
1487ea8
Merge branch 'ivana/span-first-8-bucket-based-limits-in-batcher' into…
sentrivana Mar 6, 2026
1006e7b
remove unused imports
sentrivana Mar 6, 2026
6c16dbf
Merge branch 'ivana/span-first-7-add-trace-decorator' into ivana/span…
sentrivana Mar 6, 2026
cb37a07
Merge branch 'ivana/span-first-8-bucket-based-limits-in-batcher' into…
sentrivana Mar 6, 2026
ad6e7cc
move where finished is set
sentrivana Mar 6, 2026
ba29f0c
remove finished
sentrivana Mar 6, 2026
d6a42b2
end_timestamp improvements
sentrivana Mar 6, 2026
5e20ad3
.
sentrivana Mar 6, 2026
c70fae4
fix
sentrivana Mar 6, 2026
b995770
simplify
sentrivana Mar 6, 2026
0235053
Merge branch 'master' into ivana/span-first-9-start-end
sentrivana Mar 9, 2026
60217e1
ref: Add warnings to span streaming APIs
sentrivana Mar 9, 2026
b673a09
Merge branch 'master' into ivana/span-first-9-start-end
sentrivana Mar 9, 2026
d6fa965
.
sentrivana Mar 9, 2026
3602f86
.
sentrivana Mar 9, 2026
9f59eb0
fix
sentrivana Mar 9, 2026
bd8e1c9
Merge branch 'ivana/span-first-9-start-end' into ivana/span-first-10-…
sentrivana Mar 9, 2026
9b3df81
Merge branch 'master' into ivana/span-first-10-random-improvements
sentrivana Mar 9, 2026
8614d52
Merge branch 'master' into ivana/span-first-9-start-end
sentrivana Mar 9, 2026
cdee8bc
Merge branch 'ivana/span-first-9-start-end' into ivana/span-first-10-…
sentrivana Mar 9, 2026
72f0968
move
sentrivana Mar 9, 2026
dab1970
add a guard
sentrivana Mar 9, 2026
7daa720
.
sentrivana Mar 9, 2026
b59f3cd
move warnings
sentrivana Mar 9, 2026
2f0dc01
.
sentrivana Mar 9, 2026
dc81637
Merge branch 'ivana/span-first-9-start-end' into ivana/span-first-10-…
sentrivana Mar 9, 2026
bc9f765
Merge branch 'master' into ivana/span-first-9-start-end
sentrivana Mar 9, 2026
45372c1
Merge branch 'ivana/span-first-9-start-end' into ivana/span-first-10-…
sentrivana Mar 9, 2026
09b88f0
dont redefine slots
sentrivana Mar 9, 2026
a9b33a9
Merge branch 'ivana/span-first-9-start-end' into ivana/span-first-10-…
sentrivana Mar 9, 2026
8a31eeb
Merge branch 'master' into ivana/span-first-10-random-improvements
sentrivana Mar 10, 2026
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
4 changes: 4 additions & 0 deletions sentry_sdk/_span_batcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,12 @@ def _to_transport_format(item: "StreamedSpan") -> "Any":
"span_id": item.span_id,
"name": item._name,
"status": item._status,
"start_timestamp": item._start_timestamp.timestamp(),
}

if item._timestamp:
res["end_timestamp"] = item._timestamp.timestamp()

if item._parent_span_id:
res["parent_span_id"] = item._parent_span_id

Expand Down
9 changes: 9 additions & 0 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,15 @@ def start_streamed_span(
active: bool,
) -> "StreamedSpan":
# TODO: rename to start_span once we drop the old API
if not has_span_streaming_enabled(self.client.options):
warnings.warn(
"Using span streaming API in non-span-streaming mode. Use "
"sentry_sdk.start_transaction() and sentry_sdk.start_span() "
"instead.",
stacklevel=2,
)
return NoOpStreamedSpan()

if isinstance(parent_span, NoOpStreamedSpan):
# parent_span is only set if the user explicitly set it
logger.debug(
Expand Down
176 changes: 170 additions & 6 deletions sentry_sdk/traces.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
"""
EXPERIMENTAL. Do not use in production.

The API in this file is only meant to be used in span streaming mode.

You can enable span streaming mode via
sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}).
"""

import uuid
import warnings
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import TYPE_CHECKING

import sentry_sdk
from sentry_sdk.tracing_utils import Baggage
from sentry_sdk.utils import format_attribute, logger
from sentry_sdk.utils import (
capture_internal_exceptions,
format_attribute,
logger,
nanosecond_time,
should_be_treated_as_error,
)

if TYPE_CHECKING:
from typing import Any, Callable, Optional, ParamSpec, TypeVar, Union
Expand Down Expand Up @@ -76,6 +86,9 @@ def start_span(
"""
Start a span.

EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span()
instead.

The span's parent, unless provided explicitly via the `parent_span` argument,
will be the current active span, if any. If there is none, this span will
become the root of a new span tree. If you explicitly want this span to be
Expand Down Expand Up @@ -136,6 +149,8 @@ def continue_trace(incoming: "dict[str, Any]") -> None:
"""
Continue a trace from headers or environment variables.

EXPERIMENTAL. Use sentry_sdk.continue_trace() instead.

This function sets the propagation context on the scope. Any span started
in the updated scope will belong under the trace extracted from the
provided propagation headers or environment variables.
Expand All @@ -160,6 +175,8 @@ def new_trace() -> None:
"""
Resets the propagation context, forcing a new trace.

EXPERIMENTAL.

This function sets the propagation context on the scope. Any span started
in the updated scope will start its own trace.

Expand Down Expand Up @@ -189,8 +206,12 @@ class StreamedSpan:
"_parent_span_id",
"_segment",
"_parent_sampled",
"_start_timestamp",
"_start_timestamp_monotonic_ns",
"_timestamp",
"_status",
"_scope",
"_previous_span_on_scope",
"_baggage",
)

Expand Down Expand Up @@ -223,11 +244,23 @@ def __init__(
self._parent_sampled = parent_sampled
self._baggage = baggage

self._start_timestamp = datetime.now(timezone.utc)
self._timestamp: "Optional[datetime]" = None

try:
# profiling depends on this value and requires that
# it is measured in nanoseconds
self._start_timestamp_monotonic_ns = nanosecond_time()
except AttributeError:
pass

self._span_id: "Optional[str]" = None

self._status = SpanStatus.OK.value
self.set_attribute("sentry.span.source", SegmentSource.CUSTOM.value)

self._start()

def __repr__(self) -> str:
return (
f"<{self.__class__.__name__}("
Expand All @@ -244,7 +277,79 @@ def __enter__(self) -> "StreamedSpan":
def __exit__(
self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
) -> None:
pass
if value is not None and should_be_treated_as_error(ty, value):
self.status = SpanStatus.ERROR

self._end()

def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
"""
Finish this span and queue it for sending.

:param end_timestamp: End timestamp to use instead of current time.
:type end_timestamp: "Optional[Union[float, datetime]]"
"""
self._end(end_timestamp)

def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
warnings.warn(
"span.finish() is deprecated. Use span.end() instead.",
stacklevel=2,
category=DeprecationWarning,
)

self.end(end_timestamp)

def _start(self) -> None:
if self._active:
old_span = self._scope.span
self._scope.span = self # type: ignore
self._previous_span_on_scope = old_span

def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
if self._timestamp is not None:
# This span is already finished, ignore.
return

# Detach from scope
if self._active:
with capture_internal_exceptions():
old_span = self._previous_span_on_scope
del self._previous_span_on_scope
self._scope.span = old_span

# Set attributes from the segment
self.set_attribute("sentry.segment.id", self._segment.span_id)
self.set_attribute("sentry.segment.name", self._segment.name)

# Set the end timestamp
if end_timestamp is not None:
if isinstance(end_timestamp, (float, int)):
try:
end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc)
except Exception:
pass

if isinstance(end_timestamp, datetime):
self._timestamp = end_timestamp
else:
logger.debug("Failed to set end_timestamp. Using current time instead.")

if self._timestamp is None:
try:
elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns
self._timestamp = self._start_timestamp + timedelta(
microseconds=elapsed / 1000
)
except AttributeError:
self._timestamp = datetime.now(timezone.utc)

client = sentry_sdk.get_client()
if not client.is_active():
return

# Finally, queue the span for sending to Sentry
self._scope._capture_span(self)

def get_attributes(self) -> "Attributes":
return self._attributes
Expand Down Expand Up @@ -309,10 +414,28 @@ def trace_id(self) -> str:
def sampled(self) -> "Optional[bool]":
return True

@property
def start_timestamp(self) -> "Optional[datetime]":
return self._start_timestamp

@property
def timestamp(self) -> "Optional[datetime]":
return self._timestamp


class NoOpStreamedSpan(StreamedSpan):
def __init__(self) -> None:
pass
__slots__ = (
"_scope",
"_previous_span_on_scope",
)

def __init__(
self,
scope: "Optional[sentry_sdk.Scope]" = None,
) -> None:
self._scope = scope # type: ignore[assignment]

self._start()

def __repr__(self) -> str:
return f"<{self.__class__.__name__}(sampled={self.sampled})>"
Expand All @@ -323,7 +446,36 @@ def __enter__(self) -> "NoOpStreamedSpan":
def __exit__(
self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
) -> None:
pass
self._end()

def _start(self) -> None:
if self._scope is None:
return

old_span = self._scope.span
self._scope.span = self # type: ignore
self._previous_span_on_scope = old_span

def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
if self._scope is None:
return

with capture_internal_exceptions():
old_span = self._previous_span_on_scope
del self._previous_span_on_scope
self._scope.span = old_span

def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
self._end()

def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
warnings.warn(
"span.finish() is deprecated. Use span.end() instead.",
stacklevel=2,
category=DeprecationWarning,
)

self._end()

def get_attributes(self) -> "Attributes":
return {}
Expand Down Expand Up @@ -369,6 +521,14 @@ def trace_id(self) -> str:
def sampled(self) -> "Optional[bool]":
return False

@property
def start_timestamp(self) -> "Optional[datetime]":
return None

@property
def timestamp(self) -> "Optional[datetime]":
return None


def trace(
func: "Optional[Callable[P, R]]" = None,
Expand All @@ -380,6 +540,8 @@ def trace(
"""
Decorator to start a span around a function call.

EXPERIMENTAL. Use @sentry_sdk.trace instead.

This decorator automatically creates a new span when the decorated function
is called, and finishes the span when the function returns or raises an exception.

Expand Down Expand Up @@ -427,7 +589,9 @@ def make_db_query(sql):
# Function implementation
pass
"""
from sentry_sdk.tracing_utils import create_streaming_span_decorator
from sentry_sdk.tracing_utils import (
create_streaming_span_decorator,
)

decorator = create_streaming_span_decorator(
name=name,
Expand Down
17 changes: 17 additions & 0 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import re
import sys
import warnings
from collections.abc import Mapping, MutableMapping
from datetime import timedelta
from random import Random
Expand Down Expand Up @@ -958,6 +959,14 @@ def span_decorator(f: "Any") -> "Any":

@functools.wraps(f)
async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
client = sentry_sdk.get_client()
if not has_span_streaming_enabled(client.options):
warnings.warn(
"Using span streaming API in non-span-streaming mode. Use "
"@sentry_sdk.trace instead.",
stacklevel=2,
)

span_name = name or qualname_from_function(f) or ""
Comment on lines +965 to 970
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The @traces.trace decorator issues a duplicate warning when span streaming is disabled because both the decorator wrapper and the start_span function perform the same check and warning.
Severity: MEDIUM

Suggested Fix

Remove the warning check from the async_wrapper and sync_wrapper in sentry_sdk/tracing_utils.py. Rely solely on the existing warning within the start_span() function, which is called by the wrappers, to avoid duplication.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: sentry_sdk/tracing_utils.py#L965-L970

Potential issue: When span streaming is not enabled, functions decorated with
`@traces.trace` will trigger two identical warnings for each call. One warning
originates from the decorator's wrapper in `sentry_sdk/tracing_utils.py`
(`async_wrapper` or `sync_wrapper`). The wrapper then calls `traces.start_span()`, which
issues a second, duplicate warning. Because the warnings originate from different code
locations, Python's warning deduplication mechanism does not suppress the second one,
resulting in confusing and redundant output for the user.


with start_streaming_span(
Expand All @@ -973,6 +982,14 @@ async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any":

@functools.wraps(f)
def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
client = sentry_sdk.get_client()
if not has_span_streaming_enabled(client.options):
warnings.warn(
"Using span streaming API in non-span-streaming mode. Use "
"@sentry_sdk.trace instead.",
stacklevel=2,
)

span_name = name or qualname_from_function(f) or ""

with start_streaming_span(
Expand Down
Loading