Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
1 change: 1 addition & 0 deletions sentry_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"get_isolation_scope",
"get_current_scope",
"get_current_span",
"get_current_streamed_span",
"get_traceparent",
"is_initialized",
"isolation_scope",
Expand Down
12 changes: 11 additions & 1 deletion sentry_sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def overload(x: "T") -> "T":
"get_isolation_scope",
"get_current_scope",
"get_current_span",
"get_current_streamed_span",
"get_traceparent",
"is_initialized",
"isolation_scope",
Expand Down Expand Up @@ -422,13 +423,22 @@ def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> No

def get_current_span(
scope: "Optional[Scope]" = None,
) -> "Optional[Union[Span, StreamedSpan]]":
) -> "Optional[Span]":
"""
Returns the currently active span if there is one running, otherwise `None`
"""
return tracing_utils.get_current_span(scope)


def get_current_streamed_span(
scope: "Optional[Scope]" = None,
) -> "Optional[StreamedSpan]":
"""
Returns the currently active streamed span if there is one running, otherwise `None`
"""
return tracing_utils.get_current_streamed_span(scope)
Comment thread
alexander-alderman-webb marked this conversation as resolved.
Outdated
Copy link
Copy Markdown
Contributor

@sentrivana sentrivana Apr 30, 2026

Choose a reason for hiding this comment

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

I'd not expose this as a public API function, at least while the functionality is experimental. Maybe we can move this to traces.py, or at least prefix it with an underscore.

Would also be good to add more context to the docstring so that users know when this can be used (i.e., only if trace_lifecycle="stream").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes that's a great point, it would be best to not expose too much. I've limited the API surface to _get_current_streamed_span in traces.py and expanded the docstring



Comment thread
alexander-alderman-webb marked this conversation as resolved.
Outdated
def get_traceparent() -> "Optional[str]":
"""
Returns the traceparent either from the active span or from the scope.
Expand Down
20 changes: 13 additions & 7 deletions sentry_sdk/integrations/celery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,16 @@ def setup_once() -> None:


def _set_status(status: str) -> None:
client = sentry_sdk.get_client()
span_streaming = has_span_streaming_enabled(client.options)

with capture_internal_exceptions():
scope = sentry_sdk.get_current_scope()
if scope.span is not None:
if isinstance(scope.span, Span):
scope.span.set_status(status)
else:
scope.span.status = "ok" if status == "ok" else "error"

if span_streaming and scope.streamed_span is not None:
scope.streamed_span.status = "ok" if status == "ok" else "error"
elif not span_streaming and scope.span is not None:
scope.span.set_status(status)


def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None:
Expand Down Expand Up @@ -289,7 +292,10 @@ def apply_async(*args: "Any", **kwargs: "Any") -> "Any":

span_mgr: "Union[StreamedSpan, Span, NoOpMgr]" = NoOpMgr()
if span_streaming:
if not task_started_from_beat and sentry_sdk.get_current_span() is not None:
if (
not task_started_from_beat
and sentry_sdk.get_current_streamed_span() is not None
):
span_mgr = sentry_sdk.traces.start_span(
name=task_name,
attributes={
Expand Down Expand Up @@ -570,7 +576,7 @@ def sentry_publish(self: "Producer", *args: "Any", **kwargs: "Any") -> "Any":

span: "Union[StreamedSpan, Span, None]" = None
if span_streaming:
if sentry_sdk.get_current_span() is not None:
if sentry_sdk.get_current_streamed_span() is not None:
span = sentry_sdk.traces.start_span(
name=task_name,
attributes={
Expand Down
16 changes: 10 additions & 6 deletions sentry_sdk/integrations/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,17 @@ def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any":
@wraps(old_call)
def _sentry_call(*args: "Any", **kwargs: "Any") -> "Any":
current_scope = sentry_sdk.get_current_scope()
current_span = current_scope.span

if isinstance(current_span, StreamedSpan) and not isinstance(
current_span, NoOpStreamedSpan
):
segment = current_span._segment
segment._update_active_thread()
client = sentry_sdk.get_client()
if has_span_streaming_enabled(client.options):
current_span = current_scope.streamed_span

if isinstance(current_span, StreamedSpan) and not isinstance(
current_span, NoOpStreamedSpan
):
segment = current_span._segment
segment._update_active_thread()

elif current_scope.transaction is not None:
current_scope.transaction.update_active_thread()

Expand Down
21 changes: 12 additions & 9 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
def _set_request_body_data_on_streaming_segment(
info: "Optional[Dict[str, Any]]",
) -> None:
current_span = sentry_sdk.get_current_span()
current_span = sentry_sdk.get_current_streamed_span()
if (
info
and "data" in info
Expand Down Expand Up @@ -545,21 +545,24 @@

@functools.wraps(old_func)
def _sentry_sync_func(*args: "Any", **kwargs: "Any") -> "Any":
integration = sentry_sdk.get_client().get_integration(
StarletteIntegration
)
client = sentry_sdk.get_client()

integration = client.get_integration(StarletteIntegration)
if integration is None:
return old_func(*args, **kwargs)

current_scope = sentry_sdk.get_current_scope()
current_span = current_scope.span

if isinstance(current_span, StreamedSpan) and not isinstance(
current_span, NoOpStreamedSpan
):
current_span._segment._update_active_thread()
span_streaming = has_span_streaming_enabled(client.options)
if span_streaming:
current_span = current_scope.streamed_span

if isinstance(current_span, StreamedSpan) and not isinstance(
current_span, NoOpStreamedSpan
):
current_span._segment._update_active_thread()
elif current_scope.transaction is not None:
current_scope.transaction.update_active_thread()

Check warning on line 565 in sentry_sdk/integrations/starlette.py

View check run for this annotation

@sentry/warden / warden: code-review

Transaction active-thread fallback skipped when span streaming is enabled but no streamed span exists

The refactor changes the structure to `if span_streaming: ... elif current_scope.transaction is not None:`. When span streaming is enabled but `current_scope.streamed_span` is not a usable `StreamedSpan` (e.g., it's `None` or a `NoOpStreamedSpan`), the inner branch is skipped and the `elif` is never evaluated, so `current_scope.transaction.update_active_thread()` is no longer called. Previously the equivalent fallback path executed whenever the streamed span check failed. As a result, profiling thread updates can be silently dropped for sync Starlette requests when streaming is enabled but no streamed span is active.
Comment thread
sentry-warden[bot] marked this conversation as resolved.

sentry_scope = sentry_sdk.get_isolation_scope()
if sentry_scope.profile is not None:
Expand Down
21 changes: 16 additions & 5 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,9 @@ def iter_trace_propagation_headers(
return

span = kwargs.pop("span", None)
span = span or self.span
if not span:
span_streaming = has_span_streaming_enabled(client.options)
span = self.streamed_span if span_streaming else self.span

if (
has_tracing_enabled(client.options)
Expand Down Expand Up @@ -877,12 +879,12 @@ def set_user(self, value: "Optional[Dict[str, Any]]") -> None:
session.update(user=value)

@property
def span(self) -> "Optional[Union[Span, StreamedSpan]]":
def span(self) -> "Optional[Span]":
"""Get/set current tracing span or transaction."""
return self._span
return self._span if isinstance(self._span, Span) else None

@span.setter
def span(self, span: "Optional[Union[Span, StreamedSpan]]") -> None:
def span(self, span: "Optional[Span]") -> None:
self._span = span
# XXX: this differs from the implementation in JS, there Scope.setSpan
# does not set Scope._transactionName.
Expand All @@ -893,6 +895,15 @@ def span(self, span: "Optional[Union[Span, StreamedSpan]]") -> None:
if transaction.source:
self._transaction_info["source"] = transaction.source
Comment thread
alexander-alderman-webb marked this conversation as resolved.

@property
def streamed_span(self) -> "Optional[StreamedSpan]":
"""Get/set current tracing span."""
return self._span if isinstance(self._span, StreamedSpan) else None

@streamed_span.setter
def streamed_span(self, span: "Optional[StreamedSpan]") -> None:
self._span = span
Comment thread
alexander-alderman-webb marked this conversation as resolved.

# Also set _transaction and _transaction_info in streaming mode as this
# is used for populating events and linking them to segments
if (
Expand Down Expand Up @@ -1267,7 +1278,7 @@ def start_streamed_span(
if parent_span is _DEFAULT_PARENT_SPAN or isinstance(
parent_span, NoOpStreamedSpan
):
parent_span = self.span # type: ignore
parent_span = self.streamed_span # type: ignore

# If no eligible parent_span was provided and there is no currently
# active span, this is a segment
Expand Down
12 changes: 6 additions & 6 deletions sentry_sdk/traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,8 @@ def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> No

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

def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
Expand All @@ -360,7 +360,7 @@ def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None
with capture_internal_exceptions():
old_span = self._previous_span_on_scope
del self._previous_span_on_scope
self._scope.span = old_span
self._scope.streamed_span = old_span

# Set attributes from the segment. These are set on span end on purpose
# so that we have the best chance to capture the segment's final name
Expand Down Expand Up @@ -586,8 +586,8 @@ def _start(self) -> None:
if self._scope is None:
return

old_span = self._scope.span
self._scope.span = self
old_span = self._scope.streamed_span
self._scope.streamed_span = self
self._previous_span_on_scope = old_span

def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
Expand All @@ -610,7 +610,7 @@ def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None
with capture_internal_exceptions():
old_span = self._previous_span_on_scope
del self._previous_span_on_scope
self._scope.span = old_span
self._scope.streamed_span = old_span

self._finished = True

Expand Down
13 changes: 12 additions & 1 deletion sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@ def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any":

def get_current_span(
scope: "Optional[sentry_sdk.Scope]" = None,
) -> "Optional[Union[Span, StreamedSpan]]":
) -> "Optional[Span]":
"""
Returns the currently active span if there is one running, otherwise `None`
"""
Expand All @@ -1203,6 +1203,17 @@ def get_current_span(
return current_span


def get_current_streamed_span(
Comment thread
alexander-alderman-webb marked this conversation as resolved.
Outdated
scope: "Optional[sentry_sdk.Scope]" = None,
) -> "Optional[StreamedSpan]":
"""
Returns the currently active span if there is one running, otherwise `None`
"""
scope = scope or sentry_sdk.get_current_scope()
current_span = scope.streamed_span
return current_span


Comment thread
alexander-alderman-webb marked this conversation as resolved.
Outdated
def set_span_errored(span: "Optional[Union[Span, StreamedSpan]]" = None) -> None:
"""
Set the status of the current or given span to INTERNAL_ERROR.
Expand Down
Loading