From 2cbb9c158c8324b63ae3d9bf7247f897831bd57f Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:32:17 +0200 Subject: [PATCH 1/2] RUM-7150: Add multiple extension support --- detekt_custom.yml | 4 + .../api/apiSurface | 1 + .../dd-sdk-android-session-replay-compose.api | 1 + .../compose/ComposeExtensionSupport.kt | 7 + .../compose/ComposeExtensionSupportTest.kt | 9 + .../api/apiSurface | 1 + ...dd-sdk-android-session-replay-material.api | 1 + .../material/MaterialExtensionSupport.kt | 7 + .../material/MaterialExtensionSupportTest.kt | 9 +- .../api/apiSurface | 3 + .../api/dd-sdk-android-session-replay.api | 3 + .../android/sessionreplay/ExtensionSupport.kt | 6 + .../sessionreplay/MapperTypeWrapper.kt | 13 ++ .../SessionReplayConfiguration.kt | 56 ++++- .../internal/NoOpExtensionSupport.kt | 27 --- .../SessionReplayConfigurationBuilderTest.kt | 194 ++++++++++++++++++ 16 files changed, 307 insertions(+), 35 deletions(-) delete mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/NoOpExtensionSupport.kt diff --git a/detekt_custom.yml b/detekt_custom.yml index d65a6c8386..7ee94da0b5 100644 --- a/detekt_custom.yml +++ b/detekt_custom.yml @@ -858,6 +858,7 @@ datadog: - "kotlin.collections.List.contains(kotlin.String)" - "kotlin.collections.List.contains(kotlin.String)" - "kotlin.collections.List.count()" + - "kotlin.collections.List.distinct()" - "kotlin.collections.List.drop(kotlin.Int)" - "kotlin.collections.List.elementAtOrNull(kotlin.Int)" - "kotlin.collections.List.filter(kotlin.Function1)" @@ -1014,17 +1015,20 @@ datadog: - "kotlin.collections.MutableMap?.forEach(kotlin.Function1)" - "kotlin.collections.MutableSet.add(com.datadog.android.api.feature.FeatureContextUpdateReceiver?)" - "kotlin.collections.MutableSet.add(com.datadog.android.core.internal.persistence.ConsentAwareStorage.Batch)" + - "kotlin.collections.MutableSet.add(com.datadog.android.sessionreplay.ExtensionSupport)" - "kotlin.collections.MutableSet.add(com.datadog.android.telemetry.internal.TelemetryEventId)" - "kotlin.collections.MutableSet.add(java.io.File)" - "kotlin.collections.MutableSet.add(kotlin.String)" - "kotlin.collections.MutableSet.add(kotlin.String?)" - "kotlin.collections.MutableSet.addAll(kotlin.collections.Collection)" + - "kotlin.collections.MutableSet.any(kotlin.Function1)" - "kotlin.collections.MutableSet.clear()" - "kotlin.collections.MutableSet.contains(com.datadog.android.telemetry.internal.TelemetryEventId)" - "kotlin.collections.MutableSet.contains(kotlin.String)" - "kotlin.collections.MutableSet.contains(kotlin.String?)" - "kotlin.collections.MutableSet.filter(kotlin.Function1)" - "kotlin.collections.MutableSet.firstOrNull(kotlin.Function1)" + - "kotlin.collections.MutableSet.flatMap(kotlin.Function1)" - "kotlin.collections.MutableSet.forEach(kotlin.Function1)" - "kotlin.collections.MutableSet.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)" - "kotlin.collections.MutableSet.map(kotlin.Function1)" diff --git a/features/dd-sdk-android-session-replay-compose/api/apiSurface b/features/dd-sdk-android-session-replay-compose/api/apiSurface index 75d020d268..6132cc3eec 100644 --- a/features/dd-sdk-android-session-replay-compose/api/apiSurface +++ b/features/dd-sdk-android-session-replay-compose/api/apiSurface @@ -2,4 +2,5 @@ class com.datadog.android.sessionreplay.compose.ComposeExtensionSupport : com.da override fun getCustomViewMappers(): List> override fun getOptionSelectorDetectors(): List override fun getCustomDrawableMapper(): List + override fun name(): String annotation com.datadog.android.sessionreplay.compose.ExperimentalSessionReplayApi diff --git a/features/dd-sdk-android-session-replay-compose/api/dd-sdk-android-session-replay-compose.api b/features/dd-sdk-android-session-replay-compose/api/dd-sdk-android-session-replay-compose.api index fbe651f457..bd9c6090f4 100644 --- a/features/dd-sdk-android-session-replay-compose/api/dd-sdk-android-session-replay-compose.api +++ b/features/dd-sdk-android-session-replay-compose/api/dd-sdk-android-session-replay-compose.api @@ -4,6 +4,7 @@ public final class com/datadog/android/sessionreplay/compose/ComposeExtensionSup public fun getCustomDrawableMapper ()Ljava/util/List; public fun getCustomViewMappers ()Ljava/util/List; public fun getOptionSelectorDetectors ()Ljava/util/List; + public fun name ()Ljava/lang/String; } public abstract interface annotation class com/datadog/android/sessionreplay/compose/ExperimentalSessionReplayApi : java/lang/annotation/Annotation { diff --git a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/ComposeExtensionSupport.kt b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/ComposeExtensionSupport.kt index affdc4c552..84150ea891 100644 --- a/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/ComposeExtensionSupport.kt +++ b/features/dd-sdk-android-session-replay-compose/src/main/kotlin/com/datadog/android/sessionreplay/compose/ComposeExtensionSupport.kt @@ -52,4 +52,11 @@ class ComposeExtensionSupport : ExtensionSupport { override fun getCustomDrawableMapper(): List { return emptyList() } + + override fun name(): String = + COMPOSE_EXTENSION_SUPPORT_NAME + + internal companion object { + internal const val COMPOSE_EXTENSION_SUPPORT_NAME = "ComposeExtensionSupport" + } } diff --git a/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/ComposeExtensionSupportTest.kt b/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/ComposeExtensionSupportTest.kt index cedaf6e077..55ecd64c87 100644 --- a/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/ComposeExtensionSupportTest.kt +++ b/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/ComposeExtensionSupportTest.kt @@ -43,4 +43,13 @@ class ComposeExtensionSupportTest { val composeMapper = customMappers.firstOrNull { it.supportsView(mockView) }?.getUnsafeMapper() assertThat(composeMapper).isInstanceOf(SemanticsWireframeMapper::class.java) } + + @Test + fun `M return name W name()`() { + // When + val name = testedExtensionSupport.name() + + // Then + assertThat(name).isEqualTo(ComposeExtensionSupport.COMPOSE_EXTENSION_SUPPORT_NAME) + } } diff --git a/features/dd-sdk-android-session-replay-material/api/apiSurface b/features/dd-sdk-android-session-replay-material/api/apiSurface index 51917ca056..8bc5656a0d 100644 --- a/features/dd-sdk-android-session-replay-material/api/apiSurface +++ b/features/dd-sdk-android-session-replay-material/api/apiSurface @@ -2,3 +2,4 @@ class com.datadog.android.sessionreplay.material.MaterialExtensionSupport : com. override fun getCustomViewMappers(): List> override fun getOptionSelectorDetectors(): List override fun getCustomDrawableMapper(): List + override fun name(): String diff --git a/features/dd-sdk-android-session-replay-material/api/dd-sdk-android-session-replay-material.api b/features/dd-sdk-android-session-replay-material/api/dd-sdk-android-session-replay-material.api index 7717fbf85c..5347422127 100644 --- a/features/dd-sdk-android-session-replay-material/api/dd-sdk-android-session-replay-material.api +++ b/features/dd-sdk-android-session-replay-material/api/dd-sdk-android-session-replay-material.api @@ -3,5 +3,6 @@ public final class com/datadog/android/sessionreplay/material/MaterialExtensionS public fun getCustomDrawableMapper ()Ljava/util/List; public fun getCustomViewMappers ()Ljava/util/List; public fun getOptionSelectorDetectors ()Ljava/util/List; + public fun name ()Ljava/lang/String; } diff --git a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupport.kt b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupport.kt index dce978f774..38fd92da73 100644 --- a/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupport.kt +++ b/features/dd-sdk-android-session-replay-material/src/main/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupport.kt @@ -87,4 +87,11 @@ class MaterialExtensionSupport : ExtensionSupport { override fun getCustomDrawableMapper(): List { return listOf(materialDrawableToColorMapper) } + + override fun name(): String = + MATERIAL_EXTENSION_SUPPORT_NAME + + internal companion object { + internal const val MATERIAL_EXTENSION_SUPPORT_NAME = "MaterialExtensionSupport" + } } diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupportTest.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupportTest.kt index 3f5ce9eb9d..4d0ec69f9c 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupportTest.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/MaterialExtensionSupportTest.kt @@ -6,6 +6,7 @@ package com.datadog.android.sessionreplay.material +import com.datadog.android.sessionreplay.material.MaterialExtensionSupport.Companion.MATERIAL_EXTENSION_SUPPORT_NAME import com.datadog.android.sessionreplay.material.internal.MaterialOptionSelectorDetector import com.datadog.android.sessionreplay.material.internal.SliderWireframeMapper import com.datadog.android.sessionreplay.material.internal.TabWireframeMapper @@ -25,7 +26,7 @@ import org.mockito.quality.Strictness @MockitoSettings(strictness = Strictness.LENIENT) class MaterialExtensionSupportTest { - lateinit var testedMaterialExtensionSupport: MaterialExtensionSupport + private lateinit var testedMaterialExtensionSupport: MaterialExtensionSupport @BeforeEach fun `set up`() { @@ -67,4 +68,10 @@ class MaterialExtensionSupportTest { assertThat(customDetectors.size).isEqualTo(1) assertThat(customDetectors[0]).isInstanceOf(MaterialOptionSelectorDetector::class.java) } + + @Test + fun `M return name of extension W name`() { + // Then + assertThat(testedMaterialExtensionSupport.name()).isEqualTo(MATERIAL_EXTENSION_SUPPORT_NAME) + } } diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index 139471f3fd..93f2e1338d 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -1,4 +1,5 @@ interface com.datadog.android.sessionreplay.ExtensionSupport + fun name(): String fun getCustomViewMappers(): List> fun getOptionSelectorDetectors(): List fun getCustomDrawableMapper(): List @@ -10,6 +11,8 @@ data class com.datadog.android.sessionreplay.MapperTypeWrapper, com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper) fun supportsView(android.view.View): Boolean fun getUnsafeMapper(): com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper + override fun equals(Any?): Boolean + override fun hashCode(): Int interface com.datadog.android.sessionreplay.PrivacyLevel fun android.view.View.setSessionReplayHidden(Boolean) fun android.view.View.setSessionReplayImagePrivacy(ImagePrivacy?) diff --git a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api index 201f0fc25c..2ae760309d 100644 --- a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api +++ b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api @@ -2,6 +2,7 @@ public abstract interface class com/datadog/android/sessionreplay/ExtensionSuppo public abstract fun getCustomDrawableMapper ()Ljava/util/List; public abstract fun getCustomViewMappers ()Ljava/util/List; public abstract fun getOptionSelectorDetectors ()Ljava/util/List; + public abstract fun name ()Ljava/lang/String; } public final class com/datadog/android/sessionreplay/ImagePrivacy : java/lang/Enum, com/datadog/android/sessionreplay/PrivacyLevel { @@ -58,8 +59,10 @@ public final class com/datadog/android/sessionreplay/SessionReplayConfiguration$ public synthetic fun (FILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addExtensionSupport (Lcom/datadog/android/sessionreplay/ExtensionSupport;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun build ()Lcom/datadog/android/sessionreplay/SessionReplayConfiguration; + public final fun getLogger ()Lcom/datadog/android/api/InternalLogger; public final fun setDynamicOptimizationEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setImagePrivacy (Lcom/datadog/android/sessionreplay/ImagePrivacy;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; + public final fun setLogger (Lcom/datadog/android/api/InternalLogger;)V public final fun setPrivacy (Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setSystemRequirements (Lcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setTextAndInputPrivacy (Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/ExtensionSupport.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/ExtensionSupport.kt index 1b002e7e48..b9f25a6855 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/ExtensionSupport.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/ExtensionSupport.kt @@ -19,6 +19,12 @@ import com.datadog.android.sessionreplay.utils.DrawableToColorMapper */ interface ExtensionSupport { + /** + * Identifier for the extension. + * @return the name of this extension. + */ + fun name(): String + /** * Use this method if you want to apply a custom [WireframeMapper] for a specific [View]. * @return the list of [MapperTypeWrapper] diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/MapperTypeWrapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/MapperTypeWrapper.kt index bb7895883d..87c920cb2b 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/MapperTypeWrapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/MapperTypeWrapper.kt @@ -38,4 +38,17 @@ data class MapperTypeWrapper( fun getUnsafeMapper(): WireframeMapper { return mapper as WireframeMapper } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MapperTypeWrapper<*> + + return type == other.type + } + + override fun hashCode(): Int { + return type.hashCode() + } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt index 7c7600f55d..0e42f3e79d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt @@ -7,9 +7,10 @@ package com.datadog.android.sessionreplay import androidx.annotation.FloatRange -import com.datadog.android.sessionreplay.internal.NoOpExtensionSupport +import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import java.util.Locale /** * Describes configuration to be used for the Session Replay feature. @@ -35,7 +36,18 @@ data class SessionReplayConfiguration internal constructor( * means no session will be recorded, 100 means all sessions will be recorded. * If this value is not provided then Session Replay will default to a 100 sample rate. */ - class Builder(@FloatRange(from = 0.0, to = 100.0) private val sampleRate: Float = SAMPLE_IN_ALL_SESSIONS) { + @Suppress("TooManyFunctions") + class Builder( + @FloatRange(from = 0.0, to = 100.0) private val sampleRate: Float = SAMPLE_IN_ALL_SESSIONS + ) { + private var logger = InternalLogger.UNBOUND + internal constructor( + @FloatRange(from = 0.0, to = 100.0) sampleRate: Float = SAMPLE_IN_ALL_SESSIONS, + logger: InternalLogger + ) : this(sampleRate) { + this.logger = logger + } + private var customEndpointUrl: String? = null private var privacy = SessionReplayPrivacy.MASK @@ -46,7 +58,7 @@ data class SessionReplayConfiguration internal constructor( private var startRecordingImmediately = true private var touchPrivacy = TouchPrivacy.HIDE private var textAndInputPrivacy = TextAndInputPrivacy.MASK_ALL - private var extensionSupport: ExtensionSupport = NoOpExtensionSupport() + private var extensionSupportSet: MutableSet = mutableSetOf() private var dynamicOptimizationEnabled = true private var systemRequirementsConfiguration = SystemRequirementsConfiguration.NONE @@ -57,7 +69,16 @@ data class SessionReplayConfiguration internal constructor( * @see [ExtensionSupport.getLegacyCustomViewMappers] */ fun addExtensionSupport(extensionSupport: ExtensionSupport): Builder { - this.extensionSupport = extensionSupport + if (this.extensionSupportSet.any { it.name() == extensionSupport.name() }) { + logger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + messageBuilder = { DUPLICATE_EXTENSION_DETECTED.format(Locale.US, extensionSupport.name()) } + ) + } else { + this.extensionSupportSet.add(extensionSupport) + } + return this } @@ -188,8 +209,8 @@ data class SessionReplayConfiguration internal constructor( touchPrivacy = touchPrivacy, textAndInputPrivacy = textAndInputPrivacy, customMappers = customMappers(), - customOptionSelectorDetectors = extensionSupport.getOptionSelectorDetectors(), - customDrawableMappers = extensionSupport.getCustomDrawableMapper(), + customOptionSelectorDetectors = optionsSelectorDetectors(), + customDrawableMappers = customDrawableMappers(), sampleRate = sampleRate, startRecordingImmediately = startRecordingImmediately, dynamicOptimizationEnabled = dynamicOptimizationEnabled, @@ -198,11 +219,32 @@ data class SessionReplayConfiguration internal constructor( } private fun customMappers(): List> { - return extensionSupport.getCustomViewMappers() + val allItems = extensionSupportSet.flatMap { it.getCustomViewMappers() } + + allItems.groupBy { it } + .filter { it.value.size > 1 } + .forEach { (item, _) -> + logger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + messageBuilder = { DUPLICATE_MAPPER_DETECTED.format(Locale.US, item.type) } + ) + } + + return allItems.distinct().toList() } + private fun customDrawableMappers(): List = + extensionSupportSet.flatMap { it.getCustomDrawableMapper() }.toList() + + private fun optionsSelectorDetectors(): List = + extensionSupportSet.flatMap { it.getOptionSelectorDetectors() }.toList() + internal companion object { internal const val SAMPLE_IN_ALL_SESSIONS = 100.0f + internal const val DUPLICATE_EXTENSION_DETECTED = + "Attempting to add support twice for the same extension %s. The duplicate will be ignored." + internal const val DUPLICATE_MAPPER_DETECTED = "Duplicate mapper for %s. The duplicate will be ignored." } } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/NoOpExtensionSupport.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/NoOpExtensionSupport.kt deleted file mode 100644 index 4f623cc1ab..0000000000 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/NoOpExtensionSupport.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.android.sessionreplay.internal - -import com.datadog.android.sessionreplay.ExtensionSupport -import com.datadog.android.sessionreplay.MapperTypeWrapper -import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector -import com.datadog.android.sessionreplay.utils.DrawableToColorMapper - -internal class NoOpExtensionSupport : ExtensionSupport { - - override fun getCustomViewMappers(): List> { - return emptyList() - } - - override fun getOptionSelectorDetectors(): List { - return emptyList() - } - - override fun getCustomDrawableMapper(): List { - return emptyList() - } -} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt index f5217a4c08..6f6f898eee 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt @@ -7,8 +7,17 @@ package com.datadog.android.sessionreplay import android.view.View +import android.widget.ImageView +import android.widget.TextView +import com.datadog.android.api.InternalLogger +import com.datadog.android.sessionreplay.SessionReplayConfiguration.Builder.Companion.DUPLICATE_EXTENSION_DETECTED +import com.datadog.android.sessionreplay.SessionReplayConfiguration.Builder.Companion.DUPLICATE_MAPPER_DETECTED import com.datadog.android.sessionreplay.SessionReplayConfiguration.Builder.Companion.SAMPLE_IN_ALL_SESSIONS import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector +import com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper +import com.datadog.android.sessionreplay.utils.DrawableToColorMapper +import com.datadog.android.sessionreplay.utils.verifyLog import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.FloatForgery import fr.xgouchet.elmyr.annotation.Forgery @@ -26,6 +35,7 @@ import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import java.util.Locale @Extensions( ExtendWith(MockitoExtension::class), @@ -230,4 +240,188 @@ internal class SessionReplayConfigurationBuilderTest { assertThat(sessionReplayConfiguration.touchPrivacy).isEqualTo(TouchPrivacy.HIDE) assertThat(sessionReplayConfiguration.textAndInputPrivacy).isEqualTo(TextAndInputPrivacy.MASK_ALL) } + + @Test + fun `M add custom drawable mappers W addExtensionSupport { multiple calls }`( + @Mock mockFirstExtension: ExtensionSupport, + @Mock mockSecondExtension: ExtensionSupport, + @Mock mockFirstDrawableMapper: DrawableToColorMapper, + @Mock mockSecondDrawableMapper: DrawableToColorMapper + ) { + // Given + whenever(mockFirstExtension.getCustomDrawableMapper()) + .thenReturn(listOf(mockFirstDrawableMapper)) + + whenever(mockSecondExtension.getCustomDrawableMapper()) + .thenReturn(listOf(mockSecondDrawableMapper)) + + whenever(mockFirstExtension.name()).thenReturn("firstExtension") + whenever(mockSecondExtension.name()).thenReturn("secondExtension") + + // When + testedBuilder.addExtensionSupport(mockFirstExtension) + testedBuilder.addExtensionSupport(mockSecondExtension) + val config = testedBuilder.build() + + // Then + assertThat(config.customDrawableMappers).containsAll( + setOf(mockFirstDrawableMapper, mockSecondDrawableMapper) + ) + } + + @Test + fun `M add custom options selector detectors W addExtensionSupport { multiple calls }`( + @Mock mockFirstExtension: ExtensionSupport, + @Mock mockSecondExtension: ExtensionSupport, + @Mock mockFirstOptionSelectorDetector: OptionSelectorDetector, + @Mock mockSecondOptionSelectorDetector: OptionSelectorDetector + ) { + // Given + whenever(mockFirstExtension.getOptionSelectorDetectors()) + .thenReturn(listOf(mockFirstOptionSelectorDetector)) + + whenever(mockSecondExtension.getOptionSelectorDetectors()) + .thenReturn(listOf(mockSecondOptionSelectorDetector)) + + whenever(mockFirstExtension.name()).thenReturn("firstExtension") + whenever(mockSecondExtension.name()).thenReturn("secondExtension") + + // When + testedBuilder.addExtensionSupport(mockFirstExtension) + testedBuilder.addExtensionSupport(mockSecondExtension) + val config = testedBuilder.build() + + // Then + assertThat(config.customOptionSelectorDetectors).containsAll( + setOf(mockFirstOptionSelectorDetector, mockSecondOptionSelectorDetector) + ) + } + + @Test + fun `M add custom view mappers W addExtensionSupport { multiple calls }`( + @Mock mockFirstExtension: ExtensionSupport, + @Mock mockSecondExtension: ExtensionSupport, + @Mock mockFirstMapper: WireframeMapper, + @Mock mockSecondMapper: WireframeMapper + ) { + // Given + val fakeTextMapperTypeWrapper = MapperTypeWrapper( + TextView::class.java, + mockFirstMapper + ) + + val fakeImageMapperTypeWrapper = MapperTypeWrapper( + ImageView::class.java, + mockSecondMapper + ) + + whenever(mockFirstExtension.getCustomViewMappers()) + .thenReturn(listOf(fakeTextMapperTypeWrapper)) + + whenever(mockSecondExtension.getCustomViewMappers()) + .thenReturn(listOf(fakeImageMapperTypeWrapper)) + + whenever(mockFirstExtension.name()).thenReturn("firstExtension") + whenever(mockSecondExtension.name()).thenReturn("secondExtension") + + // When + testedBuilder.addExtensionSupport(mockFirstExtension) + testedBuilder.addExtensionSupport(mockSecondExtension) + val config = testedBuilder.build() + + // Then + assertThat(config.customMappers).containsAll( + setOf(fakeTextMapperTypeWrapper, fakeImageMapperTypeWrapper) + ) + } + + @Test + fun `M take first value W addExtensionSupport { duplicate mappers }`( + @Mock mockFirstExtension: ExtensionSupport, + @Mock mockSecondExtension: ExtensionSupport, + @Mock mockMapper: WireframeMapper + ) { + // Given + val fakeMapperTypeWrapper = MapperTypeWrapper( + ImageView::class.java, + mockMapper + ) + + whenever(mockFirstExtension.getCustomViewMappers()) + .thenReturn(listOf(fakeMapperTypeWrapper)) + + whenever(mockSecondExtension.getCustomViewMappers()) + .thenReturn(listOf(fakeMapperTypeWrapper)) + + // When + testedBuilder.addExtensionSupport(mockFirstExtension) + testedBuilder.addExtensionSupport(mockSecondExtension) + val config = testedBuilder.build() + + // Then + assertThat(config.customMappers).isEqualTo(mockFirstExtension.getCustomViewMappers()) + } + + @Test + fun `M warn W addExtensionSupport { duplicate mappers }`( + @Mock mockFirstExtension: ExtensionSupport, + @Mock mockSecondExtension: ExtensionSupport, + @Mock mockMapper: WireframeMapper, + @Mock mockLogger: InternalLogger + ) { + // Given + testedBuilder = SessionReplayConfiguration.Builder(100f, mockLogger) + val fakeMapperTypeWrapper = MapperTypeWrapper( + ImageView::class.java, + mockMapper + ) + + whenever(mockFirstExtension.getCustomViewMappers()) + .thenReturn(listOf(fakeMapperTypeWrapper)) + whenever(mockFirstExtension.name()).thenReturn("firstExtension") + whenever(mockSecondExtension.name()).thenReturn("secondExtension") + + whenever(mockSecondExtension.getCustomViewMappers()) + .thenReturn(listOf(fakeMapperTypeWrapper)) + + // When + testedBuilder.addExtensionSupport(mockFirstExtension) + testedBuilder.addExtensionSupport(mockSecondExtension) + + testedBuilder.build() + + val expected = String.format(DUPLICATE_MAPPER_DETECTED, ImageView::class.java) + + // Then + mockLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + message = expected + ) + } + + @Test + fun `M warn W addExtensionSupport { duplicate extensions }`( + @Mock mockExtension: ExtensionSupport, + @Mock mockSecondExtension: ExtensionSupport, + @StringForgery fakeName: String, + @Mock mockLogger: InternalLogger + ) { + // Given + testedBuilder = SessionReplayConfiguration.Builder(100f, mockLogger) + whenever(mockExtension.name()).thenReturn(fakeName) + whenever(mockSecondExtension.name()).thenReturn(fakeName) + val expected = String.format(Locale.US, DUPLICATE_EXTENSION_DETECTED, fakeName) + + // When + testedBuilder.addExtensionSupport(mockExtension) + testedBuilder.addExtensionSupport(mockExtension) + + // Then + mockLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + message = expected + ) + } } From 6e09dbfb8cdc38e981c7151ca9ded895396e6961 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Wed, 13 Nov 2024 17:05:41 +0200 Subject: [PATCH 2/2] RUM-7150: Split configuration constructor --- .../api/apiSurface | 1 + .../api/dd-sdk-android-session-replay.api | 2 -- .../SessionReplayConfiguration.kt | 30 +++++++++++++------ .../SessionReplayConfigurationBuilderTest.kt | 15 ++++------ 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index 93f2e1338d..5e7255cb31 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -24,6 +24,7 @@ object com.datadog.android.sessionreplay.SessionReplay fun stopRecording(com.datadog.android.api.SdkCore = Datadog.getInstance()) data class com.datadog.android.sessionreplay.SessionReplayConfiguration class Builder + constructor() constructor(Float = SAMPLE_IN_ALL_SESSIONS) fun addExtensionSupport(ExtensionSupport): Builder fun useCustomEndpoint(String): Builder diff --git a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api index 2ae760309d..bc9e29de31 100644 --- a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api +++ b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api @@ -59,10 +59,8 @@ public final class com/datadog/android/sessionreplay/SessionReplayConfiguration$ public synthetic fun (FILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addExtensionSupport (Lcom/datadog/android/sessionreplay/ExtensionSupport;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun build ()Lcom/datadog/android/sessionreplay/SessionReplayConfiguration; - public final fun getLogger ()Lcom/datadog/android/api/InternalLogger; public final fun setDynamicOptimizationEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setImagePrivacy (Lcom/datadog/android/sessionreplay/ImagePrivacy;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; - public final fun setLogger (Lcom/datadog/android/api/InternalLogger;)V public final fun setPrivacy (Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setSystemRequirements (Lcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setTextAndInputPrivacy (Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt index 0e42f3e79d..303756a218 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt @@ -32,19 +32,31 @@ data class SessionReplayConfiguration internal constructor( /** * A Builder class for a [SessionReplayConfiguration]. - * @param sampleRate must be a value between 0 and 100. A value of 0 - * means no session will be recorded, 100 means all sessions will be recorded. - * If this value is not provided then Session Replay will default to a 100 sample rate. */ @Suppress("TooManyFunctions") - class Builder( - @FloatRange(from = 0.0, to = 100.0) private val sampleRate: Float = SAMPLE_IN_ALL_SESSIONS - ) { - private var logger = InternalLogger.UNBOUND + class Builder { + private val logger: InternalLogger + private val sampleRate: Float + + /** + * Calling this constructor will default to a 100% session sampling rate. + */ + constructor() : this(SAMPLE_IN_ALL_SESSIONS, InternalLogger.UNBOUND) + + /** + * @param sampleRate must be a value between 0 and 100. A value of 0 + * means no session will be recorded, 100 means all sessions will be recorded. + * If this value is not provided then Session Replay will default to a 100 sample rate. + */ + constructor( + @FloatRange(from = 0.0, to = 100.0) sampleRate: Float = SAMPLE_IN_ALL_SESSIONS + ) : this(sampleRate, InternalLogger.UNBOUND) + internal constructor( - @FloatRange(from = 0.0, to = 100.0) sampleRate: Float = SAMPLE_IN_ALL_SESSIONS, + @FloatRange(from = 0.0, to = 100.0) sampleRate: Float, logger: InternalLogger - ) : this(sampleRate) { + ) { + this.sampleRate = sampleRate this.logger = logger } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt index 6f6f898eee..1af44b49cf 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt @@ -264,9 +264,8 @@ internal class SessionReplayConfigurationBuilderTest { val config = testedBuilder.build() // Then - assertThat(config.customDrawableMappers).containsAll( - setOf(mockFirstDrawableMapper, mockSecondDrawableMapper) - ) + assertThat(config.customDrawableMappers) + .containsOnly(mockFirstDrawableMapper, mockSecondDrawableMapper) } @Test @@ -292,9 +291,8 @@ internal class SessionReplayConfigurationBuilderTest { val config = testedBuilder.build() // Then - assertThat(config.customOptionSelectorDetectors).containsAll( - setOf(mockFirstOptionSelectorDetector, mockSecondOptionSelectorDetector) - ) + assertThat(config.customOptionSelectorDetectors) + .containsOnly(mockFirstOptionSelectorDetector, mockSecondOptionSelectorDetector) } @Test @@ -330,9 +328,8 @@ internal class SessionReplayConfigurationBuilderTest { val config = testedBuilder.build() // Then - assertThat(config.customMappers).containsAll( - setOf(fakeTextMapperTypeWrapper, fakeImageMapperTypeWrapper) - ) + assertThat(config.customMappers) + .containsOnly(fakeTextMapperTypeWrapper, fakeImageMapperTypeWrapper) } @Test