Skip to content

Commit 9eb8255

Browse files
romtsnclaude
andcommitted
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) <noreply@anthropic.com>
1 parent b8d1c8d commit 9eb8255

6 files changed

Lines changed: 64 additions & 0 deletions

File tree

sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/ReplayTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,23 @@ import kotlin.test.Test
88
import leakcanary.LeakAssertions
99
import leakcanary.LeakCanary
1010
import org.awaitility.kotlin.await
11+
import org.hamcrest.CoreMatchers.`is`
12+
import org.junit.Assume.assumeThat
13+
import org.junit.Before
1114
import shark.AndroidReferenceMatchers
1215
import shark.IgnoredReferenceMatcher
1316
import shark.ReferencePattern
1417

1518
class ReplayTest : BaseUiTest() {
19+
@Before
20+
fun setup() {
21+
// we can't run on GH actions emulator, because they don't allow capturing screenshots properly
22+
@Suppress("KotlinConstantConditions")
23+
assumeThat(BuildConfig.ENVIRONMENT != "github", `is`(true))
24+
// crash on swallowed Compose masking errors (e.g. broken obfuscated internals) so regressions
25+
// fail this on-device test instead of silently under-masking (see SentryReplayDebug)
26+
System.setProperty("io.sentry.replay.compose.fail-fast", "true")
27+
}
1628

1729
@Test
1830
fun composeReplayDoesNotLeak() {

sentry-android-integration-tests/sentry-uitest-android/src/androidTestReplay/java/io/sentry/uitest/android/ReplaySnapshotTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class ReplaySnapshotTest : BaseUiTest() {
2323
// GH Actions emulators don't support capturing screenshots for replay
2424
@Suppress("KotlinConstantConditions")
2525
assumeThat(BuildConfig.ENVIRONMENT != "github", `is`(true))
26+
// crash on swallowed Compose masking errors (e.g. broken obfuscated internals) so regressions
27+
// fail this on-device test instead of silently under-masking (see SentryReplayDebug)
28+
System.setProperty("io.sentry.replay.compose.fail-fast", "true")
2629
}
2730

2831
@Test

sentry-android-replay/proguard-rules.pro

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@
2929
# Rules to detect a PreviewView view to later mask it
3030
-dontwarn androidx.camera.view.PreviewView
3131
-keepnames class androidx.camera.view.PreviewView
32+
# Rules to walk the Compose Node tree.
33+
-keep class androidx.compose.ui.node.LayoutNode {
34+
*** getChildren*(...);
35+
*** getOuterCoordinator*(...);
36+
*** getCollapsedSemantics*(...);
37+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.sentry.android.replay.util
2+
3+
/**
4+
* Internal, undocumented escape hatch used to make Session Replay fail fast instead of silently
5+
* degrading masking when an exception is swallowed (e.g. unsupported/obfuscated Compose internals).
6+
*
7+
* It is intended to be enabled only in our own sample/UI-test apps that run on real devices in CI
8+
* (which are release/obfuscated builds, so [io.sentry.android.replay.BuildConfig.DEBUG] can't be
9+
* used), so that regressions surface as crashes rather than under-masked replays. Customers should
10+
* never set this.
11+
*
12+
* Enable via:
13+
* ```
14+
* System.setProperty("io.sentry.replay.compose.fail-fast", "true")
15+
* ```
16+
*/
17+
internal object SentryReplayDebug {
18+
private const val FAIL_FAST_PROPERTY = "io.sentry.replay.compose.fail-fast"
19+
20+
/**
21+
* Read live (not cached) so it's only evaluated on the error path and unit tests can toggle it
22+
* between cases.
23+
*/
24+
val failFast: Boolean
25+
get() = "true".equals(System.getProperty(FAIL_FAST_PROPERTY), ignoreCase = true)
26+
}

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import io.sentry.SentryLevel
2222
import io.sentry.SentryMaskingOptions
2323
import io.sentry.android.replay.SentryReplayModifiers
2424
import io.sentry.android.replay.util.ComposeTextLayout
25+
import io.sentry.android.replay.util.SentryReplayDebug
2526
import io.sentry.android.replay.util.boundsInWindow
2627
import io.sentry.android.replay.util.findPainter
2728
import io.sentry.android.replay.util.findTextColor
@@ -147,6 +148,12 @@ internal object ComposeViewHierarchyNode {
147148
)
148149
}
149150

151+
// fail fast in our own sample/UI-test apps (see SentryReplayDebug), so regressions surface
152+
// as crashes instead of silently degrading masking
153+
if (SentryReplayDebug.failFast) {
154+
throw t
155+
}
156+
150157
// If we're unable to retrieve the semantics configuration
151158
// we should play safe and mask the whole node.
152159
return GenericViewHierarchyNode(
@@ -291,6 +298,11 @@ internal object ComposeViewHierarchyNode {
291298
"""
292299
.trimIndent(),
293300
)
301+
// fail fast in our own sample/UI-test apps (see SentryReplayDebug), so regressions surface
302+
// as crashes instead of silently skipping the whole Compose subtree (i.e. not masking it)
303+
if (SentryReplayDebug.failFast) {
304+
throw e
305+
}
294306
return false
295307
}
296308

sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ public class MyApplication extends Application {
99

1010
@Override
1111
public void onCreate() {
12+
// Make Session Replay fail fast instead of silently degrading masking when an exception is
13+
// swallowed (e.g. unsupported/obfuscated Compose internals). This way regressions surface as
14+
// crashes in our release/obfuscated builds that run on real devices in CI. Only meant for our
15+
// own sample/UI-test apps, customers should never set this.
16+
System.setProperty("io.sentry.replay.compose.fail-fast", "true");
1217
Sentry.startProfiler();
1318
strictMode();
1419
super.onCreate();

0 commit comments

Comments
 (0)