From 043415ac67eb28b8697eeca199a4d62a77549ba7 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 13 May 2026 09:44:00 +0200 Subject: [PATCH 1/2] feat(core): Add API to clear scope feature flags Allow feature flags stored on a scope to be cleared without resetting other scope data. Scope.clear now also resets the feature flag buffer so stale flag evaluations do not carry over after clearing a scope. Fixes #5422 Co-Authored-By: Claude --- sentry/api/sentry.api | 8 ++++++++ .../src/main/java/io/sentry/CombinedScopeView.java | 5 +++++ sentry/src/main/java/io/sentry/IScope.java | 2 ++ sentry/src/main/java/io/sentry/NoOpScope.java | 3 +++ sentry/src/main/java/io/sentry/Scope.java | 6 ++++++ .../io/sentry/featureflags/FeatureFlagBuffer.java | 7 +++++++ .../io/sentry/featureflags/IFeatureFlagBuffer.java | 2 ++ .../sentry/featureflags/NoOpFeatureFlagBuffer.java | 3 +++ .../sentry/featureflags/SpanFeatureFlagBuffer.java | 7 +++++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 14 ++++++++++++++ .../sentry/featureflags/FeatureFlagBufferTest.kt | 13 +++++++++++++ .../featureflags/SpanFeatureFlagBufferTest.kt | 12 ++++++++++++ 12 files changed, 82 insertions(+) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a433abbb37..e48c03ffb1 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -273,6 +273,7 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun clear ()V public fun clearAttachments ()V public fun clearBreadcrumbs ()V + public fun clearFeatureFlags ()V public fun clearSession ()V public fun clearTransaction ()V public fun clone ()Lio/sentry/IScope; @@ -901,6 +902,7 @@ public abstract interface class io/sentry/IScope { public abstract fun clear ()V public abstract fun clearAttachments ()V public abstract fun clearBreadcrumbs ()V + public abstract fun clearFeatureFlags ()V public abstract fun clearSession ()V public abstract fun clearTransaction ()V public abstract fun clone ()Lio/sentry/IScope; @@ -1715,6 +1717,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun clear ()V public fun clearAttachments ()V public fun clearBreadcrumbs ()V + public fun clearFeatureFlags ()V public fun clearSession ()V public fun clearTransaction ()V public fun clone ()Lio/sentry/IScope; @@ -2401,6 +2404,7 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun clear ()V public fun clearAttachments ()V public fun clearBreadcrumbs ()V + public fun clearFeatureFlags ()V public fun clearSession ()V public fun clearTransaction ()V public fun clone ()Lio/sentry/IScope; @@ -5039,6 +5043,7 @@ public final class io/sentry/exception/SentryHttpClientException : java/lang/Exc public final class io/sentry/featureflags/FeatureFlagBuffer : io/sentry/featureflags/IFeatureFlagBuffer { public fun add (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun clear ()V public fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; public synthetic fun clone ()Ljava/lang/Object; public static fun create (Lio/sentry/SentryOptions;)Lio/sentry/featureflags/IFeatureFlagBuffer; @@ -5048,6 +5053,7 @@ public final class io/sentry/featureflags/FeatureFlagBuffer : io/sentry/featuref public abstract interface class io/sentry/featureflags/IFeatureFlagBuffer { public abstract fun add (Ljava/lang/String;Ljava/lang/Boolean;)V + public abstract fun clear ()V public abstract fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; public abstract fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; } @@ -5055,6 +5061,7 @@ public abstract interface class io/sentry/featureflags/IFeatureFlagBuffer { public final class io/sentry/featureflags/NoOpFeatureFlagBuffer : io/sentry/featureflags/IFeatureFlagBuffer { public fun ()V public fun add (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun clear ()V public fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; public synthetic fun clone ()Ljava/lang/Object; public fun getFeatureFlags ()Lio/sentry/protocol/FeatureFlags; @@ -5063,6 +5070,7 @@ public final class io/sentry/featureflags/NoOpFeatureFlagBuffer : io/sentry/feat public final class io/sentry/featureflags/SpanFeatureFlagBuffer : io/sentry/featureflags/IFeatureFlagBuffer { public fun add (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun clear ()V public fun clone ()Lio/sentry/featureflags/IFeatureFlagBuffer; public synthetic fun clone ()Ljava/lang/Object; public static fun create ()Lio/sentry/featureflags/IFeatureFlagBuffer; diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index 0c61bdf912..f21f8697fa 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -549,6 +549,11 @@ public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean } } + @Override + public void clearFeatureFlags() { + getDefaultWriteScope().clearFeatureFlags(); + } + @Override public @Nullable FeatureFlags getFeatureFlags() { return getFeatureFlagBuffer().getFeatureFlags(); diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java index ccab8dbdeb..5b6c38bbcf 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -465,6 +465,8 @@ void setSpanContext( void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result); + void clearFeatureFlags(); + @ApiStatus.Internal @Nullable FeatureFlags getFeatureFlags(); diff --git a/sentry/src/main/java/io/sentry/NoOpScope.java b/sentry/src/main/java/io/sentry/NoOpScope.java index 7693ab81de..9d2f603c67 100644 --- a/sentry/src/main/java/io/sentry/NoOpScope.java +++ b/sentry/src/main/java/io/sentry/NoOpScope.java @@ -321,6 +321,9 @@ public void removeAttribute(@Nullable String key) {} @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} + @Override + public void clearFeatureFlags() {} + @Override public @Nullable FeatureFlags getFeatureFlags() { return null; diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index fa44e90a19..9e8d3ee554 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -574,6 +574,7 @@ public void clear() { eventProcessors.clear(); clearTransaction(); clearAttachments(); + clearFeatureFlags(); } /** @@ -1211,6 +1212,11 @@ public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean featureFlags.add(flag, result); } + @Override + public void clearFeatureFlags() { + featureFlags.clear(); + } + @Override public @Nullable FeatureFlags getFeatureFlags() { return featureFlags.getFeatureFlags(); diff --git a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java index f38d0b6db5..fc696b5948 100644 --- a/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/FeatureFlagBuffer.java @@ -69,6 +69,13 @@ public void add(final @Nullable String flag, final @Nullable Boolean result) { } } + @Override + public void clear() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + flags.clear(); + } + } + @Override public @Nullable FeatureFlags getFeatureFlags() { List featureFlags = new ArrayList<>(); diff --git a/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java index 7f12026a59..90a503cce4 100644 --- a/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/IFeatureFlagBuffer.java @@ -9,6 +9,8 @@ public interface IFeatureFlagBuffer { void add(final @Nullable String flag, final @Nullable Boolean result); + void clear(); + @Nullable FeatureFlags getFeatureFlags(); diff --git a/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java index 3bfc8f8fd2..e093531149 100644 --- a/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/NoOpFeatureFlagBuffer.java @@ -16,6 +16,9 @@ public static NoOpFeatureFlagBuffer getInstance() { @Override public void add(final @Nullable String flag, final @Nullable Boolean result) {} + @Override + public void clear() {} + @Override public @Nullable FeatureFlags getFeatureFlags() { return null; diff --git a/sentry/src/main/java/io/sentry/featureflags/SpanFeatureFlagBuffer.java b/sentry/src/main/java/io/sentry/featureflags/SpanFeatureFlagBuffer.java index 2afa45d38d..d31bc231d4 100644 --- a/sentry/src/main/java/io/sentry/featureflags/SpanFeatureFlagBuffer.java +++ b/sentry/src/main/java/io/sentry/featureflags/SpanFeatureFlagBuffer.java @@ -48,6 +48,13 @@ public void add(final @Nullable String flag, final @Nullable Boolean result) { } } + @Override + public void clear() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + flags = null; + } + } + @Override public @Nullable FeatureFlags getFeatureFlags() { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index 7093473a60..4b0047fdc1 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -294,6 +294,7 @@ class ScopeTest { scope.setAttribute("some", "attribute") scope.addEventProcessor(eventProcessor()) scope.addAttachment(Attachment("path")) + scope.addFeatureFlag("flag", true) scope.clear() @@ -309,6 +310,7 @@ class ScopeTest { assertEquals(0, scope.extras.size) assertEquals(0, scope.eventProcessors.size) assertEquals(0, scope.attachments.size) + assertEquals(0, scope.featureFlags!!.values.size) } @Test @@ -1155,6 +1157,18 @@ class ScopeTest { assertEquals(0, flags.values.size) } + @Test + fun `feature flags can be cleared`() { + val scope = Scope(SentryOptions.empty()) + + scope.addFeatureFlag("flag1", true) + scope.clearFeatureFlags() + + val flags = scope.featureFlags + assertNotNull(flags) + assertEquals(0, flags.values.size) + } + @Test fun `setAttribute stores attribute on scope`() { val scope = Scope(SentryOptions()) diff --git a/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt b/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt index 471ba880eb..8ec18ce02b 100644 --- a/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt +++ b/sentry/src/test/java/io/sentry/featureflags/FeatureFlagBufferTest.kt @@ -33,6 +33,19 @@ class FeatureFlagBufferTest { assertFalse(featureFlagValues[1]!!.result) } + @Test + fun `clears values`() { + val buffer = FeatureFlagBuffer.create(SentryOptions().also { it.maxFeatureFlags = 2 }) + buffer.add("a", true) + buffer.add("b", false) + + buffer.clear() + + val featureFlags = buffer.featureFlags + assertNotNull(featureFlags) + assertEquals(0, featureFlags.values.size) + } + @Test fun `drops oldest entry when limit is reached`() { val buffer = FeatureFlagBuffer.create(SentryOptions().also { it.maxFeatureFlags = 2 }) diff --git a/sentry/src/test/java/io/sentry/featureflags/SpanFeatureFlagBufferTest.kt b/sentry/src/test/java/io/sentry/featureflags/SpanFeatureFlagBufferTest.kt index 07c3feaf5c..c6c89d9f3a 100644 --- a/sentry/src/test/java/io/sentry/featureflags/SpanFeatureFlagBufferTest.kt +++ b/sentry/src/test/java/io/sentry/featureflags/SpanFeatureFlagBufferTest.kt @@ -4,6 +4,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue class SpanFeatureFlagBufferTest { @@ -26,6 +27,17 @@ class SpanFeatureFlagBufferTest { assertFalse(featureFlagValues[1]!!.result) } + @Test + fun `clears values`() { + val buffer = SpanFeatureFlagBuffer.create() + buffer.add("a", true) + buffer.add("b", false) + + buffer.clear() + + assertNull(buffer.featureFlags) + } + @Test fun `rejects new entries when limit is reached`() { val buffer = SpanFeatureFlagBuffer.create() From 1296c9f2d23c69b49ff815f51ba013a9fc5c2ac8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 13 May 2026 09:47:00 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceda85d8b9..b54d2e058e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add API to clear feature flags from scopes ([#5426](https://github.com/getsentry/sentry-java/pull/5426)) - Add support to configure reporting historical ANRs via `AndroidManifest.xml` using the `io.sentry.anr.report-historical` attribute ([#5387](https://github.com/getsentry/sentry-java/pull/5387)) ## 8.41.0