Skip to content

Commit

Permalink
RUM-6218: Add privacy override for hidden views
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Sep 29, 2024
1 parent 7ba01ef commit 601398b
Show file tree
Hide file tree
Showing 17 changed files with 588 additions and 36 deletions.
8 changes: 5 additions & 3 deletions features/dd-sdk-android-session-replay/api/apiSurface
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
interface com.datadog.android.sessionreplay.ExtensionSupport
fun getCustomViewMappers(): List<MapperTypeWrapper<*>>
fun getOptionSelectorDetectors(): List<com.datadog.android.sessionreplay.recorder.OptionSelectorDetector>
enum com.datadog.android.sessionreplay.ImagePrivacy
enum com.datadog.android.sessionreplay.ImagePrivacy : PrivacyLevel
- MASK_NONE
- MASK_LARGE_ONLY
- MASK_ALL
data class com.datadog.android.sessionreplay.MapperTypeWrapper<T: android.view.View>
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>
interface com.datadog.android.sessionreplay.PrivacyLevel
fun android.view.View.ddSessionReplayOverrideHidden(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 All @@ -28,11 +30,11 @@ enum com.datadog.android.sessionreplay.SessionReplayPrivacy
- ALLOW
- MASK
- MASK_USER_INPUT
enum com.datadog.android.sessionreplay.TextAndInputPrivacy
enum com.datadog.android.sessionreplay.TextAndInputPrivacy : PrivacyLevel
- MASK_SENSITIVE_INPUTS
- MASK_ALL_INPUTS
- MASK_ALL
enum com.datadog.android.sessionreplay.TouchPrivacy
enum com.datadog.android.sessionreplay.TouchPrivacy : PrivacyLevel
- SHOW
- HIDE
data class com.datadog.android.sessionreplay.recorder.MappingContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ public abstract interface class com/datadog/android/sessionreplay/ExtensionSuppo
public abstract fun getOptionSelectorDetectors ()Ljava/util/List;
}

public final class com/datadog/android/sessionreplay/ImagePrivacy : java/lang/Enum {
public final class com/datadog/android/sessionreplay/ImagePrivacy : java/lang/Enum, com/datadog/android/sessionreplay/PrivacyLevel {
public static final field MASK_ALL Lcom/datadog/android/sessionreplay/ImagePrivacy;
public static final field MASK_LARGE_ONLY Lcom/datadog/android/sessionreplay/ImagePrivacy;
public static final field MASK_NONE Lcom/datadog/android/sessionreplay/ImagePrivacy;
Expand All @@ -22,6 +22,13 @@ public final class com/datadog/android/sessionreplay/MapperTypeWrapper {
public fun toString ()Ljava/lang/String;
}

public abstract interface class com/datadog/android/sessionreplay/PrivacyLevel {
}

public final class com/datadog/android/sessionreplay/PrivacyOverrideExtensionsKt {
public static final fun ddSessionReplayOverrideHidden (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 Expand Up @@ -61,15 +68,15 @@ public final class com/datadog/android/sessionreplay/SessionReplayPrivacy : java
public static fun values ()[Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;
}

public final class com/datadog/android/sessionreplay/TextAndInputPrivacy : java/lang/Enum {
public final class com/datadog/android/sessionreplay/TextAndInputPrivacy : java/lang/Enum, com/datadog/android/sessionreplay/PrivacyLevel {
public static final field MASK_ALL Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;
public static final field MASK_ALL_INPUTS Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;
public static final field MASK_SENSITIVE_INPUTS Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;
public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;
public static fun values ()[Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;
}

public final class com/datadog/android/sessionreplay/TouchPrivacy : java/lang/Enum {
public final class com/datadog/android/sessionreplay/TouchPrivacy : java/lang/Enum, com/datadog/android/sessionreplay/PrivacyLevel {
public static final field HIDE Lcom/datadog/android/sessionreplay/TouchPrivacy;
public static final field SHOW Lcom/datadog/android/sessionreplay/TouchPrivacy;
public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/TouchPrivacy;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ package com.datadog.android.sessionreplay
* @see ImagePrivacy.MASK_LARGE_ONLY
* @see ImagePrivacy.MASK_ALL
*/
enum class ImagePrivacy {
enum class ImagePrivacy : PrivacyLevel {
/**
* All images will be recorded, including those downloaded from the Internet during app runtime.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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

/**
* Common interface for Session Replay privacy levels.
* Privacy levels determine the degree of masking applied to sessions.
*/
interface PrivacyLevel
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.internal.PrivacyOverrideManager

/**
* 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.ddSessionReplayOverrideHidden(hide: Boolean) {
if (hide) {
PrivacyOverrideManager.addHiddenOverride(this)
} else {
PrivacyOverrideManager.removeHiddenOverride(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ package com.datadog.android.sessionreplay
* @see TextAndInputPrivacy.MASK_ALL_INPUTS
* @see TextAndInputPrivacy.MASK_ALL
*/
enum class TextAndInputPrivacy {
enum class TextAndInputPrivacy : PrivacyLevel {

/**
* All text and inputs considered sensitive will be masked.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ package com.datadog.android.sessionreplay
* @see TouchPrivacy.SHOW
* @see TouchPrivacy.HIDE
*/
enum class TouchPrivacy {
enum class TouchPrivacy : PrivacyLevel {
/**
* All touch interactions will be recorded.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.ImagePrivacy
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.TouchPrivacy

internal data class PrivacyOverride(
val imagePrivacy: ImagePrivacy? = null,
val touchPrivacy: TouchPrivacy? = null,
val textAndInputPrivacy: TextAndInputPrivacy? = null,
val hiddenPrivacy: Boolean = false
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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 android.view.View
import com.datadog.android.sessionreplay.ImagePrivacy
import com.datadog.android.sessionreplay.PrivacyLevel
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.TouchPrivacy
import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentHashMap

internal object PrivacyOverrideManager {
private val overridesMap = ConcurrentHashMap<WeakViewKey, PrivacyOverride>()

internal fun addPrivacyOverride(view: View, level: PrivacyLevel?) {
val key = WeakViewKey(view)
val existingPrivacy = overridesMap[key] ?: PrivacyOverride()

overridesMap[key] =
when (level) {
is ImagePrivacy -> existingPrivacy.copy(
imagePrivacy = level
)

is TextAndInputPrivacy -> existingPrivacy.copy(
textAndInputPrivacy = level
)

is TouchPrivacy -> existingPrivacy.copy(
touchPrivacy = level
)

else -> return
}
}

internal fun removeTextAndInputPrivacyOverride(view: View) {
val key = WeakViewKey(view)
val existingPrivacy = overridesMap[key] ?: return

overridesMap[key] = existingPrivacy.copy(
textAndInputPrivacy = null
)
}

internal fun removeTouchPrivacyOverride(view: View) {
val key = WeakViewKey(view)
val existingPrivacy = overridesMap[key] ?: return

overridesMap[key] = existingPrivacy.copy(
touchPrivacy = null
)
}

internal fun removeImagePrivacyOverride(view: View) {
val key = WeakViewKey(view)
val existingPrivacy = overridesMap[key] ?: return

overridesMap[key] = existingPrivacy.copy(
imagePrivacy = null
)
}

internal fun addHiddenOverride(view: View) {
val key = WeakViewKey(view)
val existingPrivacy = overridesMap[key] ?: PrivacyOverride()

overridesMap[key] = existingPrivacy.copy(
hiddenPrivacy = true
)
}

internal fun removeHiddenOverride(view: View) {
val key = WeakViewKey(view)
val existingPrivacy = overridesMap[key] ?: return

overridesMap[key] = existingPrivacy.copy(
hiddenPrivacy = false
)
}

internal fun getPrivacyOverrides(view: View): PrivacyOverride? {
val key = WeakViewKey(view)
return overridesMap[key]
}

internal fun isHidden(view: View): Boolean {
val key = WeakViewKey(view)
return overridesMap[key]?.hiddenPrivacy == true
}

private class WeakViewKey(view: View) : WeakReference<View>(view) {
override fun equals(other: Any?): Boolean {
val otherValue = (other as? WeakViewKey)?.get()
return get() == otherValue
}

override fun hashCode(): Int {
return get()?.hashCode() ?: 0
}
}
}
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 @@ -172,6 +173,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.internal.PrivacyOverrideManager
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,37 +54,43 @@ internal class TreeViewTraversal(
// try to resolve from the exhaustive type mappers
var mapper = findMapperForView(view)

if (mapper != null) {
jobStatusCallback = QueueStatusCallback(recordedDataQueueRefs)
traversalStrategy = if (mapper is TraverseAllChildrenMapper) {
TraversalStrategy.TRAVERSE_ALL_CHILDREN
} else {
TraversalStrategy.STOP_AND_RETURN_NODE
}
} else if (isDecorView(view)) {
traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN
mapper = decorViewMapper
jobStatusCallback = noOpCallback
} else if (view is ViewGroup) {
traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN
mapper = defaultViewMapper
jobStatusCallback = noOpCallback
} else {
if (PrivacyOverrideManager.isHidden(view)) {
traversalStrategy = TraversalStrategy.STOP_AND_RETURN_NODE
mapper = defaultViewMapper
mapper = hiddenViewMapper
jobStatusCallback = noOpCallback
val viewType = view.javaClass.canonicalName ?: view.javaClass.name
} else {
if (mapper != null) {
jobStatusCallback = QueueStatusCallback(recordedDataQueueRefs)
traversalStrategy = if (mapper is TraverseAllChildrenMapper) {
TraversalStrategy.TRAVERSE_ALL_CHILDREN
} else {
TraversalStrategy.STOP_AND_RETURN_NODE
}
} else if (isDecorView(view)) {
traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN
mapper = decorViewMapper
jobStatusCallback = noOpCallback
} else if (view is ViewGroup) {
traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN
mapper = defaultViewMapper
jobStatusCallback = noOpCallback
} else {
traversalStrategy = TraversalStrategy.STOP_AND_RETURN_NODE
mapper = defaultViewMapper
jobStatusCallback = noOpCallback
val viewType = view.javaClass.canonicalName ?: view.javaClass.name

internalLogger.log(
level = InternalLogger.Level.INFO,
target = InternalLogger.Target.TELEMETRY,
messageBuilder = { "No mapper found for view $viewType" },
throwable = null,
onlyOnce = true,
additionalProperties = mapOf(
"replay.widget.type" to viewType
internalLogger.log(
level = InternalLogger.Level.INFO,
target = InternalLogger.Target.TELEMETRY,
messageBuilder = { "No mapper found for view $viewType" },
throwable = null,
onlyOnce = true,
additionalProperties = mapOf(
"replay.widget.type" to viewType
)
)
)
}
}

val resolvedWireframes = internalLogger.measureMethodCallPerf(
Expand Down
Loading

0 comments on commit 601398b

Please sign in to comment.