|
| 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