From f7202d8fe48a50ee1726f99af7a2b85f07f41d54 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Nov 2026 12:53:54 +0100 Subject: [PATCH 1/3] fix(android): Mask prepared text layout views in replay and screenshots React Native 0.89 renders with PreparedLayoutTextView by default. It extends ViewGroup, so the android.widget.TextView match behind maskAllText does not cover it. Fixes #7010 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 +++ .../react/RNSentryScreenshotOptionsTest.kt | 27 ++++++++++++++ .../java/io/sentry/react/RNSentryStart.java | 19 ++++++++-- .../react/RNSentryReplayOptionsTest.java | 36 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e08831bc6..d21053409d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Unreleased +### Fixes + +- Mask React Native's prepared text layout views in Android Session Replay and error screenshots ([#7010](https://github.com/getsentry/sentry-react-native/issues/7010)) + ### Dependencies - Bump Android SDK from v8.62.0 to v8.63.0 ([#6990](https://github.com/getsentry/sentry-react-native/pull/6990)) diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryScreenshotOptionsTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryScreenshotOptionsTest.kt index 9cb675237f..63bfa9d1c5 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryScreenshotOptionsTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryScreenshotOptionsTest.kt @@ -38,6 +38,28 @@ class RNSentryScreenshotOptionsTest { assertTrue(screenshot.maskViewClasses.contains("com.horcrux.svg.SvgView")) } + @Test + fun `masks prepared text layout views by default`() { + val screenshot = SentryScreenshotOptions() + + RNSentryStart.configureScreenshotMasking(screenshot, null, true) + + assertTrue(screenshot.maskViewClasses.contains(PREPARED_LAYOUT_TEXT_VIEW)) + } + + @Test + fun `does not mask prepared text layout views when text masking is disabled`() { + val screenshot = SentryScreenshotOptions() + + RNSentryStart.configureScreenshotMasking( + screenshot, + JavaOnlyMap.of("maskAllText", false), + true, + ) + + assertFalse(screenshot.maskViewClasses.contains(PREPARED_LAYOUT_TEXT_VIEW)) + } + @Test fun `masks only Sentry Mask content when masking is disabled`() { val screenshot = SentryScreenshotOptions() @@ -109,4 +131,9 @@ class RNSentryScreenshotOptionsTest { assertEquals(RNSentryReplayMask::class.java.name, screenshot.maskViewContainerClass) assertEquals(RNSentryReplayUnmask::class.java.name, screenshot.unmaskViewContainerClass) } + + private companion object { + // Pinned as a literal on purpose: it has to match React Native's class name. + const val PREPARED_LAYOUT_TEXT_VIEW = "com.facebook.react.views.text.PreparedLayoutTextView" + } } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java index 2025268110..a2db2a7992 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -35,6 +35,11 @@ final class RNSentryStart { + // Renders by default since React Native 0.89. It is a ViewGroup that draws the text + // itself, so the android.widget.TextView match behind maskAllText does not cover it. + private static final String PREPARED_LAYOUT_TEXT_VIEW_CLASS = + "com.facebook.react.views.text.PreparedLayoutTextView"; + private RNSentryStart() { throw new AssertionError("Utility class should not be instantiated"); } @@ -407,6 +412,9 @@ static void configureScreenshotMasking( || rnScreenshotOptions.getBoolean("maskAllImages"); screenshot.setMaskAllText(maskAllText); screenshot.setMaskAllImages(maskAllImages); + if (maskAllText) { + screenshot.addMaskViewClass(PREPARED_LAYOUT_TEXT_VIEW_CLASS); + } if (maskAllImages) { screenshot.addMaskViewClass("com.horcrux.svg.SvgView"); // react-native-svg } @@ -464,6 +472,9 @@ static SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { ? rnOptions.getDouble("replaysOnErrorSampleRate") : null); + // Added before the early returns below, where text stays masked by the native default. + androidReplayOptions.addMaskViewClass(PREPARED_LAYOUT_TEXT_VIEW_CLASS); + if (!rnOptions.hasKey("mobileReplayOptions")) { return androidReplayOptions; } @@ -472,9 +483,13 @@ static SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { return androidReplayOptions; } - androidReplayOptions.setMaskAllText( + final boolean maskAllText = !rnMobileReplayOptions.hasKey("maskAllText") - || rnMobileReplayOptions.getBoolean("maskAllText")); + || rnMobileReplayOptions.getBoolean("maskAllText"); + androidReplayOptions.setMaskAllText(maskAllText); + if (!maskAllText) { + androidReplayOptions.addUnmaskViewClass(PREPARED_LAYOUT_TEXT_VIEW_CLASS); + } androidReplayOptions.setMaskAllImages( !rnMobileReplayOptions.hasKey("maskAllImages") || rnMobileReplayOptions.getBoolean("maskAllImages")); diff --git a/packages/core/android/src/test/java/io/sentry/react/RNSentryReplayOptionsTest.java b/packages/core/android/src/test/java/io/sentry/react/RNSentryReplayOptionsTest.java index 7e904d8eec..46a3e816b3 100644 --- a/packages/core/android/src/test/java/io/sentry/react/RNSentryReplayOptionsTest.java +++ b/packages/core/android/src/test/java/io/sentry/react/RNSentryReplayOptionsTest.java @@ -20,6 +20,9 @@ public class RNSentryReplayOptionsTest { private static final String MASKED_CLASS = "com.example.MaskedView"; private static final String UNMASKED_CLASS = "com.example.UnmaskedView"; + // Pinned as a literal on purpose: it has to match React Native's class name. + private static final String PREPARED_LAYOUT_TEXT_VIEW = + "com.facebook.react.views.text.PreparedLayoutTextView"; private static ReadableArray singletonArray(String value) { final ReadableArray array = mock(ReadableArray.class); @@ -63,6 +66,39 @@ public void addsUserUnmaskedViewClasses() { assertTrue(replayOptions.getUnmaskViewClasses().contains(UNMASKED_CLASS)); } + @Test + public void masksPreparedTextLayoutViewsByDefault() { + final ReadableMap mobileReplayOptions = mock(ReadableMap.class); + + final SentryReplayOptions replayOptions = + RNSentryStart.getReplayOptions(rnOptionsWith(mobileReplayOptions)); + + assertTrue(replayOptions.getMaskViewClasses().contains(PREPARED_LAYOUT_TEXT_VIEW)); + } + + @Test + public void masksPreparedTextLayoutViewsWhenMobileReplayOptionsAreMissing() { + final ReadableMap rnOptions = mock(ReadableMap.class); + when(rnOptions.hasKey("replaysOnErrorSampleRate")).thenReturn(true); + when(rnOptions.getDouble("replaysOnErrorSampleRate")).thenReturn(0.75); + + final SentryReplayOptions replayOptions = RNSentryStart.getReplayOptions(rnOptions); + + assertTrue(replayOptions.getMaskViewClasses().contains(PREPARED_LAYOUT_TEXT_VIEW)); + } + + @Test + public void doesNotMaskPreparedTextLayoutViewsWhenTextMaskingIsDisabled() { + final ReadableMap mobileReplayOptions = mock(ReadableMap.class); + when(mobileReplayOptions.hasKey("maskAllText")).thenReturn(true); + when(mobileReplayOptions.getBoolean("maskAllText")).thenReturn(false); + + final SentryReplayOptions replayOptions = + RNSentryStart.getReplayOptions(rnOptionsWith(mobileReplayOptions)); + + assertFalse(replayOptions.getMaskViewClasses().contains(PREPARED_LAYOUT_TEXT_VIEW)); + } + @Test public void doesNotAddUserClassesWhenNotProvided() { final ReadableMap mobileReplayOptions = mock(ReadableMap.class); From 2bd4350f65f4e1ba4d4910336e9e8138be4b7ccc Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Nov 2026 12:54:00 +0100 Subject: [PATCH 2/3] docs(android): Link the PR in the changelog entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d21053409d..b40c314968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixes -- Mask React Native's prepared text layout views in Android Session Replay and error screenshots ([#7010](https://github.com/getsentry/sentry-react-native/issues/7010)) +- Mask React Native's prepared text layout views in Android Session Replay and error screenshots ([#7011](https://github.com/getsentry/sentry-react-native/pull/7011)) ### Dependencies From a8d5cf8ef8e0b401d352291eafa729cbc8b5c84c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Nov 2026 08:47:58 +0100 Subject: [PATCH 3/3] Update CHANGELOG to fix formatting issues --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb4906ebb..735d841cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,12 +21,10 @@ }, }); ``` -### Fixes - -- Mask React Native's prepared text layout views in Android Session Replay and error screenshots ([#7011](https://github.com/getsentry/sentry-react-native/pull/7011)) ### Fixes +- Mask React Native's prepared text layout views in Android Session Replay and error screenshots ([#7011](https://github.com/getsentry/sentry-react-native/pull/7011)) - Apply `Sentry.Mask` and `Sentry.Unmask` on iOS when you start a replay manually with both replay sample rates at `0` ([#7008](https://github.com/getsentry/sentry-react-native/pull/7008)) ### Dependencies