From 23fc2148e3d54593ac41ff8a45797a4c41030380 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 16 Jul 2026 15:14:07 +0200 Subject: [PATCH 1/9] added get/set environment to scope --- .../ApplicationExitInfoEventProcessor.java | 6 ++++ .../ApplicationExitInfoEventProcessorTest.kt | 12 +++++++ sentry/api/sentry.api | 12 +++++++ .../java/io/sentry/CombinedScopeView.java | 18 ++++++++++ sentry/src/main/java/io/sentry/IScope.java | 16 +++++++++ .../main/java/io/sentry/IScopeObserver.java | 2 ++ sentry/src/main/java/io/sentry/NoOpScope.java | 8 +++++ sentry/src/main/java/io/sentry/Scope.java | 30 +++++++++++++++++ .../java/io/sentry/ScopeObserverAdapter.java | 3 ++ .../src/main/java/io/sentry/SentryClient.java | 3 ++ .../sentry/cache/PersistingScopeObserver.java | 14 ++++++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 30 +++++++++++++++++ .../test/java/io/sentry/SentryClientTest.kt | 33 ++++++++++++------- .../cache/PersistingScopeObserverTest.kt | 11 +++++++ 14 files changed, 187 insertions(+), 11 deletions(-) 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 2eca0e68b5b..cf672aad929 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 @@ -460,6 +460,12 @@ private void setRelease(final @NotNull SentryBaseEvent event) { private void setEnvironment(final @NotNull SentryBaseEvent event) { if (event.getEnvironment() == null) { + final String scopeEnvironment = + readFromDisk(options, PersistingScopeObserver.ENVIRONMENT_FILENAME, String.class); + if (scopeEnvironment != null) { + event.setEnvironment(scopeEnvironment); + return; + } final String environment = PersistingOptionsObserver.read(options, ENVIRONMENT_FILENAME, String.class); event.setEnvironment(environment != null ? environment : options.getEnvironment()); 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 e7583429910..aada8810151 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 @@ -398,6 +398,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 dist is not persisted, backfills it from release`() { val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index c0b6fec780c..a0519d1b6a5 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -285,6 +285,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; @@ -320,6 +321,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 @@ -921,6 +923,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; @@ -956,6 +959,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 @@ -983,6 +987,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 @@ -1743,6 +1748,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; @@ -1779,6 +1785,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 @@ -2431,6 +2438,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; @@ -2466,6 +2474,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 @@ -2514,6 +2523,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 @@ -4867,6 +4877,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; @@ -4883,6 +4894,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/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index ea2d752d44b..942598cacc6 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(); diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java index 5b6c38bbcfb..df034ddfc78 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -26,6 +26,22 @@ 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()} when + * applied to events. Mainly used by hybrid SDKs to sync their environment into this SDK. + * + * @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/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index f5c57f5ac5d..5be5c694833 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -53,6 +53,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; @@ -137,6 +140,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; @@ -224,6 +228,32 @@ 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. Mainly used by hybrid SDKs (e.g. .NET, Unity) to sync their + * environment into this SDK. Takes precedence over {@link SentryOptions#getEnvironment()} when + * applied to events. + * + * @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. * 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/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 262b3a93034..a71ae6a3fe5 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1547,6 +1547,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/cache/PersistingScopeObserver.java b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java index 420d0d31e22..4dde96afcc2 100644 --- a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java +++ b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java @@ -46,6 +46,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,18 @@ public void setLevel(@Nullable SentryLevel level) { }); } + @Override + public void setEnvironment(@Nullable String environment) { + serializeToDisk( + () -> { + if (environment == null) { + delete(ENVIRONMENT_FILENAME); + } else { + store(environment, ENVIRONMENT_FILENAME); + } + }); + } + @Override public void setTransaction(@Nullable String transaction) { serializeToDisk( @@ -296,6 +309,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/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index 4b0047fdc18..6c5f25747a0 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -783,6 +783,36 @@ class ScopeTest { verify(observer).setLevel(eq(WARNING)) } + @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/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index ea64675570d..9298ecebee0 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -570,24 +570,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 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(), From 461a85d4cb485240cb867f20ec0080ab5447b338 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 17 Jul 2026 14:56:48 +0200 Subject: [PATCH 2/9] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3baff8d6b2..ea34c8953a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ import ``` +- Added `Environment` to the `Scope` ([#5765](https://github.com/getsentry/sentry-java/pull/5765)) ### Fixes From f3b38ec18101dd4be1e2ffcc26ba89722be4897d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 17 Jul 2026 15:18:24 +0200 Subject: [PATCH 3/9] updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b424e89bff..197df9c450f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Added `Environment` to the `Scope` ([#5765](https://github.com/getsentry/sentry-java/pull/5765)) +- Added `Environment` to the `Scope` ([#5772](https://github.com/getsentry/sentry-java/pull/5772)) ### Fixes From a71958eec6777a2f5fd9ca0d4ad91cdffea7fba8 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 27 Jul 2026 13:35:26 +0200 Subject: [PATCH 4/9] sync scope clear --- sentry/src/main/java/io/sentry/Scope.java | 1 + sentry/src/test/java/io/sentry/ScopeTest.kt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 5be5c694833..6e0a6f5a18a 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -592,6 +592,7 @@ public ITransaction getTransaction() { @Override public void clear() { level = null; + setEnvironment(null); user = null; request = null; screen = null; diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index 2f7d41ff9a9..c44c1a63223 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.addEventProcessor(eventProcessor()) @@ -303,6 +304,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) From 879e15ad943438ac53c066949cc5e43cdd29f5f2 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 27 Jul 2026 15:36:35 +0200 Subject: [PATCH 5/9] clear --- .../ApplicationExitInfoEventProcessor.java | 20 +++++++++++-------- .../ApplicationExitInfoEventProcessorTest.kt | 1 + sentry/src/main/java/io/sentry/Scope.java | 2 +- sentry/src/test/java/io/sentry/ScopeTest.kt | 11 ++++++++++ 4 files changed, 25 insertions(+), 9 deletions(-) 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 33c4ffbb402..bbfd41bda18 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 @@ -166,7 +166,7 @@ public ApplicationExitInfoEventProcessor( if (!backfillable.shouldEnrich()) { setRelease(event, optionsSource); - setEnvironment(event, optionsSource); + setEnvironment(event, optionsSource, false); setDist(event, optionsSource); setAppVersionAndBuild(event); options @@ -411,7 +411,7 @@ private void setRequest(final @NotNull SentryBaseEvent event) { private void backfillOptions( final @NotNull SentryEvent event, final @NotNull OptionsSource optionsSource) { setRelease(event, optionsSource); - setEnvironment(event, optionsSource); + setEnvironment(event, optionsSource, true); setDist(event, optionsSource); setDebugMeta(event, optionsSource); setSdk(event, optionsSource); @@ -479,13 +479,17 @@ private void setRelease( } private void setEnvironment( - final @NotNull SentryBaseEvent event, final @NotNull OptionsSource optionsSource) { + final @NotNull SentryBaseEvent event, + final @NotNull OptionsSource optionsSource, + final boolean readScopeEnvironment) { if (event.getEnvironment() == null) { - final String scopeEnvironment = - readFromDisk(options, PersistingScopeObserver.ENVIRONMENT_FILENAME, String.class); - if (scopeEnvironment != null) { - event.setEnvironment(scopeEnvironment); - return; + if (readScopeEnvironment) { + final String scopeEnvironment = + readFromDisk(options, PersistingScopeObserver.ENVIRONMENT_FILENAME, String.class); + if (scopeEnvironment != null) { + event.setEnvironment(scopeEnvironment); + return; + } } event.setEnvironment( getLaunchOption( 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 b2eab52c4d6..e8aebffec9b 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 @@ -494,6 +494,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/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 6e0a6f5a18a..4be03d982ef 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -591,7 +591,7 @@ public ITransaction getTransaction() { /** Resets the Scope to its default state */ @Override public void clear() { - level = null; + setLevel(null); setEnvironment(null); user = null; request = null; diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index c44c1a63223..7a49a894816 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -783,6 +783,17 @@ 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() From c66e379971636ef33d1bc4eb787f6f556bbcdd09 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 27 Jul 2026 16:00:34 +0200 Subject: [PATCH 6/9] apply to replay --- sentry/src/main/java/io/sentry/SentryClient.java | 3 +++ .../src/test/java/io/sentry/SentryClientTest.kt | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 45f424072df..15a28bfd80a 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1523,6 +1523,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 { diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 34bad35900a..aa744cb671a 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -3596,6 +3596,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 } From 8ad87f797ed86767e4350c6a654f7d7e3e3a8922 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 28 Jul 2026 12:00:20 +0200 Subject: [PATCH 7/9] scope helper --- .../ApplicationExitInfoEventProcessor.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) 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 bbfd41bda18..d08d0ec1f10 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 @@ -166,7 +166,7 @@ public ApplicationExitInfoEventProcessor( if (!backfillable.shouldEnrich()) { setRelease(event, optionsSource); - setEnvironment(event, optionsSource, false); + setEnvironment(event, optionsSource); setDist(event, optionsSource); setAppVersionAndBuild(event); options @@ -196,6 +196,7 @@ private void backfillScope( setRequest(event); setUser(event); setScopeTags(event); + setScopeEnvironment(event); setBreadcrumbs(event); setExtras(event); setContexts(event); @@ -411,7 +412,7 @@ private void setRequest(final @NotNull SentryBaseEvent event) { private void backfillOptions( final @NotNull SentryEvent event, final @NotNull OptionsSource optionsSource) { setRelease(event, optionsSource); - setEnvironment(event, optionsSource, true); + setEnvironment(event, optionsSource); setDist(event, optionsSource); setDebugMeta(event, optionsSource); setSdk(event, optionsSource); @@ -479,24 +480,24 @@ private void setRelease( } private void setEnvironment( - final @NotNull SentryBaseEvent event, - final @NotNull OptionsSource optionsSource, - final boolean readScopeEnvironment) { + final @NotNull SentryBaseEvent event, final @NotNull OptionsSource optionsSource) { if (event.getEnvironment() == null) { - if (readScopeEnvironment) { - final String scopeEnvironment = - readFromDisk(options, PersistingScopeObserver.ENVIRONMENT_FILENAME, String.class); - if (scopeEnvironment != null) { - event.setEnvironment(scopeEnvironment); - return; - } - } event.setEnvironment( getLaunchOption( ENVIRONMENT_FILENAME, String.class, options.getEnvironment(), optionsSource)); } } + 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(); From 75d361fd4cb06dbee8d6ae42f3c1fe14f752f600 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 28 Jul 2026 12:05:10 +0200 Subject: [PATCH 8/9] comment --- sentry/src/main/java/io/sentry/Scope.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 69088b2f124..d364d3b1d6b 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -240,8 +240,7 @@ public void setLevel(final @Nullable SentryLevel level) { } /** - * Sets the Scope's environment. Mainly used by hybrid SDKs (e.g. .NET, Unity) to sync their - * environment into this SDK. Takes precedence over {@link SentryOptions#getEnvironment()} when + * Sets the Scope's environment. Takes precedence over {@link SentryOptions#getEnvironment()} when * applied to events. * * @param environment the environment From 8da38a8c80127447d464c889ee8dce1bbba3a961 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 28 Jul 2026 12:33:06 +0200 Subject: [PATCH 9/9] made scope apply its environment on other events too --- .../java/io/sentry/CombinedScopeView.java | 6 ++++- sentry/src/main/java/io/sentry/IScope.java | 3 +-- sentry/src/main/java/io/sentry/Scope.java | 13 ++++++--- sentry/src/main/java/io/sentry/Sentry.java | 4 +++ .../src/main/java/io/sentry/SentryClient.java | 14 +++++++--- .../java/io/sentry/CombinedScopeViewTest.kt | 11 ++++++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 15 +++++++++++ .../test/java/io/sentry/SentryClientTest.kt | 12 +++++++++ sentry/src/test/java/io/sentry/SentryTest.kt | 27 +++++++++++++++++++ 9 files changed, 95 insertions(+), 10 deletions(-) diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index 942598cacc6..cb2ff8c6401 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -558,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 df034ddfc78..ee512c7441f 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -35,8 +35,7 @@ public interface IScope { String getEnvironment(); /** - * Sets the Scope's environment. Takes precedence over {@link SentryOptions#getEnvironment()} when - * applied to events. Mainly used by hybrid SDKs to sync their environment into this SDK. + * Sets the Scope's environment. Takes precedence over {@link SentryOptions#getEnvironment()}. * * @param environment the environment */ diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index d364d3b1d6b..95dec3e85b0 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -240,8 +240,7 @@ public void setLevel(final @Nullable SentryLevel level) { } /** - * Sets the Scope's environment. Takes precedence over {@link SentryOptions#getEnvironment()} when - * applied to events. + * Sets the Scope's environment. Takes precedence over {@link SentryOptions#getEnvironment()}. * * @param environment the environment */ @@ -1062,6 +1061,11 @@ 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()) { @@ -1076,7 +1080,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/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 8bba9d92e4f..d9131f0c4f5 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -579,6 +579,10 @@ private static void notifyOptionsObservers(final @NotNull SentryOptions options) options.findPersistingScopeObserver(); if (scopeCache != null) { scopeCache.resetCache(); + 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 4e9690afeba..2fa4531fc50 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1156,6 +1156,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()); } @@ -1164,10 +1168,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() @@ -1501,6 +1501,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()); } @@ -1537,6 +1540,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) { 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 5e045328753..3226bef585a 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -464,6 +464,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" } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 83af24c1b5a..6e9ede92298 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -1016,6 +1016,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")) } @@ -3868,6 +3878,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 @@ -3877,6 +3888,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 8d05697fda4..cd1c251f84b 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -7,6 +7,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 @@ -173,6 +174,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()