Skip to content

Commit

Permalink
RUM-7171 Add basic logic for interaction-to-next-view metric
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusc83 committed Nov 28, 2024
1 parent f1e298b commit e5a05e6
Show file tree
Hide file tree
Showing 17 changed files with 1,036 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.datadog.android.rum.internal.domain.RumContext
import com.datadog.android.rum.internal.domain.Time
import com.datadog.android.rum.internal.metric.SessionEndedMetric
import com.datadog.android.rum.internal.metric.SessionMetricDispatcher
import com.datadog.android.rum.internal.metric.interactiontonextview.InteractionToNextViewMetricResolver
import com.datadog.android.rum.internal.vitals.NoOpVitalMonitor
import com.datadog.android.rum.internal.vitals.VitalMonitor
import java.util.Locale
Expand All @@ -38,7 +39,9 @@ internal class RumViewManagerScope(
private val memoryVitalMonitor: VitalMonitor,
private val frameRateVitalMonitor: VitalMonitor,
internal var applicationDisplayed: Boolean,
internal val sampleRate: Float
internal val sampleRate: Float,
internal val interactionToNextViewMetricResolver: InteractionToNextViewMetricResolver =
InteractionToNextViewMetricResolver(internalLogger = sdkCore.internalLogger)
) : RumScope {

internal val childrenScopes = mutableListOf<RumScope>()
Expand Down Expand Up @@ -198,7 +201,8 @@ internal class RumViewManagerScope(
memoryVitalMonitor,
frameRateVitalMonitor,
trackFrustrations,
sampleRate
sampleRate,
interactionToNextViewMetricResolver
)
applicationDisplayed = true
childrenScopes.add(viewScope)
Expand Down Expand Up @@ -260,7 +264,8 @@ internal class RumViewManagerScope(
NoOpVitalMonitor(),
type = RumViewScope.RumViewType.BACKGROUND,
trackFrustrations = trackFrustrations,
sampleRate = sampleRate
sampleRate = sampleRate,
interactionToNextViewMetricResolver = interactionToNextViewMetricResolver
)
}

Expand All @@ -283,7 +288,8 @@ internal class RumViewManagerScope(
NoOpVitalMonitor(),
type = RumViewScope.RumViewType.APPLICATION_LAUNCH,
trackFrustrations = trackFrustrations,
sampleRate = sampleRate
sampleRate = sampleRate,
interactionToNextViewMetricResolver = interactionToNextViewMetricResolver
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import com.datadog.android.rum.internal.anr.ANRException
import com.datadog.android.rum.internal.domain.RumContext
import com.datadog.android.rum.internal.domain.Time
import com.datadog.android.rum.internal.metric.SessionMetricDispatcher
import com.datadog.android.rum.internal.metric.interactiontonextview.InteractionToNextViewMetricResolver
import com.datadog.android.rum.internal.metric.interactiontonextview.InternalInteractionContext
import com.datadog.android.rum.internal.metric.networksettled.NetworkSettledMetricResolver
import com.datadog.android.rum.internal.monitor.StorageEvent
import com.datadog.android.rum.internal.utils.hasUserData
Expand Down Expand Up @@ -59,6 +61,7 @@ internal open class RumViewScope(
internal val type: RumViewType = RumViewType.FOREGROUND,
private val trackFrustrations: Boolean,
internal val sampleRate: Float,
private val interactionToNextViewMetricResolver: InteractionToNextViewMetricResolver,
private val networkSettledMetricResolver: NetworkSettledMetricResolver =
NetworkSettledMetricResolver(internalLogger = sdkCore.internalLogger)
) : RumScope {
Expand Down Expand Up @@ -162,6 +165,7 @@ internal open class RumViewScope(
Log.i(RumScope.SYNTHETICS_LOGCAT_TAG, "_dd.view.id=$viewId")
}
networkSettledMetricResolver.viewWasCreated(eventTime.nanoTime)
interactionToNextViewMetricResolver.onViewCreated(viewId, eventTime.nanoTime)
}

// region RumScope
Expand Down Expand Up @@ -378,6 +382,14 @@ internal open class RumViewScope(

if (stopped) return

interactionToNextViewMetricResolver.onActionSent(
InternalInteractionContext(
viewId,
event.type,
event.eventTime.nanoTime
)
)

if (activeActionScope != null) {
if (event.type == RumActionType.CUSTOM && !event.waitForStop) {
// deliver it anyway, even if there is active action ongoing
Expand Down Expand Up @@ -818,6 +830,7 @@ internal open class RumViewScope(
private fun sendViewUpdate(event: RumRawEvent, writer: DataWriter<Any>, eventType: EventType = EventType.DEFAULT) {
val viewComplete = isViewComplete()
val timeToSettled = networkSettledMetricResolver.resolveMetric()
val interactionToNextViewTime = interactionToNextViewMetricResolver.resolveMetric(viewId)
version++

// make a local copy, so that closure captures the state as of now
Expand Down Expand Up @@ -916,6 +929,7 @@ internal open class RumViewScope(
flutterRasterTime = eventFlutterRasterTime,
jsRefreshRate = eventJsRefreshRate,
networkSettledTime = timeToSettled,
interactionToNextViewTime = interactionToNextViewTime,
loadingTime = viewLoadingTime
),
usr = if (user.hasUserData()) {
Expand Down Expand Up @@ -1335,7 +1349,8 @@ internal open class RumViewScope(
memoryVitalMonitor: VitalMonitor,
frameRateVitalMonitor: VitalMonitor,
trackFrustrations: Boolean,
sampleRate: Float
sampleRate: Float,
interactionToNextViewMetricResolver: InteractionToNextViewMetricResolver
): RumViewScope {
return RumViewScope(
parentScope,
Expand All @@ -1350,7 +1365,8 @@ internal open class RumViewScope(
memoryVitalMonitor,
frameRateVitalMonitor,
trackFrustrations = trackFrustrations,
sampleRate = sampleRate
sampleRate = sampleRate,
interactionToNextViewMetricResolver = interactionToNextViewMetricResolver
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.metric.interactiontonextview

import com.datadog.android.rum.RumActionType

internal class ActionTypeInteractionValidator : InteractionIngestionValidator {
override fun validate(
context: InternalInteractionContext
): Boolean {
return context.actionType in ALLOWED_TYPES
}

companion object {
private val ALLOWED_TYPES = setOf(
RumActionType.TAP,
RumActionType.SWIPE,
RumActionType.CLICK
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.metric.interactiontonextview

import com.datadog.tools.annotation.NoOpImplementation

@NoOpImplementation
internal interface InteractionIngestionValidator {
fun validate(
context: InternalInteractionContext
): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.metric.interactiontonextview

import androidx.annotation.VisibleForTesting
import com.datadog.android.api.InternalLogger

internal class InteractionToNextViewMetricResolver(
private val internalLogger: InternalLogger,
private val ingestionValidator: InteractionIngestionValidator = ActionTypeInteractionValidator(),
private val lastInteractionIdentifier: LastInteractionIdentifier = TimeBasedInteractionIdentifier()
) {

private val lastInteractions = LinkedHashMap<String, InternalInteractionContext>()
private val lastViewCreatedTimestamps = LinkedHashMap<String, Long>()

fun onViewCreated(viewId: String, timestamp: Long) {
lastViewCreatedTimestamps[viewId] = timestamp
purgeOldEntries()
}

fun onActionSent(context: InternalInteractionContext) {
if (ingestionValidator.validate(context)) {
lastInteractions[context.viewId] = context
}
purgeOldEntries()
}

@Suppress("ReturnCount")
fun resolveMetric(viewId: String): Long? {
purgeOldEntries()
val currentViewCreatedTimestamp = lastViewCreatedTimestamps[viewId]
if (currentViewCreatedTimestamp == null) {
internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.MAINTAINER,
{ "[ViewNetworkSettledMetric] The view was not yet created for this viewId:$viewId" }
)
return null
}
val lastPrevViewInteraction = resolveLastInteraction(viewId, currentViewCreatedTimestamp)
if (lastPrevViewInteraction != null) {
val difference = currentViewCreatedTimestamp - lastPrevViewInteraction.eventCreatedAtNanos
if (difference > 0) {
return difference
} else {
internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.MAINTAINER,
{
"[ViewNetworkSettledMetric] The difference between the last interaction " +
"and the current view is negative for viewId:$viewId"
}
)
return null
}
}
internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.MAINTAINER,
{ "[ViewNetworkSettledMetric] No previous interaction found for this viewId:$viewId" }
)
return null
}

private fun resolveLastInteraction(viewId: String, currentViewCreatedTimestamp: Long): InternalInteractionContext? {
val currentViewIdIndex = lastViewCreatedTimestamps.keys.indexOf(viewId)
val previousViewId = lastViewCreatedTimestamps.keys.elementAtOrNull(currentViewIdIndex - 1)
if (previousViewId != null) {
lastInteractions[previousViewId]?.let {
val context = PreviousViewLastInteractionContext(
it.actionType,
it.eventCreatedAtNanos,
currentViewCreatedTimestamp
)
if (lastInteractionIdentifier.validate(context)) {
return it
}
}
}
return null
}

private fun purgeOldEntries() {
while (lastInteractions.entries.size > MAX_ENTRIES) {
lastInteractions.entries.remove(lastInteractions.entries.first())
}
while (lastViewCreatedTimestamps.size > MAX_ENTRIES) {
lastViewCreatedTimestamps.remove(lastViewCreatedTimestamps.keys.first())
}
}

@VisibleForTesting
internal fun lasInteractions(): Map<String, InternalInteractionContext> {
return lastInteractions
}

@VisibleForTesting
internal fun lastViewCreatedTimestamps(): Map<String, Long> {
return lastViewCreatedTimestamps
}

companion object {
// we need to keep at least 10 entries to be able to calculate the metric for consecutive views that
// are going to be created almost in the same time and will rely on the previous view interaction
private const val MAX_ENTRIES = 10
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.metric.interactiontonextview

import com.datadog.android.rum.RumActionType

internal data class InternalInteractionContext(
internal val viewId: String,
internal val actionType: RumActionType,
internal val eventCreatedAtNanos: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.metric.interactiontonextview

import com.datadog.tools.annotation.NoOpImplementation

@NoOpImplementation
internal interface LastInteractionIdentifier {
fun validate(context: PreviousViewLastInteractionContext): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.metric.interactiontonextview

import com.datadog.android.rum.RumActionType

internal data class PreviousViewLastInteractionContext(
internal val actionType: RumActionType,
internal val eventCreatedAtNanos: Long,
internal val currentViewCreationTimestamp: Long?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.metric.interactiontonextview

import java.util.concurrent.TimeUnit

internal class TimeBasedInteractionIdentifier(
private val timeThresholdInNanoSeconds: Long = TimeUnit.MILLISECONDS.toNanos(DEFAULT_TIME_THRESHOLD_MS)
) : LastInteractionIdentifier {

override fun validate(context: PreviousViewLastInteractionContext): Boolean {
return context.currentViewCreationTimestamp?.let { viewCreatedTime ->
context.eventCreatedAtNanos - viewCreatedTime < timeThresholdInNanoSeconds
} ?: false
}

companion object {
internal const val DEFAULT_TIME_THRESHOLD_MS = 3000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,18 @@ internal class ViewEventAssert(actual: ViewEvent) :
return this
}

fun hasInteractionToNextViewTime(
expected: Long?
): ViewEventAssert {
assertThat(actual.view.interactionToNextViewTime)
.overridingErrorMessage(
"Expected event to have interactionToNextViewTime $expected" +
" but was ${actual.view.interactionToNextViewTime}"
)
.isEqualTo(expected)
return this
}

fun hasLoadingType(
expected: ViewEvent.LoadingType?
): ViewEventAssert {
Expand Down
Loading

0 comments on commit e5a05e6

Please sign in to comment.