Skip to content

Commit 653e76a

Browse files
RUM-9413: Reproduction test
1 parent 61409f2 commit 653e76a

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

instrumented/integration/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.datadog.gradle.config.AndroidConfig
88
import com.datadog.gradle.config.java17
99
import com.datadog.gradle.config.kotlinConfig
10+
import com.datadog.gradle.config.sampleAppConfig
1011

1112
plugins {
1213
id("com.android.application")
@@ -33,6 +34,10 @@ android {
3334
vectorDrawables.useSupportLibrary = true
3435

3536
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
37+
38+
val us1Config = sampleAppConfig("${rootDir.absolutePath}/config/us1.json")
39+
buildConfigField("String", "DD_BUG_REPRO_TOKEN", "\"${us1Config.token}\"")
40+
buildConfigField("String", "DD_BUG_REPRO_RUM_APP_ID", "\"${us1Config.rumApplicationId}\"")
3641
}
3742

3843
namespace = "com.datadog.android.sdk.integration"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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 android.app.ActivityManager
10+
import android.util.Log
11+
import androidx.test.ext.junit.runners.AndroidJUnit4
12+
import androidx.test.filters.LargeTest
13+
import androidx.test.platform.app.InstrumentationRegistry
14+
import com.datadog.android.Datadog
15+
import com.datadog.android.api.SdkCore
16+
import com.datadog.android.api.feature.FeatureSdkCore
17+
import com.datadog.android.core.configuration.BatchSize
18+
import com.datadog.android.core.configuration.Configuration
19+
import com.datadog.android.core.configuration.UploadFrequency
20+
import com.datadog.android.privacy.TrackingConsent
21+
import com.datadog.android.rum.DdRumContentProvider
22+
import com.datadog.android.rum.GlobalRumMonitor
23+
import com.datadog.android.rum.Rum
24+
import com.datadog.android.rum.RumConfiguration
25+
import com.datadog.android.rum.RumResourceKind
26+
import com.datadog.android.rum.RumResourceMethod
27+
import com.datadog.android.sdk.integration.BuildConfig
28+
import org.junit.After
29+
import org.junit.Assume
30+
import org.junit.Before
31+
import org.junit.Test
32+
import org.junit.runner.RunWith
33+
34+
/**
35+
* Reproduces the bug described in RUM-9413.
36+
*
37+
* In [com.datadog.android.rum.internal.domain.scope.RumViewManagerScope.handleOrphanEvent],
38+
* the condition `applicationDisplayed || !isForegroundProcess` incorrectly routes orphaned
39+
* events to handleBackgroundEvent() even when the app is actively in the foreground between
40+
* two screen transitions.
41+
*
42+
* When backgroundTrackingEnabled = true, this causes a spurious Background view to be
43+
* created for any valid background event type (error, action, resource) that arrives
44+
* between views.
45+
*
46+
* Scenario:
47+
* 1. Start and stop ScreenA — sets applicationDisplayed = true, leaves no active view scope.
48+
* 2. Fire startResource while no view is active and process is in the foreground.
49+
* 3. Bug: applicationDisplayed = true satisfies the condition → handleBackgroundEvent()
50+
* is called → a spurious Background view scope is created to hold the resource.
51+
* 4. Start and stop ScreenB — making the Background view visible as an intruder
52+
* between two legitimate views: ScreenA → Background → ScreenB.
53+
*
54+
* After running this test, find the session in the RUM explorer using the SESSION_ID
55+
* printed to logcat (tag: RUM_BUG_REPRO). Look for the spurious "Background" view
56+
* sandwiched between ScreenA and ScreenB.
57+
*
58+
* Prerequisites: config/us1.json must contain a valid token and rumApplicationId.
59+
* The test is automatically skipped if credentials are absent (e.g. in CI).
60+
*/
61+
@RunWith(AndroidJUnit4::class)
62+
@LargeTest
63+
internal class RumBackgroundViewBugReproductionTest {
64+
65+
private lateinit var sdkCore: SdkCore
66+
67+
@Before
68+
fun setUp() {
69+
Assume.assumeTrue(
70+
"config/us1.json credentials required — skipping on this machine",
71+
BuildConfig.DD_BUG_REPRO_TOKEN.isNotEmpty()
72+
)
73+
74+
InstrumentationRegistry.getInstrumentation()
75+
.targetContext
76+
.cacheDir
77+
.deleteRecursively()
78+
79+
val config = Configuration.Builder(
80+
clientToken = BuildConfig.DD_BUG_REPRO_TOKEN,
81+
env = BUG_REPRO_ENV
82+
)
83+
.setBatchSize(BatchSize.SMALL)
84+
.setUploadFrequency(UploadFrequency.FREQUENT)
85+
.build()
86+
87+
sdkCore = checkNotNull(
88+
Datadog.initialize(
89+
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext,
90+
config,
91+
TrackingConsent.GRANTED
92+
)
93+
)
94+
95+
val rumConfig = RumConfiguration.Builder(BuildConfig.DD_BUG_REPRO_RUM_APP_ID)
96+
.trackBackgroundEvents(true)
97+
.build()
98+
99+
Rum.enable(rumConfig, sdkCore)
100+
101+
// Simulate a foreground process — same as all other integration tests.
102+
// Without this, the test runner process has IMPORTANCE_FOREGROUND_SERVICE which
103+
// would make !isForegroundProcess = true, masking the applicationDisplayed bug path.
104+
DdRumContentProvider.processImportance =
105+
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
106+
}
107+
108+
@After
109+
fun tearDown() {
110+
Datadog.stopInstance()
111+
}
112+
113+
@Test
114+
fun reproduceSpuriousBackgroundViewOnOrphanedResourceBetweenForegroundViews() {
115+
val rumMonitor = GlobalRumMonitor.get(sdkCore)
116+
117+
// Step 1: Start and stop a view.
118+
// This sets applicationDisplayed = true in RumViewManagerScope and leaves
119+
// no active RumViewScope behind.
120+
rumMonitor.startView(VIEW_KEY, VIEW_NAME)
121+
rumMonitor.stopView(VIEW_KEY)
122+
123+
// Step 2: Fire a resource event while no view is active.
124+
// The process importance is IMPORTANCE_FOREGROUND so this is a foreground orphan.
125+
// Bug: applicationDisplayed = true satisfies the condition
126+
// `else if (applicationDisplayed || !isForegroundProcess)`
127+
// in RumViewManagerScope.handleOrphanEvent(), causing handleBackgroundEvent() to be
128+
// called. Since trackBackgroundEvents = true and StartResource is a validBackgroundEventType,
129+
// a spurious Background RumViewScope is created.
130+
rumMonitor.startResource(RESOURCE_KEY, RumResourceMethod.GET, RESOURCE_URL)
131+
rumMonitor.stopResource(RESOURCE_KEY, 200, null, RumResourceKind.OTHER)
132+
133+
Thread.sleep(300)
134+
135+
// Step 3: Start a second real view to illustrate that the spurious Background view
136+
// was created in the middle of two legitimate foreground views.
137+
// Expected (after fix): ScreenA → ScreenB, no Background view in between.
138+
// Actual (bug): ScreenA → Background → ScreenB.
139+
rumMonitor.startView(VIEW_KEY_2, VIEW_NAME_2)
140+
rumMonitor.stopView(VIEW_KEY_2)
141+
142+
// Step 4: Wait for the SDK to write and upload the batch.
143+
// BatchSize.SMALL + UploadFrequency.FREQUENT means uploads happen every ~500ms.
144+
Thread.sleep(UPLOAD_WAIT_MS)
145+
146+
// Retrieve the session ID for lookup in the RUM explorer.
147+
val sessionId = (sdkCore as? FeatureSdkCore)
148+
?.getFeatureContext("rum")
149+
?.get("session_id") as? String
150+
?: "unknown"
151+
152+
Log.w(TAG, "========================================")
153+
Log.w(TAG, "BUG REPRODUCTION COMPLETE")
154+
Log.w(TAG, "SESSION_ID: $sessionId")
155+
Log.w(TAG, "Find the spurious Background view at:")
156+
Log.w(TAG, "https://app.datadoghq.com/rum/sessions?query=%40session.id%3A$sessionId")
157+
Log.w(TAG, "========================================")
158+
}
159+
160+
companion object {
161+
private const val TAG = "RUM_BUG_REPRO"
162+
private const val BUG_REPRO_ENV = "bug-repro"
163+
private const val VIEW_KEY = "bug-repro-view-key-1"
164+
private const val VIEW_NAME = "BugReproScreenA"
165+
private const val VIEW_KEY_2 = "bug-repro-view-key-2"
166+
private const val VIEW_NAME_2 = "BugReproScreenB"
167+
private const val RESOURCE_KEY = "bug-repro-resource-key"
168+
private const val RESOURCE_URL = "https://httpbin.org/get"
169+
private const val UPLOAD_WAIT_MS = 15_000L
170+
}
171+
}

0 commit comments

Comments
 (0)