Skip to content

Commit 0c4a75d

Browse files
ref: Support outgoing trace propagation in span first (18) (#5638)
Couple things going on in this PR. Bear with me, this is probably the most all over the place span first PR because the outgoing trace propagation changes make mypy complain about things elsewhere in the sdk. ### 1. Outgoing trace propagation Support getting trace propagation information from the span with `_get_traceparent`, `_get_baggage`, `_iter_headers`, etc. These mirror the old `Span` class to make integrating `StreamedSpan`s with the rest of the SDK easier (since they're used throughout), with one difference: they're explicitly private, while the corresponding `Span` methods were public. Added aliases to them so that we can use the private methods everywhere. There is definite clean up potential here once we get rid of the old spans and we no longer have to make the streaming span interface work with the existing helper scope methods. ### 2. Addressing cascading mypy issues Now that we're officially allowing `StreamedSpan`s to be set on the scope, a LOT of type hints need updating all over the SDK. In many places, I've added explicit guards against functionality that doesn't exist in span first mode. This should prevent folks from using the wrong APIs in the wrong SDK mode (tracing vs. static) as well as make mypy happy. --------- Co-authored-by: Erica Pisani <pisani.erica@gmail.com>
1 parent ca37ab4 commit 0c4a75d

File tree

11 files changed

+362
-37
lines changed

11 files changed

+362
-37
lines changed

sentry_sdk/_span_batcher.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,7 @@ def _flush(self) -> None:
114114
envelopes = []
115115
for trace_id, spans in self._span_buffer.items():
116116
if spans:
117-
# TODO[span-first]
118-
# dsc = spans[0].dynamic_sampling_context()
119-
dsc = None
117+
dsc = spans[0]._dynamic_sampling_context()
120118

121119
# Max per envelope is 1000, so if we happen to have more than
122120
# 1000 spans in one bucket, we'll need to separate them.

sentry_sdk/ai/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import sentry_sdk
1414
from sentry_sdk.utils import logger
15+
from sentry_sdk.traces import StreamedSpan
16+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
1517

1618
MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB
1719
# Maximum characters when only a single message is left after bytes truncation
@@ -523,7 +525,14 @@ def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str,
523525

524526

525527
def get_start_span_function() -> "Callable[..., Any]":
528+
if has_span_streaming_enabled(sentry_sdk.get_client().options):
529+
return sentry_sdk.traces.start_span
530+
526531
current_span = sentry_sdk.get_current_span()
532+
if isinstance(current_span, StreamedSpan):
533+
# mypy
534+
return sentry_sdk.traces.start_span
535+
527536
transaction_exists = (
528537
current_span is not None and current_span.containing_transaction is not None
529538
)

sentry_sdk/api.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sentry_sdk._init_implementation import init
77
from sentry_sdk.consts import INSTRUMENTER
88
from sentry_sdk.scope import Scope, _ScopeManager, new_scope, isolation_scope
9+
from sentry_sdk.traces import StreamedSpan
910
from sentry_sdk.tracing import NoOpSpan, Transaction, trace
1011
from sentry_sdk.crons import monitor
1112

@@ -37,6 +38,7 @@
3738
LogLevelStr,
3839
SamplingContext,
3940
)
41+
from sentry_sdk.traces import StreamedSpan
4042
from sentry_sdk.tracing import Span, TransactionKwargs
4143

4244
T = TypeVar("T")
@@ -409,7 +411,9 @@ def set_measurement(name: str, value: float, unit: "MeasurementUnit" = "") -> No
409411
transaction.set_measurement(name, value, unit)
410412

411413

412-
def get_current_span(scope: "Optional[Scope]" = None) -> "Optional[Span]":
414+
def get_current_span(
415+
scope: "Optional[Scope]" = None,
416+
) -> "Optional[Union[Span, StreamedSpan]]":
413417
"""
414418
Returns the currently active span if there is one running, otherwise `None`
415419
"""
@@ -525,6 +529,16 @@ def update_current_span(
525529
if current_span is None:
526530
return
527531

532+
if isinstance(current_span, StreamedSpan):
533+
warnings.warn(
534+
"The `update_current_span` API isn't available in streaming mode. "
535+
"Retrieve the current span with get_current_span() and use its API "
536+
"directly.",
537+
DeprecationWarning,
538+
stacklevel=2,
539+
)
540+
return
541+
528542
if op is not None:
529543
current_span.op = op
530544

sentry_sdk/feature_flags.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy
22
import sentry_sdk
33
from sentry_sdk._lru_cache import LRUCache
4+
from sentry_sdk.tracing import Span
45
from threading import Lock
56

67
from typing import TYPE_CHECKING, Any
@@ -61,5 +62,5 @@ def add_feature_flag(flag: str, result: bool) -> None:
6162
flags.set(flag, result)
6263

6364
span = sentry_sdk.get_current_span()
64-
if span:
65+
if span and isinstance(span, Span):
6566
span.set_flag(f"flag.evaluation.{flag}", result)

sentry_sdk/integrations/celery/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
)
1515
from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch
1616
from sentry_sdk.integrations.logging import ignore_logger
17-
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, TransactionSource
17+
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource
1818
from sentry_sdk.tracing_utils import Baggage
1919
from sentry_sdk.utils import (
2020
capture_internal_exceptions,
@@ -34,7 +34,6 @@
3434
from typing import Union
3535

3636
from sentry_sdk._types import EventProcessor, Event, Hint, ExcInfo
37-
from sentry_sdk.tracing import Span
3837

3938
F = TypeVar("F", bound=Callable[..., Any])
4039

@@ -100,7 +99,10 @@ def _set_status(status: str) -> None:
10099
with capture_internal_exceptions():
101100
scope = sentry_sdk.get_current_scope()
102101
if scope.span is not None:
103-
scope.span.set_status(status)
102+
if isinstance(scope.span, Span):
103+
scope.span.set_status(status)
104+
else:
105+
scope.span.status = "ok" if status == "ok" else "error"
104106

105107

106108
def _capture_exception(task: "Any", exc_info: "ExcInfo") -> None:

sentry_sdk/integrations/openai_agents/utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from sentry_sdk.consts import SPANDATA, SPANSTATUS, OP
1212
from sentry_sdk.integrations import DidNotEnable
1313
from sentry_sdk.scope import should_send_default_pii
14+
from sentry_sdk.tracing import Span
1415
from sentry_sdk.tracing_utils import set_span_errored
1516
from sentry_sdk.utils import event_from_exception, safe_serialize
1617
from sentry_sdk.ai._openai_completions_api import _transform_system_instructions
@@ -22,10 +23,10 @@
2223
from typing import TYPE_CHECKING
2324

2425
if TYPE_CHECKING:
25-
from typing import Any
26+
from typing import Any, Union
2627
from agents import Usage, TResponseInputItem
2728

28-
from sentry_sdk.tracing import Span
29+
from sentry_sdk.traces import StreamedSpan
2930
from sentry_sdk._types import TextPart
3031

3132
try:
@@ -46,8 +47,15 @@ def _capture_exception(exc: "Any") -> None:
4647
sentry_sdk.capture_event(event, hint=hint)
4748

4849

49-
def _record_exception_on_span(span: "Span", error: Exception) -> "Any":
50+
def _record_exception_on_span(
51+
span: "Union[Span, StreamedSpan]", error: Exception
52+
) -> "Any":
5053
set_span_errored(span)
54+
55+
if not isinstance(span, Span):
56+
# TODO[span-first]: make this work with streamedspans
57+
return
58+
5159
span.set_data("span.status", "error")
5260

5361
# Optionally capture the error details if we have them

sentry_sdk/scope.py

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -581,8 +581,12 @@ def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]":
581581
client = self.get_client()
582582

583583
# If we have an active span, return traceparent from there
584-
if has_tracing_enabled(client.options) and self.span is not None:
585-
return self.span.to_traceparent()
584+
if (
585+
has_tracing_enabled(client.options)
586+
and self.span is not None
587+
and not isinstance(self.span, NoOpStreamedSpan)
588+
):
589+
return self.span._to_traceparent()
586590

587591
# else return traceparent from the propagation context
588592
return self.get_active_propagation_context().to_traceparent()
@@ -595,8 +599,12 @@ def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]":
595599
client = self.get_client()
596600

597601
# If we have an active span, return baggage from there
598-
if has_tracing_enabled(client.options) and self.span is not None:
599-
return self.span.to_baggage()
602+
if (
603+
has_tracing_enabled(client.options)
604+
and self.span is not None
605+
and not isinstance(self.span, NoOpStreamedSpan)
606+
):
607+
return self.span._to_baggage()
600608

601609
# else return baggage from the propagation context
602610
return self.get_active_propagation_context().get_baggage()
@@ -605,8 +613,12 @@ def get_trace_context(self) -> "Dict[str, Any]":
605613
"""
606614
Returns the Sentry "trace" context from the Propagation Context.
607615
"""
608-
if has_tracing_enabled(self.get_client().options) and self._span is not None:
609-
return self._span.get_trace_context()
616+
if (
617+
has_tracing_enabled(self.get_client().options)
618+
and self._span is not None
619+
and not isinstance(self._span, NoOpStreamedSpan)
620+
):
621+
return self._span._get_trace_context()
610622

611623
# if we are tracing externally (otel), those values take precedence
612624
external_propagation_context = get_external_propagation_context()
@@ -670,8 +682,12 @@ def iter_trace_propagation_headers(
670682
span = kwargs.pop("span", None)
671683
span = span or self.span
672684

673-
if has_tracing_enabled(client.options) and span is not None:
674-
for header in span.iter_headers():
685+
if (
686+
has_tracing_enabled(client.options)
687+
and span is not None
688+
and not isinstance(span, NoOpStreamedSpan)
689+
):
690+
for header in span._iter_headers():
675691
yield header
676692
elif has_external_propagation_context():
677693
# when we have an external_propagation_context (otlp)
@@ -718,7 +734,7 @@ def clear(self) -> None:
718734
self.clear_breadcrumbs()
719735
self._should_capture: bool = True
720736

721-
self._span: "Optional[Span]" = None
737+
self._span: "Optional[Union[Span, StreamedSpan]]" = None
722738
self._session: "Optional[Session]" = None
723739
self._force_auto_session_tracking: "Optional[bool]" = None
724740

@@ -772,6 +788,14 @@ def transaction(self) -> "Any":
772788
if self._span is None:
773789
return None
774790

791+
if isinstance(self._span, StreamedSpan):
792+
warnings.warn(
793+
"Scope.transaction is not available in streaming mode.",
794+
DeprecationWarning,
795+
stacklevel=2,
796+
)
797+
return None
798+
775799
# there is an orphan span on the scope
776800
if self._span.containing_transaction is None:
777801
return None
@@ -801,17 +825,36 @@ def transaction(self, value: "Any") -> None:
801825
"Assigning to scope.transaction directly is deprecated: use scope.set_transaction_name() instead."
802826
)
803827
self._transaction = value
804-
if self._span and self._span.containing_transaction:
805-
self._span.containing_transaction.name = value
828+
if self._span:
829+
if isinstance(self._span, StreamedSpan):
830+
warnings.warn(
831+
"Scope.transaction is not available in streaming mode.",
832+
DeprecationWarning,
833+
stacklevel=2,
834+
)
835+
return None
836+
837+
if self._span.containing_transaction:
838+
self._span.containing_transaction.name = value
806839

807840
def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> None:
808841
"""Set the transaction name and optionally the transaction source."""
809842
self._transaction = name
810-
811-
if self._span and self._span.containing_transaction:
812-
self._span.containing_transaction.name = name
813-
if source:
814-
self._span.containing_transaction.source = source
843+
if self._span:
844+
if isinstance(self._span, NoOpStreamedSpan):
845+
return
846+
847+
elif isinstance(self._span, StreamedSpan):
848+
self._span._segment.name = name
849+
if source:
850+
self._span._segment.set_attribute(
851+
"sentry.span.source", getattr(source, "value", source)
852+
)
853+
854+
elif self._span.containing_transaction:
855+
self._span.containing_transaction.name = name
856+
if source:
857+
self._span.containing_transaction.source = source
815858

816859
if source:
817860
self._transaction_info["source"] = source
@@ -834,12 +877,12 @@ def set_user(self, value: "Optional[Dict[str, Any]]") -> None:
834877
session.update(user=value)
835878

836879
@property
837-
def span(self) -> "Optional[Span]":
880+
def span(self) -> "Optional[Union[Span, StreamedSpan]]":
838881
"""Get/set current tracing span or transaction."""
839882
return self._span
840883

841884
@span.setter
842-
def span(self, span: "Optional[Span]") -> None:
885+
def span(self, span: "Optional[Union[Span, StreamedSpan]]") -> None:
843886
self._span = span
844887
# XXX: this differs from the implementation in JS, there Scope.setSpan
845888
# does not set Scope._transactionName.
@@ -1148,6 +1191,15 @@ def start_span(
11481191
be removed in the next major version. Going forward, it should only
11491192
be used by the SDK itself.
11501193
"""
1194+
client = sentry_sdk.get_client()
1195+
if has_span_streaming_enabled(client.options):
1196+
warnings.warn(
1197+
"Scope.start_span is not available in streaming mode.",
1198+
DeprecationWarning,
1199+
stacklevel=2,
1200+
)
1201+
return NoOpSpan()
1202+
11511203
if kwargs.get("description") is not None:
11521204
warnings.warn(
11531205
"The `description` parameter is deprecated. Please use `name` instead.",
@@ -1167,6 +1219,9 @@ def start_span(
11671219

11681220
# get current span or transaction
11691221
span = self.span or self.get_isolation_scope().span
1222+
if isinstance(span, StreamedSpan):
1223+
# make mypy happy
1224+
return NoOpSpan()
11701225

11711226
if span is None:
11721227
# New spans get the `trace_id` from the scope

0 commit comments

Comments
 (0)