# Session Replay

Copy as Markdown[Open in ChatGPT](https://chatgpt.com/?q=Read%20https%3A%2F%2Fcoralogix.com%2Fdocs%2Fuser-guides%2Frum%2Fsdk-installation%2Fflutter%2Fmobile%2Ffeatures%2Fsession-replay.md%20and%20help%20me%20with%20my%20question%20about%20this%20Coralogix%20documentation%20page.)[Open in Claude](https://claude.ai/new?q=Read%20https%3A%2F%2Fcoralogix.com%2Fdocs%2Fuser-guides%2Frum%2Fsdk-installation%2Fflutter%2Fmobile%2Ffeatures%2Fsession-replay.md%20and%20help%20me%20with%20my%20question%20about%20this%20Coralogix%20documentation%20page.)

## Session replay[​](#session-replay "Direct link to Session replay")

Session Replay allows you to record and replay user sessions for debugging and analytics purposes.

### Setup[​](#setup "Direct link to Setup")

First, initialize the Session Replay masking handler in your `main()` function:

```
import 'package:cx_flutter_plugin/cx_session_replay_masking.dart';



Future<void> main() async {

  WidgetsFlutterBinding.ensureInitialized();

  await SessionReplayMasking.initialize();

  

  runZonedGuarded(() {

    runApp(const MyApp());

  }, (error, stackTrace) {

    CxFlutterPlugin.reportError(error, {}, stackTrace.toString());

  });

}
```

### Initialize session replay[​](#initialize-session-replay "Direct link to Initialize session replay")

```
import 'package:cx_flutter_plugin/cx_session_replay_options.dart';



final options = CXSessionReplayOptions(

  captureScale: 1.0,                    // Screenshot scale (0.0-1.0)

  captureCompressQuality: 0.8,          // JPEG compression quality (0.0-1.0)

  sessionRecordingSampleRate: 100,      // Percentage of sessions to record (0-100)

  autoStartSessionRecording: true,      // Start recording automatically

  maskAllTexts: false,                  // Mask all text in screenshots

  textsToMask: ['password', 'credit'], // Regex patterns — mask text containing these words

  maskAllImages: false,                 // Mask all images in screenshots

);



// Masking for Flutter content is performed in DART on both iOS and Android: the

// native SDK requests a pre-masked bitmap of the Flutter view on each capture, and

// the plugin walks the render tree to black out text/images before handing the bytes

// to native. CXSessionReplayOptions is the single source of truth for masking —

// SessionReplayMasking.initialize() in main() only registers the handler and takes

// no masking arguments.

// textsToMask patterns are RegExp strings, case-sensitive by default ('password'

// will not match "Password"). Dart's RegExp has no inline (?i) flag — use a character

// class instead: '[Pp]assword'. Text fields (RenderEditable) are always masked while

// masking is active; their live content is not read, to avoid inspecting sensitive input.



await CxFlutterPlugin.initializeSessionReplay(options);
```

### Masking[​](#masking "Direct link to Masking")

All masking is configured through `CXSessionReplayOptions` — that single object is the source of truth. `SessionReplayMasking.initialize()` in `main()` only registers the handler; it takes no masking arguments. To change masking later, just call `initializeSessionReplay(newOptions)` again.

**Mask everything (text + images):**

```
CXSessionReplayOptions(

  maskAllTexts: true,

  maskAllImages: true,

  // ...other options

)
```

**Mask only specific text (regex), leave the rest visible:**

```
CXSessionReplayOptions(

  maskAllTexts: false,                  // turn off "mask all text"

  textsToMask: [r'\d{3}-\d{2}-\d{4}',   // e.g. SSNs

                '^Session.*',           // text starting with "Session"

                'password'],            // RegExp strings, case-sensitive

  maskAllImages: false,

)
```

**Mask only images:**

```
CXSessionReplayOptions(maskAllTexts: false, maskAllImages: true)
```

**Mask one specific widget** (regardless of the flags above) — wrap it in `MaskedWidget`:

```
import 'package:cx_flutter_plugin/cx_session_replay_masking.dart';



MaskedWidget(child: Image.network(avatarUrl))



// Conditional masking:

MaskedWidget(

  isMasked: true,  // set to false to temporarily disable

  child: TextField(decoration: InputDecoration(labelText: 'Credit Card')),

)
```

Notes:

* `maskAllTexts` and `maskAllImages` are independent — enable either or both.
* `maskAllImages` covers `Image.*`, `RawImage`, and `Container`/`CircleAvatar` background images (`DecorationImage`). For content the render tree can't expose (platform views, native maps/WebViews), wrap it in `MaskedWidget`.
* Icons (`Icon`, glyph fonts) are treated as neither text nor images and stay visible.

#### Masked interactions[​](#masked-interactions "Direct link to Masked interactions")

When automatic user-interaction tracking (`userActions`) is enabled, a tap on content wrapped in `MaskedWidget` is reported with masking awareness:

* **The interaction event is still emitted** (you keep your analytics), with `interaction_context.is_masked_element: true`.
* **The visible text is redacted**: `target_element_inner_text` is reported as `***`.
* **Identity fields and coordinates are reported as-is** (`target_element`, `element_classes`, x/y).
* **The Session Replay tap marker is suppressed** for that tap, so the replay never reveals which part of the masked area was touched.

The verdict is resolved on the tapped widget's tree (the same target the identity fields describe), in both directions:

* A tap on an *unmasked* child rendered inside a `MaskedWidget` subtree is masked (masked ancestors count) — including platform views (e.g. a WebView) wrapped in `MaskedWidget`.
* A tap anywhere on a control whose own subtree contains a `MaskedWidget` is masked — tapping a button's padding resolves the same as tapping its masked child `Text`, so the verdict never depends on whether the finger hit the text glyphs or the padding.

**Only `MaskedWidget` drives the interaction verdict.** The session-replay pixel policy — `maskAllTexts`, `textsToMask`, `maskAllImages` — masks **pixels in the replay only**: it suppresses tap markers over the masked regions, but taps on policy-masked content still report `is_masked_element: false` and their text in clear. The flag and the replay marker can therefore disagree for policy-masked content. This split is deliberate and matches the Android and iOS SDKs: the replay policy answers *"should these pixels be hidden"* per frame, which is not the same question as *"may this text leave the device"*. Content whose interactions must be masked needs the explicit `MaskedWidget` opt-in, which feeds both.

Masking operates at three granularities, each deliberate:

* **Replay pixels** are masked content-granular — exactly the text glyphs / images (the button chrome around a masked label stays visible).
* **Replay tap markers** are suppressed control-granular: a masked label inside a button suppresses the marker for a press *anywhere on that button* (any mask source). Otherwise marker positions on a masked-label keypad would reconstruct the entered sequence. This matches classic Android natively (`Button extends TextView` — the whole button is masked); Jetpack Compose remains text-granular in the native SDK.
* **Interaction events** carry the semantic verdict on the tapped control, from `MaskedWidget` only, as described above.

`is_masked_element` is reported on tap events, and is `false` whenever masking cannot be resolved (for example, the tapped widget cannot be determined). It is not part of scroll and swipe events, and native SDK versions older than the ones this plugin pins omit it entirely — so in `beforeSend` the field is nullable (`event.interactionContext?.isMaskedElement`) and a missing value means "not masked". Compare it with `== true` rather than treating it as a plain boolean.

> **Keypad guidance:** because identity fields are reported as-is even for masked taps, keys of a masked PIN pad (or any per-character input) must share a single identifier — build every key from the same widget type and do not give keys per-digit ids, semantic labels, or tooltips. Otherwise the reported identity reveals exactly what masking hides.

If your privacy policy requires more than redaction, use `beforeSend`:

```
// Recipe 1 — drop every masked interaction entirely:

beforeSend: (event) {

  if (event.interactionContext?.isMaskedElement == true) {

    return null; // event is not sent

  }

  return event;

},



// Recipe 2 — keep the event, blank the coordinates:

beforeSend: (event) {

  final interaction = event.interactionContext;

  if (interaction?.isMaskedElement == true) {

    interaction!.attributes?['x'] = 0;

    interaction.attributes?['y'] = 0;

  }

  return event;

},
```

### Check status[​](#check-status "Direct link to Check status")

```
// Check if Session Replay is initialized

final isInitialized = await CxFlutterPlugin.isSessionReplayInitialized();



// Check if currently recording

final isRecording = await CxFlutterPlugin.isRecording();
```

### Control recording[​](#control-recording "Direct link to Control recording")

```
// Start recording

await CxFlutterPlugin.startSessionRecording();



// Stop recording

await CxFlutterPlugin.stopSessionRecording();



// Shutdown Session Replay

await CxFlutterPlugin.shutdownSessionReplay();
```

### Capture manual screenshot[​](#capture-manual-screenshot "Direct link to Capture manual screenshot")

```
await CxFlutterPlugin.captureScreenshot();
```

### Get session replay folder path[​](#get-session-replay-folder-path "Direct link to Get session replay folder path")

```
final folderPath = await CxFlutterPlugin.getSessionReplayFolderPath();
```

For more info check <https://github.com/coralogix/cx-ios-sdk/tree/master/Coralogix/Docs>.
