Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

### 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@

final class RNSentryStart {

// Renders <Text> 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");
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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));
}
Comment thread
antonis marked this conversation as resolved.
Resolved

@Test
public void doesNotAddUserClassesWhenNotProvided() {
final ReadableMap mobileReplayOptions = mock(ReadableMap.class);
Expand Down
Loading