From 4269fb0aaf75397440334ac944cddb7c7a1b751f Mon Sep 17 00:00:00 2001 From: Omer Toledo Date: Wed, 5 Aug 2026 19:25:03 +0300 Subject: [PATCH 1/4] fix(android): apply screenshot masking in captureScreenshot() Hybrid SDK screenshot capture returned unmasked window captures on Android while iOS redacts via SentryViewPhotographer. Mirror the ScreenshotEventProcessor masking pipeline when screenshot options are configured. Co-authored-by: Cursor --- .../io/sentry/react/RNSentryModuleImpl.java | 90 ++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index f2e2e09a14..b15205689a 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -1,6 +1,5 @@ package io.sentry.react; -import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot; import static io.sentry.vendor.Base64.NO_PADDING; import static io.sentry.vendor.Base64.NO_WRAP; import static java.util.concurrent.TimeUnit.SECONDS; @@ -10,8 +9,10 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.graphics.Bitmap; import android.net.Uri; import android.util.SparseIntArray; +import android.view.View; import androidx.annotation.VisibleForTesting; import androidx.core.app.FrameMetricsAggregator; import androidx.fragment.app.FragmentActivity; @@ -47,10 +48,15 @@ import io.sentry.android.core.SentryAndroidDateProvider; import io.sentry.android.core.SentryAndroidOptions; import io.sentry.android.core.SentryFramesDelayResult; +import io.sentry.android.core.SentryScreenshotOptions; import io.sentry.android.core.SentryShakeDetector; import io.sentry.android.core.ViewHierarchyEventProcessor; import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; +import io.sentry.android.core.internal.util.ScreenshotUtils; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; +import io.sentry.android.replay.util.MaskRenderer; +import io.sentry.android.replay.util.ViewsKt; +import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.profilemeasurements.ProfileMeasurement; import io.sentry.profilemeasurements.ProfileMeasurementValue; @@ -546,7 +552,7 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) { final byte[][] bytesWrapper = {{}}; // wrapper to be able to set the value in the runnable final Runnable runTakeScreenshot = () -> { - bytesWrapper[0] = takeScreenshot(activity, logger, buildInfo); + bytesWrapper[0] = takeMaskedScreenshot(activity); doneSignal.countDown(); }; @@ -566,6 +572,86 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) { return bytesWrapper[0]; } + private static @Nullable byte[] takeMaskedScreenshot(final @NotNull Activity activity) { + final @Nullable Bitmap screenshot = + ScreenshotUtils.captureScreenshot(activity, logger, buildInfo); + if (screenshot == null) { + return null; + } + + final @Nullable SentryScreenshotOptions maskingOptions = screenshotMaskingOptions(); + if (maskingOptions == null) { + return ScreenshotUtils.compressBitmapToPng(screenshot, logger); + } + + final @Nullable Bitmap masked = maskScreenshot(activity, screenshot, maskingOptions); + if (masked == null) { + return null; + } + + return ScreenshotUtils.compressBitmapToPng(masked, logger); + } + + private static @Nullable SentryScreenshotOptions screenshotMaskingOptions() { + final @NotNull SentryOptions options = ScopesAdapter.getInstance().getOptions(); + if (!(options instanceof SentryAndroidOptions)) { + return null; + } + + final @NotNull SentryScreenshotOptions screenshotOptions = + ((SentryAndroidOptions) options).getScreenshot(); + return screenshotOptions.getMaskViewClasses().isEmpty() ? null : screenshotOptions; + } + + private static @Nullable Bitmap maskScreenshot( + final @NotNull Activity activity, + final @NotNull Bitmap screenshot, + final @NotNull SentryScreenshotOptions maskingOptions) { + Bitmap mutableScreenshot = screenshot; + boolean createdCopy = false; + try { + final @Nullable View rootView = + activity.getWindow() != null && activity.getWindow().peekDecorView() != null + ? activity.getWindow().peekDecorView().getRootView() + : null; + if (rootView == null) { + screenshot.recycle(); + return null; + } + + final @NotNull ViewHierarchyNode rootNode = + ViewHierarchyNode.Companion.fromView(rootView, null, 0, maskingOptions); + ViewsKt.traverse(rootView, rootNode, maskingOptions, logger, null); + + if (!screenshot.isMutable()) { + mutableScreenshot = screenshot.copy(Bitmap.Config.ARGB_8888, true); + if (mutableScreenshot == null) { + screenshot.recycle(); + return null; + } + createdCopy = true; + } + + try (final MaskRenderer maskRenderer = new MaskRenderer()) { + maskRenderer.renderMasks(mutableScreenshot, rootNode, null); + } + + if (createdCopy && !screenshot.isRecycled()) { + screenshot.recycle(); + } + return mutableScreenshot; + } catch (Throwable e) { // NOPMD - masking must never crash the screenshot flow + logger.log(SentryLevel.ERROR, "Failed to mask screenshot.", e); + if (createdCopy && !mutableScreenshot.isRecycled()) { + mutableScreenshot.recycle(); + } + if (!screenshot.isRecycled()) { + screenshot.recycle(); + } + return null; + } + } + public void fetchViewHierarchy(Promise promise) { final @Nullable Activity activity = getCurrentActivity(); final @Nullable ViewHierarchy viewHierarchy = From 6d0778a8eaa7376ba9ab4793e677fd210bd44745 Mon Sep 17 00:00:00 2001 From: Omer Toledo Date: Wed, 5 Aug 2026 19:26:22 +0300 Subject: [PATCH 2/4] docs: add changelog entry for android captureScreenshot masking Co-authored-by: Cursor --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30cfcb16d2..be8f019a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ ### Fixes +- Apply screenshot masking in Android `captureScreenshot()` when `screenshot` options are configured ([#TBD](https://github.com/getsentry/sentry-react-native/pull/TBD)) + + Hybrid SDK screenshot capture (`NATIVE.captureScreenshot()`, used by the Feedback Widget and custom integrations) returned unmasked window captures on Android while iOS redacts text and images via `SentryViewPhotographer`. Error-screenshot masking (`attachScreenshot` + `ScreenshotEventProcessor`) was unaffected. Android `captureScreenshot()` now reuses the same view-hierarchy masking pipeline when `options.getScreenshot()` has mask classes configured. + - Attach `debug_meta` to JS error events on Hermes when the Debug ID stack match fails ([#6545](https://github.com/getsentry/sentry-react-native/pull/6545)) - `sentry-expo-upload-sourcemaps` now reads plugin config when the plugin is registered as `@sentry/react-native` ([#6543](https://github.com/getsentry/sentry-react-native/pull/6543)) - Make the `RNSentry` SPEC CHECKSUM in `Podfile.lock` machine-independent ([#6534](https://github.com/getsentry/sentry-react-native/pull/6534)) From 1f29c111e5814fa50934b3dacb5445a6b5f82ef1 Mon Sep 17 00:00:00 2001 From: Omer Toledo Date: Wed, 5 Aug 2026 19:27:40 +0300 Subject: [PATCH 3/4] docs: link changelog entry to PR 6565 Co-authored-by: Cursor --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be8f019a67..f26ee28adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ ### Fixes -- Apply screenshot masking in Android `captureScreenshot()` when `screenshot` options are configured ([#TBD](https://github.com/getsentry/sentry-react-native/pull/TBD)) +- Apply screenshot masking in Android `captureScreenshot()` when `screenshot` options are configured Hybrid SDK screenshot capture (`NATIVE.captureScreenshot()`, used by the Feedback Widget and custom integrations) returned unmasked window captures on Android while iOS redacts text and images via `SentryViewPhotographer`. Error-screenshot masking (`attachScreenshot` + `ScreenshotEventProcessor`) was unaffected. Android `captureScreenshot()` now reuses the same view-hierarchy masking pipeline when `options.getScreenshot()` has mask classes configured. From bfb341e41b7ceda43d02836ce34d52424b7ab3e4 Mon Sep 17 00:00:00 2001 From: Omer Toledo Date: Wed, 5 Aug 2026 19:50:06 +0300 Subject: [PATCH 4/4] fix(android): honor maskAllText/maskAllImages in captureScreenshot masking Return configured screenshot options instead of gating masking on maskViewClasses being non-empty, so maskAllText and maskAllImages from Sentry.init always enable the masking pipeline. Co-authored-by: Cursor --- CHANGELOG.md | 4 ++-- .../src/main/java/io/sentry/react/RNSentryModuleImpl.java | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f26ee28adc..8536a090d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,9 +27,9 @@ ### Fixes -- Apply screenshot masking in Android `captureScreenshot()` when `screenshot` options are configured +- Apply screenshot masking in Android `captureScreenshot()` when `screenshot` options are configured ([#6565](https://github.com/getsentry/sentry-react-native/pull/6565)) - Hybrid SDK screenshot capture (`NATIVE.captureScreenshot()`, used by the Feedback Widget and custom integrations) returned unmasked window captures on Android while iOS redacts text and images via `SentryViewPhotographer`. Error-screenshot masking (`attachScreenshot` + `ScreenshotEventProcessor`) was unaffected. Android `captureScreenshot()` now reuses the same view-hierarchy masking pipeline when `options.getScreenshot()` has mask classes configured. + Hybrid SDK screenshot capture (`NATIVE.captureScreenshot()`, used by the Feedback Widget and custom integrations) returned unmasked window captures on Android while iOS redacts text and images via `SentryViewPhotographer`. Error-screenshot masking (`attachScreenshot` + `ScreenshotEventProcessor`) was unaffected. Android `captureScreenshot()` now reuses the same view-hierarchy masking pipeline for configured `screenshot` options such as `maskAllText` and `maskAllImages`. - Attach `debug_meta` to JS error events on Hermes when the Debug ID stack match fails ([#6545](https://github.com/getsentry/sentry-react-native/pull/6545)) - `sentry-expo-upload-sourcemaps` now reads plugin config when the plugin is registered as `@sentry/react-native` ([#6543](https://github.com/getsentry/sentry-react-native/pull/6543)) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index b15205689a..a7e6d9c6c7 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -598,9 +598,7 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) { return null; } - final @NotNull SentryScreenshotOptions screenshotOptions = - ((SentryAndroidOptions) options).getScreenshot(); - return screenshotOptions.getMaskViewClasses().isEmpty() ? null : screenshotOptions; + return ((SentryAndroidOptions) options).getScreenshot(); } private static @Nullable Bitmap maskScreenshot(