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 tests
  • Loading branch information
romtsn committed Sep 13, 2024
commit a660793e002026f00a3db339d8fe654bc1e76eb4
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.ignoreClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.ignoreClasses.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.redactClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.redactClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ 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
import io.sentry.android.replay.util.totalPaddingTopSafe
Expand Down Expand Up @@ -250,21 +249,29 @@ sealed class ViewHierarchyNode(
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)
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.ignoreClasses)) {
return false
}

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

fun fromView(view: View, parent: ViewHierarchyNode?, distance: Int, options: SentryOptions): ViewHierarchyNode {
val (isVisible, visibleRect) = view.isVisibleToUser()
val shouldRedact = isVisible && !view.shouldIgnore(options) && view.shouldRedact(options)
val shouldRedact = isVisible && view.shouldRedact(options)
when (view) {
is TextView -> {
parent.setImportantForCaptureToAncestors(true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
package io.sentry.android.replay.viewhierarchy

import android.app.Activity
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.LinearLayout.LayoutParams
import android.widget.RadioButton
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.SentryOptions
import io.sentry.android.replay.redactAllImages
import io.sentry.android.replay.redactAllText
import io.sentry.android.replay.sentryReplayIgnore
import io.sentry.android.replay.sentryReplayRedact
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
import org.junit.Before
import org.junit.runner.RunWith
import org.robolectric.Robolectric.buildActivity
import org.robolectric.annotation.Config
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
@Config(sdk = [30])
class RedactionOptionsTest {

@Before
fun setup() {
System.setProperty("robolectric.areWindowsMarkedVisible", "true")
}

@Test
fun `when redactAllText is set all TextView nodes are redacted`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllText = true
}

val textNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!, null, 0, options)
val radioButtonNode = ViewHierarchyNode.fromView(ExampleActivity.radioButton!!, null, 0, options)

assertTrue(textNode is TextViewHierarchyNode)
assertTrue(textNode.shouldRedact)

assertTrue(radioButtonNode is TextViewHierarchyNode)
assertTrue(radioButtonNode.shouldRedact)
}

@Test
fun `when redactAllText is set to false all TextView nodes are ignored`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllText = false
}

val textNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!, null, 0, options)
val radioButtonNode = ViewHierarchyNode.fromView(ExampleActivity.radioButton!!, null, 0, options)

assertTrue(textNode is TextViewHierarchyNode)
assertFalse(textNode.shouldRedact)

assertTrue(radioButtonNode is TextViewHierarchyNode)
assertFalse(radioButtonNode.shouldRedact)
}

@Test
fun `when redactAllImages is set all ImageView nodes are redacted`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllImages = true
}

val imageNode = ViewHierarchyNode.fromView(ExampleActivity.imageView!!, null, 0, options)

assertTrue(imageNode is ImageViewHierarchyNode)
assertTrue(imageNode.shouldRedact)
}

@Test
fun `when redactAllImages is set to false all ImageView nodes are ignored`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllImages = false
}

val imageNode = ViewHierarchyNode.fromView(ExampleActivity.imageView!!, null, 0, options)

assertTrue(imageNode is ImageViewHierarchyNode)
assertFalse(imageNode.shouldRedact)
}

@Test
fun `when sentry-redact tag is set redacts the view`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllText = false
}

ExampleActivity.textView!!.tag = "sentry-redact"
val textNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!, null, 0, options)

assertTrue(textNode.shouldRedact)
}

@Test
fun `when sentry-ignore tag is set ignores the view`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllText = true
}

ExampleActivity.textView!!.tag = "sentry-ignore"
val textNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!, null, 0, options)

assertFalse(textNode.shouldRedact)
}

@Test
fun `when sentry-privacy tag is set to redact redacts the view`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllText = false
}

ExampleActivity.textView!!.sentryReplayRedact()
val textNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!, null, 0, options)

assertTrue(textNode.shouldRedact)
}

@Test
fun `when sentry-privacy tag is set to ignore ignores the view`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllText = true
}

ExampleActivity.textView!!.sentryReplayIgnore()
val textNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!, null, 0, options)

assertFalse(textNode.shouldRedact)
}

@Test
fun `when view is not visible, does not redact the view`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllText = true
}

ExampleActivity.textView!!.visibility = View.GONE
val textNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!, null, 0, options)

assertFalse(textNode.shouldRedact)
}

@Test
fun `when added to redact list redacts custom view`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactClasses.add(CustomView::class.java.canonicalName)
}

val customViewNode = ViewHierarchyNode.fromView(ExampleActivity.customView!!, null, 0, options)

assertTrue(customViewNode.shouldRedact)
}

@Test
fun `when subclass is added to ignored classes ignores all instances of that class`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.redactAllText = true // all TextView subclasses
experimental.sessionReplay.ignoreClasses.add(RadioButton::class.java.canonicalName)
}

val textNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!, null, 0, options)
val radioButtonNode = ViewHierarchyNode.fromView(ExampleActivity.radioButton!!, null, 0, options)

assertTrue(textNode.shouldRedact)
assertFalse(radioButtonNode.shouldRedact)
}

@Test
fun `when a container view is ignored its children are not ignored`() {
buildActivity(ExampleActivity::class.java).setup()

val options = SentryOptions().apply {
experimental.sessionReplay.ignoreClasses.add(LinearLayout::class.java.canonicalName)
}

val linearLayoutNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!.parent as LinearLayout, null, 0, options)
val textNode = ViewHierarchyNode.fromView(ExampleActivity.textView!!, null, 0, options)
val imageNode = ViewHierarchyNode.fromView(ExampleActivity.imageView!!, null, 0, options)

assertFalse(linearLayoutNode.shouldRedact)
assertTrue(textNode.shouldRedact)
assertTrue(imageNode.shouldRedact)
}
}

private class CustomView(context: Context) : View(context) {

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(Color.BLACK)
}
}

private class ExampleActivity : Activity() {

companion object {
var textView: TextView? = null
var radioButton: RadioButton? = null
var imageView: ImageView? = null
var customView: CustomView? = null
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val linearLayout = LinearLayout(this).apply {
setBackgroundColor(android.R.color.white)
orientation = LinearLayout.VERTICAL
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}

textView = TextView(this).apply {
text = "Hello, World!"
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
}
linearLayout.addView(textView)

val image = this::class.java.classLoader.getResource("Tongariro.jpg")!!
imageView = ImageView(this).apply {
setImageDrawable(Drawable.createFromPath(image.path))
layoutParams = LayoutParams(50, 50).apply {
setMargins(0, 16, 0, 0)
}
}
linearLayout.addView(imageView)

radioButton = RadioButton(this).apply {
text = "Radio Button"
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
setMargins(0, 16, 0, 0)
}
}
linearLayout.addView(radioButton)

customView = CustomView(this).apply {
layoutParams = LayoutParams(50, 50).apply {
setMargins(0, 16, 0, 0)
}
}
linearLayout.addView(customView)

setContentView(linearLayout)
}
}
2 changes: 2 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -2699,6 +2699,8 @@ public final class io/sentry/SentryReplayEvent$ReplayType$Deserializer : io/sent
}

public final class io/sentry/SentryReplayOptions {
public static final field IMAGE_VIEW_CLASS_NAME Ljava/lang/String;
public static final field TEXT_VIEW_CLASS_NAME Ljava/lang/String;
public fun <init> ()V
public fun <init> (Ljava/lang/Double;Ljava/lang/Double;)V
public fun addIgnoreClass (Ljava/lang/String;)V
Expand Down
4 changes: 2 additions & 2 deletions sentry/src/main/java/io/sentry/SentryReplayOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

public final class SentryReplayOptions {

private static final String TEXT_VIEW_CLASS_NAME = "android.widget.TextView";
private static final String IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView";
public static final String TEXT_VIEW_CLASS_NAME = "android.widget.TextView";
public static final String IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView";

public enum SentryReplayQuality {
/** Video Scale: 80% Bit Rate: 50.000 */
Expand Down