From e70c3eaf4ef4a3d120c89dc28a7662f2e794bb3c Mon Sep 17 00:00:00 2001 From: Marius Constantin Date: Tue, 17 Sep 2024 15:58:06 +0200 Subject: [PATCH 1/2] RUM-6033 Add telemetry and logs related with RumMonitor#addViewLoadingTime API --- dd-sdk-android-core/api/apiSurface | 2 +- .../api/dd-sdk-android-core.api | 1 + .../com/datadog/android/api/InternalLogger.kt | 4 +- .../android/rum/internal/RumFeature.kt | 2 +- .../domain/event/RumEventSerializer.kt | 4 + .../domain/scope/RumViewManagerScope.kt | 20 ++- .../rum/internal/domain/scope/RumViewScope.kt | 87 ++++++++++--- .../internal/TelemetryEventHandler.kt | 2 +- .../domain/event/RumEventSerializerTest.kt | 110 ++++++++++++++++ .../internal/domain/scope/RumRawEventExt.kt | 10 +- .../domain/scope/RumViewManagerScopeTest.kt | 36 ++++++ .../internal/domain/scope/RumViewScopeTest.kt | 121 ++++++++++++++++++ .../android/rum/utils/InternalLoggerUtils.kt | 13 ++ .../InternalAddViewLoadingTimeEventAssert.kt | 73 +++++++++++ .../assertj/InternalApiUsageEventAssert.kt | 38 ++++++ .../internal/TelemetryEventHandlerTest.kt | 16 +-- 16 files changed, 501 insertions(+), 38 deletions(-) create mode 100644 features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/assertj/InternalAddViewLoadingTimeEventAssert.kt create mode 100644 features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/assertj/InternalApiUsageEventAssert.kt diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index 669c3cd39a..a154fffe35 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -44,7 +44,7 @@ interface com.datadog.android.api.InternalLogger fun log(Level, List, () -> String, Throwable? = null, Boolean = false, Map? = null) fun logMetric(() -> String, Map, Float) fun startPerformanceMeasure(String, com.datadog.android.core.metrics.TelemetryMetricType, Float, String): com.datadog.android.core.metrics.PerformanceMetric? - fun logApiUsage(com.datadog.android.internal.telemetry.InternalTelemetryEvent.ApiUsage, Float) + fun logApiUsage(com.datadog.android.internal.telemetry.InternalTelemetryEvent.ApiUsage, Float = 15f) companion object val UNBOUND: InternalLogger interface com.datadog.android.api.SdkCore diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index 1795e51ba9..b0ee781a8e 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -89,6 +89,7 @@ public final class com/datadog/android/api/InternalLogger$Companion { public final class com/datadog/android/api/InternalLogger$DefaultImpls { public static synthetic fun log$default (Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/api/InternalLogger$Level;Lcom/datadog/android/api/InternalLogger$Target;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;ILjava/lang/Object;)V public static synthetic fun log$default (Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/api/InternalLogger$Level;Ljava/util/List;Lkotlin/jvm/functions/Function0;Ljava/lang/Throwable;ZLjava/util/Map;ILjava/lang/Object;)V + public static synthetic fun logApiUsage$default (Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/internal/telemetry/InternalTelemetryEvent$ApiUsage;FILjava/lang/Object;)V } public final class com/datadog/android/api/InternalLogger$Level : java/lang/Enum { diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt index f49d4c8229..aa848f1a72 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt @@ -137,12 +137,12 @@ interface InternalLogger { * Logs an API usage from the internal implementation. * @param apiUsageEvent the API event being tracked * @param samplingRate value between 0-100 for sampling the event. Note that the sampling rate applied to this - * event will be applied in addition to the global telemetry sampling rate. + * event will be applied in addition to the global telemetry sampling rate. By default, the sampling rate is 15%. */ @InternalApi fun logApiUsage( apiUsageEvent: InternalTelemetryEvent.ApiUsage, - samplingRate: Float + samplingRate: Float = 15f ) companion object { diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt index 0e64b9c093..c23b1a0320 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt @@ -540,7 +540,7 @@ internal class RumFeature( internal const val ALL_IN_SAMPLE_RATE: Float = 100f internal const val DEFAULT_SAMPLE_RATE: Float = 100f internal const val DEFAULT_TELEMETRY_SAMPLE_RATE: Float = 20f - internal const val DEFAULT_TELEMETRY_CONFIGURATION_SAMPLE_RATE: Float = 100f + internal const val DEFAULT_TELEMETRY_CONFIGURATION_SAMPLE_RATE: Float = 20f internal const val DEFAULT_LONG_TASK_THRESHOLD_MS = 100L internal const val DD_TELEMETRY_CONFIG_SAMPLE_RATE_TAG = "_dd.telemetry.configuration_sample_rate" diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt index 1b63efac8c..f93fa94670 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt @@ -20,6 +20,7 @@ import com.datadog.android.rum.model.ViewEvent import com.datadog.android.telemetry.model.TelemetryConfigurationEvent import com.datadog.android.telemetry.model.TelemetryDebugEvent import com.datadog.android.telemetry.model.TelemetryErrorEvent +import com.datadog.android.telemetry.model.TelemetryUsageEvent import com.google.gson.JsonObject internal class RumEventSerializer( @@ -55,6 +56,9 @@ internal class RumEventSerializer( is TelemetryConfigurationEvent -> { model.toJson().toString() } + is TelemetryUsageEvent -> { + model.toJson().toString() + } is JsonObject -> { model.toString() } 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 881682b67c..ace9fbea48 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 @@ -13,6 +13,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.metrics.MethodCallSamplingRate +import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.internal.anr.ANRException import com.datadog.android.rum.internal.domain.RumContext @@ -144,7 +145,24 @@ internal class RumViewManagerScope( val importanceForeground = ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND val isForegroundProcess = processFlag == importanceForeground - if (applicationDisplayed || !isForegroundProcess) { + if (event is RumRawEvent.AddViewLoadingTime) { + val internalLogger = sdkCore.internalLogger + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { MESSAGE_MISSING_VIEW } + ) + internalLogger.logApiUsage( + InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( + overwrite = event.overwrite, + noView = true, + noActiveView = false + ) + ) + // we should return here and not add the event to the session ended metric missed events as we already + // send the API usage telemetry + return + } else if (applicationDisplayed || !isForegroundProcess) { handleBackgroundEvent(event, writer) } else { val isSilentOrphanEvent = event.javaClass in silentOrphanEventTypes diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt index 88624605d9..eb6669a3db 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt @@ -241,24 +241,74 @@ internal open class RumViewScope( @WorkerThread private fun onAddViewLoadingTime(event: RumRawEvent.AddViewLoadingTime, writer: DataWriter) { - if (stopped) return - val canAddViewLoadingTime = event.overwrite || viewLoadingTime == null - sdkCore.internalLogger.logApiUsage( - InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( - overwrite = event.overwrite, - noView = viewId.isEmpty(), - noActiveView = !isActive() - ), - 100.0f - ) - if (canAddViewLoadingTime) { - viewLoadingTime = event.eventTime.nanoTime - startedNanos - sendViewUpdate(event, writer) - } else { - // TODO RUM-6031 Add logs and telemetry here + val internalLogger = sdkCore.internalLogger + val canUpdateViewLoadingTime = !stopped && (viewLoadingTime == null || event.overwrite) + if (stopped) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { NO_ACTIVE_VIEW_FOR_LOADING_TIME_WARNING_MESSAGE } + ) + internalLogger.logApiUsage( + InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( + overwrite = event.overwrite, + noView = false, + noActiveView = true + ) + ) + } + + if (canUpdateViewLoadingTime) { + updateViewLoadingTime(event, internalLogger, writer) } } + private fun updateViewLoadingTime( + event: RumRawEvent.AddViewLoadingTime, + internalLogger: InternalLogger, + writer: DataWriter + ) { + val viewName = key.name + val previousViewLoadingTime = viewLoadingTime + val newLoadingTime = event.eventTime.nanoTime - startedNanos + if (previousViewLoadingTime == null) { + internalLogger.log( + InternalLogger.Level.DEBUG, + InternalLogger.Target.USER, + { ADDING_VIEW_LOADING_TIME_DEBUG_MESSAGE_FORMAT.format(Locale.US, viewLoadingTime, viewName) } + ) + internalLogger.logApiUsage( + InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( + overwrite = false, + noView = false, + noActiveView = false + ) + ) + } else if (event.overwrite) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { + OVERWRITING_VIEW_LOADING_TIME_WARNING_MESSAGE_FORMAT.format( + Locale.US, + viewName, + previousViewLoadingTime, + newLoadingTime + ) + } + ) + internalLogger.logApiUsage( + InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( + overwrite = true, + noView = false, + noActiveView = false + ) + ) + } + viewLoadingTime = newLoadingTime + sendViewUpdate(event, writer) + } + @WorkerThread private fun onStartView( event: RumRawEvent.StartView, @@ -1258,6 +1308,13 @@ internal open class RumViewScope( internal const val SLOW_RENDERED_THRESHOLD_FPS = 55 internal const val NEGATIVE_DURATION_WARNING_MESSAGE = "The computed duration for the " + "view: %s was 0 or negative. In order to keep the view we forced it to 1ns." + internal const val NO_ACTIVE_VIEW_FOR_LOADING_TIME_WARNING_MESSAGE = + "No active view found to add the loading time." + internal const val ADDING_VIEW_LOADING_TIME_DEBUG_MESSAGE_FORMAT = + "View loading time %dns added to the view %s" + internal const val OVERWRITING_VIEW_LOADING_TIME_WARNING_MESSAGE_FORMAT = + "View loading time already exists for the view %s. Replacing the existing %d ns " + + "view loading time with the new %d ns loading time." internal fun fromEvent( parentScope: RumScope, diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt index 6db598a351..007f69ced8 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt @@ -446,7 +446,7 @@ internal class TelemetryEventHandler( companion object { const val MAX_EVENTS_PER_SESSION = 100 - const val DEFAULT_CONFIGURATION_SAMPLE_RATE = 100f + const val DEFAULT_CONFIGURATION_SAMPLE_RATE = 20f const val ALREADY_SEEN_EVENT_MESSAGE = "Already seen telemetry event with identity=%s, rejecting." const val MAX_EVENT_NUMBER_REACHED_MESSAGE = diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializerTest.kt index dfe4188240..019227bc17 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializerTest.kt @@ -19,6 +19,7 @@ import com.datadog.android.rum.utils.forge.Configurator import com.datadog.android.telemetry.model.TelemetryConfigurationEvent import com.datadog.android.telemetry.model.TelemetryDebugEvent import com.datadog.android.telemetry.model.TelemetryErrorEvent +import com.datadog.android.telemetry.model.TelemetryUsageEvent import com.datadog.tools.unit.assertj.JsonObjectAssert.Companion.assertThat import com.datadog.tools.unit.forge.anException import com.google.gson.JsonObject @@ -33,6 +34,7 @@ import org.junit.jupiter.api.RepeatedTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.fail import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings @@ -748,6 +750,110 @@ internal class RumEventSerializerTest { } } + @RepeatedTest(8) + fun `M serialize RUM event W serialize() with TelemetryUsageEvent`( + @Forgery event: TelemetryUsageEvent + ) { + val serialized = testedSerializer.serialize(event) + val jsonObject = JsonParser.parseString(serialized).asJsonObject + assertThat(jsonObject) + .hasField("type", "telemetry") + .hasField("_dd") { + hasField("format_version", 2L) + } + .hasField("date", event.date) + .hasField("source", event.source.name.lowercase(Locale.US).replace('_', '-')) + .hasField("service", event.service) + .hasField("version", event.version) + .hasField("telemetry") { + hasField("usage") { + when (event.telemetry.usage) { + is TelemetryUsageEvent.Usage.AddViewLoadingTime -> { + val usage = event.telemetry.usage as TelemetryUsageEvent.Usage.AddViewLoadingTime + hasField("no_view", usage.noView) + hasField("no_active_view", usage.noActiveView) + hasField("overwritten", usage.overwritten) + } + + else -> { + fail("Usage type not covered in assertions") + } + } + } + if (event.telemetry.device != null) { + hasField("device") { + val device = event.telemetry.device + checkNotNull(device) + if (device.architecture != null) { + hasField("architecture", device.architecture!!) + } + if (device.brand != null) { + hasField("brand", device.brand!!) + } + if (device.model != null) { + hasField("model", device.model!!) + } + } + } + if (event.telemetry.os != null) { + hasField("os") { + val os = event.telemetry.os + checkNotNull(os) + if (os.build != null) { + hasField("build", os.build!!) + } + if (os.name != null) { + hasField("name", os.name!!) + } + if (os.version != null) { + hasField("version", os.version!!) + } + } + } + containsAttributes(event.telemetry.additionalProperties) + } + + val application = event.application + if (application != null) { + assertThat(jsonObject) + .hasField("application") { + hasField("id", application.id) + } + } else { + assertThat(jsonObject).doesNotHaveField("application") + } + + val session = event.session + if (session != null) { + assertThat(jsonObject) + .hasField("session") { + hasField("id", session.id) + } + } else { + assertThat(jsonObject).doesNotHaveField("session") + } + + val view = event.view + if (view != null) { + assertThat(jsonObject) + .hasField("view") { + hasField("id", view.id) + } + } else { + assertThat(jsonObject).doesNotHaveField("view") + } + + val action = event.action + if (action != null) { + assertThat(jsonObject) + .hasField("action") { + hasField("id", action.id) + } + } else { + assertThat(jsonObject).doesNotHaveField("action") + } + } + @Test fun `M serialize RUM event W serialize() with unknown event`( @Forgery unknownEvent: UserInfo @@ -1451,18 +1557,21 @@ internal class RumEventSerializerTest { usr = (it.usr ?: ViewEvent.Usr()).copy(additionalProperties = userAttributes) ) } + 2 -> this.getForgery(ActionEvent::class.java).let { it.copy( context = ActionEvent.Context(additionalProperties = attributes), usr = (it.usr ?: ActionEvent.Usr()).copy(additionalProperties = userAttributes) ) } + 3 -> this.getForgery(ErrorEvent::class.java).let { it.copy( context = ErrorEvent.Context(additionalProperties = attributes), usr = (it.usr ?: ErrorEvent.Usr()).copy(additionalProperties = userAttributes) ) } + 4 -> this.getForgery(ResourceEvent::class.java).let { it.copy( context = ResourceEvent.Context(additionalProperties = attributes), @@ -1470,6 +1579,7 @@ internal class RumEventSerializerTest { .copy(additionalProperties = userAttributes) ) } + else -> this.getForgery(LongTaskEvent::class.java).let { it.copy( context = LongTaskEvent.Context(additionalProperties = attributes), 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 22a669d234..1db369cb73 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 @@ -112,6 +112,10 @@ internal fun Forge.addErrorEvent(): RumRawEvent.AddError { ) } +internal fun Forge.addViewLoadingTimeEvent(): RumRawEvent.AddViewLoadingTime { + return RumRawEvent.AddViewLoadingTime(overwrite = aBool()) +} + internal fun Forge.addLongTaskEvent(): RumRawEvent.AddLongTask { return RumRawEvent.AddLongTask( durationNs = aLong(min = 1), @@ -178,7 +182,8 @@ internal fun Forge.invalidBackgroundEvent(): RumRawEvent { stopActionEvent(), stopResourceEvent(), stopResourceWithErrorEvent(), - stopResourceWithStacktraceEvent() + stopResourceWithStacktraceEvent(), + addViewLoadingTimeEvent() ) ) } @@ -197,7 +202,8 @@ internal fun Forge.anyRumEvent(excluding: List = listOf()): RumRawEvent { addLongTaskEvent(), addFeatureFlagEvaluationEvent(), addCustomTimingEvent(), - updatePerformanceMetricEvent() + updatePerformanceMetricEvent(), + addViewLoadingTimeEvent() ) return this.anElementFrom( allEvents.filter { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt index 3b6a0f7452..1f67cea660 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt @@ -17,6 +17,7 @@ import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.EventType import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.internal.anr.ANRDetectorRunnable @@ -28,6 +29,7 @@ import com.datadog.android.rum.internal.vitals.NoOpVitalMonitor import com.datadog.android.rum.internal.vitals.VitalMonitor import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.utils.forge.Configurator +import com.datadog.android.rum.utils.verifyApiUsage import com.datadog.android.rum.utils.verifyLog import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.BoolForgery @@ -55,6 +57,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.util.concurrent.TimeUnit @@ -818,6 +821,39 @@ internal class RumViewManagerScopeTest { // endregion + // region AddViewLoadingTime + + @Test + fun `M send a warning log and api usage telemetry W handleEvent { AddViewLoadingTime, no active view }`( + forge: Forge + ) { + // Given + val fakeEvent = forge.addViewLoadingTimeEvent() + testedScope.applicationDisplayed = true + + // When + testedScope.handleEvent(fakeEvent, mockWriter) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + RumViewManagerScope.MESSAGE_MISSING_VIEW + ) + mockInternalLogger.verifyApiUsage( + InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( + overwrite = fakeEvent.overwrite, + noView = true, + noActiveView = false + ), + 15f + ) + verifyNoMoreInteractions(mockInternalLogger) + verifyNoInteractions(mockWriter) + } + + // endregion + private fun resolveExpectedTimestamp(timestamp: Long): Long { return timestamp + fakeTime.serverTimeOffsetMs } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt index b0fb1f7964..14ea1d9668 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt @@ -18,6 +18,7 @@ import com.datadog.android.api.storage.EventType import com.datadog.android.core.feature.event.ThreadDump import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.internal.utils.loggableStackTrace +import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource @@ -44,6 +45,7 @@ import com.datadog.android.rum.model.LongTaskEvent import com.datadog.android.rum.model.ViewEvent import com.datadog.android.rum.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.rum.utils.forge.Configurator +import com.datadog.android.rum.utils.verifyApiUsage import com.datadog.android.rum.utils.verifyLog import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension @@ -7028,6 +7030,112 @@ internal class RumViewScopeTest { hasSampleRate(fakeSampleRate) } } + mockInternalLogger.verifyLog( + InternalLogger.Level.DEBUG, + InternalLogger.Target.USER, + RumViewScope.ADDING_VIEW_LOADING_TIME_DEBUG_MESSAGE_FORMAT.format( + expectedViewLoadingTime, + testedScope.key.name + ) + ) + mockInternalLogger.verifyApiUsage( + InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( + overwrite = false, + noView = false, + noActiveView = false + ), + 15f + ) + verifyNoMoreInteractions(mockWriter) + } + + @Test + fun `M overwrite view loading W handleEvent(AddViewLoadingTime, overwrite=true)`( + forge: Forge + ) { + // Given + val previousLoadingTime = forge.aPositiveLong() + testedScope.viewLoadingTime = previousLoadingTime + val newViewLoadingTime = RumRawEvent.AddViewLoadingTime(overwrite = true) + val expectedViewLoadingTime = newViewLoadingTime.eventTime.nanoTime - fakeEventTime.nanoTime + + // When + testedScope.handleEvent( + newViewLoadingTime, + mockWriter + ) + + // Then + argumentCaptor { + verify(mockWriter) + .write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + assertThat(firstValue) + .apply { + hasTimestamp(resolveExpectedTimestamp(fakeEventTime.timestamp)) + hasName(fakeKey.name) + hasUrl(fakeUrl) + hasDurationGreaterThan(1) + hasLoadingType(null) + hasVersion(2) + hasErrorCount(0) + hasResourceCount(0) + hasActionCount(0) + hasFrustrationCount(0) + hasLongTaskCount(0) + hasFrozenFrameCount(0) + hasCpuMetric(null) + hasMemoryMetric(null, null) + hasRefreshRateMetric(null, null) + isActive(true) + isSlowRendered(false) + hasUserInfo(fakeDatadogContext.userInfo) + hasViewId(testedScope.viewId) + hasApplicationId(fakeParentContext.applicationId) + hasSessionId(fakeParentContext.sessionId) + hasUserSession() + hasNoSyntheticsTest() + hasStartReason(fakeParentContext.sessionStartReason) + hasReplay(fakeHasReplay) + hasReplayStats(fakeReplayStats) + hasSource(fakeSourceViewEvent) + hasLoadingTime(expectedViewLoadingTime) + hasDeviceInfo( + fakeDatadogContext.deviceInfo.deviceName, + fakeDatadogContext.deviceInfo.deviceModel, + fakeDatadogContext.deviceInfo.deviceBrand, + fakeDatadogContext.deviceInfo.deviceType.toViewSchemaType(), + fakeDatadogContext.deviceInfo.architecture + ) + hasOsInfo( + fakeDatadogContext.deviceInfo.osName, + fakeDatadogContext.deviceInfo.osVersion, + fakeDatadogContext.deviceInfo.osMajorVersion + ) + hasConnectivityInfo(fakeDatadogContext.networkInfo) + hasServiceName(fakeDatadogContext.service) + hasVersion(fakeDatadogContext.version) + hasSessionActive(fakeParentContext.isSessionActive) + hasSampleRate(fakeSampleRate) + } + } + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + RumViewScope.OVERWRITING_VIEW_LOADING_TIME_WARNING_MESSAGE_FORMAT.format( + Locale.US, + testedScope.key.name, + previousLoadingTime, + expectedViewLoadingTime + ) + ) + mockInternalLogger.verifyApiUsage( + InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( + noActiveView = false, + noView = false, + overwrite = true + ), + 15f + ) verifyNoMoreInteractions(mockWriter) } @@ -7190,6 +7298,19 @@ internal class RumViewScopeTest { // Then assertThat(testedScope.viewLoadingTime).isNull() + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + RumViewScope.NO_ACTIVE_VIEW_FOR_LOADING_TIME_WARNING_MESSAGE + ) + mockInternalLogger.verifyApiUsage( + InternalTelemetryEvent.ApiUsage.AddViewLoadingTime( + noActiveView = true, + noView = false, + overwrite = fakeOverwrite + ), + 15f + ) verifyNoInteractions(mockWriter) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/InternalLoggerUtils.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/InternalLoggerUtils.kt index 32d736ab6a..3992b4a0da 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/InternalLoggerUtils.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/InternalLoggerUtils.kt @@ -9,6 +9,8 @@ package com.datadog.android.rum.utils import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import com.datadog.android.rum.utils.assertj.InternalApiUsageEventAssert import org.assertj.core.api.Assertions.assertThat import org.mockito.ArgumentMatchers.isA import org.mockito.kotlin.argumentCaptor @@ -18,6 +20,17 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.verification.VerificationMode +fun InternalLogger.verifyApiUsage( + apiUsage: InternalTelemetryEvent.ApiUsage, + samplingRate: Float +) { + argumentCaptor { + verify(this@verifyApiUsage).logApiUsage(capture(), eq(samplingRate)) + val event = firstValue + InternalApiUsageEventAssert.assertThat(event).isEqualTo(apiUsage) + } +} + fun InternalLogger.verifyLog( level: InternalLogger.Level, target: InternalLogger.Target, diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/assertj/InternalAddViewLoadingTimeEventAssert.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/assertj/InternalAddViewLoadingTimeEventAssert.kt new file mode 100644 index 0000000000..66c1a1e3a6 --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/assertj/InternalAddViewLoadingTimeEventAssert.kt @@ -0,0 +1,73 @@ +/* + * 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.utils.assertj + +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import org.assertj.core.api.AbstractAssert +import org.assertj.core.api.Assertions.assertThat + +internal class InternalAddViewLoadingTimeEventAssert(actual: InternalTelemetryEvent.ApiUsage.AddViewLoadingTime) : + AbstractAssert( + actual, + InternalAddViewLoadingTimeEventAssert::class.java + ) { + + fun isEqualTo(expected: InternalTelemetryEvent.ApiUsage.AddViewLoadingTime) { + hasNoView(expected.noView) + hasNoActiveView(expected.noActiveView) + hasOverwrite(expected.overwrite) + hasAdditionalProperties(expected.additionalProperties) + } + + fun hasNoView(expected: Boolean): InternalAddViewLoadingTimeEventAssert { + assertThat(actual.noView) + .overridingErrorMessage( + "Expected viewLoadingTimeTelemetryEvent event to" + + " have noView $expected but was ${actual.noView}" + ) + .isEqualTo(expected) + return this + } + + fun hasNoActiveView(expected: Boolean): InternalAddViewLoadingTimeEventAssert { + assertThat(actual.noActiveView) + .overridingErrorMessage( + "Expected viewLoadingTimeTelemetryEvent event to have" + + " noActiveView $expected but was ${actual.noActiveView}" + ) + .isEqualTo(expected) + return this + } + + fun hasOverwrite(expected: Boolean): InternalAddViewLoadingTimeEventAssert { + assertThat(actual.overwrite) + .overridingErrorMessage( + "Expected viewLoadingTimeTelemetryEvent event to have" + + " overwrite $expected but was ${actual.overwrite}" + ) + .isEqualTo(expected) + return this + } + + fun hasAdditionalProperties(expected: Map): InternalAddViewLoadingTimeEventAssert { + assertThat(actual.additionalProperties) + .overridingErrorMessage( + "Expected viewLoadingTimeTelemetryEvent event to have" + + " additionalProperties $expected but was ${actual.additionalProperties}" + ) + .isEqualTo(expected) + return this + } + + companion object { + fun assertThat( + actual: InternalTelemetryEvent.ApiUsage.AddViewLoadingTime + ): InternalAddViewLoadingTimeEventAssert { + return InternalAddViewLoadingTimeEventAssert(actual) + } + } +} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/assertj/InternalApiUsageEventAssert.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/assertj/InternalApiUsageEventAssert.kt new file mode 100644 index 0000000000..5c28a0d6ad --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/utils/assertj/InternalApiUsageEventAssert.kt @@ -0,0 +1,38 @@ +/* + * 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.utils.assertj + +import com.datadog.android.internal.telemetry.InternalTelemetryEvent +import org.assertj.core.api.AbstractAssert + +class InternalApiUsageEventAssert(actual: InternalTelemetryEvent.ApiUsage) : + AbstractAssert( + actual, + InternalApiUsageEventAssert::class.java + ) { + + fun isEqualTo(expected: InternalTelemetryEvent.ApiUsage): InternalApiUsageEventAssert { + when (actual) { + is InternalTelemetryEvent.ApiUsage.AddViewLoadingTime -> { + InternalAddViewLoadingTimeEventAssert + .assertThat(actual as InternalTelemetryEvent.ApiUsage.AddViewLoadingTime) + .isEqualTo(expected as InternalTelemetryEvent.ApiUsage.AddViewLoadingTime) + } + + else -> { + failWithMessage("Unknown event type: ${actual::class.java.simpleName}") + } + } + return this + } + + companion object { + fun assertThat(actual: InternalTelemetryEvent.ApiUsage): InternalApiUsageEventAssert { + return InternalApiUsageEventAssert(actual) + } + } +} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt index ee88da6010..f9817786ea 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt @@ -661,20 +661,6 @@ internal class TelemetryEventHandlerTest { rawEvent.eventTime.timestamp ) } - - is TelemetryConfigurationEvent -> { - assertConfigEventMatchesInternalEvent( - capturedValue, - internalTelemetryEvent as InternalTelemetryEvent.Configuration, - fakeRumContext, - rawEvent.eventTime.timestamp - ) - } - - is InternalTelemetryEvent.InterceptorInstantiated -> { - assertThat(capturedValue).isEqualTo(InternalTelemetryEvent.InterceptorInstantiated) - } - else -> throw IllegalArgumentException( "Unexpected type=${lastValue::class.jvmName} of the captured value." ) @@ -884,7 +870,7 @@ internal class TelemetryEventHandlerTest { .hasSessionId(rumContext.sessionId) .hasViewId(rumContext.viewId) .hasActionId(rumContext.actionId) - .hasAdditionalProperties(internalUsageEvent.additionalProperties ?: emptyMap()) + .hasAdditionalProperties(internalUsageEvent.additionalProperties) .hasDeviceArchitecture(fakeDeviceArchitecture) .hasDeviceBrand(fakeDeviceBrand) .hasDeviceModel(fakeDeviceModel) From 29b64be33d7b5ae6282a89641f5cd695f34987b8 Mon Sep 17 00:00:00 2001 From: Marius Constantin Date: Wed, 18 Sep 2024 15:24:50 +0200 Subject: [PATCH 2/2] RUM-6035 Add the integration tests related with RumMonitor#addViewLoadingTime API --- .../rum/integration/ManualTrackingRumTest.kt | 220 ++++++++++++++++++ .../tests/assertj/RumEventAssert.kt | 15 ++ 2 files changed, 235 insertions(+) diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/ManualTrackingRumTest.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/ManualTrackingRumTest.kt index e330008a7a..c13f21219c 100644 --- a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/ManualTrackingRumTest.kt +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/ManualTrackingRumTest.kt @@ -8,6 +8,7 @@ package com.datadog.android.rum.integration import com.datadog.android.api.feature.Feature import com.datadog.android.core.stub.StubSDKCore +import com.datadog.android.rum.ExperimentalRumApi import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.Rum import com.datadog.android.rum.RumActionType @@ -23,6 +24,7 @@ import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery @@ -30,14 +32,17 @@ import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.quality.Strictness import java.net.URL +import java.util.concurrent.TimeUnit @Extensions( ExtendWith(MockitoExtension::class), @@ -395,6 +400,221 @@ class ManualTrackingRumTest { // endregion + // region AddViewLoading time + + @OptIn(ExperimentalRumApi::class) + @Test + fun `M attach view loading time W addViewLoadingTime { active view available }`( + @StringForgery key: String, + @StringForgery name: String, + @BoolForgery overwrite: Boolean + ) { + // Given + val rumMonitor = GlobalRumMonitor.get(stubSdkCore) + val startTime = System.nanoTime() + rumMonitor.startView(key, name, emptyMap()) + + // When + val endTime = System.nanoTime() + val expectedViewLoadingTime = endTime - startTime + rumMonitor.addViewLoadingTime(overwrite) + + // Then + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + StubEventsAssert.assertThat(eventsWritten) + .hasSize(2) + .hasRumEvent(index = 0) { + // Initial view + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasActionCount(0) + doesNotHaveViewLoadingTime() + } + .hasRumEvent(index = 1) { + // view updated with loading time + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasViewLoadingTime( + expectedViewLoadingTime, + offset = Offset.offset(TimeUnit.MILLISECONDS.toNanos(5)) + ) + } + } + + @OptIn(ExperimentalRumApi::class) + @Test + fun `M not attach view loading time W addViewLoadingTime { view was stopped }`( + @StringForgery key: String, + @StringForgery name: String, + @BoolForgery overwrite: Boolean + ) { + // Given + val rumMonitor = GlobalRumMonitor.get(stubSdkCore) + rumMonitor.startView(key, name, emptyMap()) + rumMonitor.stopView(key, emptyMap()) + + // When + rumMonitor.addViewLoadingTime(overwrite) + + // Then + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + StubEventsAssert.assertThat(eventsWritten) + .hasSize(2) + .hasRumEvent(index = 0) { + // Initial view + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasActionCount(0) + doesNotHaveViewLoadingTime() + } + .hasRumEvent(index = 1) { + // view stopped + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + doesNotHaveViewLoadingTime() + } + } + + @OptIn(ExperimentalRumApi::class) + @Test + fun `M renew view loading time W addViewLoadingTime { loading time was already added, overwrite is true }`( + @StringForgery key: String, + @StringForgery name: String, + @BoolForgery overwrite: Boolean + ) { + // Given + val rumMonitor = GlobalRumMonitor.get(stubSdkCore) + val startTime = System.nanoTime() + rumMonitor.startView(key, name, emptyMap()) + val intermediateTime = System.nanoTime() + rumMonitor.addViewLoadingTime(overwrite) + + // When + Thread.sleep(100) + val endTime = System.nanoTime() + rumMonitor.addViewLoadingTime(true) + + // Then + val expectedFirstViewLoadingTime = intermediateTime - startTime + val expectedSecondViewLoadingTime = endTime - startTime + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + StubEventsAssert.assertThat(eventsWritten) + .hasSize(3) + .hasRumEvent(index = 0) { + // Initial view + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasActionCount(0) + doesNotHaveViewLoadingTime() + } + .hasRumEvent(index = 1) { + // first view loading time + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasViewLoadingTime( + expectedFirstViewLoadingTime, + offset = Offset.offset(TimeUnit.MILLISECONDS.toNanos(5)) + ) + } + .hasRumEvent(index = 2) { + // second view loading time + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasViewLoadingTime( + expectedSecondViewLoadingTime, + offset = Offset.offset(TimeUnit.MILLISECONDS.toNanos(5)) + ) + } + } + + @OptIn(ExperimentalRumApi::class) + @Test + fun `M not renew view loading time W addViewLoadingTime { loading time was already added, overwrite is false }`( + @StringForgery key: String, + @StringForgery name: String, + @BoolForgery overwrite: Boolean + ) { + // Given + val rumMonitor = GlobalRumMonitor.get(stubSdkCore) + val startTime = System.nanoTime() + rumMonitor.startView(key, name, emptyMap()) + val intermediateTime = System.nanoTime() + rumMonitor.addViewLoadingTime(overwrite) + + // When + Thread.sleep(100) + rumMonitor.addViewLoadingTime(false) + + // Then + val expectedViewLoadingTime = intermediateTime - startTime + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + StubEventsAssert.assertThat(eventsWritten) + .hasSize(2) + .hasRumEvent(index = 0) { + // Initial view + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasActionCount(0) + doesNotHaveViewLoadingTime() + } + .hasRumEvent(index = 1) { + // first view loading time + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasViewLoadingTime( + expectedViewLoadingTime, + offset = Offset.offset(TimeUnit.MILLISECONDS.toNanos(5)) + ) + } + } + + // endregion + companion object { private val mainLooper = MainLooperTestConfiguration() diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/RumEventAssert.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/RumEventAssert.kt index c4b72fe574..07594317af 100644 --- a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/RumEventAssert.kt +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/RumEventAssert.kt @@ -8,6 +8,7 @@ package com.datadog.android.rum.integration.tests.assertj import com.datadog.tools.unit.assertj.JsonObjectAssert import com.google.gson.JsonObject +import org.assertj.core.data.Offset class RumEventAssert(actual: JsonObject) : JsonObjectAssert(actual, true) { @@ -123,6 +124,20 @@ class RumEventAssert(actual: JsonObject) : // endregion + // region view loading time + + fun hasViewLoadingTime(time: Long, offset: Offset): RumEventAssert { + hasField("view.loading_time", time, offset = offset) + return this + } + + fun doesNotHaveViewLoadingTime(): RumEventAssert { + doesNotHaveField("view.loading_time") + return this + } + + // endregion + companion object { fun assertThat(actual: JsonObject): RumEventAssert { return RumEventAssert(actual)