---
title: Session Replay - Android
description: Learn how to integrate and configure Session Replay capabilities in your Android app with the CSQ SDK
lastUpdated: 11 March 2026
source_url:
  html: https://docs.contentsquare.com/en/csq-sdk-android/experience-analytics/session-replay/
  md: https://docs.contentsquare.com/en/csq-sdk-android/experience-analytics/session-replay/index.md
---

Session Replay enables replaying real sessions to observe how users interact with your product, by capturing screen content - including text and images - as users navigate your mobile app. For more details, see [Introduction to Session Replay for Apps ↗](https://support.contentsquare.com/hc/en-us/articles/37271667148561-Introduction-to-Session-Replay-for-Apps) in the Help Center.

## Starter guide

### Prerequisites

#### Update to latest SDK version

In order to enable Session Replay in your app and get the most stable version, it is required to [upgrade the SDK](https://docs.contentsquare.com/en/csq-sdk-android/experience-analytics/#install-the-sdk) to its latest version.

#### Implement screen tracking

Ensure screen tracking is implemented, since Session Replay collection starts at the first screenview event. See [Track screens](../track-screens/).

#### Handle User consent

Ensure user consent is handled for Contentsquare data collection, especially if you’re implementing the SDK for the first time. See [Privacy](../privacy/) section.

#### Add Gradle dependency for Jetpack Compose

To enable Jetpack Compose support in Session Replay, it is necessary to add a new Gradle dependency to your Gradle build file. Make sure to follow the [Track screens](../track-screens/#jetpack-compose-support) section.

* Kotlin

  **build.gradle.kts**

  ```kotlin
  implementation("com.contentsquare.android:sdk-compose:1.5.1")
  ```

* Groovy

  **build.gradle**

  ```groovy
  implementation "com.contentsquare.android:sdk-compose:1.5.1"
  ```

Note

Mandatory data collection settings are configured by Contentsquare in your project parameters. [Learn more about Data collection configurations ↗](https://support.contentsquare.com/hc/en-us/articles/37271667148561-Introduction-to-Session-Replay-for-Apps) on the Help Center.

### Configure Session Replay masking

#### Masking behavior

By default, the Contentsquare SDK is configured to mask all content displayed on the user interface (UI) to prevent unnecessary data collection. For more details, see what a fully masked screen looks like in [Masking mechanisms](#masking-mechanisms).

**For testing purposes**, use this setup:

* Either unmasking all the content by setting the [default masking](#default-masking) status to false.
* Or unmasking most content with the [Text and Image unmasking configuration ↗](https://support.contentsquare.com/hc/en-us/articles/37271682878353-Data-masking-in-Session-Replay-Mobile-Apps) in your Console parameters.

**For more granular control in deployment mode**, you can use the [Text and Image unmasking configuration ↗](https://support.contentsquare.com/hc/en-us/articles/17800059306396-Data-masking-in-Session-Replay-Mobile-Apps) in your Console parameters in combination with [SDK public masking APIs](#public-masking-apis) to fine-tune exactly which elements remain masked.

Warning

Always keep sensitive data masked to ensure compliance with privacy regulations and to protect users' personal data.

### Configure Event-Triggered Replays (optional)

If Event-Triggered Replays is included in your contract, you can set up ETR events to enable specific screen and session collection in Session Replay. See [Event-Triggered Replays (ETR)](#event-triggered-replays-etr).

### Configure Webviews (optional)

If your mobile app includes Webviews, specific implementation steps are required to ensure they are properly reflected in Session Replay:

* Implement [Webview tracking](https://docs.contentsquare.com/en/webview-tracking-tag/)
* Handle [personal data within WebViews](https://docs.contentsquare.com/en/webview-tracking-tag/session-replay/#personal-data-handling)

### Test your setup

#### Enable Session Replay on your device

If the Session Replay sampling rate is less than 100%, only a portion of sessions are collected. To ensure data collection on your device for testing and debugging purposes, you can force it from the In-app Features settings by following these steps:

1. [Enable In-app Features](../in-app-features/#enable-in-app-features)
2. Open In-app Features settings with a long press on the screenshot button
3. Under "Session Replay", toggle "Enable Session Replay" on
4. Kill the app
5. Start app, a new session is starting with Session Replay enabled

![](https://docs.contentsquare.com/_astro/android-enable-sr.Bs1TVfWt_Z1IDHPp.webp)

#### How do I know if Session Replay is enabled?

There are 2 places where you can check if Session Replay is enabled:

**In the logs:** The log `CSLIB|SessionReplay: Session Replay is starting.` will confirm that Session Replay is enabled.

**In in-app features settings:** Below the “Enable Session Replay” *(see below [Access the replay](#access-the-replay))*, you will either see:

* `No replay link available`, which means Session Replay is not running for the current session
* `Get Replay link`, which means Session Replay is running for the current session

#### Access the Replay

The replay will be **available within 5 minutes**. Only the "ended screen views" are processed *(we know a screenview is ended when we start receiving data for the next screenview)*. This means that you will be able to replay your session up to the previous screenview if the session is still running.

The Session can be accessed by tapping on `Get replay link` button from the in-app features settings:

![](https://docs.contentsquare.com/_astro/android-get-replay-link.BQJ27mmY_Z2mfm3D.webp)

## Personal Data Masking

The Session Replay feature replays every interaction of your users with your app. In order to respect the user’s right to privacy, the Contentsquare SDK:

* Masks everything by default
* Allows you to control which part of the user interface is collected via our [Masking rules](#masking-rules)

Warning

If you decide to use unmask elements leading to the collection of new data or use already collected data but for a different purpose, it is up to you to update your Google Play Data Safety declaration accordingly. Here are some common examples of data collected via unmasked session Replay: Search history, Purchase history, Advertising data, etc. See [Types of data and purpose](../privacy/) in the Contentsquare Android SDK documentation.

**Important:** Contentsquare is not intended nor designed to collect sensitive personal data (e.g. health, financial, racial data). It is the customer’s responsibility not to send any sensitive personal data to Contentsquare.

### Masking mechanisms

Every single UI element is converted into a highly pixelated image to reach a very low resolution. Text and images will appear very blurry so that the content cannot be identified.

#### Original VS Replay fully masked

Masking depends on the type of element:

* **Images**: A blurred representation is sent in place of the original content.
* **Text**: Replaced by \* (asterisk) characters, matching the original character count. Whitespace characters (spaces, tabs, line breaks) are preserved. For instance, `the lazy fox` is collected as `*** **** ***`. All other visual properties (text color, background color, alignment, etc.) are collected.
* **TextFields**: Same as Text
* **TextField with `obscureText`**: Both the obscured text and the briefly revealed character are replaced by • (bullet).
* **Containers**: Plain backgrounds, drawables and colors are send as-is. Other background elements follow the rules above.
* **Other types**: A blurred representation is sent in place of the original content.

![](https://docs.contentsquare.com/_astro/android-sr-masked.Bv63pOqr_Z1aMjBU.webp)

### Masking rules

Masking rules can be applied in two ways:

1. **Through remote masking configuration in the CSQ Console (for admin users):** These configurations are managed directly in the Console and take effect for all sessions as soon as the app is restarted or brought to the foreground See [How to customize masking rules from the Data Masking tab ↗](https://support.contentsquare.com/hc/en-us/articles/37271848716561-How-to-customize-masking-rules-from-the-Data-Masking-tab-Apps) in the Help Center.
2. **Using public masking APIs in the SDK (for mobile application developers):** These configurations require developer implementation and will be applied only after the mobile app has gone through its release cycle.

Masking rules set via the CSQ Console or the public APIs are applied according to different priorities, as described below. The SDK determines whether a view is masked by evaluating the rules in the order specified below. Once a rule is triggered, the state is set, and subsequent rules are not applied:

#### General case

| Rule | Configured via |
| - | - |
| 1. The app or SDK version is fully masked | [Data Masking tab in the CSQ Console ↗](https://support.contentsquare.com/hc/en-us/articles/37271848716561-How-to-customize-masking-rules-from-the-Data-Masking-tab-Apps) |
| 2. Remote Text or Image masking is defined | [Data Masking tab in the CSQ Console ↗](https://support.contentsquare.com/hc/en-us/articles/37271848716561-How-to-customize-masking-rules-from-the-Data-Masking-tab-Apps) |
| 3. An [instance](#maskingun-masking-by-instance) is specifically masked or unmasked | API |
| 4. A [type](#maskingun-masking-by-type) is masked or unmasked | API |
| 5. A [parent](#masking-and-unmasking-behaviors-on-a-parent-view) is specifically masked or unmasked | API |
| 6. A parent [type](#maskingun-masking-by-type) is masked or unmasked | API |
| 7. Remote Text or Image unmasking is defined | [Data Masking tab in the CSQ Console ↗](https://support.contentsquare.com/hc/en-us/articles/37271848716561-How-to-customize-masking-rules-from-the-Data-Masking-tab-Apps) |
| 8. Otherwise the [default masking state](#default-masking) is applied | API |

#### Exception: Text input fields

Special consideration is needed for text inputs, as the risk of leaking personal data is higher with these elements. As a result, the SDK applies slightly different rules to `EditText` and Jetpack Compose `TextField`:

| Rule | Configured via |
| - | - |
| 1. The app or SDK version is fully masked | [Data Masking tab in the CSQ Console ↗](https://support.contentsquare.com/hc/en-us/articles/37271848716561-How-to-customize-masking-rules-from-the-Data-Masking-tab-Apps) |
| 2. Remote Text input masking is defined | [Data Masking tab in the CSQ Console ↗](https://support.contentsquare.com/hc/en-us/articles/37271848716561-How-to-customize-masking-rules-from-the-Data-Masking-tab-Apps) |
| 3. An `EditText` [instance](#maskingun-masking-by-instance) is specifically unmasked | API |
| 4. The `EditText` [type](#maskingun-masking-by-type) is unmasked | API |
| 5. Otherwise a text input field remains masked | default |

Specifically, note that default masking and parent masking won't change the masking state of text inputs.

### Public masking APIs

#### Default masking

Warning

You should not perform any call on Contentsquare API before the first visible Activity to be sure our SDK is initialized. If you perform a call too soon, it will be discarded and you will see the following log:

```plaintext
CSLIB Contentsquare SDK: Unable to call the public API, make sure you are not opted out of the Contentsquare tracker and SDK was correctly initialized.
```

All Android View elements, their subclasses and Jetpack Compose components are fully masked by default. The SDK provides an API to change the default masking state:

```kotlin
/**
* Sets the default masking state for the Contentsquare's Session Replay features.
*
* @param masked A boolean value indicating whether masking should be enabled (true) or disabled (false).
*/
@JvmStatic
fun setDefaultMasking(masked: Boolean)
```

Note

If you change the default masking state to `false`, `EditText` will stay masked by default. This does not apply to Jetpack Compose composables, meaning `TextField` components will also be unmasked.

#### Masking/Un-masking by instance

Warning

Animations are not supported when masking by instance. To know more, see [Animations are not supported when masking by instance or type](#animations-are-unsupported-when-masking-by-instance-or-type)

* Views

  Use `mask(view: View)` and `unmask(view: View)` methods to mask or unmask a specific view instance. Alternatively you can also apply the masking rule to a specific view instance directly with `View.csqMaskContents(enable: Boolean)` extension function.

  ```kotlin
  /**
  * Masks the contents of a specific view.
  *
  * @param view an instance of [View] to mask.
  */
  @JvmStatic
  fun mask(view: View)
  ```

  ```kotlin
  /**
  * Unmasks the contents of a specific view.
  *
  * @param view an instance of [View] to unmask.
  */
  @JvmStatic
  fun unmask(view: View)
  ```

  ```kotlin
  /**
   * [View] extension function to mask the contents of a view.
   * This extension function can be used as alternative to [CSQ.mask] and [CSQ.unmask].
   *
   * @param enable A boolean value indicating whether masking should be enabled (true) or disabled (false) for this view.
   */
  fun View.csqMaskContents(enable: Boolean)
  ```

  Following the previous example, we have the views hierarchy in which all instances of TextView type are unmasked with the default masking state of all the views are masked by default.

  ![](https://docs.contentsquare.com/_astro/android-unmask-view-instance.BDxkztL1_Kt4fa.webp)

  In this example you may want to mask a specific text view TextView1, call `mask(textView1)` then this instance of TextView1 will be masked. As you can see in this example the view instance rule overrides the type rule and the default rule.

* Jetpack Compose

  Use the `CsqMask(enable: Boolean, block: @Composable () -> Unit)` composable function to mask or unmask a composable.

  ```kotlin
  /**
   * A composable function that applies masking to Compose UI content.
   *
   * @param enable A boolean value indicating whether masking should be enabled (true) or disabled (false).
   * @param block A composable lambda function representing the content to apply masking condition.
   */
  @Composable
  fun CsqMask(enable: Boolean, block: @Composable () -> Unit)
  ```

  For example:

  ```kotlin
  CsqMask(true) {
     Text(
        text = "Text"
     )
  }
  ```

#### Masking/Un-masking by type

The CSQ SDK allows masking elements by type for convenience but it isn't the recommended masking mechanism because it will impact all screens of your application. For instance, use this when you have a specific class for presenting user information or for displaying the user profile picture to make sure it always stays mask.

Warning

Animations are not supported when masking by type. To know more, see [Animations are not supported when masking by instance or type](#animations-are-unsupported-when-masking-by-instance-or-type)

* Views

  Use `mask(type: Class<*>)` and `unmask(type: Class<*>)` methods to mask or unmask a specific type.

  ```kotlin
  /**
  * Masks the contents of the view for all instances of a specific [Class] and subclass of a [View].
  * This will be applied only for Session Replay feature.
  *
  * @param type a [Class] object representing the type to be masked.
  */
  @JvmStatic
  fun mask(type: Class<*>)
  ```

  ```kotlin
  /**
  * Unmasks the contents of the view for all instances of a specific [Class] and subclass of a [View] for
  * Session Replay feature.
  *
  * @param type a [Class] object representing the type to be unmasked.
  */
  @JvmStatic
  fun unmask(type: Class<*>)
  ```

  For example, given the views hierarchy:

  ![](https://docs.contentsquare.com/_astro/android-unmask-type.C_f9t2FE_Z1WTWsj.webp)

  The default masking state of all the views is masked by default and for example, you may want to unmask all **TextView** types, you can call `unMask(TextView.class)` then all instances of **TextView** type will be unmasked. In this example, the button is also impacted by the unmask API because the button is also a **TextView** type (a subclass of **TextView**). You can also notice that the **EditText** is also a **TextView** type but it is not impacted by `unMask(TextView.class)` call because the **EditText** type is masked by default [see Masking rules](#masking-rules).

* Jetpack Compose

  As of now, it is not possible to mask or unmask composables by type.

  Alternatively, you can mask or unmask a group of composables by wrapping multiple composables within the same parent, then applying the `CsqMask(enable: Boolean, block: @Composable () -> Unit)` on the parent.

#### Masking and Unmasking behaviors on a parent view

* Views

  The masking of the ViewGroup (RelativeLayout, LinearLayout, ConstraintLayout, etc..) is supported. If a parent view is masked all of its children will inherit the masking state from the parent. The children's view masking state can be overridden by using a specific API to mask or unmask by instance or by type.

  ![](https://docs.contentsquare.com/_astro/android-unmask-parent.LsPThKi1_Z1Ixi8p.webp)

  For example, given the view hierarchy with the default masking state. All the views are masked by default in the initial state and in this case, you may want to unmask all children’s views of the parent view **LinearLayout1**, you can call `unMask(linearLayout1)` then all children views of the parent view **LinearLayout1** will be unmasked. As noticed here the **EditText** is not impacted from the parent call because the **EditText** type is masked by default [Masking rules](#masking-rules).

* Jetpack Compose

  The masking of a parent layout (for example `Column`, `Row`, or `Box`) is supported. If a parent composable is masked, then all of its children will inherit the masking state from the parent. The children's masking state can be overridden by using the dedicated `CsqMask(enable: Boolean, block: @Composable () -> Unit)` function. For example:

  ```kotlin
  CsqMask(true) {
     Column {
        Row {
           CsqMask(false) {
              // unmask this `Text` composable
              Text(
                 text = "Text",
              )
           }
        }
        Row {
           Text(
              // this `Text` composable will be masked, as it inherits the masking state from the parent
              text = "Text 2",
           )
        }
     }
  }
  ```

#### Masking and Unmasking of Bottom Sheets, Dialogs, and Menus

If you were using Contentsquare Android SDK versions prior to 4.39.0 in your mobile app, then masking is automatically enforced on all bottom sheets, dialogs, and menus, as these elements were not supported in earlier versions. To unmask any of these elements, contact your CSM to disable the forced masking and use the APIs described below.

**Bottom Sheets**

Bottom sheets can be masked or unmasked using the same APIs available for any other view, whether you're working with Android Views or Jetpack Compose. To apply the masking behavior, ensure that you call the appropriate API after the bottom sheet has been created.

**AlertDialogs**

* Views

  Dialogs, such as AlertDialog, TimePickerDialog, and DatePickerDialog, can be masked or unmasked using the APIs provided below. In addition, the masking behavior of individual components within these dialogs can be customized using the provided Kotlin extension functions.

  For instance, if an AlertDialog is fully masked by default, you can selectively unmask specific elements—such as the title or action buttons—using the extension functions provided below for more granular control.

  For example, given an AlertDialog with the default masking state, all views in the dialog are masked by default in the initial state. In this case, you may want to unmask the title and buttons of the dialog. To do that, we provide Kotlin extension functions to the Dialog API to make it easy.

  ```kotlin
  /**
   * Masks the entire [AlertDialog].
   */
  fun AlertDialog.csqMask()


  /**
   * Masks the title of the [AlertDialog].
   */
  fun AlertDialog.csqMaskTitle()


  /**
   * Masks the message of the [AlertDialog].
   */
  fun AlertDialog.csqMaskMessage()


  /**
   * Masks the positive button of the [AlertDialog].
   */
  fun AlertDialog.csqMaskPositiveButton()


  /**
   * Masks the negative button of the [AlertDialog].
   */
  fun AlertDialog.csqMaskNegativeButton()


  /**
   * Unmasks the entire [AlertDialog].
   */
  fun AlertDialog.csqUnMask()


  /**
   * Unmasks the title of the [AlertDialog].
   */
  fun AlertDialog.csqUnMaskTitle()


  /**
   * Unmasks the message of the [AlertDialog].
   */
  fun AlertDialog.csqUnMaskMessage()


  /**
   * Unmasks the positive button of the [AlertDialog].
   */
  fun AlertDialog.csqUnMaskPositiveButton()


  /**
   * Unmasks the negative button of the [AlertDialog].
   */
  fun AlertDialog.csqUnMaskNegativeButton()
  ```

  Note

  The mask API for dialogs should be called immediately after the `show` method in the main thread.

  Example:

  ```kotlin
  val builder: AlertDialog.Builder = AlertDialog.Builder(context)
  builder.setMessage("This is a dialog. This text should be masked")
  builder.setTitle("Dialog Title")
  builder.setCancelable(true)
  builder.setPositiveButton("Close") { ... }
  builder.setNegativeButton("Idle") { ... }
  val alertDialog: AlertDialog = builder.create()
  alertDialog.show()
  alertDialog.csqUnMaskTitle()
  alertDialog.csqUnMaskPositiveButton()
  alertDialog.csqUnMaskNegativeButton()
  ```

* Jetpack Compose

  The AlertDialog composable in Jetpack Compose can be masked or unmasked using the same APIs applied to other composables. Simply use the appropriate `CsqMask` composables function on the elements you want to mask or unmask within the dialog.

  Example:

  ```kotlin
  AlertDialog(
     icon = {
        Icon(icon, contentDescription = "Example Icon")
     },
     title = {
        Text(text = dialogTitle)
     },
     text = {
        // Mask the dialog text
        CsqMask(true) {
           Text(
              text = dialogText
           )
        }
     },
     onDismissRequest = {
        onDismissRequest()
     },
     confirmButton = {
        TextButton(
           onClick = {
              onConfirmation()
           }
        ) {
           // Unmask the dialog confirm button
           CsqMask(false) {
              Text(
                 "Confirm"
              )
           }
        }
     }
  )
  ```

**TimePickerDialog and DatePickerDialog**

* Views

  Time and date pickers function similarly to AlertDialogs but contain their own distinct UI components. These sections can be selectively masked or unmasked using the APIs provided below.

  To support masking on this dialogs we added the following APIs:

  ```kotlin
  /**
   * Masks the entire [DatePickerDialog].
   */
  fun DatePickerDialog.csqMask()


  /**
   * Masks the header of the DatePicker, which typically displays the selected date.
   * This affects the [DatePickerDialog].
   */
  fun DatePickerDialog.csqMaskHeader()


  /**
   * Masks the calendar view (day picker) of the [DatePickerDialog].
   */
  fun DatePickerDialog.csqMaskCalendar()


  /**
   * Masks the button panel of the [DatePickerDialog], typically containing "OK" and "Cancel" buttons.
   */
  fun DatePickerDialog.csqMaskButtonPanel()


  /**
   * Unmasks the entire [DatePickerDialog].
   */
  fun DatePickerDialog.csqUnMask()


  /**
   * Unmasks the header of the DatePicker.
   * This affects the [DatePickerDialog].
   */
  fun DatePickerDialog.csqUnMaskHeader()


  /**
   * Unmasks the calendar view (day picker) of the DatePicker.
   * This affects the [DatePickerDialog].
   */
  fun DatePickerDialog.csqUnMaskCalendar()


  /**
   * Unmasks the button panel of the [DatePickerDialog].
   */
  fun DatePickerDialog.csqUnMaskButtonPanel()


  /**
   * Masks the entire TimePicker view within the [TimePickerDialog].
   */
  fun TimePickerDialog.csqMask()


  /**
   * Masks the header of the TimePicker, including the time display and input mode header.
   * This affects the [TimePickerDialog].
   */
  fun TimePickerDialog.csqMaskHeader()


  /**
   * Masks the radial picker (clock face) of the TimePicker.
   * This affects the [TimePickerDialog].
   */
  fun TimePickerDialog.csqMaskRadialPicker()


  /**
   * Masks the input mode view of the TimePicker (keyboard input for time).
   * This affects the [TimePickerDialog].
   */
  fun TimePickerDialog.csqMaskInputMode()


  /**
   * Masks the button panel of the [TimePickerDialog], including positive, negative, and toggle mode buttons.
   */
  fun TimePickerDialog.csqMaskButtonPanel()


  /**
   * Unmasks the entire TimePicker view within the [TimePickerDialog].
   */
  fun TimePickerDialog.csqUnMask()


  /**
   * Unmasks the header of the TimePicker, including the time display and input mode header.
   * This affects the [TimePickerDialog].
   */
  fun TimePickerDialog.csqUnMaskHeader()


  /**
   * Unmasks the radial picker (clock face) of the TimePicker.
   * This affects the [TimePickerDialog].
   */
  fun TimePickerDialog.csqUnMaskRadialPicker()


  /**
   * Unmasks the input mode view of the TimePicker (keyboard input for time).
   * This affects the [TimePickerDialog].
   */
  fun TimePickerDialog.csqUnMaskInputMode()


  /**
   * Unmasks the button panel of the [TimePickerDialog], including positive, negative, and toggle mode buttons.
   */
  fun TimePickerDialog.csqUnMaskButtonPanel()
  ```

* Jetpack Compose

  The Time and Picker composables in Jetpack Compose can be masked or unmasked using the same APIs applied to other composables. Simply use the appropriate `CsqMask` composables function on the elements you want to mask or unmask within the dialog.

**Custom Dialogs**

* Views

  If you're creating a custom dialog by extending the Dialog base class and inflating a layout using the setContentView method, you can mask or unmask individual views within your custom view hierarchy, using the masking APIs.

* Jetpack Compose

  The custom dialogs in Jetpack Compose can be masked or unmasked using the same APIs applied to other composables. Simply use the appropriate `CsqMask` composables function on the elements you want to mask or unmask within the dialog.

**Menu Items**

* Views

  Menu items captured by Session Replay can be selectively masked or unmasked.

  For example, consider a context menu, including an action item and another item displaying the user's name (see XML example below). If masking is disabled by default, you may want to mask sensitive menu items to prevent personal data from appearing in Session Replays. Conversely, if masking is enabled by default, you may choose to unmask certain non-sensitive items to keep them visible.

  To achieve this, simply apply masking or unmasking to individual menu items as needed.

  ```xml
  <?xml version="1.0" encoding="utf-8"?>
  <menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
      <item
          android:id="@+id/action_settings"
          android:orderInCategory="10"
          android:title="@string/action_settings"
          app:showAsAction="never" />
      <item
          android:id="@+id/profile_settings"
          android:orderInCategory="100"
          android:title="Name Placeholder"
          app:showAsAction="never" />
  </menu>
  ```

  Note

  This API does not need to be called strictly after menu inflation; it can be invoked earlier if necessary. However, as a best practice, apply it during or after menu inflation when you have clear context about what is being masked and where.

  ```kotlin
  override fun onCreateOptionsMenu(menu: Menu): Boolean {
     // Inflate the menu; this adds items to the action bar if it is present.
     menuInflater.inflate(R.menu.activity_main, menu)
    val item = menu.findItem(R.id.profile_settings)
    item.title = current_user.fullName


    // Unmask action menu item
     CSQ.unmaskMenuItem(R.id.action_settings)
    // Mask profile menu item that contains user's name
     CSQ.maskMenuItem(R.id.profile_settings)
     return true
  }
  ```

* Jetpack Compose

  The Menu components in Jetpack Compose can be masked or unmasked using the same APIs applied to other composables. Use the appropriate `CsqMask` composable function on the elements you want to mask or unmask within the menu.

#### Handling Transparent Views

The SDK processes masked views first (using the [Masking rules](#masking-rules)), ensuring that if a view has transparency and is not masked, it will not expose any personal data since masked views have already been processed.

This approach ensures the protection of sensitive information, even in UIs with transparent views.

Warning

Keep in mind that if the view below the transparent view is a **WebView**, and you unmask the view on top of it (for example a dialog progress bar) it will leak the content of the WebView. This is a known limitation.

On the left, a possible implementation of a payment screen in a WebView. On the right, the result in Session Replay.

![](https://docs.contentsquare.com/_astro/transparent-view-on-webview-combined-no-bg.DXjZ0GVu_1MC2wU.webp)

### Implementation recommendations

#### Where to perform masking operations

Masking operation should be performed before the first draw of the view and it should always be called on the UI thread.

At the **Activity** level, call the masking API methods on the `onCreated` lifecycle method.

At the **Fragment** level, call the masking API methods on the `onViewCreated` lifecycle method.

Note

The masking API stores the view element to be masked or unmasked inside a WeakHashMap, repeating the masking operation multiple times on the same element will override the old value that is already stored in the map.

#### Keeping track of what is masked

The SDK doesn't provide a list of what is currently masked, if you need to keep track, you probably will have to write your specific wrapper.

#### Masking operations performance impact

You can call `unmask(View view)` multiple times without impacting the SDK performance or your app.

### Masking limitations

#### Maps

You can mask or unmask a Map, that will then be captured as an image in the replay. However, you won't be able to mask or unmask specific elements within the Map.

#### Navigate between fragments using animations

Masking rules don't apply on Fragments using animations. So to mask this type of Fragment, mask the container view where the Fragment is inserted, rather than masking its content. This ensures that the Fragment content remains masked during transitions.

Additionally, you can unmask some specific views within the Fragment content, by using `CSQ.unmask(view: View)`.

#### Rotated or Scaled Views

The masking solution relies on the bounding box reported by a view to determine the area that should be masked. In most cases, this approach is sufficient, as the bounding box accurately reflects the view's position and dimensions on the screen.

However, when a view is rotated or scaled, the bounding box no longer represents the view's actual position or size after the transformation.

To prevent any personal data from being leaked, we advise adopting one of the following approaches:

* **Mask the Parent View**: Apply masking to the parent container of the transformed view. This ensures that the entire area encompassing the rotated or scaled view is covered, regardless of transformations.

* **Mask the Entire Window**: In case of complex layouts, transformations, or critical scenarios where absolute certainty is required, mask the entire window to guarantee that no personal data is exposed.

These approaches provide the highest level of confidence in preventing personal data leakage while working within the constraints of the current implementation.

## Event-Triggered Replays (ETR)

Event-Triggered Replays (ETR) is an add-on feature that allows you to selectively collect specific screens or sessions for Session Replay, based on predefined custom events. Once configured, ETR events can be used to filter specific replays, or view these events as they happen in the player event stream.

To enable ETR, contact your CSM or Implementation Manager. For more information, see [Event-Triggered Replays ↗](https://support.contentsquare.com/hc/en-us/articles/4412027865874) in the Help Center.

Note

When ETR is enabled, the SDK collects data for the entire session even if none of the ETR methods described below are called. After collection, only the sessions or screens that match the global replay sampling rate or are associated with ETR events are retained.

As a result, you may notice a network activity during all sessions, regardless of whether ETR has been triggered or not.

Warning

To protect end-user personal data, ensure that appropriate masking configurations are applied to all screens even the ones that are not associated with ETR events.

### Trigger Replay for the Session

Use the following function to trigger Session Replay for the entire current session, with the parameter `name` as "ETR event".

```kotlin
CSQ.triggerReplayForCurrentSession(name: name)
```

### Trigger Replay for the Screen

Use the following function to trigger Session Replay for the entire current screen, with the parameter `name` as "ETR event".

```kotlin
CSQ.triggerReplayForCurrentScreen(name: name)
```

## Advanced features for Experience Monitoring

Note

This feature is part of [Experience Monitoring ↗](https://contentsquare.com/platform/experience-monitoring/), which is only available on Enterprise plans or as an add-on to Pro plans. Contact your Customer Success Manager or [sales ↗](https://contentsquare.com/request-a-demo/) to learn more.

### Send user identifier

CSQ SDK provides the ability to search for session(s) associated with a specific visitor, based on an identifier: email, phone number, customer ID… As these values are typically personal data, from the moment the SDK is collecting the User Identifier, we immediately encode the value using a hashing algorithm so that the information is hidden and can never be accessed.

Use the following code to send a user identifier:

```kotlin
CSQ.sendUserIdentifier("any_identifier")
```

When called, the SDK will log:

```plaintext
CSLIB: User identifier hashed sent {value}
```

#### Sending a user identifier for each session

You may want to send the user identifier for each session. While triggering the user identifier at app launch will cover most cases, it will not be enough. A session ends 30 minutes after the last event. See Session definition section for more information.

That is why we also recommend sending the user identifier every time the app enters foreground.

You can use the [`ProcessLifecycleOwner` Class ↗](https://developer.android.com/reference/androidx/lifecycle/ProcessLifecycleOwner) to detect foreground and trigger a `sendUserIdentifier()`.

#### Limitations

* User identifier max length is 100 characters (if the limit is exceeded, the user identifier will not be handled and you will see an error message in the Console/Logcat)
* Only the first 15 user identifiers per view will be processed on server side
* The SDK will trim and lowercase user identifier
* User identifier event are not visible in Log visualizer

## Integrations

CSQ SDK provides the ability to retrieve the link of the replay to be attached to other vendors such as Voice of Customer or Crash reporting tools.

### Get current replay Link

Generate a link to the replay of the current session. This link can be pushed to any internal or 3rd-party tool as a custom property/variable. For instance, you can add the replay link to:

* Each user request sent to your customer service tool
* Each user voting in your Voice of Customer tool
* Each user session in your App Performance, Observability tool
* ...

Note

The replay link can also be accessed from the in-app features settings. See [Access the Replay](#access-the-replay)

Warning

For the API to return a valid replay link, the session has to be tracked (included in tracked users and not opted-out) and a first screenview event has to be sent before it's called, since Session Replay only starts after the first screenview event.

`CSQ.metadata.sessionReplayUrl` can be used to retrieve the last Session Replay URL if the SDK was successfully initialized and the Session Replay is running. Alternatively you can use the following code to retrieve a Session Replay link each time it changes:

```kotlin
CSQ.metadata.onChanged = OnMetadataChanged { metadata ->
   // Send link to your customer service tool
   val sessionReplayLink = metadata.sessionReplayUrl
}
```

To unregister the current callback, pass `null` as the argument:

```kotlin
CSQ.metadata.onChanged = null
```

To avoid memory leaks, you can use the [`ProcessLifecycleOwner` Class ↗](https://developer.android.com/reference/androidx/lifecycle/ProcessLifecycleOwner) to detect foreground and background events and register or unregister the callback.

When a new link is available, the SDK will also log:

```plaintext
CSLIB: SessionReplay link: https://app.contentsquare.com/quick-playback/index.html?pid={projectId}&uu={userId}&sn={sessionNumber}&recordingType=cs
```

Warning

You can only have one callback at a time. Calling this API twice will override the previous callback.

The replay will be **available within 5 minutes**. Only the "ended screen views" are processed *(we know a screenview is ended when we start receiving data for the next screenview)*. This means that you will be able to replay the session up to the previous screenview if the session is still running.

## How Session Replay works

### Initialization

Sessions can be collected for Session Replay if the Session Replay feature has been enabled for your project and the session matches the collection criteria.

The following conditions will have an impact on which sessions will be collected:

* **User consent**: The users have given their **consent** (if required)
* **Collection rate**: The session is being **drawn for collection** (see Collection rate below)
* **Compatibility**: The **OS version** is supported.
* **App version**: The **app version** is not part of the block list (see App version block list below)

Note that, when Session Replay is turned off, no content specific to Session Replay is collected whatsoever. Also note that the others CSQ tracking remains unaffected by this.

#### Collection rate

Data collection for Session Replay is based on a percentage of the total sessions. By default data collection for Session Replay is disabled.

During the early access phase, the percentage or collected sessions will be set to 1% at the beginning. It will then be adjusted according to:

* The traffic on your app
* The volume of collected sessions set in the contract

#### Compatibility

See [compatibility](../compatibility/).

#### App version block list

CSQ team can add versions of your app in the block list to make sure Session Replay does not start on these versions. This is done when a problem is discovered on a specific version of your app, such as a Personal Data for which the masking was forgotten. This allows to keep Session Replay to work on the other app versions (especially the new ones with the fix).

### Data collection

The SDK monitors the application lifecycle events and the view hierarchy, and generates Session Replay data from the behavior of the app, the content of the screen and the interaction of the user. These events are then locally stored, and eventually sent to our servers in batches. We then aggregate that data to create usable visual information into our Web Application, which you use to gather insights.

Tip

Data collection for Session Replay will start at the 1st screenview event.

### Quality levels

Session Replay has three quality levels available: High, Medium, Low. Quality level is defined by the frame rate and the image quality. The quality level is defined independently for **Wi-Fi** and **Cellular Network**. The default level for both is **Medium**. For more information on network data consumption see: [Performance Impact section](#performance-impact).

Note

If you require a particular setting, reach out to your Contentsquare contact that will make the adjustment in the project configuration.

#### Quality level examples

Select the image to open it in full size and GIF format.

[![](https://docs.contentsquare.com/_astro/sr-quality-android-static.CAnn_sK5_1IaShv.webp)](https://docs.contentsquare.com/_astro/android-quality-levelgrand.Djk7sO10.gif)

### Network and Storage

By default, Session Replay data can be sent over **cellular network**. If the particular context of your app or users requires a very limited impact on cellular data consumption, **sending data over cellular network can be completely disabled**. Once disabled, data will be sent when the device is using Wi-Fi.

Reach out to your Contentsquare contact that will make the adjustment in the project configuration.

#### Storage

Before being sent, data is stored in local files on disk up to 20MB on Android. When the limit is reached, all collected data is dropped, and data collection continues. Then, every time the limit is reached again, the same process is applied.

### Requests

The maximum request size is 1Mbyte.

Requests are sent:

* On screen change
* When [Replay Link API](#get-current-replay-link) is called

## Performance impact

### Performance impact mitigation

Most operations related to Session Replay are performed on dedicated background threads, for a minimal impact on the main thread. If CPU usage is too high for background tasks, some expensive calculations will be discarded. To make sure of this, we run performance tests on testing applications, along with the Android profiler.

If too much time is spent on the main thread, the [quality level](#quality-levels) will decrease automatically: from high to medium to low to a complete stop if required. Once conditions are back to normal, quality level will be changed back to the default value set in the configuration.

We defined our network strategy to have the lesser impact on CPU, memory and data consumption of the users devices. We measure these impacts, using the Android profiler and dedicated logging system, for each release of our SDK.

You can reduce the quality level if you want to favor performance impact over quality.

### Performance test results

We always strive to be non-intrusive, and transparent to the developers of the client app. We apply this rule on the performance as well. These are the technical specifics we can share on performance, if you have any questions feel free to reach out to us.

The performance results were obtained under the following conditions:

| Condition | Value |
| - | - |
| Device model | Pixel 5 |
| Android version | 34 (Android 14) |
| Quality level | High |
| Default Masking State | Disabled |

Note

That the numbers provided below will vary depending on the user's device and Android version.

| Property | Value |
| - | - |
| Total % on main thread (CPU Profiling) | <15 % |
| Memory overhead | 15 MB |
| Session Replay data transmitted over network during 1 minute of use: | |
| - with "High quality" level | <838 KB |
| - with "Medium quality" level | <545 KB |
| - with "Low quality" level | <369 KB |

#### Additional info on performance test procedure

**CPU Usage Test** We are profiling our Demo app with and without the SDK dependency by following the same use case. We are measuring the CPU peak for both versions of the app and we are computing the difference.

**RAM Usage Test** We are profiling our Demo app with and without the SDK dependency by following the same use case. We are measuring the RAM usage peak for both versions of the app and we are computing the difference.

**Memory Leak Test** We are profiling our demo app and we are using the ADB Monkey tool to create random user input events. During this time we are monitoring the RAM behavior for any anomaly. We are using the Memory Profiler to identify leaks.

**Data consumption** We replay a predefined, repeatable and automated user scenario on our demo app and all data sent is measured in kB/min.

**JankStats** If you use the alpha library from Google to monitor the jank frames and you already have some jank frames in your application (main thread overwhelmed), you can expect an increase of 1% or 2% with data collection for Session Replay activated. Data collection for Session Replay requires to perform tasks on the main thread (screen capture and view hierarchy inspection).

## Known limitations

### SurfaceView

[SurfaceView ↗](https://developer.android.com/reference/android/view/SurfaceView) are not supported for now. They will be displayed as white views. This includes views used for video streaming and camera previews, ensuring that no such content is captured.

### Animations are unsupported when masking by instance or type

When views are masked by instance or type, personal data may leak during animations in the collected session.

To prevent this, there are two possible options: masking the entire screen or masking around the animation.

**Mask the entire Screen**

Apply default masking to the entire screen with `setDefaultMasking(true)`, without using any [instance-based](#maskingun-masking-by-instance) or [type-based](#maskingun-masking-by-type) masking/unmasking. After the user navigates to another screen, you can deactivate the default masking with `setDefaultMasking(false)` and use any view masking APIs.

**Mask around the animation**

Mask the entire screen just before the animation, following the [Masking rules](#masking-rules). This involves reversing any [instance-based](#maskingun-masking-by-instance) or [type-based](#maskingun-masking-by-type) masking/unmasking for views and then calling `setDefaultMasking(true)`. After the animation, you can restore the previous masking settings by instance or type.

## Troubleshooting

### Requests are not sent from an Android emulator

If you struggle to watch a replay created on an emulator/simulator, it may be due to some network constraints applied on your computer (VPN, company network restrictions, etc.). Check your configuration or use a real device.

See [Requests are failing](../troubleshooting/#requests-are-failing) for more information about our endpoints and requests.

#### Very long session on an Android emulator

It is important to remember that **an app kill does not end a session**. See [Session definition](../data-collection/#session-definition) for more information. If you leave the simulator/emulator running with the app in foreground, the session will not end, even if you are inactive. To better reflect actual end user behavior and avoid unusually long sessions (last hours), put the app in background or kill it.

#### Unmasking with CS InApp features is broken

When activating the toggle in CS InApp to unmask the elements in the Session Replay, it can sometimes give a strange result with some elements still masked. This is due to the fact that unmasking with InApp features acts differently than when done in the code by the app developer. To ensure a session is fully unmasked as it would be if it was done in the code of the app, follow these steps:

* Toggle the "Unmask ..." switch
* Navigate to a different screen
* Go back to the previous screen
* Put the app in background
* Wait for the session timeout
* Re-open the app

From this point on, you will have a fully unmasked session.
