From a952c2e7bfb60e5811c832470b413ffa74b5ab5d Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 5 Jun 2026 13:17:09 +0200 Subject: [PATCH 1/3] try to run replay tests on gh emulators --- .../androidTest/java/io/sentry/uitest/android/ReplayTest.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt index 5ea12ddbbc8..acac8041b54 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt @@ -16,12 +16,6 @@ import shark.IgnoredReferenceMatcher import shark.ReferencePattern class ReplayTest : BaseUiTest() { - @Before - fun setup() { - // we can't run on GH actions emulator, because they don't allow capturing screenshots properly - @Suppress("KotlinConstantConditions") - assumeThat(BuildConfig.ENVIRONMENT != "github", `is`(true)) - } @Test fun composeReplayDoesNotLeak() { From b8d1c8d48645245029f523b95d7aaf55e157df80 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Fri, 5 Jun 2026 11:20:38 +0000 Subject: [PATCH 2/3] Format code --- .../androidTest/java/io/sentry/uitest/android/ReplayTest.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt index acac8041b54..9572757124f 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt @@ -8,9 +8,6 @@ import kotlin.test.Test import leakcanary.LeakAssertions import leakcanary.LeakCanary import org.awaitility.kotlin.await -import org.hamcrest.CoreMatchers.`is` -import org.junit.Assume.assumeThat -import org.junit.Before import shark.AndroidReferenceMatchers import shark.IgnoredReferenceMatcher import shark.ReferencePattern From bfc18bd8d779a3b7b5e87a532b9ebf25013f91af Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 5 Jun 2026 16:12:11 +0200 Subject: [PATCH 3/3] feat(replay): fail fast on swallowed Compose masking errors in CI Add an internal SentryReplayDebug.failFast switch (gated on the io.sentry.replay.compose.fail-fast system property) that re-throws the exceptions ComposeViewHierarchyNode normally swallows in fromComposeNode and fromView. Enabled in the sentry-samples-android app and the on-device ReplayTest/ReplaySnapshotTest so our release/obfuscated builds running on real devices in CI crash instead of silently degrading masking. Defaults off, so customers are unaffected. Also add consumer proguard keep rules for the LayoutNode internals (getChildren/getOuterCoordinator/getCollapsedSemantics) that are looked up via reflection on Compose < 1.10, so R8 doesn't strip or rename them. Also guard the on-device tests against GitHub-hosted emulators, which can't capture screenshots reliably. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 +++++ .../io/sentry/uitest/android/ReplayTest.kt | 12 +++++++++ .../uitest/android/ReplaySnapshotTest.kt | 3 +++ sentry-android-replay/proguard-rules.pro | 6 +++++ .../android/replay/util/SentryReplayDebug.kt | 26 +++++++++++++++++++ .../viewhierarchy/ComposeViewHierarchyNode.kt | 12 +++++++++ .../sentry/samples/android/MyApplication.java | 5 ++++ 7 files changed, 70 insertions(+) create mode 100644 sentry-android-replay/src/main/java/io/sentry/android/replay/util/SentryReplayDebug.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a3b55b403..06c0b8bab55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Session Replay: Fix Compose view masking not working on obfuscated/minified builds ([#5503](https://github.com/getsentry/sentry-java/pull/5503)) + ## 8.43.1 ### Fixes diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt index 9572757124f..3827561e37c 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt @@ -8,11 +8,23 @@ import kotlin.test.Test import leakcanary.LeakAssertions import leakcanary.LeakCanary import org.awaitility.kotlin.await +import org.hamcrest.CoreMatchers.`is` +import org.junit.Assume.assumeThat +import org.junit.Before import shark.AndroidReferenceMatchers import shark.IgnoredReferenceMatcher import shark.ReferencePattern class ReplayTest : BaseUiTest() { + @Before + fun setup() { + // we can't run on GH actions emulator, because they don't allow capturing screenshots properly + @Suppress("KotlinConstantConditions") + assumeThat(BuildConfig.ENVIRONMENT != "github", `is`(true)) + // crash on swallowed Compose masking errors (e.g. broken obfuscated internals) so regressions + // fail this on-device test instead of silently under-masking (see SentryReplayDebug) + System.setProperty("io.sentry.replay.compose.fail-fast", "true") + } @Test fun composeReplayDoesNotLeak() { diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTestReplay/java/io/sentry/uitest/android/ReplaySnapshotTest.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTestReplay/java/io/sentry/uitest/android/ReplaySnapshotTest.kt index 1d82a3f8bc0..6d45b2d1f9c 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTestReplay/java/io/sentry/uitest/android/ReplaySnapshotTest.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTestReplay/java/io/sentry/uitest/android/ReplaySnapshotTest.kt @@ -23,6 +23,9 @@ class ReplaySnapshotTest : BaseUiTest() { // GH Actions emulators don't support capturing screenshots for replay @Suppress("KotlinConstantConditions") assumeThat(BuildConfig.ENVIRONMENT != "github", `is`(true)) + // crash on swallowed Compose masking errors (e.g. broken obfuscated internals) so regressions + // fail this on-device test instead of silently under-masking (see SentryReplayDebug) + System.setProperty("io.sentry.replay.compose.fail-fast", "true") } @Test diff --git a/sentry-android-replay/proguard-rules.pro b/sentry-android-replay/proguard-rules.pro index 42e3cb30a42..6ce45c1ef5d 100644 --- a/sentry-android-replay/proguard-rules.pro +++ b/sentry-android-replay/proguard-rules.pro @@ -29,3 +29,9 @@ # Rules to detect a PreviewView view to later mask it -dontwarn androidx.camera.view.PreviewView -keepnames class androidx.camera.view.PreviewView +# Rules to walk the Compose Node tree. +-keep class androidx.compose.ui.node.LayoutNode { + *** getChildren*(...); + *** getOuterCoordinator*(...); + *** getCollapsedSemantics*(...); +} \ No newline at end of file diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/SentryReplayDebug.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/SentryReplayDebug.kt new file mode 100644 index 00000000000..966428f84c2 --- /dev/null +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/SentryReplayDebug.kt @@ -0,0 +1,26 @@ +package io.sentry.android.replay.util + +/** + * Internal, undocumented escape hatch used to make Session Replay fail fast instead of silently + * degrading masking when an exception is swallowed (e.g. unsupported/obfuscated Compose internals). + * + * It is intended to be enabled only in our own sample/UI-test apps that run on real devices in CI + * (which are release/obfuscated builds, so [io.sentry.android.replay.BuildConfig.DEBUG] can't be + * used), so that regressions surface as crashes rather than under-masked replays. Customers should + * never set this. + * + * Enable via: + * ``` + * System.setProperty("io.sentry.replay.compose.fail-fast", "true") + * ``` + */ +internal object SentryReplayDebug { + private const val FAIL_FAST_PROPERTY = "io.sentry.replay.compose.fail-fast" + + /** + * Read live (not cached) so it's only evaluated on the error path and unit tests can toggle it + * between cases. + */ + val failFast: Boolean + get() = "true".equals(System.getProperty(FAIL_FAST_PROPERTY), ignoreCase = true) +} diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index a0312b69cd0..2b6bc3fc08e 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -22,6 +22,7 @@ import io.sentry.SentryLevel import io.sentry.SentryMaskingOptions import io.sentry.android.replay.SentryReplayModifiers import io.sentry.android.replay.util.ComposeTextLayout +import io.sentry.android.replay.util.SentryReplayDebug import io.sentry.android.replay.util.boundsInWindow import io.sentry.android.replay.util.findPainter import io.sentry.android.replay.util.findTextColor @@ -147,6 +148,12 @@ internal object ComposeViewHierarchyNode { ) } + // fail fast in our own sample/UI-test apps (see SentryReplayDebug), so regressions surface + // as crashes instead of silently degrading masking + if (SentryReplayDebug.failFast) { + throw t + } + // If we're unable to retrieve the semantics configuration // we should play safe and mask the whole node. return GenericViewHierarchyNode( @@ -291,6 +298,11 @@ internal object ComposeViewHierarchyNode { """ .trimIndent(), ) + // fail fast in our own sample/UI-test apps (see SentryReplayDebug), so regressions surface + // as crashes instead of silently skipping the whole Compose subtree (i.e. not masking it) + if (SentryReplayDebug.failFast) { + throw e + } return false } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index 572c4cdba72..f074901f4f0 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -9,6 +9,11 @@ public class MyApplication extends Application { @Override public void onCreate() { + // Make Session Replay fail fast instead of silently degrading masking when an exception is + // swallowed (e.g. unsupported/obfuscated Compose internals). This way regressions surface as + // crashes in our release/obfuscated builds that run on real devices in CI. Only meant for our + // own sample/UI-test apps, customers should never set this. + System.setProperty("io.sentry.replay.compose.fail-fast", "true"); Sentry.startProfiler(); strictMode(); super.onCreate();