Skip to content

Commit

Permalink
Implement masking for Android SR ReactTextView and ReactEditText
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Dec 21, 2023
1 parent 7cc8400 commit 5d0c7e1
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 79 deletions.
11 changes: 7 additions & 4 deletions packages/core/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ android {
}

repositories {
maven{
url "https://oss.sonatype.org/content/repositories/snapshots/"
}
mavenCentral()
google()
maven { url "https://jitpack.io" }
Expand Down Expand Up @@ -171,10 +174,10 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compileOnly "com.squareup.okhttp3:okhttp:3.12.13"

implementation "com.datadoghq:dd-sdk-android-rum:2.3.0"
implementation "com.datadoghq:dd-sdk-android-logs:2.3.0"
implementation "com.datadoghq:dd-sdk-android-trace:2.3.0"
implementation "com.datadoghq:dd-sdk-android-webview:2.3.0"
implementation "com.datadoghq:dd-sdk-android-rum:2.4.0-SNAPSHOT"
implementation "com.datadoghq:dd-sdk-android-logs:2.4.0-SNAPSHOT"
implementation "com.datadoghq:dd-sdk-android-trace:2.4.0-SNAPSHOT"
implementation "com.datadoghq:dd-sdk-android-webview:2.4.0-SNAPSHOT"
testImplementation "org.junit.platform:junit-platform-launcher:1.6.2"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2"
testImplementation "org.junit.jupiter:junit-jupiter-engine:5.6.2"
Expand Down
5 changes: 4 additions & 1 deletion packages/react-native-session-replay/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ android {
}

repositories {
maven{
url "https://oss.sonatype.org/content/repositories/snapshots/"
}
mavenCentral()
google()
maven { url "https://jitpack.io" }
Expand Down Expand Up @@ -169,7 +172,7 @@ dependencies {
api "com.facebook.react:react-android:$reactNativeVersion"
}
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.datadoghq:dd-sdk-android-session-replay:2.3.0"
implementation "com.datadoghq:dd-sdk-android-session-replay:2.4.0-SNAPSHOT"

testImplementation "org.junit.platform:junit-platform-launcher:1.6.2"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.datadog.android.sessionreplay.ExtensionSupport
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.android.sessionreplay.internal.recorder.OptionSelectorDetector
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
import com.datadog.reactnative.sessionreplay.mappers.ReactMaskInputTextMapper
import com.datadog.reactnative.sessionreplay.mappers.ReactMaskTextMapper
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
import com.facebook.react.bridge.ReactContext
Expand All @@ -34,10 +36,20 @@ internal class ReactNativeSessionReplayExtensionSupport(
ReactViewGroup::class.java to ReactViewGroupMapper(),
ReactTextView::class.java to ReactTextMapper(reactContext, uiManagerModule),
ReactEditText::class.java to ReactTextMapper(reactContext, uiManagerModule)
).mapValues{
it.value as WireframeMapper<View, *>
}
)
),
SessionReplayPrivacy.MASK to mapOf(
ReactViewGroup::class.java to ReactViewGroupMapper(),
ReactTextView::class.java to ReactMaskTextMapper(reactContext, uiManagerModule),
ReactEditText::class.java to ReactMaskTextMapper(reactContext, uiManagerModule)
),
SessionReplayPrivacy.MASK_USER_INPUT to mapOf(
ReactViewGroup::class.java to ReactViewGroupMapper(),
ReactTextView::class.java to ReactMaskInputTextMapper(reactContext, uiManagerModule),
ReactEditText::class.java to ReactMaskInputTextMapper(reactContext, uiManagerModule)
)
).mapValues {
it.value as Map<Class<*>, WireframeMapper<View, *>>
}
}

override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.mappers

import android.widget.TextView
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskInputTextViewMapper
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule

internal class ReactMaskInputTextMapper(
private val reactTextPropertiesResolver: TextPropertiesResolver =
NoopTextPropertiesResolver(),
private val textViewUtils: TextViewUtils = TextViewUtils()
): MaskInputTextViewMapper() {

internal constructor(
reactContext: ReactContext,
uiManagerModule: UIManagerModule?
): this(
reactTextPropertiesResolver = if (uiManagerModule == null) {
NoopTextPropertiesResolver()
} else {
ReactTextPropertiesResolver(
reactContext = reactContext,
uiManagerModule = uiManagerModule
)
}
)

override fun map(
view: TextView,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): List<MobileSegment.Wireframe> {
return textViewUtils.mapTextViewToWireframes(
mapper = this,
view = view,
mappingContext = mappingContext,
asyncJobStatusCallback = asyncJobStatusCallback,
reactTextPropertiesResolver = reactTextPropertiesResolver
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.mappers

import android.widget.TextView
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskTextViewMapper
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule

internal class ReactMaskTextMapper(
private val reactTextPropertiesResolver: TextPropertiesResolver =
NoopTextPropertiesResolver(),
private val textViewUtils: TextViewUtils = TextViewUtils()
): MaskTextViewMapper() {

internal constructor(
reactContext: ReactContext,
uiManagerModule: UIManagerModule?
): this(
reactTextPropertiesResolver = if (uiManagerModule == null) {
NoopTextPropertiesResolver()
} else {
ReactTextPropertiesResolver(
reactContext = reactContext,
uiManagerModule = uiManagerModule
)
}
)

override fun map(
view: TextView,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): List<MobileSegment.Wireframe> {
return textViewUtils.mapTextViewToWireframes(
mapper = this,
view = view,
mappingContext = mappingContext,
asyncJobStatusCallback = asyncJobStatusCallback,
reactTextPropertiesResolver = reactTextPropertiesResolver
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@
package com.datadog.reactnative.sessionreplay.mappers

import android.widget.TextView
import androidx.annotation.VisibleForTesting
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerModule

internal class ReactTextMapper(
private val reactTextPropertiesResolver: TextPropertiesResolver =
NoopTextPropertiesResolver()
NoopTextPropertiesResolver(),
private val textViewUtils: TextViewUtils = TextViewUtils()
): TextViewMapper() {

internal constructor(
Expand All @@ -42,31 +43,12 @@ internal class ReactTextMapper(
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): List<MobileSegment.Wireframe> {
val result = mutableListOf<MobileSegment.Wireframe>()
val wireframes = mapOnSuperclass(view, mappingContext, asyncJobStatusCallback)
val pixelDensity = mappingContext.systemInformation.screenDensity

wireframes.forEach { originalWireframe ->
if (originalWireframe !is MobileSegment.Wireframe.TextWireframe) {
result.add(originalWireframe)
} else {
result.add(reactTextPropertiesResolver.addReactNativeProperties(
originalWireframe = originalWireframe,
view = view,
pixelDensity = pixelDensity,
))
}
}

return result
}

@VisibleForTesting
internal fun mapOnSuperclass(
textView: TextView,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): List<MobileSegment.Wireframe> {
return super.map(textView, mappingContext, asyncJobStatusCallback)
return textViewUtils.mapTextViewToWireframes(
mapper = this,
view = view,
mappingContext = mappingContext,
asyncJobStatusCallback = asyncJobStatusCallback,
reactTextPropertiesResolver = reactTextPropertiesResolver
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
*
* * 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.utils

import android.widget.TextView
import androidx.annotation.VisibleForTesting
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver

internal class TextViewUtils {
internal fun mapTextViewToWireframes(
mapper: WireframeMapper<TextView, MobileSegment.Wireframe>,
view: TextView,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
reactTextPropertiesResolver: TextPropertiesResolver
): List<MobileSegment.Wireframe> {
val result = mutableListOf<MobileSegment.Wireframe>()
val pixelDensity = mappingContext.systemInformation.screenDensity

val wireframes = mapOnSuperclass(
mapper = mapper,
textView = view,
mappingContext = mappingContext,
asyncJobStatusCallback = asyncJobStatusCallback
)

wireframes.forEach { originalWireframe ->
if (originalWireframe !is MobileSegment.Wireframe.TextWireframe) {
result.add(originalWireframe)
} else {
result.add(reactTextPropertiesResolver.addReactNativeProperties(
originalWireframe = originalWireframe,
view = view,
pixelDensity = pixelDensity,
))
}
}

return wireframes
}

@VisibleForTesting
internal fun mapOnSuperclass(
mapper: WireframeMapper<TextView, MobileSegment.Wireframe>,
textView: TextView,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): List<MobileSegment.Wireframe> {
return mapper.map(textView, mappingContext, asyncJobStatusCallback)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package com.datadog.reactnative.sessionreplay

import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.reactnative.sessionreplay.mappers.ReactMaskInputTextMapper
import com.datadog.reactnative.sessionreplay.mappers.ReactMaskTextMapper
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
import com.facebook.react.bridge.NativeModule
Expand Down Expand Up @@ -76,6 +78,40 @@ internal class ReactNativeSessionReplayExtensionSupportTest {
.isInstanceOf(ReactTextMapper::class.java)
}

@Test
fun `M get mask input mappers W getCustomViewMappers()`() {
// When
val customViewMappers = testedExtensionSupport.getCustomViewMappers()
val maskUserInputMappers = customViewMappers[SessionReplayPrivacy.MASK_USER_INPUT]

// Then
check(maskUserInputMappers != null)
assertThat(maskUserInputMappers).hasSize(3)
assertThat(maskUserInputMappers[ReactViewGroup::class.java])
.isInstanceOf(ReactViewGroupMapper::class.java)
assertThat(maskUserInputMappers[ReactTextView::class.java])
.isInstanceOf(ReactMaskInputTextMapper::class.java)
assertThat(maskUserInputMappers[ReactEditText::class.java])
.isInstanceOf(ReactMaskInputTextMapper::class.java)
}

@Test
fun `M get mask mappers W getCustomViewMappers()`() {
// When
val customViewMappers = testedExtensionSupport.getCustomViewMappers()
val maskMappers = customViewMappers[SessionReplayPrivacy.MASK]

// Then
check(maskMappers != null)
assertThat(maskMappers).hasSize(3)
assertThat(maskMappers[ReactViewGroup::class.java])
.isInstanceOf(ReactViewGroupMapper::class.java)
assertThat(maskMappers[ReactTextView::class.java])
.isInstanceOf(ReactMaskTextMapper::class.java)
assertThat(maskMappers[ReactEditText::class.java])
.isInstanceOf(ReactMaskTextMapper::class.java)
}

@Test
fun `M return null W getUiManagerModule() { cannot get uiManagerModule }`() {
// Given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.reactnative.sessionreplay
package com.datadog.reactnative.sessionreplay.utils

import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.reactnative.sessionreplay
package com.datadog.reactnative.sessionreplay.utils

import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.LayerDrawable
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
import com.facebook.react.views.view.ReactViewBackgroundDrawable
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions.assertThat
Expand Down
Loading

0 comments on commit 5d0c7e1

Please sign in to comment.