Skip to content

Commit 2c39e81

Browse files
authored
Merge e4ec640 into 731ae5a
2 parents 731ae5a + e4ec640 commit 2c39e81

File tree

14 files changed

+474
-69
lines changed

14 files changed

+474
-69
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
- Avoid stopping appStartProfiler after application creation ([#3630](https://github.com/getsentry/sentry-java/pull/3630))
88
- Session Replay: Correctly detect dominant color for `TextView`s with Spans ([#3682](https://github.com/getsentry/sentry-java/pull/3682))
9+
- 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))
10+
- `android:tag="sentry-redact|sentry-ignore"` in XML or `view.setTag("sentry-redact|sentry-ignore")` in code tags
11+
- 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
12+
- `view.sentryReplayRedact()` or `view.sentryReplayIgnore()` extension functions
13+
- redact/ignore `View`s of a certain type by adding fully-qualified classname to one of the lists `options.experimental.sessionReplay.addRedactClass()` or `options.experimental.sessionReplay.addIgnoreClass()`. Note, that all of the view subclasses/subtypes will be redacted/ignored as well
14+
- For example, (this is already a default behavior) to redact all `TextView`s and their subclasses (`RadioButton`, `EditText`, etc.): `options.experimental.sessionReplay.addRedactClass("android.widget.TextView")`
15+
- If you're using code obfuscation, adjust your proguard-rules accordingly, so your custom view class name is not minified
916

1017
*Breaking changes*:
1118

sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -409,22 +409,12 @@ static void applyMetadata(
409409
options
410410
.getExperimental()
411411
.getSessionReplay()
412-
.setRedactAllText(
413-
readBool(
414-
metadata,
415-
logger,
416-
REPLAYS_REDACT_ALL_TEXT,
417-
options.getExperimental().getSessionReplay().getRedactAllText()));
412+
.setRedactAllText(readBool(metadata, logger, REPLAYS_REDACT_ALL_TEXT, true));
418413

419414
options
420415
.getExperimental()
421416
.getSessionReplay()
422-
.setRedactAllImages(
423-
readBool(
424-
metadata,
425-
logger,
426-
REPLAYS_REDACT_ALL_IMAGES,
427-
options.getExperimental().getSessionReplay().getRedactAllImages()));
417+
.setRedactAllImages(readBool(metadata, logger, REPLAYS_REDACT_ALL_IMAGES, true));
428418
}
429419

430420
options

sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.core.os.bundleOf
66
import androidx.test.ext.junit.runners.AndroidJUnit4
77
import io.sentry.ILogger
88
import io.sentry.SentryLevel
9+
import io.sentry.SentryReplayOptions
910
import org.junit.runner.RunWith
1011
import org.mockito.kotlin.any
1112
import org.mockito.kotlin.eq
@@ -1473,8 +1474,8 @@ class ManifestMetadataReaderTest {
14731474
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
14741475

14751476
// Assert
1476-
assertFalse(fixture.options.experimental.sessionReplay.redactAllImages)
1477-
assertFalse(fixture.options.experimental.sessionReplay.redactAllText)
1477+
assertTrue(fixture.options.experimental.sessionReplay.ignoreClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
1478+
assertTrue(fixture.options.experimental.sessionReplay.ignoreClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
14781479
}
14791480

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

14881489
// Assert
1489-
assertTrue(fixture.options.experimental.sessionReplay.redactAllImages)
1490-
assertTrue(fixture.options.experimental.sessionReplay.redactAllText)
1490+
assertTrue(fixture.options.experimental.sessionReplay.redactClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
1491+
assertTrue(fixture.options.experimental.sessionReplay.redactClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
14911492
}
14921493
}

sentry-android-replay/api/sentry-android-replay.api

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ public final class io/sentry/android/replay/ScreenshotRecorderConfig$Companion {
103103
public final fun from (Landroid/content/Context;Lio/sentry/SentryReplayOptions;)Lio/sentry/android/replay/ScreenshotRecorderConfig;
104104
}
105105

106+
public final class io/sentry/android/replay/SessionReplayOptionsKt {
107+
public static final fun getRedactAllImages (Lio/sentry/SentryReplayOptions;)Z
108+
public static final fun getRedactAllText (Lio/sentry/SentryReplayOptions;)Z
109+
public static final fun setRedactAllImages (Lio/sentry/SentryReplayOptions;Z)V
110+
public static final fun setRedactAllText (Lio/sentry/SentryReplayOptions;Z)V
111+
}
112+
113+
public final class io/sentry/android/replay/ViewExtensionsKt {
114+
public static final fun sentryReplayIgnore (Landroid/view/View;)V
115+
public static final fun sentryReplayRedact (Landroid/view/View;)V
116+
}
117+
106118
public final class io/sentry/android/replay/gestures/GestureRecorder : io/sentry/android/replay/OnRootViewsChangedListener {
107119
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/android/replay/gestures/TouchRecorderCallback;)V
108120
public fun onRootViewsChanged (Landroid/view/View;Z)V

sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class ReplayCache(
8080
if (replayCacheDir == null || bitmap.isRecycled) {
8181
return
8282
}
83+
replayCacheDir?.mkdirs()
8384

8485
val screenshot = File(replayCacheDir, "$frameTimestamp.jpg").also {
8586
it.createNewFile()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.sentry.android.replay
2+
3+
import io.sentry.SentryReplayOptions
4+
5+
// since we don't have getters for redactAllText and redactAllImages, they won't be accessible as
6+
// properties in Kotlin, therefore we create these extensions where a getter is dummy, but a setter
7+
// delegates to the corresponding method in SentryReplayOptions
8+
9+
/**
10+
* Redact all text content. Draws a rectangle of text bounds with text color on top. By default
11+
* only views extending TextView are redacted.
12+
*
13+
* <p>Default is enabled.
14+
*/
15+
var SentryReplayOptions.redactAllText: Boolean
16+
@Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR)
17+
get() = error("Getter not supported")
18+
set(value) = setRedactAllText(value)
19+
20+
/**
21+
* Redact all image content. Draws a rectangle of image bounds with image's dominant color on top.
22+
* By default only views extending ImageView with BitmapDrawable or custom Drawable type are
23+
* redacted. ColorDrawable, InsetDrawable, VectorDrawable are all considered non-PII, as they come
24+
* from the apk.
25+
*
26+
* <p>Default is enabled.
27+
*/
28+
var SentryReplayOptions.redactAllImages: Boolean
29+
@Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR)
30+
get() = error("Getter not supported")
31+
set(value) = setRedactAllImages(value)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.sentry.android.replay
2+
3+
import android.view.View
4+
5+
/**
6+
* Marks this view to be redacted in session replay.
7+
*/
8+
fun View.sentryReplayRedact() {
9+
setTag(R.id.sentry_privacy, "redact")
10+
}
11+
12+
/**
13+
* Marks this view to be ignored from redaction in session.
14+
* All its content will be visible in the replay, use with caution.
15+
*/
16+
fun View.sentryReplayIgnore() {
17+
setTag(R.id.sentry_privacy, "ignore")
18+
}

sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ internal fun interface OnRootViewsChangedListener {
134134
/**
135135
* A utility that holds the list of root views that WindowManager updates.
136136
*/
137-
internal class RootViewsSpy private constructor() {
137+
internal object RootViewsSpy {
138138

139139
val listeners: CopyOnWriteArrayList<OnRootViewsChangedListener> = object : CopyOnWriteArrayList<OnRootViewsChangedListener>() {
140140
override fun add(element: OnRootViewsChangedListener?): Boolean {
@@ -168,15 +168,13 @@ internal class RootViewsSpy private constructor() {
168168
}
169169
}
170170

171-
companion object {
172-
fun install(): RootViewsSpy {
173-
return RootViewsSpy().apply {
174-
// had to do this as a first message of the main thread queue, otherwise if this is
175-
// called from ContentProvider, it might be too early and the listener won't be installed
176-
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
177-
WindowManagerSpy.swapWindowManagerGlobalMViews { mViews ->
178-
delegatingViewList.apply { addAll(mViews) }
179-
}
171+
fun install(): RootViewsSpy {
172+
return apply {
173+
// had to do this as a first message of the main thread queue, otherwise if this is
174+
// called from ContentProvider, it might be too early and the listener won't be installed
175+
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
176+
WindowManagerSpy.swapWindowManagerGlobalMViews { mViews ->
177+
delegatingViewList.apply { addAll(mViews) }
180178
}
181179
}
182180
}

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.view.View
77
import android.widget.ImageView
88
import android.widget.TextView
99
import io.sentry.SentryOptions
10+
import io.sentry.android.replay.R
1011
import io.sentry.android.replay.util.isRedactable
1112
import io.sentry.android.replay.util.isVisibleToUser
1213
import io.sentry.android.replay.util.totalPaddingTopSafe
@@ -233,14 +234,46 @@ sealed class ViewHierarchyNode(
233234
}
234235
}
235236

236-
private fun shouldRedact(view: View, options: SentryOptions): Boolean {
237-
return options.experimental.sessionReplay.redactClasses.contains(view.javaClass.canonicalName)
237+
private const val SENTRY_IGNORE_TAG = "sentry-ignore"
238+
private const val SENTRY_REDACT_TAG = "sentry-redact"
239+
240+
private fun Class<*>.isAssignableFrom(set: Set<String>): Boolean {
241+
var cls: Class<*>? = this
242+
while (cls != null) {
243+
val canonicalName = cls.canonicalName
244+
if (canonicalName != null && set.contains(canonicalName)) {
245+
return true
246+
}
247+
cls = cls.superclass
248+
}
249+
return false
250+
}
251+
252+
private fun View.shouldRedact(options: SentryOptions): Boolean {
253+
if ((tag as? String)?.lowercase()?.contains(SENTRY_IGNORE_TAG) == true ||
254+
getTag(R.id.sentry_privacy) == "ignore"
255+
) {
256+
return false
257+
}
258+
259+
if ((tag as? String)?.lowercase()?.contains(SENTRY_REDACT_TAG) == true ||
260+
getTag(R.id.sentry_privacy) == "redact"
261+
) {
262+
return true
263+
}
264+
265+
if (this.javaClass.isAssignableFrom(options.experimental.sessionReplay.ignoreClasses)) {
266+
return false
267+
}
268+
269+
return this.javaClass.isAssignableFrom(options.experimental.sessionReplay.redactClasses)
238270
}
239271

240272
fun fromView(view: View, parent: ViewHierarchyNode?, distance: Int, options: SentryOptions): ViewHierarchyNode {
241273
val (isVisible, visibleRect) = view.isVisibleToUser()
242-
when {
243-
view is TextView && options.experimental.sessionReplay.redactAllText -> {
274+
val shouldRedact = isVisible && view.shouldRedact(options)
275+
when (view) {
276+
is TextView -> {
244277
parent.setImportantForCaptureToAncestors(true)
245278
return TextViewHierarchyNode(
246279
layout = view.layout,
@@ -252,7 +285,7 @@ sealed class ViewHierarchyNode(
252285
width = view.width,
253286
height = view.height,
254287
elevation = (parent?.elevation ?: 0f) + view.elevation,
255-
shouldRedact = isVisible,
288+
shouldRedact = shouldRedact,
256289
distance = distance,
257290
parent = parent,
258291
isImportantForContentCapture = true,
@@ -261,7 +294,7 @@ sealed class ViewHierarchyNode(
261294
)
262295
}
263296

264-
view is ImageView && options.experimental.sessionReplay.redactAllImages -> {
297+
is ImageView -> {
265298
parent.setImportantForCaptureToAncestors(true)
266299
return ImageViewHierarchyNode(
267300
x = view.x,
@@ -273,7 +306,7 @@ sealed class ViewHierarchyNode(
273306
parent = parent,
274307
isVisible = isVisible,
275308
isImportantForContentCapture = true,
276-
shouldRedact = isVisible && view.drawable?.isRedactable() == true,
309+
shouldRedact = shouldRedact && view.drawable?.isRedactable() == true,
277310
visibleRect = visibleRect
278311
)
279312
}
@@ -287,7 +320,7 @@ sealed class ViewHierarchyNode(
287320
(parent?.elevation ?: 0f) + view.elevation,
288321
distance = distance,
289322
parent = parent,
290-
shouldRedact = isVisible && shouldRedact(view, options),
323+
shouldRedact = shouldRedact,
291324
isImportantForContentCapture = false, /* will be set by children */
292325
isVisible = isVisible,
293326
visibleRect = visibleRect

sentry-android-replay/src/main/res/public.xml

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)