Session Replay

As session collection initiate upon the 1st screenview event, it’s essential to have screen tracking implemented. Make sure to follow the Flutter Track screens section.

Updating to latest SDK version

Section titled Updating to latest SDK version

To enable Session Replay in your application and ensure optimal stability, make sure to always use the last SDK version.

If you are in the process of implementing the SDK for the 1st time, or if you choose to take this update as an opportunity to review your privacy-related implementation, ensure that you follow the Flutter Privacy section and use the Opt-in API to get the user consent, otherwise no data will be collected.

WebView events can be collected as part of the Session Replay under the following conditions:

  • The web page implements the Web Tracking Tag.
  • Your webview is wrapped with the ContentsquareWebViewTrackerBuilder widget
  • Your webview allows JavaScript execution
  • The ContentsquareWebViewTracker methods are registered in the webview

For the full guide to implementation of Webview Tracking, see 📚 Mobile Apps Webview Tracking.

Enable Session Replay on your device

Section titled Enable Session Replay on your device

Since not all sessions are collected (depending on the percentage set in Contentsquare back-office), we have implemented an option to force replay collection on your device for testing and debugging purposes. This option can be enabled from the in-app features settings:

  1. Enable in-app features
  2. Open in-app features settings with a long press on the snapshot button
  3. In the “Session Replay settings” section, toggle the switch to enable Session Replay
  4. Kill the app
  5. Upon restarting the app, a new session will start with Session Replay enabled.

How do I know if Session Replay is enabled?

Section titled How do I know if Session Replay is enabled?

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

In the logs: The log Session Recording is starting will confirm that Session Replay is enabled.

In in-app features settings: Below the “Enable “will start at next app start” (see below 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

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

The replay will be available within 5 minutes. Only the concluded screen views are processed, and we identify a screenview has concluded when we begin receiving data for the subsequent screenview or if the session has ended. This means that you will be able to replay your session up to the previous screenview if the session is still ongoing.

Section titled Get the replay link programmatically

The onSessionReplayLinkChange method is designed to provide a mechanism for tracking changes to the Session Replay URL in real-time.

void onSessionReplayLinkChange(void Function(Uri url)? onLinkChanged);

onLinkChanged (void Function(Uri url)?): A callback function that receives the updated Session Replay URL. This parameter is nullable.

  • Real-Time Updates: The callback is triggered every time there is a change in the Session Replay URL. It is also triggered when the callback is first registered, providing the current Session Replay URL.

  • Single Callback Registration: Only one callback can be active at a time. If the API is called multiple times, the most recent callback registration overrides the previous one.

  • Optional Callback: The onLinkChanged argument is nullable, allowing the API to be used to stop listening to URL changes.

To start listening to changes:

Contentsquare().onSessionReplayLinkChange((Uri url) {
/// Handle the updated Session Replay URL
print('Session Replay URL: $url');
});

To stop listening to changes:

Contentsquare().onSessionReplayLinkChange(null);

Since texts are collected as data from your widgets, it is required to get the custom fonts you use in your app so the player can properly render the texts as displayed in your app.

Provide the fonts requested by your Implementation Manager in one of the following formats: otf, ttf, woff or woff2.

The Session Replay feature captures every user interaction with your app. In order to respect user privacy right, the Contentsquare SDK :

  • Masks potentially user sensitive data by default (See: Default masking)
  • Provides APIs for controlling which parts of the user interface are collected through masking and unmasking functionalities.

Masking depends on the type of element:

  • Images: Not collected, instead a placeholder is sent in place of the content, and the “IMG” placeholder is displayed within the element’s frame.
  • Text: Replaced by “la” repeated as many times as needed to equal the original character count. White characters are preserved. For instance the lazy fox is collected as lal lala ala. All other visual properties are collected (text color, background color, alignment, etc.).
  • TextFields: Same as Text
  • Other types: no specific data is collected but visual properties are collected.

If you believe a particular element might disclose personal data through any of these properties, you must mask it using one of the methods outlined below. A reliable method to assess how a view is rendered in the Session Replay is to navigate to the desired view with the CS SDK active, then use the quick replay link.

Here’s an illustration of a masked view:

Original -> Replay fully unmasked -> Replay fully masked

By default all images, text and TextFields will be masked.

SessionReplayMaskingScope is a Flutter widget that applies specified masking rules to its descendant widgets. This can be useful when you want to mask sensitive information during Session Replays, such as user data or private texts.

MaskingConfig is a class that determines what type of content should be masked during Session Replays. You can customize the masking behavior by setting the masking options for texts, text fields, and images.

How to use SessionReplayMaskingScope

Section titled How to use SessionReplayMaskingScope

To use SessionReplayMaskingScope, wrap the desired widget(s) with the SessionReplayMaskingScope widget and provide a MaskingConfig object to configure the masking rules.

Here’s a simple example of how to use SessionReplayMaskingScope:

SessionReplayMaskingScope(
maskingConfig: MaskingConfig.maskAll(),
child: const Text('Hello world'),
);

In this example, MaskingConfig.maskAll() is used to mask the Text widget.

You can also use nested SessionReplayMaskingScope widgets to apply different masking rules to different parts of your widget tree. Here’s an example:

SessionReplayMaskingScope(
maskingConfig: MaskingConfig.maskAll(),
child: Column(
children: [
const Text('This text will be masked'),
SessionReplayMaskingScope(
maskingConfig: MaskingConfig(maskTexts: false),
child: Text('This text will NOT be masked'),
),
],
),
);

In this example, the first Text widget will be masked, while the second one will not be masked due to the maskTexts: false option in the nested SessionReplayMaskingScope.

MaskingConfig allows you to configure the masking behavior for different types of content:

  • maskTexts: If set to true, any widget that creates a RenderParagraph (e.g., Text, RichText) will be masked.
  • maskTextFields: If set to true, any widget that creates a RenderEditable (e.g., TextFormField, TextField) will be masked.
  • maskImages: If set to true, any widget that creates a an image (e.g., Image, BoxDecoration.image) will be masked.

You can create a custom MaskingConfig object by using the constructor:

const MaskingConfig({
bool maskTexts,
bool maskImages,
bool maskTextFields,
})

For easier readability and convenience, you can use the following factory constructors:

  • MaskingConfig.maskAll(): Returns a MaskingConfig object with all values set to true, meaning everything will be masked.
  • MaskingConfig.unMaskAll(): Returns a MaskingConfig object with all values set to false, meaning nothing will be masked.

If any masking parameter is omitted in the MaskingConfig object, its value is inherited from the nearest SessionMaskingScope widget in the widget tree.

When using SessionReplayMaskingScope and MaskingConfig, it’s essential to understand the priority of masking rules in the widget tree. The masking rules specified in a SessionReplayMaskingScope widget will affect its descendants, and if nested scopes are used, the innermost (closest) scope takes precedence.

The priority of masking rules is as follows:

  1. The MaskingConfig object provided to a SessionReplayMaskingScope widget sets the masking rules for its child and descendants.
  2. The MaskingConfig object provided to the ContentsquareRoot widget sets the default masking rules for the entire app.

Contentsquare 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 Identifiable Information, 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:

Contentsquare().sendUserIdentifier('the_neo@one.com');

When called, the native SDK will log:

CSLIB ℹ️ Info: User identifier hashed sent {value}

Sending a user identifier for each session

Section titled Sending a user identifier for each session

You may consider sending the user identifier for each session. Although triggering the user identifier at app launch will cover most cases, it may not be adequate in all scenarios. For instance, a session might start when the app is brought to the foreground after being in the background for more than 30 minutes. For further details, refer to the Session definition section. That is why we also recommend sending the user identifier every time the app enters foreground.

  • 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 native 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 LogVisualizer

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 provided their consent (if required)
  • Collection rate: The session is designated for collection (see Collection Rate)
  • Compatibility: The OS version is supported.
  • App version: The app version is not included in the block list (see App Version Block List)

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

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

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

Refer to Compatibility.

The Contentsquare team can include versions of your app in the block list to prevent Session Replay from initiating on these versions. This functionality proves valuable when a problem is identified on a particular version of your app, such as a situation where masking for Personal Data was not implemented. By employing this feature, Session Replay can continue to operate on other app versions, particularly those with necessary fixes implemented.

The SDK monitors the application lifecycle events and the view hierarchy, capturing Session Replay data based on app behavior, screen content, and user interaction. These events are initially stored locally and later sent to our servers in batches. Subsequently, we aggregate this data to generate actionable visual insights in our Web Application, facilitating your ability to gather insights.

By default, Session Replay data can be sent over cellular network. However, if your app’s context or user preferences necessitate minimal impact on cellular data usage, you can opt to disable data transmission over cellular networks entirely. Once disabled, data will only be sent when the device is connected to Wi-Fi.

To make this adjustment in your project’s configuration, contact your Contentsquare representative.

Before being sent, data is stored in local files on disk up to 30MB on iOS and 20MB on Android. If the limit is reached, Session Replay is halted. It will resume at the next app launch once the SDK can transmit the data.

The maximum request size is 1 MByte.

Requests are sent:

Requests are sent:

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.

Most operations related to Session Collection are performed on background threads, for the impact on the main thread to be minimal.

We also set up mechanisms that stop the Session Collection if we detect the feature is using too much memory.

We defined our network strategy to have the lesser impact on CPU and battery of the users devices. We measure these impacts, using Apple Dev Tools, for each release of our SDK.

Session Collection will also stop if we use up to 30Mbytes of local storage.

We have conducted performance tests on the SDK to ensure that it has a minimal impact on the app’s performance. The tests were conducted on a variety of devices and operating systems to ensure that the SDK performs well across different platforms. These tests use the Flutter Gallery app as a reference and simulate an intensive normal user interaction with the app during 60 seconds (scrolling, tapping, etc.) using the Session Replay feature.

For iPhone 14 with iOS 16.3

MetricValue
CPU Usage Delta< 3%
Memory Usage Delta< 2%
Network Usage< 80kb

For Pixel 6 with Android 13

MetricValue
CPU Usage Delta< 25%
Memory Usage Delta< 20%
Network Usage< 80kb
  • Different quality levels are not available for the moment.
  • Images set by Ink.image right now are not supported.
  • Gradient colors are collected as a single color (The Average color of the gradient).
  • Custom shapes may lead to altered rendering in the player.
  • If a multiple lines paragraph begins or ends in the middle of a line, it can cause text to overlap.

Requests are not sent from an Android emulator / iOS simulator

Section titled Requests are not sent from an Android emulator / iOS simulator

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

See following sections for more information about our endpoints and requests:

Very long session on an Android emulator / iOS simulator

Section titled Very long session on an Android emulator / iOS simulator

It is important to remember that an app kill does not end a session. See 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.