feat(replay): Add per-class Session Replay masking - #6725
Merged
Merged
Conversation
Add optional `maskedViewClasses` / `unmaskedViewClasses` to `MobileReplayOptions`, forwarding native view class names into the native SDKs' existing per-class Session Replay APIs (iOS SentryReplayOptions maskedViewClasses/unmaskedViewClasses; Android addMaskViewClass/addUnmaskViewClass). Mirrors the existing per-class screenshot masking. No public barrel / API-report changes. Fixes #4628. Partially addresses #6601. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
Contributor
|
Added per-class Session Replay masking options for mobileReplayIntegration.
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit e7bf87d. Configure here.
Contributor
Android (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a2585ce+dirty | 426.36 ms | 483.26 ms | 56.90 ms |
| 882f8ae+dirty | 399.98 ms | 427.06 ms | 27.08 ms |
| af33f3b+dirty | 434.90 ms | 506.14 ms | 71.24 ms |
| ecf47a2+dirty | 420.40 ms | 458.02 ms | 37.62 ms |
| acd838e+dirty | 422.63 ms | 462.39 ms | 39.76 ms |
| 7fd0012+dirty | 444.73 ms | 470.08 ms | 25.35 ms |
| 580fb5c+dirty | 436.34 ms | 471.63 ms | 35.28 ms |
| 5ca03f9+dirty | 423.30 ms | 467.00 ms | 43.70 ms |
| 5c1e987+dirty | 423.52 ms | 471.64 ms | 48.12 ms |
| 822d35b+dirty | 429.31 ms | 498.04 ms | 68.73 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a2585ce+dirty | 49.74 MiB | 55.36 MiB | 5.61 MiB |
| 882f8ae+dirty | 48.30 MiB | 53.60 MiB | 5.29 MiB |
| af33f3b+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| ecf47a2+dirty | 49.74 MiB | 54.82 MiB | 5.07 MiB |
| acd838e+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| 7fd0012+dirty | 50.56 MiB | 56.46 MiB | 5.90 MiB |
| 580fb5c+dirty | 49.74 MiB | 54.79 MiB | 5.05 MiB |
| 5ca03f9+dirty | 49.74 MiB | 55.26 MiB | 5.52 MiB |
| 5c1e987+dirty | 43.75 MiB | 48.08 MiB | 4.33 MiB |
| 822d35b+dirty | 49.74 MiB | 54.84 MiB | 5.10 MiB |
antonis
marked this pull request as ready for review
September 15, 2026 10:51
📲 Install BuildsAndroid
|
Contributor
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 64630e5+dirty | 3842.70 ms | 1218.11 ms | -2624.60 ms |
| b0d3373+dirty | 3831.75 ms | 1227.29 ms | -2604.46 ms |
| b04af96+dirty | 3818.92 ms | 1219.76 ms | -2599.16 ms |
| 3d31fcf+dirty | 3838.09 ms | 1223.46 ms | -2614.63 ms |
| a0a3177+dirty | 3844.73 ms | 1225.23 ms | -2619.51 ms |
| af33f3b+dirty | 3849.98 ms | 1236.45 ms | -2613.53 ms |
| 09a902f+dirty | 3835.67 ms | 1217.11 ms | -2618.57 ms |
| 5a316ea+dirty | 3820.11 ms | 1211.28 ms | -2608.83 ms |
| acd838e+dirty | 3849.78 ms | 1230.00 ms | -2619.78 ms |
| c2e182c+dirty | 3848.40 ms | 1211.79 ms | -2636.61 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 64630e5+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| b0d3373+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| b04af96+dirty | 4.98 MiB | 6.54 MiB | 1.56 MiB |
| 3d31fcf+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| a0a3177+dirty | 4.98 MiB | 6.55 MiB | 1.58 MiB |
| af33f3b+dirty | 4.98 MiB | 6.51 MiB | 1.54 MiB |
| 09a902f+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 5a316ea+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| acd838e+dirty | 5.15 MiB | 6.70 MiB | 1.55 MiB |
| c2e182c+dirty | 4.98 MiB | 6.50 MiB | 1.52 MiB |
Contributor
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 9253345+dirty | 3862.73 ms | 1237.15 ms | -2625.59 ms |
| ef27341+dirty | 3835.20 ms | 1212.23 ms | -2622.97 ms |
| d387874+dirty | 3838.71 ms | 1211.11 ms | -2627.60 ms |
| 4acd4e9+dirty | 3806.73 ms | 1208.48 ms | -2598.25 ms |
| 0b1b5e3+dirty | 3820.72 ms | 1207.94 ms | -2612.78 ms |
| 267d3ed+dirty | 3860.14 ms | 1223.39 ms | -2636.76 ms |
| 853723c+dirty | 3849.33 ms | 1221.07 ms | -2628.26 ms |
| b726ced+dirty | 3840.50 ms | 1217.29 ms | -2623.21 ms |
| 9474ead+dirty | 3823.33 ms | 1208.31 ms | -2615.03 ms |
| 3d377b5+dirty | 1201.55 ms | 1201.80 ms | 0.25 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 9253345+dirty | 5.15 MiB | 6.89 MiB | 1.74 MiB |
| ef27341+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| d387874+dirty | 5.08 MiB | 6.68 MiB | 1.60 MiB |
| 4acd4e9+dirty | 4.98 MiB | 6.53 MiB | 1.55 MiB |
| 0b1b5e3+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
| 267d3ed+dirty | 5.15 MiB | 6.69 MiB | 1.54 MiB |
| 853723c+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| b726ced+dirty | 5.15 MiB | 6.89 MiB | 1.74 MiB |
| 9474ead+dirty | 5.15 MiB | 6.71 MiB | 1.55 MiB |
| 3d377b5+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
Contributor
Android (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3a829f0+dirty | 410.78 ms | 454.22 ms | 43.44 ms |
| a5d243c+dirty | 499.20 ms | 525.62 ms | 26.42 ms |
| a858ac3+dirty | 498.50 ms | 575.22 ms | 76.72 ms |
| f9c1ed4+dirty | 520.57 ms | 594.84 ms | 74.27 ms |
| b9bebee+dirty | 500.50 ms | 536.42 ms | 35.92 ms |
| a0d8cf8+dirty | 533.71 ms | 564.25 ms | 30.54 ms |
| 7d8c8bd+dirty | 406.06 ms | 460.88 ms | 54.81 ms |
| af33f3b+dirty | 417.68 ms | 448.04 ms | 30.37 ms |
| 1122a96+dirty | 510.16 ms | 542.00 ms | 31.84 ms |
| 8448c07+dirty | 433.51 ms | 489.13 ms | 55.61 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3a829f0+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| a5d243c+dirty | 48.30 MiB | 53.54 MiB | 5.23 MiB |
| a858ac3+dirty | 49.74 MiB | 55.08 MiB | 5.34 MiB |
| f9c1ed4+dirty | 49.74 MiB | 54.86 MiB | 5.12 MiB |
| b9bebee+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| a0d8cf8+dirty | 48.30 MiB | 53.49 MiB | 5.19 MiB |
| 7d8c8bd+dirty | 48.30 MiB | 53.54 MiB | 5.23 MiB |
| af33f3b+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| 1122a96+dirty | 48.30 MiB | 53.54 MiB | 5.24 MiB |
| 8448c07+dirty | 49.74 MiB | 55.37 MiB | 5.63 MiB |
6 tasks
This was referenced Sep 16, 2026
alwx
added a commit
that referenced
this pull request
Sep 17, 2026
Merging with the per-class masking PR (#6725) pushed mobilereplay.ts back over the 300-line lint budget.
alwx
added a commit
that referenced
this pull request
Sep 17, 2026
…d App Hang (#6727) * feat(replay): add avoidForegroundResumeHang to mitigate iOS foreground App Hang sentry-cocoa resumes Session Replay capture synchronously on UIApplicationDidBecomeActiveNotification. On a heavy view hierarchy this can block the main thread past iOS's foreground-transition watchdog and get the app killed. A manual pause()/resume() doesn't help - cocoa's automatic resume shares the same state and unconditionally re-arms capture regardless of it. When enabled, an AppState listener stops replay just before the app backgrounds (which makes the automatic resume a safe no-op) and restarts it in buffer mode a short delay after returning to the foreground, off the watchdog window. Fixes #6701 * fix(replay): address review findings in foreground replay guard - Stale cached replay id: the guard now calls into mobilereplay.ts's own cache-invalidation path instead of raw NATIVE.stopReplay/ startReplayBuffering, so getReplayId()/DSC/metric linking don't keep pointing at the pre-background session. - Dropped restart on interrupting state: the state machine no longer clears the pending restart on every AppState event - only on an actual background/active transition, with a stoppedByGuard flag instead of an eagerly-reset boolean. - Never detached: setupForegroundReplayGuard now registers detach on the client's 'close' hook. - Missing inactive handling: iOS can suspend JS between 'inactive' and 'background', so 'background' may never arrive. Mirrors onSpanEndUtils' cancelInBackground pattern - a delayed, cancelable stop on 'inactive' as a fallback. * fix(replay): close two race conditions in the foreground replay guard - stoppedByGuard was cleared before startReplayBuffering() settled, so a background event landing while the restart was still in flight could miss stopping the newly-started (unprotected) session. Track the in-flight restart separately and stop the new session once it resolves if a background event arrived in the meantime. - A rejected stopReplay() left stoppedByGuard set to true, so a later foreground event would schedule a restart against a session that may never have actually stopped. Reset the flag on stop failure instead. Also moves the restart delay's default (1000ms) into setupForegroundReplayGuard itself, keeping the mobilereplay.ts call site short enough to avoid the formatter wrapping it across extra lines. * test(replay): consolidate overlapping foreground guard test cases Merges near-duplicate cases (repeated background/active events tested across separate its; the two cache-invalidation tests) and removes a test that duplicated the "stopReplay rejects" scenario with only a different assertion. No coverage lost - one regression case remains per behavior/bug found, just fewer redundant setups. * test(replay): cut foreground guard tests down to one case per behavior 26 tests / 540 lines was excessive for this module. Down to 11 tests / 287 lines: one test per real behavior or regression (each bug found in review still has a dedicated case), dropped separate tests for symmetric/lower-risk paths (e.g. startReplayBuffering rejection logging, detach's inactive-fallback branch) and combined idempotency checks into the tests they naturally belong to instead of standalone cases. * fix(replay): guard the foreground replay guard against two more races - detach() only cleared timers, not an in-flight restart's own promise callback. If the client closed while a restart was underway and a background event had landed, the callback could still fire stopReplay() after close. Added a detached flag checked before any such post-detach side effect. - restart() fired startReplayBuffering() purely on a fixed delay, assuming that was enough time for stopReplay() to finish. If stop is slow (or the configured delay is short), the calls could overlap. restart() now waits for the in-flight stop to actually settle first. * fix(replay): check current app state, not a latched flag, after a restart backgroundedDuringRestart was set once (on a background event landing mid-restart) and never revisited. If the app went active again before the restart settled, its resolution still stopped the just-started session based on that stale flag - and nothing rescheduled a restart, since we were already active. Replaced it with isBackgrounded, updated on every transition and checked (not latched) when the restart resolves, so the decision reflects where we ended up, not where we were partway through. * docs(replay): clarify why a failed restart doesn't reset stoppedByGuard Leaving it true is intentional: the next 'active' event retries the restart. Resetting it would suppress that implicit retry. * style(replay): drop braces on a single-statement guard to fit max-lines Merging with the per-class masking PR (#6725) pushed mobilereplay.ts back over the 300-line lint budget. * style(replay): restore braces on the guard, compact mergeOptions instead yarn fix doesn't enforce max-lines (only lint:oxlint's --deny-warnings does), so the previous brace removal was masking the real budget issue rather than fixing it. Compacting the pre-existing merged-object literal in mergeOptions frees enough room to keep braces on the guard. * docs(replay): mark avoidForegroundResumeHang as experimental A stopgap for a sentry-cocoa issue, not a permanent API commitment. Per antonis's review - avoids locking in new public surface while the proper fix is pursued upstream. * fix(replay): don't start a new replay session after detach restart() guarded the post-success stopIfNeeded() call against a detach that happened mid-restart, but not the startReplayBuffering() call itself: closing the client while still waiting on pendingStop left the scheduled restart free to start a new native session anyway.
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📢 Type of change
📜 Description
Adds two optional fields to
mobileReplayIntegration's options (MobileReplayOptions):maskedViewClasses?: string[]— native view class names to mask during Session Replay, in addition to the classes implied bymaskAllText/maskAllImages/maskAllVectors.unmaskedViewClasses?: string[]— native view class names to unmask, taking precedence over themaskAll*defaults.Class names are the native names (e.g.
'RCTTextView'on iOS,'android.widget.TextView'on Android), not React component names.The fields ride the existing options merge and are forwarded across the bridge into the native SDKs' already-existing per-class replay APIs:
RNSentryReplay.mm): masked names are appended ingetReplayRNRedactClasses:on top of themaskAll*defaults; unmasked names are passed through asunmaskedViewClasses. Both are resolved by the native SDK viaNSClassFromString.RNSentryStart.java): forwarded intoSentryReplayOptions.addMaskViewClass(...)/addUnmaskViewClass(...).This mirrors the existing per-class screenshot masking (
options.screenshot.maskedViewClasses/unmaskedViewClasses) for Session Replay.No native SDK (sentry-cocoa / sentry-android) changes are required — the per-class replay APIs already exist. No public barrel or API-report changes (
MobileReplayOptionsis not exported fromsrc/js/index.ts), so this is purely additive.💡 Motivation and Context
Users want to mask (or explicitly unmask) specific native view classes in Session Replay without having to enable a blanket
maskAll*.Fixes #4628.
Partially addresses #6601 (this delivers masking by native class name; the per-instance component-prop request in #6601 remains open).
💚 How did you test it?
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps