From 85563274b476848875699235b9ce28d0a15408d9 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 30 Oct 2026 12:55:13 +0100 Subject: [PATCH 1/2] feat(android): Mask screenshots by default Error and user feedback screenshots now mask all text and images by default, matching Session Replay. The manifest reader defaulted the screenshot masking keys to false, which would have undone the new default, so it now defaults them to true like the replay keys. Masking needs sentry-android-replay at runtime. Apps that depend on sentry-android-core alone and attach screenshots now get no screenshot until they add the replay module or turn masking off. Refs JAVA-502 Co-Authored-By: Claude Opus 5.5 --- .../android/core/ManifestMetadataReader.java | 5 ++--- .../android/core/SentryScreenshotOptions.java | 8 ++++---- .../android/core/ManifestMetadataReaderTest.kt | 16 ++++++++-------- .../android/core/ScreenshotEventProcessorTest.kt | 8 +++++++- .../android/core/SentryScreenshotOptionsTest.kt | 14 ++++++++++---- .../util/ScreenshotUtilsMaskedScreenshotTest.kt | 3 +++ 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index b6e802bbd5f..003ae6cd1bc 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -806,13 +806,12 @@ static void applyMetadata( options.setSpotlightConnectionUrl(spotlightUrl); } - // Screenshot masking options (default to false for backwards compatibility) options .getScreenshot() - .setMaskAllText(readBool(metadata, logger, SCREENSHOT_MASK_ALL_TEXT, false)); + .setMaskAllText(readBool(metadata, logger, SCREENSHOT_MASK_ALL_TEXT, true)); options .getScreenshot() - .setMaskAllImages(readBool(metadata, logger, SCREENSHOT_MASK_ALL_IMAGES, false)); + .setMaskAllImages(readBool(metadata, logger, SCREENSHOT_MASK_ALL_IMAGES, true)); if (options.getAnrProfilingSampleRate() == null) { final double anrProfilingSampleRate = diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryScreenshotOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryScreenshotOptions.java index 6e1cceb760b..b028f7ffcc1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryScreenshotOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryScreenshotOptions.java @@ -6,8 +6,8 @@ * Screenshot masking options for error screenshots. Extends the base {@link SentryMaskingOptions} * with screenshot-specific defaults. * - *

By default, masking is disabled for screenshots. Enable masking by calling {@link - * #setMaskAllText(boolean)} and/or {@link #setMaskAllImages(boolean)}. + *

By default, all text and images are masked. Disable masking by calling {@link + * #setMaskAllText(boolean)} and/or {@link #setMaskAllImages(boolean)} with {@code false}. * *

Note: Screenshot masking requires the {@code sentry-android-replay} module to be present at * runtime. If masking is enabled but the replay module is not available, no screenshot is captured @@ -16,8 +16,8 @@ public final class SentryScreenshotOptions extends SentryMaskingOptions { public SentryScreenshotOptions() { - // Default to NO masking until next major version. - // maskViewClasses starts empty, so nothing is masked by default. + setMaskAllText(true); + setMaskAllImages(true); } @Override diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 6684f8c617d..8959708975a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -2869,31 +2869,31 @@ class ManifestMetadataReaderTest { @Test fun `applyMetadata reads screenshot mask-all-text to options`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.SCREENSHOT_MASK_ALL_TEXT to true) + val bundle = bundleOf(ManifestMetadataReader.SCREENSHOT_MASK_ALL_TEXT to false) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertTrue(fixture.options.screenshot.maskViewClasses.contains("android.widget.TextView")) + assertFalse(fixture.options.screenshot.maskViewClasses.contains("android.widget.TextView")) } @Test fun `applyMetadata reads screenshot mask-all-images to options`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.SCREENSHOT_MASK_ALL_IMAGES to true) + val bundle = bundleOf(ManifestMetadataReader.SCREENSHOT_MASK_ALL_IMAGES to false) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertTrue(fixture.options.screenshot.maskViewClasses.contains("android.widget.ImageView")) + assertFalse(fixture.options.screenshot.maskViewClasses.contains("android.widget.ImageView")) } @Test - fun `applyMetadata without specifying screenshot mask-all-text, stays false`() { + fun `applyMetadata without specifying screenshot mask-all-text, stays true`() { // Arrange val context = fixture.getContext() @@ -2901,11 +2901,11 @@ class ManifestMetadataReaderTest { ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertFalse(fixture.options.screenshot.maskViewClasses.contains("android.widget.TextView")) + assertTrue(fixture.options.screenshot.maskViewClasses.contains("android.widget.TextView")) } @Test - fun `applyMetadata without specifying screenshot mask-all-images, stays false`() { + fun `applyMetadata without specifying screenshot mask-all-images, stays true`() { // Arrange val context = fixture.getContext() @@ -2913,7 +2913,7 @@ class ManifestMetadataReaderTest { ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertFalse(fixture.options.screenshot.maskViewClasses.contains("android.widget.ImageView")) + assertTrue(fixture.options.screenshot.maskViewClasses.contains("android.widget.ImageView")) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt index 0abb232b807..1dfb277175d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt @@ -73,7 +73,13 @@ class ScreenshotEventProcessorTest { private class Fixture { lateinit var activity: Activity val threadChecker = mock() - val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" } + val options = + SentryAndroidOptions().apply { + dsn = "https://key@sentry.io/proj" + // Tests opt in to masking, so they also run without sentry-android-replay. + screenshot.setMaskAllText(false) + screenshot.setMaskAllImages(false) + } val mainProcessor = MainEventProcessor(options) init { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryScreenshotOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryScreenshotOptionsTest.kt index ad6d29b5614..ed7562d5323 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryScreenshotOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryScreenshotOptionsTest.kt @@ -9,9 +9,12 @@ import kotlin.test.assertTrue class SentryScreenshotOptionsTest { @Test - fun `maskViewClasses is empty by default`() { + fun `masks all text and images by default`() { val options = SentryScreenshotOptions() - assertTrue(options.maskViewClasses.isEmpty()) + assertTrue(options.maskViewClasses.contains(SentryMaskingOptions.TEXT_VIEW_CLASS_NAME)) + assertTrue(options.maskViewClasses.contains(SentryMaskingOptions.IMAGE_VIEW_CLASS_NAME)) + assertTrue(options.maskViewClasses.contains(SentryMaskingOptions.WEB_VIEW_CLASS_NAME)) + assertEquals(8, options.maskViewClasses.size) } @Test @@ -23,6 +26,8 @@ class SentryScreenshotOptionsTest { @Test fun `setMaskAllText true only adds TextView`() { val options = SentryScreenshotOptions() + options.setMaskAllText(false) + options.setMaskAllImages(false) options.setMaskAllText(true) assertTrue(options.maskViewClasses.contains(SentryMaskingOptions.TEXT_VIEW_CLASS_NAME)) @@ -35,6 +40,7 @@ class SentryScreenshotOptionsTest { @Test fun `setMaskAllImages true adds ImageView and sensitive view classes`() { val options = SentryScreenshotOptions() + options.setMaskAllImages(false) options.setMaskAllImages(true) assertTrue(options.maskViewClasses.contains(SentryMaskingOptions.IMAGE_VIEW_CLASS_NAME)) @@ -68,7 +74,7 @@ class SentryScreenshotOptionsTest { options.setMaskAllImages(true) // CopyOnWriteArraySet should prevent duplicates - assertEquals(7, options.maskViewClasses.size) + assertEquals(8, options.maskViewClasses.size) } @Test @@ -108,6 +114,6 @@ class SentryScreenshotOptionsTest { options.setMaskAllImages(false) - assertTrue(options.maskViewClasses.isEmpty()) + assertEquals(setOf(SentryMaskingOptions.TEXT_VIEW_CLASS_NAME), options.maskViewClasses) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilsMaskedScreenshotTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilsMaskedScreenshotTest.kt index 9bc6f9f85b0..9f6e220c2d2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilsMaskedScreenshotTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilsMaskedScreenshotTest.kt @@ -36,6 +36,9 @@ class ScreenshotUtilsMaskedScreenshotTest { SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" threadChecker = this@Fixture.threadChecker + // Tests opt in to masking, so they also run without sentry-android-replay. + screenshot.setMaskAllText(false) + screenshot.setMaskAllImages(false) } val buildInfoProvider = BuildInfoProvider(options.logger) From 0dadc3fd3a9b52a0a236f800678c21b3c27a4977 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 30 Oct 2026 13:04:57 +0100 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1185d47aaf9..09aecbbf9d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - Decide whether foregrounding the app starts a new session on a monotonic clock instead of the wall clock, so that a device time change no longer starts a session that should have been resumed, or resumes one that should have ended. This affects session counts ([#6096](https://github.com/getsentry/sentry-java/pull/6096)) - `SentryUserFeedbackForm` now extends `android.app.Dialog` instead of `android.app.AlertDialog` ([#6338](https://github.com/getsentry/sentry-java/pull/6338)) - The form never used `AlertDialog` features. Code that calls `AlertDialog`-only methods on it, such as `setButton` or `setMessage`, no longer compiles. +- Screenshots attached to errors and user feedback now mask all text and images by default. Turn it off with `options.screenshot.setMaskAllText(false)` and `setMaskAllImages(false)`, or the `io.sentry.screenshot.mask-all-text` and `io.sentry.screenshot.mask-all-images` manifest options. Masking needs `sentry-android-replay`, so apps that depend on `sentry-android-core` alone get no screenshot until they add it or turn masking off ([#6437](https://github.com/getsentry/sentry-java/pull/6437)) ## 8.62.0