Session Replay
Prerequisites
Section titled PrerequisitesScreen tracking implemented
Section titled Screen tracking implementedAs session collection will start at the 1st screenview event, it is required to have screen tracking implemented. Make sure to follow the Track screens section.
Updating to latest SDK version
Section titled Updating to latest SDK versionIn order to enable Session Replay in your app and get the most stable version, it is required to upgrade the SDK to its latest version.
Reminder about User consent
Section titled Reminder about User consentIf you are in the process of implementing the SDK for the 1st time (or choose to take this update as an opportunity to review your Privacy related implementation), make sure to follow the Privacy section.
WebView
Section titled WebViewWebView events can be collected as part Session Replay under the following conditions:
- The WebView is injected
- The web page implements the Tag in WebView mode
For the full guide to implementation of WebView Tracking, see 📚 Mobile Apps Webview Tracking.
WebView personal data masking is entirely handled on the web side — see 📚 Personal Data handling in WebView.
Testing and Debugging
Section titled Testing and DebuggingEnable Session Replay on your device
Section titled Enable Session Replay on your deviceSince not all sessions are collected (depending on the percentage set in the Contentsquare back office), we have implemented an option to force replay collection on your device for testing and debugging purpose. This option can be enabled from the in-app features settings:
- Enable in-app features
- Open in-app features settings with a long press on the snapshot button
- Under “Session Replay”, toggle “Enable Session Replay” on
- Kill the app
- Start app, a new session is starting 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 check 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 sessionGet Replay link
which means Session Replay is running for the current session
Access the Replay
Section titled Access the ReplayThe 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 “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.
Provide custom fonts
Section titled Provide custom fontsSince texts are collected as data for UIKit elements, 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.
Upload the fonts in otf
, ttf
, woff
or woff2
format directly from the player as described in Managing iOS Fonts In The Player ↗ on the Help Center.
Personal Data Masking
Section titled Personal Data MaskingThe 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 and un-masking APIs.
Masking mechanisms
Section titled Masking mechanismsMasking depends on the type of element:
UIKit.UIImage
andSwiftUI.Image
: image isn’t collected, a placeholder is sent instead of the content; the “IMG” placeholder will be displayed in the frame of the element.UIKit.UILabel
andUIKit.UITextView
: text is replaced by “la” repeated as many times as needed to equal the original character count. White characters are preserved. For instancethe lazy fox
is collected aslal lala ala
. All other visual properties are collected (text color, background color, alignment, etc.).UIKit.UITextField
: same asUIKit.UILabel
orUIKit.UITextView
unlessisSecureTextEntry
is set totrue
. In this case all characters, including whitespaces are replaced with ”•“.SwiftUI.Text
: element is replaced by a black rectangle.- For all other types: no specific data is collected but all visual properties are collected: size, background color, corner radius, etc.
If you think a specific element can reveal personal data from one of these properties you have to mask it using one of the method presented below. A good way to check how a view is rendered in the Session Replay is to navigate to the desired view with the CS SDK running then use the quick replay link.
Original VS Replay fully masked
Section titled Original VS Replay fully maskedDefault masking
Section titled Default maskingAll iOS views and subclasses of UIView
are fully masked by default.
The SDK provides an API to change the default masking state:
Masking/Un-masking by instances
Section titled Masking/Un-masking by instancesUse mask(view:)
and unmask(view:)
methods to deal with a specific instance. Masking is applied recursively to all subviews unless specified otherwise (cf. Masking and Unmasking behaviors on a parent view).
Following the previous example on un-masking UIButtons: you are in default masking, the UIButton
are unmasked but a screen shows a button, myButton
, that contains sensitive information. You will be able to mask it with mask(view: myButton)
.
You can mask or unmask a SwiftUI View
instance by using the following view modifier. Masking is applied recursively to all subviews unless specified otherwise (cf. Masking and Unmasking behaviors on a parent view).
Masking/Un-masking by type
Section titled Masking/Un-masking by typeThe Contentsquare 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.
Use mask(viewsOfType:)
and unmask(viewsOfType:)
methods if you want to handle a specific type (wether being a system element or a custom element).
Example: un-masking UIButtons
If you are in default masking but you don’t want any button to be masked, you will only need to call unmask(viewsOfType: UIButton.self):
to specify that all instances of UIButton and its subclasses should not be masked.
Masking/unmasking by type can be achieved using 1 or more of the 3 following methods.
Masking rules priority
Section titled Masking rules priorityThe SDK determines if a view is masked or not by applying the above rules in the following order. Once a rule is triggered, the state is set (subsequent rules won’t be applied):
Priority | Rule |
---|---|
1 | Instance is specifically masked or unmasked |
2 | A parent is specifically masked or unmasked |
3 | Type is masked or unmasked |
4 | A parent type is masked or unmasked |
5 | Otherwise the default masking state is applied |
Masking and Unmasking behaviors on a parent view
Section titled Masking and Unmasking behaviors on a parent viewUIKit
Section titled UIKitMasking is applied recursively to all children unless a specific rule has been applied to 1 of them. In this case the rule will again be applied recursively.
Examples
Let’s say we have the following structure:
- If no rule is applied: everything is masked including “Parent Label” and “Child Label”
- If parent view is unmasked and no other rule is applied: both “Parent Label” and “Child Label” are unmasked
- If parent view is unmasked and child view is masked: “Parent Label” is unmasked and “Child Label” is masked
- If default masking is set to false and no other rule is applied: “Parent Label” and “Child Label” are unmasked
- If default masking is set to false and parent view is masked: both “Parent Label” and “Child Label” are masked
And so on as explained in the Masking rules priority.
Unmask an instance when its type is masked
If you’ve masked UILabel
types specifically or are in default masking, and you call unmask(view: myLabel)
, all UILabel
instances will be masked except myLabel
.
Mask an instance when its type is unmasked
If you’ve unmasked UILabel
type specifically or are using the default masking, and you call mask(view: myLabel)
, no UILabel
will be masked except myLabel
.
Implementation recommendations
Section titled Implementation recommendationsMasking operations should always be performed before the target view is added to the window to avoid any Personal Data leak. For instance, you can set up masking in the didFinishLaunching
callback of your app or if you need to change masking behavior while your app is launched, it can be done in loadView
, viewDidLoad
or viewWillAppear
if you use a ViewController
.
Masking operations should be done as early as possible, in the setup of your app for masking by types for instance.
For masking by instance, you can do it in the body of your View
by calling the modifier csMasking
.
Keeping track of what is masked
Section titled Keeping track of what is maskedThe 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
Section titled Masking operations performance impactRepeating the same operation has little to no impact. For example, you can call unmask(viewsOfType: UIButton.self)
multiple times without impacting the SDK or your app performance.
Advanced features for Find & Fix
Section titled Advanced features for Find & FixSend user identifier
Section titled Send user identifierContentsquare 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:
When called, the SDK will log:
Sending a user identifier for each session
Section titled Sending a user identifier for each sessionYou 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 can start when the app is put in foreground, after staying in background for more than 30 minutes. 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 didBecomeActiveNotification
notification ↗ and sceneDidBecomeActive(_:)
callback ↗ to detect foreground and trigger a sendUserIdentifier()
.
Limitations
Section titled 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
Section titled IntegrationsContentsquare 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
Section titled Get current replay LinkGenerate 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
- …
Use the following code to retrieve the current replay link:
When called, the SDK will log:
If you have a Contentsquare account, you can use this link to directly watch your current Session Replay on the Contentsquare Platform.
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.
Retrieve current Session Replay Link for each session
Section titled Retrieve current Session Replay Link for each sessionYou may want to get the current Session Replay link for each session. While triggering the current Session Replay link at app launch will cover most cases, it will not be enough. A session can start when the app is put in foreground, after staying in background for more than 30 minutes. See Session definition section for more information.
That is why we also recommend sending the current Session Replay link every time the app enters foreground.
You can use didBecomeActiveNotification
notification ↗ and sceneDidBecomeActive(_:)
callback ↗ to detect foreground and trigger a ‘currentSessionReplayLink’ to be sure you always get the current session link.
How Session Replay works
Section titled How Session Replay worksInitialization
Section titled InitializationSessions 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 standard Contentsquare analytics tracking remain unaffected by this.
Collection rate
Section titled Collection rateSession 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 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
Section titled CompatibilitySee Compatibility.
App version block list
Section titled App version block listContentsquare 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 working on the other app versions (especially the new ones with the fix).
Data Collection
Section titled Data CollectionThe 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.
Quality levels
Section titled Quality levelsSession 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.
Quality level examples
Section titled Quality level examplesSelect the image to open the it in full size and GIF format.
Network and Storage
Section titled Network and StorageBy 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
Section titled StorageBefore 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 stopped. It will restart at the next app launch once the SDK is able to send the data.
Requests
Section titled RequestsThe maximum request size is 1Mbyte.
Requests are sent:
- Every 10 seconds
- At app hide
- When Replay Link API is called
Performance impact
Section titled Performance impactWe 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.
Performance impact mitigation
Section titled Performance impact mitigationMost operations related to Session Collection are performed on background threads, for the impact on the main thread to be minimal. To make sure of this, we run performance tests on testing applications, using XCTMetric and Hitch Time Ratio measures.
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.
You can reduce the quality level if you want to favor performance impact over quality.
Performance test results
Section titled Performance test resultsThe following performance results were obtained under the following conditions:
Condition | Value |
---|---|
Device model | iPhone 7 |
iOS version | 15.7.8 |
Test App built using Xcode version | 14.3.1 |
Test App built with Swift version | 5.8 |
Quality level | High |
Default Masking State | Disabled |
We conducted the tests using a default Master-Detail
app built using AdHoc distribution with no app thinning and with Swift symbols stripped. In the app, the SDK was making calls to the Public APIs, running and collection data in its default state.
Property | Value |
---|---|
Max CPU overhead | <10% |
Max RAM usage | <5MB |
Session Replay data transmitted over network during 1 minute of use | |
- with “High quality” level | <2Mbytes |
- with “Medium quality” level | <1Mbytes |
- with “Low quality” level | <900Kbytes |
Known limitations
Section titled Known limitationsMaps can potentially contain Personal Data and are therefore replaced by a placeholder with no ability to un-mask them currently.
NSAttributedString handling
Section titled NSAttributedString handlingThe exported HTML doesn’t support the expansion
, obliqueness
, paragraphStyle
and textEffect
attributes.
Multiple windows
Section titled Multiple windowsMulti-window is not supported.
System elements
Section titled System elementsSystem elements like UIImagePickerController
, SKStoreReviewController
are not collected.
Troubleshooting
Section titled TroubleshootingRequests are not sent from an iOS simulator
Section titled Requests are not sent from an iOS simulatorIf 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 or use a real device.
See Requests are failing for more information about our endpoints and requests.
Very long session on an iOS simulator
Section titled Very long session on an iOS simulatorIf you leave the simulator/emulator running with the app in foreground, the session will not end, even if you are inactive. This can lead to artificially long sessions (several hours). To better emulate actual user behavior, put the app in background or kill it.
Unmasking with CS InApp features is broken
Section titled Unmasking with CS InApp features is brokenWhen activating the toggle in CS InApp to unmask elements in Session Replay,it can sometimes give a strange result with some elements still masked, it’s especially true for SwiftUI. This is due to the fact that unmasking with InApp features acts differently that 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.
Uncaught exception due to NSAttributedString
Section titled Uncaught exception due to NSAttributedStringIf the following exception is encountered: NSInvalidArgumentException: -[__NSCFType CGColor]: unrecognized selector sent to instance
,
It is probably due to an Objective-C bridging issue in Foundation
, which makes it possible to pass CGColor
instances as values for the foregroundColor
and strikeThroughColor
attributes of a NSAttributedString
.
While a UILabel
with such an attributed string will be displayed correctly, its textColor
attribute will have the wrong type, and the SDK won’t be able to handle it, causing a crash in Session Replay.
Because of this, it is important to ensure that actual UIColor
instances are passed as values, as per the official documentation ↗.