Skip to content

Commit

Permalink
Merge pull request #574 from DataDog/jmoskovich/rum-1817/android-masking
Browse files Browse the repository at this point in the history
Implement Masking for Android SR text
  • Loading branch information
jonathanmos authored Dec 26, 2023
2 parents 13c2ee0 + 39ee8f7 commit a974ba0
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 95 deletions.
8 changes: 4 additions & 4 deletions packages/core/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,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"
implementation "com.datadoghq:dd-sdk-android-logs:2.4.0"
implementation "com.datadoghq:dd-sdk-android-trace:2.4.0"
implementation "com.datadoghq:dd-sdk-android-webview:2.4.0"
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
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ internal class ActionEventForgeryFactory :
type = ActionEvent.ActionEventSessionType.USER,
hasReplay = forge.aNullable { aBool() }
),
source = forge.aNullable { aValueFrom(ActionEvent.Source::class.java) },
source = forge.aNullable { aValueFrom(ActionEvent.ActionEventSource::class.java) },
ciTest = forge.aNullable {
ActionEvent.CiTest(anHexadecimalString())
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ internal class ResourceEventForgeryFactory :
type = ResourceEvent.ResourceEventSessionType.USER,
hasReplay = forge.aNullable { aBool() }
),
source = forge.aNullable { aValueFrom(ResourceEvent.Source::class.java) },
source = forge.aNullable { aValueFrom(ResourceEvent.ResourceEventSource::class.java) },
ciTest = forge.aNullable {
ResourceEvent.CiTest(anHexadecimalString())
},
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-session-replay/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,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"

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,
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> {
val wireframes = super.map(view, mappingContext, asyncJobStatusCallback)
return textViewUtils.mapTextViewToWireframes(
wireframes = wireframes,
view = view,
mappingContext = mappingContext,
reactTextPropertiesResolver = reactTextPropertiesResolver
)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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> {
val wireframes = super.map(view, mappingContext, asyncJobStatusCallback)
return textViewUtils.mapTextViewToWireframes(
wireframes = wireframes,
view = view,
mappingContext = mappingContext,
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)
val wireframes = super.map(view, mappingContext, asyncJobStatusCallback)
return textViewUtils.mapTextViewToWireframes(
wireframes = wireframes,
view = view,
mappingContext = mappingContext,
reactTextPropertiesResolver = reactTextPropertiesResolver
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
*
* * 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 com.datadog.android.sessionreplay.internal.recorder.MappingContext
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver

internal class TextViewUtils {
internal fun mapTextViewToWireframes(
wireframes: List<MobileSegment.Wireframe>,
view: TextView,
mappingContext: MappingContext,
reactTextPropertiesResolver: TextPropertiesResolver
): List<MobileSegment.Wireframe> {
val result = mutableListOf<MobileSegment.Wireframe>()
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
}
}
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 a974ba0

Please sign in to comment.