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 acd866ab3..5d123b3e2 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,25 +28,21 @@ class DdSessionReplayImplementation( /** * Enable session replay and start recording session. * @param replaySampleRate The sample rate applied for session replay. - * @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 privacySettings Defines the way visual elements should be masked. * @param customEndpoint Custom server url for sending replay data. */ fun enable( replaySampleRate: Double, customEndpoint: String, - imagePrivacyLevel: String, - touchPrivacyLevel: String, - textAndInputPrivacyLevel: String, + privacySettings: SessionReplayPrivacySettings, promise: Promise ) { val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore val logger = sdkCore.internalLogger val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat()) - .setImagePrivacy(convertImagePrivacyLevel(imagePrivacyLevel)) - .setTouchPrivacy(convertTouchPrivacyLevel(touchPrivacyLevel)) - .setTextAndInputPrivacy(convertTextAndInputPrivacyLevel(textAndInputPrivacyLevel)) + .setImagePrivacy(privacySettings.imagePrivacyLevel) + .setTouchPrivacy(privacySettings.touchPrivacyLevel) + .setTextAndInputPrivacy(privacySettings.textAndInputPrivacyLevel) .addExtensionSupport(ReactNativeSessionReplayExtensionSupport(reactContext, logger)) if (customEndpoint != "") { @@ -57,68 +53,7 @@ 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" - - internal fun convertImagePrivacyLevel(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 -> { - // TODO: Log wrong usage / mapping. - ImagePrivacy.MASK_ALL - } - } - } - - internal fun convertTouchPrivacyLevel(touchPrivacyLevel: String): TouchPrivacy { - return when (touchPrivacyLevel) { - "SHOW" -> TouchPrivacy.SHOW - "HIDE" -> TouchPrivacy.HIDE - else -> { - // TODO: Log wrong usage / mapping. - TouchPrivacy.HIDE - } - } - } - - internal fun convertTextAndInputPrivacyLevel(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 -> { - // TODO: Log wrong usage / mapping - TextAndInputPrivacy.MASK_ALL - } - } - } } } 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..ef4975a05 --- /dev/null +++ b/packages/react-native-session-replay/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplayPrivacySettings.kt @@ -0,0 +1,77 @@ +/* + * 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 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 -> { + // TODO: Log wrong usage / mapping. + ImagePrivacy.MASK_ALL + } + } + } + + internal fun getTouchPrivacy(touchPrivacyLevel: String): TouchPrivacy { + return when (touchPrivacyLevel) { + "SHOW" -> TouchPrivacy.SHOW + "HIDE" -> TouchPrivacy.HIDE + else -> { + // TODO: Log wrong usage / mapping. + 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 -> { + // TODO: Log wrong usage / mapping + TextAndInputPrivacy.MASK_ALL + } + } + } + } +} \ No newline at end of file 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 1dce9ab2b..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 @@ -42,9 +42,11 @@ class DdSessionReplay( implementation.enable( replaySampleRate, customEndpoint, - imagePrivacyLevel, - touchPrivacyLevel, - textAndInputPrivacyLevel, + 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 f42b91958..21efb001f 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 @@ -42,9 +42,11 @@ class DdSessionReplay( implementation.enable( replaySampleRate, customEndpoint, - imagePrivacyLevel, - touchPrivacyLevel, - textAndInputPrivacyLevel, + SessionReplayPrivacySettings( + imagePrivacyLevel = imagePrivacyLevel, + touchPrivacyLevel = touchPrivacyLevel, + textAndInputPrivacyLevel = textAndInputPrivacyLevel + ), 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 b4d54a130..c65467e22 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 @@ -19,7 +19,6 @@ import com.facebook.react.uimanager.UIManagerModule 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 @@ -56,6 +55,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>())) @@ -70,33 +86,29 @@ internal class DdSessionReplayImplementationTest { } @Test - fun `M enable session replay W privacy = ALLOW`( - @DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double, - @StringForgery(regex = ".+") customEndpoint: String - ) { - testSessionReplayEnable("ALLOW", replaySampleRate, customEndpoint) - } - - @Test - fun `M enable session replay W privacy = MASK`( - @DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double, - @StringForgery(regex = ".+") customEndpoint: String - ) { - testSessionReplayEnable("MASK", replaySampleRate, customEndpoint) - } - - @Test - fun `M enable session replay W privacy = MASK_USER_INPUT`( + fun `M enable session replay W random privacy settings`( @DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double, @StringForgery(regex = ".+") customEndpoint: String ) { - testSessionReplayEnable("MASK_USER_INPUT", replaySampleRate, customEndpoint) + val imagePrivacy = imagePrivacyMap.keys.random() + val touchPrivacy = touchPrivacyMap.keys.random() + val textAndInputPrivacy = inputPrivacyMap.keys.random() + + testSessionReplayEnable( + replaySampleRate = replaySampleRate, + customEndpoint = customEndpoint, + imagePrivacy = imagePrivacy, + touchPrivacy = touchPrivacy, + textAndInputPrivacy = textAndInputPrivacy + ) } private fun testSessionReplayEnable( - privacy: String, replaySampleRate: Double, - customEndpoint: String + customEndpoint: String, + imagePrivacy: String, + touchPrivacy: String, + textAndInputPrivacy: String ) { // Given val sessionReplayConfigCaptor = argumentCaptor() @@ -104,8 +116,8 @@ internal class DdSessionReplayImplementationTest { // When testedSessionReplay.enable( replaySampleRate, - privacy, customEndpoint, + SessionReplayPrivacySettings(imagePrivacy, touchPrivacy, textAndInputPrivacy), mockPromise ) @@ -114,43 +126,32 @@ 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 + @DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double ) { // Given + val imagePrivacy = imagePrivacyMap.keys.random() + val touchPrivacy = touchPrivacyMap.keys.random() + val textAndInputPrivacy = inputPrivacyMap.keys.random() val sessionReplayConfigCaptor = argumentCaptor() // When - testedSessionReplay.enable(replaySampleRate, privacy, "", mockPromise) + testedSessionReplay.enable( + replaySampleRate, + "", + SessionReplayPrivacySettings( + imagePrivacyLevel = imagePrivacy, + touchPrivacyLevel = touchPrivacy, + textAndInputPrivacyLevel = textAndInputPrivacy + ), + mockPromise + ) // Then verify(mockSessionReplay).enable(sessionReplayConfigCaptor.capture(), any()) diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift index eff12be3e..b8701a200 100644 --- a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift +++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift @@ -33,9 +33,9 @@ public class DdSessionReplayImplementation: NSObject { public func enable( replaySampleRate: Double, customEndpoint: String, - imagePrivacyLevel: String, - touchPrivacyLevel: String, - textAndInputPrivacyLevel: String, + imagePrivacyLevel: NSString, + touchPrivacyLevel: NSString, + textAndInputPrivacyLevel: NSString, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock ) -> Void { @@ -45,9 +45,9 @@ public class DdSessionReplayImplementation: NSObject { } var sessionReplayConfiguration = SessionReplay.Configuration( replaySampleRate: Float(replaySampleRate), + textAndInputPrivacyLevel: convertTextAndInputPrivacy(textAndInputPrivacyLevel), imagePrivacyLevel: convertImagePrivacy(imagePrivacyLevel), touchPrivacyLevel: convertTouchPrivacy(touchPrivacyLevel), - textAndInputPrivacyLevel: convertTextAndInputPrivacy(textAndInputPrivacyLevel), customEndpoint: customEndpointURL ) diff --git a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift index 7c47d2408..2d854abc1 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,61 +41,103 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithZeroReplaySampleRate() { let sessionReplayMock = MockSessionReplay() let uiManagerMock = MockUIManager() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) - .enable(replaySampleRate: 0, defaultPrivacyLevel: "MASK", customEndpoint: "", resolve: mockResolve, reject: mockReject) - - XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 0.0, privacyLevel: .mask, customEndpoint: nil)) - } - - func testEnablesSessionReplayWithMaskPrivacyLevel() { - let sessionReplayMock = MockSessionReplay() - let uiManagerMock = MockUIManager() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) - .enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK", customEndpoint: "", resolve: mockResolve, reject: mockReject) - XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .mask, customEndpoint: nil)) - } - - func testEnablesSessionReplayWithMaskUserInputPrivacyLevel() { - let sessionReplayMock = MockSessionReplay() - let uiManagerMock = MockUIManager() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) - .enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK_USER_INPUT", customEndpoint: "", 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)) + DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( + replaySampleRate: 0, + customEndpoint: "", + imagePrivacyLevel: NSString(string: imagePrivacyLevel), + touchPrivacyLevel: NSString(string: touchPrivacyLevel), + textAndInputPrivacyLevel: NSString(string: textAndInputPrivacyLevel), + resolve: mockResolve, + reject: mockReject) + + XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable( + replaySampleRate: 0.0, + customEndpoint: nil, + imagePrivacyLevel: imagePrivacy, + touchPrivacyLevel: touchPrivacy, + textAndInputPrivacyLevel: textAndInputPrivacy + )) } - func testEnablesSessionReplayWithAllowPrivacyLevel() { + func testEnablesSessionReplayWithBadPrivacyLevels() { let sessionReplayMock = MockSessionReplay() let uiManagerMock = MockUIManager() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) - .enable(replaySampleRate: 100, defaultPrivacyLevel: "ALLOW", customEndpoint: "", resolve: mockResolve, reject: mockReject) - - XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .allow, customEndpoint: nil)) - } - func testEnablesSessionReplayWithBadPrivacyLevel() { - let sessionReplayMock = MockSessionReplay() - let uiManagerMock = MockUIManager() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) - .enable(replaySampleRate: 100, defaultPrivacyLevel: "BAD_VALUE", customEndpoint: "", resolve: mockResolve, reject: mockReject) + DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock).enable( + replaySampleRate: 100, + customEndpoint: "", + imagePrivacyLevel: "BAD_VALUE", + touchPrivacyLevel: "BAD_VALUE", + textAndInputPrivacyLevel: "BAD_VALUE", + resolve: mockResolve, + reject: mockReject) - XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .mask, customEndpoint: nil)) + XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable( + replaySampleRate: 100.0, + customEndpoint: nil, + imagePrivacyLevel: .maskAll, + touchPrivacyLevel: .hide, + textAndInputPrivacyLevel: .maskAll + )) } func testEnablesSessionReplayWithCustomEndpoint() { let sessionReplayMock = MockSessionReplay() let uiManagerMock = MockUIManager() - DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) - .enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK", customEndpoint: "https://session-replay.example.com", 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"))) + 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, + customEndpoint: "https://session-replay.example.com", + imagePrivacyLevel: NSString(string: imagePrivacyLevel), + touchPrivacyLevel: NSString(string: touchPrivacyLevel), + textAndInputPrivacyLevel: NSString(string: textAndInputPrivacyLevel), + resolve: mockResolve, + reject: mockReject) + + XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable( + replaySampleRate: 100.0, + customEndpoint: URL(string: "https://session-replay.example.com/api/v2/replay"), + imagePrivacyLevel: imagePrivacy, + touchPrivacyLevel: touchPrivacy, + textAndInputPrivacyLevel: textAndInputPrivacy + )) } } private class MockSessionReplay: SessionReplayProtocol { enum CalledMethod: Equatable { - case enable(replaySampleRate: Float, privacyLevel: SessionReplayPrivacyLevel, customEndpoint: URL?) + case enable( + replaySampleRate: Float, + customEndpoint: URL?, + imagePrivacyLevel: ImagePrivacyLevel, + touchPrivacyLevel: TouchPrivacyLevel, + textAndInputPrivacyLevel: TextAndInputPrivacyLevel + ) } public var calledMethods = [CalledMethod]() @@ -87,8 +146,10 @@ private class MockSessionReplay: SessionReplayProtocol { calledMethods.append( .enable( replaySampleRate: configuration.replaySampleRate, - privacyLevel: configuration.defaultPrivacyLevel, - customEndpoint: configuration.customEndpoint + customEndpoint: configuration.customEndpoint, + imagePrivacyLevel: configuration.imagePrivacyLevel, + touchPrivacyLevel: configuration.touchPrivacyLevel, + textAndInputPrivacyLevel: configuration.textAndInputPrivacyLevel ) ) } 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 1b32ff27c..4f502c087 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,12 +33,14 @@ describe('SessionReplay', () => { expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( 0, - 'MASK', - '' + '', + 'MASK_ALL', + 'HIDE', + 'MASK_ALL' ); }); - 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, @@ -33,11 +49,73 @@ describe('SessionReplay', () => { expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( 100, - 'ALLOW', - 'https://session-replay.example.com' + 'https://session-replay.example.com', + 'MASK_NONE', + 'SHOW', + 'MASK_SENSITIVE_INPUTS' + ); + }); + + it('calls native session replay with provided configuration { w defaultPrivacyLevel = MASK }', () => { + SessionReplay.enable({ + replaySampleRate: 100, + defaultPrivacyLevel: SessionReplayPrivacy.MASK, + customEndpoint: 'https://session-replay.example.com' + }); + + expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( + 100, + 'https://session-replay.example.com', + 'MASK_ALL', + 'HIDE', + 'MASK_ALL' + ); + }); + + 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' ); }); + 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 + ); + } + }); + it('calls native session replay with edge cases in configuration', () => { SessionReplay.enable({ replaySampleRate: 0, @@ -46,8 +124,10 @@ describe('SessionReplay', () => { expect(NativeModules.DdSessionReplay.enable).toHaveBeenCalledWith( 0, - 'MASK', - '' + '', + 'MASK_ALL', + 'HIDE', + 'MASK_ALL' ); }); });