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..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 @@ -21,6 +21,9 @@ class ReplayTest : BaseUiTest() { // 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 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();