From ce9d432cf7e429615897c39572df0b27ffa6be79 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 6 Aug 2026 15:35:04 +0200 Subject: [PATCH 1/2] fix(core): Clear contexts on Scope.clear() (JAVA-683) Scope.clear() reset every field except contexts, so anything set via setContexts survived a clear. Contexts had no clear() method, unlike the tags, extra, and fingerprint collections, which is why it was skipped. Add Contexts.clear(), override it in CombinedContextsView to delegate to the default context, and clear contexts in Scope.clear() while notifying scope observers so persisted scopes stay in sync. Co-Authored-By: Claude Opus 4.8 --- sentry/api/sentry.api | 2 ++ .../java/io/sentry/CombinedContextsView.java | 5 +++++ sentry/src/main/java/io/sentry/Scope.java | 5 +++++ .../main/java/io/sentry/protocol/Contexts.java | 4 ++++ .../java/io/sentry/CombinedContextsViewTest.kt | 14 ++++++++++++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 17 +++++++++++++++++ .../java/io/sentry/protocol/ContextsTest.kt | 12 ++++++++++++ 7 files changed, 59 insertions(+) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 51cffae3302..43de4164a4f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -227,6 +227,7 @@ public final class io/sentry/CheckInStatus : java/lang/Enum { public final class io/sentry/CombinedContextsView : io/sentry/protocol/Contexts { public fun (Lio/sentry/protocol/Contexts;Lio/sentry/protocol/Contexts;Lio/sentry/protocol/Contexts;Lio/sentry/ScopeType;)V + public fun clear ()V public fun containsKey (Ljava/lang/Object;)Z public fun entrySet ()Ljava/util/Set; public fun get (Ljava/lang/Object;)Ljava/lang/Object; @@ -5795,6 +5796,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { protected final field responseLock Lio/sentry/util/AutoClosableReentrantLock; public fun ()V public fun (Lio/sentry/protocol/Contexts;)V + public fun clear ()V public fun containsKey (Ljava/lang/Object;)Z public fun entrySet ()Ljava/util/Set; public fun equals (Ljava/lang/Object;)Z diff --git a/sentry/src/main/java/io/sentry/CombinedContextsView.java b/sentry/src/main/java/io/sentry/CombinedContextsView.java index 3cb10e88b81..d9a8b14c628 100644 --- a/sentry/src/main/java/io/sentry/CombinedContextsView.java +++ b/sentry/src/main/java/io/sentry/CombinedContextsView.java @@ -293,6 +293,11 @@ public boolean containsKey(final @Nullable Object key) { return getDefaultContexts().remove(key); } + @Override + public void clear() { + getDefaultContexts().clear(); + } + @Override public @NotNull Enumeration keys() { return mergeContexts().keys(); diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 195e5b5b05a..25f36cd3f59 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -575,10 +575,15 @@ public void clear() { tags.clear(); attributes.clear(); extra.clear(); + contexts.clear(); eventProcessors.clear(); clearTransaction(); clearAttachments(); clearFeatureFlags(); + + for (final IScopeObserver observer : options.getScopeObservers()) { + observer.setContexts(contexts); + } } /** diff --git a/sentry/src/main/java/io/sentry/protocol/Contexts.java b/sentry/src/main/java/io/sentry/protocol/Contexts.java index 83a770eb0fc..f6ba635383a 100644 --- a/sentry/src/main/java/io/sentry/protocol/Contexts.java +++ b/sentry/src/main/java/io/sentry/protocol/Contexts.java @@ -248,6 +248,10 @@ public boolean containsKey(final @Nullable Object key) { return internalStorage.remove(key); } + public void clear() { + internalStorage.clear(); + } + public @NotNull Enumeration keys() { return internalStorage.keys(); } diff --git a/sentry/src/test/java/io/sentry/CombinedContextsViewTest.kt b/sentry/src/test/java/io/sentry/CombinedContextsViewTest.kt index 43ded1ad2ae..0fb7d07c0a0 100644 --- a/sentry/src/test/java/io/sentry/CombinedContextsViewTest.kt +++ b/sentry/src/test/java/io/sentry/CombinedContextsViewTest.kt @@ -605,6 +605,20 @@ class CombinedContextsViewTest { assertEquals("global", fixture.global.get("test")) } + @Test + fun `clear clears default context only`() { + val combined = fixture.getSut() + fixture.current.put("test", "current") + fixture.isolation.put("test", "isolation") + fixture.global.put("test", "global") + + combined.clear() + + assertEquals("current", fixture.current.get("test")) + assertNull(fixture.isolation.get("test")) + assertEquals("global", fixture.global.get("test")) + } + @Test fun `set null value on context does not cause exception`() { 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 7af4f6ccdca..be6a22516c0 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -292,6 +292,7 @@ class ScopeTest { scope.screen = "MainActivity" scope.setExtra("some", "extra") scope.setAttribute("some", "attribute") + scope.setContexts("some", "context") scope.addEventProcessor(eventProcessor()) scope.addAttachment(Attachment("path")) scope.addFeatureFlag("flag", true) @@ -308,6 +309,7 @@ class ScopeTest { assertEquals(0, scope.tags.size) assertEquals(0, scope.attributes.size) assertEquals(0, scope.extras.size) + assertTrue(scope.contexts.isEmpty) assertEquals(0, scope.eventProcessors.size) assertEquals(0, scope.attachments.size) assertEquals(0, scope.featureFlags!!.values.size) @@ -886,6 +888,21 @@ class ScopeTest { verify(observer).setContexts(argThat { (get("test") as Obj).stuff == 3 }) } + @Test + fun `Scope clear contexts sync scopes`() { + val observer = mock() + val options = SentryOptions().apply { addScopeObserver(observer) } + val scope = Scope(options) + + // Populate the live contexts directly so the observer is only notified by clear(). + scope.contexts.set("test", "value") + assertFalse(scope.contexts.isEmpty) + + scope.clear() + assertTrue(scope.contexts.isEmpty) + verify(observer).setContexts(argThat { isEmpty }) + } + @Test fun `Scope set propagation context sync scopes`() { val observer = mock() diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt index 0035b733b43..7a3f425e997 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt @@ -7,6 +7,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNotSame +import kotlin.test.assertTrue class ContextsTest { @Test @@ -137,4 +138,15 @@ class ContextsTest { assertEquals(listOf("a"), contexts.keys().toList()) } + + @Test + fun `clear removes all entries from context`() { + val contexts = Contexts() + contexts.put("a", "1") + contexts.put("b", "2") + + contexts.clear() + + assertTrue(contexts.isEmpty) + } } From a590282db05f488b1836ae5662af75d9d8f47f3d Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Thu, 6 Aug 2026 15:36:00 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6130860ee36..1da07233a0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- Clear contexts when calling `Scope.clear()` ([#5902](https://github.com/getsentry/sentry-java/pull/5902)) - Preserve custom `Throwable` identities when R8 optimizes Android apps ([#5881](https://github.com/getsentry/sentry-java/pull/5881)) ## 8.52.0