diff --git a/CHANGELOG.md b/CHANGELOG.md index 2087ecfce35..42c16201070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Features - Add `Session.State.Unhandled` for unhandled errors that do not terminate the process ([#5919](https://github.com/getsentry/sentry-java/pull/5919)) +- Added `Environment` to the `Scope`, allowing you to overwrite the `Environment` set on the options ([#5772](https://github.com/getsentry/sentry-java/pull/5772)) + - The scope `Environment` is also applied to the trace envelope header (DSC), logs, metrics and profiles, so all telemetry of a trace reports the same environment ### Fixes diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java index 31fe2442bf4..a917d841c2f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java @@ -196,6 +196,7 @@ private void backfillScope( setRequest(event); setUser(event); setScopeTags(event); + setScopeEnvironment(event); setBreadcrumbs(event); setExtras(event); setContexts(event); @@ -490,6 +491,16 @@ private void setEnvironment( } } + private void setScopeEnvironment(final @NotNull SentryBaseEvent event) { + if (event.getEnvironment() == null) { + final @Nullable String scopeEnvironment = + readFromDisk(options, PersistingScopeObserver.ENVIRONMENT_FILENAME, String.class); + if (scopeEnvironment != null) { + event.setEnvironment(scopeEnvironment); + } + } + } + private void setDebugMeta( final @NotNull SentryBaseEvent event, final @NotNull OptionsSource optionsSource) { DebugMeta debugMeta = event.getDebugMeta(); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt index f484f994dbb..cfc7610d97a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt @@ -419,6 +419,18 @@ class ApplicationExitInfoEventProcessorTest { assertEquals("release", processed.environment) } + @Test + fun `if scope environment is persisted, it takes precedence over the options environment`() { + val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) + + val processor = fixture.getSut(tmpDir, populateOptionsCache = true) + fixture.persistScope(PersistingScopeObserver.ENVIRONMENT_FILENAME, "staging") + + val processed = processor.process(SentryEvent(), hint) + + assertEquals("staging", processed!!.environment) + } + @Test fun `if release is not persisted and app was not updated, uses release from options`() { val hint = HintUtils.createWithTypeCheckHint(AbnormalExitHint(timestamp = 2_000)) @@ -500,6 +512,7 @@ class ApplicationExitInfoEventProcessorTest { fixture.options.release = "io.sentry.samples@1.2.0+232" fixture.options.environment = "production" fixture.options.dist = "custom-dist" + fixture.persistScope(PersistingScopeObserver.ENVIRONMENT_FILENAME, "staging") setLastUpdateTime(1_000) val processed = processor.process(SentryEvent(), hint)!! diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt index 81950647fa9..6f2b6ade8ec 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt @@ -15,6 +15,7 @@ import io.sentry.IScopes import io.sentry.PropagationContext import io.sentry.Scope.IWithPropagationContext import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryLevel.INFO import io.sentry.android.core.SentryAndroidOptions import io.sentry.util.LazyEvaluator @@ -85,6 +86,9 @@ class SentryGestureListenerClickTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) doAnswer { (it.arguments[0] as IWithPropagationContext).accept(propagationContext) propagationContext diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt index 633bb2fdb86..0607c093a1c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt @@ -17,6 +17,7 @@ import io.sentry.IScopes import io.sentry.PropagationContext import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryLevel import io.sentry.SentryLevel.INFO import io.sentry.android.core.SentryAndroidOptions @@ -82,6 +83,9 @@ class SentryGestureListenerScrollTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) doAnswer { (it.arguments[0] as Scope.IWithPropagationContext).accept(propagationContext) propagationContext diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt index 9d7606bfe44..e73bf891c8e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt @@ -13,6 +13,7 @@ import io.sentry.IScope import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryTracer import io.sentry.SpanContext import io.sentry.SpanId @@ -31,6 +32,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import org.mockito.ArgumentCaptor import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.check import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.doAnswer @@ -94,6 +96,9 @@ class SentryGestureListenerTracingTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) return SentryGestureListener(activity, scopes, options) } diff --git a/sentry-android-ndk/api/sentry-android-ndk.api b/sentry-android-ndk/api/sentry-android-ndk.api index a7c5571d0bb..0c2ae5c90f8 100644 --- a/sentry-android-ndk/api/sentry-android-ndk.api +++ b/sentry-android-ndk/api/sentry-android-ndk.api @@ -20,6 +20,7 @@ public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/ScopeObser public fun clearAttachments ()V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V + public fun setEnvironment (Ljava/lang/String;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTrace (Lio/sentry/SpanContext;Lio/sentry/IScope;)V diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java index a1474bb69c8..dc4268f173d 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java @@ -53,6 +53,15 @@ public void setUser(final @Nullable User user) { } } + @Override + public void setEnvironment(@Nullable String environment) { + try { + options.getExecutorService().submit(() -> nativeScope.setEnvironment(environment)); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, e, "Scope sync setEnvironment has an error."); + } + } + @Override public void addBreadcrumb(final @NotNull Breadcrumb crumb) { try { diff --git a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt index a8b5318bfab..19fad628158 100644 --- a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt +++ b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/NdkScopeObserverTest.kt @@ -184,4 +184,15 @@ class NdkScopeObserverTest { verify(fixture.nativeScope).clearAttachments() } + + @Test + fun `set environments forwards call to native scope`() { + val sut = fixture.getSut() + + sut.setEnvironment(null) + verify(fixture.nativeScope).setEnvironment(eq(null)) + + sut.setEnvironment("production") + verify(fixture.nativeScope).setEnvironment(eq("production")) + } } diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt index 0a8076f20f6..d35ef4e8e0f 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt @@ -16,6 +16,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryReplayEvent.ReplayType import io.sentry.android.replay.util.ReplayShadowMediaCodec @@ -72,6 +73,9 @@ class ReplaySmokeTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(it) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(it) + .configureScope(anyOrNull(), any()) on { rateLimiter }.doReturn(rateLimiter) } diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt index 0d12fd3c3ff..d01bb08fbe7 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt @@ -6,6 +6,7 @@ import io.sentry.DataCategory import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryEnvelope import io.sentry.SentryEnvelopeHeader import io.sentry.SentryOptions @@ -66,6 +67,9 @@ class BufferCaptureStrategyTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(it) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(it) + .configureScope(anyOrNull(), any()) } var persistedSegment = LinkedHashMap() val replayCache = diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt index 3d876b8f971..c1612985ea7 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt @@ -7,6 +7,7 @@ import io.sentry.DateUtils import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryReplayEvent import io.sentry.SentryReplayEvent.ReplayType @@ -76,6 +77,9 @@ class SessionCaptureStrategyTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(it) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(it) + .configureScope(anyOrNull(), any()) } var persistedSegment = LinkedHashMap() val replayCache = diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt index 8316f6c0f33..d098eabba95 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -13,6 +13,7 @@ import io.sentry.IScopes import io.sentry.ITransaction import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.SentryTraceHeader @@ -65,6 +66,9 @@ class SentryApollo3InterceptorTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(it) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(it) + .configureScope(anyOrNull(), any()) } private var httpInterceptor = SentryApollo3HttpInterceptor(scopes, captureFailedRequests = false) diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt index d92cefe9772..5a72a08f8aa 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt @@ -16,6 +16,7 @@ import io.sentry.IScopes import io.sentry.ITransaction import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.SentryTraceHeader @@ -79,6 +80,9 @@ abstract class SentryApollo4HttpInterceptorTest( doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(it) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(it) + .configureScope(anyOrNull(), any()) } private var httpInterceptor = SentryApollo4HttpInterceptor(scopes, captureFailedRequests = false) diff --git a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt index aaf9b30b7f3..4dfa3fc3b90 100644 --- a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt +++ b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt @@ -9,6 +9,7 @@ import io.sentry.IScopes import io.sentry.ITransaction import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer @@ -56,6 +57,9 @@ class SentryApolloInterceptorTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(it) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(it) + .configureScope(anyOrNull(), any()) } private var interceptor = SentryApolloInterceptor(scopes) diff --git a/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt index 759591d323c..63f6a00e57a 100644 --- a/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt +++ b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt @@ -16,6 +16,7 @@ import io.sentry.IScope import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.exception.ExceptionMechanismException import io.sentry.protocol.Request @@ -26,6 +27,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertSame import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -89,6 +91,9 @@ class ExceptionReporterTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) return exceptionReporter } diff --git a/sentry-kafka/src/test/kotlin/io/sentry/kafka/SentryKafkaProducerTest.kt b/sentry-kafka/src/test/kotlin/io/sentry/kafka/SentryKafkaProducerTest.kt index a4ba5254c36..5901880371d 100644 --- a/sentry-kafka/src/test/kotlin/io/sentry/kafka/SentryKafkaProducerTest.kt +++ b/sentry-kafka/src/test/kotlin/io/sentry/kafka/SentryKafkaProducerTest.kt @@ -7,6 +7,7 @@ import io.sentry.ISpan import io.sentry.NoOpSpan import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.SentryTraceHeader @@ -33,6 +34,7 @@ import org.apache.kafka.common.TopicPartition import org.apache.kafka.common.header.Header import org.apache.kafka.common.header.Headers import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.eq @@ -64,6 +66,9 @@ class SentryKafkaProducerTest { doAnswer { (it.arguments[0] as ScopeCallback).run(Scope(options)) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(Scope(options)) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) delegate = mock() whenever(delegate.send(any(), any())).thenReturn(CompletableFuture.completedFuture(null)) } diff --git a/sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt b/sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt index 976d3200e11..56748720934 100644 --- a/sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt +++ b/sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt @@ -16,6 +16,7 @@ import io.sentry.IScope import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.Sentry import io.sentry.SentryEvent import io.sentry.SentryOptions @@ -90,6 +91,9 @@ class SentryKtorClientPluginTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt index 9f7d8bc18fb..3d85363efae 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt @@ -10,6 +10,7 @@ import io.sentry.IScope import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.SentryTraceHeader @@ -96,6 +97,9 @@ class SentryOkHttpInterceptorTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) diff --git a/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt b/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt index c25a81f9501..5be668be8f6 100644 --- a/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt +++ b/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt @@ -10,6 +10,7 @@ import io.sentry.Breadcrumb import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer @@ -50,6 +51,9 @@ class SentryFeignClientTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) } 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 80da51f9db7..aac15a9e59f 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 @@ -15,6 +15,7 @@ import io.sentry.NoOpScopesLifecycleToken; import io.sentry.NoOpSpan; import io.sentry.ScopeBindingMode; +import io.sentry.ScopeType; import io.sentry.SentryDate; import io.sentry.SentryLevel; import io.sentry.SentryTraceHeader; @@ -231,14 +232,20 @@ private void updateBaggageValues(final @NotNull Baggage baggage) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (baggage != null && baggage.isMutable()) { final AtomicReference replayIdAtomicReference = new AtomicReference<>(); + final AtomicReference environmentAtomicReference = new AtomicReference<>(); + // the combined view resolves current -> isolation -> global, matching what event capture + // sees, so the DSC cannot disagree with the environment on the events of the same trace scopes.configureScope( + ScopeType.COMBINED, scope -> { replayIdAtomicReference.set(scope.getReplayId()); + environmentAtomicReference.set(scope.getEnvironment()); }); baggage.setValuesFromTransaction( getSpanContext().getTraceId(), replayIdAtomicReference.get(), scopes.getOptions(), + environmentAtomicReference.get(), this.getSamplingDecision(), getTransactionName(), getTransactionNameSource()); diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentrySpringFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentrySpringFilterTest.kt index 5a83c9d72a4..605c860d0a0 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentrySpringFilterTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentrySpringFilterTest.kt @@ -6,6 +6,7 @@ import io.sentry.IScopes import io.sentry.ISentryLifecycleToken import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryOptions.RequestSize.ALWAYS import io.sentry.SentryOptions.RequestSize.MEDIUM @@ -61,6 +62,9 @@ class SentrySpringFilterTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) this.request = request ?: MockHttpServletRequest().apply { diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptorTest.kt index 85d138604dd..267477df6e8 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptorTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptorTest.kt @@ -3,6 +3,7 @@ package io.sentry.spring7.tracing import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.SentryTraceHeader @@ -13,6 +14,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -44,6 +46,9 @@ class SentrySpanClientHttpRequestInterceptorTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) whenever(requestExecution.execute(any(), any())).thenReturn(mock()) } diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt index 7b50e923b1a..9b35c0e2abc 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt @@ -5,6 +5,7 @@ import io.sentry.Breadcrumb import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer @@ -54,6 +55,9 @@ class SentrySpanRestClientCustomizerTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) transaction = SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) } diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt index 988824465bb..381a10975f6 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt @@ -5,6 +5,7 @@ import io.sentry.Breadcrumb import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer @@ -58,6 +59,9 @@ class SentrySpanRestTemplateCustomizerTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) transaction = SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) } diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanWebClientCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanWebClientCustomizerTest.kt index b593dd261dc..b30de4f0763 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanWebClientCustomizerTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanWebClientCustomizerTest.kt @@ -6,6 +6,7 @@ import io.sentry.IScope import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.Sentry.OptionsConfiguration import io.sentry.SentryOptions import io.sentry.SentryTraceHeader @@ -69,6 +70,9 @@ class SentrySpanWebClientCustomizerTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) transaction = SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) val webClientBuilder = WebClient.builder() diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt index 7a2e6a0ff4f..ac38f48047a 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt @@ -5,6 +5,7 @@ import io.sentry.Breadcrumb import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer @@ -54,6 +55,9 @@ class SentrySpanRestClientCustomizerTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) transaction = SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) } diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt index c6c7707aab9..e30c2e99d72 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt @@ -5,6 +5,7 @@ import io.sentry.Breadcrumb import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer @@ -58,6 +59,9 @@ class SentrySpanRestTemplateCustomizerTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) transaction = SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) } diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt index 83321f30e78..d34e6322abb 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt @@ -6,6 +6,7 @@ import io.sentry.IScope import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.Sentry.OptionsConfiguration import io.sentry.SentryOptions import io.sentry.SentryTraceHeader @@ -69,6 +70,9 @@ class SentrySpanWebClientCustomizerTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) transaction = SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) val webClientBuilder = WebClient.builder() diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanRestTemplateCustomizerTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanRestTemplateCustomizerTest.kt index 29699f61022..7fcba218af4 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanRestTemplateCustomizerTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanRestTemplateCustomizerTest.kt @@ -5,6 +5,7 @@ import io.sentry.Breadcrumb import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer @@ -58,6 +59,9 @@ class SentrySpanRestTemplateCustomizerTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) transaction = SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) } diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanWebClientCustomizerTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanWebClientCustomizerTest.kt index 80c6398fbe7..92ec1db72a5 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanWebClientCustomizerTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanWebClientCustomizerTest.kt @@ -6,6 +6,7 @@ import io.sentry.IScope import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.Sentry.OptionsConfiguration import io.sentry.SentryOptions import io.sentry.SentryTraceHeader @@ -69,6 +70,9 @@ class SentrySpanWebClientCustomizerTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) transaction = SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) val webClientBuilder = WebClient.builder() diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt index 349839b5d15..4bb44ac5f44 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt @@ -6,6 +6,7 @@ import io.sentry.IScopes import io.sentry.ISentryLifecycleToken import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryOptions.RequestSize.ALWAYS import io.sentry.SentryOptions.RequestSize.MEDIUM @@ -61,6 +62,9 @@ class SentrySpringFilterTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) this.request = request ?: MockHttpServletRequest().apply { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptorTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptorTest.kt index e3f9ad2dfa9..504b6d7bd12 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptorTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptorTest.kt @@ -3,6 +3,7 @@ package io.sentry.spring.jakarta.tracing import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.SentryTraceHeader @@ -13,6 +14,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -44,6 +46,9 @@ class SentrySpanClientHttpRequestInterceptorTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) whenever(requestExecution.execute(any(), any())).thenReturn(mock()) } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/SentrySpringFilterTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/SentrySpringFilterTest.kt index eb145bcd8a1..ad990a96569 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/SentrySpringFilterTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/SentrySpringFilterTest.kt @@ -6,6 +6,7 @@ import io.sentry.IScopes import io.sentry.ISentryLifecycleToken import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryOptions.RequestSize.ALWAYS import io.sentry.SentryOptions.RequestSize.MEDIUM @@ -61,6 +62,9 @@ class SentrySpringFilterTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) this.request = request ?: MockHttpServletRequest().apply { diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptorTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptorTest.kt index a850480099a..6531cd7fd04 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptorTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptorTest.kt @@ -3,6 +3,7 @@ package io.sentry.spring.tracing import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.SentryTraceHeader @@ -13,6 +14,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -44,6 +46,9 @@ class SentrySpanClientHttpRequestInterceptorTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) whenever(requestExecution.execute(any(), any())).thenReturn(mock()) } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 39fbbe3096d..7e27bc9060e 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -77,7 +77,7 @@ public final class io/sentry/Baggage { public fun setUserId (Ljava/lang/String;)V public fun setValuesFromSamplingDecision (Lio/sentry/TracesSamplingDecision;)V public fun setValuesFromScope (Lio/sentry/IScope;Lio/sentry/SentryOptions;)V - public fun setValuesFromTransaction (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Lio/sentry/SentryOptions;Lio/sentry/TracesSamplingDecision;Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V + public fun setValuesFromTransaction (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Lio/sentry/SentryOptions;Ljava/lang/String;Lio/sentry/TracesSamplingDecision;Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public fun toHeaderString (Ljava/lang/String;)Ljava/lang/String; public fun toTraceContext ()Lio/sentry/TraceContext; } @@ -287,6 +287,7 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun getBreadcrumbs ()Ljava/util/Queue; public fun getClient ()Lio/sentry/ISentryClient; public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getEnvironment ()Ljava/lang/String; public fun getEventProcessors ()Ljava/util/List; public fun getEventProcessorsWithOrder ()Ljava/util/List; public fun getExtras ()Ljava/util/Map; @@ -322,6 +323,7 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun setContexts (Ljava/lang/String;Ljava/lang/String;)V public fun setContexts (Ljava/lang/String;Ljava/util/Collection;)V public fun setContexts (Ljava/lang/String;[Ljava/lang/Object;)V + public fun setEnvironment (Ljava/lang/String;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLastEventId (Lio/sentry/protocol/SentryId;)V @@ -932,6 +934,7 @@ public abstract interface class io/sentry/IScope { public abstract fun getBreadcrumbs ()Ljava/util/Queue; public abstract fun getClient ()Lio/sentry/ISentryClient; public abstract fun getContexts ()Lio/sentry/protocol/Contexts; + public abstract fun getEnvironment ()Ljava/lang/String; public abstract fun getEventProcessors ()Ljava/util/List; public abstract fun getEventProcessorsWithOrder ()Ljava/util/List; public abstract fun getExtras ()Ljava/util/Map; @@ -967,6 +970,7 @@ public abstract interface class io/sentry/IScope { public abstract fun setContexts (Ljava/lang/String;Ljava/lang/String;)V public abstract fun setContexts (Ljava/lang/String;Ljava/util/Collection;)V public abstract fun setContexts (Ljava/lang/String;[Ljava/lang/Object;)V + public abstract fun setEnvironment (Ljava/lang/String;)V public abstract fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public abstract fun setFingerprint (Ljava/util/List;)V public abstract fun setLastEventId (Lio/sentry/protocol/SentryId;)V @@ -994,6 +998,7 @@ public abstract interface class io/sentry/IScopeObserver { public abstract fun removeTag (Ljava/lang/String;)V public abstract fun setBreadcrumbs (Ljava/util/Collection;)V public abstract fun setContexts (Lio/sentry/protocol/Contexts;)V + public abstract fun setEnvironment (Ljava/lang/String;)V public abstract fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public abstract fun setExtras (Ljava/util/Map;)V public abstract fun setFingerprint (Ljava/util/Collection;)V @@ -1763,6 +1768,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun getBreadcrumbs ()Ljava/util/Queue; public fun getClient ()Lio/sentry/ISentryClient; public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getEnvironment ()Ljava/lang/String; public fun getEventProcessors ()Ljava/util/List; public fun getEventProcessorsWithOrder ()Ljava/util/List; public fun getExtras ()Ljava/util/Map; @@ -1799,6 +1805,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun setContexts (Ljava/lang/String;Ljava/lang/String;)V public fun setContexts (Ljava/lang/String;Ljava/util/Collection;)V public fun setContexts (Ljava/lang/String;[Ljava/lang/Object;)V + public fun setEnvironment (Ljava/lang/String;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLastEventId (Lio/sentry/protocol/SentryId;)V @@ -2150,6 +2157,7 @@ public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentr public fun hashCode ()I public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setDebugMeta (Lio/sentry/protocol/DebugMeta;)V + public fun setEnvironment (Ljava/lang/String;)V public fun setSampledProfile (Ljava/lang/String;)V public fun setSentryProfile (Lio/sentry/protocol/profiling/SentryProfile;)V public fun setUnknown (Ljava/util/Map;)V @@ -2459,6 +2467,7 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun getBreadcrumbs ()Ljava/util/Queue; public fun getClient ()Lio/sentry/ISentryClient; public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getEnvironment ()Ljava/lang/String; public fun getEventProcessors ()Ljava/util/List; public fun getEventProcessorsWithOrder ()Ljava/util/List; public fun getExtras ()Ljava/util/Map; @@ -2494,6 +2503,7 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun setContexts (Ljava/lang/String;Ljava/lang/String;)V public fun setContexts (Ljava/lang/String;Ljava/util/Collection;)V public fun setContexts (Ljava/lang/String;[Ljava/lang/Object;)V + public fun setEnvironment (Ljava/lang/String;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLastEventId (Lio/sentry/protocol/SentryId;)V @@ -2546,6 +2556,7 @@ public abstract class io/sentry/ScopeObserverAdapter : io/sentry/IScopeObserver public fun removeTag (Ljava/lang/String;)V public fun setBreadcrumbs (Ljava/util/Collection;)V public fun setContexts (Lio/sentry/protocol/Contexts;)V + public fun setEnvironment (Ljava/lang/String;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setExtras (Ljava/util/Map;)V public fun setFingerprint (Ljava/util/Collection;)V @@ -4942,6 +4953,7 @@ public final class io/sentry/cache/PersistingOptionsObserver : io/sentry/IOption public final class io/sentry/cache/PersistingScopeObserver : io/sentry/ScopeObserverAdapter { public static final field BREADCRUMBS_FILENAME Ljava/lang/String; public static final field CONTEXTS_FILENAME Ljava/lang/String; + public static final field ENVIRONMENT_FILENAME Ljava/lang/String; public static final field EXTRAS_FILENAME Ljava/lang/String; public static final field FINGERPRINT_FILENAME Ljava/lang/String; public static final field LEVEL_FILENAME Ljava/lang/String; @@ -4958,6 +4970,7 @@ public final class io/sentry/cache/PersistingScopeObserver : io/sentry/ScopeObse public fun resetCache ()V public fun setBreadcrumbs (Ljava/util/Collection;)V public fun setContexts (Lio/sentry/protocol/Contexts;)V + public fun setEnvironment (Ljava/lang/String;)V public fun setExtras (Ljava/util/Map;)V public fun setFingerprint (Ljava/util/Collection;)V public fun setLevel (Lio/sentry/SentryLevel;)V diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 9f0753f8671..aeadbdacd65 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -514,13 +514,14 @@ public void setValuesFromTransaction( final @NotNull SentryId traceId, final @Nullable SentryId replayId, final @NotNull SentryOptions sentryOptions, + final @Nullable String scopeEnvironment, final @Nullable TracesSamplingDecision samplingDecision, final @Nullable String transactionName, final @Nullable TransactionNameSource transactionNameSource) { setTraceId(traceId.toString()); setPublicKey(sentryOptions.retrieveParsedDsn().getPublicKey()); setRelease(sentryOptions.getRelease()); - setEnvironment(sentryOptions.getEnvironment()); + setEnvironment(scopeEnvironment != null ? scopeEnvironment : sentryOptions.getEnvironment()); setTransaction(isHighQualityTransactionName(transactionNameSource) ? transactionName : null); if (replayId != null && !SentryId.EMPTY_ID.equals(replayId)) { setReplayId(replayId.toString()); @@ -557,7 +558,8 @@ public void setValuesFromScope( setTraceId(propagationContext.getTraceId().toString()); setPublicKey(options.retrieveParsedDsn().getPublicKey()); setRelease(options.getRelease()); - setEnvironment(options.getEnvironment()); + final @Nullable String scopeEnvironment = scope.getEnvironment(); + setEnvironment(scopeEnvironment != null ? scopeEnvironment : options.getEnvironment()); if (!SentryId.EMPTY_ID.equals(replayId)) { setReplayId(replayId.toString()); } diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index ea2d752d44b..cb2ff8c6401 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -56,6 +56,24 @@ public void setLevel(@Nullable SentryLevel level) { getDefaultWriteScope().setLevel(level); } + @Override + public @Nullable String getEnvironment() { + final @Nullable String current = scope.getEnvironment(); + if (current != null) { + return current; + } + final @Nullable String isolation = isolationScope.getEnvironment(); + if (isolation != null) { + return isolation; + } + return globalScope.getEnvironment(); + } + + @Override + public void setEnvironment(@Nullable String environment) { + getDefaultWriteScope().setEnvironment(environment); + } + @Override public @Nullable String getTransactionName() { final @Nullable String current = scope.getTransactionName(); @@ -540,7 +558,11 @@ public void addEventProcessor(@NotNull EventProcessor eventProcessor) { @Override public @Nullable Scope.SessionPair startSession() { - return getDefaultWriteScope().startSession(); + final IScope defaultScope = getDefaultWriteScope(); + if (defaultScope instanceof Scope) { + return ((Scope) defaultScope).startSession(getEnvironment()); + } + return defaultScope.startSession(); } @Override diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java index 5b6c38bbcfb..ee512c7441f 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -26,6 +26,21 @@ public interface IScope { */ void setLevel(final @Nullable SentryLevel level); + /** + * Returns the Scope's environment. + * + * @return the environment or {@code null} if not set on the scope + */ + @Nullable + String getEnvironment(); + + /** + * Sets the Scope's environment. Takes precedence over {@link SentryOptions#getEnvironment()}. + * + * @param environment the environment + */ + void setEnvironment(final @Nullable String environment); + /** * Returns the Scope's transaction name. * diff --git a/sentry/src/main/java/io/sentry/IScopeObserver.java b/sentry/src/main/java/io/sentry/IScopeObserver.java index e1b9a785043..b6210c88701 100644 --- a/sentry/src/main/java/io/sentry/IScopeObserver.java +++ b/sentry/src/main/java/io/sentry/IScopeObserver.java @@ -38,6 +38,8 @@ public interface IScopeObserver { void setLevel(@Nullable SentryLevel level); + void setEnvironment(@Nullable String environment); + void setContexts(@NotNull Contexts contexts); void setTransaction(@Nullable String transaction); diff --git a/sentry/src/main/java/io/sentry/NoOpScope.java b/sentry/src/main/java/io/sentry/NoOpScope.java index 9d2f603c673..a452cb3f350 100644 --- a/sentry/src/main/java/io/sentry/NoOpScope.java +++ b/sentry/src/main/java/io/sentry/NoOpScope.java @@ -41,6 +41,14 @@ public static NoOpScope getInstance() { @Override public void setLevel(@Nullable SentryLevel level) {} + @Override + public @Nullable String getEnvironment() { + return null; + } + + @Override + public void setEnvironment(@Nullable String environment) {} + @Override public @Nullable String getTransactionName() { return null; diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java index 13b40784144..68cb2201663 100644 --- a/sentry/src/main/java/io/sentry/ProfileChunk.java +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -102,6 +102,11 @@ public void setDebugMeta(final @Nullable DebugMeta debugMeta) { return environment; } + @ApiStatus.Internal + public void setEnvironment(final @Nullable String environment) { + this.environment = environment; + } + public @NotNull String getPlatform() { return platform; } diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 54e8b893555..0c462133591 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -54,6 +54,9 @@ public final class Scope implements IScope { /** Scope's screen */ private @Nullable String screen; + /** Scope's environment */ + private @Nullable String environment; + /** Scope's request */ private @Nullable Request request; @@ -138,6 +141,7 @@ private Scope(final @NotNull Scope scope) { final User userRef = scope.user; this.user = userRef != null ? new User(userRef) : null; this.screen = scope.screen; + this.environment = scope.environment; this.replayId = scope.replayId; final Request requestRef = scope.request; @@ -225,6 +229,30 @@ public void setLevel(final @Nullable SentryLevel level) { } } + /** + * Returns the Scope's environment. + * + * @return the environment or {@code null} if not set on the scope + */ + @Override + public @Nullable String getEnvironment() { + return environment; + } + + /** + * Sets the Scope's environment. Takes precedence over {@link SentryOptions#getEnvironment()}. + * + * @param environment the environment + */ + @Override + public void setEnvironment(final @Nullable String environment) { + this.environment = environment; + + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setEnvironment(environment); + } + } + /** * Returns the Scope's transaction name. * @@ -566,7 +594,8 @@ public ITransaction getTransaction() { /** Resets the Scope to its default state */ @Override public void clear() { - level = null; + setLevel(null); + setEnvironment(null); user = null; request = null; screen = null; @@ -1038,6 +1067,11 @@ public interface IWithSession { @Nullable @Override public SessionPair startSession() { + return startSession(environment); + } + + @Nullable + SessionPair startSession(final @Nullable String sessionEnvironment) { Session previousSession; SessionPair pair = null; try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) { @@ -1052,7 +1086,10 @@ public SessionPair startSession() { if (options.getRelease() != null) { session = new Session( - options.getDistinctId(), user, options.getEnvironment(), options.getRelease()); + options.getDistinctId(), + user, + sessionEnvironment != null ? sessionEnvironment : options.getEnvironment(), + options.getRelease()); final Session previousClone = previousSession != null ? previousSession.clone() : null; pair = new SessionPair(session.clone(), previousClone); diff --git a/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java b/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java index 4f6a5ac842c..e80d8b6b208 100644 --- a/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java @@ -46,6 +46,9 @@ public void setFingerprint(@NotNull Collection fingerprint) {} @Override public void setLevel(@Nullable SentryLevel level) {} + @Override + public void setEnvironment(@Nullable String environment) {} + @Override public void setContexts(@NotNull Contexts contexts) {} diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index d3926d56993..2ac8178d655 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -905,7 +905,7 @@ public void flush(long timeoutMillis) { "Instance is disabled and this 'captureTransaction' call is a no-op."); } else { try { - sentryId = getClient().captureProfileChunk(profilingContinuousData, getScope()); + sentryId = getClient().captureProfileChunk(profilingContinuousData, getCombinedScopeView()); } catch (Throwable e) { getOptions() .getLogger() diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 03288ae00ad..00f5729535a 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -597,6 +597,12 @@ private static void notifyOptionsObservers(final @NotNull SentryOptions options) options.findPersistingScopeObserver(); if (scopeCache != null) { scopeCache.resetCache(); + // the scope environment is configuration, not telemetry state, so we persist it + // again after the reset to keep it available for the next app launch + final @Nullable String environment = globalScope.getEnvironment(); + if (environment != null) { + scopeCache.setEnvironment(environment); + } } }); } catch (Throwable e) { diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 0e44812a491..8b6a92a073d 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1002,6 +1002,13 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint if (shouldApplyScopeData(transaction, hint)) { addScopeAttachmentsToHint(scope, hint); + + if (profilingTraceData != null && scope != null) { + final @Nullable String scopeEnvironment = scope.getEnvironment(); + if (scopeEnvironment != null) { + profilingTraceData.setEnvironment(scopeEnvironment); + } + } } options @@ -1125,6 +1132,13 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint .getLogger() .log(SentryLevel.DEBUG, "Capturing profile chunk: %s", profileChunk.getChunkId()); + if (scope != null) { + final @Nullable String scopeEnvironment = scope.getEnvironment(); + if (scopeEnvironment != null) { + profileChunk.setEnvironment(scopeEnvironment); + } + } + @NotNull SentryId sentryId = profileChunk.getChunkId(); final DebugMeta debugMeta = DebugMeta.buildDebugMeta(profileChunk.getDebugMeta(), options); if (debugMeta != null) { @@ -1166,6 +1180,10 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint hint = new Hint(); } + if (shouldApplyScopeData(checkIn, hint)) { + checkIn = applyScope(checkIn, scope); + } + if (checkIn.getEnvironment() == null) { checkIn.setEnvironment(options.getEnvironment()); } @@ -1174,10 +1192,6 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint checkIn.setRelease(options.getRelease()); } - if (shouldApplyScopeData(checkIn, hint)) { - checkIn = applyScope(checkIn, scope); - } - if (CheckInUtils.isIgnored(options.getIgnoredCheckIns(), checkIn.getMonitorSlug())) { options .getLogger() @@ -1513,6 +1527,9 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri private @Nullable SentryEvent applyFeedbackScope( @NotNull SentryEvent event, final @NotNull IScope scope, final @NotNull Hint hint) { + if (event.getEnvironment() == null) { + event.setEnvironment(scope.getEnvironment()); + } if (event.getUser() == null) { event.setUser(scope.getUser()); } @@ -1549,6 +1566,9 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri private @NotNull CheckIn applyScope(@NotNull CheckIn checkIn, final @Nullable IScope scope) { if (scope != null) { + if (checkIn.getEnvironment() == null) { + checkIn.setEnvironment(scope.getEnvironment()); + } // Set trace data from active span to connect events with transactions final ISpan span = scope.getSpan(); if (checkIn.getContexts().getTrace() == null) { @@ -1574,6 +1594,9 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri if (replayEvent.getUser() == null) { replayEvent.setUser(scope.getUser()); } + if (replayEvent.getEnvironment() == null) { + replayEvent.setEnvironment(scope.getEnvironment()); + } if (replayEvent.getTags() == null) { replayEvent.setTags(scope.getTags()); } else { @@ -1614,6 +1637,9 @@ public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metri if (sentryBaseEvent.getUser() == null) { sentryBaseEvent.setUser(scope.getUser()); } + if (sentryBaseEvent.getEnvironment() == null) { + sentryBaseEvent.setEnvironment(scope.getEnvironment()); + } if (sentryBaseEvent.getTags() == null) { sentryBaseEvent.setTags(scope.getTags()); } else { diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 723538b9924..1146cb6c822 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -672,14 +672,20 @@ private void updateBaggageValues(final @NotNull Baggage baggage) { try (final @NotNull ISentryLifecycleToken ignored = tracerLock.acquire()) { if (baggage.isMutable()) { final AtomicReference replayId = new AtomicReference<>(); + final AtomicReference environment = new AtomicReference<>(); + // the combined view resolves current -> isolation -> global, matching what event capture + // sees, so the DSC cannot disagree with the environment on the events of the same trace scopes.configureScope( + ScopeType.COMBINED, scope -> { replayId.set(scope.getReplayId()); + environment.set(scope.getEnvironment()); }); baggage.setValuesFromTransaction( getSpanContext().getTraceId(), replayId.get(), scopes.getOptions(), + environment.get(), this.getSamplingDecision(), getName(), getTransactionNameSource()); diff --git a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java index d6137ae051a..03686a472b8 100644 --- a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java +++ b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java @@ -57,6 +57,7 @@ public final class PersistingScopeObserver extends ScopeObserverAdapter { public static final String CONTEXTS_FILENAME = "contexts.json"; public static final String REQUEST_FILENAME = "request.json"; public static final String LEVEL_FILENAME = "level.json"; + public static final String ENVIRONMENT_FILENAME = "environment.json"; public static final String FINGERPRINT_FILENAME = "fingerprint.json"; public static final String TRANSACTION_FILENAME = "transaction.json"; public static final String TRACE_FILENAME = "trace.json"; @@ -189,6 +190,11 @@ public void setLevel(@Nullable SentryLevel level) { enqueue(LEVEL_FILENAME, level); } + @Override + public void setEnvironment(@Nullable String environment) { + enqueue(ENVIRONMENT_FILENAME, environment); + } + @Override public void setTransaction(@Nullable String transaction) { enqueue(TRANSACTION_FILENAME, transaction); @@ -371,6 +377,7 @@ public void resetCache() { // the rest we can safely delete delete(USER_FILENAME); delete(LEVEL_FILENAME); + delete(ENVIRONMENT_FILENAME); delete(REQUEST_FILENAME); delete(FINGERPRINT_FILENAME); delete(CONTEXTS_FILENAME); diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index c203dcbfb8f..9ec3c29ecfb 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -213,7 +213,9 @@ private void captureLog( new SentryLogEventAttributeValue(SentryAttributeType.STRING, sdkVersion.getVersion())); } - final @Nullable String environment = scopes.getOptions().getEnvironment(); + final @Nullable String scopeEnvironment = scopes.getCombinedScopeView().getEnvironment(); + final @Nullable String environment = + scopeEnvironment != null ? scopeEnvironment : scopes.getOptions().getEnvironment(); if (environment != null) { attributes.put( "sentry.environment", diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index cebcad9735c..0d1bdc1f56d 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -200,7 +200,9 @@ private void captureMetrics( new SentryLogEventAttributeValue(SentryAttributeType.STRING, sdkVersion.getVersion())); } - final @Nullable String environment = scopes.getOptions().getEnvironment(); + final @Nullable String scopeEnvironment = scopes.getCombinedScopeView().getEnvironment(); + final @Nullable String environment = + scopeEnvironment != null ? scopeEnvironment : scopes.getOptions().getEnvironment(); if (environment != null) { attributes.put( "sentry.environment", diff --git a/sentry/src/main/java/io/sentry/util/TracingUtils.java b/sentry/src/main/java/io/sentry/util/TracingUtils.java index 9de7adec4be..8a1aa0ac660 100644 --- a/sentry/src/main/java/io/sentry/util/TracingUtils.java +++ b/sentry/src/main/java/io/sentry/util/TracingUtils.java @@ -8,6 +8,7 @@ import io.sentry.ISpan; import io.sentry.NoOpLogger; import io.sentry.PropagationContext; +import io.sentry.ScopeType; import io.sentry.SentryOptions; import io.sentry.SentryTraceHeader; import io.sentry.SpanContext; @@ -75,7 +76,10 @@ public static void setTrace( return new TracingHeaders(sentryTraceHeader, baggageHeader, w3cTraceparentHeader); } else { final @NotNull PropagationContextHolder returnValue = new PropagationContextHolder(); + // the combined view writes the propagation context to the same default scope as before, but + // reads scope data such as the environment across current -> isolation -> global scopes.configureScope( + ScopeType.COMBINED, (scope) -> { returnValue.propagationContext = maybeUpdateBaggage(scope, sentryOptions); }); diff --git a/sentry/src/test/java/io/sentry/BaggageTest.kt b/sentry/src/test/java/io/sentry/BaggageTest.kt index e177645734d..1a5ca8da418 100644 --- a/sentry/src/test/java/io/sentry/BaggageTest.kt +++ b/sentry/src/test/java/io/sentry/BaggageTest.kt @@ -1,6 +1,7 @@ package io.sentry import com.github.javafaker.Faker +import com.google.common.truth.Truth.assertThat import io.sentry.Baggage.MAX_BAGGAGE_LIST_MEMBER_COUNT import io.sentry.Baggage.MAX_BAGGAGE_STRING_LENGTH import io.sentry.protocol.SentryId @@ -737,6 +738,80 @@ class BaggageTest { assertNull(baggage.sampleRate) } + @Test + fun `setValuesFromScope prefers the scope environment over the options environment`() { + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/456" + environment = "options-env" + } + val scope = Scope(options).apply { environment = "scope-env" } + val baggage = Baggage(logger) + + baggage.setValuesFromScope(scope, options) + + assertThat(baggage.environment).isEqualTo("scope-env") + } + + @Test + fun `setValuesFromScope uses the options environment when the scope has none`() { + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/456" + environment = "options-env" + } + val scope = Scope(options) + val baggage = Baggage(logger) + + baggage.setValuesFromScope(scope, options) + + assertThat(baggage.environment).isEqualTo("options-env") + } + + @Test + fun `setValuesFromTransaction prefers the scope environment over the options environment`() { + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/456" + environment = "options-env" + } + val baggage = Baggage(logger) + + baggage.setValuesFromTransaction( + SentryId(), + SentryId(), + options, + "scope-env", + TracesSamplingDecision(true, 1.0), + "test-transaction", + TransactionNameSource.CUSTOM, + ) + + assertThat(baggage.environment).isEqualTo("scope-env") + } + + @Test + fun `setValuesFromTransaction uses the options environment when the scope has none`() { + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/456" + environment = "options-env" + } + val baggage = Baggage(logger) + + baggage.setValuesFromTransaction( + SentryId(), + SentryId(), + options, + null, + TracesSamplingDecision(true, 1.0), + "test-transaction", + TransactionNameSource.CUSTOM, + ) + + assertThat(baggage.environment).isEqualTo("options-env") + } + @Test fun `setValuesFromScope falls back to DSN org id when explicit orgId is empty`() { val options = @@ -780,6 +855,7 @@ class BaggageTest { SentryId(), SentryId(), options, + null, TracesSamplingDecision(true, 1.0), "test-transaction", TransactionNameSource.CUSTOM, diff --git a/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt index fd187235a92..9439306eec5 100644 --- a/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt +++ b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt @@ -1115,6 +1115,17 @@ class CombinedScopeViewTest { assertNull(fixture.globalScope.session) } + @Test + fun `starts session with the combined environment`() { + val combined = fixture.getSut() + fixture.options.environment = "options-environment" + fixture.scope.environment = "current-environment" + + combined.startSession() + + assertEquals("current-environment", fixture.isolationScope.session!!.environment) + } + @Test fun `ends session on default scope`() { val combined = fixture.getSut() diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index be6a22516c0..9cde2c1289f 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -290,6 +290,7 @@ class ScopeTest { scope.addBreadcrumb(Breadcrumb()) scope.setTag("some", "tag") scope.screen = "MainActivity" + scope.environment = "staging" scope.setExtra("some", "extra") scope.setAttribute("some", "attribute") scope.setContexts("some", "context") @@ -304,6 +305,7 @@ class ScopeTest { assertNull(scope.user) assertNull(scope.request) assertNull(scope.screen) + assertNull(scope.environment) assertEquals(0, scope.fingerprint.size) assertEquals(0, scope.breadcrumbs.size) assertEquals(0, scope.tags.size) @@ -464,6 +466,21 @@ class ScopeTest { } } + @Test + fun `Scope starts a new session with scope environment`() { + val options = + SentryOptions().apply { + release = "rel" + environment = "options-env" + } + val scope = Scope(options) + scope.environment = "scope-env" + + val sessionPair = scope.startSession() + + assertNotNull(sessionPair) { assertEquals("scope-env", it.current.environment) } + } + @Test fun `Scope ends a session and returns it if theres one`() { val options = SentryOptions().apply { release = "0.0.1" } @@ -804,6 +821,47 @@ class ScopeTest { verify(observer).setLevel(eq(WARNING)) } + @Test + fun `Scope clear level sync scopes`() { + val observer = mock() + val options = SentryOptions().apply { addScopeObserver(observer) } + val scope = Scope(options) + + scope.clear() + + verify(observer).setLevel(null) + } + + @Test + fun `Scope set environment sync scopes`() { + val observer = mock() + val options = SentryOptions().apply { addScopeObserver(observer) } + val scope = Scope(options) + + scope.environment = "staging" + verify(observer).setEnvironment(eq("staging")) + } + + @Test + fun `Scope set environment null sync scopes`() { + val observer = mock() + val options = SentryOptions().apply { addScopeObserver(observer) } + val scope = Scope(options) + + scope.environment = null + verify(observer).setEnvironment(null) + } + + @Test + fun `Scope clone copies environment`() { + val scope = Scope(SentryOptions()) + scope.environment = "staging" + + val clone = scope.clone() + + assertEquals("staging", clone.environment) + } + @Test fun `Scope set transaction name sync scopes`() { val observer = mock() diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index d1cb38c6495..42fc5378240 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2750,6 +2750,24 @@ class ScopesTest { ) } + @Test + fun `log environment attribute uses the scope environment`() { + val (sut, mockClient) = + getEnabledScopes { + it.logs.isEnabled = true + it.environment = "testenv" + } + sut.configureScope(ScopeType.GLOBAL) { it.environment = "scope-env" } + + sut.logger().log(SentryLogLevel.WARN, "log message") + + verify(mockClient) + .captureLog( + check { assertEquals("scope-env", it.attributes?.get("sentry.environment")?.value) }, + anyOrNull(), + ) + } + @Test fun `creating log with timestamp works`() { val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } @@ -3377,6 +3395,21 @@ class ScopesTest { ) } + @Test + fun `metric environment attribute uses the scope environment`() { + val (sut, mockClient) = getEnabledScopes { it.environment = "testenv" } + sut.configureScope(ScopeType.GLOBAL) { it.environment = "scope-env" } + + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { assertEquals("scope-env", it.attributes?.get("sentry.environment")?.value) }, + anyOrNull(), + anyOrNull(), + ) + } + @Test fun `metric with manual origin does not have origin attribute`() { val (sut, mockClient) = getEnabledScopes() diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 3066d2c1b53..66c5f5ff2a1 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -682,24 +682,35 @@ class SentryClientTest { } @Test - fun `when event has environment, value from options not applied`() { + fun `when scope has environment, it takes precedence over options`() { val event = SentryEvent() - val expected = "original" - fixture.sentryOptions.environment = "not to be applied" - event.environment = expected + val scope = createScope() + scope.environment = "from-scope" + fixture.sentryOptions.environment = "from-options" val sut = fixture.getSut() - sut.captureEvent(event) - assertEquals(expected, event.environment) + sut.captureEvent(event, scope) + assertEquals("from-scope", event.environment) } @Test - fun `when event doesn't have environment, value from options applied`() { + fun `when event has environment, scope environment not applied`() { val event = SentryEvent() - val expected = "original" - fixture.sentryOptions.environment = expected + event.environment = "from-event" + val scope = createScope() + scope.environment = "from-scope" val sut = fixture.getSut() - sut.captureEvent(event) - assertEquals(expected, event.environment) + sut.captureEvent(event, scope) + assertEquals("from-event", event.environment) + } + + @Test + fun `when scope has no environment, value from options applied`() { + val event = SentryEvent() + val scope = createScope() + fixture.sentryOptions.environment = "from-options" + val sut = fixture.getSut() + sut.captureEvent(event, scope) + assertEquals("from-options", event.environment) } @Test @@ -1007,6 +1018,16 @@ class SentryClientTest { ) } + @Test + fun `when captureCheckIn, scope environment takes precedence over options`() { + val scope = Scope(fixture.sentryOptions).apply { environment = "scope-environment" } + val sut = fixture.getSut { it.environment = "options-environment" } + + sut.captureCheckIn(checkIn, scope, null) + + assertEquals("scope-environment", checkIn.environment) + } + @Test fun `when captureCheckIn, envelope is sent if ignored slug does not match`() { val sut = fixture.getSut { options -> options.setIgnoredCheckIns(listOf("non_matching_slug")) } @@ -2017,6 +2038,43 @@ class SentryClientTest { verifyProfileChunkInEnvelope(fixture.profileChunk.chunkId) } + @Test + fun `captureProfileChunk applies the scope environment`() { + fixture.sentryOptions.environment = "options-env" + val scope = Scope(fixture.sentryOptions).apply { environment = "scope-env" } + + fixture.getSut().captureProfileChunk(fixture.profileChunk, scope) + + assertEquals("scope-env", fixture.profileChunk.environment) + } + + @Test + fun `captureProfileChunk keeps the chunk environment when the scope has none`() { + val environmentAtCreation = fixture.profileChunk.environment + + fixture.getSut().captureProfileChunk(fixture.profileChunk, Scope(fixture.sentryOptions)) + + assertEquals(environmentAtCreation, fixture.profileChunk.environment) + } + + @Test + fun `captureTransaction applies the scope environment to the profiling trace data`() { + fixture.sentryOptions.environment = "options-env" + val scope = Scope(fixture.sentryOptions).apply { environment = "scope-env" } + + fixture + .getSut() + .captureTransaction( + SentryTransaction(fixture.sentryTracer), + null, + scope, + null, + fixture.profilingTraceData, + ) + + assertEquals("scope-env", fixture.profilingTraceData.environment) + } + @Test fun `captureProfileChunk adds options proguard debug meta`() { fixture.sentryOptions.proguardUuid = "current-uuid" @@ -3826,6 +3884,21 @@ class SentryClientTest { assertTrue(invoked) } + @Test + fun `when captureReplayEvent, scope environment overrides options`() { + var capturedEnvironment: String? = null + fixture.sentryOptions.environment = "from-options" + fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> + capturedEnvironment = replay.environment + replay + } + val scope = Scope(fixture.sentryOptions).apply { environment = "from-scope" } + + fixture.getSut().captureReplayEvent(SentryReplayEvent(), scope, Hint()) + + assertEquals("from-scope", capturedEnvironment) + } + @Test fun `when beforeSendReplay returns null, event is dropped`() { fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> null } @@ -3925,6 +3998,7 @@ class SentryClientTest { val scope = createScope() val scopeReplayId = SentryId() scope.contexts.setTrace(SpanContext("test")) + scope.environment = "scope-environment" scope.setContexts("context-key", "context-value") scope.screen = "screen" scope.replayId = scopeReplayId @@ -3934,6 +4008,7 @@ class SentryClientTest { assertNotNull(sentFeedback) // User, tags and contexts are applied to the feedback assertEquals(scope.user, sentEvent!!.user) + assertEquals("scope-environment", sentEvent!!.environment) assertEquals("tags", sentEvent!!.tags!!["tags"]) assertEquals( scope.contexts.trace!!.traceId.toString(), diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 3d91c0f8ead..c7249a69975 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -8,6 +8,7 @@ import io.sentry.backpressure.BackpressureMonitor import io.sentry.backpressure.NoOpBackpressureMonitor import io.sentry.cache.EnvelopeCache import io.sentry.cache.IEnvelopeCache +import io.sentry.cache.PersistingScopeObserver import io.sentry.internal.debugmeta.IDebugMetaLoader import io.sentry.internal.debugmeta.ResourcesDebugMetaLoader import io.sentry.internal.modules.CompositeModulesLoader @@ -174,6 +175,32 @@ class SentryTest { assertFalse(Sentry.getGlobalScope().client.isEnabled) } + @Test + fun `init persists pre-init global scope environment`() { + val options = + SentryOptions().apply { + dsn = this@SentryTest.dsn + cacheDirPath = getTempPath() + executorService = ImmediateExecutorService() + addScopeObserver(PersistingScopeObserver(this)) + } + Sentry.getGlobalScope().environment = "pre-init-environment" + + try { + initForTest(options) + + assertEquals( + "pre-init-environment", + PersistingScopeObserver(options) + .read(options, PersistingScopeObserver.ENVIRONMENT_FILENAME, String::class.java), + ) + } finally { + Sentry.close() + Sentry.getGlobalScope().replaceOptions(SentryOptions.empty()) + Sentry.getGlobalScope().environment = null + } + } + @Test fun `close calls scopes close with isRestarting false`() { val scopes = mock() diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 20eeafffe92..c8fa9138b74 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -767,6 +767,26 @@ class SentryTracerTest { } } + @Test + fun `trace state uses the scope environment when set`() { + val transaction = fixture.getSut({ it.isTraceSampling = true }) + fixture.scopes.configureScope { it.environment = "scope-environment" } + + val trace = transaction.traceContext() + + assertNotNull(trace) { assertEquals("scope-environment", it.environment) } + } + + @Test + fun `trace state uses the global scope environment when set`() { + val transaction = fixture.getSut({ it.isTraceSampling = true }) + fixture.scopes.configureScope(ScopeType.GLOBAL) { it.environment = "global-environment" } + + val trace = transaction.traceContext() + + assertNotNull(trace) { assertEquals("global-environment", it.environment) } + } + @Test fun `returns trace state without userId if not send pii`() { val transaction = fixture.getSut({ it.isTraceSampling = true }) diff --git a/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt b/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt index 1ee1d41b71b..f0152ab9a00 100644 --- a/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt @@ -67,6 +67,7 @@ class TraceContextSerializationTest { release = "1.0.17" tracesSampleRate = sRate }, + null, TracesSamplingDecision(sRate > 0.5, sRate), "name", TransactionNameSource.ROUTE, diff --git a/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverTest.kt b/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverTest.kt index f682ac4c474..b98c5b8a319 100644 --- a/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverTest.kt +++ b/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverTest.kt @@ -9,6 +9,7 @@ import io.sentry.SpanContext import io.sentry.SpanId import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME import io.sentry.cache.PersistingScopeObserver.CONTEXTS_FILENAME +import io.sentry.cache.PersistingScopeObserver.ENVIRONMENT_FILENAME import io.sentry.cache.PersistingScopeObserver.EXTRAS_FILENAME import io.sentry.cache.PersistingScopeObserver.FINGERPRINT_FILENAME import io.sentry.cache.PersistingScopeObserver.LEVEL_FILENAME @@ -196,6 +197,15 @@ class PersistingScopeObserverTest( DeletedEntityProvider { null }, ) + private fun environment(): Array = + arrayOf( + "staging", + StoreScopeValue { environment, _ -> setEnvironment(environment) }, + ENVIRONMENT_FILENAME, + DeleteScopeValue { setEnvironment(null) }, + DeletedEntityProvider { null }, + ) + private fun transaction(): Array = arrayOf( "MainActivity", @@ -296,6 +306,7 @@ class PersistingScopeObserverTest( request(), fingerprint(), level(), + environment(), transaction(), trace(), contexts(), diff --git a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt index edfcc361b08..8fa8eb05ef3 100644 --- a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt @@ -7,6 +7,7 @@ import io.sentry.NoOpSpan import io.sentry.PropagationContext import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.ScopeType import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.Span @@ -16,6 +17,7 @@ import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.W3CTraceparentHeader import io.sentry.protocol.SentryId +import io.sentry.test.createTestScopes import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals @@ -25,6 +27,7 @@ import kotlin.test.assertSame import kotlin.test.assertTrue import org.junit.Test import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -42,6 +45,9 @@ class TracingUtilsTest { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } .whenever(scopes) .configureScope(any()) + doAnswer { (it.arguments[1] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(anyOrNull(), any()) span = Span( TransactionContext("name", "op", TracesSamplingDecision(true)), @@ -300,6 +306,23 @@ class TracingUtilsTest { assertNotEquals(propagationContextBefore.spanId, fixture.scope.propagationContext.spanId) } + @Test + fun `baggage picks up the environment from the global scope`() { + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + environment = "options-environment" + } + val scopes = createTestScopes(options) + scopes.configureScope(ScopeType.GLOBAL) { it.environment = "global-environment" } + + val headers = TracingUtils.trace(scopes, null, null) + + assertNotNull(headers?.baggageHeader) { + assertTrue(it.value.contains("sentry-environment=global-environment")) + } + } + @Test fun `updates mutable baggage`() { fixture.setup()