Skip to content

Commit 4f1bef2

Browse files
committed
RUM-9908 Propagate parent attributes to the ActionScope
1 parent c932cc3 commit 4f1bef2

File tree

5 files changed

+264
-34
lines changed

5 files changed

+264
-34
lines changed

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package com.datadog.android.rum.internal.domain.scope
99
import androidx.annotation.WorkerThread
1010
import com.datadog.android.api.storage.DataWriter
1111
import com.datadog.android.core.InternalSdkCore
12-
import com.datadog.android.rum.GlobalRumMonitor
1312
import com.datadog.android.rum.RumActionType
1413
import com.datadog.android.rum.internal.FeaturesContextResolver
1514
import com.datadog.android.rum.internal.domain.RumContext
@@ -51,9 +50,7 @@ internal class RumActionScope(
5150
private var lastInteractionNanos: Long = startedNanos
5251
private val networkInfo = sdkCore.networkInfo
5352

54-
internal val attributes: MutableMap<String, Any?> = initialAttributes.toMutableMap().apply {
55-
putAll(GlobalRumMonitor.get(sdkCore).getAttributes())
56-
}
53+
internal val actionAttributes: MutableMap<String, Any?> = initialAttributes.toMutableMap()
5754

5855
private val ongoingResourceKeys = mutableListOf<WeakReference<Any>>()
5956

@@ -99,6 +96,10 @@ internal class RumActionScope(
9996
return parentScope.getRumContext()
10097
}
10198

99+
override fun getCustomAttributes(): Map<String, Any?> {
100+
return parentScope.getCustomAttributes() + actionAttributes
101+
}
102+
102103
override fun isActive(): Boolean {
103104
return !stopped
104105
}
@@ -141,7 +142,7 @@ internal class RumActionScope(
141142
) {
142143
event.type?.let { type = it }
143144
event.name?.let { name = it }
144-
attributes.putAll(event.attributes)
145+
actionAttributes.putAll(event.attributes)
145146
stopped = true
146147
stoppedNanos = now
147148
lastInteractionNanos = now
@@ -205,8 +206,6 @@ internal class RumActionScope(
205206
if (sent) return
206207

207208
val actualType = type
208-
attributes.putAll(GlobalRumMonitor.get(sdkCore).getAttributes())
209-
val eventAttributes = attributes.toMutableMap()
210209
val rumContext = getRumContext()
211210

212211
// make a copy so that closure captures at the state as of now
@@ -303,7 +302,7 @@ internal class RumActionScope(
303302
brand = datadogContext.deviceInfo.deviceBrand,
304303
architecture = datadogContext.deviceInfo.architecture
305304
),
306-
context = ActionEvent.Context(additionalProperties = eventAttributes),
305+
context = ActionEvent.Context(additionalProperties = getCustomAttributes().toMutableMap()),
307306
dd = ActionEvent.Dd(
308307
session = ActionEvent.DdSession(
309308
sessionPrecondition = rumContext.sessionStartReason.toActionSessionPrecondition()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
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.rum.internal.domain.scope
8+
9+
import com.datadog.android.api.InternalLogger
10+
import com.datadog.android.api.context.DatadogContext
11+
import com.datadog.android.api.context.NetworkInfo
12+
import com.datadog.android.api.feature.Feature
13+
import com.datadog.android.api.feature.FeatureScope
14+
import com.datadog.android.api.storage.DataWriter
15+
import com.datadog.android.api.storage.EventBatchWriter
16+
import com.datadog.android.api.storage.EventType
17+
import com.datadog.android.core.InternalSdkCore
18+
import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver
19+
import com.datadog.android.rum.RumActionType
20+
import com.datadog.android.rum.assertj.ActionEventAssert.Companion.assertThat
21+
import com.datadog.android.rum.internal.FeaturesContextResolver
22+
import com.datadog.android.rum.internal.domain.RumContext
23+
import com.datadog.android.rum.internal.domain.Time
24+
import com.datadog.android.rum.internal.vitals.VitalMonitor
25+
import com.datadog.android.rum.model.ActionEvent
26+
import com.datadog.android.rum.utils.config.GlobalRumMonitorTestConfiguration
27+
import com.datadog.android.rum.utils.forge.Configurator
28+
import com.datadog.tools.unit.annotations.TestConfigurationsProvider
29+
import com.datadog.tools.unit.extensions.TestConfigurationExtension
30+
import com.datadog.tools.unit.extensions.config.TestConfiguration
31+
import com.datadog.tools.unit.forge.exhaustiveAttributes
32+
import fr.xgouchet.elmyr.Forge
33+
import fr.xgouchet.elmyr.annotation.BoolForgery
34+
import fr.xgouchet.elmyr.annotation.FloatForgery
35+
import fr.xgouchet.elmyr.annotation.Forgery
36+
import fr.xgouchet.elmyr.annotation.LongForgery
37+
import fr.xgouchet.elmyr.annotation.StringForgery
38+
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
39+
import fr.xgouchet.elmyr.junit5.ForgeExtension
40+
import org.assertj.core.api.Assertions.assertThat
41+
import org.junit.jupiter.api.BeforeEach
42+
import org.junit.jupiter.api.Test
43+
import org.junit.jupiter.api.extension.ExtendWith
44+
import org.junit.jupiter.api.extension.Extensions
45+
import org.mockito.Mock
46+
import org.mockito.junit.jupiter.MockitoExtension
47+
import org.mockito.junit.jupiter.MockitoSettings
48+
import org.mockito.kotlin.any
49+
import org.mockito.kotlin.argumentCaptor
50+
import org.mockito.kotlin.doAnswer
51+
import org.mockito.kotlin.doReturn
52+
import org.mockito.kotlin.eq
53+
import org.mockito.kotlin.mock
54+
import org.mockito.kotlin.verify
55+
import org.mockito.kotlin.whenever
56+
import org.mockito.quality.Strictness
57+
58+
@Extensions(
59+
ExtendWith(MockitoExtension::class),
60+
ExtendWith(ForgeExtension::class),
61+
ExtendWith(TestConfigurationExtension::class)
62+
)
63+
@MockitoSettings(strictness = Strictness.LENIENT)
64+
@ForgeConfiguration(Configurator::class)
65+
internal class RumActionScopeAttributePropagationTest {
66+
67+
lateinit var testedScope: RumActionScope
68+
69+
@Mock
70+
lateinit var mockSdkCore: InternalSdkCore
71+
72+
@Mock
73+
lateinit var mockParentScope: RumScope
74+
75+
@Mock
76+
lateinit var mockWriter: DataWriter<Any>
77+
78+
@Mock
79+
lateinit var mockResolver: FirstPartyHostHeaderTypeResolver
80+
81+
@Mock
82+
lateinit var mockCpuVitalMonitor: VitalMonitor
83+
84+
@Mock
85+
lateinit var mockMemoryVitalMonitor: VitalMonitor
86+
87+
@Mock
88+
lateinit var mockFrameRateVitalMonitor: VitalMonitor
89+
90+
@Mock
91+
lateinit var mockInternalLogger: InternalLogger
92+
93+
@Mock
94+
lateinit var mockRumFeatureScope: FeatureScope
95+
96+
@Mock
97+
lateinit var mockEventBatchWriter: EventBatchWriter
98+
99+
@Mock
100+
lateinit var mockFeaturesContextResolver: FeaturesContextResolver
101+
102+
lateinit var fakeParentAttributes: Map<String, Any?>
103+
104+
lateinit var fakeActionAttributes: Map<String, Any?>
105+
106+
@Forgery
107+
lateinit var fakeParentContext: RumContext
108+
109+
lateinit var fakeEventTime: Time
110+
111+
lateinit var fakeEvent: RumRawEvent
112+
113+
@Forgery
114+
lateinit var fakeType: RumActionType
115+
116+
@StringForgery
117+
lateinit var fakeName: String
118+
119+
@Forgery
120+
lateinit var fakeNetworkInfoAtScopeStart: NetworkInfo
121+
122+
@Forgery
123+
lateinit var fakeDatadogContext: DatadogContext
124+
125+
@BoolForgery
126+
var fakeHasReplay: Boolean = false
127+
128+
@FloatForgery(min = 0f, max = 100f)
129+
var fakeSampleRate: Float = 0f
130+
131+
@BoolForgery
132+
var fakeTrackFrustrations: Boolean = true
133+
134+
@LongForgery(-1000L, 1000L)
135+
var fakeServerOffset: Long = 0L
136+
137+
@BeforeEach
138+
fun `set up`(forge: Forge) {
139+
fakeEventTime = Time()
140+
141+
fakeParentAttributes = forge.exhaustiveAttributes()
142+
fakeActionAttributes = forge.exhaustiveAttributes()
143+
whenever(mockParentScope.getCustomAttributes()) doReturn fakeParentAttributes.toMutableMap()
144+
whenever(mockParentScope.getRumContext()) doReturn fakeParentContext
145+
146+
whenever(rumMonitor.mockSdkCore.internalLogger) doReturn mock()
147+
whenever(rumMonitor.mockSdkCore.networkInfo) doReturn fakeNetworkInfoAtScopeStart
148+
whenever(rumMonitor.mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeatureScope
149+
whenever(mockRumFeatureScope.withWriteContext(any(), any())) doAnswer {
150+
val callback = it.getArgument<(DatadogContext, EventBatchWriter) -> Unit>(1)
151+
callback.invoke(fakeDatadogContext, mockEventBatchWriter)
152+
}
153+
whenever(mockWriter.write(eq(mockEventBatchWriter), any(), eq(EventType.DEFAULT))) doReturn true
154+
155+
testedScope = RumActionScope(
156+
parentScope = mockParentScope,
157+
sdkCore = rumMonitor.mockSdkCore,
158+
waitForStop = false,
159+
eventTime = fakeEventTime,
160+
initialType = fakeType,
161+
initialName = fakeName,
162+
initialAttributes = fakeActionAttributes,
163+
serverTimeOffsetInMs = fakeServerOffset,
164+
inactivityThresholdMs = TEST_INACTIVITY_MS,
165+
maxDurationMs = TEST_MAX_DURATION_MS,
166+
featuresContextResolver = mockFeaturesContextResolver,
167+
trackFrustrations = true,
168+
sampleRate = fakeSampleRate
169+
)
170+
}
171+
172+
// region Propagate parent attributes
173+
174+
@Test
175+
fun `M return parent and action attributes W getCustomAttributes()`() {
176+
// Given
177+
val expectedAttributes = mutableMapOf<String, Any?>()
178+
expectedAttributes.putAll(fakeParentAttributes)
179+
expectedAttributes.putAll(fakeActionAttributes)
180+
181+
// When
182+
val customAttributes = testedScope.getCustomAttributes()
183+
184+
// Then
185+
assertThat(customAttributes)
186+
.containsExactlyInAnyOrderEntriesOf(expectedAttributes)
187+
}
188+
189+
@Test
190+
fun `M send Action with parent attributes W handleEvent(any)`() {
191+
// Given
192+
val expectedAttributes = mutableMapOf<String, Any?>()
193+
expectedAttributes.putAll(fakeParentAttributes)
194+
expectedAttributes.putAll(fakeActionAttributes)
195+
196+
// When
197+
Thread.sleep(TEST_INACTIVITY_MS * 2)
198+
testedScope.handleEvent(mockEvent(), mockWriter)
199+
200+
// Then
201+
argumentCaptor<ActionEvent> {
202+
verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT))
203+
assertThat(lastValue)
204+
.containsExactlyContextAttributes(expectedAttributes)
205+
}
206+
}
207+
208+
// endregion
209+
210+
// region Internal
211+
212+
private fun mockEvent(): RumRawEvent {
213+
val event: RumRawEvent = mock()
214+
whenever(event.eventTime) doReturn Time()
215+
return event
216+
}
217+
218+
// endregion
219+
220+
companion object {
221+
222+
val rumMonitor = GlobalRumMonitorTestConfiguration()
223+
224+
@TestConfigurationsProvider
225+
@JvmStatic
226+
fun getTestConfigurations(): List<TestConfiguration> {
227+
return listOf(rumMonitor)
228+
}
229+
230+
private const val TEST_SLEEP_MS = 50L
231+
private const val TEST_INACTIVITY_MS = TEST_SLEEP_MS * 3
232+
private const val TEST_MAX_DURATION_MS = TEST_SLEEP_MS * 10
233+
}
234+
}

features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ import org.mockito.kotlin.verifyNoMoreInteractions
6363
import org.mockito.kotlin.whenever
6464
import org.mockito.quality.Strictness
6565
import java.util.concurrent.TimeUnit
66-
import kotlin.math.max
6766

6867
@Extensions(
6968
ExtendWith(MockitoExtension::class),
@@ -1629,13 +1628,13 @@ internal class RumActionScopeTest {
16291628
forge: Forge
16301629
) {
16311630
// Given
1632-
val fakeGlobalAttributes = forge.aFilteredMap(excludedKeys = fakeAttributes.keys) {
1631+
val fakeParentAttributes = forge.aFilteredMap(excludedKeys = fakeAttributes.keys) {
16331632
anHexadecimalString() to anAsciiString()
16341633
}
16351634
val expectedAttributes = mutableMapOf<String, Any?>()
1635+
expectedAttributes.putAll(fakeParentAttributes)
16361636
expectedAttributes.putAll(fakeAttributes)
1637-
expectedAttributes.putAll(fakeGlobalAttributes)
1638-
whenever(rumMonitor.mockInstance.getAttributes()) doReturn fakeGlobalAttributes
1637+
whenever(mockParentScope.getCustomAttributes()) doReturn fakeParentAttributes
16391638
testedScope = RumActionScope(
16401639
mockParentScope,
16411640
rumMonitor.mockSdkCore,
@@ -1651,7 +1650,6 @@ internal class RumActionScopeTest {
16511650
fakeTrackFrustrations,
16521651
fakeSampleRate
16531652
)
1654-
whenever(rumMonitor.mockInstance.getAttributes()) doReturn emptyMap()
16551653
fakeParentContext = fakeParentContext.copy(
16561654
syntheticsTestId = fakeTestId,
16571655
syntheticsResultId = fakeResultId
@@ -1711,17 +1709,17 @@ internal class RumActionScopeTest {
17111709
}
17121710

17131711
@Test
1714-
fun `M send Action with initial global attributes after threshold W init()+handleEvent(any) `(
1712+
fun `M send Action with parent attributes after threshold W init()+handleEvent(any) `(
17151713
forge: Forge
17161714
) {
17171715
// Given
1718-
val fakeGlobalAttributes = forge.aFilteredMap(excludedKeys = fakeAttributes.keys) {
1716+
val fakeParentAttributes = forge.aFilteredMap(excludedKeys = fakeAttributes.keys) {
17191717
anHexadecimalString() to anAsciiString()
17201718
}
17211719
val expectedAttributes = mutableMapOf<String, Any?>()
1720+
expectedAttributes.putAll(fakeParentAttributes)
17221721
expectedAttributes.putAll(fakeAttributes)
1723-
expectedAttributes.putAll(fakeGlobalAttributes)
1724-
whenever(rumMonitor.mockInstance.getAttributes()) doReturn fakeGlobalAttributes
1722+
whenever(mockParentScope.getCustomAttributes()) doReturn fakeParentAttributes
17251723
testedScope = RumActionScope(
17261724
mockParentScope,
17271725
rumMonitor.mockSdkCore,
@@ -1737,7 +1735,6 @@ internal class RumActionScopeTest {
17371735
fakeTrackFrustrations,
17381736
fakeSampleRate
17391737
)
1740-
whenever(rumMonitor.mockInstance.getAttributes()) doReturn emptyMap()
17411738

17421739
// When
17431740
Thread.sleep(TEST_INACTIVITY_MS)
@@ -1792,18 +1789,18 @@ internal class RumActionScopeTest {
17921789
}
17931790

17941791
@Test
1795-
fun `M send Action with global attributes after threshold W handleEvent(any)`(
1792+
fun `M send Action with parent attributes after threshold W handleEvent(any)`(
17961793
forge: Forge
17971794
) {
17981795
// Given
1799-
val fakeGlobalAttributes = forge.aFilteredMap(excludedKeys = fakeAttributes.keys) {
1796+
val fakeParentAttributes = forge.aFilteredMap(excludedKeys = fakeAttributes.keys) {
18001797
anHexadecimalString() to anAsciiString()
18011798
}
18021799
val expectedAttributes = mutableMapOf<String, Any?>()
1800+
expectedAttributes.putAll(fakeParentAttributes)
18031801
expectedAttributes.putAll(fakeAttributes)
1804-
expectedAttributes.putAll(fakeGlobalAttributes)
1802+
whenever(mockParentScope.getCustomAttributes()) doReturn fakeParentAttributes
18051803
Thread.sleep(TEST_INACTIVITY_MS)
1806-
whenever(rumMonitor.mockInstance.getAttributes()) doReturn fakeGlobalAttributes
18071804

18081805
// When
18091806
val result = testedScope.handleEvent(mockEvent(), mockWriter)

0 commit comments

Comments
 (0)