From f15dbbbb1f12f5e95e812e00ca779bf4676972cb Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 22 Apr 2025 13:14:46 +0200 Subject: [PATCH 01/16] Support globalHubMode for OpenTelemetry --- .../api/sentry-opentelemetry-bootstrap.api | 6 +++++ .../opentelemetry/IOtelSpanWrapper.java | 6 +++++ .../OtelStrongRefSpanWrapper.java | 10 +++++++++ .../opentelemetry/SentryContextStorage.java | 12 ++++++++++ .../opentelemetry/SentryContextWrapper.java | 22 +++++++++++++++++-- .../opentelemetry/SentryWeakSpanStorage.java | 14 ++++++++++++ .../api/sentry-opentelemetry-core.api | 2 ++ .../sentry/opentelemetry/OtelSpanWrapper.java | 19 ++++++++++++++++ sentry/api/sentry.api | 1 + sentry/src/main/java/io/sentry/Sentry.java | 4 ++++ 10 files changed, 94 insertions(+), 2 deletions(-) diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api index 8e6b59b4be..8718c8761e 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -1,6 +1,7 @@ public abstract interface class io/sentry/opentelemetry/IOtelSpanWrapper : io/sentry/ISpan { public abstract fun getData ()Ljava/util/Map; public abstract fun getMeasurements ()Ljava/util/Map; + public abstract fun getOpenTelemetrySpan ()Lio/opentelemetry/api/trace/Span; public abstract fun getOpenTelemetrySpanAttributes ()Lio/opentelemetry/api/common/Attributes; public abstract fun getScopes ()Lio/sentry/IScopes; public abstract fun getTags ()Ljava/util/Map; @@ -8,6 +9,7 @@ public abstract interface class io/sentry/opentelemetry/IOtelSpanWrapper : io/se public abstract fun getTransactionName ()Ljava/lang/String; public abstract fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; public abstract fun isProfileSampled ()Ljava/lang/Boolean; + public abstract fun isRoot ()Z public abstract fun setTransactionName (Ljava/lang/String;)V public abstract fun setTransactionName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public abstract fun storeInContext (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Context; @@ -52,6 +54,7 @@ public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/ public fun getDescription ()Ljava/lang/String; public fun getFinishDate ()Lio/sentry/SentryDate; public fun getMeasurements ()Ljava/util/Map; + public fun getOpenTelemetrySpan ()Lio/opentelemetry/api/trace/Span; public fun getOpenTelemetrySpanAttributes ()Lio/opentelemetry/api/common/Attributes; public fun getOperation ()Ljava/lang/String; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; @@ -68,6 +71,7 @@ public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/ public fun isFinished ()Z public fun isNoOp ()Z public fun isProfileSampled ()Ljava/lang/Boolean; + public fun isRoot ()Z public fun isSampled ()Ljava/lang/Boolean; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V @@ -151,6 +155,7 @@ public final class io/sentry/opentelemetry/SentryContextStorage : io/opentelemet public fun (Lio/opentelemetry/context/ContextStorage;)V public fun attach (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Scope; public fun current ()Lio/opentelemetry/context/Context; + public fun root ()Lio/opentelemetry/context/Context; } public final class io/sentry/opentelemetry/SentryContextStorageProvider : io/opentelemetry/context/ContextStorageProvider { @@ -181,6 +186,7 @@ public final class io/sentry/opentelemetry/SentryOtelThreadLocalStorage : io/ope public final class io/sentry/opentelemetry/SentryWeakSpanStorage { public fun clear ()V public static fun getInstance ()Lio/sentry/opentelemetry/SentryWeakSpanStorage; + public fun getLastKnownUnfinishedRootSpan ()Lio/sentry/opentelemetry/IOtelSpanWrapper; public fun getSentrySpan (Lio/opentelemetry/api/trace/SpanContext;)Lio/sentry/opentelemetry/IOtelSpanWrapper; public fun storeSentrySpan (Lio/opentelemetry/api/trace/SpanContext;Lio/sentry/opentelemetry/IOtelSpanWrapper;)V } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java index 1eefc854a8..6e34f508ed 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java @@ -1,6 +1,7 @@ package io.sentry.opentelemetry; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.sentry.IScopes; import io.sentry.ISpan; @@ -52,4 +53,9 @@ public interface IOtelSpanWrapper extends ISpan { @ApiStatus.Internal @Nullable Attributes getOpenTelemetrySpanAttributes(); + + boolean isRoot(); + + @Nullable + Span getOpenTelemetrySpan(); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java index a4008a0128..2d9460a325 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java @@ -310,4 +310,14 @@ public void setContext(@Nullable String key, @Nullable Object context) { public @Nullable Attributes getOpenTelemetrySpanAttributes() { return delegate.getOpenTelemetrySpanAttributes(); } + + @Override + public boolean isRoot() { + return delegate.isRoot(); + } + + @Override + public @Nullable Span getOpenTelemetrySpan() { + return delegate.getOpenTelemetrySpan(); + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java index 4f3efa40c2..304920b2d4 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java @@ -3,6 +3,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.Scope; +import io.sentry.Sentry; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -38,4 +39,15 @@ public Scope attach(Context toAttach) { public Context current() { return contextStorage.current(); } + + @Override + public Context root() { + final @NotNull Context originalRoot = ContextStorage.super.root(); + + if (Sentry.isGlobalHubMode()) { + return SentryContextWrapper.wrap(originalRoot); + } + + return originalRoot; + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java index a0213bafe6..4978476fa2 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java @@ -20,9 +20,27 @@ private SentryContextWrapper(final @NotNull Context delegate) { this.delegate = delegate; } + @SuppressWarnings("unchecked") @Override - public V get(final @NotNull ContextKey contextKey) { - return delegate.get(contextKey); + public @Nullable V get(final @NotNull ContextKey contextKey) { + V result = delegate.get(contextKey); + if (Sentry.isGlobalHubMode()) { + if (result == null + || (result instanceof Span && !((Span) result).getSpanContext().isValid())) { + if (isOpentelemetrySpan(contextKey)) { + IOtelSpanWrapper sentrySpan = + SentryWeakSpanStorage.getInstance().getLastKnownUnfinishedRootSpan(); + if (sentrySpan != null) { + try { + return (V) sentrySpan.getOpenTelemetrySpan(); + } catch (Throwable t) { + return result; + } + } + } + } + } + return result; } @Override diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java index 5096c011e2..b215c06069 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java @@ -4,6 +4,7 @@ import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; import io.sentry.ISentryLifecycleToken; import io.sentry.util.AutoClosableReentrantLock; +import java.lang.ref.WeakReference; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -34,6 +35,8 @@ public final class SentryWeakSpanStorage { // weak keys, spawns a thread to clean up values that have been garbage collected private final @NotNull WeakConcurrentMap sentrySpans = new WeakConcurrentMap<>(true); + private volatile @NotNull WeakReference lastKnownRootSpan = + new WeakReference<>(null); private SentryWeakSpanStorage() {} @@ -44,6 +47,17 @@ private SentryWeakSpanStorage() {} public void storeSentrySpan( final @NotNull SpanContext otelSpan, final @NotNull IOtelSpanWrapper sentrySpan) { this.sentrySpans.put(otelSpan, sentrySpan); + if (sentrySpan.isRoot()) { + lastKnownRootSpan = new WeakReference<>(sentrySpan); + } + } + + public @Nullable IOtelSpanWrapper getLastKnownUnfinishedRootSpan() { + final @Nullable IOtelSpanWrapper span = lastKnownRootSpan.get(); + if (span != null && !span.isFinished()) { + return span; + } + return null; } @TestOnly diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api index f20ea0ab86..21489b4aa6 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api +++ b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api @@ -61,6 +61,7 @@ public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/opentelem public fun getDescription ()Ljava/lang/String; public fun getFinishDate ()Lio/sentry/SentryDate; public fun getMeasurements ()Ljava/util/Map; + public fun getOpenTelemetrySpan ()Lio/opentelemetry/api/trace/Span; public fun getOpenTelemetrySpanAttributes ()Lio/opentelemetry/api/common/Attributes; public fun getOperation ()Ljava/lang/String; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; @@ -77,6 +78,7 @@ public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/opentelem public fun isFinished ()Z public fun isNoOp ()Z public fun isProfileSampled ()Ljava/lang/Boolean; + public fun isRoot ()Z public fun isSampled ()Ljava/lang/Boolean; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java index bc78643fb9..aadf040e0b 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java @@ -515,6 +515,25 @@ public Map getMeasurements() { } } + @Override + public boolean isRoot() { + if (context.getParentSpanId() == null) { + return true; + } + + final @Nullable ReadWriteSpan readWriteSpan = span.get(); + if (readWriteSpan != null) { + return readWriteSpan.getParentSpanContext().isRemote(); + } + + return false; + } + + @Override + public @Nullable Span getOpenTelemetrySpan() { + return span.get(); + } + @SuppressWarnings("MustBeClosedChecker") @ApiStatus.Internal @Override diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index e4d5970f6d..4e39fbda97 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2529,6 +2529,7 @@ public final class io/sentry/Sentry { public static fun init (Ljava/lang/String;)V public static fun isCrashedLastRun ()Ljava/lang/Boolean; public static fun isEnabled ()Z + public static fun isGlobalHubMode ()Z public static fun isHealthy ()Z public static fun popScope ()V public static fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 70d4c7b380..421f296270 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1225,4 +1225,8 @@ public interface OptionsConfiguration { public static @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return getCurrentScopes().captureCheckIn(checkIn); } + + public static boolean isGlobalHubMode() { + return globalHubMode; + } } From 3c7772916e98ce3db136cdca60c1c3ea8086e37e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 22 Apr 2025 13:42:34 +0200 Subject: [PATCH 02/16] refactor check --- .../opentelemetry/SentryContextWrapper.java | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java index 4978476fa2..137129126a 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java @@ -20,24 +20,38 @@ private SentryContextWrapper(final @NotNull Context delegate) { this.delegate = delegate; } - @SuppressWarnings("unchecked") @Override public @Nullable V get(final @NotNull ContextKey contextKey) { - V result = delegate.get(contextKey); - if (Sentry.isGlobalHubMode()) { - if (result == null - || (result instanceof Span && !((Span) result).getSpanContext().isValid())) { - if (isOpentelemetrySpan(contextKey)) { - IOtelSpanWrapper sentrySpan = - SentryWeakSpanStorage.getInstance().getLastKnownUnfinishedRootSpan(); - if (sentrySpan != null) { - try { - return (V) sentrySpan.getOpenTelemetrySpan(); - } catch (Throwable t) { - return result; - } - } - } + final @Nullable V result = delegate.get(contextKey); + if (shouldReturnRootSpanInstead(contextKey, result)) { + return returnUnfinishedRootSpanIfAvailable(result); + } + return result; + } + + private boolean shouldReturnRootSpanInstead( + final @NotNull ContextKey contextKey, final @Nullable V result) { + if (!Sentry.isGlobalHubMode()) { + return false; + } + if (!isOpentelemetrySpan(contextKey)) { + return false; + } + if (result == null) { + return true; + } + return result instanceof Span && !((Span) result).getSpanContext().isValid(); + } + + @SuppressWarnings("unchecked") + private @Nullable V returnUnfinishedRootSpanIfAvailable(final @Nullable V result) { + final @Nullable IOtelSpanWrapper sentrySpan = + SentryWeakSpanStorage.getInstance().getLastKnownUnfinishedRootSpan(); + if (sentrySpan != null) { + try { + return (V) sentrySpan.getOpenTelemetrySpan(); + } catch (Throwable t) { + return result; } } return result; From 20c7fef2fcb0bb04331dd7a0b6f8764c79b4cd1c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 22 Apr 2025 13:44:35 +0200 Subject: [PATCH 03/16] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df43f3704..2dcf9e8296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Features - Add `SentryWrapper.wrapRunnable` to wrap `Runnable` for use with Sentry ([#4332](https://github.com/getsentry/sentry-java/pull/4332)) +- Support `globalHubMode` for OpenTelemetry + - Sentry now adds OpenTelemetry spans without a parent to the last known unfinished root span (transaction) + - Previously those spans would end up in Sentry as separate transactions ### Fixes From 049f59fc2e26a5a60323ee244f86a06675fb973b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 22 Apr 2025 12:37:33 +0000 Subject: [PATCH 04/16] release: 8.9.0-alpha.1 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dcf9e8296..249dc24f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 8.9.0-alpha.1 ### Features diff --git a/gradle.properties b/gradle.properties index 71e3511fb2..9b8992c159 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ org.gradle.workers.max=2 android.useAndroidX=true # Release information -versionName=8.8.0 +versionName=8.9.0-alpha.1 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android From 46170d46acd769819b60e17f12d0f8beb26a1e47 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 23 Apr 2025 13:55:05 +0200 Subject: [PATCH 05/16] add tests --- .../opentelemetry/SentryWeakSpanStorage.java | 1 + .../build.gradle.kts | 1 + .../test/kotlin/SentryContextWrapperTest.kt | 164 ++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java index b215c06069..ebe38480bd 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java @@ -63,5 +63,6 @@ public void storeSentrySpan( @TestOnly public void clear() { sentrySpans.clear(); + lastKnownRootSpan.clear(); } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts index 05c194d860..df06b30352 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(Config.TestLibs.kotlinTestJunit) testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) testImplementation(Config.TestLibs.awaitility) testImplementation(Config.Libs.OpenTelemetry.otelSdk) diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt new file mode 100644 index 0000000000..003da75ae5 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt @@ -0,0 +1,164 @@ +package io.sentry.opentelemetry + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanContext +import io.opentelemetry.api.trace.TraceFlags +import io.opentelemetry.api.trace.TraceState +import io.opentelemetry.context.Context +import io.opentelemetry.context.ContextKey +import io.opentelemetry.sdk.trace.ReadWriteSpan +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.sdk.trace.data.StatusData +import io.sentry.Sentry +import io.sentry.SentryNanotimeDate +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertNull +import kotlin.test.assertSame + +class SentryContextWrapperTest { + + val spanStorage = SentryWeakSpanStorage.getInstance() + + @BeforeTest + fun setup() { + spanStorage.clear() + } + + @AfterTest + fun cleanup() { + spanStorage.clear() + Sentry.close() + } + + @Test + fun `returns null if no transaction is available`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val c = SentryContextWrapper.wrap(Context.root()) + val returnedSpan = Span.fromContextOrNull(c) + assertNull(returnedSpan) + } + + @Test + fun `returns available transaction`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val otelSpan = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + val c = SentryContextWrapper.wrap(Context.root()) + val returnedSpan = Span.fromContextOrNull(c) + assertSame(otelSpan, returnedSpan) + } + + @Test + fun `returns available transaction if span in context is invalid`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val otelSpan = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + val nonWrappedContext = Context.root() + val wrappedContext = SentryContextWrapper.wrap(Span.getInvalid().storeInContext(nonWrappedContext)) + val returnedSpan = Span.fromContextOrNull(wrappedContext) + assertSame(otelSpan, returnedSpan) + } + + @Test + fun `returns span from context if valid`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val otelSpan = createOtelSpan() + val otelSpanInContext = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + val nonWrappedContext = Context.root() + val wrappedContext = SentryContextWrapper.wrap(otelSpanInContext.storeInContext(nonWrappedContext)) + val returnedSpan = Span.fromContextOrNull(wrappedContext) + assertSame(otelSpanInContext, returnedSpan) + } + + @Test + fun `returns null if transaction is available but globalHubMode is false`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = false + } + + val otelSpan = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + val c = SentryContextWrapper.wrap(Context.root()) + val returnedSpan = Span.fromContextOrNull(c) + assertNull(returnedSpan) + } + + @Test + fun `returns null if available transaction is already finished`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val otelSpan = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + sentrySpan.finish() + + val c = SentryContextWrapper.wrap(Context.root()) + val returnedSpan = Span.fromContextOrNull(c) + assertSame(otelSpan, returnedSpan) + } + + private fun createSentrySpan(otelSpan: ReadWriteSpan): OtelSpanWrapper { + val scopes = Sentry.getCurrentScopes() + return OtelSpanWrapper(otelSpan, scopes, SentryNanotimeDate(), null, null, null, null) + } + + private fun createOtelSpan(): ReadWriteSpan { + val otelSpanContext = SpanContext.create( + "f9118105af4a2d42b4124532cd1065ff", + "424cffc8f94feeee", + TraceFlags.getSampled(), + TraceState.getDefault() + ) + val otelSpan = mock() + whenever(otelSpan.spanContext).thenReturn(otelSpanContext) + whenever(otelSpan.name).thenReturn("some-name") + + val spanData = mock() + whenever(spanData.status).thenReturn(StatusData.ok()) + whenever(otelSpan.toSpanData()).thenReturn(spanData) + + whenever(otelSpan.storeInContext(any())).thenCallRealMethod() + + return otelSpan + } + +} From 126799366a7a90de626767c7a04ed0cd60cf8871 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 23 Apr 2025 13:56:46 +0200 Subject: [PATCH 06/16] revert release changes --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 249dc24f11..2dcf9e8296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 8.9.0-alpha.1 +## Unreleased ### Features diff --git a/gradle.properties b/gradle.properties index 9b8992c159..71e3511fb2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ org.gradle.workers.max=2 android.useAndroidX=true # Release information -versionName=8.9.0-alpha.1 +versionName=8.8.0 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android From 66c10d96cc84bb27d24968583df2c85195b9c417 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 23 Apr 2025 11:58:37 +0000 Subject: [PATCH 07/16] Format code --- .../src/test/kotlin/SentryContextWrapperTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt index 003da75ae5..ad686e3313 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt @@ -5,7 +5,6 @@ import io.opentelemetry.api.trace.SpanContext import io.opentelemetry.api.trace.TraceFlags import io.opentelemetry.api.trace.TraceState import io.opentelemetry.context.Context -import io.opentelemetry.context.ContextKey import io.opentelemetry.sdk.trace.ReadWriteSpan import io.opentelemetry.sdk.trace.data.SpanData import io.opentelemetry.sdk.trace.data.StatusData @@ -160,5 +159,4 @@ class SentryContextWrapperTest { return otelSpan } - } From f8a9c458a4a4092c3befac250645d799beb90298 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 24 Apr 2025 16:00:18 +0200 Subject: [PATCH 08/16] new option ignoreStandaloneClientSpans; prefer spans created via Sentry API as fallback root --- .../InternalSemanticAttributes.java | 1 + .../opentelemetry/SentryWeakSpanStorage.java | 25 ++++++++++++++++++- .../sentry/opentelemetry/SentrySampler.java | 8 ++++-- .../main/java/io/sentry/SentryOptions.java | 12 +++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java index 4795401266..19062855a3 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java @@ -21,4 +21,5 @@ public final class InternalSemanticAttributes { public static final AttributeKey BAGGAGE = AttributeKey.stringKey("sentry.baggage"); public static final AttributeKey BAGGAGE_MUTABLE = AttributeKey.booleanKey("sentry.baggage_mutable"); + public static final AttributeKey CREATED_VIA_SENTRY_API = AttributeKey.booleanKey("sentry.is_created_via_sentry_api"); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java index ebe38480bd..c4d87cdc89 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java @@ -1,5 +1,7 @@ package io.sentry.opentelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; import io.sentry.ISentryLifecycleToken; @@ -46,12 +48,33 @@ private SentryWeakSpanStorage() {} public void storeSentrySpan( final @NotNull SpanContext otelSpan, final @NotNull IOtelSpanWrapper sentrySpan) { + System.out.println("storing span: " + sentrySpan.getOperation()); this.sentrySpans.put(otelSpan, sentrySpan); - if (sentrySpan.isRoot()) { + if (shouldStoreSpanAsRootSpan(sentrySpan)) { + System.out.println("storing span as last known root: " + sentrySpan.getOperation()); lastKnownRootSpan = new WeakReference<>(sentrySpan); } } + private boolean shouldStoreSpanAsRootSpan(final @NotNull IOtelSpanWrapper sentrySpan) { + if (!sentrySpan.isRoot()) { + return false; + } + + final @Nullable IOtelSpanWrapper previousRootSpan = getLastKnownUnfinishedRootSpan(); + if (previousRootSpan == null) { + return true; + } + + final @Nullable Attributes attributes = previousRootSpan.getOpenTelemetrySpanAttributes(); + if (attributes == null) { + return true; + } + + final @Nullable Boolean isCreatedViaSentryApi = attributes.get(InternalSemanticAttributes.CREATED_VIA_SENTRY_API); + return isCreatedViaSentryApi == true; + } + public @Nullable IOtelSpanWrapper getLastKnownUnfinishedRootSpan() { final @Nullable IOtelSpanWrapper span = lastKnownRootSpan.get(); if (span != null && !span.isFinished()) { diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java index 5493ba033c..4c362f10a0 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java @@ -66,7 +66,7 @@ public SamplingResult shouldSample( if (samplingDecision != null) { return new SentrySamplingResult(samplingDecision); } else { - return handleRootOtelSpan(traceId, parentContext, attributes); + return handleRootOtelSpan(traceId, parentContext, attributes, spanKind); } } } @@ -74,10 +74,14 @@ public SamplingResult shouldSample( private @NotNull SamplingResult handleRootOtelSpan( final @NotNull String traceId, final @NotNull Context parentContext, - final @NotNull Attributes attributes) { + final @NotNull Attributes attributes, + final @NotNull SpanKind spanKind) { if (!scopes.getOptions().isTracingEnabled()) { return SamplingResult.create(SamplingDecision.RECORD_ONLY); } + if (scopes.getOptions().isIgnoreStandaloneClientSpans() && SpanKind.CLIENT.equals(spanKind)) { + return SamplingResult.create(SamplingDecision.DROP); + } @Nullable Baggage baggage = null; @Nullable SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 54e37a405c..9b1205d749 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -568,6 +568,8 @@ public class SentryOptions { private @NotNull ISocketTagger socketTagger = NoOpSocketTagger.getInstance(); + private boolean ignoreStandaloneClientSpans = false; + /** * Adds an event processor * @@ -2847,6 +2849,16 @@ public void setSocketTagger(final @Nullable ISocketTagger socketTagger) { this.socketTagger = socketTagger != null ? socketTagger : NoOpSocketTagger.getInstance(); } + @ApiStatus.Experimental + public void setIgnoreStandaloneClientSpans(final boolean ignoreStandaloneClientSpans) { + this.ignoreStandaloneClientSpans = ignoreStandaloneClientSpans; + } + + @ApiStatus.Experimental + public boolean isIgnoreStandaloneClientSpans() { + return ignoreStandaloneClientSpans; + } + /** * Load the lazy fields. Useful to load in the background, so that results are already cached. DO * NOT CALL THIS METHOD ON THE MAIN THREAD. From c0720004de9429c3e07dea5b1b901b3e504db7a8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 24 Apr 2025 16:03:36 +0200 Subject: [PATCH 09/16] update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dcf9e8296..a1d75ce605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,12 @@ ### Features - Add `SentryWrapper.wrapRunnable` to wrap `Runnable` for use with Sentry ([#4332](https://github.com/getsentry/sentry-java/pull/4332)) -- Support `globalHubMode` for OpenTelemetry +- Support `globalHubMode` for OpenTelemetry ([#4349](https://github.com/getsentry/sentry-java/pull/4349)) - Sentry now adds OpenTelemetry spans without a parent to the last known unfinished root span (transaction) - Previously those spans would end up in Sentry as separate transactions + - Spans created via Sentry API are preferred over those created through OpenTelemetry API or auto instrumentation +- New option `ignoreStandaloneClientSpans` that prevents Sentry from creating transactions for OpenTelemetry spans with kind `CLIENT` ([#4349](https://github.com/getsentry/sentry-java/pull/4349)) + - Defaults to `false` meaning standalone OpenTelemetry spans with kind `CLIENT` will be turned into Sentry transactions ### Fixes From 0ba317ad67bf06e280d013534bc41646477d482a Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 24 Apr 2025 14:05:21 +0000 Subject: [PATCH 10/16] Format code --- .../io/sentry/opentelemetry/InternalSemanticAttributes.java | 3 ++- .../java/io/sentry/opentelemetry/SentryWeakSpanStorage.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java index 19062855a3..a60ca9bab0 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java @@ -21,5 +21,6 @@ public final class InternalSemanticAttributes { public static final AttributeKey BAGGAGE = AttributeKey.stringKey("sentry.baggage"); public static final AttributeKey BAGGAGE_MUTABLE = AttributeKey.booleanKey("sentry.baggage_mutable"); - public static final AttributeKey CREATED_VIA_SENTRY_API = AttributeKey.booleanKey("sentry.is_created_via_sentry_api"); + public static final AttributeKey CREATED_VIA_SENTRY_API = + AttributeKey.booleanKey("sentry.is_created_via_sentry_api"); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java index c4d87cdc89..c737256f2f 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java @@ -1,7 +1,6 @@ package io.sentry.opentelemetry; import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; import io.sentry.ISentryLifecycleToken; @@ -71,7 +70,8 @@ private boolean shouldStoreSpanAsRootSpan(final @NotNull IOtelSpanWrapper sentry return true; } - final @Nullable Boolean isCreatedViaSentryApi = attributes.get(InternalSemanticAttributes.CREATED_VIA_SENTRY_API); + final @Nullable Boolean isCreatedViaSentryApi = + attributes.get(InternalSemanticAttributes.CREATED_VIA_SENTRY_API); return isCreatedViaSentryApi == true; } From 0f63cb1e9c4bbe7b3c2e32b9eda68456cbf2b7a7 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 24 Apr 2025 16:13:57 +0200 Subject: [PATCH 11/16] move changelog --- CHANGELOG.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04315264c0..f18bd14b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Features + +- Support `globalHubMode` for OpenTelemetry ([#4349](https://github.com/getsentry/sentry-java/pull/4349)) + - Sentry now adds OpenTelemetry spans without a parent to the last known unfinished root span (transaction) + - Previously those spans would end up in Sentry as separate transactions + - Spans created via Sentry API are preferred over those created through OpenTelemetry API or auto instrumentation +- New option `ignoreStandaloneClientSpans` that prevents Sentry from creating transactions for OpenTelemetry spans with kind `CLIENT` ([#4349](https://github.com/getsentry/sentry-java/pull/4349)) + - Defaults to `false` meaning standalone OpenTelemetry spans with kind `CLIENT` will be turned into Sentry transactions + ### Fixes - Update profile chunk rate limit and client report ([#4353](https://github.com/getsentry/sentry-java/pull/4353)) @@ -11,12 +20,6 @@ ### Features - Add `SentryWrapper.wrapRunnable` to wrap `Runnable` for use with Sentry ([#4332](https://github.com/getsentry/sentry-java/pull/4332)) -- Support `globalHubMode` for OpenTelemetry ([#4349](https://github.com/getsentry/sentry-java/pull/4349)) - - Sentry now adds OpenTelemetry spans without a parent to the last known unfinished root span (transaction) - - Previously those spans would end up in Sentry as separate transactions - - Spans created via Sentry API are preferred over those created through OpenTelemetry API or auto instrumentation -- New option `ignoreStandaloneClientSpans` that prevents Sentry from creating transactions for OpenTelemetry spans with kind `CLIENT` ([#4349](https://github.com/getsentry/sentry-java/pull/4349)) - - Defaults to `false` meaning standalone OpenTelemetry spans with kind `CLIENT` will be turned into Sentry transactions ### Fixes From 61e6d3e61924ac47d2205b166cee3da3c0858008 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 24 Apr 2025 16:32:44 +0200 Subject: [PATCH 12/16] api --- .../api/sentry-opentelemetry-bootstrap.api | 1 + sentry/api/sentry.api | 2 ++ 2 files changed, 3 insertions(+) diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api index 8718c8761e..f047d13d4e 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -18,6 +18,7 @@ public abstract interface class io/sentry/opentelemetry/IOtelSpanWrapper : io/se public final class io/sentry/opentelemetry/InternalSemanticAttributes { public static final field BAGGAGE Lio/opentelemetry/api/common/AttributeKey; public static final field BAGGAGE_MUTABLE Lio/opentelemetry/api/common/AttributeKey; + public static final field CREATED_VIA_SENTRY_API Lio/opentelemetry/api/common/AttributeKey; public static final field IS_REMOTE_PARENT Lio/opentelemetry/api/common/AttributeKey; public static final field PARENT_SAMPLED Lio/opentelemetry/api/common/AttributeKey; public static final field PROFILE_SAMPLED Lio/opentelemetry/api/common/AttributeKey; diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index d9880a27d2..461902f461 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3157,6 +3157,7 @@ public class io/sentry/SentryOptions { public fun isEnabled ()Z public fun isForceInit ()Z public fun isGlobalHubMode ()Ljava/lang/Boolean; + public fun isIgnoreStandaloneClientSpans ()Z public fun isPrintUncaughtStackTrace ()Z public fun isProfilingEnabled ()Z public fun isSendClientReports ()Z @@ -3217,6 +3218,7 @@ public class io/sentry/SentryOptions { public fun setGestureTargetLocators (Ljava/util/List;)V public fun setGlobalHubMode (Ljava/lang/Boolean;)V public fun setIdleTimeout (Ljava/lang/Long;)V + public fun setIgnoreStandaloneClientSpans (Z)V public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setIgnoredErrors (Ljava/util/List;)V public fun setIgnoredSpanOrigins (Ljava/util/List;)V From 965c00b973c99077c10fcdc668a06df803ec9d76 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 24 Apr 2025 17:10:09 +0200 Subject: [PATCH 13/16] fix NPE --- .../java/io/sentry/opentelemetry/SentryWeakSpanStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java index c737256f2f..b14ea0f77f 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java @@ -72,7 +72,7 @@ private boolean shouldStoreSpanAsRootSpan(final @NotNull IOtelSpanWrapper sentry final @Nullable Boolean isCreatedViaSentryApi = attributes.get(InternalSemanticAttributes.CREATED_VIA_SENTRY_API); - return isCreatedViaSentryApi == true; + return isCreatedViaSentryApi != null && isCreatedViaSentryApi == true; } public @Nullable IOtelSpanWrapper getLastKnownUnfinishedRootSpan() { From a18ffa830263a4be6a8713a57083cb31f7eeda87 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 24 Apr 2025 15:34:03 +0000 Subject: [PATCH 14/16] release: 8.10.0-alpha.1 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f18bd14b53..ea961cf57c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 8.10.0-alpha.1 ### Features diff --git a/gradle.properties b/gradle.properties index c919781bce..2de3f385f1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ org.gradle.workers.max=2 android.useAndroidX=true # Release information -versionName=8.9.0 +versionName=8.10.0-alpha.1 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android From 4d4f6a8cf4126ce0c54a03d063b6df34c6b72364 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 29 Apr 2025 15:14:44 +0200 Subject: [PATCH 15/16] store placeholder span in root context --- .../api/sentry-opentelemetry-bootstrap.api | 14 ++++ .../opentelemetry/SentryContextStorage.java | 5 +- .../opentelemetry/SentryContextWrapper.java | 3 + .../SentryOtelGlobalHubModeSpan.java | 78 +++++++++++++++++++ .../test/kotlin/SentryContextWrapperTest.kt | 5 +- 5 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelGlobalHubModeSpan.java diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api index f047d13d4e..fbff8db2c1 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -171,6 +171,20 @@ public final class io/sentry/opentelemetry/SentryContextWrapper : io/opentelemet public static fun wrap (Lio/opentelemetry/context/Context;)Lio/sentry/opentelemetry/SentryContextWrapper; } +public final class io/sentry/opentelemetry/SentryOtelGlobalHubModeSpan : io/opentelemetry/api/trace/Span { + public fun ()V + public fun addEvent (Ljava/lang/String;Lio/opentelemetry/api/common/Attributes;)Lio/opentelemetry/api/trace/Span; + public fun addEvent (Ljava/lang/String;Lio/opentelemetry/api/common/Attributes;JLjava/util/concurrent/TimeUnit;)Lio/opentelemetry/api/trace/Span; + public fun end ()V + public fun end (JLjava/util/concurrent/TimeUnit;)V + public fun getSpanContext ()Lio/opentelemetry/api/trace/SpanContext; + public fun isRecording ()Z + public fun recordException (Ljava/lang/Throwable;Lio/opentelemetry/api/common/Attributes;)Lio/opentelemetry/api/trace/Span; + public fun setAttribute (Lio/opentelemetry/api/common/AttributeKey;Ljava/lang/Object;)Lio/opentelemetry/api/trace/Span; + public fun setStatus (Lio/opentelemetry/api/trace/StatusCode;Ljava/lang/String;)Lio/opentelemetry/api/trace/Span; + public fun updateName (Ljava/lang/String;)Lio/opentelemetry/api/trace/Span; +} + public final class io/sentry/opentelemetry/SentryOtelKeys { public static final field SENTRY_BAGGAGE_KEY Lio/opentelemetry/context/ContextKey; public static final field SENTRY_SCOPES_KEY Lio/opentelemetry/context/ContextKey; diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java index 304920b2d4..a47460593f 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java @@ -42,10 +42,11 @@ public Context current() { @Override public Context root() { - final @NotNull Context originalRoot = ContextStorage.super.root(); + final @NotNull Context originalRoot = contextStorage.root(); if (Sentry.isGlobalHubMode()) { - return SentryContextWrapper.wrap(originalRoot); + return new SentryOtelGlobalHubModeSpan() + .storeInContext(SentryContextWrapper.wrap(originalRoot)); } return originalRoot; diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java index 137129126a..429d841547 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java @@ -40,6 +40,9 @@ private boolean shouldReturnRootSpanInstead( if (result == null) { return true; } + if (result instanceof SentryOtelGlobalHubModeSpan) { + return true; + } return result instanceof Span && !((Span) result).getSpanContext().isValid(); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelGlobalHubModeSpan.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelGlobalHubModeSpan.java new file mode 100644 index 0000000000..213cb5519c --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelGlobalHubModeSpan.java @@ -0,0 +1,78 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.StatusCode; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Experimental +public final class SentryOtelGlobalHubModeSpan implements Span { + + private @NotNull Span getOtelSpan() { + final @Nullable IOtelSpanWrapper lastKnownUnfinishedRootSpan = + SentryWeakSpanStorage.getInstance().getLastKnownUnfinishedRootSpan(); + if (lastKnownUnfinishedRootSpan != null) { + final @Nullable Span openTelemetrySpan = lastKnownUnfinishedRootSpan.getOpenTelemetrySpan(); + if (openTelemetrySpan != null) { + return openTelemetrySpan; + } + } + + return Span.getInvalid(); + } + + @Override + public Span setAttribute(AttributeKey key, T value) { + return getOtelSpan().setAttribute(key, value); + } + + @Override + public Span addEvent(String name, Attributes attributes) { + return getOtelSpan().addEvent(name, attributes); + } + + @Override + public Span addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { + return getOtelSpan().addEvent(name, attributes, timestamp, unit); + } + + @Override + public Span setStatus(StatusCode statusCode, String description) { + return getOtelSpan().setStatus(statusCode, description); + } + + @Override + public Span recordException(Throwable exception, Attributes additionalAttributes) { + return getOtelSpan().recordException(exception, additionalAttributes); + } + + @Override + public Span updateName(String name) { + return getOtelSpan().updateName(name); + } + + @Override + public void end() { + getOtelSpan().end(); + } + + @Override + public void end(long timestamp, TimeUnit unit) { + getOtelSpan().end(timestamp, unit); + } + + @Override + public SpanContext getSpanContext() { + return getOtelSpan().getSpanContext(); + } + + @Override + public boolean isRecording() { + return getOtelSpan().isRecording(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt index ad686e3313..2d61c833a0 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt @@ -18,6 +18,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertNull import kotlin.test.assertSame +import kotlin.test.assertTrue class SentryContextWrapperTest { @@ -35,7 +36,7 @@ class SentryContextWrapperTest { } @Test - fun `returns null if no transaction is available`() { + fun `returns global hub span if no transaction is available`() { Sentry.init { it.dsn = "https://key@sentry.io/proj" it.isGlobalHubMode = true @@ -43,7 +44,7 @@ class SentryContextWrapperTest { val c = SentryContextWrapper.wrap(Context.root()) val returnedSpan = Span.fromContextOrNull(c) - assertNull(returnedSpan) + assertTrue(returnedSpan is SentryOtelGlobalHubModeSpan) } @Test From 54c4b941953c032e3417d02c6a58ebbd43d0d655 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 29 Apr 2025 13:32:39 +0000 Subject: [PATCH 16/16] release: 8.11.0-alpha.1 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19e6d6f30e..5d7d5ca907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 8.11.0-alpha.1 ### Features diff --git a/gradle.properties b/gradle.properties index 418a5780d8..5bc6931774 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ org.gradle.workers.max=2 android.useAndroidX=true # Release information -versionName=8.10.0 +versionName=8.11.0-alpha.1 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android