Skip to content

fix(otel): only set Scope.Transaction in SentrySpanProcessor when null#5337

Open
tsushanth wants to merge 1 commit into
getsentry:mainfrom
tsushanth:fix/span-processor-overwrite-transaction
Open

fix(otel): only set Scope.Transaction in SentrySpanProcessor when null#5337
tsushanth wants to merge 1 commit into
getsentry:mainfrom
tsushanth:fix/span-processor-overwrite-transaction

Conversation

@tsushanth

Copy link
Copy Markdown

Fixes #5314.

Problem

SentrySpanProcessor.OnStart calls ConfigureScope unconditionally:

_hub.ConfigureScope(static (scope, transaction) => scope.Transaction = transaction, transaction);

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 ConfigureScope lambda:

_hub.ConfigureScope(static (scope, transaction) =>
{
    if (scope.Transaction is null)
    {
        scope.Transaction = transaction;
    }
}, transaction);

Scope.Transaction is only set when it is currently null; an already-present transaction is left unchanged.

Test

Added OnStart_WithExistingTransactionOnScope_DoesNotOverwriteExistingTransaction to SentrySpanProcessorTests: sets a transaction on the scope, starts a new root activity, calls OnStart, and asserts the scope still holds the original transaction.

This is intentionally not bundled with #2399 (the AutoSetScopeTransactions opt-in), per the issue's notes.

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
@github-actions github-actions Bot added the risk: low PR risk score: low label Jul 3, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8a3f87c. Configure here.

Comment on lines +179 to +185
_hub.ConfigureScope(static (scope, transaction) =>
{
if (scope.Transaction is null)
{
scope.Transaction = transaction;
}
}, transaction);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

@Flash0ver Flash0ver Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: test was neither run nor built

  • there is no ITransaction type
  • this project does not have Nullable Reference Types enabled

Comment on lines +1016 to +1017
[Fact]
public void OnStart_WithExistingTransactionOnScope_DoesNotOverwriteExistingTransaction()

@Flash0ver Flash0ver Jul 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: test is ineffective

Currently, this test does not actually cover the modification, and does not cover the modified method SentrySpanProcessor.CreateRootSpan.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment on lines +1019 to +1021
// 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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, Act is missing for the Act portion of the test
  • Assert: the because string 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.

Comment on lines +175 to +178
// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +175 to +178
// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@Flash0ver

Copy link
Copy Markdown
Member

Thank you very much, @tsushanth, for your contribution to the Sentry .NET SDK!
I left some comments about the test-case, and commented on the comments 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: low PR risk score: low

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenTelemetry SpanProcessor should only set Scope.Transaction when it is null

2 participants