From a33139fe15d01e5a91a7b306496bebb6c348c2a2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 29 Oct 2026 16:48:25 +0100 Subject: [PATCH 1/5] fix(core): Default trace context status to OK Active spans usually have no status until they finish, which causes trace contexts attached to events to appear as unknown. Default copied trace contexts to OK without mutating the running span, while preserving explicitly set statuses. Fixes GH-4769 --- sentry/src/main/java/io/sentry/Scope.java | 4 +-- .../src/main/java/io/sentry/SentryClient.java | 8 +++--- .../src/main/java/io/sentry/SpanContext.java | 12 +++++++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 10 ++++++-- sentry/src/test/java/io/sentry/ScopesTest.kt | 21 ++++++++++++---- .../test/java/io/sentry/SentryClientTest.kt | 12 +++++++-- .../test/java/io/sentry/SpanContextTest.kt | 25 +++++++++++++++++++ 7 files changed, 77 insertions(+), 15 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index ec98cc5fcaf..61e6826421e 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -300,7 +300,7 @@ public void setTransaction(final @Nullable ITransaction transaction) { for (final IScopeObserver observer : options.getScopeObservers()) { if (transaction != null) { observer.setTransaction(transaction.getName()); - observer.setTrace(transaction.getSpanContext(), this); + observer.setTrace(transaction.getSpanContext().withDefaultStatus(), this); } else { observer.setTransaction(null); observer.setTrace(null, this); @@ -1246,7 +1246,7 @@ public void assignTraceContext(final @NotNull SentryEvent event) { if (event.getContexts().getTrace() == null && spanWeakRef != null) { final ISpan span = spanWeakRef.get(); if (span != null) { - event.getContexts().setTrace(span.getSpanContext()); + event.getContexts().setTrace(span.getSpanContext().withDefaultStatus()); } } final String transactionName = pair.getSecond(); diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 9c019df1f58..1b8bef31ac3 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1442,7 +1442,7 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri .getContexts() .setTrace(TransactionContext.fromPropagationContext(scope.getPropagationContext())); } else { - event.getContexts().setTrace(span.getSpanContext()); + event.getContexts().setTrace(span.getSpanContext().withDefaultStatus()); } } @@ -1487,7 +1487,7 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri .getContexts() .setTrace(TransactionContext.fromPropagationContext(scope.getPropagationContext())); } else { - event.getContexts().setTrace(span.getSpanContext()); + event.getContexts().setTrace(span.getSpanContext().withDefaultStatus()); } } @@ -1505,7 +1505,7 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri .getContexts() .setTrace(TransactionContext.fromPropagationContext(scope.getPropagationContext())); } else { - checkIn.getContexts().setTrace(span.getSpanContext()); + checkIn.getContexts().setTrace(span.getSpanContext().withDefaultStatus()); } } } @@ -1546,7 +1546,7 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri .getContexts() .setTrace(TransactionContext.fromPropagationContext(scope.getPropagationContext())); } else { - replayEvent.getContexts().setTrace(span.getSpanContext()); + replayEvent.getContexts().setTrace(span.getSpanContext().withDefaultStatus()); } } } diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index e260062147f..a314556a96c 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -194,6 +194,18 @@ public SpanId getParentSpanId() { return status; } + @NotNull + SpanContext withDefaultStatus() { + if (getStatus() != null) { + return this; + } + + final @NotNull SpanContext traceContext = new SpanContext(this); + traceContext.setOrigin(getOrigin()); + traceContext.setStatus(SpanStatus.OK); + return traceContext; + } + public @NotNull Map getTags() { return tags; } diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index 69f19b6d421..119455914fb 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -849,13 +849,19 @@ class ScopeTest { val observer = mock() val options = SentryOptions().apply { addScopeObserver(observer) } val scope = Scope(options) + val spanContext = SpanContext("ui.load") scope.transaction = mock { whenever(mock.name).thenReturn("main") - whenever(mock.spanContext).thenReturn(SpanContext("ui.load")) + whenever(mock.spanContext).thenReturn(spanContext) } verify(observer).setTransaction(eq("main")) - verify(observer).setTrace(argThat { operation == "ui.load" }, eq(scope)) + verify(observer) + .setTrace( + argThat { operation == "ui.load" && status == SpanStatus.OK }, + eq(scope), + ) + assertThat(spanContext.status).isNull() } @Test diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index cdf9919a275..fd6ef367bfb 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -415,7 +415,7 @@ class ScopesTest { val hints = HintUtils.createWithTypeCheckHint({}) sut.captureEvent(event, hints) - assertEquals(span.spanContext, event.contexts.trace) + assertTraceContextDefaultsToOk(span.spanContext, event.contexts.trace) verify(mockClient).captureEvent(eq(event), any(), eq(hints)) } @@ -431,7 +431,7 @@ class ScopesTest { val hints = HintUtils.createWithTypeCheckHint({}) sut.captureEvent(event, hints) - assertEquals(span.spanContext, event.contexts.trace) + assertTraceContextDefaultsToOk(span.spanContext, event.contexts.trace) verify(mockClient).captureEvent(eq(event), any(), eq(hints)) } @@ -448,7 +448,7 @@ class ScopesTest { val hints = HintUtils.createWithTypeCheckHint({}) sut.captureEvent(event, hints) - assertEquals(span.spanContext, event.contexts.trace) + assertTraceContextDefaultsToOk(span.spanContext, event.contexts.trace) verify(mockClient).captureEvent(eq(event), any(), eq(hints)) } @@ -729,7 +729,7 @@ class ScopesTest { verify(mockClient) .captureEvent( check { - assertEquals(span.spanContext, it.contexts.trace) + assertTraceContextDefaultsToOk(span.spanContext, it.contexts.trace) assertEquals("tx-name", it.transaction) }, any(), @@ -2153,7 +2153,7 @@ class ScopesTest { verify(mockClient) .captureEvent( - check { assertEquals(span.spanContext, it.contexts.trace) }, + check { assertTraceContextDefaultsToOk(span.spanContext, it.contexts.trace) }, anyOrNull(), anyOrNull(), ) @@ -4442,6 +4442,17 @@ class ScopesTest { return fileHashFolder.absolutePath } + private fun assertTraceContextDefaultsToOk( + expectedContext: SpanContext, + actualContext: SpanContext?, + ) { + assertThat(actualContext).isNotNull() + assertThat(actualContext!!.traceId).isEqualTo(expectedContext.traceId) + assertThat(actualContext.spanId).isEqualTo(expectedContext.spanId) + assertThat(actualContext.status).isEqualTo(SpanStatus.OK) + assertThat(expectedContext.status).isNull() + } + private fun equalTraceContext(expectedContext: TraceContext?): TraceContext? { expectedContext ?: return eq(null) diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 926cf2d2884..cc8ccf1b78d 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -2239,7 +2239,10 @@ class SentryClientTest { transaction.finish() sut.captureEvent(event, scope) assertNotNull(event.contexts.trace) - assertEquals(transaction.root.spanContext, event.contexts.trace) + assertThat(event.contexts.trace!!.traceId).isEqualTo(transaction.root.spanContext.traceId) + assertThat(event.contexts.trace!!.spanId).isEqualTo(transaction.root.spanContext.spanId) + assertThat(event.contexts.trace!!.status).isEqualTo(SpanStatus.OK) + assertThat(transaction.root.spanContext.status).isNull() } @Test @@ -2252,7 +2255,10 @@ class SentryClientTest { val span = transaction.startChild("op") sut.captureEvent(event, scope) assertNotNull(event.contexts.trace) - assertEquals(span.spanContext, event.contexts.trace) + assertThat(event.contexts.trace!!.traceId).isEqualTo(span.spanContext.traceId) + assertThat(event.contexts.trace!!.spanId).isEqualTo(span.spanContext.spanId) + assertThat(event.contexts.trace!!.status).isEqualTo(SpanStatus.OK) + assertThat(span.spanContext.status).isNull() } @Test @@ -3288,6 +3294,8 @@ class SentryClientTest { assertEquals(spanContext.traceId, sentryEvent.contexts.trace!!.traceId) assertEquals(spanContext.spanId, sentryEvent.contexts.trace!!.spanId) + assertThat(sentryEvent.contexts.trace!!.status).isEqualTo(SpanStatus.OK) + assertThat(spanContext.status).isNull() assertNotEquals(scopePropagationContext.traceId, sentryEvent.contexts.trace!!.traceId) assertNotEquals(scopePropagationContext.spanId, sentryEvent.contexts.trace!!.spanId) } diff --git a/sentry/src/test/java/io/sentry/SpanContextTest.kt b/sentry/src/test/java/io/sentry/SpanContextTest.kt index aa941135e88..15f2618c28b 100644 --- a/sentry/src/test/java/io/sentry/SpanContextTest.kt +++ b/sentry/src/test/java/io/sentry/SpanContextTest.kt @@ -1,5 +1,6 @@ package io.sentry +import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -21,6 +22,30 @@ class SpanContextTest { assertNotNull(trace.data[SpanDataConvention.THREAD_NAME]) } + @Test + fun `withDefaultStatus returns a copy with the default status without modifying the original`() { + val spanContext = SpanContext("op").apply { origin = "auto.test" } + + val traceContext = spanContext.withDefaultStatus() + + assertThat(traceContext).isNotSameInstanceAs(spanContext) + assertThat(traceContext.traceId).isEqualTo(spanContext.traceId) + assertThat(traceContext.spanId).isEqualTo(spanContext.spanId) + assertThat(traceContext.origin).isEqualTo(spanContext.origin) + assertThat(traceContext.status).isEqualTo(SpanStatus.OK) + assertThat(spanContext.status).isNull() + } + + @Test + fun `withDefaultStatus preserves an explicitly set status`() { + val spanContext = SpanContext("op").apply { status = SpanStatus.INTERNAL_ERROR } + + val traceContext = spanContext.withDefaultStatus() + + assertThat(traceContext).isSameInstanceAs(spanContext) + assertThat(traceContext.status).isEqualTo(SpanStatus.INTERNAL_ERROR) + } + @Test fun `sets tag`() { val trace = SpanContext("op") From b597db4099b7ce38680f1cda5e78cbead56074b7 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 29 Oct 2026 16:51:03 +0100 Subject: [PATCH 2/5] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb80a6a295..13fbfa748a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Breaking Changes +- Default trace context status to `ok` when an active span has no explicit status ([#6418](https://github.com/getsentry/sentry-java/pull/6418)) - Remove the deprecated user feedback APIs ([#6337](https://github.com/getsentry/sentry-java/pull/6337)) - `UserFeedback` and `Sentry.captureUserFeedback(...)`: use `Sentry.feedback().capture(...)` with `io.sentry.protocol.Feedback` instead - `Sentry.captureFeedback(...)`: use `Sentry.feedback().capture(...)` instead From fee62a1d0ea524ea037c0362b051b004fa224f63 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 29 Oct 2026 17:47:25 +0100 Subject: [PATCH 3/5] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13fbfa748a7..41f882c75f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Breaking Changes +- Stop OpenTelemetry propagators from injecting tracing headers when an outgoing span has no URL ([#6419](https://github.com/getsentry/sentry-java/pull/6419)) + - Messaging producer spans continue to propagate trace context through message metadata - Default trace context status to `ok` when an active span has no explicit status ([#6418](https://github.com/getsentry/sentry-java/pull/6418)) - Remove the deprecated user feedback APIs ([#6337](https://github.com/getsentry/sentry-java/pull/6337)) - `UserFeedback` and `Sentry.captureUserFeedback(...)`: use `Sentry.feedback().capture(...)` with `io.sentry.protocol.Feedback` instead From 41e5de4a12a8a26d74e18bb5bac3684266574e92 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 29 Oct 2026 17:47:50 +0100 Subject: [PATCH 4/5] fix(core): Snapshot trace context before defaulting status Copy the span context before checking whether its status needs a default. This prevents a concurrent explicit status update from being overwritten with OK. Refs #4769 Co-Authored-By: Claude --- sentry/src/main/java/io/sentry/SpanContext.java | 8 +++----- sentry/src/test/java/io/sentry/SpanContextTest.kt | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index a314556a96c..53f3d57f4e0 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -196,13 +196,11 @@ public SpanId getParentSpanId() { @NotNull SpanContext withDefaultStatus() { - if (getStatus() != null) { - return this; - } - final @NotNull SpanContext traceContext = new SpanContext(this); traceContext.setOrigin(getOrigin()); - traceContext.setStatus(SpanStatus.OK); + if (traceContext.getStatus() == null) { + traceContext.setStatus(SpanStatus.OK); + } return traceContext; } diff --git a/sentry/src/test/java/io/sentry/SpanContextTest.kt b/sentry/src/test/java/io/sentry/SpanContextTest.kt index 15f2618c28b..7ebc7f70155 100644 --- a/sentry/src/test/java/io/sentry/SpanContextTest.kt +++ b/sentry/src/test/java/io/sentry/SpanContextTest.kt @@ -42,7 +42,7 @@ class SpanContextTest { val traceContext = spanContext.withDefaultStatus() - assertThat(traceContext).isSameInstanceAs(spanContext) + assertThat(traceContext).isNotSameInstanceAs(spanContext) assertThat(traceContext.status).isEqualTo(SpanStatus.INTERNAL_ERROR) } From fc5ba089c9c45276b1ff5eb15fd0e661721afe30 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 29 Oct 2026 17:48:42 +0100 Subject: [PATCH 5/5] Revert "changelog" This reverts commit fee62a1d0ea524ea037c0362b051b004fa224f63. --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f882c75f6..13fbfa748a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,6 @@ ### Breaking Changes -- Stop OpenTelemetry propagators from injecting tracing headers when an outgoing span has no URL ([#6419](https://github.com/getsentry/sentry-java/pull/6419)) - - Messaging producer spans continue to propagate trace context through message metadata - Default trace context status to `ok` when an active span has no explicit status ([#6418](https://github.com/getsentry/sentry-java/pull/6418)) - Remove the deprecated user feedback APIs ([#6337](https://github.com/getsentry/sentry-java/pull/6337)) - `UserFeedback` and `Sentry.captureUserFeedback(...)`: use `Sentry.feedback().capture(...)` with `io.sentry.protocol.Feedback` instead