> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apsio.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Replay and masking

> What session replay records, how it masks on the device, and how to set what it shows, on iOS, Android and React Native.

Replay records the layout of the screen as a wireframe, never pixels: the kind of each view,
its position and size, its style, its state and the touches over it. Text is masked on the
device before a frame leaves memory, so what reaches Apsio is already masked.

## When a session is recorded

Replay is off until your app turns it on: `ApsioReplayOptions(enabled: true)` on
[iOS](/sdks/ios#replay), `replay.enabled = true` on [Android](/sdks/android#replay), and the
`replay` option on [React Native](/sdks/react-native#session-replay). Then a session is
recorded when all of these hold:

* the [remote configuration](/remote-configuration) has not turned replay off;
* the session is kept by [sampling](/concepts/sampling): replay is part of a session's detail.

The SDK captures the screen only after something changed (a touch, a new screen, the keyboard,
a scroll), at most twice a second, so a still screen costs nothing. Frames are grouped into
segments of about ten seconds, compressed and uploaded as attachments; the open segment is
closed when the app moves to the background, so a crash loses at most the last few seconds.
Withdrawing consent or the kill switch drops the segments not yet sent.

Replay is detail: it is kept for your plan's detail retention (see [Pricing](/pricing)). Read a
session's segments with `GET /v1/projects/{projectId}/sessions/{sessionId}/replay/segments` and
each one with `GET /v1/projects/{projectId}/attachments/{attachmentId}`.

## What is masked by default

Masking fails closed. With no setting at all:

* **All text is masked**, not only inputs: every character becomes `x`, and spaces and line
  breaks stay, so a masked text keeps its length and shape. You can opt down to masking text
  inputs only (`textMasking` `inputs`).
* **Secure fields** (passwords, one-time codes, card numbers) are always masked. No setting
  shows them.
* **Images** are boxes.
* **What the SDK cannot read is a hidden box**, with nothing inside: web views, maps, video,
  camera previews, and views that draw their own content. On iOS, SwiftUI content is drawn into
  plain layers the SDK cannot read, so it is recorded as boxes without content.
* **Touches** are recorded only over what is shown: never over masked text, an image, a secure
  field, the keyboard or a hidden box. One masked or hidden view under a touch is enough to drop
  it, whatever is drawn above, so taps on a PIN pad of plain buttons do not give the PIN away.
  You can also turn touches off (`touches` `hide`).

## Your settings

Each view can be given one of three settings:

| Setting | What replay does |
| - | - |
| `mask` | Replaces the view's text, and hides touches over it |
| `unmask` | Shows the view's text (never a secure field's) |
| `hide` | Records the view as a box with nothing inside, and nothing of what it contains |

You set them on a view instance, on a class of views, or for the whole app (the text level), and
the remote configuration can add its own. The rules are the same on every platform:

1. Secure fields are always masked.
2. Nothing is shown inside a hidden view.
3. At one level, `hide` beats `mask`, which beats `unmask`.
4. The nearest setting wins: a view's own setting beats its class's, and its class's beats the
   app-wide text level. A view without one takes its nearest ancestor's.

### Per view

| Platform | How |
| - | - |
| iOS, UIKit | `view.apsioReplayMask()`, `view.apsioReplayUnmask()`, `view.apsioReplayHide()` |
| iOS, SwiftUI | `.apsioReplayHide()` and `.apsioReplayMask()`; `.apsioReplayUnmask()` has no effect, since SwiftUI content is never read |
| Android, views | `Apsio.replayMask(view)`, `Apsio.replayUnmask(view)`, `Apsio.replayHide(view)` |
| Android, Compose | `Modifier.apsioReplayMask()`, `apsioReplayUnmask()`, `apsioReplayHide()` |
| React Native | `replayProps('mask' \| 'unmask' \| 'hide')` or `<ApsioReplayMask setting="...">` |

### Tokens in an identifier

A view whose accessibility identifier (iOS), view tag or Compose test tag (Android) or `testID`
(React Native) holds the word `apsio-mask`, `apsio-unmask` or `apsio-hide` gets that setting,
as if set on the instance. This is how React Native sets them, and it works in any app.

A token counts only as a **whole word**, exactly as written. A word is a run of ASCII letters,
digits and hyphens; every other character, the underscore, the dot and white space included,
separates words.

| Identifier | Setting |
| - | - |
| `row apsio-mask` | mask |
| `checkout.coupon.apsio-hide` | hide |
| `card_number_apsio-mask` | mask |
| `price_apsio-mask,total` | mask |
| `apsio-unmasked-total` | none: `apsio-unmasked-total` is one word |
| `my-apsio-hide` | none: one word |
| `apsio-unmask2` | none |
| `Apsio-Hide` | none: the case differs |

So a test identifier never unmasks a view by accident.

### Per class

`maskClasses`, `unmaskClasses` and `hideClasses` apply a setting to every view of a class or its
subclasses, such as a view a library draws. The SDK matches classes by their name at run time:

* **iOS:** the name `NSStringFromClass` gives. A Swift class carries its module:
  `Checkout.CardField`, not `CardField`. In Swift, pass the class itself
  (`hideClasses: [CardField.self]`) and the SDK takes its name.
* **Android:** the fully qualified name. R8 renames classes in a minified release build, so keep
  the name of every class you name, with `-keepnames class com.example.checkout.CardView` in your
  R8 rules, or pass the class itself (`replay.hideClass(CardView::class.java)`), which needs no
  rule. The SDK keeps the names of the platform, AndroidX, Material and React Native classes it
  recognizes. See [Class names and R8](/sdks/android#class-names-and-r8).
* **React Native:** the native class names above, one list for both platforms.

### Strict mode

A name in a hide or mask list, yours or the remote configuration's, that matches no class of the
app would fail open: the views it was meant to hide would be shown. So a name that does not
resolve, whether a typo, a Swift class without its module or a class R8 renamed, puts the session
in **strict mode** for the rest of its replay:

* every text and input is masked and every image hidden, whatever unmasks them;
* every view the SDK knows only as its own drawing is hidden;
* the device logs a warning naming the classes, and the session gets one
  `apsio.replay.strict_mode` record whose `apsio.replay.unresolved_classes` lists them.

The names are checked again for every session, so fixing the name or keeping it from R8 ends
strict mode from the next session. A name in an unmask list that does not resolve unmasks
nothing and does not start strict mode.

### Remote tightening

The [remote configuration](/remote-configuration) can mask more without a release: mask all
text, which voids every unmask the app set, and add mask and hide classes. Its classes win over
every setting of the app, on the view and on everything inside it. It can never loosen masking.

## Check what is recorded

The **mask preview** draws over your app what replay records, from each capture: red where it
masks, grey where it hides, and a green outline where touches are recorded. It works whether or
not the session records replay, and only in a debug build:

| Platform | Call |
| - | - |
| iOS | `Apsio.showReplayMaskPreview(true)`, in `DEBUG` builds only |
| Android | `Apsio.showReplayMaskPreview(true)`, drawn only when the app is debuggable |

Check every screen with the preview before you turn replay on in a release.

## What it costs

Reading the screen is the only work on the main thread. The SDKs budget it: when one capture
takes longer than 4 ms, captures come less often. Masking the text, comparing frames and
compressing them happen on a background thread. A large view tree is cut: past 2,000 views on
iOS, the rest of the tree is one hidden box.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.