Skip to content

feat(replay): Add per-class Session Replay masking - #6725

Merged
antonis merged 2 commits into
mainfrom
feat/replay-per-class-masking
Sep 16, 2026
Merged

antonis merged 2 commits into
mainfrom
feat/replay-per-class-masking

Conversation

@antonis

@antonis antonis commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • New feature

📜 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 by maskAllText / maskAllImages / maskAllVectors.
  • unmaskedViewClasses?: string[] — native view class names to unmask, taking precedence over the maskAll* 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:

  • iOS (RNSentryReplay.mm): masked names are appended in getReplayRNRedactClasses: on top of the maskAll* defaults; unmasked names are passed through as unmaskedViewClasses. Both are resolved by the native SDK via NSClassFromString.
  • Android (RNSentryStart.java): forwarded into SentryReplayOptions.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 (MobileReplayOptions is not exported from src/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

  • I added tests to verify changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed. feat(react-native): Document per-class Session Replay masking sentry-docs#19433
  • I updated the wizard if needed.
  • All tests passing.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.
  • No breaking changes.

🔮 Next steps

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>
@github-actions

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

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


  • feat(replay): Add per-class Session Replay masking by antonis in #6725
  • test(e2e): Verify replay runtime controls drive native recording by antonis in #6704
  • feat(replay): Add Session Replay runtime controls by antonis in #6703
  • fix(android): honor host app's pinned ndkVersion by alwx in #6724
  • chore: Deduplicate yarn.lock by antonis in #6706
  • fix(tracing): background root spans overwrite native propagation context by alwx in #6720
  • ci(e2e): Fix broken idb-companion install on iOS e2e by antonis in #6719
  • chore(deps): Bump smol-toml to ^1.7.1 by antonis in #6713
  • chore(deps): Bump js-yaml to patched versions to resolve security alerts by antonis in #6710
  • chore(deps): Bump morgan to ^1.12.0 to resolve security alert by antonis in #6712
  • chore(deps): Bump joi to ^17.13.6 by antonis in #6714
  • chore(deps): Bump sharp to ^0.35.4 to resolve security alert by antonis in #6711
  • chore(deps): bump actions/setup-java from 6.0.0 to 6.0.1 by dependabot in #6716
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.30.1 to 2.31.0 by dependabot in #6718
  • chore(deps): bump getsentry/craft from 2.30.1 to 2.31.0 by dependabot in #6717
  • chore(deps): bump the codeql-action group with 3 updates by dependabot in #6715
  • fix(core): Fix process.exit typo in debugid script by friederbluemle in #6707
  • chore(ci): Only build production builds for visionOS and tvOS by antonis in #6700
  • chore(deps): update Sentry Android Gradle Plugin to v6.22.0 by github-actions in #6699

🤖 This preview updates automatically when you update the PR.

@github-actions

github-actions Bot commented Sep 15, 2026

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

Generated by 🚫 dangerJS against a11dd1f

@antonis antonis added the ready-to-merge Triggers the full CI test suite label Sep 15, 2026
Added per-class Session Replay masking options for mobileReplayIntegration.

@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.

✅ 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.

@github-actions

Copy link
Copy Markdown
Contributor

Android (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 421.90 ms 438.37 ms 16.47 ms
Size 50.56 MiB 56.51 MiB 5.95 MiB

Baseline results on branch: main

Startup times

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
antonis marked this pull request as ready for review September 15, 2026 10:51
@sentry

sentry Bot commented Sep 15, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
Sentry RN io.sentry.reactnative.sample 8.26.0 (106) Release

⚙️ sentry-react-native Build Distribution Settings

@github-actions

Copy link
Copy Markdown
Contributor

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3869.05 ms 1244.17 ms -2624.88 ms
Size 5.15 MiB 6.90 MiB 1.75 MiB

Baseline results on branch: main

Startup times

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

@github-actions

Copy link
Copy Markdown
Contributor

iOS (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3863.44 ms 1232.80 ms -2630.65 ms
Size 5.15 MiB 6.90 MiB 1.75 MiB

Baseline results on branch: main

Startup times

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

@github-actions

Copy link
Copy Markdown
Contributor

Android (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 425.74 ms 461.31 ms 35.57 ms
Size 50.56 MiB 56.51 MiB 5.95 MiB

Baseline results on branch: main

Startup times

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

@lucas-zimerman lucas-zimerman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

@antonis
antonis merged commit aecf0c8 into main Sep 16, 2026
115 of 116 checks passed
@antonis
antonis deleted the feat/replay-per-class-masking branch September 16, 2026 05:05
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Triggers the full CI test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Add text input masking

2 participants