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

[SR] Add custom redaction options #3689

Merged
merged 13 commits into from
Sep 16, 2024
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

- Avoid stopping appStartProfiler after application creation ([#3630](https://github.com/getsentry/sentry-java/pull/3630))
- Session Replay: Correctly detect dominant color for `TextView`s with Spans ([#3682](https://github.com/getsentry/sentry-java/pull/3682))
- Session Replay: Add options to selectively redact/ignore views from being captured. The following options are available: ([#3689](https://github.com/getsentry/sentry-java/pull/3689))
- `android:tag="sentry-redact|sentry-ignore"` in XML or `view.setTag("sentry-redact|sentry-ignore")` in code tags
- if you already have a tag set for a view, you can set a tag by id: `<tag android:id="@id/sentry_privacy" android:value="redact|ignore"/>` in XML or `view.setTag(io.sentry.android.replay.R.id.sentry_privacy, "redact|ignore")` in code
- `view.sentryReplayRedact()` or `view.sentryReplayIgnore()` extension functions
- redact/ignore `View`s of a certain type by adding fully-qualified classname to one of the lists `options.experimental.sessionReplay.addRedactViewClass()` or `options.experimental.sessionReplay.addIgnoreViewClass()`. Note, that all of the view subclasses/subtypes will be redacted/ignored as well
- For example, (this is already a default behavior) to redact all `TextView`s and their subclasses (`RadioButton`, `EditText`, etc.): `options.experimental.sessionReplay.addRedactViewClass("android.widget.TextView")`
- If you're using code obfuscation, adjust your proguard-rules accordingly, so your custom view class name is not minified

*Breaking changes*:

Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.core.os.bundleOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.ILogger
import io.sentry.SentryLevel
import io.sentry.SentryReplayOptions
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
Expand Down Expand Up @@ -1473,8 +1474,8 @@ class ManifestMetadataReaderTest {
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertFalse(fixture.options.experimental.sessionReplay.redactAllImages)
assertFalse(fixture.options.experimental.sessionReplay.redactAllText)
assertTrue(fixture.options.experimental.sessionReplay.ignoreViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.ignoreViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
}

@Test
Expand All @@ -1486,7 +1487,7 @@ class ManifestMetadataReaderTest {
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertTrue(fixture.options.experimental.sessionReplay.redactAllImages)
assertTrue(fixture.options.experimental.sessionReplay.redactAllText)
assertTrue(fixture.options.experimental.sessionReplay.redactViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.redactViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
}
}
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.isRedactable
import io.sentry.android.replay.util.isVisibleToUser
import io.sentry.android.replay.util.totalPaddingTopSafe
Expand Down Expand Up @@ -233,14 +234,46 @@ 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.shouldRedact(options: SentryOptions): Boolean {
if ((tag as? String)?.lowercase()?.contains(SENTRY_IGNORE_TAG) == true ||
getTag(R.id.sentry_privacy) == "ignore"
) {
return false
}

if ((tag as? String)?.lowercase()?.contains(SENTRY_REDACT_TAG) == true ||
getTag(R.id.sentry_privacy) == "redact"
) {
return true
}

if (this.javaClass.isAssignableFrom(options.experimental.sessionReplay.ignoreViewClasses)) {
return false
}

return this.javaClass.isAssignableFrom(options.experimental.sessionReplay.redactViewClasses)
}

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.shouldRedact(options)
when (view) {
is TextView -> {
parent.setImportantForCaptureToAncestors(true)
return TextViewHierarchyNode(
layout = view.layout,
Expand All @@ -252,7 +285,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 @@ -261,7 +294,7 @@ sealed class ViewHierarchyNode(
)
}

view is ImageView && options.experimental.sessionReplay.redactAllImages -> {
is ImageView -> {
parent.setImportantForCaptureToAncestors(true)
return ImageViewHierarchyNode(
x = view.x,
Expand All @@ -273,7 +306,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 @@ -287,7 +320,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>
Loading
Loading