Skip to content

Commit 53d8975

Browse files
committed
RUM-17613 Add integration tests for timeseries collection
Add a playground activity and an instrumented test that assert CPU and memory timeseries are collected, flushed on backgrounding and resumed on return to foreground. Ref: RUM-17613
1 parent c932d24 commit 53d8975

5 files changed

Lines changed: 225 additions & 0 deletions

File tree

features/dd-sdk-android-rum/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ dependencies {
105105
)
106106
}
107107
}
108+
testFixturesImplementation(libs.gson)
108109
testFixturesImplementation(libs.kotlin)
110+
testFixturesImplementation(libs.jUnit4)
109111
testFixturesImplementation(libs.bundles.jUnit5)
110112
testFixturesImplementation(libs.okHttp)
111113
testFixturesImplementation(libs.bundles.testTools)

instrumented/integration/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.datadog.gradle.config.AndroidConfig
88
import com.datadog.gradle.config.depotProxied
99
import com.datadog.gradle.config.java17
1010
import com.datadog.gradle.config.kotlinConfig
11+
import org.gradle.api.artifacts.ModuleDependency
1112

1213
plugins {
1314
id("ktlint")
@@ -134,6 +135,11 @@ dependencies {
134135
androidTestImplementation(libs.bundles.integrationTests)
135136
androidTestImplementation(libs.okHttpMock)
136137
androidTestImplementation(project(":features:dd-sdk-android-trace-internal"))
138+
androidTestImplementation(
139+
(testFixtures(project(":features:dd-sdk-android-rum")) as ModuleDependency).apply {
140+
exclude(mapOf("group" to "dd-sdk-android-timeseries.tools", "module" to "unit"))
141+
}
142+
)
137143
androidTestImplementation(testFixtures(project(":features:dd-sdk-android-trace")))
138144
if (project.hasProperty(com.datadog.gradle.Properties.USE_API21_JAVA_BACKPORT)) {
139145
// this is needed to make AssertJ working on APIs <24
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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.sdk.integration.rum
8+
9+
import androidx.test.ext.junit.runners.AndroidJUnit4
10+
import androidx.test.filters.LargeTest
11+
import androidx.test.platform.app.InstrumentationRegistry
12+
import com.datadog.android.privacy.TrackingConsent
13+
import com.datadog.android.rum.assertj.TimeseriesCpuEventAssert
14+
import com.datadog.android.rum.assertj.TimeseriesMemoryEventAssert
15+
import com.datadog.android.rum.model.TimeseriesCpuEvent
16+
import com.datadog.android.rum.model.TimeseriesMemoryEvent
17+
import com.datadog.android.sdk.integration.RuntimeConfig
18+
import com.datadog.android.sdk.integration.rum.TimeseriesTrackingPlaygroundActivity.Companion.SAMPLE_INTERVAL_MS
19+
import com.datadog.android.sdk.rules.HandledRequest
20+
import com.datadog.android.sdk.rules.RumMockServerActivityTestRule
21+
import com.datadog.tools.unit.ConditionWatcher
22+
import com.google.gson.JsonObject
23+
import org.assertj.core.api.Assertions.assertThat
24+
import org.junit.Assert.assertEquals
25+
import org.junit.Assert.assertTrue
26+
import org.junit.Rule
27+
import org.junit.Test
28+
import org.junit.runner.RunWith
29+
30+
@RunWith(AndroidJUnit4::class)
31+
@LargeTest
32+
internal class TimeseriesCollectionTest {
33+
34+
@get:Rule
35+
val mockServerRule = RumMockServerActivityTestRule(
36+
TimeseriesTrackingPlaygroundActivity::class.java,
37+
keepRequests = true,
38+
trackingConsent = TrackingConsent.GRANTED
39+
)
40+
41+
@Test
42+
fun verifyCpuAndMemoryTimeseriesAreCollected() {
43+
// Given
44+
Thread.sleep(FOREGROUND_COLLECTION_DURATION_MS)
45+
46+
// When
47+
moveActivityToBackground()
48+
val events: List<JsonObject> = waitForTimeseriesEvents(expectedCount = TIMESERIES_PIPELINE_COUNT)
49+
50+
// Then
51+
assertTrue(events.size >= TIMESERIES_PIPELINE_COUNT)
52+
53+
TimeseriesCpuEventAssert.assertThat(events.cpuEvents().single())
54+
.hasDataPointsCount(EXPECTED_FOREGROUND_CPU_DATA_POINTS)
55+
56+
TimeseriesMemoryEventAssert.assertThat(events.memoryEvents().single())
57+
.hasDataPointsCount(EXPECTED_FOREGROUND_MEMORY_DATA_POINTS)
58+
}
59+
60+
@Test
61+
fun verifyCollectionFlushesPausesAndResumesAcrossBackgroundTransition() {
62+
// Given
63+
Thread.sleep(FOREGROUND_COLLECTION_DURATION_MS)
64+
65+
// When
66+
moveActivityToBackground()
67+
val firstForegroundEvents = waitForTimeseriesEvents(expectedCount = TIMESERIES_PIPELINE_COUNT)
68+
Thread.sleep(BACKGROUND_OBSERVATION_DURATION_MS)
69+
val backgroundEvents = timeseriesEvents()
70+
moveActivityToForeground()
71+
Thread.sleep(FOREGROUND_COLLECTION_DURATION_MS)
72+
moveActivityToBackground()
73+
val secondForegroundEvents = waitForTimeseriesEvents(expectedCount = TIMESERIES_PIPELINE_COUNT * 2)
74+
75+
// Then
76+
assertEquals(TIMESERIES_PIPELINE_COUNT, firstForegroundEvents.size)
77+
TimeseriesCpuEventAssert.assertThat(firstForegroundEvents.cpuEvents().single())
78+
.hasDataPointsCount(EXPECTED_FOREGROUND_CPU_DATA_POINTS)
79+
TimeseriesMemoryEventAssert.assertThat(firstForegroundEvents.memoryEvents().single())
80+
.hasDataPointsCount(EXPECTED_FOREGROUND_MEMORY_DATA_POINTS)
81+
assertThat(firstForegroundEvents.size).isEqualTo(backgroundEvents.size)
82+
assertThat(secondForegroundEvents.size).isEqualTo(TIMESERIES_PIPELINE_COUNT * 2)
83+
secondForegroundEvents.memoryEvents().forEach {
84+
TimeseriesMemoryEventAssert.assertThat(it)
85+
.hasDataPointsCount(EXPECTED_FOREGROUND_MEMORY_DATA_POINTS)
86+
}
87+
secondForegroundEvents.cpuEvents().forEach {
88+
TimeseriesCpuEventAssert.assertThat(it)
89+
.hasDataPointsCount(EXPECTED_FOREGROUND_CPU_DATA_POINTS)
90+
}
91+
}
92+
93+
private fun moveActivityToBackground() {
94+
val instrumentation = InstrumentationRegistry.getInstrumentation()
95+
instrumentation.runOnMainSync {
96+
instrumentation.callActivityOnPause(mockServerRule.activity)
97+
instrumentation.callActivityOnStop(mockServerRule.activity)
98+
}
99+
instrumentation.waitForIdleSync()
100+
}
101+
102+
private fun moveActivityToForeground() {
103+
val instrumentation = InstrumentationRegistry.getInstrumentation()
104+
instrumentation.runOnMainSync {
105+
instrumentation.callActivityOnStart(mockServerRule.activity)
106+
instrumentation.callActivityOnResume(mockServerRule.activity)
107+
}
108+
instrumentation.waitForIdleSync()
109+
}
110+
111+
private fun waitForTimeseriesEvents(expectedCount: Int): List<JsonObject> {
112+
var events = emptyList<JsonObject>()
113+
ConditionWatcher {
114+
events = timeseriesEvents()
115+
assertTrue(events.size >= expectedCount)
116+
true
117+
}.doWait(timeoutMs = RumTest.FINAL_WAIT_MS)
118+
return events
119+
}
120+
121+
private fun timeseriesEvents(): List<JsonObject> = mockServerRule
122+
.getRequests(RuntimeConfig.rumEndpointUrl)
123+
.rumEvents()
124+
.filter { it.get(TYPE_KEY)?.asString == TIMESERIES_TYPE }
125+
126+
private fun List<HandledRequest>.rumEvents(): List<JsonObject> = flatMap { request ->
127+
request.textBody?.let(::rumPayloadToJsonList).orEmpty()
128+
}
129+
130+
companion object {
131+
private const val CPU_TIMESERIES_NAME = "cpu"
132+
private const val MEMORY_TIMESERIES_NAME = "memory"
133+
private const val TIMESERIES_TYPE = "timeseries"
134+
private const val TYPE_KEY = "type"
135+
private const val TIMESERIES_KEY = "timeseries"
136+
private const val NAME_KEY = "name"
137+
138+
// One independent pipeline for each timeseries source: CPU and memory.
139+
private const val TIMESERIES_PIPELINE_COUNT = 2
140+
private const val ACTIVITY_STOP_DELAY_MS = 200L
141+
private const val FOREGROUND_COLLECTION_DURATION_MS = SAMPLE_INTERVAL_MS * 2 + SAMPLE_INTERVAL_MS / 4
142+
private const val UPLOAD_GRACE_PERIOD_MS = 500L
143+
private const val BACKGROUND_OBSERVATION_DURATION_MS =
144+
SAMPLE_INTERVAL_MS + UPLOAD_GRACE_PERIOD_MS
145+
146+
private const val EXPECTED_FOREGROUND_MEMORY_DATA_POINTS =
147+
((FOREGROUND_COLLECTION_DURATION_MS + ACTIVITY_STOP_DELAY_MS) / SAMPLE_INTERVAL_MS).toInt()
148+
private const val EXPECTED_FOREGROUND_CPU_DATA_POINTS =
149+
EXPECTED_FOREGROUND_MEMORY_DATA_POINTS - 1
150+
151+
private val JsonObject.timeseriesName: String?
152+
get() = getAsJsonObject(TIMESERIES_KEY).get(NAME_KEY).asString
153+
154+
private fun List<JsonObject>.filterByName(name: String): List<JsonObject> = filter { it.timeseriesName == name }
155+
156+
private fun List<JsonObject>.cpuEvents(): List<TimeseriesCpuEvent> =
157+
filterByName(CPU_TIMESERIES_NAME).map(TimeseriesCpuEvent::fromJsonObject)
158+
159+
private fun List<JsonObject>.memoryEvents(): List<TimeseriesMemoryEvent> =
160+
filterByName(MEMORY_TIMESERIES_NAME).map(TimeseriesMemoryEvent::fromJsonObject)
161+
}
162+
}

instrumented/integration/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
android:name=".rum.ActivityTrackingPlaygroundActivity"
3030
android:label="@string/rum_activity_view_tracking_end_to_end"
3131
android:windowSoftInputMode="adjustResize"/>
32+
<activity
33+
android:name=".rum.TimeseriesTrackingPlaygroundActivity"
34+
android:label="RUM timeseries end-to-end" />
3235
<activity
3336
android:name=".rum.KioskSplashPlaygroundActivity"
3437
android:label="@string/activity_kiosk_end_to_end"
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CheckInternal")
7+
8+
package com.datadog.android.sdk.integration.rum
9+
10+
import android.app.Activity
11+
import android.app.ActivityManager
12+
import android.os.Bundle
13+
import com.datadog.android.Datadog
14+
import com.datadog.android.rum.DdRumContentProvider
15+
import com.datadog.android.rum.ExperimentalRumApi
16+
import com.datadog.android.rum.Rum
17+
import com.datadog.android.rum.timeseries.TimeseriesConfiguration
18+
import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy
19+
import com.datadog.android.sdk.integration.RuntimeConfig
20+
import com.datadog.android.sdk.utils.getTrackingConsent
21+
22+
@OptIn(ExperimentalRumApi::class)
23+
internal class TimeseriesTrackingPlaygroundActivity : Activity() {
24+
25+
override fun onCreate(savedInstanceState: Bundle?) {
26+
super.onCreate(savedInstanceState)
27+
28+
val sdkCore = checkNotNull(
29+
Datadog.initialize(
30+
this,
31+
RuntimeConfig.configBuilder().build(),
32+
intent.getTrackingConsent()
33+
)
34+
)
35+
36+
DdRumContentProvider.processImportance = ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
37+
38+
Rum.enable(
39+
sdkCore = sdkCore,
40+
rumConfiguration = RuntimeConfig.rumConfigBuilder()
41+
.useViewTrackingStrategy(ActivityViewTrackingStrategy(trackExtras = false))
42+
.setTimeseriesConfiguration(
43+
TimeseriesConfiguration.Builder().build()
44+
)
45+
.build()
46+
)
47+
}
48+
49+
companion object {
50+
const val SAMPLE_INTERVAL_MS = TimeseriesConfiguration.DEFAULT_INTERVAL_MS
51+
}
52+
}

0 commit comments

Comments
 (0)