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
Custom redaction works for Compose
  • Loading branch information
romtsn committed Sep 30, 2024
commit 31722cebe89e85ed05ea0844bacebc1557baabce
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.sentry.android.replay

import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.semantics
import io.sentry.android.replay.SentryReplayModifiers.SentryPrivacy

public object SentryReplayModifiers {
val SentryPrivacy = SemanticsPropertyKey<String>(
name = "SentryPrivacy",
mergePolicy = { parentValue, _ -> parentValue }
)
}

public fun Modifier.sentryReplayRedact(): Modifier {
return semantics(
properties = {
this[SentryPrivacy] = "redact"
}
)
}

public fun Modifier.sentryReplayIgnore(): Modifier {
return semantics(
properties = {
this[SentryPrivacy] = "ignore"
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ internal class ScreenshotRecorder(
node.visibleRect ?: return@traverse false

// TODO: investigate why it returns true on RN when it shouldn't
if (viewHierarchy.isObscured(node)) {
return@traverse true
}
// if (viewHierarchy.isObscured(node)) {
// return@traverse true
// }

val (visibleRects, color) = when (node) {
is ImageViewHierarchyNode -> {
Expand Down Expand Up @@ -261,12 +261,7 @@ internal class ScreenshotRecorder(
return
}

var isCompose: Boolean
val time = measureNanoTime {
isCompose = ComposeViewHierarchyNode.fromView(this, parentNode, options)
}
if (isCompose) {
Log.e("TIME", String.format("%.2f", time / 1_000_000.0) + "ms")
if (ComposeViewHierarchyNode.fromView(this, parentNode, options)) {
// if it's a compose view, we can skip the children as they are already traversed in
// the ComposeViewHierarchyNode.fromView method
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ internal class ComposeTextLayout(internal val layout: TextLayoutResult, private
override fun getLineStart(line: Int): Int = layout.getLineStart(line)
}

// TODO: probably most of the below we can do via bytecode instrumentation and speed up at runtime

/**
* This method is necessary to redact images in Compose.
*
Expand Down Expand Up @@ -84,6 +86,20 @@ internal fun androidx.compose.ui.geometry.Rect.toRect(): Rect {

internal data class TextAttributes(val color: Color?, val hasFillModifier: Boolean)

/**
* This method is necessary to redact text in Compose.
*
* We heuristically look up for classes that have a [Text] modifier, usually they all have a
* `Text` string in their name, e.g. TextStringSimpleElement or TextAnnotatedStringElement. We then
* get the color from the modifier, to be able to redact it with the correct color.
*
* We also look up for classes that have a [Fill] modifier, usually they all have a `Fill` string in
* their name, e.g. FillElement. This is necessary to workaround a Compose bug where single-line
* text composable without a `fill` modifier still thinks that there's one and wrongly calculates
* horizontal position.
*
* We also add special proguard rules to keep the `Text` class names and their `color` member.
*/
internal fun LayoutNode.findTextAttributes(): TextAttributes {
val modifierInfos = getModifierInfo()
var color: Color? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import androidx.compose.ui.text.TextLayoutResult
import io.sentry.SentryLevel
import io.sentry.SentryOptions
import io.sentry.SentryReplayOptions
import io.sentry.android.replay.SentryReplayModifiers
import io.sentry.android.replay.util.ComposeTextLayout
import io.sentry.android.replay.util.findPainter
import io.sentry.android.replay.util.findTextAttributes
Expand Down Expand Up @@ -43,6 +44,15 @@ internal object ComposeViewHierarchyNode {
}

private fun LayoutNode.shouldRedact(isImage: Boolean, options: SentryOptions): Boolean {
val sentryPrivacyModifier = collapsedSemantics?.getOrNull(SentryReplayModifiers.SentryPrivacy)
if (sentryPrivacyModifier == "ignore") {
return false
}

if (sentryPrivacyModifier == "redact") {
return true
}

val className = getProxyClassName(isImage)
if (options.experimental.sessionReplay.ignoreViewClasses.contains(className)) {
return false
Expand Down Expand Up @@ -124,6 +134,9 @@ internal object ComposeViewHierarchyNode {
} else {
val shouldRedact = isVisible && node.shouldRedact(isImage = false, options)

// TODO: this currently does not support embedded AndroidViews, we'd have to
// TODO: traverse the ViewHierarchyNode here again. For now we can recommend
// TODO: using custom modifiers to obscure the entire node if it's sensitive
GenericViewHierarchyNode(
x = positionInWindow.x,
y = positionInWindow.y,
Expand All @@ -132,7 +145,7 @@ internal object ComposeViewHierarchyNode {
elevation = (parent?.elevation ?: 0f),
distance = distance,
parent = parent,
shouldRedact = shouldRedact, // TODO: use custom modifier to mark views that should be redacted/ignored
shouldRedact = shouldRedact,
isImportantForContentCapture = false, /* will be set by children */
isVisible = isVisible,
visibleRect = visibleRect
Expand All @@ -156,7 +169,7 @@ internal object ComposeViewHierarchyNode {
rootNode.traverse(parent, options)
} catch (e: Throwable) {
options.logger.log(SentryLevel.ERROR, e, """
Error traversing Compose view. Most likely you're using an unsupported version of
Error traversing Compose tree. Most likely you're using an unsupported version of
androidx.compose.ui:ui. The minimum supported version is 1.5.0. If it's a newer
version, please open a github issue with the version you're using, so we can add
support for it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import coil.compose.AsyncImage
import io.sentry.android.replay.sentryReplayIgnore
import io.sentry.compose.SentryTraced
import io.sentry.compose.withSentryObservableEffect
import io.sentry.samples.android.GithubAPI
Expand Down Expand Up @@ -147,7 +148,7 @@ fun Github(
.testTag("button_list_repos_async")
.padding(top = 32.dp)
) {
Text("Make Request")
Text("Make Request", modifier = Modifier.sentryReplayIgnore())
}
}
}
Expand Down