diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 0d3a0c6cd3..e3a8460a21 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -912,6 +912,12 @@ def span(self, span: "Optional[Span]") -> None: self._transaction = transaction.name if transaction.source: self._transaction_info["source"] = transaction.source + elif span is None: + self._transaction = None + # The transaction source describes the transaction name; leaving + # it behind would stamp stale transaction_info on later events + # that no longer carry a transaction. + self._transaction_info = {} @property def streamed_span(self) -> "Optional[StreamedSpan]": diff --git a/tests/test_scope.py b/tests/test_scope.py index f05f17b313..78ef3a7b54 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -1110,3 +1110,45 @@ def test_conversation_id_clear(): scope.clear() assert scope.get_conversation_id() is None + + +def test_transaction_name_cleared_when_span_unset(): + """Scope._transaction follows the span lifecycle (GH-7774). + + Set by a Transaction, preserved while a child span is active, cleared + when the span is set back to None (top-level transaction exit). + """ + from sentry_sdk.tracing import Transaction + + scope = Scope() + + transaction = Transaction(name="my-transaction") + scope.span = transaction + assert scope._transaction == "my-transaction" + assert scope._transaction_info.get("source") # info follows the name + + child = transaction.start_child(op="child") + scope.span = child + assert scope._transaction == "my-transaction" + + scope.span = None + assert scope._transaction is None + assert scope._transaction_info == {} # no stale source left behind + + +def test_transaction_name_survives_nested_span_exit(): + """Exiting a child span restores the transaction on the scope. + + The transaction name must not be dropped when an inner span finishes + while the transaction is still live. + """ + scope = Scope() + + with sentry_sdk.start_transaction(name="my-transaction", scope=scope): + assert scope._transaction == "my-transaction" + with sentry_sdk.start_span(op="child", scope=scope): + assert scope._transaction == "my-transaction" + # span restored to the transaction, not None + assert scope._transaction == "my-transaction" + + assert scope._transaction is None