Skip to content

Commit c02df17

Browse files
authored
Merge branch 'main' into feat/experimental-otel-global-hub-mode
2 parents 4d4f6a8 + 02931bf commit c02df17

File tree

7 files changed

+113
-20
lines changed

7 files changed

+113
-20
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
- Spans created via Sentry API are preferred over those created through OpenTelemetry API or auto instrumentation
1111
- New option `ignoreStandaloneClientSpans` that prevents Sentry from creating transactions for OpenTelemetry spans with kind `CLIENT` ([#4349](https://github.com/getsentry/sentry-java/pull/4349))
1212
- Defaults to `false` meaning standalone OpenTelemetry spans with kind `CLIENT` will be turned into Sentry transactions
13+
- Make `RequestDetailsResolver` public ([#4326](https://github.com/getsentry/sentry-java/pull/4326))
14+
- `RequestDetailsResolver` is now public and has an additional constructor, making it easier to use a custom `TransportFactory`
15+
16+
### Fixes
17+
18+
- Session Replay: Fix masking of non-styled `Text` Composables ([#4361](https://github.com/getsentry/sentry-java/pull/4361))
19+
- Session Replay: Fix masking read-only `TextField` Composables ([#4362](https://github.com/getsentry/sentry-java/pull/4362))
1320

1421
## 8.10.0
1522

@@ -20,7 +27,7 @@
2027
- Set `-Dio.opentelemetry.context.contextStorageProvider=io.sentry.opentelemetry.SentryContextStorageProvider` on your `java` command
2128
- Sentry will then wrap the other `ContextStorageProvider` that has been configured by loading it through SPI
2229
- If no other `ContextStorageProvider` is available or there are problems loading it, we fall back to using `SentryOtelThreadLocalStorage`
23-
30+
2431
### Fixes
2532

2633
- Update profile chunk rate limit and client report ([#4353](https://github.com/getsentry/sentry-java/pull/4353))

scripts/toggle-codec-logs.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ case "$ACTION" in
4545
adb shell setprop log.tag.BufferQueueProducer D
4646
adb shell setprop log.tag.ReflectedParamUpdater D
4747
adb shell setprop log.tag.hw-BpHwBinder D
48+
adb shell setprop log.tag.ACodec D
49+
adb shell setprop log.tag.VideoCapabilities D
50+
adb shell setprop log.tag.OMXUtils D
51+
adb shell setprop log.tag.OMXClient D
4852
echo "✅ Logs ENABLED"
4953
;;
5054
disable)
@@ -67,6 +71,10 @@ case "$ACTION" in
6771
adb shell setprop log.tag.BufferQueueProducer SILENT
6872
adb shell setprop log.tag.ReflectedParamUpdater SILENT
6973
adb shell setprop log.tag.hw-BpHwBinder SILENT
74+
adb shell setprop log.tag.ACodec SILENT
75+
adb shell setprop log.tag.VideoCapabilities SILENT
76+
adb shell setprop log.tag.OMXUtils SILENT
77+
adb shell setprop log.tag.OMXClient SILENT
7078
echo "🚫 Logs DISABLED"
7179
;;
7280
*)

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.ui.semantics.SemanticsActions
1414
import androidx.compose.ui.semantics.SemanticsProperties
1515
import androidx.compose.ui.semantics.getOrNull
1616
import androidx.compose.ui.text.TextLayoutResult
17+
import androidx.compose.ui.unit.TextUnit
1718
import io.sentry.SentryLevel
1819
import io.sentry.SentryOptions
1920
import io.sentry.SentryReplayOptions
@@ -40,7 +41,8 @@ internal object ComposeViewHierarchyNode {
4041
return when {
4142
isImage -> SentryReplayOptions.IMAGE_VIEW_CLASS_NAME
4243
collapsedSemantics?.contains(SemanticsProperties.Text) == true ||
43-
collapsedSemantics?.contains(SemanticsActions.SetText) == true -> SentryReplayOptions.TEXT_VIEW_CLASS_NAME
44+
collapsedSemantics?.contains(SemanticsActions.SetText) == true ||
45+
collapsedSemantics?.contains(SemanticsProperties.EditableText) == true -> SentryReplayOptions.TEXT_VIEW_CLASS_NAME
4446
else -> "android.view.View"
4547
}
4648
}
@@ -86,7 +88,8 @@ internal object ComposeViewHierarchyNode {
8688
val isVisible = !node.outerCoordinator.isTransparent() &&
8789
(semantics == null || !semantics.contains(SemanticsProperties.InvisibleToUser)) &&
8890
visibleRect.height() > 0 && visibleRect.width() > 0
89-
val isEditable = semantics?.contains(SemanticsActions.SetText) == true
91+
val isEditable = semantics?.contains(SemanticsActions.SetText) == true ||
92+
semantics?.contains(SemanticsProperties.EditableText) == true
9093
return when {
9194
semantics?.contains(SemanticsProperties.Text) == true || isEditable -> {
9295
val shouldMask = isVisible && node.shouldMask(isImage = false, options)
@@ -100,14 +103,19 @@ internal object ComposeViewHierarchyNode {
100103
?.invoke(textLayoutResults)
101104

102105
val (color, hasFillModifier) = node.findTextAttributes()
103-
var textColor = textLayoutResults.firstOrNull()?.layoutInput?.style?.color
106+
val textLayoutResult = textLayoutResults.firstOrNull()
107+
var textColor = textLayoutResult?.layoutInput?.style?.color
104108
if (textColor?.isUnspecified == true) {
105109
textColor = color
106110
}
107-
// TODO: support multiple text layouts
111+
val isLaidOut = textLayoutResult?.layoutInput?.style?.fontSize != TextUnit.Unspecified
108112
// TODO: support editable text (currently there's a way to get @Composable's padding only via reflection, and we can't reliably mask input fields based on TextLayout, so we mask the whole view instead)
109113
TextViewHierarchyNode(
110-
layout = if (textLayoutResults.isNotEmpty() && !isEditable) ComposeTextLayout(textLayoutResults.first(), hasFillModifier) else null,
114+
layout = if (textLayoutResult != null && !isEditable && isLaidOut) {
115+
ComposeTextLayout(textLayoutResult, hasFillModifier)
116+
} else {
117+
null
118+
},
111119
dominantColor = textColor?.toArgb()?.toOpaque(),
112120
x = visibleRect.left.toFloat(),
113121
y = visibleRect.top.toFloat(),

sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ComposeMaskingOptionsTest.kt

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ import androidx.compose.material3.TextField
1616
import androidx.compose.ui.Alignment
1717
import androidx.compose.ui.Modifier
1818
import androidx.compose.ui.platform.testTag
19+
import androidx.compose.ui.semantics.clearAndSetSemantics
20+
import androidx.compose.ui.semantics.editableText
1921
import androidx.compose.ui.semantics.invisibleToUser
2022
import androidx.compose.ui.semantics.semantics
23+
import androidx.compose.ui.text.AnnotatedString
2124
import androidx.compose.ui.text.input.TextFieldValue
25+
import androidx.compose.ui.unit.TextUnit
2226
import androidx.compose.ui.unit.dp
27+
import androidx.compose.ui.unit.sp
2328
import androidx.test.ext.junit.runners.AndroidJUnit4
2429
import coil.compose.AsyncImage
2530
import io.sentry.SentryOptions
@@ -41,6 +46,7 @@ import java.io.File
4146
import kotlin.test.Test
4247
import kotlin.test.assertEquals
4348
import kotlin.test.assertFalse
49+
import kotlin.test.assertNull
4450
import kotlin.test.assertTrue
4551

4652
@RunWith(AndroidJUnit4::class)
@@ -52,7 +58,9 @@ class ComposeMaskingOptionsTest {
5258
System.setProperty("robolectric.areWindowsMarkedVisible", "true")
5359
System.setProperty("robolectric.pixelCopyRenderMode", "hardware")
5460
ComposeMaskingOptionsActivity.textModifierApplier = null
61+
ComposeMaskingOptionsActivity.textFieldModifierApplier = null
5562
ComposeMaskingOptionsActivity.containerModifierApplier = null
63+
ComposeMaskingOptionsActivity.fontSizeApplier = null
5664
}
5765

5866
@Test
@@ -67,8 +75,40 @@ class ComposeMaskingOptionsTest {
6775
val textNodes = activity.get().collectNodesOfType<TextViewHierarchyNode>(options)
6876
assertEquals(4, textNodes.size) // [TextField, Text, Button, Activity Title]
6977
assertTrue(textNodes.all { it.shouldMask })
70-
// just a sanity check for parsing the tree
71-
assertEquals("Random repo", (textNodes[1].layout as ComposeTextLayout).layout.layoutInput.text.text)
78+
// no fontSize specified - we don't use the text layout
79+
assertNull(textNodes.first().layout)
80+
}
81+
82+
@Test
83+
fun `when text is laid out nodes use it`() {
84+
ComposeMaskingOptionsActivity.fontSizeApplier = { 20.sp }
85+
val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup()
86+
shadowOf(Looper.getMainLooper()).idle()
87+
88+
val options = SentryOptions().apply {
89+
sessionReplay.maskAllText = true
90+
}
91+
92+
val textNodes = activity.get().collectNodesOfType<TextViewHierarchyNode>(options)
93+
// the text should be laid out when fontSize is specified
94+
assertEquals("Random repo", (textNodes.first().layout as? ComposeTextLayout)?.layout?.layoutInput?.text?.text)
95+
}
96+
97+
@Test
98+
fun `when text input field is readOnly still masks it`() {
99+
ComposeMaskingOptionsActivity.textFieldModifierApplier = {
100+
// newer versions of compose basically do this when a TextField is readOnly
101+
Modifier.clearAndSetSemantics { editableText = AnnotatedString("Placeholder") }
102+
}
103+
val activity = buildActivity(ComposeMaskingOptionsActivity::class.java).setup()
104+
shadowOf(Looper.getMainLooper()).idle()
105+
106+
val options = SentryOptions().apply {
107+
sessionReplay.maskAllText = true
108+
}
109+
110+
val textNodes = activity.get().collectNodesOfType<TextViewHierarchyNode>(options)
111+
assertTrue(textNodes[1].shouldMask)
72112
}
73113

74114
@Test
@@ -212,7 +252,9 @@ private class ComposeMaskingOptionsActivity : ComponentActivity() {
212252

213253
companion object {
214254
var textModifierApplier: (() -> Modifier)? = null
255+
var textFieldModifierApplier: (() -> Modifier)? = null
215256
var containerModifierApplier: (() -> Modifier)? = null
257+
var fontSizeApplier: (() -> TextUnit)? = null
216258
}
217259

218260
override fun onCreate(savedInstanceState: Bundle?) {
@@ -232,11 +274,12 @@ private class ComposeMaskingOptionsActivity : ComponentActivity() {
232274
contentDescription = null,
233275
modifier = Modifier.padding(vertical = 16.dp)
234276
)
277+
Text("Random repo", fontSize = fontSizeApplier?.invoke() ?: TextUnit.Unspecified)
235278
TextField(
279+
modifier = textFieldModifierApplier?.invoke() ?: Modifier,
236280
value = TextFieldValue("Placeholder"),
237281
onValueChange = { _ -> }
238282
)
239-
Text("Random repo")
240283
Button(
241284
onClick = {},
242285
modifier = Modifier

sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ fun Github(
106106
val scope = rememberCoroutineScope()
107107

108108
LaunchedEffect(perPage) {
109-
result = GithubAPI.service.listReposAsync(user.text, perPage).random().full_name
109+
result = try {
110+
GithubAPI.service.listReposAsync(user.text, perPage).random().full_name
111+
} catch (e: Throwable) {
112+
"error"
113+
}
110114
}
111115

112116
SentryTraced("github-$user") {
@@ -133,12 +137,15 @@ fun Github(
133137
user = newText
134138
}
135139
)
136-
Text("Random repo $result")
140+
Text("Random repo: $result")
137141
Button(
138142
onClick = {
139143
scope.launch {
140-
result =
144+
result = try {
141145
GithubAPI.service.listReposAsync(user.text, perPage).random().full_name
146+
} catch (e: Throwable) {
147+
"error"
148+
}
142149
}
143150
},
144151
modifier = Modifier

sentry/api/sentry.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,6 +2185,12 @@ public final class io/sentry/RequestDetails {
21852185
public fun getUrl ()Ljava/net/URL;
21862186
}
21872187

2188+
public final class io/sentry/RequestDetailsResolver {
2189+
public fun <init> (Lio/sentry/SentryOptions;)V
2190+
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
2191+
public fun resolve ()Lio/sentry/RequestDetails;
2192+
}
2193+
21882194
public final class io/sentry/SamplingContext {
21892195
public fun <init> (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;)V
21902196
public fun <init> (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;Ljava/lang/Double;Ljava/util/Map;)V

sentry/src/main/java/io/sentry/RequestDetailsResolver.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,39 @@
44
import java.net.URI;
55
import java.util.HashMap;
66
import java.util.Map;
7+
import org.jetbrains.annotations.ApiStatus;
78
import org.jetbrains.annotations.NotNull;
9+
import org.jetbrains.annotations.Nullable;
810

911
/** Resolves {@link RequestDetails}. */
10-
final class RequestDetailsResolver {
12+
@ApiStatus.Experimental
13+
public final class RequestDetailsResolver {
1114
/** HTTP Header for the user agent. */
1215
private static final String USER_AGENT = "User-Agent";
1316
/** HTTP Header for the authentication to Sentry. */
1417
private static final String SENTRY_AUTH = "X-Sentry-Auth";
1518

16-
private final @NotNull SentryOptions options;
19+
private final @NotNull Dsn dsn;
20+
private final @Nullable String sentryClientName;
1721

22+
public RequestDetailsResolver(
23+
final @NotNull String dsn, final @Nullable String sentryClientName) {
24+
Objects.requireNonNull(dsn, "dsn is required");
25+
26+
this.dsn = new Dsn(dsn);
27+
this.sentryClientName = sentryClientName;
28+
}
29+
30+
@ApiStatus.Internal
1831
public RequestDetailsResolver(final @NotNull SentryOptions options) {
19-
this.options = Objects.requireNonNull(options, "options is required");
32+
Objects.requireNonNull(options, "options is required");
33+
34+
this.dsn = options.retrieveParsedDsn();
35+
this.sentryClientName = options.getSentryClientName();
2036
}
2137

2238
@NotNull
23-
RequestDetails resolve() {
24-
final Dsn dsn = options.retrieveParsedDsn();
39+
public RequestDetails resolve() {
2540
final URI sentryUri = dsn.getSentryUri();
2641
final String envelopeUrl = sentryUri.resolve(sentryUri.getPath() + "/envelope/").toString();
2742

@@ -33,15 +48,14 @@ RequestDetails resolve() {
3348
+ SentryClient.SENTRY_PROTOCOL_VERSION
3449
+ ","
3550
+ "sentry_client="
36-
+ options.getSentryClientName()
51+
+ sentryClientName
3752
+ ","
3853
+ "sentry_key="
3954
+ publicKey
4055
+ (secretKey != null && secretKey.length() > 0 ? (",sentry_secret=" + secretKey) : "");
41-
final String userAgent = options.getSentryClientName();
4256

4357
final Map<String, String> headers = new HashMap<>();
44-
headers.put(USER_AGENT, userAgent);
58+
headers.put(USER_AGENT, sentryClientName);
4559
headers.put(SENTRY_AUTH, authHeader);
4660

4761
return new RequestDetails(envelopeUrl, headers);

0 commit comments

Comments
 (0)