fix(android): apply screenshot masking in captureScreenshot() - #6565
fix(android): apply screenshot masking in captureScreenshot()#6565OmerToledo wants to merge 4 commits into
Conversation
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 <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
| final Runnable runTakeScreenshot = | ||
| () -> { | ||
| bytesWrapper[0] = takeScreenshot(activity, logger, buildInfo); | ||
| bytesWrapper[0] = takeMaskedScreenshot(activity); |
There was a problem hiding this comment.
Mask rendering blocks Android UI thread
Medium Severity
takeMaskedScreenshot() performs hierarchy traversal and full-bitmap mask rendering inside the UI-thread runnable. Complex screens can freeze rendering or trigger an ANR, while background callers may time out and receive null even though masking continues.
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 1f29c11. Configure here.
…sking 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 <cursoragent@cursor.com>
|
Pushed a follow-up commit ( |
| if (maskingOptions == null) { | ||
| return ScreenshotUtils.compressBitmapToPng(screenshot, logger); | ||
| } |
There was a problem hiding this comment.
Bug: The takeMaskedScreenshot method leaks a Bitmap object on every call because it fails to call recycle() after the bitmap is compressed, in both masked and unmasked code paths.
Severity: HIGH
Suggested Fix
Wrap the Bitmap usage in a try-finally block within the takeMaskedScreenshot method. In the finally block, ensure that the Bitmap object (screenshot or masked) is recycled by calling recycle() on it, but only after it has been successfully compressed to a byte array.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location:
packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java#L583-L585
Potential issue: The `takeMaskedScreenshot` method leaks a `Bitmap` object's native
memory on every invocation. When screenshot masking is not configured, the original
`screenshot` `Bitmap` is compressed but never recycled. When masking is successful, the
`masked` `Bitmap` is compressed but also not recycled. Since this can be called
frequently, this memory leak can accumulate quickly, potentially leading to
`OutOfMemoryError` crashes in the application.
Also affects:
packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java:591~593
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bfb341e. Configure here.
| return null; | ||
| } | ||
|
|
||
| return ((SentryAndroidOptions) options).getScreenshot(); |
There was a problem hiding this comment.
Disabled masking now requires Replay
Medium Severity
getScreenshot() is non-null even when masking is disabled. Returning it unconditionally routes every capture through Replay masking, so apps excluding sentry-android-replay now receive null from captureScreenshot() and ordinary captures can fail due to unrelated masking errors.
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit bfb341e. Configure here.


📜 Description
On Android,
RNSentryModuleImpl.captureScreenshot()(hybrid SDK /NATIVE.captureScreenshot()) captured a raw window bitmap viaScreenshotUtils.takeScreenshot()with no masking, while iOS routes the same API throughPrivateSentrySDKOnly.captureScreenshots()→SentryViewPhotographer, which redacts text and images by default.Screenshot masking options bridged in #6007 (
screenshot.maskAllText,maskAllImages, view classes) are applied today only on theattachScreenshoterror path viaScreenshotEventProcessor. They are not used by the hybrid capture path.That creates a platform asymmetry for any feature built on
NATIVE.captureScreenshot():Sentry.captureScreenshot()usageiOS: personal text redacted in the capture primitive
Android: cleartext text in the PNG until this fix
This PR mirrors the masking pipeline already used in
ScreenshotEventProcessor:ScreenshotUtils.captureScreenshot)MaskRenderer.renderMaskswhenoptions.getScreenshot().getMaskViewClasses()is non-emptyWhen masking is configured but fails, the method returns
null(fail closed) rather than returning an unmasked capture.Requires
sentry-android-replayat runtime — already pulled in bysentry-android, same as error-screenshot masking.💡 Motivation and Context
We hit this in production while building a custom in-app feedback modal that attaches a screenshot. iOS behaved as documented (“data will be blurred”); Android did not, despite setting
screenshot: { maskAllText: true, maskAllImages: true }inSentry.init().Related:
ScreenshotEventProcessorThis closes the gap for the hybrid capture API, not just init options.
💚 How did you test it?
main(same imports/pattern asScreenshotEventProcessor)No new automated test in this PR yet — happy to add an Android instrumentation/unit test if maintainers point to the preferred harness.
📝 Checklist
sendDefaultPiichangesMade with Cursor