Skip to content

Commit e406a73

Browse files
authored
Merge fcae43b into 260324f
2 parents 260324f + fcae43b commit e406a73

File tree

2 files changed

+44
-11
lines changed

2 files changed

+44
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Fix Session Replay masking for newer versions of Jetpack Compose (1.8+) ([#4485](https://github.com/getsentry/sentry-java/pull/4485))
6+
57
### Fixes
68

79
- Send UI Profiling app start chunk when it finishes ([#4423](https://github.com/getsentry/sentry-java/pull/4423))

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

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import androidx.compose.ui.layout.findRootCoordinates
1111
import androidx.compose.ui.node.LayoutNode
1212
import androidx.compose.ui.node.Owner
1313
import androidx.compose.ui.semantics.SemanticsActions
14+
import androidx.compose.ui.semantics.SemanticsConfiguration
1415
import androidx.compose.ui.semantics.SemanticsProperties
1516
import androidx.compose.ui.semantics.getOrNull
1617
import androidx.compose.ui.text.TextLayoutResult
1718
import androidx.compose.ui.unit.TextUnit
19+
import io.sentry.ILogger
1820
import io.sentry.SentryLevel
1921
import io.sentry.SentryOptions
2022
import io.sentry.SentryReplayOptions
@@ -29,26 +31,55 @@ import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHiera
2931
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.ImageViewHierarchyNode
3032
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarchyNode
3133
import java.lang.ref.WeakReference
34+
import java.lang.reflect.Method
3235

3336
@TargetApi(26)
3437
internal object ComposeViewHierarchyNode {
3538

39+
private val getSemanticsConfigurationMethod: Method? by lazy {
40+
try {
41+
return@lazy LayoutNode::class.java.getDeclaredMethod("getSemanticsConfiguration").apply {
42+
isAccessible = true
43+
}
44+
} catch (_: Throwable) {
45+
// ignore, as this method may not be available
46+
}
47+
return@lazy null
48+
}
49+
50+
private fun LayoutNode.retrieveSemanticsConfiguration(logger: ILogger): SemanticsConfiguration? {
51+
// Jetpack Compose 1.8 or newer provides SemanticsConfiguration via SemanticsInfo
52+
// See https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
53+
// and https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
54+
try {
55+
getSemanticsConfigurationMethod?.let {
56+
return it.invoke(this) as SemanticsConfiguration?
57+
}
58+
} catch (_: Throwable) {
59+
logger.log(
60+
SentryLevel.WARNING,
61+
"Failed to invoke LayoutNode.getSemanticsConfiguration"
62+
)
63+
}
64+
65+
// for backwards compatibility
66+
return collapsedSemantics
67+
}
68+
3669
/**
3770
* Since Compose doesn't have a concept of a View class (they are all composable functions),
3871
* we need to map the semantics node to a corresponding old view system class.
3972
*/
40-
private fun LayoutNode.getProxyClassName(isImage: Boolean): String {
73+
private fun SemanticsConfiguration.getProxyClassName(isImage: Boolean): String {
4174
return when {
4275
isImage -> SentryReplayOptions.IMAGE_VIEW_CLASS_NAME
43-
collapsedSemantics?.contains(SemanticsProperties.Text) == true ||
44-
collapsedSemantics?.contains(SemanticsActions.SetText) == true ||
45-
collapsedSemantics?.contains(SemanticsProperties.EditableText) == true -> SentryReplayOptions.TEXT_VIEW_CLASS_NAME
76+
contains(SemanticsProperties.Text) || contains(SemanticsActions.SetText) || contains(SemanticsProperties.EditableText) -> SentryReplayOptions.TEXT_VIEW_CLASS_NAME
4677
else -> "android.view.View"
4778
}
4879
}
4980

50-
private fun LayoutNode.shouldMask(isImage: Boolean, options: SentryOptions): Boolean {
51-
val sentryPrivacyModifier = collapsedSemantics?.getOrNull(SentryReplayModifiers.SentryPrivacy)
81+
private fun SemanticsConfiguration?.shouldMask(isImage: Boolean, options: SentryOptions): Boolean {
82+
val sentryPrivacyModifier = this?.getOrNull(SentryReplayModifiers.SentryPrivacy)
5283
if (sentryPrivacyModifier == "unmask") {
5384
return false
5485
}
@@ -57,7 +88,7 @@ internal object ComposeViewHierarchyNode {
5788
return true
5889
}
5990

60-
val className = getProxyClassName(isImage)
91+
val className = this?.getProxyClassName(isImage)
6192
if (options.sessionReplay.unmaskViewClasses.contains(className)) {
6293
return false
6394
}
@@ -83,7 +114,7 @@ internal object ComposeViewHierarchyNode {
83114
_rootCoordinates = WeakReference(node.coordinates.findRootCoordinates())
84115
}
85116

86-
val semantics = node.collapsedSemantics
117+
val semantics = node.retrieveSemanticsConfiguration(options.logger)
87118
val visibleRect = node.coordinates.boundsInWindow(_rootCoordinates?.get())
88119
val isVisible = !node.outerCoordinator.isTransparent() &&
89120
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
@@ -92,7 +123,7 @@ internal object ComposeViewHierarchyNode {
92123
semantics?.contains(SemanticsProperties.EditableText) == true
93124
return when {
94125
semantics?.contains(SemanticsProperties.Text) == true || isEditable -> {
95-
val shouldMask = isVisible && node.shouldMask(isImage = false, options)
126+
val shouldMask = isVisible && semantics.shouldMask(isImage = false, options)
96127

97128
parent?.setImportantForCaptureToAncestors(true)
98129
// TODO: if we get reports that it's slow, we can drop this, and just mask
@@ -133,7 +164,7 @@ internal object ComposeViewHierarchyNode {
133164
else -> {
134165
val painter = node.findPainter()
135166
if (painter != null) {
136-
val shouldMask = isVisible && node.shouldMask(isImage = true, options)
167+
val shouldMask = isVisible && semantics.shouldMask(isImage = true, options)
137168

138169
parent?.setImportantForCaptureToAncestors(true)
139170
ImageViewHierarchyNode(
@@ -150,7 +181,7 @@ internal object ComposeViewHierarchyNode {
150181
visibleRect = visibleRect
151182
)
152183
} else {
153-
val shouldMask = isVisible && node.shouldMask(isImage = false, options)
184+
val shouldMask = isVisible && semantics.shouldMask(isImage = false, options)
154185

155186
// TODO: this currently does not support embedded AndroidViews, we'd have to
156187
// TODO: traverse the ViewHierarchyNode here again. For now we can recommend

0 commit comments

Comments
 (0)