ref: Allow to start and finish StreamedSpans (9)#5598
ref: Allow to start and finish StreamedSpans (9)#5598sentrivana wants to merge 53 commits intomasterfrom
StreamedSpans (9)#5598Conversation
…first-6-add-continue-and-new-trace
…na/span-first-7-add-trace-decorator
…-first-8-bucket-based-limits-in-batcher
…-first-8-bucket-based-limits-in-batcher
…-first-8-bucket-based-limits-in-batcher
… ivana/span-first-9-start-end
…-first-8-bucket-based-limits-in-batcher
… ivana/span-first-9-start-end
| if item._timestamp: | ||
| res["end_timestamp"] = item._timestamp.timestamp() |
There was a problem hiding this comment.
There is no scenario where a span that's already in the span batcher wouldn't have an end _timestamp, but mypy doesn't know that. 🤡
| try: | ||
| # profiling depends on this value and requires that | ||
| # it is measured in nanoseconds | ||
| self._start_timestamp_monotonic_ns = nanosecond_time() |
There was a problem hiding this comment.
Anything related to _start_timestamp_monotonic_ns is just copy-pasted from the Span implementation
| """ | ||
| self._end(end_timestamp) | ||
|
|
||
| def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None: |
There was a problem hiding this comment.
Just here for compat reasons, to make the transition from the old API easier (very slightly).
| self.end(end_timestamp) | ||
|
|
||
| def _start(self) -> None: | ||
| if self._active: |
There was a problem hiding this comment.
_active basically controls whether a span will be set on the scope, so if it's False, we don't need to keep track of the old parent span to restore it later, because we're never replacing it.
| if self._scope is None: | ||
| return | ||
|
|
||
| old_span = self._scope.span | ||
| self._scope.span = self | ||
| self._previous_span_on_scope = old_span |
There was a problem hiding this comment.
Unlike the main span class, no-op spans don't have an active flag. The user cannot control whether a no-op span will be set on the scope; the SDK does this internally, and it uses the _scope attribute to remember the decision. In the case of normal spans, the _scope attribute plays a different role: they all need a scope, regardless of whether they'll actually be set on it or not, because they will be captured using that scope in either case.
Coming back to no-op spans, the decision whether to set them on the scope or not is tightly coupled to how we want ignore_spans to work (coming in a future PR). Basically, if a segment/root span would be ignored, all of its children should be as well. That's easiest to accomplish if we actually set the root no-op span on the scope. On the other hand, if a non-root span is ignored, it's not set on scope, so that any children spans effectively use the last not ignored span that's set on the scope as parent.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: NoOpStreamedSpan._end() missing double-end guard causes error logs
- Added a double-end guard in
NoOpStreamedSpan._end()that returns when_previous_span_on_scopeis already removed, preventing spurious internal error logging on repeatedend()calls.
- Added a double-end guard in
Or push these changes by commenting:
@cursor push 048194f3a9
Preview (048194f3a9)
diff --git a/sentry_sdk/traces.py b/sentry_sdk/traces.py
--- a/sentry_sdk/traces.py
+++ b/sentry_sdk/traces.py
@@ -448,7 +448,7 @@
self._previous_span_on_scope = old_span
def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
- if self._scope is None:
+ if self._scope is None or not hasattr(self, "_previous_span_on_scope"):
return
with capture_internal_exceptions():There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Description
This PR adds logic for starting and finishing spans, both via a context manager
with start_span(...), as well as directly withspan = start_span(...)andspan.end().Summary
_start()and_end()functions that handle the starting and ending logic._start()handles keeping track of what was calledcontext_manager_stateon old Spans, i.e., remembering which span was set on the scope before this span so that it can be restored once this span is over._end()handles restoring the old span, setting the end timestamp, adding final attributes on it, and queueing it in the span batcher.__enter__and__exit__methods on bothStreamedSpanandNoOpStreamedSpan. These call the internal methods above, with__exit__additionally setting the span status to error if it encountered one.span.end()public function that allows to set an optionalend_timestamp. If provided, it sets it and calls_end()internally.span.finish()too, which is deprecated from the get go, just to make the migration to span first a bit easier (since the method is calledfinish()on legacy spans).Issues
Reminders
tox -e linters.feat:,fix:,ref:,meta:)