Skip to content

feat(screenshot): Add screenshot masking using view hierarchy#5077

Open
romtsn wants to merge 20 commits intomainfrom
rz/feat/screenshot-masking
Open

feat(screenshot): Add screenshot masking using view hierarchy#5077
romtsn wants to merge 20 commits intomainfrom
rz/feat/screenshot-masking

Conversation

@romtsn
Copy link
Member

@romtsn romtsn commented Feb 5, 2026

📜 Description

Adds masking support to error screenshots by reusing the Session Replay masking logic. This allows sensitive content (text, images) to be masked before attaching screenshots to error events.

  • Add SentryMaskingOptions base class for shared masking configuration
  • Add SentryScreenshotOptions for screenshot-specific masking settings
  • Create MaskRenderer utility for shared mask rendering (used by both replay and screenshots)
  • Add manifest metadata support for screenshot masking options
  • Add snapshot tests with Dropbox Differ library for visual regression
  • Update CLAUDE.md with dependency management guidelines

Masking requires the sentry-android-replay module to be present at runtime. Without it, screenshots are captured without masking.

some example events:

  1. https://sentry-sdks.sentry.io/issues/7241812873/events/5652b2955b8d4e638be96a8bed0ddd2c/
  2. https://sentry-sdks.sentry.io/issues/7235688870/events/9412e7165a4e4be98d3b9d9ceaea70b6/

Also verified that pixelCopy strategy still works fine after the change, replay here:
https://sentry-sdks.sentry.io/explore/replays/cb4a531e5851484cb547c93e31a9c9f3/

💡 Motivation and Context

Closes #3286

💚 How did you test it?

Manually + automated

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • [] I updated the docs if needed.
  • [] I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

Docs and maybe wizard

@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (distribution) Add installGroupsOverride parameter by runningcode in #5066
  • (replay) Track custom masking usage via integration by markushi in #5070
  • (screenshot) Add screenshot masking using view hierarchy by romtsn in #5077

Bug Fixes 🐛

  • (android) Merge tombstone and Native SDK event message. by supervacuus in #5095
  • (logs,metrics) Attach user attributes to logs and metrics regardless of sendDefaultPii by romtsn in #5099

Internal Changes 🔧

Deps

  • Update Native SDK to v0.12.7 by github-actions in #5098
  • Bump getsentry/craft from 2.20.1 to 2.21.2 by dependabot in #5083
  • Bump github/codeql-action from 4.32.0 to 4.32.2 by dependabot in #5082
  • Update Native SDK to v0.12.6 by github-actions in #5071

Other

  • Session Replay: Add PreviewView to default masked view classes by szijpeter in #5097
  • No longer log a warning if a logging integration cannot initialize Sentry due to missing DSN. by adinauer in #5075
  • Fix thread leak caused by eager creation of SentryExecutorService in SentryOptions by adinauer in #5093

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against b85f24e

Adds masking support to error screenshots by reusing the Session Replay
masking logic. This allows sensitive content (text, images) to be masked
before attaching screenshots to error events.

- Add SentryMaskingOptions base class for shared masking configuration
- Add SentryScreenshotOptions for screenshot-specific masking settings
- Create MaskRenderer utility for shared mask rendering (used by both
  replay and screenshots)
- Add manifest metadata support for screenshot masking options
- Add snapshot tests with Dropbox Differ library for visual regression
- Update CLAUDE.md with dependency management guidelines

Masking requires the sentry-android-replay module to be present at runtime.
Without it, screenshots are captured without masking.

Refs: #3286

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@romtsn romtsn force-pushed the rz/feat/screenshot-masking branch from 8afa77c to 17beece Compare February 5, 2026 13:33
@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 316.29 ms 364.46 ms 48.17 ms
Size 1.58 MiB 2.29 MiB 720.91 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
3998a95 415.94 ms 478.54 ms 62.60 ms
d15471f 369.38 ms 459.08 ms 89.70 ms
6405ec5 310.88 ms 354.56 ms 43.69 ms
a416a65 295.53 ms 373.74 ms 78.21 ms
6edfca2 305.52 ms 432.78 ms 127.26 ms
d15471f 303.49 ms 439.08 ms 135.59 ms
319f256 315.96 ms 372.96 ms 57.00 ms
694d587 379.62 ms 400.80 ms 21.18 ms
9fbb112 404.51 ms 475.65 ms 71.14 ms
b03edbb 352.20 ms 423.69 ms 71.49 ms

App size

Revision Plain With Sentry Diff
3998a95 1.58 MiB 2.10 MiB 532.96 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
6405ec5 1.58 MiB 2.12 MiB 552.23 KiB
a416a65 1.58 MiB 2.12 MiB 555.26 KiB
6edfca2 1.58 MiB 2.13 MiB 559.07 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
319f256 1.58 MiB 2.19 MiB 619.79 KiB
694d587 1.58 MiB 2.19 MiB 620.06 KiB
9fbb112 1.58 MiB 2.11 MiB 539.18 KiB
b03edbb 1.58 MiB 2.13 MiB 557.32 KiB

Previous results on branch: rz/feat/screenshot-masking

Startup times

Revision Plain With Sentry Diff
4b6bd27 331.80 ms 389.30 ms 57.50 ms
f839405 337.96 ms 405.19 ms 67.23 ms
5a5e340 396.65 ms 506.92 ms 110.27 ms
30ecad1 377.35 ms 410.86 ms 33.50 ms
dd8c794 343.71 ms 447.48 ms 103.77 ms
c8b63af 324.09 ms 399.74 ms 75.65 ms
26069fa 341.00 ms 421.66 ms 80.66 ms
a36ad8f 306.38 ms 370.08 ms 63.70 ms
91309a4 309.08 ms 359.69 ms 50.61 ms

App size

Revision Plain With Sentry Diff
4b6bd27 1.58 MiB 2.29 MiB 720.62 KiB
f839405 1.58 MiB 2.29 MiB 720.70 KiB
5a5e340 1.58 MiB 2.29 MiB 720.61 KiB
30ecad1 1.58 MiB 2.29 MiB 720.80 KiB
dd8c794 1.58 MiB 2.19 MiB 620.03 KiB
c8b63af 1.58 MiB 2.29 MiB 720.85 KiB
26069fa 1.58 MiB 2.29 MiB 720.85 KiB
a36ad8f 1.58 MiB 2.29 MiB 720.81 KiB
91309a4 1.58 MiB 2.29 MiB 720.85 KiB

romtsn and others added 3 commits February 5, 2026 16:11
…s configured

The isMaskingEnabled() method was logging a warning before checking if
masking was actually configured. This caused users who never set up
screenshot masking to see spurious warnings on every event.

Co-Authored-By: Claude <noreply@anthropic.com>
…false) is called

setMaskAllImages(true) was adding WebView, VideoView, and ExoPlayer
classes to maskViewClasses, but setMaskAllImages(false) only removed
ImageView. This caused asymmetric toggle behavior where disabling
image masking didn't restore the original state.

Co-Authored-By: Claude <noreply@anthropic.com>
…mory leak

When an exception occurred in applyMasking after creating a mutable
copy of the bitmap, the catch block returned the original screenshot
without recycling the copy. This caused bitmap memory to accumulate
until GC runs, potentially causing OOM issues on frequent errors.

Co-Authored-By: Claude <noreply@anthropic.com>
public void setMaskAllImages(final boolean maskAllImages) {
super.setMaskAllImages(maskAllImages);
if (maskAllImages) {
addSensitiveViewClasses();
Copy link
Member Author

Choose a reason for hiding this comment

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

I guess this is questionable if you enable masking images, we will also mask other media. But I didn't want to introduce new flag, so made it dependant on this one. We can document this, or introduce a new flag. Thoughts @markushi ?

I think adding those classes by default is probably a no-go for screenshots, since it's a single frame screenshot, not a sequence like in replay, where potentially a lot more could be leaked

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I would just document it.

Copy link
Member

@markushi markushi left a comment

Choose a reason for hiding this comment

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

Looking good! Just one concern around threading I'd like to discuss before approving.

public void setMaskAllImages(final boolean maskAllImages) {
super.setMaskAllImages(maskAllImages);
if (maskAllImages) {
addSensitiveViewClasses();
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I would just document it.

Copy link
Member

Choose a reason for hiding this comment

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

do we need any license attribution for this file?

Copy link
Member Author

@romtsn romtsn Feb 16, 2026

Choose a reason for hiding this comment

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

good question, but we already have it in sentry-android-replay/src/test/resources/ and sentry/src/test/resources/ so I'd say no. Also, looks like it'd been taken from this website https://pixabay.com/images/search/tongariro/ which claims the license is free for use (we don't distribute it)

romtsn and others added 2 commits February 16, 2026 15:57
…sking

# Conflicts:
#	CHANGELOG.md
#	sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
#	sentry/api/sentry.api
#	sentry/src/main/java/io/sentry/SentryReplayOptions.java
Move trackCustomMasking() to SentryMaskingOptions as an abstract method
so it can be called polymorphically from replay view hierarchy code.
SentryReplayOptions provides the real implementation, while
SentryScreenshotOptions provides a no-op. Also adds
CAMERAX_PREVIEW_VIEW_CLASS_NAME to SentryMaskingOptions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
romtsn and others added 2 commits February 16, 2026 17:14
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
peekDecorView returns null if the decor view hasn't been created yet,
avoiding forced creation. This is consistent with the rest of the
codebase (ScreenshotUtils, ViewHierarchyEventProcessor).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…and don't leak unmasked screenshots

- Use per-call MaskRenderer via try-with-resources instead of shared instance
- Remove Closeable from ScreenshotEventProcessor (nothing to clean up)
- Capture view hierarchy on main thread via runOnUiThread + CountDownLatch
- Apply masking on the calling thread (only VH traversal needs main thread)
- Return null on masking failure to avoid sending unmasked screenshots
- Fix setMaskViewContainerClass to not trigger trackCustomMasking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
romtsn and others added 2 commits February 17, 2026 13:24
…lass

addMaskViewClass now removes from unmaskViewClasses and vice versa,
preventing stale entries from silently blocking masking when
setMaskAllText(false)/setMaskAllImages(false) is called with defaults.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… memory leaks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

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.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

…r instead of per event

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@romtsn romtsn requested a review from markushi February 17, 2026 17:19
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.

Redact Screenshots via View Hierarchy

3 participants

Comments