Skip to content

Commit

Permalink
Merge pull request #2291 from DataDog/jmoskovich/rum-6218/implement-h…
Browse files Browse the repository at this point in the history
…idden-privacy-override

RUM-6218: Add privacy override for hidden views
  • Loading branch information
jonathanmos authored Oct 14, 2024
2 parents 4b83197 + f16a5d4 commit 25f9a39
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 1 deletion.
1 change: 1 addition & 0 deletions features/dd-sdk-android-session-replay/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ data class com.datadog.android.sessionreplay.MapperTypeWrapper<T: android.view.V
constructor(Class<T>, com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper<T>)
fun supportsView(android.view.View): Boolean
fun getUnsafeMapper(): com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper<android.view.View>
fun android.view.View.setSessionReplayHidden(Boolean)
object com.datadog.android.sessionreplay.SessionReplay
fun enable(SessionReplayConfiguration, com.datadog.android.api.SdkCore = Datadog.getInstance())
fun startRecording(com.datadog.android.api.SdkCore = Datadog.getInstance())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public final class com/datadog/android/sessionreplay/MapperTypeWrapper {
public fun toString ()Ljava/lang/String;
}

public final class com/datadog/android/sessionreplay/PrivacyOverrideExtensionsKt {
public static final fun setSessionReplayHidden (Landroid/view/View;Z)V
}

public final class com/datadog/android/sessionreplay/SessionReplay {
public static final field INSTANCE Lcom/datadog/android/sessionreplay/SessionReplay;
public static final fun enable (Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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

import android.view.View

/**
* Allows setting a view to be "hidden" in the hierarchy in Session Replay.
* When hidden the view will be replaced with a placeholder in the replay and
* no attempt will be made to record it's children.
*
* @param hide pass `true` to hide the view, or `false` to remove the override
*/
fun View.setSessionReplayHidden(hide: Boolean) {
if (hide) {
this.setTag(R.id.datadog_hidden, true)
} else {
this.setTag(R.id.datadog_hidden, null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.datadog.android.sessionreplay.internal.processor.RecordedDataProcesso
import com.datadog.android.sessionreplay.internal.processor.RumContextDataHandler
import com.datadog.android.sessionreplay.internal.recorder.callback.OnWindowRefreshedCallback
import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.HiddenViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper
import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapCachesManager
import com.datadog.android.sessionreplay.internal.recorder.resources.BitmapPool
Expand Down Expand Up @@ -173,6 +174,10 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
mappers = mappers,
defaultViewMapper = defaultVWM,
decorViewMapper = DecorViewMapper(defaultVWM, viewIdentifierResolver),
hiddenViewMapper = HiddenViewMapper(
viewBoundsResolver = viewBoundsResolver,
viewIdentifierResolver = viewIdentifierResolver
),
viewUtilsInternal = ViewUtilsInternal(),
internalLogger = internalLogger
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import com.datadog.android.api.InternalLogger
import com.datadog.android.api.feature.measureMethodCallPerf
import com.datadog.android.core.metrics.MethodCallSamplingRate
import com.datadog.android.sessionreplay.MapperTypeWrapper
import com.datadog.android.sessionreplay.R
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
import com.datadog.android.sessionreplay.internal.recorder.mapper.HiddenViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.QueueStatusCallback
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.recorder.MappingContext
Expand All @@ -25,6 +27,7 @@ import com.datadog.android.sessionreplay.utils.NoOpAsyncJobStatusCallback
internal class TreeViewTraversal(
private val mappers: List<MapperTypeWrapper<*>>,
private val defaultViewMapper: WireframeMapper<View>,
private val hiddenViewMapper: HiddenViewMapper,
private val decorViewMapper: WireframeMapper<View>,
private val viewUtilsInternal: ViewUtilsInternal,
private val internalLogger: InternalLogger
Expand All @@ -51,7 +54,11 @@ internal class TreeViewTraversal(
// try to resolve from the exhaustive type mappers
var mapper = findMapperForView(view)

if (mapper != null) {
if (isHidden(view)) {
traversalStrategy = TraversalStrategy.STOP_AND_RETURN_NODE
mapper = hiddenViewMapper
jobStatusCallback = noOpCallback
} else if (mapper != null) {
jobStatusCallback = QueueStatusCallback(recordedDataQueueRefs)
traversalStrategy = if (mapper is TraverseAllChildrenMapper) {
TraversalStrategy.TRAVERSE_ALL_CHILDREN
Expand Down Expand Up @@ -105,6 +112,9 @@ internal class TreeViewTraversal(
return mappers.firstOrNull { it.supportsView(view) }?.getUnsafeMapper()
}

private fun isHidden(view: View): Boolean =
view.getTag(R.id.datadog_hidden) == true

data class TraversedTreeView(
val mappedWireframes: List<MobileSegment.Wireframe>,
val nextActionStrategy: TraversalStrategy
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.recorder.mapper

import android.view.View
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.recorder.MappingContext
import com.datadog.android.sessionreplay.recorder.mapper.WireframeMapper
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.ViewBoundsResolver
import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver

internal class HiddenViewMapper(
val viewIdentifierResolver: ViewIdentifierResolver,
val viewBoundsResolver: ViewBoundsResolver
) : WireframeMapper<View> {
override fun map(
view: View,
mappingContext: MappingContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
internalLogger: InternalLogger
): List<MobileSegment.Wireframe> {
val id = viewIdentifierResolver.resolveChildUniqueIdentifier(view, HIDDEN_KEY_NAME)
?: return emptyList()

val density = mappingContext.systemInformation.screenDensity
val viewGlobalBounds = viewBoundsResolver.resolveViewGlobalBounds(view, density)

return listOf(
MobileSegment.Wireframe.PlaceholderWireframe(
id = id,
x = viewGlobalBounds.x,
y = viewGlobalBounds.y,
width = viewGlobalBounds.width,
height = viewGlobalBounds.height,
label = HIDDEN_VIEW_PLACEHOLDER_TEXT
)
)
}

internal companion object {
internal const val HIDDEN_VIEW_PLACEHOLDER_TEXT = "Hidden"
private const val HIDDEN_KEY_NAME = "hidden"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->

<resources>
<item name="datadog_hidden" type="id"/>
</resources>
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.android.sessionreplay

import android.view.View
import com.datadog.android.sessionreplay.forge.ForgeConfigurator
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.Extensions
import org.mockito.Mockito.mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.kotlin.eq
import org.mockito.kotlin.isNull
import org.mockito.kotlin.verify
import org.mockito.quality.Strictness

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(ForgeConfigurator::class)
internal class PrivacyOverrideExtensionsTest {

@Test
fun `M set tag W setSessionReplayHidden() { hide is true }`() {
// Given
val mockView = mock<View>()

// When
mockView.setSessionReplayHidden(true)

// Then
verify(mockView).setTag(eq(R.id.datadog_hidden), eq(true))
}

@Test
fun `M set tag to null W setSessionReplayHidden() { hide is false }`() {
// Given
val mockView = mock<View>()

// When
mockView.setSessionReplayHidden(false)

// Then
verify(mockView).setTag(eq(R.id.datadog_hidden), isNull())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.datadog.android.sessionreplay.forge.ForgeConfigurator
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
import com.datadog.android.sessionreplay.internal.recorder.TreeViewTraversal.Companion.METHOD_CALL_MAP_PREFIX
import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.HiddenViewMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.recorder.MappingContext
Expand Down Expand Up @@ -68,6 +69,9 @@ internal class TreeViewTraversalTest {
@Mock
lateinit var mockDecorViewMapper: DecorViewMapper

@Mock
lateinit var mockHiddenViewMapper: HiddenViewMapper

@Mock
lateinit var mockViewUtilsInternal: ViewUtilsInternal

Expand All @@ -84,6 +88,7 @@ internal class TreeViewTraversalTest {
testedTreeViewTraversal = TreeViewTraversal(
emptyList(),
mockDefaultViewMapper,
mockHiddenViewMapper,
mockDecorViewMapper,
mockViewUtilsInternal,
mockInternalLogger
Expand Down Expand Up @@ -123,6 +128,7 @@ internal class TreeViewTraversalTest {
testedTreeViewTraversal = TreeViewTraversal(
fakeTypeMapperWrappers,
mockDefaultViewMapper,
mockHiddenViewMapper,
mockDecorViewMapper,
mockViewUtilsInternal,
mockInternalLogger
Expand Down Expand Up @@ -156,6 +162,7 @@ internal class TreeViewTraversalTest {
testedTreeViewTraversal = TreeViewTraversal(
emptyList(),
mockDefaultViewMapper,
mockHiddenViewMapper,
mockDecorViewMapper,
mockViewUtilsInternal,
mockInternalLogger
Expand Down Expand Up @@ -190,6 +197,7 @@ internal class TreeViewTraversalTest {
testedTreeViewTraversal = TreeViewTraversal(
emptyList(),
mockDefaultViewMapper,
mockHiddenViewMapper,
mockDecorViewMapper,
mockViewUtilsInternal,
mockInternalLogger
Expand Down Expand Up @@ -239,6 +247,7 @@ internal class TreeViewTraversalTest {
testedTreeViewTraversal = TreeViewTraversal(
fakeTypeMapperWrappers,
mockDefaultViewMapper,
mockHiddenViewMapper,
mockDecorViewMapper,
mockViewUtilsInternal,
mockInternalLogger
Expand Down Expand Up @@ -272,6 +281,7 @@ internal class TreeViewTraversalTest {
testedTreeViewTraversal = TreeViewTraversal(
emptyList(),
mockDefaultViewMapper,
mockHiddenViewMapper,
mockDecorViewMapper,
mockViewUtilsInternal,
mockInternalLogger
Expand Down Expand Up @@ -306,6 +316,7 @@ internal class TreeViewTraversalTest {
testedTreeViewTraversal = TreeViewTraversal(
emptyList(),
mockDefaultViewMapper,
mockHiddenViewMapper,
mockDecorViewMapper,
mockViewUtilsInternal,
mockInternalLogger
Expand Down Expand Up @@ -442,6 +453,7 @@ internal class TreeViewTraversalTest {
testedTreeViewTraversal = TreeViewTraversal(
listOf(mockMapper),
mockDefaultViewMapper,
mockHiddenViewMapper,
mockDecorViewMapper,
mockViewUtilsInternal,
mockInternalLogger
Expand Down
Loading

0 comments on commit 25f9a39

Please sign in to comment.