diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt index d43e590f00..965fb72c3a 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt @@ -11,6 +11,7 @@ import android.os.Looper import com.datadog.android.Datadog import com.datadog.android.api.InternalLogger import com.datadog.android.api.SdkCore +import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.sampling.RateBasedSampler @@ -30,6 +31,7 @@ object Rum { * @param sdkCore SDK instance to register feature in. If not provided, default SDK instance * will be used. */ + @Suppress("ReturnCount") @JvmOverloads @JvmStatic fun enable(rumConfiguration: RumConfiguration, sdkCore: SdkCore = Datadog.getInstance()) { @@ -52,18 +54,35 @@ object Rum { return } + if (sdkCore.getFeature(Feature.RUM_FEATURE_NAME) != null) { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { RUM_FEATURE_ALREADY_ENABLED } + ) + return + } + val rumFeature = RumFeature( - sdkCore = sdkCore as FeatureSdkCore, + sdkCore = sdkCore, applicationId = rumConfiguration.applicationId, configuration = rumConfiguration.featureConfiguration ) sdkCore.registerFeature(rumFeature) + val rumMonitor = createMonitor(sdkCore, rumFeature) GlobalRumMonitor.registerIfAbsent( - monitor = createMonitor(sdkCore, rumFeature), + monitor = rumMonitor, sdkCore ) + + // TODO RUM-0000 there is a small chance of application crashing between RUM monitor + // registration and the moment SDK init is processed, in this case we will miss this crash + // (it won't activate new session). Ideally we should start session when monitor is created + // and before it is registered, but with current code (internal RUM scopes using the + // `GlobalRumMonitor`) it is impossible to break cycle dependency. + rumMonitor.start() } // region private @@ -104,5 +123,8 @@ object Rum { "You're trying to create a RumMonitor instance, " + "but the RUM application id was empty. No RUM data will be sent." + internal const val RUM_FEATURE_ALREADY_ENABLED = + "RUM Feature is already enabled in this SDK core, ignoring the call to enable it." + // endregion } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt index b747e831d2..ca5960cd3c 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt @@ -6,21 +6,15 @@ package com.datadog.android.rum.internal.domain.scope -import android.app.ActivityManager import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.Feature import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver -import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumSessionListener -import com.datadog.android.rum.internal.AppStartTimeProvider -import com.datadog.android.rum.internal.DefaultAppStartTimeProvider import com.datadog.android.rum.internal.domain.RumContext -import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.vitals.VitalMonitor -import java.util.concurrent.TimeUnit @Suppress("LongParameterList") internal class RumApplicationScope( @@ -33,8 +27,7 @@ internal class RumApplicationScope( private val cpuVitalMonitor: VitalMonitor, private val memoryVitalMonitor: VitalMonitor, private val frameRateVitalMonitor: VitalMonitor, - private val sessionListener: RumSessionListener?, - private val appStartTimeProvider: AppStartTimeProvider = DefaultAppStartTimeProvider() + private val sessionListener: RumSessionListener? ) : RumScope, RumViewChangedListener { private var rumContext = RumContext(applicationId = applicationId) @@ -62,7 +55,6 @@ internal class RumApplicationScope( } private var lastActiveViewInfo: RumViewInfo? = null - private var isSentAppStartedEvent = false // region RumScope @@ -87,10 +79,6 @@ internal class RumApplicationScope( } } - if (!isSentAppStartedEvent) { - sendApplicationStartEvent(event.eventTime, writer) - } - delegateToChildren(event, writer) return this @@ -166,37 +154,9 @@ internal class RumApplicationScope( } } - @WorkerThread - private fun sendApplicationStartEvent(eventTime: Time, writer: DataWriter) { - val processImportance = DdRumContentProvider.processImportance - val isForegroundProcess = processImportance == - ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - if (isForegroundProcess) { - val processStartTimeNs = appStartTimeProvider.appStartTimeNs - // processStartTime is the time in nanoseconds since VM start. To get a timestamp, we want - // to convert it to milliseconds since epoch provided by System.currentTimeMillis. - // To do so, we take the offset of those times in the event time, which should be consistent, - // then add that to our processStartTime to get the correct value. - val timestampNs = ( - TimeUnit.MILLISECONDS.toNanos(eventTime.timestamp) - eventTime.nanoTime - ) + processStartTimeNs - val applicationLaunchViewTime = Time( - timestamp = TimeUnit.NANOSECONDS.toMillis(timestampNs), - nanoTime = processStartTimeNs - ) - val startupTime = eventTime.nanoTime - processStartTimeNs - val appStartedEvent = - RumRawEvent.ApplicationStarted(applicationLaunchViewTime, startupTime) - delegateToChildren(appStartedEvent, writer) - isSentAppStartedEvent = true - } - } - // endregion companion object { - internal const val LAST_ACTIVE_VIEW_GONE_WARNING_MESSAGE = "Attempting to start a new " + - "session on the last known view (%s) failed because that view has been disposed. " internal const val MULTIPLE_ACTIVE_SESSIONS_ERROR = "Application has multiple active " + "sessions when starting a new session" } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt index 73b06efc68..3547e3c091 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt @@ -215,4 +215,10 @@ internal sealed class RumRawEvent { override val eventTime: Time = Time(), val isMetric: Boolean = false ) : RumRawEvent() + + internal data class SdkInit( + val isAppInForeground: Boolean, + val appStartTimeNs: Long, + override val eventTime: Time = Time() + ) : RumRawEvent() } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt index 3829468356..1b8b2a32fd 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt @@ -13,6 +13,7 @@ import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.internal.domain.RumContext +import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.storage.NoOpDataWriter import com.datadog.android.rum.internal.vitals.VitalMonitor import com.datadog.android.rum.utils.percent @@ -33,7 +34,7 @@ internal class RumSessionScope( cpuVitalMonitor: VitalMonitor, memoryVitalMonitor: VitalMonitor, frameRateVitalMonitor: VitalMonitor, - internal val sessionListener: RumSessionListener?, + private val sessionListener: RumSessionListener?, applicationDisplayed: Boolean, private val sessionInactivityNanos: Long = DEFAULT_SESSION_INACTIVITY_NS, private val sessionMaxDurationNanos: Long = DEFAULT_SESSION_MAX_DURATION_NS @@ -118,7 +119,20 @@ internal class RumSessionScope( val actualWriter = if (sessionState == State.TRACKED) writer else noOpWriter - childScope = childScope?.handleEvent(event, actualWriter) + val downStreamEvent = if (event is RumRawEvent.SdkInit) { + if (event.isAppInForeground) { + createApplicationStartEvent(event) + } else { + // stop here, we initialized the session, no need to go down + null + } + } else { + event + } + + if (downStreamEvent != null) { + childScope = childScope?.handleEvent(downStreamEvent, actualWriter) + } return if (isSessionComplete()) { null @@ -165,9 +179,10 @@ internal class RumSessionScope( val isInteraction = (event is RumRawEvent.StartView) || (event is RumRawEvent.StartAction) val isBackgroundEvent = event.javaClass in RumViewManagerScope.validBackgroundEventTypes - val isApplicationStartEvent = event is RumRawEvent.ApplicationStarted + val isSdkInitInForeground = event is RumRawEvent.SdkInit && event.isAppInForeground + val isSdkInitInBackground = event is RumRawEvent.SdkInit && !event.isAppInForeground - if (isInteraction || isApplicationStartEvent) { + if (isInteraction || isSdkInitInForeground) { if (isNewSession || isExpired || isTimedOut) { val reason = if (isNewSession) { StartReason.USER_APP_LAUNCH @@ -180,7 +195,7 @@ internal class RumSessionScope( } lastUserInteractionNs.set(nanoTime) } else if (isExpired) { - if (backgroundTrackingEnabled && isBackgroundEvent) { + if (backgroundTrackingEnabled && (isBackgroundEvent || isSdkInitInBackground)) { renewSession(nanoTime, StartReason.INACTIVITY_TIMEOUT) lastUserInteractionNs.set(nanoTime) } else { @@ -213,6 +228,26 @@ internal class RumSessionScope( ) } + private fun createApplicationStartEvent( + sdkInitEvent: RumRawEvent.SdkInit + ): RumRawEvent.ApplicationStarted { + val processStartTimeNs = sdkInitEvent.appStartTimeNs + val eventTime = sdkInitEvent.eventTime + // processStartTime is the time in nanoseconds since VM start. To get a timestamp, we want + // to convert it to milliseconds since epoch provided by System.currentTimeMillis. + // To do so, we take the offset of those times in the event time, which should be consistent, + // then add that to our processStartTime to get the correct value. + val timestampNs = ( + TimeUnit.MILLISECONDS.toNanos(eventTime.timestamp) - eventTime.nanoTime + ) + processStartTimeNs + val applicationLaunchViewTime = Time( + timestamp = TimeUnit.NANOSECONDS.toMillis(timestampNs), + nanoTime = processStartTimeNs + ) + val startupTime = sdkInitEvent.eventTime.nanoTime - processStartTimeNs + return RumRawEvent.ApplicationStarted(applicationLaunchViewTime, startupTime) + } + // endregion companion object { diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt index a9e883b750..cb873293e2 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt @@ -280,7 +280,7 @@ internal class RumViewManagerScope( internal const val MESSAGE_MISSING_VIEW = "A RUM event was detected, but no view is active. " + "To track views automatically, try calling the " + - "Configuration.Builder.useViewTrackingStrategy() method.\n" + + "RumConfiguration.Builder.useViewTrackingStrategy() method.\n" + "You can also track views manually using the RumMonitor.startView() and " + "RumMonitor.stopView() methods." } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedRumMonitor.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedRumMonitor.kt index 31df3060f4..99761bc8e6 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedRumMonitor.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedRumMonitor.kt @@ -22,6 +22,8 @@ internal interface AdvancedRumMonitor : RumMonitor, AdvancedNetworkRumMonitor { fun resetSession() + fun start() + fun sendWebViewEvent() fun addLongTask(durationNs: Long, target: String) diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt index 5a703af102..58e6b3d6fc 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt @@ -6,6 +6,7 @@ package com.datadog.android.rum.internal.monitor +import android.app.ActivityManager import android.os.Handler import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.Feature @@ -14,6 +15,7 @@ import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.internal.utils.loggableStackTrace import com.datadog.android.core.internal.utils.submitSafe +import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource @@ -23,7 +25,9 @@ import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum._RumInternalProxy +import com.datadog.android.rum.internal.AppStartTimeProvider import com.datadog.android.rum.internal.CombinedRumSessionListener +import com.datadog.android.rum.internal.DefaultAppStartTimeProvider import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.debug.RumDebugListener @@ -66,6 +70,7 @@ internal class DatadogRumMonitor( memoryVitalMonitor: VitalMonitor, frameRateVitalMonitor: VitalMonitor, sessionListener: RumSessionListener, + private val appStartTimeProvider: AppStartTimeProvider = DefaultAppStartTimeProvider(), private val executorService: ExecutorService = Executors.newSingleThreadExecutor() ) : RumMonitor, AdvancedRumMonitor { @@ -397,6 +402,16 @@ internal class DatadogRumMonitor( ) } + override fun start() { + val processImportance = DdRumContentProvider.processImportance + val isAppInForeground = processImportance == + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + val processStartTimeNs = appStartTimeProvider.appStartTimeNs + handleEvent( + RumRawEvent.SdkInit(isAppInForeground, processStartTimeNs) + ) + } + override fun waitForResourceTiming(key: String) { handleEvent( RumRawEvent.WaitForResourceTiming(key) diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumTest.kt index 99271a0481..f54073d600 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumTest.kt @@ -8,6 +8,7 @@ package com.datadog.android.rum import android.os.Looper import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.sampling.RateBasedSampler @@ -197,6 +198,27 @@ internal class RumTest { check(GlobalRumMonitor.get(mockSdkCore) is NoOpRumMonitor) } + @Test + fun `𝕄 register nothing 𝕎 build() { RUM feature already registered }`( + @Forgery fakeRumConfiguration: RumConfiguration + ) { + // Given + val mockInternalLogger = mock() + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mock() + + // When + Rum.enable(fakeRumConfiguration, mockSdkCore) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + Rum.RUM_FEATURE_ALREADY_ENABLED + ) + verify(mockSdkCore, never()).registerFeature(any()) + } + companion object { private val mainLooper = MainLooperTestConfiguration() diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt index c76939ae40..ffa40655f1 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt @@ -6,7 +6,6 @@ package com.datadog.android.rum.internal.domain.scope -import android.app.ActivityManager import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.context.TimeInfo import com.datadog.android.api.feature.Feature @@ -14,10 +13,8 @@ import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver -import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumSessionListener -import com.datadog.android.rum.internal.AppStartTimeProvider import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.vitals.VitalMonitor import com.datadog.android.rum.utils.forge.Configurator @@ -47,7 +44,6 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness -import java.util.concurrent.TimeUnit @Extensions( ExtendWith(MockitoExtension::class), @@ -71,9 +67,6 @@ internal class RumApplicationScopeTest { @Mock lateinit var mockResolver: FirstPartyHostHeaderTypeResolver - @Mock - lateinit var mockAppStartTimeProvider: AppStartTimeProvider - @Mock lateinit var mockCpuVitalMonitor: VitalMonitor @@ -126,8 +119,7 @@ internal class RumApplicationScopeTest { mockCpuVitalMonitor, mockMemoryVitalMonitor, mockFrameRateVitalMonitor, - mockSessionListener, - mockAppStartTimeProvider + mockSessionListener ) } @@ -370,87 +362,4 @@ internal class RumApplicationScopeTest { assertThat(rumContext["view_id"]).isNotNull } } - - @Test - fun `M send ApplicationStarted event once W handleEvent { app is in foreground }`( - forge: Forge - ) { - // Given - val fakeEvents = forge.aList { - forge.anyRumEvent(excluding = listOf(RumRawEvent.ApplicationStarted::class.java)) - } - val firstEvent = fakeEvents.first() - val appStartTimeNs = forge.aLong(min = 0, max = fakeEvents.first().eventTime.nanoTime) - whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn appStartTimeNs - DdRumContentProvider.processImportance = - ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - val mockSessionScope = mock() - testedScope.childScopes.clear() - testedScope.childScopes += mockSessionScope - - val expectedEventTimestamp = - TimeUnit.NANOSECONDS.toMillis( - TimeUnit.MILLISECONDS.toNanos(firstEvent.eventTime.timestamp) - - firstEvent.eventTime.nanoTime + appStartTimeNs - ) - - // When - fakeEvents.forEach { - testedScope.handleEvent(it, mockWriter) - } - - // Then - argumentCaptor { - verify(mockSessionScope).handleEvent(capture(), eq(mockWriter)) - assertThat(firstValue).isInstanceOf(RumRawEvent.ApplicationStarted::class.java) - val appStartEventTime = (firstValue as RumRawEvent.ApplicationStarted).eventTime - assertThat(appStartEventTime.timestamp).isEqualTo(expectedEventTimestamp) - assertThat(appStartEventTime.nanoTime).isEqualTo(appStartTimeNs) - - val processStartTimeNs = - (firstValue as RumRawEvent.ApplicationStarted).applicationStartupNanos - assertThat(processStartTimeNs).isEqualTo(firstEvent.eventTime.nanoTime - appStartTimeNs) - - assertThat(allValues.filterIsInstance()).hasSize(1) - } - } - - @Test - fun `M not send ApplicationStarted event W handleEvent { app is not in foreground }`( - forge: Forge - ) { - // Given - val fakeEvents = forge.aList { - forge.anyRumEvent(excluding = listOf(RumRawEvent.ApplicationStarted::class.java)) - } - val appStartTimeNs = forge.aLong(min = 0, max = fakeEvents.first().eventTime.nanoTime) - whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn appStartTimeNs - DdRumContentProvider.processImportance = forge.anElementFrom( - ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING, - @Suppress("DEPRECATION") - ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE - ) - val mockSessionScope = mock() - testedScope.childScopes.clear() - testedScope.childScopes += mockSessionScope - - // When - fakeEvents.forEach { - testedScope.handleEvent(it, mockWriter) - } - - // Then - argumentCaptor { - verify(mockSessionScope).handleEvent(capture(), eq(mockWriter)) - assertThat(allValues).doesNotHaveSameClassAs(RumRawEvent.ApplicationStarted::class.java) - } - } } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt index 3843db06fe..dc3311df80 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt @@ -124,6 +124,15 @@ internal fun Forge.applicationStartedEvent(): RumRawEvent.ApplicationStarted { ) } +internal fun Forge.sdkInitEvent(): RumRawEvent.SdkInit { + val time = Time() + return RumRawEvent.SdkInit( + isAppInForeground = aBool(), + appStartTimeNs = aLong(min = 0L, max = time.nanoTime), + eventTime = time + ) +} + internal fun Forge.validBackgroundEvent(): RumRawEvent { return this.anElementFrom( listOf( diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt index b8c358ddc7..8207af1fe1 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt @@ -195,6 +195,53 @@ internal class RumSessionScopeTest { verify(mockChildScope).handleEvent(same(mockEvent), isA>()) } + @Test + fun `M send ApplicationStarted event once W handleEvent(SdkInit) { app is in foreground }`( + forge: Forge + ) { + // Given + val fakeEvent = forge.sdkInitEvent().copy(isAppInForeground = true) + + val expectedEventTimestamp = + TimeUnit.NANOSECONDS.toMillis( + TimeUnit.MILLISECONDS.toNanos(fakeEvent.eventTime.timestamp) - + fakeEvent.eventTime.nanoTime + fakeEvent.appStartTimeNs + ) + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + argumentCaptor { + verify(mockChildScope).handleEvent(capture(), eq(mockWriter)) + assertThat(firstValue).isInstanceOf(RumRawEvent.ApplicationStarted::class.java) + val appStartEventTime = (firstValue as RumRawEvent.ApplicationStarted).eventTime + assertThat(appStartEventTime.timestamp).isEqualTo(expectedEventTimestamp) + assertThat(appStartEventTime.nanoTime).isEqualTo(fakeEvent.appStartTimeNs) + + val processStartTimeNs = (firstValue as RumRawEvent.ApplicationStarted) + .applicationStartupNanos + assertThat(processStartTimeNs) + .isEqualTo(fakeEvent.eventTime.nanoTime - fakeEvent.appStartTimeNs) + + assertThat(allValues.filterIsInstance()).hasSize(1) + } + } + + @Test + fun `M not send ApplicationStarted event W handleEvent(SdkInit) { app is not in foreground }`( + forge: Forge + ) { + // Given + val fakeEvent = forge.sdkInitEvent().copy(isAppInForeground = false) + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + verifyNoInteractions(mockChildScope) + } + // endregion // region Stopping Sessions @@ -347,14 +394,15 @@ internal class RumSessionScopeTest { } @Test - fun `𝕄 create new session context 𝕎 handleEvent(appStarted)+getRumContext() {sampling = 100}`( + fun `𝕄 create new session context 𝕎 handleEvent(SdkInit)+getRumContext() {sampling = 100, foreground}`( forge: Forge ) { // Given initializeTestedScope(100f) // When - val result = testedScope.handleEvent(forge.applicationStartedEvent(), mockWriter) + val result = testedScope + .handleEvent(forge.sdkInitEvent().copy(isAppInForeground = true), mockWriter) val context = testedScope.getRumContext() // Then @@ -366,6 +414,48 @@ internal class RumSessionScopeTest { assertThat(context.viewId).isEqualTo(fakeParentContext.viewId) } + @Test + fun `𝕄 create new session context 𝕎 handleEvent(SdkInit)+getRumContext(){sampling=100,background+enabled}`( + forge: Forge + ) { + // Given + initializeTestedScope(100f, backgroundTrackingEnabled = true) + + // When + val result = testedScope + .handleEvent(forge.sdkInitEvent().copy(isAppInForeground = false), mockWriter) + val context = testedScope.getRumContext() + + // Then + assertThat(result).isSameAs(testedScope) + assertThat(context.sessionId).isNotEqualTo(RumContext.NULL_UUID) + assertThat(context.sessionState).isEqualTo(RumSessionScope.State.TRACKED) + assertThat(context.sessionStartReason).isEqualTo(RumSessionScope.StartReason.INACTIVITY_TIMEOUT) + assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) + assertThat(context.viewId).isEqualTo(fakeParentContext.viewId) + } + + @Test + fun `𝕄 not create new session context 𝕎 handleEvent(SdkInit)+getRumContext(){sampling=100,background+disabled}`( + forge: Forge + ) { + // Given + initializeTestedScope(100f, backgroundTrackingEnabled = false) + + // When + val result = testedScope + .handleEvent(forge.sdkInitEvent().copy(isAppInForeground = false), mockWriter) + val context = testedScope.getRumContext() + + // Then + assertThat(result).isSameAs(testedScope) + assertThat(context.sessionId).isEqualTo(RumContext.NULL_UUID) + assertThat(context.sessionState).isEqualTo(RumSessionScope.State.EXPIRED) + assertThat(context.sessionStartReason).isEqualTo(RumSessionScope.StartReason.USER_APP_LAUNCH) + assertThat(context.applicationId).isEqualTo(fakeParentContext.applicationId) + assertThat(context.viewId).isEqualTo(fakeParentContext.viewId) + } + @Test fun `𝕄 keep session context 𝕎 handleEvent(non interactive) {before expiration}`( forge: Forge diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt index 4454b0fb65..4d10b48551 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt @@ -6,6 +6,7 @@ package com.datadog.android.rum.internal.monitor +import android.app.ActivityManager import android.os.Handler import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.Feature @@ -14,6 +15,7 @@ import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.internal.utils.loggableStackTrace +import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource @@ -21,6 +23,7 @@ import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.RumSessionListener +import com.datadog.android.rum.internal.AppStartTimeProvider import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.RumFeature import com.datadog.android.rum.internal.debug.RumDebugListener @@ -131,6 +134,9 @@ internal class DatadogRumMonitorTest { @Mock lateinit var mockInternalLogger: InternalLogger + @Mock + lateinit var mockAppStartTimeProvider: AppStartTimeProvider + @StringForgery(regex = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") lateinit var fakeApplicationId: String @@ -142,6 +148,9 @@ internal class DatadogRumMonitorTest { @LongForgery(TIMESTAMP_MIN, TIMESTAMP_MAX) var fakeTimestamp: Long = 0L + @LongForgery(min = 0L) + var fakeAppStartTimeNs: Long = 0L + @BoolForgery var fakeBackgroundTrackingEnabled: Boolean = false @@ -151,6 +160,7 @@ internal class DatadogRumMonitorTest { @BeforeEach fun `set up`(forge: Forge) { whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockAppStartTimeProvider.appStartTimeNs) doReturn fakeAppStartTimeNs fakeAttributes = forge.exhaustiveAttributes() testedMonitor = DatadogRumMonitor( @@ -166,7 +176,8 @@ internal class DatadogRumMonitorTest { mockCpuVitalMonitor, mockMemoryVitalMonitor, mockFrameRateVitalMonitor, - mockSessionListener + mockSessionListener, + mockAppStartTimeProvider ) testedMonitor.rootScope = mockScope } @@ -745,6 +756,65 @@ internal class DatadogRumMonitorTest { verifyNoMoreInteractions(mockScope, mockWriter) } + @Test + fun `M delegate event to rootScope W start() { application is in foreground }`() { + // Given + DdRumContentProvider.processImportance = + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + + // When + testedMonitor.start() + Thread.sleep(PROCESSING_DELAY) + + // Then + argumentCaptor { + verify(mockScope).handleEvent(capture(), same(mockWriter)) + + assertThat(firstValue).isInstanceOf(RumRawEvent.SdkInit::class.java) + with(firstValue as RumRawEvent.SdkInit) { + assertThat(isAppInForeground).isTrue() + assertThat(appStartTimeNs).isEqualTo(fakeAppStartTimeNs) + } + } + verifyNoMoreInteractions(mockScope, mockWriter) + } + + @Test + fun `M delegate event to rootScope W start() { application is not in foreground }`( + forge: Forge + ) { + // Given + DdRumContentProvider.processImportance = forge.anElementFrom( + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING, + @Suppress("DEPRECATION") + ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE + ) + + // When + testedMonitor.start() + Thread.sleep(PROCESSING_DELAY) + + // Then + argumentCaptor { + verify(mockScope).handleEvent(capture(), same(mockWriter)) + + assertThat(firstValue).isInstanceOf(RumRawEvent.SdkInit::class.java) + with(firstValue as RumRawEvent.SdkInit) { + assertThat(isAppInForeground).isFalse() + assertThat(appStartTimeNs).isEqualTo(fakeAppStartTimeNs) + } + } + verifyNoMoreInteractions(mockScope, mockWriter) + } + @Test fun `M delegate event to rootScope with timestamp W startView()`( @StringForgery(type = StringForgeryType.ASCII) key: String, @@ -1317,6 +1387,7 @@ internal class DatadogRumMonitorTest { mockMemoryVitalMonitor, mockFrameRateVitalMonitor, mockSessionListener, + mockAppStartTimeProvider, mockExecutor ) @@ -1361,6 +1432,7 @@ internal class DatadogRumMonitorTest { mockMemoryVitalMonitor, mockFrameRateVitalMonitor, mockSessionListener, + mockAppStartTimeProvider, mockExecutorService ) @@ -1392,6 +1464,7 @@ internal class DatadogRumMonitorTest { mockMemoryVitalMonitor, mockFrameRateVitalMonitor, mockSessionListener, + mockAppStartTimeProvider, mockExecutorService ) whenever(mockExecutorService.isShutdown).thenReturn(true) diff --git a/instrumented/integration/src/main/kotlin/com/datadog/android/sdk/integration/rum/ActivityTrackingPlaygroundActivity.kt b/instrumented/integration/src/main/kotlin/com/datadog/android/sdk/integration/rum/ActivityTrackingPlaygroundActivity.kt index 945985b8a5..67b0902887 100644 --- a/instrumented/integration/src/main/kotlin/com/datadog/android/sdk/integration/rum/ActivityTrackingPlaygroundActivity.kt +++ b/instrumented/integration/src/main/kotlin/com/datadog/android/sdk/integration/rum/ActivityTrackingPlaygroundActivity.kt @@ -33,19 +33,19 @@ internal class ActivityTrackingPlaygroundActivity : AppCompatActivity() { val sdkCore = Datadog.initialize(this, config, trackingConsent) checkNotNull(sdkCore) - val rumConfig = RuntimeConfig.rumConfigBuilder() - .trackUserInteractions() - .trackLongTasks(RuntimeConfig.LONG_TASK_LARGE_THRESHOLD) - .useViewTrackingStrategy(ActivityViewTrackingStrategy(true)) - .build() - Rum.enable(rumConfig, sdkCore) - DdRumContentProvider::class.java.declaredMethods.firstOrNull() { it.name == "overrideProcessImportance" }?.apply { isAccessible = true invoke(null, ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) } + + val rumConfig = RuntimeConfig.rumConfigBuilder() + .trackUserInteractions() + .trackLongTasks(RuntimeConfig.LONG_TASK_LARGE_THRESHOLD) + .useViewTrackingStrategy(ActivityViewTrackingStrategy(true)) + .build() + Rum.enable(rumConfig, sdkCore) setContentView(R.layout.fragment_tracking_layout) } }