Skip to content

Commit 44d37ef

Browse files
authored
Merge pull request #2832 from DataDog/yl/nav3/add-lifecycle
RUM-11386: Make Navigation3 tracking listen to lifecycle
2 parents dad5b4c + 92f3a43 commit 44d37ef

File tree

9 files changed

+370
-29
lines changed

9 files changed

+370
-29
lines changed

detekt_custom.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,10 @@ datadog:
536536
- "androidx.compose.runtime.DisposableEffect(kotlin.Any?, kotlin.Any?, kotlin.Function1)"
537537
- "androidx.compose.runtime.DisposableEffectScope.onDispose(kotlin.Function0)"
538538
- "androidx.compose.runtime.LaunchedEffect(kotlin.Any?, kotlin.coroutines.SuspendFunction1)"
539+
- "androidx.compose.runtime.LaunchedEffect(kotlin.Any?, kotlin.Any?, kotlin.coroutines.SuspendFunction1)"
539540
- "androidx.compose.runtime.LaunchedEffect(kotlin.Any?, kotlin.Any?, kotlin.Any?, kotlin.coroutines.SuspendFunction1)"
541+
- "androidx.compose.runtime.produceState(kotlin.Boolean, kotlin.Any?, kotlin.coroutines.SuspendFunction1)"
542+
- "androidx.compose.runtime.ProduceStateScope.awaitDispose(kotlin.Function0)"
540543
- "androidx.compose.runtime.remember(kotlin.Any?, kotlin.Any?, kotlin.Function0)"
541544
- "androidx.compose.runtime.remember(kotlin.Function0)"
542545
- "androidx.compose.runtime.rememberUpdatedState(com.datadog.android.rum.tracking.ComponentPredicate)"
@@ -582,6 +585,8 @@ datadog:
582585
- "androidx.fragment.app.FragmentManager.unregisterFragmentLifecycleCallbacks(androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks)"
583586
- "androidx.lifecycle.Lifecycle.addObserver(androidx.lifecycle.LifecycleObserver)"
584587
- "androidx.lifecycle.Lifecycle.removeObserver(androidx.lifecycle.LifecycleObserver)"
588+
- "androidx.lifecycle.Lifecycle.State.isAtLeast(androidx.lifecycle.Lifecycle.State)"
589+
- "androidx.lifecycle.LifecycleEventObserver(kotlin.Function2)"
585590
- "androidx.navigation.NavController.addOnDestinationChangedListener(androidx.navigation.NavController.OnDestinationChangedListener)"
586591
- "androidx.navigation.NavController.removeOnDestinationChangedListener(androidx.navigation.NavController.OnDestinationChangedListener)"
587592
- "androidx.recyclerview.widget.RecyclerView.findContainingViewHolder(android.view.View)"
@@ -1277,7 +1282,7 @@ datadog:
12771282
- "kotlin.Triple.constructor(kotlin.Nothing?, kotlin.Nothing?, kotlin.Nothing?)"
12781283
# endregion
12791284
# region Kotlin String
1280-
- "kotlin.Any?.takeIf(kotlin.Function1)"
1285+
- "kotlin.Any?.hashCode()"
12811286
- "kotlin.CharSequence.isNullOrEmpty()"
12821287
- "kotlin.String.all(kotlin.Function1)"
12831288
- "kotlin.String.codePoints()"
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
open class com.datadog.android.compose.AcceptAllNavKeyPredicate<T> : com.datadog.android.rum.tracking.ComponentPredicate<T>
1+
open class com.datadog.android.compose.AcceptAllNavKeyPredicate<T: Any> : com.datadog.android.rum.tracking.ComponentPredicate<T>
22
override fun accept(T): Boolean
33
override fun getViewName(T): String?
44
override fun equals(Any?): Boolean
55
override fun hashCode(): Int
6-
interface com.datadog.android.compose.AttributesResolver<T>
6+
interface com.datadog.android.compose.AttributesResolver<T: Any>
77
fun resolveAttributes(T): Map<String, Any?>?
8+
interface com.datadog.android.compose.BackStackKeyResolver<T: Any>
9+
fun getStableKey(T): String
810
annotation com.datadog.android.compose.ComposeInstrumentation
911
fun androidx.compose.ui.Modifier.datadog(String, Boolean = false): androidx.compose.ui.Modifier
1012
annotation com.datadog.android.compose.ExperimentalTrackingApi
13+
class com.datadog.android.compose.HashcodeBackStackKeyResolver<T: Any> : BackStackKeyResolver<T>
14+
override fun getStableKey(T): String
1115
fun trackClick(String, Map<String, Any?> = remember { emptyMap() }, com.datadog.android.api.SdkCore = Datadog.getInstance(), () -> Unit): () -> Unit
1216
fun TrackInteractionEffect(String, androidx.compose.foundation.interaction.InteractionSource, InteractionType, Map<String, Any?> = emptyMap(), com.datadog.android.api.SdkCore = Datadog.getInstance())
1317
sealed class com.datadog.android.compose.InteractionType
@@ -16,5 +20,5 @@ sealed class com.datadog.android.compose.InteractionType
1620
class Scroll : InteractionType
1721
constructor(androidx.compose.foundation.gestures.ScrollableState, androidx.compose.foundation.gestures.Orientation, Boolean = false)
1822
fun NavigationViewTrackingEffect(androidx.navigation.NavController, Boolean = true, com.datadog.android.rum.tracking.ComponentPredicate<androidx.navigation.NavDestination> = AcceptAllNavDestinations(), com.datadog.android.api.SdkCore = Datadog.getInstance())
19-
fun <T> Navigation3TrackingEffect(List<T>, com.datadog.android.rum.tracking.ComponentPredicate<T> = AcceptAllNavKeyPredicate(), AttributesResolver<T>? = null, com.datadog.android.api.SdkCore = Datadog.getInstance())
23+
fun <T: Any> Navigation3TrackingEffect(List<T>, com.datadog.android.rum.tracking.ComponentPredicate<T> = AcceptAllNavKeyPredicate(), BackStackKeyResolver<T> = HashcodeBackStackKeyResolver(), AttributesResolver<T>? = null, com.datadog.android.api.SdkCore = Datadog.getInstance())
2024
fun com.datadog.android.rum.RumConfiguration.Builder.enableComposeActionTracking(): com.datadog.android.rum.RumConfiguration.Builder

integrations/dd-sdk-android-compose/src/main/kotlin/com/datadog/android/compose/AcceptAllNavKeyPredicate.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ package com.datadog.android.compose
99
import com.datadog.android.rum.tracking.ComponentPredicate
1010

1111
/**
12-
* A [ComponentPredicate] that accepts all keys of navigation backstack.
12+
* A [ComponentPredicate] that accepts all keys of navigation back stack.
1313
*
1414
* This predicate is used when you want to track all navigation keys without any filtering.
1515
*
1616
* @param T the type of the component.
1717
*/
18-
open class AcceptAllNavKeyPredicate<T> : ComponentPredicate<T> {
18+
open class AcceptAllNavKeyPredicate<T : Any> : ComponentPredicate<T> {
1919
override fun accept(component: T): Boolean {
2020
return true
2121
}

integrations/dd-sdk-android-compose/src/main/kotlin/com/datadog/android/compose/AttributesResolver.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ package com.datadog.android.compose
1111
*
1212
* @param T the type of the key of navigation back stack.
1313
*/
14-
interface AttributesResolver<T> {
14+
interface AttributesResolver<T : Any> {
1515

1616
/**
1717
* Resolves attributes for the given backstack key.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.compose
8+
9+
/**
10+
* Resolves a stable key for an item in a back stack.
11+
* A stable key means that the same item will always return the same key during its lifetime in the
12+
* back stack, and that two different items should not return the same key.
13+
*
14+
* This is used in [com.datadog.android.rum.RumMonitor.startView] and
15+
* [com.datadog.android.rum.RumMonitor.stopView] as the key to identify items in the back
16+
* stack and track them as Views in RUM.
17+
*
18+
* @param T the type of item in the back stack.
19+
*/
20+
interface BackStackKeyResolver<T : Any> {
21+
22+
/**
23+
* Returns a stable key for the given item.
24+
*
25+
* @param item the item to get the stable key for.
26+
* @return a stable key for the item.
27+
*/
28+
fun getStableKey(item: T): String
29+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.compose
8+
9+
/**
10+
* A [BackStackKeyResolver] that uses the [Any.hashCode] of the item as its stable key.
11+
*
12+
* @param T the type of item in the back stack.
13+
*/
14+
class HashcodeBackStackKeyResolver<T : Any> : BackStackKeyResolver<T> {
15+
16+
override fun getStableKey(
17+
item: T
18+
): String {
19+
return item.hashCode().toString()
20+
}
21+
}

integrations/dd-sdk-android-compose/src/main/kotlin/com/datadog/android/compose/Navigation3.kt

Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ package com.datadog.android.compose
1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.runtime.LaunchedEffect
1212
import androidx.compose.runtime.NonRestartableComposable
13+
import androidx.compose.runtime.State
14+
import androidx.compose.runtime.getValue
15+
import androidx.compose.runtime.produceState
16+
import androidx.compose.ui.platform.LocalLifecycleOwner
17+
import androidx.lifecycle.Lifecycle
18+
import androidx.lifecycle.LifecycleEventObserver
1319
import com.datadog.android.Datadog
1420
import com.datadog.android.api.InternalLogger
1521
import com.datadog.android.api.SdkCore
@@ -19,27 +25,29 @@ import com.datadog.android.compose.internal.SupportLibrary
1925
import com.datadog.android.compose.internal.sendTelemetry
2026
import com.datadog.android.internal.attributes.ViewScopeInstrumentationType
2127
import com.datadog.android.internal.attributes.enrichWithConstantAttribute
22-
import com.datadog.android.rum.ExperimentalRumApi
2328
import com.datadog.android.rum.GlobalRumMonitor
29+
import com.datadog.android.rum.RumMonitor
2430
import com.datadog.android.rum.tracking.ComponentPredicate
2531

2632
/**
2733
* A side effect which should be used to track view navigation with the Navigation3
2834
* for Jetpack Compose setup.
2935
*
3036
* @param T the type of the key of navigation back stack.
31-
* @param backStack backStack of the navigation to watch.
32-
* @param keyPredicate to accept the backstack key that will be taken into account as
37+
* @param backStack back stack of the navigation to watch.
38+
* @param keyPredicate to accept the back stack key that will be taken into account as
3339
* valid RUM View events.
34-
* @param attributesResolver to resolve attributes for the current backstack key.
40+
* @param backStackKeyResolver to resolve stable keys for the back stack keys.
41+
* @param attributesResolver to resolve attributes for the current back stack key.
3542
* @param sdkCore the SDK instance to use. If not provided, default instance will be used.
3643
*/
3744
@Composable
3845
@NonRestartableComposable
39-
@ExperimentalRumApi
40-
fun <T> Navigation3TrackingEffect(
46+
@ExperimentalTrackingApi
47+
fun <T : Any> Navigation3TrackingEffect(
4148
backStack: List<T>,
4249
keyPredicate: ComponentPredicate<T> = AcceptAllNavKeyPredicate(),
50+
backStackKeyResolver: BackStackKeyResolver<T> = HashcodeBackStackKeyResolver(),
4351
attributesResolver: AttributesResolver<T>? = null,
4452
sdkCore: SdkCore = Datadog.getInstance()
4553
) {
@@ -54,16 +62,18 @@ fun <T> Navigation3TrackingEffect(
5462
InternalNavigation3TrackingStrategy(
5563
backStack = backStack,
5664
destinationPredicate = keyPredicate,
65+
backStackKeyResolver = backStackKeyResolver,
5766
attributesResolver = attributesResolver,
5867
sdkCore = sdkCore
5968
)
6069
}
6170

6271
@Composable
6372
@NonRestartableComposable
64-
internal fun <T> InstrumentedNavigation3TrackingEffect(
73+
internal fun <T : Any> InstrumentedNavigation3TrackingEffect(
6574
backStack: List<T>,
6675
keyPredicate: ComponentPredicate<T> = AcceptAllNavKeyPredicate(),
76+
backStackKeyResolver: BackStackKeyResolver<T> = HashcodeBackStackKeyResolver(),
6777
attributesResolver: AttributesResolver<T>? = null,
6878
sdkCore: SdkCore = Datadog.getInstance()
6979
) {
@@ -79,42 +89,95 @@ internal fun <T> InstrumentedNavigation3TrackingEffect(
7989
backStack = backStack,
8090
destinationPredicate = keyPredicate,
8191
attributesResolver = attributesResolver,
92+
backStackKeyResolver = backStackKeyResolver,
8293
sdkCore = sdkCore
8394
)
8495
}
8596

8697
@Composable
8798
@NonRestartableComposable
88-
private fun <T> InternalNavigation3TrackingStrategy(
99+
private fun <T : Any> InternalNavigation3TrackingStrategy(
89100
backStack: List<T>,
90101
destinationPredicate: ComponentPredicate<T> = AcceptAllNavKeyPredicate(),
102+
backStackKeyResolver: BackStackKeyResolver<T> = HashcodeBackStackKeyResolver(),
91103
attributesResolver: AttributesResolver<T>? = null,
92104
sdkCore: SdkCore = Datadog.getInstance()
93105
) {
106+
val topKey = backStack.lastOrNull() ?: return
107+
val isResumed by rememberIsResumed()
94108
val internalLogger = (sdkCore as? FeatureSdkCore)?.internalLogger ?: InternalLogger.UNBOUND
95-
val topKey = backStack.lastOrNull()
96-
LaunchedEffect(topKey) {
97-
topKey?.takeIf { destinationPredicate.accept(it) }?.let { current ->
98-
try {
109+
LaunchedEffect(isResumed, topKey) {
110+
trackBackStack(
111+
topKey = topKey,
112+
isResumed = isResumed,
113+
keyPredicate = destinationPredicate,
114+
backStackKeyResolver = backStackKeyResolver,
115+
attributesResolver = attributesResolver,
116+
rumMonitor = GlobalRumMonitor.get(sdkCore),
117+
internalLogger = internalLogger
118+
)
119+
}
120+
}
121+
122+
internal fun <T : Any> trackBackStack(
123+
topKey: T,
124+
isResumed: Boolean,
125+
keyPredicate: ComponentPredicate<T> = AcceptAllNavKeyPredicate(),
126+
backStackKeyResolver: BackStackKeyResolver<T>,
127+
attributesResolver: AttributesResolver<T>? = null,
128+
rumMonitor: RumMonitor,
129+
internalLogger: InternalLogger
130+
) {
131+
val viewKey = backStackKeyResolver.getStableKey(topKey)
132+
if (isResumed) {
133+
try {
134+
if (keyPredicate.accept(topKey)) {
99135
val attributes =
100-
attributesResolver?.resolveAttributes(current)?.toMutableMap()
136+
attributesResolver?.resolveAttributes(topKey)?.toMutableMap()
101137
?.enrichWithConstantAttribute(ViewScopeInstrumentationType.COMPOSE)
102138
?: emptyMap()
103139

104-
GlobalRumMonitor.get(sdkCore).startView(
105-
name = destinationPredicate.getViewName(current)
106-
?: resolveDefaultViewName(current),
107-
key = current.toString(),
140+
rumMonitor.startView(
141+
name = keyPredicate.getViewName(topKey)
142+
?: resolveDefaultViewName(topKey),
143+
key = viewKey,
108144
attributes = attributes
109145
)
110-
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
146+
} else {
111147
internalLogger.log(
112-
InternalLogger.Level.ERROR,
113-
listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
114-
{ "Internal operation failed on ComponentPredicate" },
115-
e
148+
InternalLogger.Level.DEBUG,
149+
InternalLogger.Target.USER,
150+
{ "The provided keyPredicate did not accept the back stack top key: $topKey" }
116151
)
117152
}
153+
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
154+
internalLogger.log(
155+
InternalLogger.Level.ERROR,
156+
listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
157+
{ "Internal operation failed on ComponentPredicate" },
158+
e
159+
)
160+
}
161+
} else {
162+
rumMonitor.stopView(viewKey)
163+
}
164+
}
165+
166+
@Composable
167+
private fun rememberIsResumed(): State<Boolean> {
168+
val lifecycle = LocalLifecycleOwner.current.lifecycle
169+
return produceState(
170+
initialValue = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED),
171+
key1 = lifecycle
172+
) {
173+
val observer = LifecycleEventObserver { _, _ ->
174+
value = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
175+
}
176+
@Suppress("ThreadSafety") // TODO RUM-525 check composable threading rules
177+
lifecycle.addObserver(observer)
178+
awaitDispose {
179+
@Suppress("ThreadSafety") // TODO RUM-525 check composable threading rules
180+
lifecycle.removeObserver(observer)
118181
}
119182
}
120183
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.compose
8+
9+
import fr.xgouchet.elmyr.annotation.IntForgery
10+
import fr.xgouchet.elmyr.annotation.StringForgery
11+
import fr.xgouchet.elmyr.junit5.ForgeExtension
12+
import org.assertj.core.api.Assertions.assertThat
13+
import org.junit.jupiter.api.Test
14+
import org.junit.jupiter.api.extension.ExtendWith
15+
16+
@ExtendWith(ForgeExtension::class)
17+
internal class HashcodeBackStackKeyResolverTest {
18+
19+
private val testedResolver = HashcodeBackStackKeyResolver<Any>()
20+
21+
@Test
22+
fun `M return item hashcode as string W getStableKey()`(
23+
@StringForgery testItem: String
24+
) {
25+
// Given
26+
val expectedKey = testItem.hashCode().toString()
27+
28+
// When
29+
val actualKey = testedResolver.getStableKey(testItem)
30+
31+
// Then
32+
assertThat(actualKey).isEqualTo(expectedKey)
33+
}
34+
35+
@Test
36+
fun `M return item hashcode as string W getStableKey {data class}`(
37+
@StringForgery testItemId: String,
38+
@IntForgery testItemValue: Int
39+
) {
40+
// Given
41+
val testItem = TestDataClass(testItemId, testItemValue)
42+
val identicalItem = TestDataClass(testItemId, testItemValue)
43+
val expectedKey = identicalItem.hashCode().toString()
44+
45+
// When
46+
val actualKey = testedResolver.getStableKey(testItem)
47+
48+
// Then
49+
assertThat(actualKey).isEqualTo(expectedKey)
50+
}
51+
52+
@Test
53+
fun `M return item hashcode as string W getStableKey {integer}`(
54+
@IntForgery testItem: Int
55+
) {
56+
// Given
57+
val expectedKey = testItem.hashCode().toString()
58+
59+
// When
60+
val actualKey = testedResolver.getStableKey(testItem)
61+
62+
// Then
63+
assertThat(actualKey).isEqualTo(expectedKey)
64+
}
65+
66+
data class TestDataClass(val id: String, val value: Int)
67+
}

0 commit comments

Comments
 (0)