Planning: Session replay form/input masking
Status: Future / optional. Not implemented. Use this document when adding form masking to rrweb session recordings.
References: rrweb record options, rrweb privacy.
Goal
When a “masking” setting is enabled (e.g. “mask form inputs in session replay” or the existing “mask email and phone for marketing”), session recordings should not capture sensitive form input values. Masking must happen at record time in the tracker; rrweb does not support post-recording masking of already-captured input values.
Current behaviour
- Tracker (
src/tracking/trackerScript.ts): Loads rrweb from CDN and callsrecord()with:blockClass: 'rr-block'— elements with this class are replaced by a block in replay.maskInputOptions: { password: true }— only password fields are masked; email, tel, and text inputs are recorded in full.
- Masking elsewhere: The app has API/dashboard PII masking (
mask_email_phone_for_marketingin AppSettings). That affects only API responses and dashboard display for the marketing role; it does not affect what the tracker records or what is stored in rrweb events in R2.
Desired behaviour
When “mask form inputs in replay” (or equivalent) is on:
- The tracker receives this setting at init/record time (e.g. from session start response or a config endpoint).
- rrweb is started with options that mask or block form-related content:
- Option A (recommended): Broaden
maskInputOptionsto includeemail,tel,text(and keeppassword: true), so input values are replaced by placeholders in the recording. - Option B: Use
maskAllInputs: trueto mask every input element. - Option C: Use
blockClass(e.g. keep or extendrr-block) so that form containers with that class are fully blocked in replay; site owners (or the form script) add the class when masking is desired.
- Option A (recommended): Broaden
Recordings already stored in R2 cannot be retroactively masked; only new recordings will respect the setting.
High-level flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ App / site configuration │
│ • New setting e.g. mask_form_inputs_in_replay (or reuse existing masking) │
│ • Stored in AppSettings and/or per-site config │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Tracker init │
│ • Script loads, calls POST /track/session/start (or GET /track/config) │
│ • Response includes flag: e.g. mask_form_inputs_in_replay: true │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ startRrwebRecording() │
│ • If mask_form_inputs_in_replay === true: │
│ record({ maskInputOptions: { password: true, email: true, tel: true, │
│ text: true }, ... }) │
│ else: │
│ record({ maskInputOptions: { password: true }, ... }) // current │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ rrweb events (masked at source) → POST /track/recording → R2 │
│ Replay viewer shows inputs as masked; no post-processing needed. │
└─────────────────────────────────────────────────────────────────────────────┘Design choices (to decide when implementing)
1. Setting scope and name
- Option A: New app-level setting: e.g.
mask_form_inputs_in_replayin AppSettings (dashboard Settings page). When on, all sites using the tracker with this Worker get form masking in replay. - Option B: Reuse “mask email and phone for marketing” so that when that is on, in addition to API/dashboard masking we also enable form masking in replay. Con: that setting is about who sees data (marketing vs admin), not whether we record PII; coupling could be confusing.
- Option C: Per-site or per–site-key setting (e.g. in Sites table or a config endpoint keyed by site). Allows some forms to be masked and others not. More flexible, more implementation work.
Recommendation: Start with a single app-level setting (Option A) for simplicity; add per-site later if needed.
2. How the tracker gets the setting
- Option A: Include in
POST /track/session/startresponse. Worker loads AppSettings (or config) and returns e.g.{ session_id, started_at, visitor_id, mask_form_inputs_in_replay: true }. Tracker already waits for session start before callingstartRrwebRecording(); no extra round trip. - Option B: New endpoint e.g.
GET /track/config?site_key=...returning{ mask_form_inputs_in_replay: true }. Tracker calls it before or after session start. Adds a request and requires site_key in tracker init if not already present. - Option C: Inject config when serving
tracker.js: Worker replaces a placeholder in the script with the current setting. No extra request; config is fixed at script load time and may be cached.
Recommendation: Option A (session start response) — one round trip, no new endpoint, config is up to date when recording starts.
3. rrweb options when masking is on
- maskInputOptions:
{ password: true, email: true, tel: true, text: true }— masks by input type. Covers most form fields (email, phone, plain text). Leaves other types (e.g. number, date) unmasked unless we add more. - maskAllInputs:
true— masks every<input>. Simpler and complete; may mask non-sensitive fields (e.g. search) as well. - blockClass:
KeepblockClass: 'rr-block'so that any element with classrr-blockis fully blocked. Site or form script can add this to whole form wrappers for “block entire form” behaviour alongside input-level masking.
Recommendation: When the setting is on, use maskInputOptions: { password: true, email: true, tel: true, text: true } for a good balance; document rr-block for full-block use. Revisit maskAllInputs if product wants “mask everything” with no exceptions.
4. Backward compatibility
- Recordings made before the feature are unchanged (no masking).
- If the Worker is rolled back or the setting is off, new recordings again use only
password: true. - Replay viewer needs no change; rrweb player already replays whatever was recorded (masked or not).
Implementation outline (when building)
- AppSettings / DB: Add
mask_form_inputs_in_replay(boolean, default e.g.false) in AppSettings table and migration; expose in dashboard Settings and inPATCH /analytics/settings(admin-only). - Session start: In the handler for
POST /track/session/start, load app settings (or minimal config) and includemask_form_inputs_in_replayin the JSON response. - Tracker script: In the inline script, store the flag from session start response; in
startRrwebRecording(), pass the broadermaskInputOptions(and keepblockClass: 'rr-block') when the flag is true. - Documentation: Update visitor-tracking and settings docs to describe the new setting and that masking applies only to new recordings.
Comparison with current approach
| Aspect | Current | With form masking on |
|---|---|---|
| Password fields | Masked by rrweb | Still masked |
| Email / tel / text inputs | Recorded in full | Masked in recording (placeholder in replay) |
| Who controls masking | N/A | App/site setting |
| Existing recordings in R2 | Unchanged | Unchanged (no retroactive masking) |
| API/dashboard PII masking | Separate (marketing role) | Unchanged; replay masking is independent |
When to consider implementing
- You want to avoid storing or replaying sensitive form input (email, phone, free text) in session recordings when a site or org has “masking” enabled.
- You are aligning with privacy expectations or compliance where “mask form inputs in replay” is a requested option.
- You already have (or plan) a Settings UI and AppSettings for app-level toggles so adding one more is consistent.
Related docs
- Visitor tracking — tracker script, session start, events, rrweb recording.
- Settings — AppSettings, dashboard Settings page,
mask_email_phone_for_marketing. - ROADMAP.md — Phase 6 “Session replay form/input masking” (later).
- rrweb record options —
maskInputOptions,maskAllInputs,blockClass.