From fe91a89171ea44125854cd46c63a2b7d3255e621 Mon Sep 17 00:00:00 2001 From: AP Date: Mon, 5 Oct 2026 11:10:11 +0530 Subject: [PATCH 1/2] fix: clear Scope._transaction when the span is unset The span setter copied a Transaction's name into _transaction but never cleared it, so after a top-level transaction finished (span restored to None) later events on the same scope kept the stale transaction name. The clear only happens on None: a finishing child span restores the transaction itself, so a live transaction's name is preserved. Fixes #7774. --- sentry_sdk/scope.py | 2 ++ tests/test_scope.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 0d3a0c6cd3..a54e1062dc 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -912,6 +912,8 @@ 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 @property def streamed_span(self) -> "Optional[StreamedSpan]": diff --git a/tests/test_scope.py b/tests/test_scope.py index f05f17b313..029f19ff07 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -1110,3 +1110,43 @@ 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" + + child = transaction.start_child(op="child") + scope.span = child + assert scope._transaction == "my-transaction" + + scope.span = None + assert scope._transaction is None + + +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 From cb1513e346bead88b696aec356a44b867a6cf344 Mon Sep 17 00:00:00 2001 From: AP Date: Mon, 5 Oct 2026 11:59:12 +0530 Subject: [PATCH 2/2] fix: clear stale _transaction_info when the span is unset Follow-up to the transaction-clearing change: unsetting the span reset _transaction but left _transaction_info behind, so events captured later on the same scope still received the finished transaction's source via _apply_transaction_info_to_event. Reset it alongside, matching Scope.clear(). --- sentry_sdk/scope.py | 4 ++++ tests/test_scope.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index a54e1062dc..e3a8460a21 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -914,6 +914,10 @@ def span(self, span: "Optional[Span]") -> None: 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 029f19ff07..78ef3a7b54 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -1125,6 +1125,7 @@ def test_transaction_name_cleared_when_span_unset(): 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 @@ -1132,6 +1133,7 @@ def test_transaction_name_cleared_when_span_unset(): 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():