diff --git a/features/dd-sdk-android-rum/api/apiSurface b/features/dd-sdk-android-rum/api/apiSurface index 0080c280c1..3b1d07bb39 100644 --- a/features/dd-sdk-android-rum/api/apiSurface +++ b/features/dd-sdk-android-rum/api/apiSurface @@ -166,7 +166,7 @@ interface com.datadog.android.rum.event.ViewEventMapper : com.datadog.android.ev override fun map(com.datadog.android.rum.model.ViewEvent): com.datadog.android.rum.model.ViewEvent data class com.datadog.android.rum.internal.domain.event.ResourceTiming constructor(Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L, Long = 0L) -interface com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor +interface com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor : com.datadog.android.rum.RumMonitor fun waitForResourceTiming(Any) fun addResourceTiming(Any, com.datadog.android.rum.internal.domain.event.ResourceTiming) fun notifyInterceptorInstantiated() @@ -223,8 +223,6 @@ abstract class com.datadog.android.rum.tracking.ActivityLifecycleTrackingStrateg override fun onActivityStopped(android.app.Activity) override fun onActivityCreated(android.app.Activity, android.os.Bundle?) override fun onActivityResumed(android.app.Activity) - protected fun convertToRumAttributes(android.content.Intent?): Map - protected fun convertToRumAttributes(android.os.Bundle?): Map protected fun withSdkCore((com.datadog.android.api.feature.FeatureSdkCore) -> T): T? class com.datadog.android.rum.tracking.ActivityViewTrackingStrategy : ActivityLifecycleTrackingStrategy, ViewTrackingStrategy constructor(Boolean, ComponentPredicate = AcceptAllActivities()) @@ -232,6 +230,7 @@ class com.datadog.android.rum.tracking.ActivityViewTrackingStrategy : ActivityLi override fun onActivityStopped(android.app.Activity) override fun equals(Any?): Boolean override fun hashCode(): Int +fun android.os.Bundle?.convertToRumViewAttributes(): Map interface com.datadog.android.rum.tracking.ComponentPredicate fun accept(T): Boolean fun getViewName(T): String? diff --git a/features/dd-sdk-android-rum/api/dd-sdk-android-rum.api b/features/dd-sdk-android-rum/api/dd-sdk-android-rum.api index 1942571a28..63bfc6cc69 100644 --- a/features/dd-sdk-android-rum/api/dd-sdk-android-rum.api +++ b/features/dd-sdk-android-rum/api/dd-sdk-android-rum.api @@ -287,7 +287,7 @@ public abstract class com/datadog/android/rum/internal/instrumentation/gestures/ public fun onScroll (Landroid/view/MotionEvent;Landroid/view/MotionEvent;FF)Z } -public abstract interface class com/datadog/android/rum/internal/monitor/AdvancedNetworkRumMonitor { +public abstract interface class com/datadog/android/rum/internal/monitor/AdvancedNetworkRumMonitor : com/datadog/android/rum/RumMonitor { public abstract fun addResourceTiming (Ljava/lang/Object;Lcom/datadog/android/rum/internal/domain/event/ResourceTiming;)V public abstract fun notifyInterceptorInstantiated ()V public abstract fun startResource (Lcom/datadog/android/rum/resource/ResourceId;Lcom/datadog/android/rum/RumResourceMethod;Ljava/lang/String;Ljava/util/Map;)V @@ -5062,8 +5062,6 @@ public class com/datadog/android/rum/tracking/AcceptAllSupportFragments : com/da public abstract class com/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy : android/app/Application$ActivityLifecycleCallbacks, com/datadog/android/rum/tracking/TrackingStrategy { protected field sdkCore Lcom/datadog/android/api/feature/FeatureSdkCore; public fun ()V - protected final fun convertToRumAttributes (Landroid/content/Intent;)Ljava/util/Map; - protected final fun convertToRumAttributes (Landroid/os/Bundle;)Ljava/util/Map; protected final fun getSdkCore ()Lcom/datadog/android/api/feature/FeatureSdkCore; public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V public fun onActivityDestroyed (Landroid/app/Activity;)V @@ -5088,6 +5086,10 @@ public final class com/datadog/android/rum/tracking/ActivityViewTrackingStrategy public fun onActivityStopped (Landroid/app/Activity;)V } +public final class com/datadog/android/rum/tracking/BundleExtKt { + public static final fun convertToRumViewAttributes (Landroid/os/Bundle;)Ljava/util/Map; +} + public abstract interface class com/datadog/android/rum/tracking/ComponentPredicate { public abstract fun accept (Ljava/lang/Object;)Z public abstract fun getViewName (Ljava/lang/Object;)Ljava/lang/String; diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedNetworkRumMonitor.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedNetworkRumMonitor.kt index dcd58e29f3..6a40cd99e9 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedNetworkRumMonitor.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/AdvancedNetworkRumMonitor.kt @@ -9,6 +9,7 @@ package com.datadog.android.rum.internal.monitor import com.datadog.android.lint.InternalApi import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource +import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.domain.event.ResourceTiming @@ -19,7 +20,7 @@ import com.datadog.android.rum.resource.ResourceId */ @SuppressWarnings("UndocumentedPublicFunction") @InternalApi -interface AdvancedNetworkRumMonitor { +interface AdvancedNetworkRumMonitor : RumMonitor { @InternalApi fun waitForResourceTiming(key: Any) diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy.kt index 79b81a2fe2..062f8cd5e6 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/ActivityLifecycleTrackingStrategy.kt @@ -9,7 +9,6 @@ package com.datadog.android.rum.tracking import android.app.Activity import android.app.Application import android.content.Context -import android.content.Intent import android.os.Bundle import androidx.annotation.MainThread import com.datadog.android.api.InternalLogger @@ -106,57 +105,6 @@ abstract class ActivityLifecycleTrackingStrategy : // endregion - // region Utils - - /** - * Maps the Bundle key - value properties into compatible attributes for the Rum Events. - * @param intent the [Intent] we need to transform. Returns an empty Map if this is null. - */ - protected fun convertToRumAttributes(intent: Intent?): Map { - if (intent == null) return emptyMap() - - val attributes = mutableMapOf() - - intent.action?.let { - attributes[INTENT_ACTION_TAG] = it - } - intent.dataString?.let { - attributes[INTENT_URI_TAG] = it - } - - intent.safeExtras?.let { bundle -> - bundle.keySet().forEach { - // TODO RUM-503 Bundle#get is deprecated, but there is no replacement for it. - // Issue is opened in the Google Issue Tracker. - @Suppress("DEPRECATION") - attributes["$ARGUMENT_TAG.$it"] = bundle.get(it) - } - } - - return attributes - } - - /** - * Maps the Bundle key - value properties into compatible attributes for the Rum Events. - * @param bundle the Bundle we need to transform. Returns an empty Map if this is null. - */ - protected fun convertToRumAttributes(bundle: Bundle?): Map { - if (bundle == null) return emptyMap() - - val attributes = mutableMapOf() - - bundle.keySet().forEach { - // TODO RUM-503 Bundle#get is deprecated, but there is no replacement for it. - // Issue is opened in the Google Issue Tracker. - @Suppress("DEPRECATION") - attributes["$ARGUMENT_TAG.$it"] = bundle.get(it) - } - - return attributes - } - - // endregion - // region Helper /** @@ -181,24 +129,9 @@ abstract class ActivityLifecycleTrackingStrategy : } } - private val Intent.safeExtras: Bundle? - get() = try { - // old Androids can throw different exceptions here making native calls - extras - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - internalLogger.log( - InternalLogger.Level.ERROR, - InternalLogger.Target.USER, - { "Error getting Intent extras, ignoring it." }, - e - ) - null - } + // endregion internal companion object { - internal const val ARGUMENT_TAG = "view.arguments" - internal const val INTENT_ACTION_TAG = "view.intent.action" - internal const val INTENT_URI_TAG = "view.intent.uri" internal const val EXTRA_SYNTHETICS_TEST_ID = "_dd.synthetics.test_id" internal const val EXTRA_SYNTHETICS_RESULT_ID = "_dd.synthetics.result_id" diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/ActivityViewTrackingStrategy.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/ActivityViewTrackingStrategy.kt index 2ebb49c874..07a16354cd 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/ActivityViewTrackingStrategy.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/ActivityViewTrackingStrategy.kt @@ -7,7 +7,10 @@ package com.datadog.android.rum.tracking import android.app.Activity +import android.content.Intent +import android.os.Bundle import androidx.annotation.MainThread +import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.utils.scheduleSafe import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumMonitor @@ -101,9 +104,47 @@ constructor( return withSdkCore { GlobalRumMonitor.get(it) } } + /** + * Maps the Bundle key - value properties into compatible attributes for the Rum Events. + * @param intent the [Intent] we need to transform. Returns an empty Map if this is null. + */ + private fun convertToRumAttributes(intent: Intent?): Map { + if (intent == null) return emptyMap() + + val attributes = mutableMapOf() + + intent.action?.let { + attributes[INTENT_ACTION_TAG] = it + } + intent.dataString?.let { + attributes[INTENT_URI_TAG] = it + } + + attributes.putAll(intent.safeExtras.convertToRumViewAttributes()) + + return attributes + } + + private val Intent.safeExtras: Bundle? + get() = try { + // old Androids can throw different exceptions here making native calls + extras + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "Error getting Intent extras, ignoring it." }, + e + ) + null + } + // endregion internal companion object { private const val STOP_VIEW_DELAY_MS = 200L + + internal const val INTENT_ACTION_TAG = "view.intent.action" + internal const val INTENT_URI_TAG = "view.intent.uri" } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/BundleExt.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/BundleExt.kt new file mode 100644 index 0000000000..599b32d503 --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/BundleExt.kt @@ -0,0 +1,29 @@ +/* + * 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.tracking + +import android.os.Bundle + +internal const val ARGUMENT_TAG = "view.arguments" + +/** + * Converts this bundle into a Map of attributes to be included in a RUM View event. + */ +fun Bundle?.convertToRumViewAttributes(): Map { + if (this == null) return emptyMap() + + val attributes = mutableMapOf() + + keySet().forEach { + // TODO RUM-503 Bundle#get is deprecated, but there is no replacement for it. + // Issue is opened in the Google Issue Tracker. + @Suppress("DEPRECATION") + attributes["$ARGUMENT_TAG.$it"] = get(it) + } + + return attributes +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt index 303aeecbc2..19dbeefbae 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/FragmentViewTrackingStrategy.kt @@ -73,7 +73,7 @@ internal constructor( if (rumFeature != null && rumMonitor != null) { AndroidXFragmentLifecycleCallbacks( argumentsProvider = { - if (trackArguments) convertToRumAttributes(it.arguments) else emptyMap() + if (trackArguments) it.arguments.convertToRumViewAttributes() else emptyMap() }, componentPredicate = supportFragmentComponentPredicate, rumMonitor = rumMonitor, @@ -96,7 +96,7 @@ internal constructor( ) { OreoFragmentLifecycleCallbacks( argumentsProvider = { - if (trackArguments) convertToRumAttributes(it.arguments) else emptyMap() + if (trackArguments) it.arguments.convertToRumViewAttributes() else emptyMap() }, componentPredicate = defaultFragmentComponentPredicate, rumMonitor = rumMonitor, diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/NavigationViewTrackingStrategy.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/NavigationViewTrackingStrategy.kt index 63f55f0de3..cd24adc246 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/NavigationViewTrackingStrategy.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/NavigationViewTrackingStrategy.kt @@ -94,7 +94,7 @@ class NavigationViewTrackingStrategy( ) { val rumMonitor = withSdkCore { GlobalRumMonitor.get(it) } componentPredicate.runIfValid(destination, internalLogger) { - val attributes = if (trackArguments) convertToRumAttributes(arguments) else emptyMap() + val attributes = if (trackArguments) arguments.convertToRumViewAttributes() else emptyMap() val viewName = componentPredicate.resolveViewName(destination) rumMonitor?.startView(destination, viewName, attributes) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/tracking/BundleExtTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/tracking/BundleExtTest.kt new file mode 100644 index 0000000000..993b586382 --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/tracking/BundleExtTest.kt @@ -0,0 +1,111 @@ +package com.datadog.android.rum.tracking + +import android.os.Bundle +import com.datadog.android.rum.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +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 + +@Extensions( + ExtendWith(ForgeExtension::class), + ExtendWith(MockitoExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +class BundleExtTest { + @Test + fun `M return empty map W convertToRumViewAttributes() {null bundle}`() { + // Given + val bundle: Bundle? = null + + // When + val result = bundle.convertToRumViewAttributes() + + // Then + + assertThat(result).isEmpty() + } + + @Test + fun `M return empty map W convertToRumViewAttributes() {empty bundle}`() { + // Given + val bundle = Bundle() + + // When + val result = bundle.convertToRumViewAttributes() + + // Then + + assertThat(result).isEmpty() + } + + @Test + fun `M return map with String attributes W convertToRumViewAttributes() {bundle}`( + forge: Forge + ) { + // Given + val expectedAttributes = mutableMapOf() + val bundle = Bundle() + repeat(forge.aSmallInt()) { + val key = forge.anAlphabeticalString() + val value = forge.aNullable { aString() } + bundle.putString(key, value) + expectedAttributes["view.arguments.$key"] = value + } + + // When + val result = bundle.convertToRumViewAttributes() + + // Then + assertThat(result).isEqualTo(expectedAttributes) + } + + @Test + fun `M return map with Int attributes W convertToRumViewAttributes() {bundle}`( + forge: Forge + ) { + // Given + val expectedAttributes = mutableMapOf() + val bundle = Bundle() + repeat(forge.aSmallInt()) { + val key = forge.anAlphabeticalString() + val value = forge.anInt() + bundle.putInt(key, value) + expectedAttributes["view.arguments.$key"] = value + } + + // When + val result = bundle.convertToRumViewAttributes() + + // Then + assertThat(result).isEqualTo(expectedAttributes) + } + + @Test + fun `M return map with Float attributes W convertToRumViewAttributes() {bundle}`( + forge: Forge + ) { + // Given + val expectedAttributes = mutableMapOf() + val bundle = Bundle() + repeat(forge.aSmallInt()) { + val key = forge.anAlphabeticalString() + val value = forge.aFloat() + bundle.putFloat(key, value) + expectedAttributes["view.arguments.$key"] = value + } + + // When + val result = bundle.convertToRumViewAttributes() + + // Then + assertThat(result).isEqualTo(expectedAttributes) + } +} diff --git a/integrations/dd-sdk-android-compose/src/main/kotlin/com/datadog/android/compose/internal/ComposeNavigationObserver.kt b/integrations/dd-sdk-android-compose/src/main/kotlin/com/datadog/android/compose/internal/ComposeNavigationObserver.kt index 265e8ab284..66efa3fa6f 100644 --- a/integrations/dd-sdk-android-compose/src/main/kotlin/com/datadog/android/compose/internal/ComposeNavigationObserver.kt +++ b/integrations/dd-sdk-android-compose/src/main/kotlin/com/datadog/android/compose/internal/ComposeNavigationObserver.kt @@ -15,6 +15,7 @@ import androidx.navigation.NavDestination import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.tracking.AcceptAllNavDestinations import com.datadog.android.rum.tracking.ComponentPredicate +import com.datadog.android.rum.tracking.convertToRumViewAttributes internal class ComposeNavigationObserver( private val trackArguments: Boolean = true, @@ -64,29 +65,10 @@ internal class ComposeNavigationObserver( key = route, name = viewName, attributes = if (trackArguments) { - convertToRumAttributes(arguments) + arguments.convertToRumViewAttributes() } else { emptyMap() } ) } - - private fun convertToRumAttributes(bundle: Bundle?): Map { - if (bundle == null) return emptyMap() - - val attributes = mutableMapOf() - - bundle.keySet().forEach { - // TODO RUM-503 Bundle#get is deprecated, but there is no replacement for it. - // Issue is opened in the Google Issue Tracker. - @Suppress("DEPRECATION") - attributes["$ARGUMENT_TAG.$it"] = bundle.get(it) - } - - return attributes - } - - companion object { - private const val ARGUMENT_TAG: String = "view.arguments" - } } diff --git a/reliability/single-fit/rum/build.gradle.kts b/reliability/single-fit/rum/build.gradle.kts index 090f7296ac..0af8f8ce0d 100644 --- a/reliability/single-fit/rum/build.gradle.kts +++ b/reliability/single-fit/rum/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(project(":dd-sdk-android-core")) implementation(project(":features:dd-sdk-android-rum")) implementation(libs.kotlin) + implementation(libs.bundles.androidXNavigation) // Testing testImplementation(project(":tools:unit")) { @@ -44,6 +45,7 @@ dependencies { } testImplementation(testFixtures(project(":dd-sdk-android-core"))) testImplementation(project(":reliability:stub-core")) + testImplementation(libs.bundles.androidXNavigation) testImplementation(libs.bundles.jUnit5) testImplementation(libs.bundles.testTools) testImplementation(libs.okHttp) diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/ActivityViewTrackingStrategyTest.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/ActivityViewTrackingStrategyTest.kt new file mode 100644 index 0000000000..8555d6925e --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/ActivityViewTrackingStrategyTest.kt @@ -0,0 +1,164 @@ +/* + * 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.integration + +import android.app.Activity +import android.app.Application +import com.datadog.android.api.feature.Feature +import com.datadog.android.core.stub.StubSDKCore +import com.datadog.android.rum.Rum +import com.datadog.android.rum.RumConfiguration +import com.datadog.android.rum.integration.tests.assertj.hasRumEvent +import com.datadog.android.rum.integration.tests.elmyr.RumIntegrationForgeConfigurator +import com.datadog.android.rum.integration.tests.utils.MainLooperTestConfiguration +import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy +import com.datadog.android.tests.assertj.StubEventsAssert.Companion.assertThat +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.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.verify +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@ForgeConfiguration(RumIntegrationForgeConfigurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +class ActivityViewTrackingStrategyTest { + + private lateinit var stubSdkCore: StubSDKCore + + private lateinit var testedViewTrackingStrategy: ActivityViewTrackingStrategy + + @Mock + lateinit var stubActivity: StubActivity + + @Mock + lateinit var mockApplicationContext: Application + + @StringForgery + private lateinit var fakeApplicationId: String + + @BeforeEach + fun `set up`(forge: Forge) { + stubSdkCore = StubSDKCore(forge) + + val fakeRumConfiguration = RumConfiguration.Builder(fakeApplicationId) + .trackNonFatalAnrs(false) // required to prevent infinite loop in tests + .build() + Rum.enable(fakeRumConfiguration, stubSdkCore) + + testedViewTrackingStrategy = ActivityViewTrackingStrategy(true) + testedViewTrackingStrategy.register(stubSdkCore, mockApplicationContext) + verify(mockApplicationContext).registerActivityLifecycleCallbacks(testedViewTrackingStrategy) + } + + @AfterEach + fun `tear down`() { + testedViewTrackingStrategy.unregister(mockApplicationContext) + verify(mockApplicationContext).unregisterActivityLifecycleCallbacks(testedViewTrackingStrategy) + } + + // region Activity Lifecycle + + @RepeatedTest(4) + fun `M send RUM View W onActivityResumed()`() { + // Given + + // When + testedViewTrackingStrategy.onActivityResumed(stubActivity) + + // Then + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + assertThat(eventsWritten) + .hasSize(1) + .hasRumEvent(index = 0) { + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl("com/datadog/android/rum/integration/ActivityViewTrackingStrategyTest/StubActivity") + hasViewName("com.datadog.android.rum.integration.ActivityViewTrackingStrategyTest.StubActivity") + } + } + + @RepeatedTest(4) + fun `M send RUM View update W onActivityResumed() + onActivityStopped()`() { + // Given + + // When + testedViewTrackingStrategy.onActivityResumed(stubActivity) + testedViewTrackingStrategy.onActivityStopped(stubActivity) + Thread.sleep(250L) + + // Then + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + assertThat(eventsWritten) + .hasSize(2) + .hasRumEvent(index = 0) { + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl("com/datadog/android/rum/integration/ActivityViewTrackingStrategyTest/StubActivity") + hasViewName("com.datadog.android.rum.integration.ActivityViewTrackingStrategyTest.StubActivity") + } + .hasRumEvent(index = 1) { + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl("com/datadog/android/rum/integration/ActivityViewTrackingStrategyTest/StubActivity") + hasViewName("com.datadog.android.rum.integration.ActivityViewTrackingStrategyTest.StubActivity") + hasViewIsActive(false) + } + } + + @RepeatedTest(4) + fun `M not send RUM View update W onActivityStopped() {start not tracked}`() { + // Given + + // When + testedViewTrackingStrategy.onActivityStopped(stubActivity) + Thread.sleep(250L) + + // Then + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + assertThat(eventsWritten).hasSize(0) + } + + // endregion + + class StubActivity : Activity() + + companion object { + private val mainLooper = MainLooperTestConfiguration() + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(mainLooper) + } + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/GlobalRumMonitorTest.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/GlobalRumMonitorTest.kt new file mode 100644 index 0000000000..9225107238 --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/GlobalRumMonitorTest.kt @@ -0,0 +1,89 @@ +/* + * 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.integration + +import com.datadog.android.api.SdkCore +import com.datadog.android.core.stub.StubSDKCore +import com.datadog.android.rum.GlobalRumMonitor +import com.datadog.android.rum.Rum +import com.datadog.android.rum.RumConfiguration +import com.datadog.android.rum.integration.tests.elmyr.RumIntegrationForgeConfigurator +import com.datadog.android.rum.integration.tests.utils.MainLooperTestConfiguration +import com.datadog.android.tests.assertj.StubEventsAssert.Companion.assertThat +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.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mockito.mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@ForgeConfiguration(RumIntegrationForgeConfigurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +class GlobalRumMonitorTest { + + private lateinit var stubSdkCore: StubSDKCore + + @StringForgery + private lateinit var fakeApplicationId: String + + @BeforeEach + fun `set up`(forge: Forge) { + stubSdkCore = StubSDKCore(forge) + val fakeRumConfiguration = RumConfiguration.Builder(fakeApplicationId) + .trackNonFatalAnrs(false) + .build() + Rum.enable(fakeRumConfiguration, stubSdkCore) + } + + @Test + fun `M return true W isRegistered()`() { + // Given + + // When + val isRegistered = GlobalRumMonitor.isRegistered(stubSdkCore) + + // Then + assertThat(isRegistered).isTrue() + } + + @Test + fun `M return false W isRegistered() {sdkCore without RUM}`() { + // Given + val otherSdkCore: SdkCore = mock() + + // When + val isRegistered = GlobalRumMonitor.isRegistered(otherSdkCore) + + // Then + assertThat(isRegistered).isFalse() + } + + companion object { + private val mainLooper = MainLooperTestConfiguration() + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(mainLooper) + } + } +} 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 c13f21219c..7621aaeee4 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 @@ -19,7 +19,9 @@ import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.integration.tests.assertj.hasRumEvent import com.datadog.android.rum.integration.tests.elmyr.RumIntegrationForgeConfigurator import com.datadog.android.rum.integration.tests.utils.MainLooperTestConfiguration -import com.datadog.android.tests.assertj.StubEventsAssert +import com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor +import com.datadog.android.rum.resource.ResourceId +import com.datadog.android.tests.assertj.StubEventsAssert.Companion.assertThat import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration @@ -42,6 +44,7 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.quality.Strictness import java.net.URL +import java.util.UUID import java.util.concurrent.TimeUnit @Extensions( @@ -97,7 +100,7 @@ class ManualTrackingRumTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(1) .hasRumEvent(index = 0) { hasService(stubSdkCore.getDatadogContext().service) @@ -124,7 +127,7 @@ class ManualTrackingRumTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(2) .hasRumEvent(index = 0) { hasService(stubSdkCore.getDatadogContext().service) @@ -167,7 +170,7 @@ class ManualTrackingRumTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) print(eventsWritten[1].eventData) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(2) .hasRumEvent(index = 0) { hasService(stubSdkCore.getDatadogContext().service) @@ -211,7 +214,7 @@ class ManualTrackingRumTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(4) .hasRumEvent(index = 0) { // Initial view @@ -281,7 +284,7 @@ class ManualTrackingRumTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(4) .hasRumEvent(index = 0) { // Initial view @@ -350,7 +353,7 @@ class ManualTrackingRumTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(4) .hasRumEvent(index = 0) { // Initial view @@ -373,6 +376,7 @@ class ManualTrackingRumTest { hasType("resource") hasViewUrl(key) hasViewName(name) + hasResourceUrl(resourceUrl.toString()) } .hasRumEvent(index = 2) { // View updated with event @@ -384,10 +388,80 @@ class ManualTrackingRumTest { hasViewUrl(key) hasViewName(name) hasResourceCount(1) + } + .hasRumEvent(index = 3) { + // View stopped + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + } + } + + @RepeatedTest(16) + fun `M send view event with resource W startView() + startResource() + stopResource() {using ResourceId}`( + @StringForgery key: String, + @StringForgery name: String, + @StringForgery resourceKey: String, + @Forgery resourceUuid: UUID, + @Forgery resourceUrl: URL, + @IntForgery(200, 599) resourceStatus: Int, + @LongForgery(0) resourceSize: Long + ) { + // Given + val rumMonitor = GlobalRumMonitor.get(stubSdkCore) as AdvancedNetworkRumMonitor + val resourceId = ResourceId(resourceKey, resourceUuid.toString()) + + // When + rumMonitor.startView(key, name, emptyMap()) + rumMonitor.startResource(resourceId, RumResourceMethod.GET, resourceUrl.toString()) + Thread.sleep(100) + rumMonitor.stopResource(resourceId, resourceStatus, resourceSize, RumResourceKind.NATIVE, emptyMap()) + rumMonitor.stopView(key, emptyMap()) + + // Then + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + assertThat(eventsWritten) + .hasSize(4) + .hasRumEvent(index = 0) { + // Initial view + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasActionCount(0) doesNotHaveField("feature_flag") } + .hasRumEvent(index = 1) { + // Custom event + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("resource") + hasViewUrl(key) + hasViewName(name) + hasResourceUrl(resourceUrl.toString()) + } + .hasRumEvent(index = 2) { + // View updated with event + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewUrl(key) + hasViewName(name) + hasResourceCount(1) + } .hasRumEvent(index = 3) { - // View updated with FF + // View stopped hasService(stubSdkCore.getDatadogContext().service) hasApplicationId(fakeApplicationId) hasSessionType("user") @@ -421,7 +495,7 @@ class ManualTrackingRumTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(2) .hasRumEvent(index = 0) { // Initial view @@ -468,7 +542,7 @@ class ManualTrackingRumTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(2) .hasRumEvent(index = 0) { // Initial view @@ -518,7 +592,7 @@ class ManualTrackingRumTest { val expectedFirstViewLoadingTime = intermediateTime - startTime val expectedSecondViewLoadingTime = endTime - startTime val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(3) .hasRumEvent(index = 0) { // Initial view @@ -583,7 +657,7 @@ class ManualTrackingRumTest { // Then val expectedViewLoadingTime = intermediateTime - startTime val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasSize(2) .hasRumEvent(index = 0) { // Initial view diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumConfigurationTest.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumConfigurationTest.kt index b9296fb138..9c86056b9b 100644 --- a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumConfigurationTest.kt +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumConfigurationTest.kt @@ -7,6 +7,8 @@ package com.datadog.android.rum.integration import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.StorageBackedFeature +import com.datadog.android.api.net.RequestExecutionContext import com.datadog.android.core.stub.StubSDKCore import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.Rum @@ -15,10 +17,12 @@ import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod +import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.integration.tests.assertj.hasRumEvent import com.datadog.android.rum.integration.tests.elmyr.RumIntegrationForgeConfigurator import com.datadog.android.rum.integration.tests.utils.MainLooperTestConfiguration -import com.datadog.android.tests.assertj.StubEventsAssert +import com.datadog.android.rum.integration.tests.utils.RumBatchEvent +import com.datadog.android.tests.assertj.StubEventsAssert.Companion.assertThat import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration @@ -55,6 +59,9 @@ class RumConfigurationTest { @StringForgery private lateinit var fakeApplicationId: String + @Forgery + private lateinit var fakeExecutionContext: RequestExecutionContext + @BeforeEach fun `set up`(forge: Forge) { stubSdkCore = StubSDKCore(forge) @@ -67,7 +74,7 @@ class RumConfigurationTest { ) { // Given val fakeRumConfiguration = RumConfiguration.Builder(fakeApplicationId) - .trackNonFatalAnrs(false) + .trackNonFatalAnrs(false) // required to prevent infinite loop in tests .setSessionSampleRate(0f) .build() Rum.enable(fakeRumConfiguration, stubSdkCore) @@ -79,7 +86,7 @@ class RumConfigurationTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten).hasSize(0) + assertThat(eventsWritten).hasSize(0) } @RepeatedTest(16) @@ -170,7 +177,7 @@ class RumConfigurationTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasRumEvent(index = 1) { hasResourceUrl(mappedResourceUrl) } @@ -205,7 +212,7 @@ class RumConfigurationTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasRumEvent(index = 1) { hasErrorMessage(mappedErrorMessage) hasErrorFingerprint(mappedErrorFingerprint) @@ -242,7 +249,7 @@ class RumConfigurationTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasRumEvent(index = 1) { hasActionTargetName(mappedTargetName) } @@ -272,13 +279,143 @@ class RumConfigurationTest { // Then val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) - StubEventsAssert.assertThat(eventsWritten) + assertThat(eventsWritten) .hasRumEvent(index = 0) { hasViewName(mappedViewName) hasViewUrl(mappedViewUrl) } } + @RepeatedTest(16) + fun `M use session listener W setSessionListener()`( + @StringForgery viewKey: String, + @StringForgery viewName: String + ) { + // Given + var sessionIdCallback: String? = null + var isDiscardedCallback: Boolean? = null + val fakeRumConfiguration = RumConfiguration.Builder(fakeApplicationId) + .trackNonFatalAnrs(false) + .setSessionListener(object : RumSessionListener { + override fun onSessionStarted(sessionId: String, isDiscarded: Boolean) { + sessionIdCallback = sessionId + isDiscardedCallback = isDiscarded + } + }) + .build() + Rum.enable(fakeRumConfiguration, stubSdkCore) + val rumMonitor = GlobalRumMonitor.get(stubSdkCore) + + // When + rumMonitor.startView(viewKey, viewName, emptyMap()) + + // Then + assertThat(sessionIdCallback).isNotNull() + .matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}") + assertThat(isDiscardedCallback).isNotNull() + } + + // region RequestFactory + + @RepeatedTest(16) + fun `M createRequest to core site W RumConfiguration#Builder()`( + @Forgery fakeRumEvents: List, + @StringForgery fakeMetadata: String + ) { + // Given + val datadogContext = stubSdkCore.getDatadogContext() + val expectedSite = datadogContext.site + val expectedClientToken = datadogContext.clientToken + val expectedSource = datadogContext.source + val expectedSdkVersion = datadogContext.sdkVersion + val expectedTags = listOf( + "service" to datadogContext.service, + "version" to datadogContext.version, + "sdk_version" to expectedSdkVersion, + "env" to datadogContext.env, + "variant" to datadogContext.variant, + "retry_count" to fakeExecutionContext.previousResponseCode?.let { fakeExecutionContext.attemptNumber }, + "last_failure_status" to fakeExecutionContext.previousResponseCode + ) + .filter { it.second != null } + .joinToString(",") { it.first + ":" + it.second } + val fakeRumConfiguration = RumConfiguration.Builder(fakeApplicationId) + .trackNonFatalAnrs(false) // required to prevent infinite loop in tests + .build() + val fakeBatch = fakeRumEvents.map { it.batchEvent } + Rum.enable(fakeRumConfiguration, stubSdkCore) + + // When + val rumFeature = stubSdkCore.getFeature(Feature.RUM_FEATURE_NAME)?.unwrap() + val requestFactory = rumFeature?.requestFactory + val request = requestFactory?.create( + datadogContext, + fakeExecutionContext, + fakeBatch, + fakeMetadata.toByteArray() + ) + + // Then + checkNotNull(request) + assertThat( + request.url + ).isEqualTo("${expectedSite.intakeEndpoint}/api/v2/rum?ddsource=$expectedSource&ddtags=$expectedTags") + assertThat(request.headers).containsEntry("DD-API-KEY", expectedClientToken) + assertThat(request.headers).containsEntry("DD-EVP-ORIGIN", expectedSource) + assertThat(request.headers).containsEntry("DD-EVP-ORIGIN-VERSION", expectedSdkVersion) + assertThat(request.contentType).isEqualTo("text/plain;charset=UTF-8") + } + + @RepeatedTest(16) + fun `M createRequest to custom endpoint W RumConfiguration#Builder#useCustomEndpoint()`( + @StringForgery fakeEndpoint: String, + @Forgery fakeRumEvents: List, + @StringForgery fakeMetadata: String + ) { + // Given + val datadogContext = stubSdkCore.getDatadogContext() + val expectedClientToken = datadogContext.clientToken + val expectedSource = datadogContext.source + val expectedSdkVersion = datadogContext.sdkVersion + val expectedTags = listOf( + "service" to datadogContext.service, + "version" to datadogContext.version, + "sdk_version" to expectedSdkVersion, + "env" to datadogContext.env, + "variant" to datadogContext.variant, + "retry_count" to fakeExecutionContext.previousResponseCode?.let { fakeExecutionContext.attemptNumber }, + "last_failure_status" to fakeExecutionContext.previousResponseCode + ) + .filter { it.second != null } + .joinToString(",") { it.first + ":" + it.second } + val fakeBatch = fakeRumEvents.map { it.batchEvent } + val fakeRumConfiguration = RumConfiguration.Builder(fakeApplicationId) + .trackNonFatalAnrs(false) // required to prevent infinite loop in tests + .useCustomEndpoint(fakeEndpoint) + .build() + Rum.enable(fakeRumConfiguration, stubSdkCore) + + // When + val rumFeature = stubSdkCore.getFeature(Feature.RUM_FEATURE_NAME)?.unwrap() + val requestFactory = rumFeature?.requestFactory + val request = requestFactory?.create( + datadogContext, + fakeExecutionContext, + fakeBatch, + fakeMetadata.toByteArray() + ) + + // Then + checkNotNull(request) + assertThat(request.url).isEqualTo("$fakeEndpoint/api/v2/rum?ddsource=$expectedSource&ddtags=$expectedTags") + assertThat(request.headers).containsEntry("DD-API-KEY", expectedClientToken) + assertThat(request.headers).containsEntry("DD-EVP-ORIGIN", expectedSource) + assertThat(request.headers).containsEntry("DD-EVP-ORIGIN-VERSION", expectedSdkVersion) + assertThat(request.contentType).isEqualTo("text/plain;charset=UTF-8") + } + + // endregion + companion object { private val mainLooper = MainLooperTestConfiguration() diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumResourceInputStreamTest.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumResourceInputStreamTest.kt new file mode 100644 index 0000000000..56b4ad8091 --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumResourceInputStreamTest.kt @@ -0,0 +1,181 @@ +/* + * 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.integration + +import com.datadog.android.api.feature.Feature +import com.datadog.android.core.stub.StubSDKCore +import com.datadog.android.rum.GlobalRumMonitor +import com.datadog.android.rum.Rum +import com.datadog.android.rum.RumConfiguration +import com.datadog.android.rum.integration.tests.assertj.hasRumEvent +import com.datadog.android.rum.integration.tests.elmyr.RumIntegrationForgeConfigurator +import com.datadog.android.rum.integration.tests.utils.MainLooperTestConfiguration +import com.datadog.android.rum.resource.RumResourceInputStream +import com.datadog.android.tests.assertj.StubEventsAssert.Companion.assertThat +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.Forgery +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.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mockito.mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.ByteArrayOutputStream +import java.io.InputStream + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class), + ExtendWith(TestConfigurationExtension::class) +) +@ForgeConfiguration(RumIntegrationForgeConfigurator::class) +@MockitoSettings(strictness = Strictness.LENIENT) +class RumResourceInputStreamTest { + + private lateinit var stubSdkCore: StubSDKCore + + @StringForgery + private lateinit var fakeApplicationId: String + + @BeforeEach + fun `set up`(forge: Forge) { + stubSdkCore = StubSDKCore(forge) + val fakeRumConfiguration = RumConfiguration.Builder(fakeApplicationId) + .trackNonFatalAnrs(false) + .build() + Rum.enable(fakeRumConfiguration, stubSdkCore) + } + + @RepeatedTest(4) + fun `M report RUM Resource W asRumResource()`( + @StringForgery viewKey: String, + @StringForgery viewName: String, + @StringForgery resourceUrl: String, + @StringForgery data: String + ) { + // Given + GlobalRumMonitor.get(stubSdkCore).startView(viewKey, viewName) + val input = data.toByteArray() + val inputStream = input.inputStream() + val rumResourceInputStream = RumResourceInputStream(inputStream, resourceUrl, stubSdkCore) + val outputStream = ByteArrayOutputStream(input.size) + + // When + rumResourceInputStream.use { + it.transferTo(outputStream) + } + + // Then + assertThat(outputStream.toByteArray()).isEqualTo(input) + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + assertThat(eventsWritten).hasSize(3) + .hasRumEvent(index = 0) { + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewName(viewName) + hasResourceCount(0) + } + .hasRumEvent(index = 1) { + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("resource") + hasViewName(viewName) + hasResourceUrl(resourceUrl) + } + .hasRumEvent(index = 2) { + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewName(viewName) + hasResourceCount(1) + } + } + + @RepeatedTest(4) + fun `M report RUM Error W asRumResource() + read()`( + @StringForgery viewKey: String, + @StringForgery viewName: String, + @StringForgery resourceUrl: String, + @Forgery error: Throwable + ) { + // Given + GlobalRumMonitor.get(stubSdkCore).startView(viewKey, viewName) + val inputStream: InputStream = mock() + val rumResourceInputStream = RumResourceInputStream(inputStream, resourceUrl, stubSdkCore) + whenever(inputStream.read()) doThrow error + + // When + var forwardedError: Throwable? = null + try { + rumResourceInputStream.read() + } catch (e: Throwable) { + forwardedError = e + } + + // Then + assertThat(forwardedError).isEqualTo(error) + val eventsWritten = stubSdkCore.eventsWritten(Feature.RUM_FEATURE_NAME) + assertThat(eventsWritten).hasSize(3) + .hasRumEvent(index = 0) { + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewName(viewName) + hasResourceCount(0) + hasErrorCount(0) + } + .hasRumEvent(index = 1) { + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("error") + hasViewName(viewName) + hasErrorType(error.javaClass.name) + } + .hasRumEvent(index = 2) { + hasService(stubSdkCore.getDatadogContext().service) + hasApplicationId(fakeApplicationId) + hasSessionType("user") + hasSource("android") + hasType("view") + hasViewName(viewName) + hasResourceCount(0) + hasErrorCount(1) + } + } + + companion object { + private val mainLooper = MainLooperTestConfiguration() + + @TestConfigurationsProvider + @JvmStatic + fun getTestConfigurations(): List { + return listOf(mainLooper) + } + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumSessionEndedIntegrationTelemetryTest.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumSessionEndedIntegrationTelemetryTest.kt index dffbb8ec8d..08fd47cb16 100644 --- a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumSessionEndedIntegrationTelemetryTest.kt +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/RumSessionEndedIntegrationTelemetryTest.kt @@ -7,13 +7,14 @@ package com.datadog.android.rum.integration import com.datadog.android.core.stub.StubSDKCore +import com.datadog.android.core.stub.StubTelemetryEvent import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.Rum import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceMethod -import com.datadog.android.rum.integration.tests.assertj.TelemetryMetricAssert.Companion.assertThat +import com.datadog.android.rum.integration.tests.assertj.StubTelemetryEventAssert.Companion.assertThat import com.datadog.android.rum.integration.tests.elmyr.RumIntegrationForgeConfigurator import com.datadog.android.rum.integration.tests.utils.MainLooperTestConfiguration import com.datadog.tools.unit.annotations.TestConfigurationsProvider @@ -79,7 +80,7 @@ class RumSessionEndedIntegrationTelemetryTest { rumMonitor.stopSession() // Then - assertThat(stubSdkCore.lastMetric()).isEmpty() + assertThat(stubSdkCore.lastMetric()).isNull() } @Test @@ -219,7 +220,12 @@ class RumSessionEndedIntegrationTelemetryTest { .hasNoViewResourceEventCounts(missedResourceCount) } + private fun StubSDKCore.lastMetric(): StubTelemetryEvent? { + return telemetryEventsWritten().lastOrNull { it.type == StubTelemetryEvent.Type.METRIC } + } + companion object { + private val mainLooper = MainLooperTestConfiguration() @TestConfigurationsProvider 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 07594317af..d9a85585d7 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 @@ -103,6 +103,11 @@ class RumEventAssert(actual: JsonObject) : // region Error Attributes + fun hasErrorType(kind: String): RumEventAssert { + hasField("error.type", kind) + return this + } + fun hasErrorMessage(message: String): RumEventAssert { hasField("error.message", message) return this diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/StubTelemetryEventAssert.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/StubTelemetryEventAssert.kt new file mode 100644 index 0000000000..0a2c06f57a --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/StubTelemetryEventAssert.kt @@ -0,0 +1,69 @@ +/* + * 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.integration.tests.assertj + +import com.datadog.android.core.stub.StubTelemetryEvent +import com.datadog.tools.unit.assertj.JsonObjectAssert.Companion.assertThat +import com.google.gson.Gson +import org.assertj.core.api.AbstractObjectAssert + +class StubTelemetryEventAssert(actual: StubTelemetryEvent?) : + AbstractObjectAssert(actual, StubTelemetryEventAssert::class.java) { + + val additionalPropertiesJson by lazy { Gson().toJsonTree(actual!!.additionalProperties).asJsonObject } + + fun hasWasStopped(wasStopped: Boolean): StubTelemetryEventAssert { + assertThat(additionalPropertiesJson, true).hasField("rse.was_stopped", wasStopped) + return this + } + + fun hasViewCount(viewCount: Int): StubTelemetryEventAssert { + assertThat(additionalPropertiesJson, true).hasField("rse.views_count.total", viewCount) + return this + } + + fun hasBackgroundEventsTrackingEnable(enable: Boolean): StubTelemetryEventAssert { + assertThat(additionalPropertiesJson, true).hasField("rse.has_background_events_tracking_enabled", enable) + return this + } + + fun hasNtpOffsetAtStart(ntpOffsetAtStart: Long): StubTelemetryEventAssert { + assertThat(additionalPropertiesJson, true).hasField("rse.ntp_offset.at_start", ntpOffsetAtStart) + return this + } + + fun hasNtpOffsetAtEnd(ntpOffsetAtEnd: Long): StubTelemetryEventAssert { + assertThat(additionalPropertiesJson, true).hasField("rse.ntp_offset.at_end", ntpOffsetAtEnd) + return this + } + + fun hasNoViewActionEventCounts(count: Int): StubTelemetryEventAssert { + assertThat(additionalPropertiesJson, true).hasField("rse.no_view_events_count.actions", count) + return this + } + + fun hasNoViewErrorEventCounts(count: Int): StubTelemetryEventAssert { + assertThat(additionalPropertiesJson, true).hasField("rse.no_view_events_count.errors", count) + return this + } + + fun hasNoViewResourceEventCounts(count: Int): StubTelemetryEventAssert { + assertThat(additionalPropertiesJson, true).hasField("rse.no_view_events_count.resources", count) + return this + } + + fun hasNoViewLongTaskEventCounts(count: Int): StubTelemetryEventAssert { + assertThat(additionalPropertiesJson, true).hasField("rse.no_view_events_count.long_tasks", count) + return this + } + + companion object { + fun assertThat(actual: StubTelemetryEvent?): StubTelemetryEventAssert { + return StubTelemetryEventAssert(actual) + } + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/TelemetryMetricAssert.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/TelemetryMetricAssert.kt deleted file mode 100644 index 768d422fde..0000000000 --- a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/assertj/TelemetryMetricAssert.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.integration.tests.assertj - -import com.datadog.tools.unit.assertj.JsonObjectAssert -import com.google.gson.Gson -import com.google.gson.JsonObject - -class TelemetryMetricAssert(actual: JsonObject) : JsonObjectAssert(actual, true) { - - fun hasWasStopped(wasStopped: Boolean): TelemetryMetricAssert { - hasField("additionalProperties.rse.was_stopped", wasStopped) - return this - } - - fun hasViewCount(viewCount: Int): TelemetryMetricAssert { - hasField("additionalProperties.rse.views_count.total", viewCount) - return this - } - - fun hasBackgroundEventsTrackingEnable(enable: Boolean): TelemetryMetricAssert { - hasField("additionalProperties.rse.has_background_events_tracking_enabled", enable) - return this - } - - fun hasNtpOffsetAtStart(ntpOffsetAtStart: Long): TelemetryMetricAssert { - hasField("additionalProperties.rse.ntp_offset.at_start", ntpOffsetAtStart) - return this - } - - fun hasNtpOffsetAtEnd(ntpOffsetAtEnd: Long): TelemetryMetricAssert { - hasField("additionalProperties.rse.ntp_offset.at_end", ntpOffsetAtEnd) - return this - } - - fun hasNoViewActionEventCounts(count: Int): TelemetryMetricAssert { - hasField("additionalProperties.rse.no_view_events_count.actions", count) - return this - } - - fun hasNoViewErrorEventCounts(count: Int): TelemetryMetricAssert { - hasField("additionalProperties.rse.no_view_events_count.errors", count) - return this - } - - fun hasNoViewResourceEventCounts(count: Int): TelemetryMetricAssert { - hasField("additionalProperties.rse.no_view_events_count.resources", count) - return this - } - - fun hasNoViewLongTaskEventCounts(count: Int): TelemetryMetricAssert { - hasField("additionalProperties.rse.no_view_events_count.long_tasks", count) - return this - } - - companion object { - fun assertThat(actual: Map<*, *>?): TelemetryMetricAssert { - return TelemetryMetricAssert(Gson().toJsonTree(actual).asJsonObject) - } - } -} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ActionEventForgeryFactory.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ActionEventForgeryFactory.kt new file mode 100644 index 0000000000..23f1eb42b6 --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ActionEventForgeryFactory.kt @@ -0,0 +1,110 @@ +/* + * 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.integration.tests.elmyr + +import com.datadog.android.rum.model.ActionEvent +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import fr.xgouchet.elmyr.jvm.ext.aTimestamp +import java.net.URL +import java.util.UUID + +internal class ActionEventForgeryFactory : + ForgeryFactory { + override fun getForgery(forge: Forge): ActionEvent { + return ActionEvent( + date = forge.aTimestamp(), + action = ActionEvent.ActionEventAction( + type = forge.getForgery(), + id = forge.aNullable { getForgery().toString() }, + target = forge.aNullable { + ActionEvent.ActionEventActionTarget(anAlphabeticalString()) + }, + error = forge.aNullable { ActionEvent.Error(aLong(0, 512)) }, + crash = forge.aNullable { ActionEvent.Crash(aLong(0, 512)) }, + resource = forge.aNullable { ActionEvent.Resource(aLong(0, 512)) }, + longTask = forge.aNullable { ActionEvent.LongTask(aLong(0, 512)) }, + loadingTime = forge.aNullable { aPositiveLong(strict = true) }, + frustration = forge.aNullable { + ActionEvent.Frustration( + type = forge.aList { + forge.aValueFrom(ActionEvent.Type::class.java) + }.distinct() + ) + } + ), + view = ActionEvent.ActionEventView( + id = forge.getForgery().toString(), + url = forge.aStringMatching("https://[a-z]+.[a-z]{3}/[a-z0-9_/]+"), + referrer = forge.aNullable { getForgery().toString() }, + name = forge.aNullable { anAlphabeticalString() }, + inForeground = forge.aNullable { aBool() } + ), + connectivity = forge.aNullable { + ActionEvent.Connectivity( + status = getForgery(), + interfaces = aList { getForgery() }, + cellular = aNullable { + ActionEvent.Cellular( + technology = aNullable { anAlphabeticalString() }, + carrierName = aNullable { anAlphabeticalString() } + ) + } + ) + }, + synthetics = forge.aNullable { + ActionEvent.Synthetics( + testId = forge.anHexadecimalString(), + resultId = forge.anHexadecimalString() + ) + }, + usr = forge.aNullable { + ActionEvent.Usr( + id = aNullable { anHexadecimalString() }, + name = aNullable { aStringMatching("[A-Z][a-z]+ [A-Z]\\. [A-Z][a-z]+") }, + email = aNullable { aStringMatching("[a-z]+\\.[a-z]+@[a-z]+\\.[a-z]{3}") }, + additionalProperties = exhaustiveAttributes(excludedKeys = setOf("id", "name", "email")) + ) + }, + application = ActionEvent.Application(forge.getForgery().toString()), + service = forge.aNullable { anAlphabeticalString() }, + session = ActionEvent.ActionEventSession( + id = forge.getForgery().toString(), + type = ActionEvent.ActionEventSessionType.USER, + hasReplay = forge.aNullable { aBool() } + ), + source = forge.aNullable { aValueFrom(ActionEvent.ActionEventSource::class.java) }, + ciTest = forge.aNullable { + ActionEvent.CiTest(anHexadecimalString()) + }, + os = forge.aNullable { + ActionEvent.Os( + name = forge.aString(), + version = "${forge.aSmallInt()}.${forge.aSmallInt()}.${forge.aSmallInt()}", + versionMajor = forge.aSmallInt().toString() + ) + }, + device = forge.aNullable { + ActionEvent.Device( + name = forge.aString(), + model = forge.aString(), + brand = forge.aString(), + type = forge.aValueFrom(ActionEvent.DeviceType::class.java), + architecture = forge.aString() + ) + }, + context = forge.aNullable { + ActionEvent.Context(additionalProperties = forge.exhaustiveAttributes()) + }, + dd = ActionEvent.Dd( + session = forge.aNullable { ActionEvent.DdSession(aNullable { getForgery() }) }, + browserSdkVersion = forge.aNullable { aStringMatching("\\d+\\.\\d+\\.\\d+") } + ) + ) + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ErrorEventForgeryFactory.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ErrorEventForgeryFactory.kt new file mode 100644 index 0000000000..139bce068b --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ErrorEventForgeryFactory.kt @@ -0,0 +1,132 @@ +/* + * 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.integration.tests.elmyr + +import com.datadog.android.core.internal.utils.loggableStackTrace +import com.datadog.android.rum.model.ErrorEvent +import com.datadog.tools.unit.forge.aThrowable +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import fr.xgouchet.elmyr.jvm.ext.aTimestamp +import java.net.URL +import java.util.UUID + +internal class ErrorEventForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): ErrorEvent { + return ErrorEvent( + buildId = forge.aNullable { getForgery().toString() }, + date = forge.aTimestamp(), + error = ErrorEvent.Error( + id = forge.aNullable { getForgery().toString() }, + message = forge.anAlphabeticalString(), + source = forge.getForgery(), + stack = forge.aNullable { aThrowable().loggableStackTrace() }, + resource = forge.aNullable { + ErrorEvent.Resource( + url = aStringMatching("https://[a-z]+.[a-z]{3}/[a-z0-9_/]+"), + method = getForgery(), + statusCode = aLong(200, 600), + provider = aNullable { + ErrorEvent.Provider( + domain = aNullable { aStringMatching("[a-z]+\\.[a-z]{3}") }, + name = aNullable { anAlphabeticalString() }, + type = aNullable() + ) + } + ) + }, + sourceType = forge.aNullable { forge.getForgery() }, + isCrash = forge.aNullable { aBool() }, + type = forge.aNullable { anAlphabeticalString() }, + handling = forge.aNullable { getForgery() }, + handlingStack = forge.aNullable { aThrowable().loggableStackTrace() }, + category = forge.aNullable { getForgery() }, + threads = forge.aNullable { + aList { + ErrorEvent.Thread( + name = anAlphaNumericalString(), + crashed = aBool(), + stack = aThrowable().stackTraceToString(), + state = aNullable { getForgery().name.lowercase() } + ) + } + }, + timeSinceAppStart = forge.aNullable { aPositiveLong() } + ), + view = ErrorEvent.ErrorEventView( + id = forge.getForgery().toString(), + url = forge.aStringMatching("https://[a-z]+.[a-z]{3}/[a-z0-9_/]+"), + referrer = forge.aNullable { getForgery().toString() }, + name = forge.aNullable { anAlphabeticalString() }, + inForeground = forge.aNullable { aBool() } + ), + connectivity = forge.aNullable { + ErrorEvent.Connectivity( + status = getForgery(), + interfaces = aList { getForgery() }, + cellular = aNullable { + ErrorEvent.Cellular( + technology = aNullable { anAlphabeticalString() }, + carrierName = aNullable { anAlphabeticalString() } + ) + } + ) + }, + synthetics = forge.aNullable { + ErrorEvent.Synthetics( + testId = forge.anHexadecimalString(), + resultId = forge.anHexadecimalString() + ) + }, + usr = forge.aNullable { + ErrorEvent.Usr( + id = aNullable { anHexadecimalString() }, + name = aNullable { aStringMatching("[A-Z][a-z]+ [A-Z]\\. [A-Z][a-z]+") }, + email = aNullable { aStringMatching("[a-z]+\\.[a-z]+@[a-z]+\\.[a-z]{3}") }, + additionalProperties = exhaustiveAttributes(excludedKeys = setOf("id", "name", "email")) + ) + }, + action = forge.aNullable { ErrorEvent.Action(aList { getForgery().toString() }) }, + application = ErrorEvent.Application(forge.getForgery().toString()), + service = forge.aNullable { anAlphabeticalString() }, + session = ErrorEvent.ErrorEventSession( + id = forge.getForgery().toString(), + type = ErrorEvent.ErrorEventSessionType.USER, + hasReplay = forge.aNullable { aBool() } + ), + source = forge.aNullable { aValueFrom(ErrorEvent.ErrorEventSource::class.java) }, + ciTest = forge.aNullable { + ErrorEvent.CiTest(anHexadecimalString()) + }, + os = forge.aNullable { + ErrorEvent.Os( + name = forge.aString(), + version = "${forge.aSmallInt()}.${forge.aSmallInt()}.${forge.aSmallInt()}", + versionMajor = forge.aSmallInt().toString() + ) + }, + device = forge.aNullable { + ErrorEvent.Device( + name = forge.aString(), + model = forge.aString(), + brand = forge.aString(), + type = forge.aValueFrom(ErrorEvent.DeviceType::class.java), + architecture = forge.aString() + ) + }, + context = forge.aNullable { + ErrorEvent.Context(additionalProperties = forge.exhaustiveAttributes()) + }, + dd = ErrorEvent.Dd( + session = forge.aNullable { ErrorEvent.DdSession(aNullable { getForgery() }) }, + browserSdkVersion = forge.aNullable { aStringMatching("\\d+\\.\\d+\\.\\d+") } + ) + ) + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/LongTaskEventForgeryFactory.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/LongTaskEventForgeryFactory.kt new file mode 100644 index 0000000000..d5547c54d6 --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/LongTaskEventForgeryFactory.kt @@ -0,0 +1,100 @@ +/* + * 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.integration.tests.elmyr + +import com.datadog.android.rum.model.LongTaskEvent +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import fr.xgouchet.elmyr.jvm.ext.aTimestamp +import java.net.URL +import java.util.UUID + +internal class LongTaskEventForgeryFactory : + ForgeryFactory { + override fun getForgery(forge: Forge): LongTaskEvent { + return LongTaskEvent( + date = forge.aTimestamp(), + longTask = LongTaskEvent.LongTask( + id = forge.aNullable { getForgery().toString() }, + duration = forge.aPositiveLong(), + isFrozenFrame = forge.aNullable { aBool() } + ), + view = LongTaskEvent.LongTaskEventView( + id = forge.getForgery().toString(), + referrer = forge.aNullable { getForgery().toString() }, + url = forge.aStringMatching("https://[a-z]+.[a-z]{3}/[a-z0-9_/]+"), + name = forge.aNullable { anAlphabeticalString() } + ), + connectivity = forge.aNullable { + LongTaskEvent.Connectivity( + status = getForgery(), + interfaces = aList { getForgery() }, + cellular = aNullable { + LongTaskEvent.Cellular( + technology = aNullable { anAlphabeticalString() }, + carrierName = aNullable { anAlphabeticalString() } + ) + } + ) + }, + synthetics = forge.aNullable { + LongTaskEvent.Synthetics( + testId = forge.anHexadecimalString(), + resultId = forge.anHexadecimalString() + ) + }, + usr = forge.aNullable { + LongTaskEvent.Usr( + id = aNullable { anHexadecimalString() }, + name = aNullable { aStringMatching("[A-Z][a-z]+ [A-Z]\\. [A-Z][a-z]+") }, + email = aNullable { aStringMatching("[a-z]+\\.[a-z]+@[a-z]+\\.[a-z]{3}") }, + additionalProperties = exhaustiveAttributes(excludedKeys = setOf("id", "name", "email")) + ) + }, + action = forge.aNullable { + LongTaskEvent.Action(aList { getForgery().toString() }) + }, + application = LongTaskEvent.Application(forge.getForgery().toString()), + service = forge.aNullable { anAlphabeticalString() }, + session = LongTaskEvent.LongTaskEventSession( + id = forge.getForgery().toString(), + type = LongTaskEvent.LongTaskEventSessionType.USER, + hasReplay = forge.aNullable { aBool() } + ), + source = forge.aNullable { aValueFrom(LongTaskEvent.LongTaskEventSource::class.java) }, + ciTest = forge.aNullable { + LongTaskEvent.CiTest(anHexadecimalString()) + }, + os = forge.aNullable { + LongTaskEvent.Os( + name = forge.aString(), + version = "${forge.aSmallInt()}.${forge.aSmallInt()}.${forge.aSmallInt()}", + versionMajor = forge.aSmallInt().toString() + ) + }, + device = forge.aNullable { + LongTaskEvent.Device( + name = forge.aString(), + model = forge.aString(), + brand = forge.aString(), + type = forge.aValueFrom(LongTaskEvent.DeviceType::class.java), + architecture = forge.aString() + ) + }, + context = forge.aNullable { + LongTaskEvent.Context( + additionalProperties = forge.exhaustiveAttributes() + ) + }, + dd = LongTaskEvent.Dd( + session = forge.aNullable { LongTaskEvent.DdSession(getForgery()) }, + browserSdkVersion = forge.aNullable { aStringMatching("\\d+\\.\\d+\\.\\d+") } + ) + ) + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ResourceEventForgeryFactory.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ResourceEventForgeryFactory.kt new file mode 100644 index 0000000000..0feadcf8c2 --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ResourceEventForgeryFactory.kt @@ -0,0 +1,134 @@ +/* + * 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.integration.tests.elmyr + +import com.datadog.android.rum.internal.domain.event.ResourceTiming +import com.datadog.android.rum.model.ResourceEvent +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import fr.xgouchet.elmyr.jvm.ext.aTimestamp +import java.net.URL +import java.util.UUID + +internal class ResourceEventForgeryFactory : + ForgeryFactory { + override fun getForgery(forge: Forge): ResourceEvent { + val timing = forge.aNullable() + return ResourceEvent( + date = forge.aTimestamp(), + resource = ResourceEvent.Resource( + id = forge.aNullable { getForgery().toString() }, + type = forge.getForgery(), + url = forge.aStringMatching("https://[a-z]+.[a-z]{3}/[a-z0-9_/]+"), + duration = forge.aNullable { aPositiveLong() }, + method = forge.aNullable(), + statusCode = forge.aNullable { aLong(200, 600) }, + size = forge.aNullable { aPositiveLong() }, + dns = timing?.dns(), + connect = timing?.connect(), + ssl = timing?.ssl(), + firstByte = timing?.firstByte(), + download = timing?.download(), + redirect = forge.aNullable { + ResourceEvent.Redirect( + aPositiveLong(), + aPositiveLong() + ) + }, + provider = forge.aNullable { + ResourceEvent.Provider( + domain = aNullable { aStringMatching("[a-z]+\\.[a-z]{3}") }, + name = aNullable { anAlphabeticalString() }, + type = aNullable() + ) + }, + graphql = forge.aNullable { + ResourceEvent.Graphql( + operationType = aValueFrom(ResourceEvent.OperationType::class.java), + operationName = aNullable { aString() }, + payload = aNullable { aString() }, + variables = aNullable { aString() } + ) + } + ), + view = ResourceEvent.ResourceEventView( + id = forge.getForgery().toString(), + url = forge.aStringMatching("https://[a-z]+.[a-z]{3}/[a-z0-9_/]+"), + referrer = forge.aNullable { getForgery().toString() }, + name = forge.aNullable { anAlphabeticalString() } + ), + connectivity = forge.aNullable { + ResourceEvent.Connectivity( + status = getForgery(), + interfaces = aList { getForgery() }, + cellular = aNullable { + ResourceEvent.Cellular( + technology = aNullable { anAlphabeticalString() }, + carrierName = aNullable { anAlphabeticalString() } + ) + } + ) + }, + synthetics = forge.aNullable { + ResourceEvent.Synthetics( + testId = forge.anHexadecimalString(), + resultId = forge.anHexadecimalString() + ) + }, + usr = forge.aNullable { + ResourceEvent.Usr( + id = aNullable { anHexadecimalString() }, + name = aNullable { aStringMatching("[A-Z][a-z]+ [A-Z]\\. [A-Z][a-z]+") }, + email = aNullable { aStringMatching("[a-z]+\\.[a-z]+@[a-z]+\\.[a-z]{3}") }, + additionalProperties = exhaustiveAttributes(excludedKeys = setOf("id", "name", "email")) + ) + }, + action = forge.aNullable { + ResourceEvent.Action(aList { getForgery().toString() }) + }, + application = ResourceEvent.Application(forge.getForgery().toString()), + service = forge.aNullable { anAlphabeticalString() }, + session = ResourceEvent.ResourceEventSession( + id = forge.getForgery().toString(), + type = ResourceEvent.ResourceEventSessionType.USER, + hasReplay = forge.aNullable { aBool() } + ), + source = forge.aNullable { aValueFrom(ResourceEvent.ResourceEventSource::class.java) }, + ciTest = forge.aNullable { + ResourceEvent.CiTest(anHexadecimalString()) + }, + os = forge.aNullable { + ResourceEvent.Os( + name = forge.aString(), + version = "${forge.aSmallInt()}.${forge.aSmallInt()}.${forge.aSmallInt()}", + versionMajor = forge.aSmallInt().toString() + ) + }, + device = forge.aNullable { + ResourceEvent.Device( + name = forge.aString(), + model = forge.aString(), + brand = forge.aString(), + type = forge.aValueFrom(ResourceEvent.DeviceType::class.java), + architecture = forge.aString() + ) + }, + context = forge.aNullable { + ResourceEvent.Context( + additionalProperties = forge.exhaustiveAttributes() + ) + }, + dd = ResourceEvent.Dd( + session = forge.aNullable { ResourceEvent.DdSession(aNullable { getForgery() }) }, + browserSdkVersion = forge.aNullable { aStringMatching("\\d+\\.\\d+\\.\\d+") }, + spanId = forge.aNullable { aNumericalString() }, + traceId = forge.aNullable { aNumericalString() } + ) + ) + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ResourceTimingForgeryFactory.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ResourceTimingForgeryFactory.kt new file mode 100644 index 0000000000..49fda46776 --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ResourceTimingForgeryFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.integration.tests.elmyr + +import com.datadog.android.rum.internal.domain.event.ResourceTiming +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class ResourceTimingForgeryFactory : + ForgeryFactory { + override fun getForgery(forge: Forge): ResourceTiming { + return ResourceTiming( + dnsStart = forge.aPositiveLong(), + dnsDuration = forge.aPositiveLong(), + connectStart = forge.aPositiveLong(), + connectDuration = forge.aPositiveLong(), + sslStart = forge.aPositiveLong(), + sslDuration = forge.aPositiveLong(), + firstByteStart = forge.aPositiveLong(), + firstByteDuration = forge.aPositiveLong(), + downloadStart = forge.aPositiveLong(), + downloadDuration = forge.aPositiveLong() + ) + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumBatchEventForgeryFactory.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumBatchEventForgeryFactory.kt new file mode 100644 index 0000000000..d88df866d2 --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumBatchEventForgeryFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.integration.tests.elmyr + +import com.datadog.android.api.storage.RawBatchEvent +import com.datadog.android.rum.integration.tests.utils.RumBatchEvent +import com.datadog.android.rum.model.ActionEvent +import com.datadog.android.rum.model.ErrorEvent +import com.datadog.android.rum.model.LongTaskEvent +import com.datadog.android.rum.model.ResourceEvent +import com.datadog.android.rum.model.ViewEvent +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +class RumBatchEventForgeryFactory : ForgeryFactory { + override fun getForgery(forge: Forge): RumBatchEvent { + val jsonElement = forge.anElementFrom( + forge.getForgery().toJson(), + forge.getForgery().toJson(), + forge.getForgery().toJson(), + forge.getForgery().toJson(), + forge.getForgery().toJson() + ) + + return RumBatchEvent( + jsonElement, + RawBatchEvent( + jsonElement.toString().toByteArray() + ) + ) + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumEventExt.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumEventExt.kt new file mode 100644 index 0000000000..484d021b12 --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumEventExt.kt @@ -0,0 +1,487 @@ +/* + * 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. + */ +@file:Suppress("TooManyFunctions") + +package com.datadog.android.rum.integration.tests.elmyr + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DeviceType +import com.datadog.android.api.context.NetworkInfo +import com.datadog.android.rum.RumActionType +import com.datadog.android.rum.RumErrorSource +import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod +import com.datadog.android.rum.internal.domain.event.ResourceTiming +import com.datadog.android.rum.model.ActionEvent +import com.datadog.android.rum.model.ErrorEvent +import com.datadog.android.rum.model.LongTaskEvent +import com.datadog.android.rum.model.ResourceEvent +import com.datadog.android.rum.model.ViewEvent +import java.util.Locale + +// region Resource.Method conversion + +internal fun RumResourceMethod.toResourceMethod(): ResourceEvent.Method { + return when (this) { + RumResourceMethod.GET -> ResourceEvent.Method.GET + RumResourceMethod.POST -> ResourceEvent.Method.POST + RumResourceMethod.HEAD -> ResourceEvent.Method.HEAD + RumResourceMethod.PUT -> ResourceEvent.Method.PUT + RumResourceMethod.DELETE -> ResourceEvent.Method.DELETE + RumResourceMethod.PATCH -> ResourceEvent.Method.PATCH + RumResourceMethod.TRACE -> ResourceEvent.Method.TRACE + RumResourceMethod.OPTIONS -> ResourceEvent.Method.OPTIONS + RumResourceMethod.CONNECT -> ResourceEvent.Method.CONNECT + } +} + +internal fun RumResourceMethod.toErrorMethod(): ErrorEvent.Method { + return when (this) { + RumResourceMethod.GET -> ErrorEvent.Method.GET + RumResourceMethod.POST -> ErrorEvent.Method.POST + RumResourceMethod.HEAD -> ErrorEvent.Method.HEAD + RumResourceMethod.PUT -> ErrorEvent.Method.PUT + RumResourceMethod.DELETE -> ErrorEvent.Method.DELETE + RumResourceMethod.PATCH -> ErrorEvent.Method.PATCH + RumResourceMethod.TRACE -> ErrorEvent.Method.TRACE + RumResourceMethod.OPTIONS -> ErrorEvent.Method.OPTIONS + RumResourceMethod.CONNECT -> ErrorEvent.Method.CONNECT + } +} + +// endregion + +internal fun String.toOperationType(internalLogger: InternalLogger): ResourceEvent.OperationType? { + return try { + ResourceEvent.OperationType.valueOf(this.uppercase(Locale.US)) + } catch (e: IllegalArgumentException) { + internalLogger.log( + InternalLogger.Level.ERROR, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { "Unable to convert [$this] to a valid graphql operation type" }, + e + ) + null + } +} + +internal fun RumResourceKind.toSchemaType(): ResourceEvent.ResourceType { + return when (this) { + RumResourceKind.BEACON -> ResourceEvent.ResourceType.BEACON + RumResourceKind.FETCH -> ResourceEvent.ResourceType.FETCH + RumResourceKind.XHR -> ResourceEvent.ResourceType.XHR + RumResourceKind.DOCUMENT -> ResourceEvent.ResourceType.DOCUMENT + RumResourceKind.IMAGE -> ResourceEvent.ResourceType.IMAGE + RumResourceKind.JS -> ResourceEvent.ResourceType.JS + RumResourceKind.FONT -> ResourceEvent.ResourceType.FONT + RumResourceKind.CSS -> ResourceEvent.ResourceType.CSS + RumResourceKind.MEDIA -> ResourceEvent.ResourceType.MEDIA + RumResourceKind.NATIVE -> ResourceEvent.ResourceType.NATIVE + RumResourceKind.UNKNOWN, + RumResourceKind.OTHER -> ResourceEvent.ResourceType.OTHER + } +} + +internal fun RumErrorSource.toSchemaSource(): ErrorEvent.ErrorSource { + return when (this) { + RumErrorSource.NETWORK -> ErrorEvent.ErrorSource.NETWORK + RumErrorSource.SOURCE -> ErrorEvent.ErrorSource.SOURCE + RumErrorSource.CONSOLE -> ErrorEvent.ErrorSource.CONSOLE + RumErrorSource.LOGGER -> ErrorEvent.ErrorSource.LOGGER + RumErrorSource.AGENT -> ErrorEvent.ErrorSource.AGENT + RumErrorSource.WEBVIEW -> ErrorEvent.ErrorSource.WEBVIEW + } +} + +internal fun ResourceTiming.dns(): ResourceEvent.Dns? { + return if (dnsStart > 0) { + ResourceEvent.Dns(duration = dnsDuration, start = dnsStart) + } else { + null + } +} + +internal fun ResourceTiming.connect(): ResourceEvent.Connect? { + return if (connectStart > 0) { + ResourceEvent.Connect(duration = connectDuration, start = connectStart) + } else { + null + } +} + +internal fun ResourceTiming.ssl(): ResourceEvent.Ssl? { + return if (sslStart > 0) { + ResourceEvent.Ssl(duration = sslDuration, start = sslStart) + } else { + null + } +} + +internal fun ResourceTiming.firstByte(): ResourceEvent.FirstByte? { + return if (firstByteStart >= 0 && firstByteDuration > 0) { + ResourceEvent.FirstByte(duration = firstByteDuration, start = firstByteStart) + } else { + null + } +} + +internal fun ResourceTiming.download(): ResourceEvent.Download? { + return if (downloadStart > 0) { + ResourceEvent.Download(duration = downloadDuration, start = downloadStart) + } else { + null + } +} + +internal fun RumActionType.toSchemaType(): ActionEvent.ActionEventActionType { + return when (this) { + RumActionType.TAP -> ActionEvent.ActionEventActionType.TAP + RumActionType.SCROLL -> ActionEvent.ActionEventActionType.SCROLL + RumActionType.SWIPE -> ActionEvent.ActionEventActionType.SWIPE + RumActionType.CLICK -> ActionEvent.ActionEventActionType.CLICK + RumActionType.BACK -> ActionEvent.ActionEventActionType.BACK + RumActionType.CUSTOM -> ActionEvent.ActionEventActionType.CUSTOM + } +} + +// region NetworkInfo conversion + +internal fun NetworkInfo.toResourceConnectivity(): ResourceEvent.Connectivity { + val status = if (isConnected()) { + ResourceEvent.Status.CONNECTED + } else { + ResourceEvent.Status.NOT_CONNECTED + } + val interfaces = when (connectivity) { + NetworkInfo.Connectivity.NETWORK_ETHERNET -> listOf(ResourceEvent.Interface.ETHERNET) + NetworkInfo.Connectivity.NETWORK_WIFI -> listOf(ResourceEvent.Interface.WIFI) + NetworkInfo.Connectivity.NETWORK_WIMAX -> listOf(ResourceEvent.Interface.WIMAX) + NetworkInfo.Connectivity.NETWORK_BLUETOOTH -> listOf(ResourceEvent.Interface.BLUETOOTH) + NetworkInfo.Connectivity.NETWORK_2G, + NetworkInfo.Connectivity.NETWORK_3G, + NetworkInfo.Connectivity.NETWORK_4G, + NetworkInfo.Connectivity.NETWORK_5G, + NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, + NetworkInfo.Connectivity.NETWORK_CELLULAR -> listOf(ResourceEvent.Interface.CELLULAR) + + NetworkInfo.Connectivity.NETWORK_OTHER -> listOf(ResourceEvent.Interface.OTHER) + NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED -> emptyList() + } + + val cellular = if (cellularTechnology != null || carrierName != null) { + ResourceEvent.Cellular( + technology = cellularTechnology, + carrierName = carrierName + ) + } else { + null + } + return ResourceEvent.Connectivity( + status, + interfaces, + cellular = cellular + ) +} + +internal fun NetworkInfo.toErrorConnectivity(): ErrorEvent.Connectivity { + val status = if (isConnected()) { + ErrorEvent.Status.CONNECTED + } else { + ErrorEvent.Status.NOT_CONNECTED + } + val interfaces = when (connectivity) { + NetworkInfo.Connectivity.NETWORK_ETHERNET -> listOf(ErrorEvent.Interface.ETHERNET) + NetworkInfo.Connectivity.NETWORK_WIFI -> listOf(ErrorEvent.Interface.WIFI) + NetworkInfo.Connectivity.NETWORK_WIMAX -> listOf(ErrorEvent.Interface.WIMAX) + NetworkInfo.Connectivity.NETWORK_BLUETOOTH -> listOf(ErrorEvent.Interface.BLUETOOTH) + NetworkInfo.Connectivity.NETWORK_2G, + NetworkInfo.Connectivity.NETWORK_3G, + NetworkInfo.Connectivity.NETWORK_4G, + NetworkInfo.Connectivity.NETWORK_5G, + NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, + NetworkInfo.Connectivity.NETWORK_CELLULAR -> listOf(ErrorEvent.Interface.CELLULAR) + + NetworkInfo.Connectivity.NETWORK_OTHER -> listOf(ErrorEvent.Interface.OTHER) + NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED -> emptyList() + } + + val cellular = if (cellularTechnology != null || carrierName != null) { + ErrorEvent.Cellular( + technology = cellularTechnology, + carrierName = carrierName + ) + } else { + null + } + return ErrorEvent.Connectivity( + status, + interfaces, + cellular = cellular + ) +} + +internal fun NetworkInfo.toLongTaskConnectivity(): LongTaskEvent.Connectivity { + val status = if (isConnected()) { + LongTaskEvent.Status.CONNECTED + } else { + LongTaskEvent.Status.NOT_CONNECTED + } + val interfaces = when (connectivity) { + NetworkInfo.Connectivity.NETWORK_ETHERNET -> listOf(LongTaskEvent.Interface.ETHERNET) + NetworkInfo.Connectivity.NETWORK_WIFI -> listOf(LongTaskEvent.Interface.WIFI) + NetworkInfo.Connectivity.NETWORK_WIMAX -> listOf(LongTaskEvent.Interface.WIMAX) + NetworkInfo.Connectivity.NETWORK_BLUETOOTH -> listOf(LongTaskEvent.Interface.BLUETOOTH) + NetworkInfo.Connectivity.NETWORK_2G, + NetworkInfo.Connectivity.NETWORK_3G, + NetworkInfo.Connectivity.NETWORK_4G, + NetworkInfo.Connectivity.NETWORK_5G, + NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, + NetworkInfo.Connectivity.NETWORK_CELLULAR -> listOf(LongTaskEvent.Interface.CELLULAR) + + NetworkInfo.Connectivity.NETWORK_OTHER -> listOf(LongTaskEvent.Interface.OTHER) + NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED -> emptyList() + } + + val cellular = if (cellularTechnology != null || carrierName != null) { + LongTaskEvent.Cellular( + technology = cellularTechnology, + carrierName = carrierName + ) + } else { + null + } + return LongTaskEvent.Connectivity( + status, + interfaces, + cellular = cellular + ) +} + +internal fun NetworkInfo.toViewConnectivity(): ViewEvent.Connectivity { + val status = if (isConnected()) { + ViewEvent.Status.CONNECTED + } else { + ViewEvent.Status.NOT_CONNECTED + } + val interfaces = when (connectivity) { + NetworkInfo.Connectivity.NETWORK_ETHERNET -> listOf(ViewEvent.Interface.ETHERNET) + NetworkInfo.Connectivity.NETWORK_WIFI -> listOf(ViewEvent.Interface.WIFI) + NetworkInfo.Connectivity.NETWORK_WIMAX -> listOf(ViewEvent.Interface.WIMAX) + NetworkInfo.Connectivity.NETWORK_BLUETOOTH -> listOf(ViewEvent.Interface.BLUETOOTH) + NetworkInfo.Connectivity.NETWORK_2G, + NetworkInfo.Connectivity.NETWORK_3G, + NetworkInfo.Connectivity.NETWORK_4G, + NetworkInfo.Connectivity.NETWORK_5G, + NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, + NetworkInfo.Connectivity.NETWORK_CELLULAR -> listOf(ViewEvent.Interface.CELLULAR) + + NetworkInfo.Connectivity.NETWORK_OTHER -> listOf(ViewEvent.Interface.OTHER) + NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED -> emptyList() + } + + val cellular = if (cellularTechnology != null || carrierName != null) { + ViewEvent.Cellular( + technology = cellularTechnology, + carrierName = carrierName + ) + } else { + null + } + return ViewEvent.Connectivity( + status, + interfaces, + cellular = cellular + ) +} + +internal fun NetworkInfo.toActionConnectivity(): ActionEvent.Connectivity { + val status = if (isConnected()) { + ActionEvent.Status.CONNECTED + } else { + ActionEvent.Status.NOT_CONNECTED + } + val interfaces = when (connectivity) { + NetworkInfo.Connectivity.NETWORK_ETHERNET -> listOf(ActionEvent.Interface.ETHERNET) + NetworkInfo.Connectivity.NETWORK_WIFI -> listOf(ActionEvent.Interface.WIFI) + NetworkInfo.Connectivity.NETWORK_WIMAX -> listOf(ActionEvent.Interface.WIMAX) + NetworkInfo.Connectivity.NETWORK_BLUETOOTH -> listOf(ActionEvent.Interface.BLUETOOTH) + NetworkInfo.Connectivity.NETWORK_2G, + NetworkInfo.Connectivity.NETWORK_3G, + NetworkInfo.Connectivity.NETWORK_4G, + NetworkInfo.Connectivity.NETWORK_5G, + NetworkInfo.Connectivity.NETWORK_MOBILE_OTHER, + NetworkInfo.Connectivity.NETWORK_CELLULAR -> listOf(ActionEvent.Interface.CELLULAR) + + NetworkInfo.Connectivity.NETWORK_OTHER -> listOf(ActionEvent.Interface.OTHER) + NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED -> emptyList() + } + + val cellular = if (cellularTechnology != null || carrierName != null) { + ActionEvent.Cellular( + technology = cellularTechnology, + carrierName = carrierName + ) + } else { + null + } + return ActionEvent.Connectivity( + status, + interfaces, + cellular = cellular + ) +} + +internal fun NetworkInfo.isConnected(): Boolean { + return connectivity != NetworkInfo.Connectivity.NETWORK_NOT_CONNECTED +} + +// endregion + +// region DeviceType conversion + +internal fun DeviceType.toViewSchemaType(): ViewEvent.DeviceType { + return when (this) { + DeviceType.MOBILE -> ViewEvent.DeviceType.MOBILE + DeviceType.TABLET -> ViewEvent.DeviceType.TABLET + DeviceType.TV -> ViewEvent.DeviceType.TV + DeviceType.DESKTOP -> ViewEvent.DeviceType.DESKTOP + else -> ViewEvent.DeviceType.OTHER + } +} + +internal fun DeviceType.toActionSchemaType(): ActionEvent.DeviceType { + return when (this) { + DeviceType.MOBILE -> ActionEvent.DeviceType.MOBILE + DeviceType.TABLET -> ActionEvent.DeviceType.TABLET + DeviceType.TV -> ActionEvent.DeviceType.TV + DeviceType.DESKTOP -> ActionEvent.DeviceType.DESKTOP + else -> ActionEvent.DeviceType.OTHER + } +} + +internal fun DeviceType.toLongTaskSchemaType(): LongTaskEvent.DeviceType { + return when (this) { + DeviceType.MOBILE -> LongTaskEvent.DeviceType.MOBILE + DeviceType.TABLET -> LongTaskEvent.DeviceType.TABLET + DeviceType.TV -> LongTaskEvent.DeviceType.TV + DeviceType.DESKTOP -> LongTaskEvent.DeviceType.DESKTOP + else -> LongTaskEvent.DeviceType.OTHER + } +} + +internal fun DeviceType.toResourceSchemaType(): ResourceEvent.DeviceType { + return when (this) { + DeviceType.MOBILE -> ResourceEvent.DeviceType.MOBILE + DeviceType.TABLET -> ResourceEvent.DeviceType.TABLET + DeviceType.TV -> ResourceEvent.DeviceType.TV + DeviceType.DESKTOP -> ResourceEvent.DeviceType.DESKTOP + else -> ResourceEvent.DeviceType.OTHER + } +} + +internal fun DeviceType.toErrorSchemaType(): ErrorEvent.DeviceType { + return when (this) { + DeviceType.MOBILE -> ErrorEvent.DeviceType.MOBILE + DeviceType.TABLET -> ErrorEvent.DeviceType.TABLET + DeviceType.TV -> ErrorEvent.DeviceType.TV + DeviceType.DESKTOP -> ErrorEvent.DeviceType.DESKTOP + else -> ErrorEvent.DeviceType.OTHER + } +} + +// endregion + +// region Source + +internal fun ViewEvent.ViewEventSource.Companion.tryFromSource( + source: String, + internalLogger: InternalLogger +): ViewEvent.ViewEventSource? { + return try { + fromJson(source) + } catch (e: NoSuchElementException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { UNKNOWN_SOURCE_WARNING_MESSAGE_FORMAT.format(Locale.US, source) }, + e + ) + null + } +} + +internal fun LongTaskEvent.LongTaskEventSource.Companion.tryFromSource( + source: String, + internalLogger: InternalLogger +): LongTaskEvent.LongTaskEventSource? { + return try { + fromJson(source) + } catch (e: NoSuchElementException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { UNKNOWN_SOURCE_WARNING_MESSAGE_FORMAT.format(Locale.US, source) }, + e + ) + null + } +} + +internal fun ErrorEvent.ErrorEventSource.Companion.tryFromSource( + source: String, + internalLogger: InternalLogger +): ErrorEvent.ErrorEventSource? { + return try { + fromJson(source) + } catch (e: NoSuchElementException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { UNKNOWN_SOURCE_WARNING_MESSAGE_FORMAT.format(Locale.US, source) }, + e + ) + null + } +} + +internal fun ActionEvent.ActionEventSource.Companion.tryFromSource( + source: String, + internalLogger: InternalLogger +): ActionEvent.ActionEventSource? { + return try { + fromJson(source) + } catch (e: NoSuchElementException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { UNKNOWN_SOURCE_WARNING_MESSAGE_FORMAT.format(Locale.US, source) }, + e + ) + null + } +} + +internal fun ResourceEvent.ResourceEventSource.Companion.tryFromSource( + source: String, + internalLogger: InternalLogger +): ResourceEvent.ResourceEventSource? { + return try { + fromJson(source) + } catch (e: NoSuchElementException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { UNKNOWN_SOURCE_WARNING_MESSAGE_FORMAT.format(Locale.US, source) }, + e + ) + null + } +} + +internal const val UNKNOWN_SOURCE_WARNING_MESSAGE_FORMAT = "You are using an unknown " + + "source %s for your events" + +// endregion diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumIntegrationForgeConfigurator.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumIntegrationForgeConfigurator.kt index 49ac809228..aed3f4dd2d 100644 --- a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumIntegrationForgeConfigurator.kt +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/RumIntegrationForgeConfigurator.kt @@ -16,5 +16,14 @@ class RumIntegrationForgeConfigurator : BaseConfigurator() { super.configure(forge) forge.useJvmFactories() forge.useCoreFactories() + + forge.addFactory(ActionEventForgeryFactory()) + forge.addFactory(ErrorEventForgeryFactory()) + forge.addFactory(ResourceEventForgeryFactory()) + forge.addFactory(ResourceTimingForgeryFactory()) + forge.addFactory(LongTaskEventForgeryFactory()) + forge.addFactory(ViewEventForgeryFactory()) + + forge.addFactory(RumBatchEventForgeryFactory()) } } diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ViewEventForgeryFactory.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ViewEventForgeryFactory.kt new file mode 100644 index 0000000000..414290d15a --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/elmyr/ViewEventForgeryFactory.kt @@ -0,0 +1,133 @@ +/* + * 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.integration.tests.elmyr + +import com.datadog.android.rum.model.ViewEvent +import com.datadog.tools.unit.forge.exhaustiveAttributes +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory +import fr.xgouchet.elmyr.jvm.ext.aTimestamp +import java.net.URL +import java.util.UUID + +internal class ViewEventForgeryFactory : ForgeryFactory { + + override fun getForgery(forge: Forge): ViewEvent { + return ViewEvent( + date = forge.aTimestamp(), + view = ViewEvent.ViewEventView( + id = forge.getForgery().toString(), + url = forge.aStringMatching("https://[a-z]+.[a-z]{3}/[a-z0-9_/]+"), + referrer = forge.aNullable { getForgery().toString() }, + name = forge.aNullable { anAlphabeticalString() }, + timeSpent = forge.aPositiveLong(), + error = ViewEvent.Error(forge.aPositiveLong()), + crash = forge.aNullable { ViewEvent.Crash(aPositiveLong()) }, + action = ViewEvent.Action(forge.aPositiveLong()), + resource = ViewEvent.Resource(forge.aPositiveLong()), + longTask = forge.aNullable { ViewEvent.LongTask(forge.aPositiveLong()) }, + frozenFrame = forge.aNullable { ViewEvent.FrozenFrame(aPositiveLong()) }, + loadingType = forge.aNullable(), + loadingTime = forge.aNullable { aPositiveLong() }, + firstContentfulPaint = forge.aNullable { aPositiveLong() }, + largestContentfulPaint = forge.aNullable { aPositiveLong() }, + firstInputDelay = forge.aNullable { aPositiveLong() }, + firstInputTime = forge.aNullable { aPositiveLong() }, + cumulativeLayoutShift = forge.aNullable { aPositiveLong() }, + domComplete = forge.aNullable { aPositiveLong() }, + domContentLoaded = forge.aNullable { aPositiveLong() }, + domInteractive = forge.aNullable { aPositiveLong() }, + loadEvent = forge.aNullable { aPositiveLong() }, + customTimings = forge.aNullable { + ViewEvent.CustomTimings( + aMap { anAlphabeticalString() to aLong() }.toMutableMap() + ) + }, + isActive = forge.aNullable { aBool() }, + isSlowRendered = forge.aNullable { aBool() }, + inForegroundPeriods = forge.aNullable { + aList { + ViewEvent.InForegroundPeriod( + start = aPositiveLong(), + duration = aPositiveLong() + ) + } + }, + memoryAverage = forge.aNullable { aPositiveDouble() }, + memoryMax = forge.aNullable { aPositiveDouble() }, + cpuTicksCount = forge.aNullable { aPositiveDouble() }, + cpuTicksPerSecond = forge.aNullable { aPositiveDouble() }, + refreshRateAverage = forge.aNullable { aPositiveDouble() }, + refreshRateMin = forge.aNullable { aPositiveDouble() }, + frustration = forge.aNullable { ViewEvent.Frustration(aPositiveLong()) } + ), + connectivity = forge.aNullable { + ViewEvent.Connectivity( + status = getForgery(), + interfaces = aList { getForgery() }, + cellular = aNullable { + ViewEvent.Cellular( + technology = aNullable { anAlphabeticalString() }, + carrierName = aNullable { anAlphabeticalString() } + ) + } + ) + }, + synthetics = forge.aNullable { + ViewEvent.Synthetics( + testId = forge.anHexadecimalString(), + resultId = forge.anHexadecimalString() + ) + }, + usr = forge.aNullable { + ViewEvent.Usr( + id = aNullable { anHexadecimalString() }, + name = aNullable { aStringMatching("[A-Z][a-z]+ [A-Z]\\. [A-Z][a-z]+") }, + email = aNullable { aStringMatching("[a-z]+\\.[a-z]+@[a-z]+\\.[a-z]{3}") }, + additionalProperties = exhaustiveAttributes(excludedKeys = setOf("id", "name", "email")) + ) + }, + application = ViewEvent.Application(forge.getForgery().toString()), + service = forge.aNullable { anAlphabeticalString() }, + session = ViewEvent.ViewEventSession( + id = forge.getForgery().toString(), + type = ViewEvent.ViewEventSessionType.USER, + hasReplay = forge.aNullable { aBool() } + ), + source = forge.aNullable { aValueFrom(ViewEvent.ViewEventSource::class.java) }, + ciTest = forge.aNullable { + ViewEvent.CiTest(anHexadecimalString()) + }, + os = forge.aNullable { + ViewEvent.Os( + name = forge.aString(), + version = "${forge.aSmallInt()}.${forge.aSmallInt()}.${forge.aSmallInt()}", + versionMajor = forge.aSmallInt().toString() + ) + }, + device = forge.aNullable { + ViewEvent.Device( + name = forge.aString(), + model = forge.aString(), + brand = forge.aString(), + type = forge.aValueFrom(ViewEvent.DeviceType::class.java), + architecture = forge.aString() + ) + }, + context = forge.aNullable { + ViewEvent.Context( + additionalProperties = exhaustiveAttributes() + ) + }, + dd = ViewEvent.Dd( + session = forge.aNullable { ViewEvent.DdSession(aNullable { getForgery() }) }, + browserSdkVersion = forge.aNullable { aStringMatching("\\d+\\.\\d+\\.\\d+") }, + documentVersion = forge.aPositiveLong(strict = true) + ) + ) + } +} diff --git a/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/utils/RumBatchEvent.kt b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/utils/RumBatchEvent.kt new file mode 100644 index 0000000000..d2655af801 --- /dev/null +++ b/reliability/single-fit/rum/src/test/kotlin/com/datadog/android/rum/integration/tests/utils/RumBatchEvent.kt @@ -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.integration.tests.utils + +import com.datadog.android.api.storage.RawBatchEvent +import com.google.gson.JsonElement + +data class RumBatchEvent( + val rumEvent: JsonElement, + val batchEvent: RawBatchEvent +) diff --git a/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubInternalLogger.kt b/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubInternalLogger.kt index 87737f7ea3..b1f5e3b1bb 100644 --- a/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubInternalLogger.kt +++ b/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubInternalLogger.kt @@ -14,7 +14,8 @@ import com.datadog.android.internal.telemetry.InternalTelemetryEvent @Suppress("UnsafeThirdPartyFunctionCall") internal class StubInternalLogger : InternalLogger { - val telemetryEventsWritten = mutableListOf>() + val telemetryEventsWritten = mutableListOf() + override fun log( level: InternalLogger.Level, target: InternalLogger.Target, @@ -23,9 +24,21 @@ internal class StubInternalLogger : InternalLogger { onlyOnce: Boolean, additionalProperties: Map? ) { - println("${level.name.first()} [${target.name.first()}]: ${messageBuilder()}") + val message = messageBuilder() + println("${level.name.first()} [${target.name.first()}]: $message") additionalProperties?.log() throwable?.printStackTrace() + + if (target == InternalLogger.Target.TELEMETRY) { + val telemetryEvent = StubTelemetryEvent( + type = StubTelemetryEvent.Type.LOG, + message = message, + additionalProperties = additionalProperties.orEmpty(), + level = level, + samplingRate = if (onlyOnce) -1f else 100f + ) + telemetryEventsWritten.add(telemetryEvent) + } } override fun log( @@ -36,21 +49,31 @@ internal class StubInternalLogger : InternalLogger { onlyOnce: Boolean, additionalProperties: Map? ) { - println("${level.name.first()} [${targets.joinToString { it.name.first().toString() }}]: ${messageBuilder()}") + val message = messageBuilder() + println("${level.name.first()} [${targets.joinToString { it.name.first().toString() }}]: $message") additionalProperties?.log() throwable?.printStackTrace() + if (InternalLogger.Target.TELEMETRY in targets) { + val telemetryEvent = StubTelemetryEvent( + type = StubTelemetryEvent.Type.LOG, + message = message, + additionalProperties = additionalProperties.orEmpty(), + level = level + ) + telemetryEventsWritten.add(telemetryEvent) + } } override fun logMetric(messageBuilder: () -> String, additionalProperties: Map, samplingRate: Float) { println("M [T]: ${messageBuilder()} | $samplingRate%") additionalProperties.log() val message = messageBuilder() - val telemetryEvent = - mapOf( - "type" to "mobile_metric", - "message" to message, - "additionalProperties" to additionalProperties - ) + val telemetryEvent = StubTelemetryEvent( + type = StubTelemetryEvent.Type.METRIC, + message = message, + additionalProperties = additionalProperties, + samplingRate = samplingRate + ) telemetryEventsWritten.add(telemetryEvent) } @@ -68,8 +91,15 @@ internal class StubInternalLogger : InternalLogger { apiUsageEvent: InternalTelemetryEvent.ApiUsage, samplingRate: Float ) { - println("${apiUsageEvent::class.simpleName} | $samplingRate%") + println("U [T]: ${apiUsageEvent.javaClass.simpleName} | $samplingRate%") apiUsageEvent.additionalProperties.log() + val telemetryEvent = StubTelemetryEvent( + type = StubTelemetryEvent.Type.API_USAGE, + message = apiUsageEvent.javaClass.name, + additionalProperties = apiUsageEvent.additionalProperties, + samplingRate = samplingRate + ) + telemetryEventsWritten.add(telemetryEvent) } private fun Map.log() { diff --git a/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubSDKCore.kt b/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubSDKCore.kt index 7ce6a41e2d..075b68a018 100644 --- a/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubSDKCore.kt +++ b/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubSDKCore.kt @@ -55,6 +55,14 @@ class StubSDKCore( return featureScopes[featureName]?.eventsWritten() ?: emptyList() } + /** + * Lists all the telemetry events written to this sdk instance. + * @return a list of [StubEvent] + */ + fun telemetryEventsWritten(): List { + return (internalLogger as StubInternalLogger).telemetryEventsWritten + } + /** * Lists all the events sent to the given feature. * @param featureName the name of the feature @@ -72,14 +80,6 @@ class StubSDKCore( datadogContext = datadogContext.copy(networkInfo = networkInfo) } - /** - * Returns the last metric is sent by [StubInternalLogger]. - */ - fun lastMetric(): Map { - return (internalLogger as StubInternalLogger) - .telemetryEventsWritten.lastOrNull { it["type"] == "mobile_metric" }.orEmpty() - } - /** * Stubs the user info visible via the SDK Core. * @param userInfo the user info diff --git a/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubTelemetryEvent.kt b/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubTelemetryEvent.kt new file mode 100644 index 0000000000..1507c88e5f --- /dev/null +++ b/reliability/stub-core/src/main/kotlin/com/datadog/android/core/stub/StubTelemetryEvent.kt @@ -0,0 +1,39 @@ +/* + * 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.core.stub + +import com.datadog.android.api.InternalLogger + +/** + * Stubs a telemetry event. + * @param type the [Type] of event + * @param message the message attached to this telemetry + * @param additionalProperties a map of additional properties + * @param samplingRate the (optional) sampling rate to avoid spamming the same message in our telemetry org + * @param level the (optional) level attached to Log telemetry events + */ +data class StubTelemetryEvent( + val type: Type, + val message: String, + val additionalProperties: Map, + val samplingRate: Float? = null, + val level: InternalLogger.Level? = null +) { + /** + * The type of Telemetry event. + */ + enum class Type { + /** An API Usage event. */ + API_USAGE, + + /** A Log event. */ + LOG, + + /** A Metric event. */ + METRIC + } +} diff --git a/tools/unit/src/main/kotlin/com/datadog/tools/unit/assertj/JsonObjectAssert.kt b/tools/unit/src/main/kotlin/com/datadog/tools/unit/assertj/JsonObjectAssert.kt index 77c719a59c..3b0d1345af 100644 --- a/tools/unit/src/main/kotlin/com/datadog/tools/unit/assertj/JsonObjectAssert.kt +++ b/tools/unit/src/main/kotlin/com/datadog/tools/unit/assertj/JsonObjectAssert.kt @@ -643,9 +643,10 @@ open class JsonObjectAssert( /** * Create assertion for [JsonObject]. * @param actual the actual object to assert on + * @param lenientKeys whether keys should be evaluated leniently (default false) * @return the created assertion object. */ - fun assertThat(actual: JsonObject): JsonObjectAssert = - JsonObjectAssert(actual) + fun assertThat(actual: JsonObject, lenientKeys: Boolean = false): JsonObjectAssert = + JsonObjectAssert(actual, lenientKeys) } }