Skip to content

Commit

Permalink
Merge pull request #1356 from DataDog/jward/RUMM-2873-stop-session
Browse files Browse the repository at this point in the history
RUMM-2873 Allow users to stop a RUM session
  • Loading branch information
fuzzybinary authored Apr 3, 2023
2 parents bcf0d12 + c08ad2f commit c1fb449
Show file tree
Hide file tree
Showing 29 changed files with 1,035 additions and 67 deletions.
11 changes: 6 additions & 5 deletions dd-sdk-android/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ interface com.datadog.android.rum.RumMonitor
fun addErrorWithStacktrace(String, RumErrorSource, String?, Map<String, Any?>)
fun addTiming(String)
fun addFeatureFlagEvaluation(String, Any)
fun stopSession()
fun _getInternal(): _RumInternalProxy?
class Builder
fun sampleRumSessions(Float): Builder
Expand Down Expand Up @@ -667,7 +668,7 @@ data class com.datadog.android.rum.model.ActionEvent
fun fromJson(kotlin.String): Application
fun fromJsonObject(com.google.gson.JsonObject): Application
data class ActionEventSession
constructor(kotlin.String, ActionEventSessionType, kotlin.Boolean? = null)
constructor(kotlin.String, ActionEventSessionType, kotlin.Boolean? = null, kotlin.Boolean? = true)
fun toJson(): com.google.gson.JsonElement
companion object
fun fromJson(kotlin.String): ActionEventSession
Expand Down Expand Up @@ -907,7 +908,7 @@ data class com.datadog.android.rum.model.ErrorEvent
fun fromJson(kotlin.String): Application
fun fromJsonObject(com.google.gson.JsonObject): Application
data class ErrorEventSession
constructor(kotlin.String, ErrorEventSessionType, kotlin.Boolean? = null)
constructor(kotlin.String, ErrorEventSessionType, kotlin.Boolean? = null, kotlin.Boolean? = true)
fun toJson(): com.google.gson.JsonElement
companion object
fun fromJson(kotlin.String): ErrorEventSession
Expand Down Expand Up @@ -1156,7 +1157,7 @@ data class com.datadog.android.rum.model.LongTaskEvent
fun fromJson(kotlin.String): Application
fun fromJsonObject(com.google.gson.JsonObject): Application
data class LongTaskEventSession
constructor(kotlin.String, LongTaskEventSessionType, kotlin.Boolean? = null)
constructor(kotlin.String, LongTaskEventSessionType, kotlin.Boolean? = null, kotlin.Boolean? = true)
fun toJson(): com.google.gson.JsonElement
companion object
fun fromJson(kotlin.String): LongTaskEventSession
Expand Down Expand Up @@ -1326,7 +1327,7 @@ data class com.datadog.android.rum.model.ResourceEvent
fun fromJson(kotlin.String): Application
fun fromJsonObject(com.google.gson.JsonObject): Application
data class ResourceEventSession
constructor(kotlin.String, ResourceEventSessionType, kotlin.Boolean? = null)
constructor(kotlin.String, ResourceEventSessionType, kotlin.Boolean? = null, kotlin.Boolean? = true)
fun toJson(): com.google.gson.JsonElement
companion object
fun fromJson(kotlin.String): ResourceEventSession
Expand Down Expand Up @@ -1590,7 +1591,7 @@ data class com.datadog.android.rum.model.ViewEvent
fun fromJson(kotlin.String): Application
fun fromJsonObject(com.google.gson.JsonObject): Application
data class ViewEventSession
constructor(kotlin.String, ViewEventSessionType, kotlin.Boolean? = null)
constructor(kotlin.String, ViewEventSessionType, kotlin.Boolean? = null, kotlin.Boolean? = true)
fun toJson(): com.google.gson.JsonElement
companion object
fun fromJson(kotlin.String): ViewEventSession
Expand Down
6 changes: 6 additions & 0 deletions dd-sdk-android/src/main/json/rum/_common-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
"type": "boolean",
"description": "Whether this session has a replay",
"readOnly": true
},
"is_active": {
"type": "boolean",
"description": "Whether this session is currently active. Set to false to manually stop a session",
"default": true,
"readOnly": true
}
},
"readOnly": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ interface RumMonitor {
value: Any
)

/**
* Stops the current session.
* A new session will start in response to a call to `startView`, `addUserAction`, or
* `startUserAction`. If the session is started because of a call to `addUserAction`,
* or `startUserAction`, the last know view is restarted in the new session.
*/
fun stopSession()

/**
* For Datadog internal use only.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import java.util.UUID
internal data class RumContext(
val applicationId: String = NULL_UUID,
val sessionId: String = NULL_UUID,
val isSessionActive: Boolean = false,
val viewId: String? = null,
val viewName: String? = null,
val viewUrl: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,76 @@ package com.datadog.android.rum.internal.domain.scope

import androidx.annotation.WorkerThread
import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver
import com.datadog.android.core.internal.utils.internalLogger
import com.datadog.android.rum.RumSessionListener
import com.datadog.android.rum.internal.RumFeature
import com.datadog.android.rum.internal.domain.RumContext
import com.datadog.android.rum.internal.vitals.VitalMonitor
import com.datadog.android.v2.api.InternalLogger
import com.datadog.android.v2.api.SdkCore
import com.datadog.android.v2.core.internal.ContextProvider
import com.datadog.android.v2.core.internal.storage.DataWriter
import java.util.Locale

@Suppress("LongParameterList")
internal class RumApplicationScope(
applicationId: String,
sdkCore: SdkCore,
private val sdkCore: SdkCore,
internal val samplingRate: Float,
internal val backgroundTrackingEnabled: Boolean,
internal val trackFrustrations: Boolean,
firstPartyHostHeaderTypeResolver: FirstPartyHostHeaderTypeResolver,
cpuVitalMonitor: VitalMonitor,
memoryVitalMonitor: VitalMonitor,
frameRateVitalMonitor: VitalMonitor,
sessionListener: RumSessionListener?,
contextProvider: ContextProvider
) : RumScope {
private val firstPartyHostHeaderTypeResolver: FirstPartyHostHeaderTypeResolver,
private val cpuVitalMonitor: VitalMonitor,
private val memoryVitalMonitor: VitalMonitor,
private val frameRateVitalMonitor: VitalMonitor,
private val sessionListener: RumSessionListener?,
private val contextProvider: ContextProvider
) : RumScope, RumViewChangedListener {

private val rumContext = RumContext(applicationId = applicationId)
internal val childScope: RumScope = RumSessionScope(
this,
sdkCore,
samplingRate,
backgroundTrackingEnabled,
trackFrustrations,
firstPartyHostHeaderTypeResolver,
cpuVitalMonitor,
memoryVitalMonitor,
frameRateVitalMonitor,
sessionListener,
contextProvider
internal val childScopes: MutableList<RumScope> = mutableListOf(
RumSessionScope(
this,
sdkCore,
samplingRate,
backgroundTrackingEnabled,
trackFrustrations,
this,
firstPartyHostHeaderTypeResolver,
cpuVitalMonitor,
memoryVitalMonitor,
frameRateVitalMonitor,
sessionListener,
contextProvider,
false
)
)

val activeSession: RumScope?
get() {
return childScopes.find { it.isActive() }
}

private var lastActiveViewInfo: RumViewInfo? = null

// region RumScope

@WorkerThread
override fun handleEvent(
event: RumRawEvent,
writer: DataWriter<Any>
): RumScope {
childScope.handleEvent(event, writer)
val isInteraction = (event is RumRawEvent.StartView) || (event is RumRawEvent.StartAction)
if (activeSession == null && isInteraction) {
startNewSession(event, writer)
} else if (event is RumRawEvent.StopSession) {
sdkCore.updateFeatureContext(RumFeature.RUM_FEATURE_NAME) {
it.putAll(getRumContext().toMap())
}
}

delegateToChildren(event, writer)

return this
}

Expand All @@ -65,4 +90,80 @@ internal class RumApplicationScope(
}

// endregion

override fun onViewChanged(viewInfo: RumViewInfo) {
if (viewInfo.isActive) {
lastActiveViewInfo = viewInfo
}
}

@WorkerThread
private fun delegateToChildren(
event: RumRawEvent,
writer: DataWriter<Any>
) {
val iterator = childScopes.iterator()
@Suppress("UnsafeThirdPartyFunctionCall") // next/remove can't fail: we checked hasNext
while (iterator.hasNext()) {
val result = iterator.next().handleEvent(event, writer)
if (result == null) {
iterator.remove()
}
}
}

@WorkerThread
private fun startNewSession(event: RumRawEvent, writer: DataWriter<Any>) {
val newSession = RumSessionScope(
this,
sdkCore,
samplingRate,
backgroundTrackingEnabled,
trackFrustrations,
this,
firstPartyHostHeaderTypeResolver,
cpuVitalMonitor,
memoryVitalMonitor,
frameRateVitalMonitor,
sessionListener,
contextProvider,
true
)
childScopes.add(newSession)
if (event !is RumRawEvent.StartView) {
lastActiveViewInfo?.let {
val key = it.keyRef.get()
if (key != null) {
val startViewEvent = RumRawEvent.StartView(
key = key,
name = it.name,
attributes = it.attributes
)
newSession.handleEvent(startViewEvent, writer)
} else {
internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.USER,
LAST_ACTIVE_VIEW_GONE_WARNING_MESSAGE.format(Locale.US, it.name)
)
}
}
}

// Confidence telemety, only end up with one active session
if (childScopes.filter { it.isActive() }.size > 1) {
internalLogger.log(
InternalLogger.Level.ERROR,
InternalLogger.Target.TELEMETRY,
MULTIPLE_ACTIVE_SESSIONS_ERROR
)
}
}

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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ internal sealed class RumRawEvent {
override val eventTime: Time = Time()
) : RumRawEvent()

internal data class StopSession(
override val eventTime: Time = Time()
) : RumRawEvent()

internal data class UpdatePerformanceMetric(
val metric: RumPerformanceMetric,
val value: Double,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,22 @@ internal class RumSessionScope(
internal val samplingRate: Float,
internal val backgroundTrackingEnabled: Boolean,
internal val trackFrustrations: Boolean,
internal val viewChangedListener: RumViewChangedListener?,
internal val firstPartyHostHeaderTypeResolver: FirstPartyHostHeaderTypeResolver,
cpuVitalMonitor: VitalMonitor,
memoryVitalMonitor: VitalMonitor,
frameRateVitalMonitor: VitalMonitor,
internal val sessionListener: RumSessionListener?,
contextProvider: ContextProvider,
applicationDisplayed: Boolean,
appStartTimeProvider: AppStartTimeProvider = DefaultAppStartTimeProvider(),
private val sessionInactivityNanos: Long = DEFAULT_SESSION_INACTIVITY_NS,
private val sessionMaxDurationNanos: Long = DEFAULT_SESSION_MAX_DURATION_NS
) : RumScope {

internal var sessionId = RumContext.NULL_UUID
internal var sessionState: State = State.NOT_TRACKED
internal var isActive: Boolean = true
private val sessionStartNs = AtomicLong(System.nanoTime())
private val lastUserInteractionNs = AtomicLong(0L)

Expand All @@ -52,17 +55,19 @@ internal class RumSessionScope(
private val noOpWriter = NoOpDataWriter<Any>()

@Suppress("LongParameterList")
internal var childScope: RumScope = RumViewManagerScope(
internal var childScope: RumScope? = RumViewManagerScope(
this,
sdkCore,
backgroundTrackingEnabled,
trackFrustrations,
viewChangedListener,
firstPartyHostHeaderTypeResolver,
cpuVitalMonitor,
memoryVitalMonitor,
frameRateVitalMonitor,
appStartTimeProvider,
contextProvider
contextProvider,
applicationDisplayed
)

init {
Expand All @@ -83,36 +88,51 @@ internal class RumSessionScope(
override fun handleEvent(
event: RumRawEvent,
writer: DataWriter<Any>
): RumScope {
): RumScope? {
if (event is RumRawEvent.ResetSession) {
renewSession(System.nanoTime())
} else if (event is RumRawEvent.StopSession) {
stopSession()
}

updateSession(event)

val actualWriter = if (sessionState == State.TRACKED) writer else noOpWriter

childScope.handleEvent(event, actualWriter)
childScope = childScope?.handleEvent(event, actualWriter)

return this
return if (isSessionComplete()) {
null
} else {
this
}
}

override fun getRumContext(): RumContext {
val parentContext = parentScope.getRumContext()
return parentContext.copy(
sessionId = sessionId,
sessionState = sessionState
sessionState = sessionState,
isSessionActive = isActive
)
}

override fun isActive(): Boolean {
return true
return isActive
}

// endregion

// region Internal

private fun stopSession() {
isActive = false
}

private fun isSessionComplete(): Boolean {
return !isActive && childScope == null
}

@Suppress("ComplexMethod")
private fun updateSession(event: RumRawEvent) {
val nanoTime = System.nanoTime()
Expand Down
Loading

0 comments on commit c1fb449

Please sign in to comment.