Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Masking for Android SR text #574

Merged
merged 2 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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