Skip to content

Commit 5e1d6b3

Browse files
authored
Merge pull request #3208 from DataDog/yl/lower-compose-reflection-error
Lower layoutNodeUtils telemetry to warning with callsite information
2 parents 77f3587 + 716c22f commit 5e1d6b3

File tree

2 files changed

+103
-9
lines changed

2 files changed

+103
-9
lines changed

integrations/dd-sdk-android-compose/src/main/kotlin/com/datadog/android/compose/internal/utils/LayoutNodeUtils.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal class LayoutNodeUtils {
2727

2828
@Suppress("NestedBlockDepth", "CyclomaticComplexMethod")
2929
fun resolveLayoutNode(node: LayoutNode): TargetNode? {
30-
return runSafe {
30+
return runSafe("resolveLayoutNode") {
3131
var isClickable = false
3232
var isScrollable = false
3333
var datadogTag: String? = null
@@ -51,11 +51,11 @@ internal class LayoutNodeUtils {
5151
role = role ?: getOrNull(SemanticsProperties.Role)
5252
}
5353
} else {
54-
when (modifier::class.qualifiedName) {
54+
when (val name = modifier::class.qualifiedName) {
5555
CLASS_NAME_SELECTABLE_ELEMENT,
5656
CLASS_NAME_CLICKABLE_ELEMENT,
5757
CLASS_NAME_COMBINED_CLICKABLE_ELEMENT -> {
58-
role = role ?: getRole(modifier)
58+
role = role ?: getRole(modifier, name)
5959
isClickable = true
6060
}
6161

@@ -89,30 +89,30 @@ internal class LayoutNodeUtils {
8989
}
9090

9191
@Suppress("UnsafeThirdPartyFunctionCall")
92-
private fun getRole(obj: Any): Role? {
93-
return runSafe {
92+
private fun getRole(obj: Any, modifierClassName: String): Role? {
93+
return runSafe("getRole($modifierClassName)") {
9494
val roleField = obj::class.java.getDeclaredField("role")
9595
roleField.isAccessible = true
9696
roleField.get(obj) as? Role
9797
}
9898
}
9999

100100
fun getLayoutNodeBoundsInWindow(node: LayoutNode): Rect? {
101-
return runSafe {
101+
return runSafe("getLayoutNodeBoundsInWindow") {
102102
node.layoutDelegate.outerCoordinator.coordinates.boundsInWindow()
103103
}
104104
}
105105

106-
private fun <T> runSafe(action: () -> T): T? {
106+
private fun <T> runSafe(callSite: String, action: () -> T): T? {
107107
try {
108108
return action()
109109
} catch (@Suppress("TooGenericExceptionCaught") e: Throwable) {
110110
// We rely on visibility suppression to access internal field,
111111
// any runtime exception must be caught here.
112112
(Datadog.getInstance() as? FeatureSdkCore)?.internalLogger?.log(
113-
level = InternalLogger.Level.ERROR,
113+
level = InternalLogger.Level.WARN,
114114
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
115-
messageBuilder = { "LayoutNodeUtils execution failure." },
115+
messageBuilder = { "LayoutNodeUtils execution failure in $callSite." },
116116
throwable = e,
117117
onlyOnce = true
118118
)

integrations/dd-sdk-android-compose/src/test/kotlin/com/datadog/android/compose/internal/utils/LayoutNodeUtilsTest.kt

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,36 @@ import androidx.compose.ui.semantics.SemanticsActions
2222
import androidx.compose.ui.semantics.SemanticsConfiguration
2323
import androidx.compose.ui.semantics.SemanticsModifier
2424
import androidx.compose.ui.semantics.getOrNull
25+
import com.datadog.android.Datadog
26+
import com.datadog.android.api.InternalLogger
27+
import com.datadog.android.api.SdkCore
28+
import com.datadog.android.api.feature.FeatureSdkCore
2529
import com.datadog.android.compose.DatadogSemanticsPropertyKey
2630
import com.datadog.tools.unit.forge.BaseConfigurator
31+
import com.datadog.tools.unit.getFieldValue
32+
import com.datadog.tools.unit.getStaticValue
2733
import fr.xgouchet.elmyr.Forge
2834
import fr.xgouchet.elmyr.annotation.StringForgery
2935
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
3036
import fr.xgouchet.elmyr.junit5.ForgeExtension
3137
import org.assertj.core.api.Assertions.assertThat
38+
import org.junit.jupiter.api.AfterEach
39+
import org.junit.jupiter.api.BeforeEach
3240
import org.junit.jupiter.api.Test
3341
import org.junit.jupiter.api.extension.ExtendWith
3442
import org.junit.jupiter.api.extension.Extensions
3543
import org.junit.jupiter.params.ParameterizedTest
3644
import org.junit.jupiter.params.provider.MethodSource
45+
import org.mockito.ArgumentMatchers.isA
46+
import org.mockito.Mock
3747
import org.mockito.junit.jupiter.MockitoExtension
3848
import org.mockito.junit.jupiter.MockitoSettings
49+
import org.mockito.kotlin.argumentCaptor
3950
import org.mockito.kotlin.doReturn
51+
import org.mockito.kotlin.eq
4052
import org.mockito.kotlin.mock
53+
import org.mockito.kotlin.same
54+
import org.mockito.kotlin.verify
4155
import org.mockito.kotlin.whenever
4256
import org.mockito.quality.Strictness
4357
import java.util.stream.Stream
@@ -54,6 +68,25 @@ class LayoutNodeUtilsTest {
5468

5569
private val testedLayoutNodeUtils = LayoutNodeUtils()
5670

71+
@Mock
72+
lateinit var mockSdkCore: FeatureSdkCore
73+
74+
@Mock
75+
lateinit var mockInternalLogger: InternalLogger
76+
77+
@BeforeEach
78+
fun `set up`() {
79+
whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger
80+
val registry: Any = Datadog::class.java.getStaticValue("registry")
81+
val instances: MutableMap<String, SdkCore> = registry.getFieldValue("instances")
82+
instances += DEFAULT_INSTANCE_NAME to mockSdkCore
83+
}
84+
85+
@AfterEach
86+
fun `tear down`() {
87+
Datadog.stopInstance(DEFAULT_INSTANCE_NAME)
88+
}
89+
5790
// region Legacy Compose (SemanticsModifier)
5891

5992
@Test
@@ -131,6 +164,60 @@ class LayoutNodeUtilsTest {
131164

132165
// endregion
133166

167+
// region Error Handling
168+
169+
@Test
170+
fun `M log WARN to telemetry W resolveLayoutNode() {getModifierInfo throws}`() {
171+
// Given
172+
val exception = RuntimeException("test exception")
173+
val mockNode = mock<LayoutNode>()
174+
whenever(mockNode.getModifierInfo()).thenThrow(exception)
175+
176+
// When
177+
val result = testedLayoutNodeUtils.resolveLayoutNode(mockNode)
178+
179+
// Then
180+
assertThat(result).isNull()
181+
argumentCaptor<() -> String> {
182+
verify(mockInternalLogger).log(
183+
eq(InternalLogger.Level.WARN),
184+
eq(listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY)),
185+
capture(),
186+
same(exception),
187+
eq(true),
188+
eq(null)
189+
)
190+
assertThat(firstValue()).isEqualTo("LayoutNodeUtils execution failure in resolveLayoutNode.")
191+
}
192+
}
193+
194+
@Test
195+
fun `M log WARN to telemetry W getLayoutNodeBoundsInWindow() {layoutDelegate access throws}`() {
196+
// Given
197+
// mock<LayoutNode>() returns null for unstubbed layoutDelegate, causing NPE when chained
198+
val mockNode = mock<LayoutNode>()
199+
200+
// When
201+
val result = testedLayoutNodeUtils.getLayoutNodeBoundsInWindow(mockNode)
202+
203+
// Then
204+
assertThat(result).isNull()
205+
argumentCaptor<() -> String> {
206+
verify(mockInternalLogger).log(
207+
eq(InternalLogger.Level.WARN),
208+
eq(listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY)),
209+
capture(),
210+
isA(NullPointerException::class.java),
211+
eq(true),
212+
eq(null)
213+
)
214+
assertThat(firstValue())
215+
.isEqualTo("LayoutNodeUtils execution failure in getLayoutNodeBoundsInWindow.")
216+
}
217+
}
218+
219+
// endregion
220+
134221
// region Private
135222

136223
private fun mockLayoutNodeWithModifiers(
@@ -192,6 +279,13 @@ class LayoutNodeUtilsTest {
192279
}
193280

194281
companion object {
282+
283+
/**
284+
* LayoutNodeUtils sends telemetry with default SDK core instance, so in the test we must
285+
* register the mocked SDK core with default name from [SdkCoreRegistry.DEFAULT_INSTANCE_NAME].
286+
*/
287+
private const val DEFAULT_INSTANCE_NAME = "_dd.sdk_core.default"
288+
195289
@JvmStatic
196290
fun clickableModifierElements(): Stream<ModifierTestCase> = Stream.of(
197291
ModifierTestCase("TriStateToggleableElement", TriStateToggleableElement()),

0 commit comments

Comments
 (0)