Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUM-7171 Implement the basic logic for interaction-to-next-view-metric #2417

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1033,10 +1033,13 @@ datadog:
- "kotlin.collections.MutableSet.firstOrNull(kotlin.Function1)"
- "kotlin.collections.MutableSet.flatMap(kotlin.Function1)"
- "kotlin.collections.MutableSet.forEach(kotlin.Function1)"
- "kotlin.collections.MutableSet.elementAtOrNull(kotlin.Int)"
- "kotlin.collections.MutableSet.indexOf(kotlin.String)"
- "kotlin.collections.MutableSet.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)"
- "kotlin.collections.MutableSet.map(kotlin.Function1)"
- "kotlin.collections.MutableSet.remove(com.datadog.android.core.internal.persistence.ConsentAwareStorage.Batch)"
- "kotlin.collections.MutableSet.remove(java.io.File)"
- "kotlin.collections.MutableSet.remove(kotlin.collections.MutableMap.Mutab(...)"
- "kotlin.collections.MutableSet.removeAll(kotlin.collections.Collection)"
- "kotlin.collections.MutableSet.toList()"
- "kotlin.collections.MutableSet.lastOrNull()"
Expand Down
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,
private 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,26 @@
/*
* 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,
RumActionType.BACK
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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

internal interface InteractionIngestionValidator {
fun validate(
context: InternalInteractionContext
): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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
}
}
// in case there are no previous interactions for this view and there's only one view created
// we are probably in the first view of the app (AppLaunch) and we can't calculate the metric
if (lastViewCreatedTimestamps.size > 1) {
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) {
@Suppress("UnsafeThirdPartyFunctionCall")
// we make sure the collection is never empty
lastInteractions.entries.remove(lastInteractions.entries.first())
}
while (lastViewCreatedTimestamps.entries.size > MAX_ENTRIES) {
@Suppress("UnsafeThirdPartyFunctionCall")
// we make sure the collection is never empty
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 4 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
internal const val MAX_ENTRIES = 4
}
}
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,11 @@
/*
* 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

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
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ internal class RumSessionScopeTest {
whenever(mockSdkCore.getFeature(Feature.SESSION_REPLAY_FEATURE_NAME)) doReturn
mockSessionReplayFeatureScope
whenever(mockSdkCore.time) doReturn (fakeTimeInfo)
whenever(mockSdkCore.internalLogger) doReturn mock()

initializeTestedScope()
}
Expand Down
Loading