Skip to content

Commit

Permalink
Added tests for SR FGM configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-saia-datadog committed Nov 18, 2024
1 parent 9b312d3 commit 2edd342
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 176 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "") {
Expand All @@ -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
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ class DdSessionReplay(
implementation.enable(
replaySampleRate,
customEndpoint,
imagePrivacyLevel,
touchPrivacyLevel,
textAndInputPrivacyLevel,
SessionReplayPrivacySettings(
imagePrivacyLevel = imagePrivacyLevel,
touchPrivacyLevel = touchPrivacyLevel,
textAndInputPrivacyLevel = textAndInputPrivacyLevel
),
promise
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ class DdSessionReplay(
implementation.enable(
replaySampleRate,
customEndpoint,
imagePrivacyLevel,
touchPrivacyLevel,
textAndInputPrivacyLevel,
SessionReplayPrivacySettings(
imagePrivacyLevel = imagePrivacyLevel,
touchPrivacyLevel = touchPrivacyLevel,
textAndInputPrivacyLevel = textAndInputPrivacyLevel
),
promise
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Class<NativeModule>>()))
Expand All @@ -70,42 +86,38 @@ 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<SessionReplayConfiguration>()

// When
testedSessionReplay.enable(
replaySampleRate,
privacy,
customEndpoint,
SessionReplayPrivacySettings(imagePrivacy, touchPrivacy, textAndInputPrivacy),
mockPromise
)

Expand All @@ -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<SessionReplayConfiguration>()

// When
testedSessionReplay.enable(replaySampleRate, privacy, "", mockPromise)
testedSessionReplay.enable(
replaySampleRate,
"",
SessionReplayPrivacySettings(
imagePrivacyLevel = imagePrivacy,
touchPrivacyLevel = touchPrivacy,
textAndInputPrivacyLevel = textAndInputPrivacy
),
mockPromise
)

// Then
verify(mockSessionReplay).enable(sessionReplayConfigCaptor.capture(), any())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
)

Expand Down
Loading

0 comments on commit 2edd342

Please sign in to comment.