diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt index f46c34527..354a03a24 100644 --- a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt @@ -28,22 +28,25 @@ class DdSessionReplayImplementation( /** * Enable session replay and start recording session. * @param replaySampleRate The sample rate applied for session replay. - * @param defaultPrivacyLevel The privacy level used for replay. + * @param customEndpoint Custom server url for sending replay data. + * @param privacySettings Defines the way visual elements should be masked. * @param customEndpoint Custom server url for sending replay data. * @param startRecordingImmediately Whether the recording should start immediately when the feature is enabled. */ fun enable( replaySampleRate: Double, - defaultPrivacyLevel: String, customEndpoint: String, + privacySettings: SessionReplayPrivacySettings, startRecordingImmediately: Boolean, promise: Promise ) { val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore val logger = sdkCore.internalLogger val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat()) - .configurePrivacy(defaultPrivacyLevel) .startRecordingImmediately(startRecordingImmediately) + .setImagePrivacy(privacySettings.imagePrivacyLevel) + .setTouchPrivacy(privacySettings.touchPrivacyLevel) + .setTextAndInputPrivacy(privacySettings.textAndInputPrivacyLevel) .addExtensionSupport(ReactNativeSessionReplayExtensionSupport(reactContext, logger)) if (customEndpoint != "") { @@ -74,32 +77,6 @@ class DdSessionReplayImplementation( promise.resolve(null) } - @Deprecated("Privacy should be set with separate properties mapped to " + - "`setImagePrivacy`, `setTouchPrivacy`, `setTextAndInputPrivacy`, but they are" + - " currently unavailable.") - private fun SessionReplayConfiguration.Builder.configurePrivacy( - defaultPrivacyLevel: String - ): SessionReplayConfiguration.Builder { - when (defaultPrivacyLevel.lowercase(Locale.US)) { - "mask" -> { - this.setTextAndInputPrivacy(TextAndInputPrivacy.MASK_ALL) - this.setImagePrivacy(ImagePrivacy.MASK_ALL) - this.setTouchPrivacy(TouchPrivacy.HIDE) - } - "mask_user_input" -> { - this.setTextAndInputPrivacy(TextAndInputPrivacy.MASK_ALL_INPUTS) - this.setImagePrivacy(ImagePrivacy.MASK_NONE) - this.setTouchPrivacy(TouchPrivacy.HIDE) - } - "allow" -> { - this.setTextAndInputPrivacy(TextAndInputPrivacy.MASK_SENSITIVE_INPUTS) - this.setImagePrivacy(ImagePrivacy.MASK_NONE) - this.setTouchPrivacy(TouchPrivacy.SHOW) - } - } - return this - } - companion object { internal const val NAME = "DdSessionReplay" } diff --git a/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplayPrivacySettings.kt b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplayPrivacySettings.kt new file mode 100644 index 000000000..5af4819fe --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplayPrivacySettings.kt @@ -0,0 +1,90 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.reactnative.sessionreplay + +import android.util.Log +import com.datadog.android.sessionreplay.ImagePrivacy +import com.datadog.android.sessionreplay.TextAndInputPrivacy +import com.datadog.android.sessionreplay.TouchPrivacy + +/** + * A utility class to store Session Replay privacy settings, and convert them from string to + * enum values. + * + * @param imagePrivacyLevel Defines the way images should be masked. + * @param touchPrivacyLevel Defines the way user touches should be masked. + * @param textAndInputPrivacyLevel Defines the way text and input should be masked. + */ +class SessionReplayPrivacySettings( + imagePrivacyLevel: String, + touchPrivacyLevel: String, + textAndInputPrivacyLevel: String +){ + /** + * Defines the way images should be masked. + */ + val imagePrivacyLevel = getImagePrivacy(imagePrivacyLevel) + + /** + * Defines the way user touches should be masked. + */ + val touchPrivacyLevel = getTouchPrivacy(touchPrivacyLevel) + + /** + * Defines the way text and input should be masked. + */ + val textAndInputPrivacyLevel = getTextAndInputPrivacy(textAndInputPrivacyLevel) + + companion object { + internal fun getImagePrivacy(imagePrivacyLevel: String): ImagePrivacy { + return when (imagePrivacyLevel) { + "MASK_NON_BUNDLED_ONLY" -> ImagePrivacy.MASK_LARGE_ONLY + "MASK_ALL" -> ImagePrivacy.MASK_ALL + "MASK_NONE" -> ImagePrivacy.MASK_NONE + else -> { + Log.w( + SessionReplayPrivacySettings::class.java.canonicalName, + "Unknown Session Replay Image Privacy Level given: $imagePrivacyLevel, " + + "using ${ImagePrivacy.MASK_ALL} as default" + ) + ImagePrivacy.MASK_ALL + } + } + } + + internal fun getTouchPrivacy(touchPrivacyLevel: String): TouchPrivacy { + return when (touchPrivacyLevel) { + "SHOW" -> TouchPrivacy.SHOW + "HIDE" -> TouchPrivacy.HIDE + else -> { + Log.w( + SessionReplayPrivacySettings::class.java.canonicalName, + "Unknown Session Replay Touch Privacy Level given: $touchPrivacyLevel, " + + "using ${TouchPrivacy.HIDE} as default" + ) + TouchPrivacy.HIDE + } + } + } + + internal fun getTextAndInputPrivacy(textAndInputPrivacyLevel: String): TextAndInputPrivacy { + return when (textAndInputPrivacyLevel) { + "MASK_SENSITIVE_INPUTS" -> TextAndInputPrivacy.MASK_SENSITIVE_INPUTS + "MASK_ALL_INPUTS" -> TextAndInputPrivacy.MASK_ALL_INPUTS + "MASK_ALL" -> TextAndInputPrivacy.MASK_ALL + else -> { + Log.w( + SessionReplayPrivacySettings::class.java.canonicalName, + "Unknown Session Replay Text And Input Privacy Level given: $textAndInputPrivacyLevel, " + + "using ${TextAndInputPrivacy.MASK_ALL} as default" + ) + TextAndInputPrivacy.MASK_ALL + } + } + } + } +} diff --git a/packages/react-native-session-replay/android/src/newarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt b/packages/react-native-session-replay/android/src/newarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt index f9f72aa62..a3bf919ee 100644 --- a/packages/react-native-session-replay/android/src/newarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt +++ b/packages/react-native-session-replay/android/src/newarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt @@ -26,14 +26,28 @@ class DdSessionReplay( * @param replaySampleRate The sample rate applied for session replay. * @param defaultPrivacyLevel The privacy level used for replay. * @param customEndpoint Custom server url for sending replay data. + * @param imagePrivacyLevel Defines the way images should be masked. + * @param touchPrivacyLevel Defines the way user touches should be masked. + * @param textAndInputPrivacyLevel Defines the way text and input should be masked. */ @ReactMethod override fun enable( replaySampleRate: Double, - defaultPrivacyLevel: String, customEndpoint: String, + imagePrivacyLevel: String, + touchPrivacyLevel: String, + textAndInputPrivacyLevel: String, promise: Promise ) { - implementation.enable(replaySampleRate, defaultPrivacyLevel, customEndpoint, promise) + implementation.enable( + replaySampleRate, + customEndpoint, + SessionReplayPrivacySettings( + imagePrivacyLevel = imagePrivacyLevel, + touchPrivacyLevel = touchPrivacyLevel, + textAndInputPrivacyLevel = textAndInputPrivacyLevel + ), + promise + ) } } diff --git a/packages/react-native-session-replay/android/src/oldarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt b/packages/react-native-session-replay/android/src/oldarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt index 0cfdbc138..e8269a6e2 100644 --- a/packages/react-native-session-replay/android/src/oldarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt +++ b/packages/react-native-session-replay/android/src/oldarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt @@ -25,22 +25,30 @@ class DdSessionReplay( /** * Enable session replay and start recording session. * @param replaySampleRate The sample rate applied for session replay. - * @param defaultPrivacyLevel The privacy level used for replay. * @param customEndpoint Custom server url for sending replay data. + * @param imagePrivacyLevel Defines the way images should be masked. + * @param touchPrivacyLevel Defines the way user touches should be masked. + * @param textAndInputPrivacyLevel Defines the way text and input should be masked. * @param startRecordingImmediately Whether the recording should start immediately when the feature is enabled. */ @ReactMethod fun enable( replaySampleRate: Double, - defaultPrivacyLevel: String, customEndpoint: String, + imagePrivacyLevel: String, + touchPrivacyLevel: String, + textAndInputPrivacyLevel: String, startRecordingImmediately: Boolean, promise: Promise ) { implementation.enable( replaySampleRate, - defaultPrivacyLevel, customEndpoint, + SessionReplayPrivacySettings( + imagePrivacyLevel = imagePrivacyLevel, + touchPrivacyLevel = touchPrivacyLevel, + textAndInputPrivacyLevel = textAndInputPrivacyLevel + ), startRecordingImmediately, promise ) diff --git a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementationTest.kt b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementationTest.kt index 1be9191dc..3d6a2c6d5 100644 --- a/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementationTest.kt +++ b/packages/react-native-session-replay/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementationTest.kt @@ -20,7 +20,6 @@ import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.DoubleForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeExtension -import java.util.Locale import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -57,6 +56,23 @@ internal class DdSessionReplayImplementationTest { @Mock lateinit var mockUiManagerModule: UIManagerModule + private val imagePrivacyMap = mapOf( + "MASK_ALL" to ImagePrivacy.MASK_ALL, + "MASK_NON_BUNDLED_ONLY" to ImagePrivacy.MASK_LARGE_ONLY, + "MASK_NONE" to ImagePrivacy.MASK_NONE + ) + + private val touchPrivacyMap = mapOf( + "SHOW" to TouchPrivacy.SHOW, + "HIDE" to TouchPrivacy.HIDE + ) + + private val inputPrivacyMap = mapOf( + "MASK_ALL" to TextAndInputPrivacy.MASK_ALL, + "MASK_ALL_INPUTS" to TextAndInputPrivacy.MASK_ALL_INPUTS, + "MASK_SENSITIVE_INPUTS" to TextAndInputPrivacy.MASK_SENSITIVE_INPUTS + ) + @BeforeEach fun `set up`() { whenever(mockReactContext.getNativeModule(any>())) @@ -71,51 +87,31 @@ internal class DdSessionReplayImplementationTest { } @Test - fun `M enable session replay W privacy = ALLOW`( + fun `M enable session replay W random privacy settings`( @DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double, @StringForgery(regex = ".+") customEndpoint: String, @BoolForgery startRecordingImmediately: Boolean ) { - testSessionReplayEnable( - "ALLOW", - replaySampleRate, - customEndpoint, - startRecordingImmediately - ) - } + val imagePrivacy = imagePrivacyMap.keys.random() + val touchPrivacy = touchPrivacyMap.keys.random() + val textAndInputPrivacy = inputPrivacyMap.keys.random() - @Test - fun `M enable session replay W privacy = MASK`( - @DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double, - @StringForgery(regex = ".+") customEndpoint: String, - @BoolForgery startRecordingImmediately: Boolean - ) { testSessionReplayEnable( - "MASK", - replaySampleRate, - customEndpoint, - startRecordingImmediately - ) - } - - @Test - fun `M enable session replay W privacy = MASK_USER_INPUT`( - @DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double, - @StringForgery(regex = ".+") customEndpoint: String, - @BoolForgery startRecordingImmediately: Boolean - ) { - testSessionReplayEnable( - "MASK_USER_INPUT", - replaySampleRate, - customEndpoint, - startRecordingImmediately + replaySampleRate = replaySampleRate, + customEndpoint = customEndpoint, + imagePrivacy = imagePrivacy, + touchPrivacy = touchPrivacy, + textAndInputPrivacy = textAndInputPrivacy, + startRecordingImmediately = startRecordingImmediately ) } private fun testSessionReplayEnable( - privacy: String, replaySampleRate: Double, customEndpoint: String, + imagePrivacy: String, + touchPrivacy: String, + textAndInputPrivacy: String, startRecordingImmediately: Boolean ) { // Given @@ -124,8 +120,8 @@ internal class DdSessionReplayImplementationTest { // When testedSessionReplay.enable( replaySampleRate, - privacy, customEndpoint, + SessionReplayPrivacySettings(imagePrivacy, touchPrivacy, textAndInputPrivacy), startRecordingImmediately, mockPromise ) @@ -135,47 +131,31 @@ internal class DdSessionReplayImplementationTest { assertThat(sessionReplayConfigCaptor.firstValue) .hasFieldEqualTo("sampleRate", replaySampleRate.toFloat()) .hasFieldEqualTo("customEndpointUrl", customEndpoint) - - when (privacy.lowercase(Locale.US)) { - "mask_user_input" -> { - assertThat(sessionReplayConfigCaptor.firstValue) - .hasFieldEqualTo("textAndInputPrivacy", TextAndInputPrivacy.MASK_ALL_INPUTS) - .hasFieldEqualTo("imagePrivacy", ImagePrivacy.MASK_NONE) - .hasFieldEqualTo("touchPrivacy", TouchPrivacy.HIDE) - } - "allow" -> { - assertThat(sessionReplayConfigCaptor.firstValue) - .hasFieldEqualTo( - "textAndInputPrivacy", - TextAndInputPrivacy.MASK_SENSITIVE_INPUTS - ) - .hasFieldEqualTo("imagePrivacy", ImagePrivacy.MASK_NONE) - .hasFieldEqualTo("touchPrivacy", TouchPrivacy.SHOW) - } - else -> { - assertThat(sessionReplayConfigCaptor.firstValue) - .hasFieldEqualTo("textAndInputPrivacy", TextAndInputPrivacy.MASK_ALL) - .hasFieldEqualTo("imagePrivacy", ImagePrivacy.MASK_ALL) - .hasFieldEqualTo("touchPrivacy", TouchPrivacy.HIDE) - } - } + .hasFieldEqualTo("textAndInputPrivacy", inputPrivacyMap[textAndInputPrivacy]) + .hasFieldEqualTo("imagePrivacy", imagePrivacyMap[imagePrivacy]) + .hasFieldEqualTo("touchPrivacy", touchPrivacyMap[touchPrivacy]) } @Test fun `M enable session replay without custom endpoint W empty string()`( @DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double, - // Not ALLOW nor MASK_USER_INPUT - @StringForgery(regex = "^/(?!ALLOW|MASK_USER_INPUT)([a-z0-9]+)$/i") privacy: String, @BoolForgery startRecordingImmediately: Boolean ) { // Given + val imagePrivacy = imagePrivacyMap.keys.random() + val touchPrivacy = touchPrivacyMap.keys.random() + val textAndInputPrivacy = inputPrivacyMap.keys.random() val sessionReplayConfigCaptor = argumentCaptor() // When testedSessionReplay.enable( replaySampleRate, - privacy, "", + SessionReplayPrivacySettings( + imagePrivacyLevel = imagePrivacy, + touchPrivacyLevel = touchPrivacy, + textAndInputPrivacyLevel = textAndInputPrivacy + ), startRecordingImmediately, mockPromise ) diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm b/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm index 1b33e79f1..c750f0979 100644 --- a/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm +++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm @@ -18,15 +18,19 @@ @implementation DdSessionReplay RCT_EXPORT_MODULE() RCT_REMAP_METHOD(enable, withEnableReplaySampleRate:(double)replaySampleRate - withDefaultPrivacyLevel:(NSString*)defaultPrivacyLevel withCustomEndpoint:(NSString*)customEndpoint + withImagePrivacyLevel:(NSString*)imagePrivacyLevel + withTouchPrivacyLevel:(NSString*)touchPrivacyLevel + withTextAndInputPrivacyLevel:(NSString*)textAndInputPrivacyLevel withStartRecordingImmediately:(BOOL)startRecordingImmediately withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) { [self enable:replaySampleRate - defaultPrivacyLevel:defaultPrivacyLevel customEndpoint:customEndpoint + imagePrivacyLevel:imagePrivacyLevel + touchPrivacyLevel:touchPrivacyLevel + textAndInputPrivacyLevel:textAndInputPrivacyLevel startRecordingImmediately:startRecordingImmediately resolve:resolve reject:reject]; @@ -64,14 +68,18 @@ + (BOOL)requiresMainQueueSetup { } - (void)enable:(double)replaySampleRate - defaultPrivacyLevel:(NSString *)defaultPrivacyLevel customEndpoint:(NSString*)customEndpoint + imagePrivacyLevel:(NSString *)imagePrivacyLevel + touchPrivacyLevel:(NSString *)touchPrivacyLevel + textAndInputPrivacyLevel:(NSString *)textAndInputPrivacyLevel startRecordingImmediately:(BOOL)startRecordingImmediately resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self.ddSessionReplayImplementation enableWithReplaySampleRate:replaySampleRate - defaultPrivacyLevel:defaultPrivacyLevel customEndpoint:customEndpoint + imagePrivacyLevel:imagePrivacyLevel + touchPrivacyLevel:touchPrivacyLevel + textAndInputPrivacyLevel:textAndInputPrivacyLevel startRecordingImmediately:startRecordingImmediately resolve:resolve reject:reject]; diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift index 6166d8866..ffa627d79 100644 --- a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift +++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift @@ -32,8 +32,10 @@ public class DdSessionReplayImplementation: NSObject { @objc public func enable( replaySampleRate: Double, - defaultPrivacyLevel: String, customEndpoint: String, + imagePrivacyLevel: NSString, + touchPrivacyLevel: NSString, + textAndInputPrivacyLevel: NSString, startRecordingImmediately: Bool, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock @@ -44,7 +46,9 @@ public class DdSessionReplayImplementation: NSObject { } var sessionReplayConfiguration = SessionReplay.Configuration( replaySampleRate: Float(replaySampleRate), - defaultPrivacyLevel: buildPrivacyLevel(privacyLevel: defaultPrivacyLevel as NSString), + textAndInputPrivacyLevel: convertTextAndInputPrivacy(textAndInputPrivacyLevel), + imagePrivacyLevel: convertImagePrivacy(imagePrivacyLevel), + touchPrivacyLevel: convertTouchPrivacy(touchPrivacyLevel), startRecordingImmediately: startRecordingImmediately, customEndpoint: customEndpointURL ) @@ -85,16 +89,43 @@ public class DdSessionReplayImplementation: NSObject { resolve(nil) } - func buildPrivacyLevel(privacyLevel: NSString) -> SessionReplayPrivacyLevel { - switch privacyLevel.lowercased { - case "mask": - return .mask - case "mask_user_input": - return .maskUserInput - case "allow": - return .allow + func convertImagePrivacy(_ imagePrivacy: NSString) -> ImagePrivacyLevel { + switch imagePrivacy { + case "MASK_NON_BUNDLED_ONLY": + return .maskNonBundledOnly + case "MASK_ALL": + return .maskAll + case "MASK_NONE": + return .maskNone default: - return .mask + consolePrint("Unknown Session Replay Image Privacy Level given: \(imagePrivacy), using .maskAll as default.", .warn) + return .maskAll + } + } + + func convertTouchPrivacy(_ touchPrivacy: NSString) -> TouchPrivacyLevel { + switch touchPrivacy { + case "SHOW": + return .show + case "HIDE": + return .hide + default: + consolePrint("Unknown Session Replay Touch Privacy Level given: \(touchPrivacy), using .hide as default.", .warn) + return .hide + } + } + + func convertTextAndInputPrivacy(_ textAndInputPrivacy: NSString) -> TextAndInputPrivacyLevel { + switch textAndInputPrivacy { + case "MASK_SENSITIVE_INPUTS": + return .maskSensitiveInputs + case "MASK_ALL_INPUTS": + return .maskAllInputs + case "MASK_ALL": + return .maskAll + default: + consolePrint("Unknown Session Replay Text and Input Privacy Level given: \(textAndInputPrivacy), using .maskAll as default.", .warn) + return .maskAll } } } diff --git a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift index dd486e449..6626d93c7 100644 --- a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift +++ b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift @@ -12,6 +12,23 @@ import DatadogInternal import React internal class DdSessionReplayTests: XCTestCase { + private let imagePrivacyMap: [String: ImagePrivacyLevel] = [ + "MASK_ALL": .maskAll, + "MASK_NON_BUNDLED_ONLY": .maskNonBundledOnly, + "MASK_NONE": .maskNone + ] + + private let touchPrivacyMap: [String: TouchPrivacyLevel] = [ + "SHOW": .show, + "HIDE": .hide + ] + + private let inputPrivacyMap: [String: TextAndInputPrivacyLevel] = [ + "MASK_ALL": .maskAll, + "MASK_ALL_INPUTS": .maskAllInputs, + "MASK_SENSITIVE_INPUTS": .maskSensitiveInputs + ] + private func mockResolve(args: Any?) {} private func mockReject(args: String?, arg: String?, err: Error?) {} @@ -24,93 +41,59 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithZeroReplaySampleRate() { let sessionReplayMock = MockSessionReplay() let uiManagerMock = MockUIManager() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) - .enable(replaySampleRate: 0, defaultPrivacyLevel: "MASK", customEndpoint: "", startRecordingImmediately: true, resolve: mockResolve, reject: mockReject) - - XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable( - replaySampleRate: 0.0, - privacyLevel: .mask, - customEndpoint: nil, - startRecordingImmediately: true - )) - } - - func testEnablesSessionReplayWithMaskPrivacyLevel() { - let sessionReplayMock = MockSessionReplay() - let uiManagerMock = MockUIManager() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( - replaySampleRate: 100, - defaultPrivacyLevel: "MASK", - customEndpoint: "", - startRecordingImmediately: true, - resolve: mockResolve, - reject: mockReject - ) - XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable( - replaySampleRate: 100.0, - privacyLevel: .mask, - customEndpoint: nil, - startRecordingImmediately: true - )) - } - - func testEnablesSessionReplayWithMaskUserInputPrivacyLevel() { - let sessionReplayMock = MockSessionReplay() - let uiManagerMock = MockUIManager() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( - replaySampleRate: 100, - defaultPrivacyLevel: "MASK_USER_INPUT", - customEndpoint: "", - startRecordingImmediately: true, - resolve: mockResolve, - reject: mockReject - ) + guard + let imagePrivacyLevel = imagePrivacyMap.keys.randomElement(), + let imagePrivacy = imagePrivacyMap[imagePrivacyLevel], + let touchPrivacyLevel = touchPrivacyMap.keys.randomElement(), + let touchPrivacy = touchPrivacyMap[touchPrivacyLevel], + let textAndInputPrivacyLevel = inputPrivacyMap.keys.randomElement(), + let textAndInputPrivacy = inputPrivacyMap[textAndInputPrivacyLevel] + else { + XCTFail("Cannot retrieve privacy levels from maps") + return + } - XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable( - replaySampleRate: 100.0, - privacyLevel: .maskUserInput, - customEndpoint: nil, - startRecordingImmediately: true - )) - } - - func testEnablesSessionReplayWithAllowPrivacyLevel() { - let sessionReplayMock = MockSessionReplay() - let uiManagerMock = MockUIManager() DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( - replaySampleRate: 100, - defaultPrivacyLevel: "ALLOW", + replaySampleRate: 0, customEndpoint: "", + imagePrivacyLevel: NSString(string: imagePrivacyLevel), + touchPrivacyLevel: NSString(string: touchPrivacyLevel), + textAndInputPrivacyLevel: NSString(string: textAndInputPrivacyLevel), startRecordingImmediately: true, resolve: mockResolve, - reject: mockReject - ) - + reject: mockReject) + XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable( - replaySampleRate: 100.0, - privacyLevel: .allow, + replaySampleRate: 0.0, customEndpoint: nil, + imagePrivacyLevel: imagePrivacy, + touchPrivacyLevel: touchPrivacy, + textAndInputPrivacyLevel: textAndInputPrivacy, startRecordingImmediately: true )) } - func testEnablesSessionReplayWithBadPrivacyLevel() { + func testEnablesSessionReplayWithBadPrivacyLevels() { let sessionReplayMock = MockSessionReplay() let uiManagerMock = MockUIManager() + DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( replaySampleRate: 100, - defaultPrivacyLevel: "BAD_VALUE", customEndpoint: "", + imagePrivacyLevel: "BAD_VALUE", + touchPrivacyLevel: "BAD_VALUE", + textAndInputPrivacyLevel: "BAD_VALUE", startRecordingImmediately: true, resolve: mockResolve, - reject: mockReject - ) + reject: mockReject) XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable( replaySampleRate: 100.0, - privacyLevel: .mask, customEndpoint: nil, + imagePrivacyLevel: .maskAll, + touchPrivacyLevel: .hide, + textAndInputPrivacyLevel: .maskAll, startRecordingImmediately: true )) } @@ -118,65 +101,48 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithCustomEndpoint() { let sessionReplayMock = MockSessionReplay() let uiManagerMock = MockUIManager() + + guard + let imagePrivacyLevel = imagePrivacyMap.keys.randomElement(), + let imagePrivacy = imagePrivacyMap[imagePrivacyLevel], + let touchPrivacyLevel = touchPrivacyMap.keys.randomElement(), + let touchPrivacy = touchPrivacyMap[touchPrivacyLevel], + let textAndInputPrivacyLevel = inputPrivacyMap.keys.randomElement(), + let textAndInputPrivacy = inputPrivacyMap[textAndInputPrivacyLevel] + else { + XCTFail("Cannot retrieve privacy levels from maps") + return + } + DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( replaySampleRate: 100, - defaultPrivacyLevel: "MASK", customEndpoint: "https://session-replay.example.com", + imagePrivacyLevel: NSString(string: imagePrivacyLevel), + touchPrivacyLevel: NSString(string: touchPrivacyLevel), + textAndInputPrivacyLevel: NSString(string: textAndInputPrivacyLevel), startRecordingImmediately: true, resolve: mockResolve, reject: mockReject) XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable( replaySampleRate: 100.0, - privacyLevel: .mask, customEndpoint: URL(string: "https://session-replay.example.com/api/v2/replay"), + imagePrivacyLevel: imagePrivacy, + touchPrivacyLevel: touchPrivacy, + textAndInputPrivacyLevel: textAndInputPrivacy, startRecordingImmediately: true )) } - - func testStartSessionReplayManually() { - let sessionReplayMock = MockSessionReplay() - let uiManagerMock = MockUIManager() - let sessionReplay = DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) - sessionReplay.enable( - replaySampleRate: 100, - defaultPrivacyLevel: "MASK", - customEndpoint: "https://session-replay.example.com", - startRecordingImmediately: true, - resolve: mockResolve, - reject: mockReject - ) - - sessionReplay.startRecording(resolve: mockResolve, reject: mockReject) - - XCTAssertEqual(sessionReplayMock.calledMethods.last, .startRecording) - } - - func testStopSessionReplayManually() { - let sessionReplayMock = MockSessionReplay() - let uiManagerMock = MockUIManager() - let sessionReplay = DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) - sessionReplay.enable( - replaySampleRate: 100, - defaultPrivacyLevel: "MASK", - customEndpoint: "https://session-replay.example.com", - startRecordingImmediately: true, - resolve: mockResolve, - reject: mockReject - ) - - sessionReplay.stopRecording(resolve: mockResolve, reject: mockReject) - - XCTAssertEqual(sessionReplayMock.calledMethods.last, .stopRecording) - } } private class MockSessionReplay: SessionReplayProtocol { enum CalledMethod: Equatable { case enable( replaySampleRate: Float, - privacyLevel: SessionReplayPrivacyLevel, customEndpoint: URL?, + imagePrivacyLevel: ImagePrivacyLevel, + touchPrivacyLevel: TouchPrivacyLevel, + textAndInputPrivacyLevel: TextAndInputPrivacyLevel, startRecordingImmediately: Bool ) case startRecording @@ -189,8 +155,10 @@ private class MockSessionReplay: SessionReplayProtocol { calledMethods.append( .enable( replaySampleRate: configuration.replaySampleRate, - privacyLevel: configuration.defaultPrivacyLevel, customEndpoint: configuration.customEndpoint, + imagePrivacyLevel: configuration.imagePrivacyLevel, + touchPrivacyLevel: configuration.touchPrivacyLevel, + textAndInputPrivacyLevel: configuration.textAndInputPrivacyLevel, startRecordingImmediately: configuration.startRecordingImmediately ) ) diff --git a/packages/react-native-session-replay/src/SessionReplay.ts b/packages/react-native-session-replay/src/SessionReplay.ts index cfdf8de96..7875f63af 100644 --- a/packages/react-native-session-replay/src/SessionReplay.ts +++ b/packages/react-native-session-replay/src/SessionReplay.ts @@ -12,6 +12,49 @@ export enum SessionReplayPrivacy { MASK_USER_INPUT = 'MASK_USER_INPUT' } +export enum ImagePrivacyLevel { + /** + * Only images that are bundled within the application will be recorded. + * + * On Android, all images larger than 100x100 dp will be masked, as we consider them non-bundled images. + */ + MASK_NON_BUNDLED_ONLY = 'MASK_NON_BUNDLED_ONLY', + /** + * No images will be recorded. + */ + MASK_ALL = 'MASK_ALL', + /** + * All images will be recorded, including the ones downloaded from the Internet or generated during the app runtime. + */ + MASK_NONE = 'MASK_NONE' +} + +export enum TouchPrivacyLevel { + /** + * Show all user touches. + */ + SHOW = 'SHOW', + /** + * Hide all user touches. + */ + HIDE = 'HIDE' +} + +export enum TextAndInputPrivacyLevel { + /** + * Show all texts except sensitive inputs (e.g password fields). + */ + MASK_SENSITIVE_INPUTS = 'MASK_SENSITIVE_INPUTS', + /** + * Mask all input fields (e.g text fields, switches, checkboxes). + */ + MASK_ALL_INPUTS = 'MASK_ALL_INPUTS', + /** + * Mask all texts and inputs (e.g labels). + */ + MASK_ALL = 'MASK_ALL' +} + /** * The Session Replay configuration object. */ @@ -24,16 +67,27 @@ export interface SessionReplayConfiguration { * Default value is `20`. */ replaySampleRate?: number; + /** - * Defines the way sensitive content (e.g. text) should be masked. - * - * Default `SessionReplayPrivacy.MASK`. + * Defines the way images should be masked (Default: `MASK_ALL`) */ - defaultPrivacyLevel?: SessionReplayPrivacy; + imagePrivacyLevel?: ImagePrivacyLevel; + + /** + * Defines the way user touches (e.g tap) should be masked (Default: `HIDE`) + */ + touchPrivacyLevel?: TouchPrivacyLevel; + + /** + * Defines the way text and input (e.g text fields, checkboxes) should be masked (Default: `MASK_ALL`) + */ + textAndInputPrivacyLevel?: TextAndInputPrivacyLevel; + /** * Custom server url for sending replay data. */ customEndpoint?: string; + /** * Whether the recording should start automatically when the feature is enabled. * When `true`, the recording starts automatically. @@ -41,12 +95,41 @@ export interface SessionReplayConfiguration { * Default: `true`. */ startRecordingImmediately?: boolean; + + /** + * Defines the way sensitive content (e.g. text) should be masked. + * + * Default `SessionReplayPrivacy.MASK`. + * @deprecated Use {@link imagePrivacyLevel}, {@link touchPrivacyLevel} and {@link textAndInputPrivacyLevel} instead. + * Note: setting this property (`defaultPrivacyLevel`) will override the individual privacy levels. + */ + defaultPrivacyLevel?: SessionReplayPrivacy; } -const DEFAULTS = { +type InternalBaseSessionReplayConfiguration = { + replaySampleRate: number; + customEndpoint: string; + startRecordingImmediately: boolean; +}; + +type InternalPrivacySessionReplayConfiguration = { + imagePrivacyLevel: ImagePrivacyLevel; + touchPrivacyLevel: TouchPrivacyLevel; + textAndInputPrivacyLevel: TextAndInputPrivacyLevel; +}; + +type InternalSessionReplayConfiguration = InternalBaseSessionReplayConfiguration & + InternalPrivacySessionReplayConfiguration; + +const DEFAULTS: InternalSessionReplayConfiguration & { + defaultPrivacyLevel: SessionReplayPrivacy; +} = { replaySampleRate: 0, defaultPrivacyLevel: SessionReplayPrivacy.MASK, customEndpoint: '', + imagePrivacyLevel: ImagePrivacyLevel.MASK_ALL, + touchPrivacyLevel: TouchPrivacyLevel.HIDE, + textAndInputPrivacyLevel: TextAndInputPrivacyLevel.MASK_ALL, startRecordingImmediately: true }; @@ -57,30 +140,21 @@ export class SessionReplayWrapper { private buildConfiguration = ( configuration?: SessionReplayConfiguration - ): { - replaySampleRate: number; - defaultPrivacyLevel: SessionReplayPrivacy; - customEndpoint: string; - startRecordingImmediately: boolean; - } => { + ): InternalSessionReplayConfiguration => { if (!configuration) { return DEFAULTS; } const { replaySampleRate, - defaultPrivacyLevel, customEndpoint, startRecordingImmediately } = configuration; - return { + + const baseConfig: InternalBaseSessionReplayConfiguration = { replaySampleRate: replaySampleRate !== undefined ? replaySampleRate : DEFAULTS.replaySampleRate, - defaultPrivacyLevel: - defaultPrivacyLevel !== undefined - ? defaultPrivacyLevel - : DEFAULTS.defaultPrivacyLevel, customEndpoint: customEndpoint !== undefined ? customEndpoint @@ -90,6 +164,45 @@ export class SessionReplayWrapper { ? startRecordingImmediately : DEFAULTS.startRecordingImmediately }; + + const privacyConfig: InternalPrivacySessionReplayConfiguration = { + imagePrivacyLevel: + configuration.imagePrivacyLevel ?? DEFAULTS.imagePrivacyLevel, + touchPrivacyLevel: + configuration.touchPrivacyLevel ?? DEFAULTS.touchPrivacyLevel, + textAndInputPrivacyLevel: + configuration.textAndInputPrivacyLevel ?? + DEFAULTS.textAndInputPrivacyLevel + }; + + // Legacy Default Privacy Level property handling + if (configuration.defaultPrivacyLevel) { + switch (configuration.defaultPrivacyLevel) { + case SessionReplayPrivacy.MASK: + privacyConfig.imagePrivacyLevel = + ImagePrivacyLevel.MASK_ALL; + privacyConfig.touchPrivacyLevel = TouchPrivacyLevel.HIDE; + privacyConfig.textAndInputPrivacyLevel = + TextAndInputPrivacyLevel.MASK_ALL; + break; + case SessionReplayPrivacy.MASK_USER_INPUT: + privacyConfig.imagePrivacyLevel = + ImagePrivacyLevel.MASK_NONE; + privacyConfig.touchPrivacyLevel = TouchPrivacyLevel.HIDE; + privacyConfig.textAndInputPrivacyLevel = + TextAndInputPrivacyLevel.MASK_ALL_INPUTS; + break; + case SessionReplayPrivacy.ALLOW: + privacyConfig.imagePrivacyLevel = + ImagePrivacyLevel.MASK_NONE; + privacyConfig.touchPrivacyLevel = TouchPrivacyLevel.SHOW; + privacyConfig.textAndInputPrivacyLevel = + TextAndInputPrivacyLevel.MASK_SENSITIVE_INPUTS; + break; + } + } + + return { ...baseConfig, ...privacyConfig }; }; /** @@ -99,15 +212,19 @@ export class SessionReplayWrapper { enable = (configuration?: SessionReplayConfiguration): Promise => { const { replaySampleRate, - defaultPrivacyLevel, customEndpoint, + imagePrivacyLevel, + touchPrivacyLevel, + textAndInputPrivacyLevel, startRecordingImmediately } = this.buildConfiguration(configuration); return this.nativeSessionReplay.enable( replaySampleRate, - defaultPrivacyLevel, customEndpoint, + imagePrivacyLevel, + touchPrivacyLevel, + textAndInputPrivacyLevel, startRecordingImmediately ); }; diff --git a/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts b/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts index a1ed2e01b..fe5003782 100644 --- a/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts +++ b/packages/react-native-session-replay/src/__tests__/SessionReplay.test.ts @@ -6,7 +6,21 @@ import { NativeModules } from 'react-native'; -import { SessionReplay, SessionReplayPrivacy } from '../SessionReplay'; +import { + ImagePrivacyLevel, + SessionReplay, + SessionReplayPrivacy, + TextAndInputPrivacyLevel, + TouchPrivacyLevel +} from '../SessionReplay'; + +function getRandomEnumValue< + T extends { [s: string]: T[keyof T] } | ArrayLike +>(enumObj: T): T[keyof T] { + const values = Object.values(enumObj) as T[keyof T][]; // Get all enum values + const randomIndex = Math.floor(Math.random() * values.length); // Generate a random index + return values[randomIndex]; // Return the random value +} beforeEach(() => { NativeModules.DdSessionReplay.enable.mockClear(); @@ -19,13 +33,15 @@ describe('SessionReplay', () => { expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( 0, - 'MASK', '', + 'MASK_ALL', + 'HIDE', + 'MASK_ALL', true ); }); - it('calls native session replay with provided configuration', () => { + it('calls native session replay with provided configuration { w defaultPrivacyLevel = ALLOW }', () => { SessionReplay.enable({ replaySampleRate: 100, defaultPrivacyLevel: SessionReplayPrivacy.ALLOW, @@ -34,34 +50,90 @@ describe('SessionReplay', () => { expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( 100, - 'ALLOW', 'https://session-replay.example.com', + 'MASK_NONE', + 'SHOW', + 'MASK_SENSITIVE_INPUTS', true ); }); - it('calls native session replay with edge cases in configuration', () => { + it('calls native session replay with provided configuration { w defaultPrivacyLevel = MASK }', () => { SessionReplay.enable({ - replaySampleRate: 0, - customEndpoint: '' + replaySampleRate: 100, + defaultPrivacyLevel: SessionReplayPrivacy.MASK, + customEndpoint: 'https://session-replay.example.com' }); expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( - 0, - 'MASK', - '', + 100, + 'https://session-replay.example.com', + 'MASK_ALL', + 'HIDE', + 'MASK_ALL', true ); }); - it('calls native session replay with start immediately = false', () => { - SessionReplay.enable({ startRecordingImmediately: false }); + it('calls native session replay with provided configuration { w defaultPrivacyLevel = MASK_USER_INPUT }', () => { + SessionReplay.enable({ + replaySampleRate: 100, + defaultPrivacyLevel: SessionReplayPrivacy.MASK_USER_INPUT, + customEndpoint: 'https://session-replay.example.com' + }); + + expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( + 100, + 'https://session-replay.example.com', + 'MASK_NONE', + 'HIDE', + 'MASK_ALL_INPUTS', + true + ); + }); + + it('calls native session replay with provided configuration { w random privacy levels }', () => { + const TIMES = 20; + + const image = getRandomEnumValue(ImagePrivacyLevel); + const touch = getRandomEnumValue(TouchPrivacyLevel); + const textAndInput = getRandomEnumValue(TextAndInputPrivacyLevel); + + for (let i = 0; i < TIMES; ++i) { + SessionReplay.enable({ + replaySampleRate: 100, + customEndpoint: 'https://session-replay.example.com', + imagePrivacyLevel: image, + touchPrivacyLevel: touch, + textAndInputPrivacyLevel: textAndInput + }); + + expect( + NativeModules.DdSessionReplay.enable + ).toHaveBeenCalledWith( + 100, + 'https://session-replay.example.com', + image, + touch, + textAndInput, + true + ); + } + }); + + it('calls native session replay with edge cases in configuration', () => { + SessionReplay.enable({ + replaySampleRate: 0, + customEndpoint: '' + }); expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( 0, - 'MASK', '', - false + 'MASK_ALL', + 'HIDE', + 'MASK_ALL', + true ); }); }); diff --git a/packages/react-native-session-replay/src/nativeModulesTypes.ts b/packages/react-native-session-replay/src/nativeModulesTypes.ts index 00f90202e..86e7d4668 100644 --- a/packages/react-native-session-replay/src/nativeModulesTypes.ts +++ b/packages/react-native-session-replay/src/nativeModulesTypes.ts @@ -11,7 +11,12 @@ import type { Spec as NativeDdSessionReplay } from './specs/NativeDdSessionRepla * As we cannot use enums or classes in the specs, we override methods using them here. */ -type PrivacyLevel = 'MASK' | 'MASK_USER_INPUT' | 'ALLOW'; +type ImagePrivacyLevel = 'MASK_NON_BUNDLED_ONLY' | 'MASK_ALL' | 'MASK_NONE'; +type TouchPrivacyLevel = 'SHOW' | 'HIDE'; +type TextAndInputPrivacyLevel = + | 'MASK_SENSITIVE_INPUTS' + | 'MASK_ALL_INPUTS' + | 'MASK_ALL'; /** * The entry point to use Datadog's Session Replay feature. @@ -20,14 +25,20 @@ export interface NativeSessionReplayType extends NativeDdSessionReplay { /** * Enable session replay and start recording session. * @param replaySampleRate: The sample rate applied for session replay. - * @param defaultPrivacyLevel: The privacy level used for replay. * @param customEndpoint: Custom server url for sending replay data. - * @param startRecordingImmediately: Whether the recording should start automatically when the feature is enabled. When `true`, the recording starts automatically; when `false` it doesn't, and the recording will need to be started manually. Default: `true`. + * @param imagePrivacyLevel: Defines the way images should be masked. + * @param touchPrivacyLevel: Defines the way user touches should be masked. + * @param textAndInputPrivacyLevel: Defines the way text and input should be masked. + * @param startRecordingImmediately: Whether the recording should start automatically when the feature is enabled. + * When `true`, the recording starts automatically; when `false` it doesn't, and the recording will need + * to be started manually. Default: `true`. */ enable( replaySampleRate: number, - defaultPrivacyLevel: PrivacyLevel, customEndpoint: string, + imagePrivacyLevel: ImagePrivacyLevel, + touchPrivacyLevel: TouchPrivacyLevel, + textAndInputPrivacyLevel: TextAndInputPrivacyLevel, startRecordingImmediately: boolean ): Promise; diff --git a/packages/react-native-session-replay/src/specs/NativeDdSessionReplay.ts b/packages/react-native-session-replay/src/specs/NativeDdSessionReplay.ts index 272ebcf77..61a73eed1 100644 --- a/packages/react-native-session-replay/src/specs/NativeDdSessionReplay.ts +++ b/packages/react-native-session-replay/src/specs/NativeDdSessionReplay.ts @@ -17,14 +17,20 @@ export interface Spec extends TurboModule { /** * Enable session replay and start recording session. * @param replaySampleRate: The sample rate applied for session replay. - * @param defaultPrivacyLevel: The privacy level used for replay. * @param customEndpoint: Custom server url for sending replay data. - * @param startRecordingImmediately: Whether the recording should start automatically when the feature is enabled. When `true`, the recording starts automatically; when `false` it doesn't, and the recording will need to be started manually. Default: `true`. + * @param imagePrivacyLevel: Defines the way images should be masked. + * @param touchPrivacyLevel: Defines the way user touches should be masked. + * @param textAndInputPrivacyLevel: Defines the way text and input should be masked. + * @param startRecordingImmediately: Whether the recording should start automatically when the feature is enabled. + * When `true`, the recording starts automatically; when `false` it doesn't, + * and the recording will need to be started manually. Default: `true`. */ enable( replaySampleRate: number, - defaultPrivacyLevel: string, customEndpoint: string, + imagePrivacyLevel: string, + touchPrivacyLevel: string, + textAndInputPrivacyLevel: string, startRecordingImmediately: boolean ): Promise;