fix(otel): only set Scope.Transaction in SentrySpanProcessor when null#5337
fix(otel): only set Scope.Transaction in SentrySpanProcessor when null#5337tsushanth wants to merge 1 commit into
Conversation
When two root OTel activities overlap and neither is a child of the other, OnStart was calling ConfigureScope unconditionally with scope.Transaction = transaction, silently overwriting whatever transaction was already on the scope. The more intuitive default is to not overwrite an existing transaction. Add a null-check guard to ConfigureScope so Scope.Transaction is only set when it is currently null. Fixes getsentry#5314
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.
Reviewed by Cursor Bugbot for commit 8a3f87c. Configure here.
| ITransaction? scopeTransaction = null; | ||
| _fixture.Hub.ConfigureScope(scope => scopeTransaction = scope.Transaction); | ||
| scopeTransaction.Should().BeSameAs(existing, "OnStart should not overwrite an existing Scope.Transaction"); | ||
| } |
There was a problem hiding this comment.
Test skips CreateRootSpan path
Medium Severity
OnStart_WithExistingTransactionOnScope_DoesNotOverwriteExistingTransaction seeds the scope with a Sentry-instrumented transaction, so OnStart infers a non-OTel parent and calls CreateChildSpan instead of CreateRootSpan. The new null guard on scope.Transaction never runs, so the test would still pass if that guard were removed and does not cover overlapping OTel root activities from GH-5314.
Reviewed by Cursor Bugbot for commit 8a3f87c. Configure here.
| _hub.ConfigureScope(static (scope, transaction) => | ||
| { | ||
| if (scope.Transaction is null) | ||
| { | ||
| scope.Transaction = transaction; | ||
| } | ||
| }, transaction); |
There was a problem hiding this comment.
Bug: When multiple root activities overlap, if the first one finishes, a new activity can take over the scope's transaction, leaving a longer-running, pre-existing activity untracked on the scope.
Severity: LOW
Suggested Fix
The logic in scope.ResetTransaction should be enhanced. Instead of unconditionally setting the transaction to null when the current one finishes, it should check for other active transactions that could take ownership of the scope. This would prevent a new, unrelated transaction from claiming the scope while another is still active, maintaining a more consistent state.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: src/Sentry.OpenTelemetry/SentrySpanProcessor.cs#L179-L185
Potential issue: A state inconsistency can occur with multiple overlapping root
OpenTelemetry activities. The new logic only sets `scope.Transaction` if it's `null`. If
the first root activity (A) finishes, it calls `scope.ResetTransaction(A)`, which
nullifies the transaction on the scope. If a second root activity (B) started while A
was active, it would not have been set on the scope. If a third root activity (C) starts
after A finishes, it will find `scope.Transaction` is `null` and set itself as the
current transaction. This leads to a situation where activity B is still running, but
the scope incorrectly points to activity C, creating a logical inconsistency in which
transaction is considered 'current'. While this doesn't cause data loss or crashes, it
violates the principle that the scope should track the active transaction.
Did we get this right? 👍 / 👎 to inform future reviews.
| sut.OnStart(data!); | ||
|
|
||
| // Assert - the pre-existing transaction must still be on the scope | ||
| ITransaction? scopeTransaction = null; |
There was a problem hiding this comment.
issue: test was neither run nor built
- there is no
ITransactiontype - this project does not have Nullable Reference Types enabled
| [Fact] | ||
| public void OnStart_WithExistingTransactionOnScope_DoesNotOverwriteExistingTransaction() |
There was a problem hiding this comment.
issue: test is ineffective
Currently, this test does not actually cover the modification, and does not cover the modified method SentrySpanProcessor.CreateRootSpan.
There was a problem hiding this comment.
@jamescrosswell I wonder if Mutation Testing could cover such issues early ... e.g. if we only run mutation tests on the changes per PR, and only via the test cases that cover the /src/ changes of the PR.
| // Arrange - simulate a root OTel activity starting while another transaction is | ||
| // already on the scope (e.g. two overlapping root activities). The processor should | ||
| // not overwrite the existing transaction (see GH-5314). |
There was a problem hiding this comment.
thought: comments don't add additional value
IMO, the comments in this test apart from Arrange/Assert/Assert don't really add any additional value:
- Assert: the existing transaction is called
"Existing" - Act: the New Root Activity that is started is called
"NewRootActivity"- also: actually,
Actis missing for the Act portion of the test
- also: actually,
- Assert: the
becausestring parameter to the assertion describes
To me, the combination of Test-Name + Test-Logic + String-Parameters already describe what this test is covering and asserting.
| // Only set Scope.Transaction when it is currently null. Overwriting an existing | ||
| // transaction (which can happen when two root OTel activities overlap and neither is | ||
| // a child of the other) silently drops the previous transaction's scope context. | ||
| // See https://github.com/getsentry/sentry-dotnet/issues/5314 |
There was a problem hiding this comment.
suggestion: shorten comment
Although this comment does add value ... I find it a bit lengthy.
Perhaps we can shorten it a bit: for example: Only set Scope.Transaction when it is currently null is just describing the C# syntax in use.
| // Only set Scope.Transaction when it is currently null. Overwriting an existing | ||
| // transaction (which can happen when two root OTel activities overlap and neither is | ||
| // a child of the other) silently drops the previous transaction's scope context. | ||
| // See https://github.com/getsentry/sentry-dotnet/issues/5314 |
There was a problem hiding this comment.
question: do we want to add links to issues more aggressively? @jamescrosswell
Previously, we only added links to the respective GitHub issue, when the code isn't too obvious what it's trying to achieve in a rather complex flow ... or when the solution in place should be reviewed/revisited at some point in time when it is a workaround.
But it seems that Coding Agents like to add both a summary from the GitHub issue, as well as a link to the GitHub issue, and additionally describe the code semantics ahead.
In this case, I would prefer to either have a one-liner, or two/three words + link to the GitHub issue ... but both - to me - is a bit lengthy. To be fair, I might exaggerate "the problem" here a bit. What do you think?
|
Thank you very much, @tsushanth, for your contribution to the Sentry .NET SDK! |


Fixes #5314.
Problem
SentrySpanProcessor.OnStartcallsConfigureScopeunconditionally:When two root OTel activities overlap and neither is a child of the other, this silently overwrites whatever transaction was already on the scope with the new one. As the issue notes, the intuitive default is to not overwrite an existing transaction.
Fix
Add a null-check guard inside the
ConfigureScopelambda:Scope.Transactionis only set when it is currentlynull; an already-present transaction is left unchanged.Test
Added
OnStart_WithExistingTransactionOnScope_DoesNotOverwriteExistingTransactiontoSentrySpanProcessorTests: sets a transaction on the scope, starts a new root activity, callsOnStart, and asserts the scope still holds the original transaction.This is intentionally not bundled with #2399 (the
AutoSetScopeTransactionsopt-in), per the issue's notes.