From f9275fc046fd17bfcdd14291a981f35440ef6051 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 30 Oct 2026 12:55:13 +0100 Subject: [PATCH 1/4] 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 7695d86f0be..b50999ac1ef 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 @@ -801,13 +801,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 d7481c22b25..4a4dba19861 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 @@ -2843,31 +2843,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() @@ -2875,11 +2875,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() @@ -2887,7 +2887,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 5a1aee4145a1ab3380ca50a0a0a46d65ef8c9992 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 30 Oct 2026 13:04:57 +0100 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 686f2d778bd..68067625432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ - Remove the Android `enablePerformanceV2` option and its `io.sentry.performance-v2.enable` manifest metadata. Performance V2 is always on ([#6458](https://github.com/getsentry/sentry-java/pull/6458)) - App starts are measured from process start on Android 7.0+ (API 24), and slow and frozen frames are measured per span - `ActivityFramesTracker` is removed, and the `ActivityLifecycleIntegration` constructor no longer takes it +- 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)) ### Improvements From 8ba018ebd95c3514c5a0103e5efea0430ec9096e Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Fri, 6 Nov 2026 16:01:47 +0100 Subject: [PATCH 3/4] use getFatalLogger --- .../io/sentry/android/core/internal/util/ScreenshotUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java index 657ba54cbd2..00d9cc37e3c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java @@ -87,7 +87,7 @@ public class ScreenshotUtils { final boolean isMaskingConfigured = !options.getScreenshot().getMaskViewClasses().isEmpty(); if (isMaskingConfigured && !isReplayAvailable) { options - .getLogger() + .getFatalLogger() .log(SentryLevel.WARNING, "Screenshot masking requires sentry-android-replay module"); return null; } From 4becdf29c3b6978dca12f5b0d013137cd37d387a Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 9 Nov 2026 09:15:06 +0100 Subject: [PATCH 4/4] use getFatalLogger for native error screenshots too ScreenshotEventProcessor checks for the missing replay module itself and returns before captureMaskedScreenshot, so the warning was still gated on options.debug for native errors. Co-Authored-By: Claude Opus 5.5 --- .../java/io/sentry/android/core/ScreenshotEventProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java index 8b51c512c87..28deaf620c6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java @@ -83,7 +83,7 @@ public ScreenshotEventProcessor( if (!isReplayAvailable && !options.getScreenshot().getMaskViewClasses().isEmpty()) { if (!isReplayModuleAbsenceLogged.getAndSet(true)) { options - .getLogger() + .getFatalLogger() .log(SentryLevel.WARNING, "Screenshot masking requires sentry-android-replay module"); } return event;