Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add custom redaction logic
  • Loading branch information
romtsn committed Sep 12, 2024
commit 3d5e837b07457748087b9f969f4322c3c60c51f0
Original file line number Diff line number Diff line change
Expand Up @@ -409,22 +409,12 @@ static void applyMetadata(
options
.getExperimental()
.getSessionReplay()
.setRedactAllText(
readBool(
metadata,
logger,
REPLAYS_REDACT_ALL_TEXT,
options.getExperimental().getSessionReplay().getRedactAllText()));
.setRedactAllText(readBool(metadata, logger, REPLAYS_REDACT_ALL_TEXT, true));

options
.getExperimental()
.getSessionReplay()
.setRedactAllImages(
readBool(
metadata,
logger,
REPLAYS_REDACT_ALL_IMAGES,
options.getExperimental().getSessionReplay().getRedactAllImages()));
.setRedactAllImages(readBool(metadata, logger, REPLAYS_REDACT_ALL_IMAGES, true));
}

options
Expand Down
12 changes: 12 additions & 0 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ public final class io/sentry/android/replay/ScreenshotRecorderConfig$Companion {
public final fun from (Landroid/content/Context;Lio/sentry/SentryReplayOptions;)Lio/sentry/android/replay/ScreenshotRecorderConfig;
}

public final class io/sentry/android/replay/SessionReplayOptionsKt {
public static final fun getRedactAllImages (Lio/sentry/SentryReplayOptions;)Z
public static final fun getRedactAllText (Lio/sentry/SentryReplayOptions;)Z
public static final fun setRedactAllImages (Lio/sentry/SentryReplayOptions;Z)V
public static final fun setRedactAllText (Lio/sentry/SentryReplayOptions;Z)V
}

public final class io/sentry/android/replay/ViewExtensionsKt {
public static final fun sentryReplayIgnore (Landroid/view/View;)V
public static final fun sentryReplayRedact (Landroid/view/View;)V
}

public final class io/sentry/android/replay/gestures/GestureRecorder : io/sentry/android/replay/OnRootViewsChangedListener {
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/android/replay/gestures/TouchRecorderCallback;)V
public fun onRootViewsChanged (Landroid/view/View;Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public class ReplayCache(
if (replayCacheDir == null || bitmap.isRecycled) {
return
}
replayCacheDir?.mkdirs()

val screenshot = File(replayCacheDir, "$frameTimestamp.jpg").also {
it.createNewFile()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.sentry.android.replay

import io.sentry.SentryReplayOptions

// since we don't have getters for redactAllText and redactAllImages, they won't be accessible as
// properties in Kotlin, therefore we create these extensions where a getter is dummy, but a setter
// delegates to the corresponding method in SentryReplayOptions

/**
* Redact all text content. Draws a rectangle of text bounds with text color on top. By default
* only views extending TextView are redacted.
*
* <p>Default is enabled.
*/
var SentryReplayOptions.redactAllText: Boolean
@Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR)
get() = error("Getter not supported")
Comment on lines +16 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Why is this deprecated?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explained in a comment on top of the file, but basically we scream to the dev that this should not be used to get the value. This extension only exists to set the value conveniently in Kotlin (redactAllText = true instead of setRedactAllText(true)), so only the setter matters here

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!! I didn't get the comment.

Can you just remove get() as in C# where you can have a property with just a setter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, unfortunately not possible in Kotlin. It's possible to only have a getter without a setter, but not the other way around

set(value) = setRedactAllText(value)

/**
* Redact all image content. Draws a rectangle of image bounds with image's dominant color on top.
* By default only views extending ImageView with BitmapDrawable or custom Drawable type are
* redacted. ColorDrawable, InsetDrawable, VectorDrawable are all considered non-PII, as they come
* from the apk.
*
* <p>Default is enabled.
*/
var SentryReplayOptions.redactAllImages: Boolean
@Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR)
get() = error("Getter not supported")
set(value) = setRedactAllImages(value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.sentry.android.replay

import android.view.View

/**
* Marks this view to be redacted in session replay.
*/
fun View.sentryReplayRedact() {
setTag(R.id.sentry_privacy, "redact")
}

/**
* Marks this view to be ignored from redaction in session.
* All its content will be visible in the replay, use with caution.
*/
fun View.sentryReplayIgnore() {
setTag(R.id.sentry_privacy, "ignore")
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ internal fun interface OnRootViewsChangedListener {
/**
* A utility that holds the list of root views that WindowManager updates.
*/
internal class RootViewsSpy private constructor() {
internal object RootViewsSpy {

val listeners: CopyOnWriteArrayList<OnRootViewsChangedListener> = object : CopyOnWriteArrayList<OnRootViewsChangedListener>() {
override fun add(element: OnRootViewsChangedListener?): Boolean {
Expand Down Expand Up @@ -168,15 +168,13 @@ internal class RootViewsSpy private constructor() {
}
}

companion object {
fun install(): RootViewsSpy {
return RootViewsSpy().apply {
// had to do this as a first message of the main thread queue, otherwise if this is
// called from ContentProvider, it might be too early and the listener won't be installed
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
WindowManagerSpy.swapWindowManagerGlobalMViews { mViews ->
delegatingViewList.apply { addAll(mViews) }
}
fun install(): RootViewsSpy {
return apply {
// had to do this as a first message of the main thread queue, otherwise if this is
// called from ContentProvider, it might be too early and the listener won't be installed
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
WindowManagerSpy.swapWindowManagerGlobalMViews { mViews ->
delegatingViewList.apply { addAll(mViews) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.view.View
import android.widget.ImageView
import android.widget.TextView
import io.sentry.SentryOptions
import io.sentry.android.replay.R
import io.sentry.android.replay.util.dominantTextColor
import io.sentry.android.replay.util.isRedactable
import io.sentry.android.replay.util.isVisibleToUser
Expand Down Expand Up @@ -234,14 +235,38 @@ sealed class ViewHierarchyNode(
}
}

private fun shouldRedact(view: View, options: SentryOptions): Boolean {
return options.experimental.sessionReplay.redactClasses.contains(view.javaClass.canonicalName)
private const val SENTRY_IGNORE_TAG = "sentry-ignore"
private const val SENTRY_REDACT_TAG = "sentry-redact"

private fun Class<*>.isAssignableFrom(set: Set<String>): Boolean {
var cls: Class<*>? = this
while (cls != null) {
val canonicalName = cls.canonicalName
if (canonicalName != null && set.contains(canonicalName)) {
return true
}
cls = cls.superclass
}
return false
}

private fun View.shouldIgnore(options: SentryOptions): Boolean {
return (tag as? String)?.lowercase()?.contains(SENTRY_IGNORE_TAG) == true ||
getTag(R.id.sentry_privacy) == "ignore" ||
this.javaClass.isAssignableFrom(options.experimental.sessionReplay.ignoreClasses)
}

private fun View.shouldRedact(options: SentryOptions): Boolean {
return (tag as? String)?.lowercase()?.contains(SENTRY_REDACT_TAG) == true ||
getTag(R.id.sentry_privacy) == "redact" ||
this.javaClass.isAssignableFrom(options.experimental.sessionReplay.redactClasses)
}

fun fromView(view: View, parent: ViewHierarchyNode?, distance: Int, options: SentryOptions): ViewHierarchyNode {
val (isVisible, visibleRect) = view.isVisibleToUser()
when {
view is TextView && options.experimental.sessionReplay.redactAllText -> {
val shouldRedact = isVisible && !view.shouldIgnore(options) && view.shouldRedact(options)
when (view) {
is TextView -> {
parent.setImportantForCaptureToAncestors(true)
return TextViewHierarchyNode(
layout = view.layout,
Expand All @@ -253,7 +278,7 @@ sealed class ViewHierarchyNode(
width = view.width,
height = view.height,
elevation = (parent?.elevation ?: 0f) + view.elevation,
shouldRedact = isVisible,
shouldRedact = shouldRedact,
distance = distance,
parent = parent,
isImportantForContentCapture = true,
Expand All @@ -262,7 +287,7 @@ sealed class ViewHierarchyNode(
)
}

view is ImageView && options.experimental.sessionReplay.redactAllImages -> {
is ImageView -> {
parent.setImportantForCaptureToAncestors(true)
return ImageViewHierarchyNode(
x = view.x,
Expand All @@ -274,7 +299,7 @@ sealed class ViewHierarchyNode(
parent = parent,
isVisible = isVisible,
isImportantForContentCapture = true,
shouldRedact = isVisible && view.drawable?.isRedactable() == true,
shouldRedact = shouldRedact && view.drawable?.isRedactable() == true,
visibleRect = visibleRect
)
}
Expand All @@ -288,7 +313,7 @@ sealed class ViewHierarchyNode(
(parent?.elevation ?: 0f) + view.elevation,
distance = distance,
parent = parent,
shouldRedact = isVisible && shouldRedact(view, options),
shouldRedact = shouldRedact,
isImportantForContentCapture = false, /* will be set by children */
isVisible = isVisible,
visibleRect = visibleRect
Expand Down
4 changes: 0 additions & 4 deletions sentry-android-replay/src/main/res/public.xml

This file was deleted.

5 changes: 5 additions & 0 deletions sentry-android-replay/src/main/res/values/public.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<public name="sentry_privacy" type="id"/>
<item name="sentry_privacy" type="id" format="string"/>
</resources>
6 changes: 3 additions & 3 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -2701,13 +2701,13 @@ public final class io/sentry/SentryReplayEvent$ReplayType$Deserializer : io/sent
public final class io/sentry/SentryReplayOptions {
public fun <init> ()V
public fun <init> (Ljava/lang/Double;Ljava/lang/Double;)V
public fun addClassToRedact (Ljava/lang/String;)V
public fun addIgnoreClass (Ljava/lang/String;)V
public fun addRedactClass (Ljava/lang/String;)V
public fun getErrorReplayDuration ()J
public fun getFrameRate ()I
public fun getIgnoreClasses ()Ljava/util/Set;
public fun getOnErrorSampleRate ()Ljava/lang/Double;
public fun getQuality ()Lio/sentry/SentryReplayOptions$SentryReplayQuality;
public fun getRedactAllImages ()Z
public fun getRedactAllText ()Z
public fun getRedactClasses ()Ljava/util/Set;
public fun getSessionDuration ()J
public fun getSessionSampleRate ()Ljava/lang/Double;
Expand Down
49 changes: 0 additions & 49 deletions sentry/src/main/java/io/sentry/ReplayApi.java

This file was deleted.

7 changes: 0 additions & 7 deletions sentry/src/main/java/io/sentry/Sentry.java
Original file line number Diff line number Diff line change
Expand Up @@ -985,13 +985,6 @@ public static MetricsApi metrics() {
return getCurrentHub().metrics();
}

/** the replay API */
@NotNull
@ApiStatus.Experimental
public static ReplayController replay() {
return getCurrentHub().getOptions().getReplayController();
}

/**
* Configuration options callback
*
Expand Down
Loading