From 2bac415e634a24623529317efc4e1a3bb3a9afe7 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 24 Jun 2026 16:33:17 +1200 Subject: [PATCH 1/2] fix: OpenTelemetry transactions for failed requests keep their route name and otel context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the OpenTelemetry integration, transactions are created by the SentrySpanProcessor on Activity start and only get their final name, operation and otel/response contexts on Activity end (parsed from http.route etc.). When a request threw an unhandled exception, Hub.CaptureEvent finished the scope's transaction as Aborted immediately after capturing the event — before the Activity ended — so the OTel enrichment was never applied. This caused failed requests to be reported under the raw activity name (e.g. "Microsoft.AspNetCore.Hosting.HttpRequestIn") with transaction source "custom" and no otel context, collapsing all failures into a single bucket with a 0% failure rate. Skip the early abort for OTel-instrumented transactions; the SentrySpanProcessor owns their lifecycle and finishes them with the correct name/operation/status when the Activity ends. Co-Authored-By: Claude Opus 4.8 --- src/Sentry/Internal/Hub.cs | 18 ++++++++++++++++-- test/Sentry.Tests/HubTests.cs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index dade93b89f..0086348f80 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -667,8 +667,22 @@ private SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Scope scope) { // Event contains a terminal exception -> finish any current transaction as aborted // Do this *after* the event was captured, so that the event is still linked to the transaction. - _options.LogDebug("Ending transaction as Aborted, due to unhandled exception."); - transaction.Finish(SpanStatus.Aborted); + // Exception: OpenTelemetry-instrumented transactions are owned and finished by the + // SentrySpanProcessor when the underlying Activity ends. That's also where the transaction + // name, operation and otel/response contexts get populated (from the http.route etc. + // attributes, which aren't available yet at this point). Finishing it early here would + // capture it before that enrichment, sending it with the raw activity name (e.g. + // "Microsoft.AspNetCore.Hosting.HttpRequestIn") and no otel context. See issue #5091. + if (transaction is IBaseTracer { IsOtelInstrumenter: true }) + { + _options.LogDebug( + "Not ending OpenTelemetry transaction as Aborted; it is finished by the SentrySpanProcessor."); + } + else + { + _options.LogDebug("Ending transaction as Aborted, due to unhandled exception."); + transaction.Finish(SpanStatus.Aborted); + } } return id; diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index c669a97060..eea49f6250 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -545,6 +545,36 @@ public void CaptureEvent_TerminalUnhandledException_AbortsActiveTransaction() transaction.IsFinished.Should().BeTrue(); } + [Fact] + public void CaptureEvent_TerminalUnhandledException_DoesNotAbortOpenTelemetryTransaction() + { + // OpenTelemetry-instrumented transactions are owned and finished by the SentrySpanProcessor + // when the underlying Activity ends (which is also where the transaction name, operation and + // otel/response contexts get populated). Finishing them early here would capture them with the + // raw activity name and no otel context. See https://github.com/getsentry/sentry-dotnet/issues/5091 + + // Arrange + _fixture.Options.TracesSampleRate = 1.0; + _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; + var hub = _fixture.GetSut(); + + var transactionContext = new TransactionContext("test", "operation") + { + Instrumenter = Instrumenter.OpenTelemetry + }; + var transaction = hub.StartTransaction(transactionContext); + hub.ConfigureScope(scope => scope.Transaction = transaction); + + var exception = new Exception("test"); + exception.SetSentryMechanism("test", handled: false, terminal: true); + + // Act + hub.CaptureEvent(new SentryEvent(exception)); + + // Assert + transaction.IsFinished.Should().BeFalse(); + } + [Fact] public void CaptureEvent_NonTerminalUnhandledException_DoesNotAbortActiveTransaction() { From 4b2eddb92e3fddabc5a3bac42a66cade78dbd7a9 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 24 Jun 2026 20:05:48 +1200 Subject: [PATCH 2/2] Update src/Sentry/Internal/Hub.cs --- src/Sentry/Internal/Hub.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 0086348f80..605970ecbb 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -667,12 +667,7 @@ private SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Scope scope) { // Event contains a terminal exception -> finish any current transaction as aborted // Do this *after* the event was captured, so that the event is still linked to the transaction. - // Exception: OpenTelemetry-instrumented transactions are owned and finished by the - // SentrySpanProcessor when the underlying Activity ends. That's also where the transaction - // name, operation and otel/response contexts get populated (from the http.route etc. - // attributes, which aren't available yet at this point). Finishing it early here would - // capture it before that enrichment, sending it with the raw activity name (e.g. - // "Microsoft.AspNetCore.Hosting.HttpRequestIn") and no otel context. See issue #5091. + // Skip for OpenTelemetry transactions - these get handled by the SpanProcessor instead. See https://github.com/getsentry/sentry-dotnet/pull/5310 if (transaction is IBaseTracer { IsOtelInstrumenter: true }) { _options.LogDebug(