Skip to content

fix(android): apply screenshot masking in captureScreenshot() - #6565

Open
OmerToledo wants to merge 4 commits into
getsentry:mainfrom
OmerToledo:fix/android-capture-screenshot-masking
Open

fix(android): apply screenshot masking in captureScreenshot()#6565
OmerToledo wants to merge 4 commits into
getsentry:mainfrom
OmerToledo:fix/android-capture-screenshot-masking

Conversation

@OmerToledo

Copy link
Copy Markdown

📜 Description

On Android, RNSentryModuleImpl.captureScreenshot() (hybrid SDK / NATIVE.captureScreenshot()) captured a raw window bitmap via ScreenshotUtils.takeScreenshot() with no masking, while iOS routes the same API through PrivateSentrySDKOnly.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 the attachScreenshot error path via ScreenshotEventProcessor. They are not used by the hybrid capture path.

That creates a platform asymmetry for any feature built on NATIVE.captureScreenshot():

  • Feedback Widget “Take screenshot”
  • Custom feedback / bug-report flows (our production case in Bob Mobile)
  • Any direct Sentry.captureScreenshot() usage

iOS: 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:

  1. Capture bitmap (ScreenshotUtils.captureScreenshot)
  2. Build view hierarchy + MaskRenderer.renderMasks when options.getScreenshot().getMaskViewClasses() is non-empty
  3. Compress to PNG

When masking is configured but fails, the method returns null (fail closed) rather than returning an unmasked capture.

Requires sentry-android-replay at runtime — already pulled in by sentry-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 } in Sentry.init().

Related:

This closes the gap for the hybrid capture API, not just init options.

💚 How did you test it?

  • Verified the change compiles against current main (same imports/pattern as ScreenshotEventProcessor)
  • Exercised manually in Bob Mobile (custom feedback modal): Android screenshot attachments now redact on-screen text when masking options are enabled

No new automated test in this PR yet — happy to add an Android instrumentation/unit test if maintainers point to the preferred harness.

📝 Checklist

  • I added a changelog entry
  • I added tests to verify the changes (seeking guidance on preferred Android test location)
  • No new PII added / no sendDefaultPii changes

Made with Cursor

OmerToledo and others added 2 commits August 5, 2026 19:25
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 1f29c11. Configure here.

Comment thread packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java Outdated
…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>
@OmerToledo

Copy link
Copy Markdown
Author

Pushed a follow-up commit (bfb341e4) addressing review feedback: screenshotMaskingOptions() now always returns configured SentryScreenshotOptions instead of gating on getMaskViewClasses().isEmpty(), so maskAllText / maskAllImages from Sentry.init() reliably enable the masking pipeline.

Comment on lines +583 to +585
if (maskingOptions == null) {
return ScreenshotUtils.compressBitmapToPng(screenshot, logger);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit bfb341e. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant