Skip to content

Commit

Permalink
RUM-1702: Start session when RUM is initialized
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnm committed Jan 22, 2024
1 parent 8145782 commit 14025c5
Show file tree
Hide file tree
Showing 13 changed files with 294 additions and 151 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()) {
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -62,7 +55,6 @@ internal class RumApplicationScope(
}

private var lastActiveViewInfo: RumViewInfo? = null
private var isSentAppStartedEvent = false

// region RumScope

Expand All @@ -87,10 +79,6 @@ internal class RumApplicationScope(
}
}

if (!isSentAppStartedEvent) {
sendApplicationStartEvent(event.eventTime, writer)
}

delegateToChildren(event, writer)

return this
Expand Down Expand Up @@ -166,37 +154,9 @@ internal class RumApplicationScope(
}
}

@WorkerThread
private fun sendApplicationStartEvent(eventTime: Time, writer: DataWriter<Any>) {
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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ internal interface AdvancedRumMonitor : RumMonitor, AdvancedNetworkRumMonitor {

fun resetSession()

fun start()

fun sendWebViewEvent()

fun addLongTask(durationNs: Long, target: String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<InternalLogger>()
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()

Expand Down

0 comments on commit 14025c5

Please sign in to comment.