Skip to content

Commit 817c655

Browse files
committed
Add active
1 parent ff136ce commit 817c655

4 files changed

Lines changed: 140 additions & 18 deletions

File tree

sentry_sdk/scope.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,7 @@ def start_streamed_span(
12251225
name: str,
12261226
attributes: "Optional[Attributes]" = None,
12271227
parent_span: "Optional[StreamedSpan]" = None,
1228+
active: bool = True,
12281229
**kwargs: "Any", # TODO[span-first]: remove, just for expediting seer testing
12291230
) -> "StreamedSpan":
12301231
# TODO: rename to start_span once we drop the old API
@@ -1259,6 +1260,7 @@ def start_streamed_span(
12591260
return StreamedSpan(
12601261
name=name,
12611262
attributes=attributes,
1263+
active=active,
12621264
scope=self,
12631265
segment=None,
12641266
trace_id=propagation_context.trace_id,
@@ -1277,6 +1279,7 @@ def start_streamed_span(
12771279
return StreamedSpan(
12781280
name=name,
12791281
attributes=attributes,
1282+
active=active,
12801283
scope=self,
12811284
trace_id=parent_span.trace_id,
12821285
parent_span_id=parent_span.span_id,

sentry_sdk/traces.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def start_span(
102102
name: str,
103103
attributes: "Optional[Attributes]" = None,
104104
parent_span: "Optional[StreamedSpan]" = None,
105+
active: bool = True,
105106
) -> "StreamedSpan":
106107
"""
107108
Start a span.
@@ -136,19 +137,28 @@ def start_span(
136137
137138
:param name: The name to identify this span by.
138139
:type name: str
140+
139141
:param attributes: Key-value attributes to set on the span from the start.
140142
When provided via the `start_span()` function, these will also be
141143
accessible in the traces sampler.
142144
:type attributes: "Optional[Attributes]"
145+
143146
:param parent_span: A span instance that the new span should be parented to.
144147
If not provided, the parent will be set to the currently active span,
145148
if any.
146149
:type parent_span: "Optional[StreamedSpan]"
150+
151+
:param active: Controls whether spans started while this span is running
152+
will automatically become its children. That's the default behavior. If
153+
you want to create a span that shouldn't have any children (unless
154+
provided explicitly via the `parent_span` argument), set this to False.
155+
:type active: bool
156+
147157
:return: A span.
148158
:rtype: StreamedSpan
149159
"""
150160
return sentry_sdk.get_current_scope().start_streamed_span(
151-
name, attributes, parent_span
161+
name, attributes, parent_span, active
152162
)
153163

154164

@@ -201,6 +211,7 @@ class StreamedSpan:
201211
__slots__ = (
202212
"_name",
203213
"_attributes",
214+
"_active",
204215
"_span_id",
205216
"_trace_id",
206217
"parent_span_id",
@@ -227,6 +238,7 @@ def __init__(
227238
name: str,
228239
scope: "sentry_sdk.Scope",
229240
attributes: "Optional[Attributes]" = None,
241+
active: bool = True,
230242
# TODO[span-first]: would be good to actually take this propagation
231243
# context stuff directly from the PropagationContext, but for that
232244
# we'd actually need to refactor PropagationContext to stay in sync
@@ -241,6 +253,7 @@ def __init__(
241253
last_valid_parent_id: "Optional[str]" = None,
242254
) -> None:
243255
self._scope = scope
256+
self._active = active
244257

245258
self._name: str = name
246259
self._attributes: "Attributes" = {}
@@ -295,14 +308,16 @@ def __repr__(self) -> str:
295308
f"trace_id={self.trace_id}, "
296309
f"span_id={self.span_id}, "
297310
f"parent_span_id={self.parent_span_id}, "
298-
f"sampled={self.sampled})>"
311+
f"sampled={self.sampled}, "
312+
f"active={self._active})>"
299313
)
300314

301315
def __enter__(self) -> "StreamedSpan":
302316
scope = self._scope or sentry_sdk.get_current_scope()
303-
old_span = scope.span
304-
scope.span = self
305-
self._context_manager_state = (scope, old_span)
317+
if self._active:
318+
old_span = scope.span
319+
scope.span = self
320+
self._context_manager_state = (scope, old_span)
306321

307322
if self.is_segment():
308323
sampling_context = {
@@ -338,11 +353,15 @@ def __exit__(
338353
if value is not None and should_be_treated_as_error(ty, value):
339354
self.set_status(SpanStatus.ERROR)
340355

341-
with capture_internal_exceptions():
342-
scope, old_span = self._context_manager_state
343-
del self._context_manager_state
344-
self._end(scope=scope)
345-
scope.span = old_span
356+
if self._active:
357+
with capture_internal_exceptions():
358+
scope, old_span = self._context_manager_state
359+
del self._context_manager_state
360+
scope.span = old_span
361+
else:
362+
scope = self._scope
363+
364+
self._end(scope=self._scope)
346365

347366
def start(self) -> "StreamedSpan":
348367
"""
@@ -761,6 +780,7 @@ def trace(
761780
*,
762781
name: "Optional[str]" = None,
763782
attributes: "Optional[dict[str, Any]]" = None,
783+
active: bool = True,
764784
) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]":
765785
"""
766786
Decorator to start a span around a function call.
@@ -783,6 +803,12 @@ def trace(
783803
attributes provide additional context about the span's execution.
784804
:type attributes: dict[str, Any] or None
785805
806+
:param active: Controls whether spans started while this span is running
807+
will automatically become its children. That's the default behavior. If
808+
you want to create a span that shouldn't have any children (unless
809+
provided explicitly via the `parent_span` argument), set this to False.
810+
:type active: bool
811+
786812
:returns: When used as ``@trace``, returns the decorated function. When used as
787813
``@trace(...)`` with parameters, returns a decorator function.
788814
:rtype: Callable or decorator function
@@ -811,6 +837,7 @@ def make_db_query(sql):
811837
decorator = create_streaming_span_decorator(
812838
name=name,
813839
attributes=attributes,
840+
active=active,
814841
)
815842

816843
if func:

sentry_sdk/tracing_utils.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,14 +1046,10 @@ def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
10461046
def create_streaming_span_decorator(
10471047
name: "Optional[str]" = None,
10481048
attributes: "Optional[dict[str, Any]]" = None,
1049+
active: bool = True,
10491050
) -> "Any":
10501051
"""
1051-
Create a span decorator that can wrap both sync and async functions.
1052-
1053-
:param name: The name of the span.
1054-
:type name: str or None
1055-
:param attributes: Additional attributes to set on the span.
1056-
:type attributes: dict or None
1052+
Create a span creating decorator that can wrap both sync and async functions.
10571053
"""
10581054
from sentry_sdk.scope import should_send_default_pii
10591055

@@ -1066,7 +1062,9 @@ def span_decorator(f: "Any") -> "Any":
10661062
async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
10671063
span_name = name or qualname_from_function(f) or ""
10681064

1069-
with start_streaming_span(name=span_name, attributes=attributes):
1065+
with start_streaming_span(
1066+
name=span_name, attributes=attributes, active=active
1067+
):
10701068
result = await f(*args, **kwargs)
10711069
return result
10721070

@@ -1079,7 +1077,9 @@ async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
10791077
def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
10801078
span_name = name or qualname_from_function(f) or ""
10811079

1082-
with start_streaming_span(name=span_name, attributes=attributes):
1080+
with start_streaming_span(
1081+
name=span_name, attributes=attributes, active=active
1082+
):
10831083
return f(*args, **kwargs)
10841084

10851085
try:

tests/tracing/test_span_streaming.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,43 @@ def traces_sampler(sampling_context):
428428
assert span_id_in_traces_sampler == segment.span_id
429429

430430

431+
def test_start_inactive_span(sentry_init, capture_envelopes):
432+
sentry_init(
433+
traces_sample_rate=1.0,
434+
_experiments={"trace_lifecycle": "stream"},
435+
)
436+
437+
events = capture_envelopes()
438+
439+
with sentry_sdk.traces.start_span(name="segment") as segment:
440+
with sentry_sdk.traces.start_span(name="child1", active=False):
441+
with sentry_sdk.traces.start_span(name="child2"):
442+
# Should have segment as parent since child1 is inactive
443+
pass
444+
445+
sentry_sdk.get_client().flush()
446+
spans = envelopes_to_spans(events)
447+
448+
assert len(spans) == 3
449+
child2, child1, segment = spans
450+
451+
assert segment["is_segment"] is True
452+
assert segment["name"] == "segment"
453+
assert segment["attributes"]["sentry.segment.name"] == "segment"
454+
455+
assert child1["is_segment"] is False
456+
assert child1["name"] == "child1"
457+
assert child1["attributes"]["sentry.segment.name"] == "segment"
458+
assert child1["parent_span_id"] == segment["span_id"]
459+
assert child1["trace_id"] == segment["trace_id"]
460+
461+
assert child2["is_segment"] is False
462+
assert child2["name"] == "child2"
463+
assert child2["attributes"]["sentry.segment.name"] == "segment"
464+
assert child2["parent_span_id"] == segment["span_id"]
465+
assert child2["trace_id"] == segment["trace_id"]
466+
467+
431468
def test_start_span_override_parent(sentry_init, capture_envelopes):
432469
sentry_init(
433470
traces_sample_rate=1.0,
@@ -685,6 +722,33 @@ def traced_function(): ...
685722
assert span["status"] == "ok"
686723

687724

725+
def test_trace_decorator_inactive(sentry_init, capture_envelopes):
726+
sentry_init(
727+
traces_sample_rate=1.0,
728+
_experiments={"trace_lifecycle": "stream"},
729+
)
730+
731+
events = capture_envelopes()
732+
733+
@sentry_sdk.traces.trace(name="outer", active=False)
734+
def traced_function():
735+
with sentry_sdk.traces.start_span(name="inner"):
736+
...
737+
738+
traced_function()
739+
740+
sentry_sdk.get_client().flush()
741+
spans = envelopes_to_spans(events)
742+
743+
assert len(spans) == 2
744+
(span1, span2) = spans
745+
746+
assert span1["name"] == "inner"
747+
assert span1["parent_span_id"] != span2["span_id"]
748+
749+
assert span2["name"] == "outer"
750+
751+
688752
@minimum_python_38
689753
def test_trace_decorator_async(sentry_init, capture_envelopes):
690754
sentry_init(
@@ -737,6 +801,34 @@ async def traced_function(): ...
737801
assert span["status"] == "ok"
738802

739803

804+
@minimum_python_38
805+
def test_trace_decorator_async_inactive(sentry_init, capture_envelopes):
806+
sentry_init(
807+
traces_sample_rate=1.0,
808+
_experiments={"trace_lifecycle": "stream"},
809+
)
810+
811+
events = capture_envelopes()
812+
813+
@sentry_sdk.traces.trace(name="outer", active=False)
814+
async def traced_function():
815+
with sentry_sdk.traces.start_span(name="inner"):
816+
...
817+
818+
asyncio.run(traced_function())
819+
820+
sentry_sdk.get_client().flush()
821+
spans = envelopes_to_spans(events)
822+
823+
assert len(spans) == 2
824+
(span1, span2) = spans
825+
826+
assert span1["name"] == "inner"
827+
assert span1["parent_span_id"] != span2["span_id"]
828+
829+
assert span2["name"] == "outer"
830+
831+
740832
def test_set_span_status(sentry_init, capture_envelopes):
741833
sentry_init(
742834
traces_sample_rate=1.0,

0 commit comments

Comments
 (0)