diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index 2c445293d6..414dc607b4 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -53,6 +53,7 @@ interface com.datadog.android.api.InternalLogger fun logApiUsage(Float = DEFAULT_API_USAGE_TELEMETRY_SAMPLING_RATE, () -> com.datadog.android.internal.telemetry.InternalTelemetryEvent.ApiUsage) companion object val UNBOUND: InternalLogger +fun InternalLogger.logToUser(InternalLogger.Level, Boolean = false, () -> String) interface com.datadog.android.api.SdkCore val name: String val time: com.datadog.android.api.context.TimeInfo @@ -158,12 +159,21 @@ interface com.datadog.android.api.instrumentation.network.HttpRequestInfo val contentType: String? val method: String fun contentLength(): Long? +interface com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder + fun setUrl(String): HttpRequestInfoBuilder + fun addHeader(String, String): HttpRequestInfoBuilder + fun removeHeader(String): HttpRequestInfoBuilder + fun replaceHeader(String, String): HttpRequestInfoBuilder + fun addTag(Class, T?): HttpRequestInfoBuilder + fun build(): HttpRequestInfo interface com.datadog.android.api.instrumentation.network.HttpResponseInfo val url: String val statusCode: Int val headers: Map> val contentType: String? val contentLength: Long? +interface com.datadog.android.api.instrumentation.network.MutableHttpRequestInfo + fun newBuilder(): HttpRequestInfoBuilder data class com.datadog.android.api.net.Request constructor(String, String, String, Map, ByteArray, String? = null) data class com.datadog.android.api.net.RequestExecutionContext diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index 5a022d8580..d41d93f087 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -127,6 +127,11 @@ public final class com/datadog/android/api/InternalLogger$Target : java/lang/Enu public static fun values ()[Lcom/datadog/android/api/InternalLogger$Target; } +public final class com/datadog/android/api/InternalLoggerKt { + public static final fun logToUser (Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/api/InternalLogger$Level;ZLkotlin/jvm/functions/Function0;)V + public static synthetic fun logToUser$default (Lcom/datadog/android/api/InternalLogger;Lcom/datadog/android/api/InternalLogger$Level;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V +} + public abstract interface class com/datadog/android/api/SdkCore { public abstract fun addAccountExtraInfo (Ljava/util/Map;)V public abstract fun addUserProperties (Ljava/util/Map;)V @@ -462,6 +467,19 @@ public abstract interface class com/datadog/android/api/instrumentation/network/ public abstract fun getUrl ()Ljava/lang/String; } +public abstract interface class com/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder { + public abstract fun addHeader (Ljava/lang/String;[Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public abstract fun addTag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public abstract fun build ()Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo; + public abstract fun removeHeader (Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public abstract fun replaceHeader (Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public abstract fun setUrl (Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; +} + +public final class com/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder$DefaultImpls { + public static fun replaceHeader (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; +} + public abstract interface class com/datadog/android/api/instrumentation/network/HttpResponseInfo { public abstract fun getContentLength ()Ljava/lang/Long; public abstract fun getContentType ()Ljava/lang/String; @@ -470,6 +488,10 @@ public abstract interface class com/datadog/android/api/instrumentation/network/ public abstract fun getUrl ()Ljava/lang/String; } +public abstract interface class com/datadog/android/api/instrumentation/network/MutableHttpRequestInfo { + public abstract fun newBuilder ()Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; +} + public final class com/datadog/android/api/net/Request { public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[BLjava/lang/String;)V public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[BLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt index 95e217157d..a371f35006 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/InternalLogger.kt @@ -192,3 +192,17 @@ interface InternalLogger { val UNBOUND: InternalLogger = SdkInternalLogger(null) } } + +/** + * Convenience extension function to log a message directly to the user via Logcat. + * This is equivalent to calling [InternalLogger.log] with [InternalLogger.Target.USER]. + * + * @param level the severity level of the log. + * @param onlyOnce whether only one instance of the message should be sent per lifetime of the logger. + * @param messageBuilder the lambda building the log message. + */ +fun InternalLogger.logToUser( + level: InternalLogger.Level, + onlyOnce: Boolean = false, + messageBuilder: () -> String +) = log(level, InternalLogger.Target.USER, messageBuilder, onlyOnce = onlyOnce) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder.kt new file mode 100644 index 0000000000..a074c9377e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder.kt @@ -0,0 +1,73 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.api.instrumentation.network + +import com.datadog.android.lint.InternalApi + +/** + * For internal usage only. + * + * A builder interface for modifying [com.datadog.android.api.instrumentation.network.HttpRequestInfo] instances. + * This interface allows to build new [com.datadog.android.api.instrumentation.network.HttpRequestInfo] with + * modified HTTP request properties such as URL, headers, and tags. + * + * Use [com.datadog.android.api.instrumentation.network.MutableHttpRequestInfo.newBuilder] to obtain a builder instance. + */ +@InternalApi +interface HttpRequestInfoBuilder { + + /** + * Sets the URL for this request. + * @param url the new URL to set. + * @return this modifier for chaining. + */ + fun setUrl(url: String): HttpRequestInfoBuilder + + /** + * Adds a header with the specified key and values. + * If a header with the same key already exists, the new values are appended. + * @param key the header name. + * @param values the header values. + * @return this modifier for chaining. + */ + fun addHeader(key: String, vararg values: String): HttpRequestInfoBuilder + + /** + * Removes a header with the specified key. + * @param key the header name to remove. + * @return this modifier for chaining. + */ + fun removeHeader(key: String): HttpRequestInfoBuilder + + /** + * Replaces a header with the specified key and value. + * This is equivalent to removing the existing header and adding a new one. + * @param key the header name. + * @param value the new header value. + * @return this modifier for chaining. + */ + fun replaceHeader(key: String, value: String): HttpRequestInfoBuilder = apply { + removeHeader(key) + addHeader(key, value) + } + + /** + * Adds a tag of the specified type to this request. + * Tags can be used to attach arbitrary metadata to requests for later retrieval. + * @param T the type of the tag. + * @param type the class representing the tag type. + * @param tag the tag value, or null to remove the tag. + * @return this modifier for chaining. + */ + fun addTag(type: Class, tag: T?): HttpRequestInfoBuilder + + /** + * Builds and returns the modified [com.datadog.android.api.instrumentation.network.HttpRequestInfo]. + * @return the resulting [com.datadog.android.api.instrumentation.network.HttpRequestInfo] with all modifications applied. + */ + fun build(): HttpRequestInfo +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/MutableHttpRequestInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/MutableHttpRequestInfo.kt new file mode 100644 index 0000000000..434e04795d --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/MutableHttpRequestInfo.kt @@ -0,0 +1,27 @@ +/* + * 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.api.instrumentation.network + +import com.datadog.android.lint.InternalApi + +/** + * For internal usage only. + * + * Represents an HTTP request info that can be modified. + * + * This interface allows instrumentation components to create a modified copy + * of the request info (e.g., to add tracing headers) while preserving the + * original request data. + */ +@InternalApi +interface MutableHttpRequestInfo { + /** + * Creates a modifier to modify this request info. + * @return a new [com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder] initialized with this request's data. + */ + fun newBuilder(): HttpRequestInfoBuilder +} diff --git a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RequestInfoForgeryFactory.kt b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RequestInfoForgeryFactory.kt index 811fdca28b..5df182f733 100644 --- a/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RequestInfoForgeryFactory.kt +++ b/dd-sdk-android-core/src/testFixtures/kotlin/com/datadog/android/tests/elmyr/RequestInfoForgeryFactory.kt @@ -8,6 +8,8 @@ package com.datadog.android.tests.elmyr import com.datadog.android.api.instrumentation.network.ExtendedRequestInfo import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.api.instrumentation.network.MutableHttpRequestInfo import com.datadog.android.core.internal.net.HttpSpec import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory @@ -29,13 +31,32 @@ class RequestInfoForgeryFactory : ForgeryFactory { override val headers: Map>, override val contentType: String?, override val method: String, - private val contentLength: Long?, - private val tags: Map - ) : HttpRequestInfo, ExtendedRequestInfo { + internal val contentLength: Long?, + internal val tags: Map + ) : HttpRequestInfo, ExtendedRequestInfo, MutableHttpRequestInfo { @Suppress("UNCHECKED_CAST") override fun tag(type: Class): T? = tags[type] as? T override fun contentLength(): Long? = contentLength + override fun newBuilder(): HttpRequestInfoBuilder = StubHttpRequestInfoBuilder(this.copy()) + } + + private data class StubHttpRequestInfoBuilder(private var request: StubRequestInfo) : HttpRequestInfoBuilder { + override fun setUrl(url: String) = apply { request = request.copy(url = url) } + + override fun addHeader(key: String, vararg values: String) = apply { + request = request.copy(headers = request.headers.toMutableMap().also { it[key] = values.asList() }) + } + + override fun removeHeader(key: String) = apply { + request = request.copy(headers = request.headers.toMutableMap().also { it.remove(key) }) + } + + override fun addTag(type: Class, tag: T?) = apply { + request = request.copy(tags = request.tags.toMutableMap().also { it[type] = tag }) + } + + override fun build(): HttpRequestInfo = request.copy() } } diff --git a/dd-sdk-android-internal/api/apiSurface b/dd-sdk-android-internal/api/apiSurface index 4eab508776..3438e5b819 100644 --- a/dd-sdk-android-internal/api/apiSurface +++ b/dd-sdk-android-internal/api/apiSurface @@ -65,29 +65,6 @@ enum com.datadog.android.internal.network.GraphQLHeaders - DD_GRAPHQL_VARIABLES_HEADER - DD_GRAPHQL_TYPE_HEADER - DD_GRAPHQL_PAYLOAD_HEADER -object com.datadog.android.internal.network.HttpSpec - object Method - const val GET: String - const val POST: String - const val PATCH: String - const val PUT: String - const val HEAD: String - const val DELETE: String - const val TRACE: String - const val OPTIONS: String - const val CONNECT: String - fun values() - object Headers - const val CONTENT_TYPE: String - const val CONTENT_LENGTH: String - const val WEBSOCKET_ACCEPT_HEADER: String - object ContentType - const val TEXT_PLAIN: String - const val TEXT_EVENT_STREAM: String - const val APPLICATION_GRPC: String - const val APPLICATION_GRPC_PROTO: String - const val APPLICATION_GRPC_JSON: String - fun isStream(String?): Boolean interface com.datadog.android.internal.profiler.BenchmarkCounter fun add(Long, Map) interface com.datadog.android.internal.profiler.BenchmarkMeter diff --git a/dd-sdk-android-internal/api/dd-sdk-android-internal.api b/dd-sdk-android-internal/api/dd-sdk-android-internal.api index a931b20fa4..da168ac551 100644 --- a/dd-sdk-android-internal/api/dd-sdk-android-internal.api +++ b/dd-sdk-android-internal/api/dd-sdk-android-internal.api @@ -131,41 +131,6 @@ public final class com/datadog/android/internal/network/GraphQLHeaders : java/la public static fun values ()[Lcom/datadog/android/internal/network/GraphQLHeaders; } -public final class com/datadog/android/internal/network/HttpSpec { - public static final field INSTANCE Lcom/datadog/android/internal/network/HttpSpec; -} - -public final class com/datadog/android/internal/network/HttpSpec$ContentType { - public static final field APPLICATION_GRPC Ljava/lang/String; - public static final field APPLICATION_GRPC_JSON Ljava/lang/String; - public static final field APPLICATION_GRPC_PROTO Ljava/lang/String; - public static final field INSTANCE Lcom/datadog/android/internal/network/HttpSpec$ContentType; - public static final field TEXT_EVENT_STREAM Ljava/lang/String; - public static final field TEXT_PLAIN Ljava/lang/String; - public final fun isStream (Ljava/lang/String;)Z -} - -public final class com/datadog/android/internal/network/HttpSpec$Headers { - public static final field CONTENT_LENGTH Ljava/lang/String; - public static final field CONTENT_TYPE Ljava/lang/String; - public static final field INSTANCE Lcom/datadog/android/internal/network/HttpSpec$Headers; - public static final field WEBSOCKET_ACCEPT_HEADER Ljava/lang/String; -} - -public final class com/datadog/android/internal/network/HttpSpec$Method { - public static final field CONNECT Ljava/lang/String; - public static final field DELETE Ljava/lang/String; - public static final field GET Ljava/lang/String; - public static final field HEAD Ljava/lang/String; - public static final field INSTANCE Lcom/datadog/android/internal/network/HttpSpec$Method; - public static final field OPTIONS Ljava/lang/String; - public static final field PATCH Ljava/lang/String; - public static final field POST Ljava/lang/String; - public static final field PUT Ljava/lang/String; - public static final field TRACE Ljava/lang/String; - public final fun values ()Ljava/util/List; -} - public abstract interface class com/datadog/android/internal/profiler/BenchmarkCounter { public abstract fun add (JLjava/util/Map;)V } diff --git a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/network/HttpSpec.kt b/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/network/HttpSpec.kt deleted file mode 100644 index 118e2e83d4..0000000000 --- a/dd-sdk-android-internal/src/main/java/com/datadog/android/internal/network/HttpSpec.kt +++ /dev/null @@ -1,100 +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.internal.network - -/** - * HTTP specification constants and utilities. - */ -object HttpSpec { - - /** - * Standard HTTP request methods. - */ - object Method { - /** HTTP GET method. */ - const val GET: String = "GET" - - /** HTTP POST method. */ - const val POST: String = "POST" - - /** HTTP PATCH method. */ - const val PATCH: String = "PATCH" - - /** HTTP PUT method. */ - const val PUT: String = "PUT" - - /** HTTP HEAD method. */ - const val HEAD: String = "HEAD" - - /** HTTP DELETE method. */ - const val DELETE: String = "DELETE" - - /** HTTP TRACE method. */ - const val TRACE: String = "TRACE" - - /** HTTP OPTIONS method. */ - const val OPTIONS: String = "OPTIONS" - - /** HTTP CONNECT method. */ - const val CONNECT: String = "CONNECT" - - /** - * Returns a list of all HTTP methods. - */ - fun values() = listOf(GET, POST, PATCH, PUT, HEAD, DELETE, TRACE, OPTIONS, CONNECT) - } - - /** - * Standard HTTP header names. - */ - object Headers { - /** Content-Type header name. */ - const val CONTENT_TYPE: String = "Content-Type" - - /** Content-Length header name. */ - const val CONTENT_LENGTH: String = "Content-Length" - - /** Sec-WebSocket-Accept header name. */ - const val WEBSOCKET_ACCEPT_HEADER: String = "Sec-WebSocket-Accept" - } - - /** - * HTTP content type values and utilities. - */ - object ContentType { - - /** Plain text content type. */ - const val TEXT_PLAIN: String = "text/plain" - - /** Server-Sent Events content type. */ - const val TEXT_EVENT_STREAM: String = "text/event-stream" - - /** gRPC content type. */ - const val APPLICATION_GRPC: String = "application/grpc" - - /** gRPC with Protocol Buffers content type. */ - const val APPLICATION_GRPC_PROTO: String = "application/grpc+proto" - - /** gRPC with JSON content type. */ - const val APPLICATION_GRPC_JSON: String = "application/grpc+json" - - /** - * Checks if the given content type represents a streaming protocol. - * @param contentType the content type to check - * @return true if the content type is a stream type, false otherwise - */ - fun isStream(contentType: String?): Boolean { - return contentType != null && contentType in STREAM_CONTENT_TYPES - } - - private val STREAM_CONTENT_TYPES = setOf( - TEXT_EVENT_STREAM, - APPLICATION_GRPC, - APPLICATION_GRPC_PROTO, - APPLICATION_GRPC_JSON - ) - } -} diff --git a/detekt_custom_safe_calls.yml b/detekt_custom_safe_calls.yml index c91b356cc5..bb69aeb6b6 100644 --- a/detekt_custom_safe_calls.yml +++ b/detekt_custom_safe_calls.yml @@ -665,6 +665,7 @@ datadog: - "java.lang.StringBuilder.constructor()" - "java.math.BigInteger.toHexString()" - "java.math.BigInteger.toLong()" + - "java.nio.ByteBuffer.hashCode()" - "java.nio.charset.Charset.defaultCharset()" - "java.nio.CharBuffer.position()" - "java.security.MessageDigest.digest()" @@ -952,6 +953,7 @@ datadog: - "kotlin.collections.MutableMap.remove(androidx.compose.foundation.interaction.DragInteraction.Start)" - "kotlin.collections.MutableMap.remove(com.datadog.android.api.SdkCore)" - "kotlin.collections.MutableMap.remove(com.datadog.android.rum.internal.vitals.VitalListener)" + - "kotlin.collections.MutableMap.remove(java.lang.Class)" - "kotlin.collections.MutableMap.remove(kotlin.Int)" - "kotlin.collections.MutableMap.remove(kotlin.Long)" - "kotlin.collections.MutableMap.remove(kotlin.String)" diff --git a/features/dd-sdk-android-flags-openfeature/src/main/kotlin/com/datadog/android/flags/openfeature/internal/FlagsClientExt.kt b/features/dd-sdk-android-flags-openfeature/src/main/kotlin/com/datadog/android/flags/openfeature/internal/FlagsClientExt.kt index 002bb052f0..d56ff7bce1 100644 --- a/features/dd-sdk-android-flags-openfeature/src/main/kotlin/com/datadog/android/flags/openfeature/internal/FlagsClientExt.kt +++ b/features/dd-sdk-android-flags-openfeature/src/main/kotlin/com/datadog/android/flags/openfeature/internal/FlagsClientExt.kt @@ -24,6 +24,9 @@ import kotlin.coroutines.suspendCoroutine * @throws [OpenFeatureError.GeneralError] if setting the context fails or times out. */ internal suspend fun FlagsClient.setEvaluationContextSuspend(context: EvaluationContext) { + // Subsequent invocation of any resume function will produce + // an IllegalStateException, but we are safe here + @Suppress("UnsafeThirdPartyFunctionCall") suspendCoroutine { continuation -> val callback = object : EvaluationContextCallback { override fun onSuccess() { diff --git a/features/dd-sdk-android-rum/api/apiSurface b/features/dd-sdk-android-rum/api/apiSurface index 1ca892d841..6892194343 100644 --- a/features/dd-sdk-android-rum/api/apiSurface +++ b/features/dd-sdk-android-rum/api/apiSurface @@ -179,6 +179,10 @@ class com.datadog.android.rum._RumInternalProxy fun setRumSessionTypeOverride(com.datadog.android.rum.RumConfiguration.Builder, RumSessionType): com.datadog.android.rum.RumConfiguration.Builder fun setDisableJankStats(com.datadog.android.rum.RumConfiguration.Builder, Boolean): com.datadog.android.rum.RumConfiguration.Builder fun setInsightsCollector(com.datadog.android.rum.RumConfiguration.Builder, com.datadog.android.rum.internal.instrumentation.insights.InsightsCollector): com.datadog.android.rum.RumConfiguration.Builder + fun createRumResourceInstrumentation(String, com.datadog.android.rum.configuration.RumResourceInstrumentationConfiguration): com.datadog.android.rum.internal.net.RumResourceInstrumentation +class com.datadog.android.rum.configuration.RumResourceInstrumentationConfiguration + fun setSdkInstanceName(String) + fun setRumResourceAttributesProvider(com.datadog.android.rum.RumResourceAttributesProvider) data class com.datadog.android.rum.configuration.SlowFramesConfiguration constructor(Int = DEFAULT_SLOW_FRAME_RECORDS_MAX_AMOUNT, Long = DEFAULT_FROZEN_FRAME_THRESHOLD_NS, Long = DEFAULT_CONTINUOUS_SLOW_FRAME_THRESHOLD_NS, Long = DEFAULT_FREEZE_DURATION_NS, Long = DEFAULT_VIEW_LIFETIME_THRESHOLD_NS) companion object @@ -225,7 +229,7 @@ class com.datadog.android.rum.internal.net.RumResourceInstrumentation fun sendWaitForResourceTimingEvent(com.datadog.android.api.instrumentation.network.HttpRequestInfo) fun sendTiming(com.datadog.android.api.instrumentation.network.HttpRequestInfo, com.datadog.android.rum.internal.domain.event.ResourceTiming) fun startResource(com.datadog.android.api.instrumentation.network.HttpRequestInfo) - fun stopResource(com.datadog.android.api.instrumentation.network.HttpRequestInfo, com.datadog.android.api.instrumentation.network.HttpResponseInfo) + fun stopResource(com.datadog.android.api.instrumentation.network.HttpRequestInfo, com.datadog.android.api.instrumentation.network.HttpResponseInfo, Map = emptyMap()) fun stopResourceWithError(com.datadog.android.api.instrumentation.network.HttpRequestInfo, Throwable) fun reportInstrumentationError(String) companion object 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 2e665b3136..c480238f3e 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 @@ -272,6 +272,7 @@ public final class com/datadog/android/rum/_RumInternalProxy { } public final class com/datadog/android/rum/_RumInternalProxy$Companion { + public final fun createRumResourceInstrumentation (Ljava/lang/String;Lcom/datadog/android/rum/configuration/RumResourceInstrumentationConfiguration;)Lcom/datadog/android/rum/internal/net/RumResourceInstrumentation; public final fun setAdditionalConfiguration (Lcom/datadog/android/rum/RumConfiguration$Builder;Ljava/util/Map;)Lcom/datadog/android/rum/RumConfiguration$Builder; public final fun setComposeActionTrackingStrategy (Lcom/datadog/android/rum/RumConfiguration$Builder;Lcom/datadog/android/rum/tracking/ActionTrackingStrategy;)Lcom/datadog/android/rum/RumConfiguration$Builder; public final fun setDisableJankStats (Lcom/datadog/android/rum/RumConfiguration$Builder;Z)Lcom/datadog/android/rum/RumConfiguration$Builder; @@ -280,6 +281,12 @@ public final class com/datadog/android/rum/_RumInternalProxy$Companion { public final fun setTelemetryConfigurationEventMapper (Lcom/datadog/android/rum/RumConfiguration$Builder;Lcom/datadog/android/event/EventMapper;)Lcom/datadog/android/rum/RumConfiguration$Builder; } +public final class com/datadog/android/rum/configuration/RumResourceInstrumentationConfiguration { + public fun ()V + public final fun setRumResourceAttributesProvider (Lcom/datadog/android/rum/RumResourceAttributesProvider;)Lcom/datadog/android/rum/configuration/RumResourceInstrumentationConfiguration; + public final fun setSdkInstanceName (Ljava/lang/String;)Lcom/datadog/android/rum/configuration/RumResourceInstrumentationConfiguration; +} + public final class com/datadog/android/rum/configuration/SlowFramesConfiguration { public static final field Companion Lcom/datadog/android/rum/configuration/SlowFramesConfiguration$Companion; public fun ()V @@ -428,7 +435,8 @@ public final class com/datadog/android/rum/internal/net/RumResourceInstrumentati public final fun sendTiming (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;Lcom/datadog/android/rum/internal/domain/event/ResourceTiming;)V public final fun sendWaitForResourceTimingEvent (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;)V public final fun startResource (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;)V - public final fun stopResource (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;Lcom/datadog/android/api/instrumentation/network/HttpResponseInfo;)V + public final fun stopResource (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;Lcom/datadog/android/api/instrumentation/network/HttpResponseInfo;Ljava/util/Map;)V + public static synthetic fun stopResource$default (Lcom/datadog/android/rum/internal/net/RumResourceInstrumentation;Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;Lcom/datadog/android/api/instrumentation/network/HttpResponseInfo;Ljava/util/Map;ILjava/lang/Object;)V public final fun stopResourceWithError (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;Ljava/lang/Throwable;)V } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/_RumInternalProxy.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/_RumInternalProxy.kt index 9930b51a3f..789eb69e24 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/_RumInternalProxy.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/_RumInternalProxy.kt @@ -11,8 +11,10 @@ import android.content.Intent import com.datadog.android.event.EventMapper import com.datadog.android.lint.InternalApi import com.datadog.android.rum.RumConfiguration.Builder +import com.datadog.android.rum.configuration.RumResourceInstrumentationConfiguration import com.datadog.android.rum.internal.instrumentation.insights.InsightsCollector import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor +import com.datadog.android.rum.internal.net.RumResourceInstrumentation import com.datadog.android.rum.tracking.ActionTrackingStrategy import com.datadog.android.telemetry.model.TelemetryConfigurationEvent @@ -130,5 +132,10 @@ class _RumInternalProxy internal constructor(private val rumMonitor: AdvancedRum ): Builder { return builder.setInsightsCollector(insightsCollector) } + + fun createRumResourceInstrumentation( + name: String, + configuration: RumResourceInstrumentationConfiguration + ): RumResourceInstrumentation = configuration.createInstrumentation(name) } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/configuration/RumResourceInstrumentationConfiguration.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/configuration/RumResourceInstrumentationConfiguration.kt new file mode 100644 index 0000000000..7ded7ea16d --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/configuration/RumResourceInstrumentationConfiguration.kt @@ -0,0 +1,45 @@ +/* + * 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.configuration + +import com.datadog.android.rum.NoOpRumResourceAttributesProvider +import com.datadog.android.rum.RumResourceAttributesProvider +import com.datadog.android.rum.internal.net.RumResourceInstrumentation + +/** + * Configuration that allows to configure RUM resource tracking for network requests. + */ +class RumResourceInstrumentationConfiguration { + private var sdkInstanceName: String? = null + private var resourceAttributesProvider: RumResourceAttributesProvider = NoOpRumResourceAttributesProvider() + + /** + * Set the SDK instance name to bind to, the default value is null. + * @param sdkInstanceName SDK instance name to bind to, the default value is null. + * Instrumentation won't be working until SDK instance is ready. + */ + fun setSdkInstanceName(sdkInstanceName: String) = apply { + this.sdkInstanceName = sdkInstanceName + } + + /** + * Sets the [RumResourceAttributesProvider] to use to provide custom attributes to the RUM. + * By default it won't attach any custom attributes. + * @param rumResourceAttributesProvider the [RumResourceAttributesProvider] to use. + */ + fun setRumResourceAttributesProvider( + rumResourceAttributesProvider: RumResourceAttributesProvider + ) = apply { + this.resourceAttributesProvider = rumResourceAttributesProvider + } + + internal fun createInstrumentation(instrumentationName: String) = RumResourceInstrumentation( + sdkInstanceName, + instrumentationName, + resourceAttributesProvider + ) +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt index b83046ff8e..bc529f024a 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt @@ -15,6 +15,7 @@ import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.feature.EventWriteScope import com.datadog.android.api.feature.Feature import com.datadog.android.api.feature.measureMethodCallPerf +import com.datadog.android.api.logToUser import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.feature.event.ThreadDump @@ -962,15 +963,6 @@ internal class DatadogRumMonitor( internal const val FO_ERROR_INVALID_OPERATION_KEY = "Feature operation key cannot be an empty or blank string but was \"%s\". Vital event won't be sent." - private fun InternalLogger.logToUser( - level: InternalLogger.Level, - messageProvider: () -> String - ) = log( - level = level, - target = InternalLogger.Target.USER, - messageBuilder = messageProvider - ) - private fun InternalLogger.reportFeatureOperationApiUsage(actionType: ActionType) = logApiUsage { InternalTelemetryEvent.ApiUsage.AddOperationStepVital(actionType) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentation.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentation.kt index 84e4f6b5e1..39addc7acf 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentation.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentation.kt @@ -83,14 +83,19 @@ class RumResourceInstrumentation( * Stops tracking a network resource with a successful response. * @param requestInfo the request information * @param responseInfo the response information + * @param attributes additional attributes to attach to the resource event */ - fun stopResource(requestInfo: HttpRequestInfo, responseInfo: HttpResponseInfo) = ifRumEnabled { sdkCore -> + fun stopResource( + requestInfo: HttpRequestInfo, + responseInfo: HttpResponseInfo, + attributes: Map = emptyMap() + ) = ifRumEnabled { sdkCore -> sdkCore.networkMonitor?.stopResource( buildResourceId(requestInfo, generateUuid = false), responseInfo.statusCode, responseInfo.getBodyLength(), responseInfo.getRumResourceKind(), - rumResourceAttributesProvider.onProvideAttributes(requestInfo, responseInfo, null) + attributes + rumResourceAttributesProvider.onProvideAttributes(requestInfo, responseInfo, null) ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumInternalProxyTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumInternalProxyTest.kt index b2f7ab6308..33ffc06057 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumInternalProxyTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/RumInternalProxyTest.kt @@ -7,6 +7,7 @@ package com.datadog.android.rum import android.app.Activity +import com.datadog.android.rum.configuration.RumResourceInstrumentationConfiguration import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor import com.datadog.android.rum.utils.forge.Configurator import fr.xgouchet.elmyr.Forge @@ -15,6 +16,7 @@ import fr.xgouchet.elmyr.annotation.LongForgery 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.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions @@ -93,4 +95,21 @@ internal class RumInternalProxyTest { // Then verify(mockRumMonitor).enableJankStatsTracking(activity) } + + @Test + fun `M return RumResourceInstrumentation W build() {non-null builder}`( + @StringForgery fakeInstrumentationName: String + ) { + // Given + val builder = RumResourceInstrumentationConfiguration() + + // When + val result = with(_RumInternalProxy.Companion) { + builder.createInstrumentation(fakeInstrumentationName) + } + + // Then + assertThat(result).isNotNull() + assertThat(result.networkInstrumentationName).isEqualTo(fakeInstrumentationName) + } } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/net/RumInstrumentationConfigurationTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/net/RumInstrumentationConfigurationTest.kt new file mode 100644 index 0000000000..a8a6396610 --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/net/RumInstrumentationConfigurationTest.kt @@ -0,0 +1,93 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.net + +import com.datadog.android.rum.NoOpRumResourceAttributesProvider +import com.datadog.android.rum.RumResourceAttributesProvider +import com.datadog.android.rum.configuration.RumResourceInstrumentationConfiguration +import com.datadog.android.rum.utils.forge.Configurator +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.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) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class RumInstrumentationConfigurationTest { + + private lateinit var testedBuilder: RumResourceInstrumentationConfiguration + + @Mock + lateinit var mockResourceAttributesProvider: RumResourceAttributesProvider + + @StringForgery + lateinit var fakeInstrumentationName: String + + @BeforeEach + fun `set up`() { + testedBuilder = RumResourceInstrumentationConfiguration() + } + + @Test + fun `M build with default values W createInstrumentation()`() { + // When + val result = testedBuilder.createInstrumentation(fakeInstrumentationName) + + // Then + assertThat(result.sdkInstanceName).isNull() + assertThat(result.networkInstrumentationName).isEqualTo(fakeInstrumentationName) + assertThat(result.rumResourceAttributesProvider).isInstanceOf(NoOpRumResourceAttributesProvider::class.java) + } + + @Test + fun `M set SDK instance name W setSdkInstanceName()`( + @StringForgery fakeSdkInstanceName: String + ) { + // When + val result = testedBuilder.setSdkInstanceName( + fakeSdkInstanceName + ).createInstrumentation(fakeInstrumentationName) + + // Then + assertThat(result.sdkInstanceName).isEqualTo(fakeSdkInstanceName) + } + + @Test + fun `M set resource attributes provider W setRumResourceAttributesProvider()`() { + // When + val result = testedBuilder.setRumResourceAttributesProvider(mockResourceAttributesProvider) + .createInstrumentation(fakeInstrumentationName) + + // Then + assertThat(result.rumResourceAttributesProvider).isSameAs(mockResourceAttributesProvider) + } + + @Test + fun `M return self W chaining builder methods()`( + @StringForgery fakeSdkInstanceName: String + ) { + // When + val result = testedBuilder + .setSdkInstanceName(fakeSdkInstanceName) + .setRumResourceAttributesProvider(mockResourceAttributesProvider) + + // Then + assertThat(result).isSameAs(testedBuilder) + } +} diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentationTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentationTest.kt new file mode 100644 index 0000000000..3af9df4bec --- /dev/null +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentationTest.kt @@ -0,0 +1,660 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.rum.internal.net + +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.instrumentation.network.ExtendedRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.api.instrumentation.network.HttpResponseInfo +import com.datadog.android.api.instrumentation.network.MutableHttpRequestInfo +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.internal.net.HttpSpec +import com.datadog.android.rum.GlobalRumMonitor +import com.datadog.android.rum.RumErrorSource +import com.datadog.android.rum.RumMonitor +import com.datadog.android.rum.RumResourceAttributesProvider +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.internal.monitor.AdvancedNetworkRumMonitor +import com.datadog.android.rum.resource.ResourceId +import com.datadog.android.rum.utils.forge.Configurator +import com.datadog.android.rum.utils.verifyLog +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +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.AfterEach +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.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.Locale +import java.util.UUID + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class RumResourceInstrumentationTest { + + private lateinit var testedInstrumentation: RumResourceInstrumentation + + @Mock + lateinit var mockSdkCore: InternalSdkCore + + @Mock + lateinit var mockRumMonitor: FakeNetworkRumMonitor + + @Mock + lateinit var mockRumResourceAttributesProvider: RumResourceAttributesProvider + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockRumFeature: FeatureScope + + @Mock + lateinit var mockResponseInfo: HttpResponseInfo + + @StringForgery + lateinit var fakeNetworkInstrumentationName: String + + @StringForgery + lateinit var fakeUrl: String + + @StringForgery + lateinit var fakeMethod: String + + private lateinit var fakeRequestInfo: StubHttpRequestInfo + + @BeforeEach + fun `set up`() { + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeature + + datadogRegistryRegisterMethod.invoke(datadogRegistryField.get(null), null, mockSdkCore) + GlobalRumMonitor.registerIfAbsent(mockRumMonitor, mockSdkCore) + + fakeRequestInfo = StubHttpRequestInfo( + url = fakeUrl, + method = fakeMethod, + headers = emptyMap(), + contentType = null, + contentLength = null, + tags = mutableMapOf() + ) + + testedInstrumentation = RumResourceInstrumentation( + sdkInstanceName = null, + networkInstrumentationName = fakeNetworkInstrumentationName, + rumResourceAttributesProvider = mockRumResourceAttributesProvider + ) + } + + @AfterEach + fun `tear down`() { + GlobalRumMonitor.clear() + datadogRegistryClearMethod.invoke(datadogRegistryField.get(null)) + } + + @Test + fun `M call waitForResourceTiming W sendWaitForResourceTimingEvent()`() { + // When + testedInstrumentation.sendWaitForResourceTimingEvent(fakeRequestInfo) + + // Then + argumentCaptor { + verify(mockRumMonitor).waitForResourceTiming(capture()) + assertThat(firstValue.key).isEqualTo("$fakeMethod•$fakeUrl") + assertThat(firstValue.uuid).isNotNull() + } + } + + @Test + fun `M generate new UUID W sendWaitForResourceTimingEvent()`() { + // When + testedInstrumentation.sendWaitForResourceTimingEvent(fakeRequestInfo) + + // Then + argumentCaptor { + verify(mockRumMonitor).waitForResourceTiming(capture()) + assertThat(firstValue.uuid).isNotNull() + } + } + + @Test + fun `M call addResourceTiming W sendTiming()`( + @Forgery fakeResourceTiming: ResourceTiming + ) { + // When + testedInstrumentation.sendTiming(fakeRequestInfo, fakeResourceTiming) + + // Then + argumentCaptor { + verify(mockRumMonitor).addResourceTiming(capture(), eq(fakeResourceTiming)) + assertThat(firstValue.key).isEqualTo("$fakeMethod•$fakeUrl") + } + } + + @Test + fun `M not generate new UUID W sendTiming()`( + @Forgery fakeResourceTiming: ResourceTiming + ) { + // When + testedInstrumentation.sendTiming(fakeRequestInfo, fakeResourceTiming) + + // Then + argumentCaptor { + verify(mockRumMonitor).addResourceTiming(capture(), eq(fakeResourceTiming)) + assertThat(firstValue.uuid).isNull() + } + } + + @Test + fun `M use existing UUID from request tag W sendTiming()`( + @Forgery fakeResourceTiming: ResourceTiming, + @Forgery fakeUuid: UUID + ) { + // Given + fakeRequestInfo = fakeRequestInfo.copy(tags = mutableMapOf(UUID::class.java to fakeUuid)) + + // When + testedInstrumentation.sendTiming(fakeRequestInfo, fakeResourceTiming) + + // Then + argumentCaptor { + verify(mockRumMonitor).addResourceTiming(capture(), eq(fakeResourceTiming)) + assertThat(firstValue.uuid).isEqualTo(fakeUuid.toString()) + } + } + + @ParameterizedTest + @MethodSource("httpMethodsToRumMethods") + fun `M call startResource with correct method W startResource()`( + httpMethod: String, + expectedRumMethod: RumResourceMethod + ) { + // Given + fakeRequestInfo = fakeRequestInfo.copy(method = httpMethod) + + // When + testedInstrumentation.startResource(fakeRequestInfo) + + // Then + argumentCaptor { + verify(mockRumMonitor).startResource( + capture(), + eq(expectedRumMethod), + eq(fakeUrl), + eq(emptyMap()) + ) + assertThat(firstValue.key).isEqualTo("$httpMethod•$fakeUrl") + assertThat(firstValue.uuid).isNotNull() + } + } + + @Test + fun `M log warning and use GET W startResource() {unknown HTTP method}`( + @StringForgery fakeUnknownMethod: String + ) { + // Given + fakeRequestInfo = fakeRequestInfo.copy(method = fakeUnknownMethod) + + // When + testedInstrumentation.startResource(fakeRequestInfo) + + // Then + verify(mockRumMonitor).startResource( + any(), + eq(RumResourceMethod.GET), + eq(fakeUrl), + eq(emptyMap()) + ) + + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.USER, InternalLogger.Target.TELEMETRY), + RumResourceInstrumentation.UNSUPPORTED_HTTP_METHOD.format( + Locale.US, + fakeUnknownMethod, + fakeNetworkInstrumentationName + ) + ) + } + + @Test + fun `M call stopResource W stopResource()`( + @IntForgery(min = 200, max = 600) fakeStatusCode: Int, + @LongForgery(min = 0) fakeContentLength: Long, + @StringForgery fakeMimeType: String + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + whenever(mockResponseInfo.contentLength) doReturn fakeContentLength + whenever(mockResponseInfo.contentType) doReturn fakeMimeType + whenever(mockResponseInfo.headers) doReturn emptyMap() + whenever( + mockRumResourceAttributesProvider.onProvideAttributes( + any(), + anyOrNull(), + anyOrNull() + ) + ) doReturn emptyMap() + + // When + testedInstrumentation.stopResource(fakeRequestInfo, mockResponseInfo) + + // Then + argumentCaptor { + verify(mockRumMonitor).stopResource( + capture(), + eq(fakeStatusCode), + eq(fakeContentLength), + eq(RumResourceKind.fromMimeType(fakeMimeType)), + any>() + ) + assertThat(firstValue.key).isEqualTo("$fakeMethod•$fakeUrl") + } + } + + @Test + fun `M merge attributes W stopResource()`( + @IntForgery(min = 200, max = 600) fakeStatusCode: Int, + @LongForgery(min = 0) fakeContentLength: Long, + forge: Forge + ) { + // Given + val fakePassedAttributes = forge.aMap { forge.aString() to forge.aString() } + val fakeProviderAttributes = forge.aMap { forge.aString() to forge.aString() } + + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + whenever(mockResponseInfo.contentLength) doReturn fakeContentLength + whenever(mockResponseInfo.contentType) doReturn null + whenever(mockResponseInfo.headers) doReturn emptyMap() + whenever( + mockRumResourceAttributesProvider.onProvideAttributes( + any(), + anyOrNull(), + anyOrNull() + ) + ) doReturn fakeProviderAttributes + + // When + testedInstrumentation.stopResource(fakeRequestInfo, mockResponseInfo, fakePassedAttributes) + + // Then + argumentCaptor> { + verify(mockRumMonitor).stopResource( + any(), + anyOrNull(), + anyOrNull(), + any(), + capture() + ) + assertThat(firstValue).containsAllEntriesOf(fakePassedAttributes) + assertThat(firstValue).containsAllEntriesOf(fakeProviderAttributes) + } + } + + @Test + fun `M return null body length for stream W stopResource() {streaming content type}`( + @IntForgery(min = 200, max = 600) fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + whenever(mockResponseInfo.contentLength) doReturn 1000L + whenever(mockResponseInfo.contentType) doReturn HttpSpec.ContentType.TEXT_EVENT_STREAM + whenever(mockResponseInfo.headers) doReturn emptyMap() + whenever( + mockRumResourceAttributesProvider.onProvideAttributes( + any(), + anyOrNull(), + anyOrNull() + ) + ) doReturn emptyMap() + + // When + testedInstrumentation.stopResource(fakeRequestInfo, mockResponseInfo) + + // Then + verify(mockRumMonitor).stopResource( + any(), + eq(fakeStatusCode), + eq(null), + any(), + any>() + ) + } + + @Test + fun `M return null body length W stopResource() {WebSocket}`( + @IntForgery(min = 200, max = 600) fakeStatusCode: Int, + @StringForgery fakeWebSocketAccept: String + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + whenever(mockResponseInfo.contentLength) doReturn 1000L + whenever(mockResponseInfo.contentType) doReturn null + whenever(mockResponseInfo.headers) doReturn mapOf( + HttpSpec.Headers.WEBSOCKET_ACCEPT_HEADER to listOf(fakeWebSocketAccept) + ) + whenever( + mockRumResourceAttributesProvider.onProvideAttributes( + any(), + anyOrNull(), + anyOrNull() + ) + ) doReturn emptyMap() + + // When + testedInstrumentation.stopResource(fakeRequestInfo, mockResponseInfo) + + // Then + verify(mockRumMonitor).stopResource( + any(), + eq(fakeStatusCode), + eq(null), + any(), + any>() + ) + } + + @Test + fun `M return RumResourceKind NATIVE W stopResource() {null content type}`( + @IntForgery(min = 200, max = 600) fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + whenever(mockResponseInfo.contentLength) doReturn null + whenever(mockResponseInfo.contentType) doReturn null + whenever(mockResponseInfo.headers) doReturn emptyMap() + whenever( + mockRumResourceAttributesProvider.onProvideAttributes( + any(), + anyOrNull(), + anyOrNull() + ) + ) doReturn emptyMap() + + // When + testedInstrumentation.stopResource(fakeRequestInfo, mockResponseInfo) + + // Then + verify(mockRumMonitor).stopResource( + any(), + anyOrNull(), + anyOrNull(), + eq(RumResourceKind.NATIVE), + any>() + ) + } + + @Test + fun `M call stopResourceWithError W stopResourceWithError()`( + @Forgery fakeThrowable: Throwable + ) { + // Given + whenever( + mockRumResourceAttributesProvider.onProvideAttributes( + any(), + anyOrNull(), + anyOrNull() + ) + ) doReturn emptyMap() + + // When + testedInstrumentation.stopResourceWithError(fakeRequestInfo, fakeThrowable) + + // Then + val expectedMessage = RumResourceInstrumentation.ERROR_MSG_FORMAT.format( + Locale.US, + fakeNetworkInstrumentationName, + fakeMethod, + fakeUrl + ) + + argumentCaptor { + verify(mockRumMonitor).stopResourceWithError( + capture(), + eq(null), + eq(expectedMessage), + eq(RumErrorSource.NETWORK), + eq(fakeThrowable), + any>() + ) + assertThat(firstValue.key).isEqualTo("$fakeMethod•$fakeUrl") + } + } + + @Test + fun `M pass provider attributes W stopResourceWithError()`( + @Forgery fakeThrowable: Throwable, + forge: Forge + ) { + // Given + val fakeProviderAttributes = forge.aMap { forge.aString() to forge.aString() } + whenever( + mockRumResourceAttributesProvider.onProvideAttributes( + any(), + anyOrNull(), + anyOrNull() + ) + ) doReturn fakeProviderAttributes + + // When + testedInstrumentation.stopResourceWithError(fakeRequestInfo, fakeThrowable) + + // Then + argumentCaptor> { + verify(mockRumMonitor).stopResourceWithError( + any(), + anyOrNull(), + any(), + any(), + any(), + capture() + ) + assertThat(firstValue).containsAllEntriesOf(fakeProviderAttributes) + } + } + + @Test + fun `M log warning W reportInstrumentationError()`( + @StringForgery fakeErrorMessage: String + ) { + // When + testedInstrumentation.reportInstrumentationError(fakeErrorMessage) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + "Unable to instrument RUM resource: $fakeErrorMessage" + ) + } + + @Test + fun `M generate UUID W buildResourceId() {generateUuid = true}`() { + // When + val resourceId = RumResourceInstrumentation.buildResourceId(fakeRequestInfo, generateUuid = true) + + // Then + assertThat(resourceId.key).isEqualTo("$fakeMethod•$fakeUrl") + assertThat(resourceId.uuid).isNotNull() + } + + @Test + fun `M not generate UUID W buildResourceId() {generateUuid = false}`() { + // When + val resourceId = RumResourceInstrumentation.buildResourceId(fakeRequestInfo, generateUuid = false) + + // Then + assertThat(resourceId.key).isEqualTo("$fakeMethod•$fakeUrl") + assertThat(resourceId.uuid).isNull() + } + + @Test + fun `M use existing UUID from request tag W buildResourceId()`( + @Forgery fakeUuid: UUID + ) { + // Given + fakeRequestInfo = fakeRequestInfo.copy(tags = mutableMapOf(UUID::class.java to fakeUuid)) + + // When + val resourceId = RumResourceInstrumentation.buildResourceId(fakeRequestInfo, generateUuid = false) + + // Then + assertThat(resourceId.uuid).isEqualTo(fakeUuid.toString()) + } + + @Test + fun `M include content length and type in key W buildResourceId() {non-empty body}`( + @LongForgery(min = 1) fakeContentLength: Long, + @StringForgery fakeContentType: String + ) { + // Given + fakeRequestInfo = fakeRequestInfo.copy(contentLength = fakeContentLength, contentType = fakeContentType) + + // When + val resourceId = RumResourceInstrumentation.buildResourceId(fakeRequestInfo, generateUuid = false) + + // Then + assertThat(resourceId.key).isEqualTo("$fakeMethod•$fakeUrl•$fakeContentLength•$fakeContentType") + } + + @Test + fun `M include content type in key W buildResourceId() {with content type only}`( + @StringForgery fakeContentType: String + ) { + // Given + fakeRequestInfo = fakeRequestInfo.copy(contentLength = 0L, contentType = fakeContentType) + + // When + val resourceId = RumResourceInstrumentation.buildResourceId(fakeRequestInfo, generateUuid = false) + + // Then + assertThat(resourceId.key).isEqualTo("$fakeMethod•$fakeUrl•0•$fakeContentType") + } + + @Test + fun `M log info W startResource() {RUM disabled}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + + val testedInstrumentationNoRum = RumResourceInstrumentation( + sdkInstanceName = null, + networkInstrumentationName = fakeNetworkInstrumentationName, + rumResourceAttributesProvider = mockRumResourceAttributesProvider + ) + + // When + testedInstrumentationNoRum.startResource(fakeRequestInfo) + + // Then + mockInternalLogger.verifyLog( + InternalLogger.Level.INFO, + InternalLogger.Target.USER, + RumResourceInstrumentation.WARN_RUM_DISABLED.format( + Locale.US, + fakeNetworkInstrumentationName, + "Default SDK instance" + ) + ) + } + + companion object { + private val datadogRegistryField = Datadog::class.java.getDeclaredField("registry").apply { + isAccessible = true + } + private val datadogRegistryRegisterMethod = datadogRegistryField.type.getMethod( + "register", + String::class.java, + SdkCore::class.java + ) + private val datadogRegistryClearMethod = datadogRegistryField.type.getMethod("clear") + + @JvmStatic + fun httpMethodsToRumMethods() = listOf( + Arguments.of(HttpSpec.Method.GET, RumResourceMethod.GET), + Arguments.of(HttpSpec.Method.POST, RumResourceMethod.POST), + Arguments.of(HttpSpec.Method.PUT, RumResourceMethod.PUT), + Arguments.of(HttpSpec.Method.DELETE, RumResourceMethod.DELETE), + Arguments.of(HttpSpec.Method.PATCH, RumResourceMethod.PATCH), + Arguments.of(HttpSpec.Method.HEAD, RumResourceMethod.HEAD), + Arguments.of(HttpSpec.Method.OPTIONS, RumResourceMethod.OPTIONS), + Arguments.of(HttpSpec.Method.TRACE, RumResourceMethod.TRACE), + Arguments.of(HttpSpec.Method.CONNECT, RumResourceMethod.CONNECT) + ) + } + + private data class StubHttpRequestInfo( + override val url: String, + override val method: String, + override val headers: Map>, + override val contentType: String?, + val contentLength: Long?, + val tags: MutableMap + ) : HttpRequestInfo, ExtendedRequestInfo, MutableHttpRequestInfo { + @Suppress("UNCHECKED_CAST") + override fun tag(type: Class): T? = tags[type] as? T + + override fun contentLength(): Long? = contentLength + + override fun newBuilder(): HttpRequestInfoBuilder = StubHttpRequestInfoBuilder(this.copy()) + } + + private data class StubHttpRequestInfoBuilder( + private var request: StubHttpRequestInfo + ) : HttpRequestInfoBuilder { + override fun setUrl(url: String) = apply { request = request.copy(url = url) } + + override fun addHeader(key: String, vararg values: String) = apply { + request = request.copy( + headers = request.headers.toMutableMap().also { it[key] = values.asList() } + ) + } + + override fun removeHeader(key: String) = apply { + request = request.copy(headers = request.headers.toMutableMap().also { it.remove(key) }) + } + + override fun addTag(type: Class, tag: T?) = apply { + request = request.copy(tags = request.tags.toMutableMap().also { it[type] = tag }) + } + + override fun build(): HttpRequestInfo = request.copy() + } + + internal interface FakeNetworkRumMonitor : RumMonitor, AdvancedNetworkRumMonitor +} diff --git a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentationAssert.kt b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentationAssert.kt index 9b6c155ee3..fa387e8029 100644 --- a/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentationAssert.kt +++ b/features/dd-sdk-android-rum/src/testFixtures/kotlin/com/datadog/android/rum/internal/net/RumResourceInstrumentationAssert.kt @@ -28,7 +28,7 @@ class RumResourceInstrumentationAssert private constructor(actual: RumResourceIn Assertions.assertThat(actual.rumResourceAttributesProvider).isEqualTo(expected) } - companion object Companion { + companion object { fun assertThat(instrumentation: RumResourceInstrumentation): RumResourceInstrumentationAssert { return RumResourceInstrumentationAssert(instrumentation) } diff --git a/features/dd-sdk-android-trace-api/api/apiSurface b/features/dd-sdk-android-trace-api/api/apiSurface index eed9ae753c..d38ca2d430 100644 --- a/features/dd-sdk-android-trace-api/api/apiSurface +++ b/features/dd-sdk-android-trace-api/api/apiSurface @@ -44,6 +44,9 @@ object com.datadog.android.trace.api.DatadogTracingConstants const val UNSET: Byte const val HTTP_SERVER_DECORATOR: Byte const val DEFAULT: Byte +interface com.datadog.android.trace.api.propagation.DatadogHttpHeadersPropagation + fun handleSampledHeaders(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder, com.datadog.android.trace.api.tracer.DatadogTracer, com.datadog.android.trace.api.span.DatadogSpan): com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder + fun handleNotSampledHeaders(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder, com.datadog.android.trace.api.tracer.DatadogTracer, com.datadog.android.trace.api.span.DatadogSpan): com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder interface com.datadog.android.trace.api.propagation.DatadogPropagation fun inject(com.datadog.android.trace.api.span.DatadogSpanContext, C, (C) -> Unit) fun extract(C, (C) -> Unit): com.datadog.android.trace.api.span.DatadogSpanContext? diff --git a/features/dd-sdk-android-trace-api/api/dd-sdk-android-trace-api.api b/features/dd-sdk-android-trace-api/api/dd-sdk-android-trace-api.api index 501fd66e96..9a44cc1af2 100644 --- a/features/dd-sdk-android-trace-api/api/dd-sdk-android-trace-api.api +++ b/features/dd-sdk-android-trace-api/api/dd-sdk-android-trace-api.api @@ -62,6 +62,11 @@ public final class com/datadog/android/trace/api/DatadogTracingConstants$TracerC public static final field URL_AS_RESOURCE_NAME Ljava/lang/String; } +public abstract interface class com/datadog/android/trace/api/propagation/DatadogHttpHeadersPropagation { + public abstract fun handleNotSampledHeaders (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;Lcom/datadog/android/trace/api/tracer/DatadogTracer;Lcom/datadog/android/trace/api/span/DatadogSpan;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public abstract fun handleSampledHeaders (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;Lcom/datadog/android/trace/api/tracer/DatadogTracer;Lcom/datadog/android/trace/api/span/DatadogSpan;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; +} + public abstract interface class com/datadog/android/trace/api/propagation/DatadogPropagation { public abstract fun extract (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Lcom/datadog/android/trace/api/span/DatadogSpanContext; public abstract fun inject (Lcom/datadog/android/trace/api/span/DatadogSpanContext;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;)V diff --git a/features/dd-sdk-android-trace-api/src/main/kotlin/com/datadog/android/trace/api/propagation/DatadogHttpHeadersPropagation.kt b/features/dd-sdk-android-trace-api/src/main/kotlin/com/datadog/android/trace/api/propagation/DatadogHttpHeadersPropagation.kt new file mode 100644 index 0000000000..642762bb3d --- /dev/null +++ b/features/dd-sdk-android-trace-api/src/main/kotlin/com/datadog/android/trace/api/propagation/DatadogHttpHeadersPropagation.kt @@ -0,0 +1,50 @@ +/* + * 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.trace.api.propagation + +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.tracer.DatadogTracer + +/** + * Interface for handling trace context propagation via HTTP headers. + * Implementations of this interface are responsible for injecting trace context + * headers into HTTP requests based on the sampling decision. + */ +interface DatadogHttpHeadersPropagation { + + /** + * Handles HTTP header injection for sampled requests. + * This method is called when a request has been sampled and trace context headers + * should be injected to propagate the trace to downstream services. + * + * @param requestInfoBuilder the modifier to use for adding headers to the request. + * @param tracer the tracer instance used for context propagation. + * @param span the span associated with this request. + * @return the modified request info builder with headers injected. + */ + fun handleSampledHeaders( + requestInfoBuilder: HttpRequestInfoBuilder, + tracer: DatadogTracer, + span: DatadogSpan + ): HttpRequestInfoBuilder + + /** + * Handles HTTP header injection for non-sampled requests. + * This method is called when a request has not been sampled. Depending on the + * trace context injection strategy, headers may or may not be injected. + * + * @param requestInfoBuilder the modifier to use for adding headers to the request. + * @param tracer the tracer instance used for context propagation. + * @param span the span associated with this request. + * @return the modified request info builder with headers injected (if applicable). + */ + fun handleNotSampledHeaders( + requestInfoBuilder: HttpRequestInfoBuilder, + tracer: DatadogTracer, + span: DatadogSpan + ): HttpRequestInfoBuilder +} diff --git a/features/dd-sdk-android-trace-internal/api/dd-sdk-android-trace-internal.api b/features/dd-sdk-android-trace-internal/api/dd-sdk-android-trace-internal.api index c21e408872..9820102496 100644 --- a/features/dd-sdk-android-trace-internal/api/dd-sdk-android-trace-internal.api +++ b/features/dd-sdk-android-trace-internal/api/dd-sdk-android-trace-internal.api @@ -3056,6 +3056,18 @@ public abstract class com/datadog/trace/core/monitor/HealthMetrics : java/lang/A public fun summary ()Ljava/lang/String; } +public class com/datadog/trace/core/propagation/B3HttpCodec { + public static final field B3_KEY Ljava/lang/String; + public static final field SAMPLING_PRIORITY_KEY Ljava/lang/String; + public static final field SPAN_ID_KEY Ljava/lang/String; + public static final field TRACE_ID_KEY Ljava/lang/String; + public static fun newCombinedInjector (Z)Lcom/datadog/trace/core/propagation/HttpCodec$Injector; + public static fun newMultiExtractor (Lcom/datadog/trace/api/Config;Lcom/datadog/android/trace/internal/compat/function/Supplier;)Lcom/datadog/trace/core/propagation/HttpCodec$Extractor; + public static fun newMultiInjector (Z)Lcom/datadog/trace/core/propagation/HttpCodec$Injector; + public static fun newSingleExtractor (Lcom/datadog/trace/api/Config;Lcom/datadog/android/trace/internal/compat/function/Supplier;)Lcom/datadog/trace/core/propagation/HttpCodec$Extractor; + public static fun newSingleInjector (Z)Lcom/datadog/trace/core/propagation/HttpCodec$Injector; +} + public class com/datadog/trace/core/propagation/B3TraceId : com/datadog/trace/api/DDTraceId { protected final field delegate Lcom/datadog/trace/api/DDTraceId; protected final field original Ljava/lang/String; @@ -3124,6 +3136,16 @@ public class com/datadog/trace/core/propagation/CorePropagation : com/datadog/tr public fun inject (Lcom/datadog/trace/bootstrap/instrumentation/api/AgentSpan;Ljava/lang/Object;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentPropagation$Setter;Lcom/datadog/trace/api/TracePropagationStyle;)V } +public class com/datadog/trace/core/propagation/DatadogHttpCodec { + public static final field DATADOG_TAGS_KEY Ljava/lang/String; + public static final field ORIGIN_KEY Ljava/lang/String; + public static final field SAMPLING_PRIORITY_KEY Ljava/lang/String; + public static final field SPAN_ID_KEY Ljava/lang/String; + public static final field TRACE_ID_KEY Ljava/lang/String; + public static fun newExtractor (Lcom/datadog/trace/api/Config;Lcom/datadog/android/trace/internal/compat/function/Supplier;)Lcom/datadog/trace/core/propagation/HttpCodec$Extractor; + public static fun newInjector (Ljava/util/Map;)Lcom/datadog/trace/core/propagation/HttpCodec$Injector; +} + public class com/datadog/trace/core/propagation/ExtractedContext : com/datadog/trace/bootstrap/instrumentation/api/TagContext { public fun (Lcom/datadog/trace/api/DDTraceId;JILjava/lang/CharSequence;JLjava/util/Map;Ljava/util/Map;Lcom/datadog/trace/bootstrap/instrumentation/api/TagContext$HttpHeaders;Lcom/datadog/trace/core/propagation/PropagationTags;Lcom/datadog/trace/api/TraceConfig;Lcom/datadog/trace/api/TracePropagationStyle;)V public fun (Lcom/datadog/trace/api/DDTraceId;JILjava/lang/CharSequence;Lcom/datadog/trace/core/propagation/PropagationTags;Lcom/datadog/trace/api/TracePropagationStyle;)V @@ -3215,6 +3237,14 @@ public class com/datadog/trace/core/propagation/TagContextExtractor : com/datado public fun extract (Ljava/lang/Object;Lcom/datadog/trace/bootstrap/instrumentation/api/AgentPropagation$ContextVisitor;)Lcom/datadog/trace/bootstrap/instrumentation/api/TagContext; } +public class com/datadog/trace/core/propagation/W3CHttpCodec { + public static final field BAGGAGE_KEY Ljava/lang/String; + public static final field TRACE_PARENT_KEY Ljava/lang/String; + public static final field TRACE_STATE_KEY Ljava/lang/String; + public static fun newExtractor (Lcom/datadog/trace/api/Config;Lcom/datadog/android/trace/internal/compat/function/Supplier;)Lcom/datadog/trace/core/propagation/HttpCodec$Extractor; + public static fun newInjector (Ljava/util/Map;)Lcom/datadog/trace/core/propagation/HttpCodec$Injector; +} + public class com/datadog/trace/core/propagation/ptags/PTagsFactory : com/datadog/trace/core/propagation/PropagationTags$Factory { public fun (I)V public final fun empty ()Lcom/datadog/trace/core/propagation/PropagationTags; diff --git a/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/B3HttpCodec.java b/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/B3HttpCodec.java index 64c7d0376a..d955615500 100644 --- a/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/B3HttpCodec.java +++ b/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/B3HttpCodec.java @@ -23,17 +23,17 @@ import com.datadog.android.trace.internal.compat.function.Supplier; /** A codec designed for HTTP transport via headers using B3 headers */ -class B3HttpCodec { +public class B3HttpCodec { private static final Logger log = LoggerFactory.getLogger(B3HttpCodec.class); private static final String B3_TRACE_ID = "b3.traceid"; private static final String B3_SPAN_ID = "b3.spanid"; - static final String TRACE_ID_KEY = "X-B3-TraceId"; - static final String SPAN_ID_KEY = "X-B3-SpanId"; - private static final String SAMPLING_PRIORITY_KEY = "X-B3-Sampled"; + public static final String TRACE_ID_KEY = "X-B3-TraceId"; + public static final String SPAN_ID_KEY = "X-B3-SpanId"; + public static final String SAMPLING_PRIORITY_KEY = "X-B3-Sampled"; // See https://github.com/openzipkin/b3-propagation#single-header for b3 header documentation - private static final String B3_KEY = "b3"; + public static final String B3_KEY = "b3"; private static final String SAMPLING_PRIORITY_ACCEPT = String.valueOf(1); private static final String SAMPLING_PRIORITY_DROP = String.valueOf(0); diff --git a/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/DatadogHttpCodec.java b/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/DatadogHttpCodec.java index e73ed27618..dc013586dc 100644 --- a/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/DatadogHttpCodec.java +++ b/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/DatadogHttpCodec.java @@ -26,16 +26,16 @@ import java.util.TreeMap; /** A codec designed for HTTP transport via headers using Datadog headers */ -class DatadogHttpCodec { +public class DatadogHttpCodec { private static final Logger log = LoggerFactory.getLogger(DatadogHttpCodec.class); static final String OT_BAGGAGE_PREFIX = "ot-baggage-"; - static final String TRACE_ID_KEY = "x-datadog-trace-id"; - static final String SPAN_ID_KEY = "x-datadog-parent-id"; - static final String SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority"; - static final String ORIGIN_KEY = "x-datadog-origin"; + public static final String TRACE_ID_KEY = "x-datadog-trace-id"; + public static final String SPAN_ID_KEY = "x-datadog-parent-id"; + public static final String SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority"; + public static final String ORIGIN_KEY = "x-datadog-origin"; private static final String E2E_START_KEY = OT_BAGGAGE_PREFIX + DDTags.TRACE_START_TIME; - static final String DATADOG_TAGS_KEY = "x-datadog-tags"; + public static final String DATADOG_TAGS_KEY = "x-datadog-tags"; private DatadogHttpCodec() { // This class should not be created. This also makes code coverage checks happy. diff --git a/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/W3CHttpCodec.java b/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/W3CHttpCodec.java index b4b9b9c1f3..a3c56bdbc5 100644 --- a/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/W3CHttpCodec.java +++ b/features/dd-sdk-android-trace-internal/src/main/java/com/datadog/trace/core/propagation/W3CHttpCodec.java @@ -31,9 +31,8 @@ /** * A codec designed for HTTP transport via headers using W3C traceparent and tracestate headers */ -class W3CHttpCodec { +public class W3CHttpCodec { private static final Logger log = LoggerFactory.getLogger(W3CHttpCodec.class); - public static final String TRACE_PARENT_KEY = "traceparent"; public static final String TRACE_STATE_KEY = "tracestate"; public static final String BAGGAGE_KEY = "baggage"; diff --git a/features/dd-sdk-android-trace/api/apiSurface b/features/dd-sdk-android-trace/api/apiSurface index 6cf16b399c..4d328a2cde 100644 --- a/features/dd-sdk-android-trace/api/apiSurface +++ b/features/dd-sdk-android-trace/api/apiSurface @@ -1,5 +1,22 @@ +class com.datadog.android.trace.ApmNetworkInstrumentationConfiguration + constructor(List) + fun setTraceOrigin(String) + fun setSdkInstanceName(String) + fun setTracedRequestListener(NetworkTracedRequestListener) + fun setTraceSampleRate(Float) + fun setTraceSampler(com.datadog.android.core.sampling.Sampler) + fun setTraceContextInjection(TraceContextInjection) + fun set404ResourcesRedacted(Boolean) + fun setTracingScope(ApmNetworkTracingScope) +enum com.datadog.android.trace.ApmNetworkTracingScope + - DETAILED + - APPLICATION_LEVEL_REQUESTS_ONLY object com.datadog.android.trace.DatadogTracing fun newTracerBuilder(com.datadog.android.api.SdkCore = Datadog.getInstance()): com.datadog.android.trace.api.tracer.DatadogTracerBuilder +open class com.datadog.android.trace.DeterministicTraceSampler : com.datadog.android.core.sampling.DeterministicSampler + constructor(() -> Float) + constructor(Float) + constructor(Double) object com.datadog.android.trace.GlobalDatadogTracer fun registerIfAbsent(com.datadog.android.trace.api.tracer.DatadogTracer): Boolean fun get(): com.datadog.android.trace.api.tracer.DatadogTracer @@ -7,6 +24,8 @@ object com.datadog.android.trace.GlobalDatadogTracer fun clear() interface com.datadog.android.trace.InternalCoreWriterProvider fun getCoreTracerWriter(): com.datadog.android.trace.api.span.DatadogSpanWriter +interface com.datadog.android.trace.NetworkTracedRequestListener + fun onRequestIntercepted(com.datadog.android.api.instrumentation.network.HttpRequestInfo, com.datadog.android.trace.api.span.DatadogSpan, com.datadog.android.api.instrumentation.network.HttpResponseInfo?, Throwable?) fun withinSpan(String, com.datadog.android.trace.api.span.DatadogSpan? = null, Boolean = true, com.datadog.android.trace.api.span.DatadogSpan.() -> T): T object com.datadog.android.trace.Trace fun enable(TraceConfiguration, com.datadog.android.api.SdkCore = Datadog.getInstance()) @@ -16,11 +35,24 @@ data class com.datadog.android.trace.TraceConfiguration fun setEventMapper(com.datadog.android.trace.event.SpanEventMapper): Builder fun setNetworkInfoEnabled(Boolean): Builder fun build(): TraceConfiguration +enum com.datadog.android.trace.TraceContextInjection + - ALL + - SAMPLED interface com.datadog.android.trace.event.SpanEventMapper : com.datadog.android.event.EventMapper override fun map(com.datadog.android.trace.model.SpanEvent): com.datadog.android.trace.model.SpanEvent +class com.datadog.android.trace.internal.ApmNetworkInstrumentation + fun onRequest(com.datadog.android.api.instrumentation.network.HttpRequestInfo): com.datadog.android.trace.internal.net.RequestTraceState? + fun onResponseSucceeded(com.datadog.android.trace.internal.net.RequestTraceState, com.datadog.android.api.instrumentation.network.HttpResponseInfo) + fun onResponseFailed(com.datadog.android.trace.internal.net.RequestTraceState, Throwable) class com.datadog.android.trace.internal.DatadogPropagationHelper fun isExtractedContext(com.datadog.android.trace.api.span.DatadogSpanContext): Boolean + fun setTraceContext(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder, String, String, Int) + fun extractParentContext(com.datadog.android.trace.api.tracer.DatadogTracer, com.datadog.android.api.instrumentation.network.HttpRequestInfo): com.datadog.android.trace.api.span.DatadogSpanContext? fun createExtractedContext(String, String, Int): com.datadog.android.trace.api.span.DatadogSpanContext + fun propagateSampledHeaders(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder, com.datadog.android.trace.api.tracer.DatadogTracer, com.datadog.android.trace.api.span.DatadogSpan, Set) + fun propagateNotSampledHeaders(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder, com.datadog.android.trace.api.tracer.DatadogTracer, com.datadog.android.trace.api.span.DatadogSpan, Set, com.datadog.android.trace.TraceContextInjection, String?) + fun extractSamplingDecision(com.datadog.android.api.instrumentation.network.HttpRequestInfo): Boolean? + companion object class com.datadog.android.trace.internal.DatadogSpanIdConverter fun fromHex(String): Long fun toHexStringPadded(Long): String @@ -34,10 +66,16 @@ object com.datadog.android.trace.internal.DatadogTracingToolkit fun addThrowable(com.datadog.android.trace.api.span.DatadogSpan, Throwable, Byte) fun activateSpan(com.datadog.android.trace.api.tracer.DatadogTracer, com.datadog.android.trace.api.span.DatadogSpan, Boolean): com.datadog.android.trace.api.scope.DatadogScope? fun mergeBaggage(String?, String): String + fun createApmNetworkInstrumentation(String, com.datadog.android.trace.ApmNetworkInstrumentationConfiguration): ApmNetworkInstrumentation class com.datadog.android.trace.internal.RumContextPropagator constructor(() -> com.datadog.android.api.feature.FeatureSdkCore?) companion object fun com.datadog.android.trace.api.span.DatadogSpan.extractRumContext(RumContextPropagator, Boolean = false) +data class com.datadog.android.trace.internal.net.RequestTraceState + constructor(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder, Boolean = false, com.datadog.android.trace.api.span.DatadogSpan? = null, Float? = null) + val requestInfo: com.datadog.android.api.instrumentation.network.HttpRequestInfo +data class com.datadog.android.trace.internal.net.TraceContext + constructor(String, String, Int) fun android.database.sqlite.SQLiteDatabase.transactionTraced(String, Boolean = true, com.datadog.android.trace.api.span.DatadogSpan.(android.database.sqlite.SQLiteDatabase) -> T): T data class com.datadog.android.trace.model.SpanEvent constructor(kotlin.String, kotlin.String, kotlin.String, kotlin.String, kotlin.String, kotlin.String, kotlin.Long, kotlin.Long, kotlin.Long = 0L, Metrics, Meta) diff --git a/features/dd-sdk-android-trace/api/dd-sdk-android-trace.api b/features/dd-sdk-android-trace/api/dd-sdk-android-trace.api index 10997e84de..3068b902d4 100644 --- a/features/dd-sdk-android-trace/api/dd-sdk-android-trace.api +++ b/features/dd-sdk-android-trace/api/dd-sdk-android-trace.api @@ -1,9 +1,34 @@ +public final class com/datadog/android/trace/ApmNetworkInstrumentationConfiguration { + public fun (Ljava/util/List;)V + public final fun set404ResourcesRedacted (Z)Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration; + public final fun setSdkInstanceName (Ljava/lang/String;)Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration; + public final fun setTraceContextInjection (Lcom/datadog/android/trace/TraceContextInjection;)Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration; + public final fun setTraceOrigin (Ljava/lang/String;)Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration; + public final fun setTraceSampleRate (F)Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration; + public final fun setTraceSampler (Lcom/datadog/android/core/sampling/Sampler;)Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration; + public final fun setTracedRequestListener (Lcom/datadog/android/trace/NetworkTracedRequestListener;)Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration; + public final fun setTracingScope (Lcom/datadog/android/trace/ApmNetworkTracingScope;)Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration; +} + +public final class com/datadog/android/trace/ApmNetworkTracingScope : java/lang/Enum { + public static final field APPLICATION_LEVEL_REQUESTS_ONLY Lcom/datadog/android/trace/ApmNetworkTracingScope; + public static final field DETAILED Lcom/datadog/android/trace/ApmNetworkTracingScope; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/trace/ApmNetworkTracingScope; + public static fun values ()[Lcom/datadog/android/trace/ApmNetworkTracingScope; +} + public final class com/datadog/android/trace/DatadogTracing { public static final field INSTANCE Lcom/datadog/android/trace/DatadogTracing; public static final fun newTracerBuilder (Lcom/datadog/android/api/SdkCore;)Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder; public static synthetic fun newTracerBuilder$default (Lcom/datadog/android/api/SdkCore;ILjava/lang/Object;)Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder; } +public class com/datadog/android/trace/DeterministicTraceSampler : com/datadog/android/core/sampling/DeterministicSampler { + public fun (D)V + public fun (F)V + public fun (Lkotlin/jvm/functions/Function0;)V +} + public final class com/datadog/android/trace/GlobalDatadogTracer { public static final field INSTANCE Lcom/datadog/android/trace/GlobalDatadogTracer; public final fun clear ()V @@ -16,6 +41,10 @@ public abstract interface class com/datadog/android/trace/InternalCoreWriterProv public abstract fun getCoreTracerWriter ()Lcom/datadog/android/trace/api/span/DatadogSpanWriter; } +public abstract interface class com/datadog/android/trace/NetworkTracedRequestListener { + public abstract fun onRequestIntercepted (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;Lcom/datadog/android/trace/api/span/DatadogSpan;Lcom/datadog/android/api/instrumentation/network/HttpResponseInfo;Ljava/lang/Throwable;)V +} + public final class com/datadog/android/trace/SpanExtKt { public static final fun withinSpan (Ljava/lang/String;Lcom/datadog/android/trace/api/span/DatadogSpan;ZLkotlin/jvm/functions/Function1;)Ljava/lang/Object; public static synthetic fun withinSpan$default (Ljava/lang/String;Lcom/datadog/android/trace/api/span/DatadogSpan;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object; @@ -44,13 +73,35 @@ public final class com/datadog/android/trace/TraceConfiguration$Builder { public final fun useCustomEndpoint (Ljava/lang/String;)Lcom/datadog/android/trace/TraceConfiguration$Builder; } +public final class com/datadog/android/trace/TraceContextInjection : java/lang/Enum { + public static final field ALL Lcom/datadog/android/trace/TraceContextInjection; + public static final field SAMPLED Lcom/datadog/android/trace/TraceContextInjection; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/trace/TraceContextInjection; + public static fun values ()[Lcom/datadog/android/trace/TraceContextInjection; +} + public abstract interface class com/datadog/android/trace/event/SpanEventMapper : com/datadog/android/event/EventMapper { public abstract fun map (Lcom/datadog/android/trace/model/SpanEvent;)Lcom/datadog/android/trace/model/SpanEvent; } +public final class com/datadog/android/trace/internal/ApmNetworkInstrumentation { + public final fun onRequest (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;)Lcom/datadog/android/trace/internal/net/RequestTraceState; + public final fun onResponseFailed (Lcom/datadog/android/trace/internal/net/RequestTraceState;Ljava/lang/Throwable;)V + public final fun onResponseSucceeded (Lcom/datadog/android/trace/internal/net/RequestTraceState;Lcom/datadog/android/api/instrumentation/network/HttpResponseInfo;)V +} + public final class com/datadog/android/trace/internal/DatadogPropagationHelper { + public static final field Companion Lcom/datadog/android/trace/internal/DatadogPropagationHelper$Companion; public final fun createExtractedContext (Ljava/lang/String;Ljava/lang/String;I)Lcom/datadog/android/trace/api/span/DatadogSpanContext; + public final fun extractParentContext (Lcom/datadog/android/trace/api/tracer/DatadogTracer;Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;)Lcom/datadog/android/trace/api/span/DatadogSpanContext; + public final fun extractSamplingDecision (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo;)Ljava/lang/Boolean; public final fun isExtractedContext (Lcom/datadog/android/trace/api/span/DatadogSpanContext;)Z + public final fun propagateNotSampledHeaders (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;Lcom/datadog/android/trace/api/tracer/DatadogTracer;Lcom/datadog/android/trace/api/span/DatadogSpan;Ljava/util/Set;Lcom/datadog/android/trace/TraceContextInjection;Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public final fun propagateSampledHeaders (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;Lcom/datadog/android/trace/api/tracer/DatadogTracer;Lcom/datadog/android/trace/api/span/DatadogSpan;Ljava/util/Set;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public final fun setTraceContext (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;Ljava/lang/String;Ljava/lang/String;I)V +} + +public final class com/datadog/android/trace/internal/DatadogPropagationHelper$Companion { } public final class com/datadog/android/trace/internal/DatadogSpanIdConverter { @@ -67,6 +118,7 @@ public final class com/datadog/android/trace/internal/DatadogTracingToolkit { public static final field spanIdConverter Lcom/datadog/android/trace/internal/DatadogSpanIdConverter; public static final fun activateSpan (Lcom/datadog/android/trace/api/tracer/DatadogTracer;Lcom/datadog/android/trace/api/span/DatadogSpan;Z)Lcom/datadog/android/trace/api/scope/DatadogScope; public static final fun addThrowable (Lcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Throwable;B)V + public final fun createApmNetworkInstrumentation (Ljava/lang/String;Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration;)Lcom/datadog/android/trace/internal/ApmNetworkInstrumentation; public final fun getPropagationHelper ()Lcom/datadog/android/trace/internal/DatadogPropagationHelper; public final fun mergeBaggage (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public final fun setSdkV2Compatible (Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder;)Lcom/datadog/android/trace/api/tracer/DatadogTracerBuilder; @@ -84,6 +136,38 @@ public final class com/datadog/android/trace/internal/RumContextPropagator$Compa public static synthetic fun extractRumContext$default (Lcom/datadog/android/trace/internal/RumContextPropagator$Companion;Lcom/datadog/android/trace/api/span/DatadogSpan;Lcom/datadog/android/trace/internal/RumContextPropagator;ZILjava/lang/Object;)Lcom/datadog/android/trace/api/span/DatadogSpan; } +public final class com/datadog/android/trace/internal/net/RequestTraceState { + public fun (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;)V + public synthetic fun (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component2 ()Z + public final fun component3 ()Lcom/datadog/android/trace/api/span/DatadogSpan; + public final fun component4 ()Ljava/lang/Float; + public final fun copy (Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;)Lcom/datadog/android/trace/internal/net/RequestTraceState; + public static synthetic fun copy$default (Lcom/datadog/android/trace/internal/net/RequestTraceState;Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder;ZLcom/datadog/android/trace/api/span/DatadogSpan;Ljava/lang/Float;ILjava/lang/Object;)Lcom/datadog/android/trace/internal/net/RequestTraceState; + public fun equals (Ljava/lang/Object;)Z + public final fun getRequestInfo ()Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo; + public final fun getSampleRate ()Ljava/lang/Float; + public final fun getSpan ()Lcom/datadog/android/trace/api/span/DatadogSpan; + public fun hashCode ()I + public final fun isSampled ()Z + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/trace/internal/net/TraceContext { + public fun (Ljava/lang/String;Ljava/lang/String;I)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()I + public final fun copy (Ljava/lang/String;Ljava/lang/String;I)Lcom/datadog/android/trace/internal/net/TraceContext; + public static synthetic fun copy$default (Lcom/datadog/android/trace/internal/net/TraceContext;Ljava/lang/String;Ljava/lang/String;IILjava/lang/Object;)Lcom/datadog/android/trace/internal/net/TraceContext; + public fun equals (Ljava/lang/Object;)Z + public final fun getSamplingPriority ()I + public final fun getSpanId ()Ljava/lang/String; + public final fun getTraceId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/datadog/android/trace/model/SpanEvent { public static final field Companion Lcom/datadog/android/trace/model/SpanEvent$Companion; public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JJJLcom/datadog/android/trace/model/SpanEvent$Metrics;Lcom/datadog/android/trace/model/SpanEvent$Meta;)V diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/ApmNetworkInstrumentationConfiguration.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/ApmNetworkInstrumentationConfiguration.kt new file mode 100644 index 0000000000..07b70f68bf --- /dev/null +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/ApmNetworkInstrumentationConfiguration.kt @@ -0,0 +1,196 @@ +/* + * 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.trace + +import androidx.annotation.FloatRange +import com.datadog.android.api.SdkCore +import com.datadog.android.core.configuration.HostsSanitizer +import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver +import com.datadog.android.core.sampling.Sampler +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.trace.internal.ApmNetworkInstrumentation +import com.datadog.android.trace.internal.net.TracerProvider + +/** + * Configuration that allows to configure APM tracing for network requests. + * + * @param tracedHostsWithHeaderType a list of all the hosts and header types that you want to + * be automatically tracked by this interceptor. If registering a [GlobalDatadogTracer], the tracer must be + * configured with [com.datadog.android.trace.api.tracer.DatadogTracerBuilder.withTracingHeadersTypes] containing all the necessary + * header types configured for network tracking. + */ +@Suppress("TooManyFunctions") +class ApmNetworkInstrumentationConfiguration internal constructor( + internal val tracedHostsWithHeaderType: Map> +) { + internal var traceOrigin: String? = null + internal var redacted404ResourceName = true + internal var sdkInstanceName: String? = null + + internal var localTracerFactory = DEFAULT_LOCAL_TRACER_FACTORY + internal var traceContextInjection = TraceContextInjection.SAMPLED + internal var tracedRequestListener: NetworkTracedRequestListener = NoOpNetworkTracedRequestListener() + internal var traceSampler: Sampler = + DeterministicTraceSampler(DEFAULT_TRACE_SAMPLE_RATE) + internal var globalTracerProvider: () -> DatadogTracer? = { GlobalDatadogTracer.getOrNull() } + internal var networkTracingScope: ApmNetworkTracingScope = ApmNetworkTracingScope.DETAILED + + constructor( + tracedHosts: List + ) : this( + tracedHosts.associateWith { + setOf( + TracingHeaderType.DATADOG, + TracingHeaderType.TRACECONTEXT + ) + } + ) + + /** + * Set the origin of the trace. + * @param traceOrigin the origin of the trace. + */ + fun setTraceOrigin(traceOrigin: String) = apply { + this.traceOrigin = traceOrigin + } + + /** + * Set the SDK instance name to bind to, the default value is null. + * @param sdkInstanceName SDK instance name to bind to, the default value is null. + * Instrumentation won't be working until SDK instance is ready. + */ + fun setSdkInstanceName(sdkInstanceName: String) = apply { + this.sdkInstanceName = sdkInstanceName + } + + /** + * Set the listener for automatically created [DatadogSpan]s. + * @param tracedRequestListener a listener for automatically created [DatadogSpan]s + */ + fun setTracedRequestListener(tracedRequestListener: NetworkTracedRequestListener) = apply { + this.tracedRequestListener = tracedRequestListener + } + + /** + * Set the trace sample rate controlling the sampling of APM traces created for + * auto-instrumented requests. If there is a parent trace attached to the network span created, then its + * sampling decision will be used instead. + * @param sampleRate the sample rate to use (percentage between 0f and 100f, default is 100f). + */ + fun setTraceSampleRate(@FloatRange(from = 0.0, to = 100.0) sampleRate: Float) = apply { + this.traceSampler = DeterministicTraceSampler(sampleRate) + } + + /** + * Set the trace sampler controlling the sampling of APM traces created for + * auto-instrumented requests. If there is a parent trace attached to the network span created, then its + * sampling decision will be used instead. + * @param traceSampler the trace sampler controlling the sampling of APM traces. + * By default it is a sampler accepting 100% of the traces. + */ + fun setTraceSampler(traceSampler: Sampler) = apply { + this.traceSampler = traceSampler + } + + /** + * Set the trace context injection behavior for this interceptor in the intercepted requests. + * By default this is set to [TraceContextInjection.SAMPLED], meaning that only the sampled request will + * propagate the trace context. In case of [TraceContextInjection.ALL] all the trace context + * will be propagated in the intercepted requests no matter if the span created around the request + * is sampled or not. + * @param traceContextInjection the trace context injection option. + * @see TraceContextInjection.ALL + * @see TraceContextInjection.SAMPLED + */ + fun setTraceContextInjection(traceContextInjection: TraceContextInjection) = apply { + this.traceContextInjection = traceContextInjection + } + + /** + * Set whether network requests returning a 404 status code should have their resource name redacted. + * In order to reduce the cardinality of resource names in APM, 404 URLs are automatically redacted to + * "404". + * @param redacted if true, all 404 requests will have a resource name set to "404", else the resource name + * will be the URL + */ + fun set404ResourcesRedacted(redacted: Boolean) = apply { + redacted404ResourceName = redacted + } + + /** + * Sets the tracing scope for network instrumentation. + * + * This controls how detailed the tracing will be: + * - [ApmNetworkTracingScope.DETAILED]: Traces both application-level requests and internal + * network operations (redirects, retries). This is the default. + * - [ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY]: Only traces the top-level + * application request, while still maintaining RUM-APM linking capabilities. + * + * @param networkTracingScope the tracing scope to use + * @see ApmNetworkTracingScope + */ + fun setTracingScope(networkTracingScope: ApmNetworkTracingScope) = apply { + this.networkTracingScope = networkTracingScope + } + + internal fun setLocalTracerFactory(factory: (SdkCore, Set) -> DatadogTracer) = apply { + this.localTracerFactory = factory + } + + internal fun setGlobalTracerProvider(globalTracerProvider: () -> DatadogTracer?) = apply { + this.globalTracerProvider = globalTracerProvider + } + + internal companion object { + internal const val ALL_IN_SAMPLE_RATE: Double = 100.0 + internal const val DEFAULT_TRACE_SAMPLE_RATE: Float = 100f + internal const val NETWORK_REQUESTS_TRACKING_FEATURE_NAME = "Network Requests" + + internal fun ApmNetworkInstrumentationConfiguration.createInstrumentation( + instrumentationName: String + ): ApmNetworkInstrumentation { + val localFirstPartyHostHeaderTypeResolver = DefaultFirstPartyHostHeaderTypeResolver( + resolveHosts(tracedHostsWithHeaderType) + ) + + val tracerProvider = TracerProvider(localTracerFactory, globalTracerProvider) + + return ApmNetworkInstrumentation( + traceOrigin = traceOrigin, + traceSampler = traceSampler, + tracerProvider = tracerProvider, + sdkInstanceName = sdkInstanceName, + injectionType = traceContextInjection, + networkTracingScope = networkTracingScope, + networkingLibraryName = instrumentationName, + tracedRequestListener = tracedRequestListener, + redacted404ResourceName = redacted404ResourceName, + localFirstPartyHostHeaderTypeResolver = localFirstPartyHostHeaderTypeResolver + ) + } + + private fun resolveHosts( + tracedHosts: Map> + ): Map> { + val sanitizer = HostsSanitizer() + val sanitizedHosts = sanitizer.sanitizeHosts( + tracedHosts.keys.toList(), + NETWORK_REQUESTS_TRACKING_FEATURE_NAME + ) + + return tracedHosts.filterKeys { sanitizedHosts.contains(it) } + } + + private val DEFAULT_LOCAL_TRACER_FACTORY: (SdkCore, Set) -> DatadogTracer = + { sdkCore, tracingHeaderTypes: Set -> + DatadogTracing.newTracerBuilder(sdkCore) + .withTracingHeadersTypes(tracingHeaderTypes) + .withSampleRate(ALL_IN_SAMPLE_RATE) + .build() + } + } +} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/ApmNetworkTracingScope.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/ApmNetworkTracingScope.kt new file mode 100644 index 0000000000..eadd9651a8 --- /dev/null +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/ApmNetworkTracingScope.kt @@ -0,0 +1,30 @@ +/* + * 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.trace + +/** + * Defines the scope of network tracing instrumentation. + * + * This enum controls how detailed the tracing will be for network requests, + * specifically whether internal network operations (redirects, retries) should be traced + * in addition to the application-level requests. + */ +enum class ApmNetworkTracingScope { + /** + * Default one. + * With this scope the Datadog SDK will trace both the application level requests and the network + * layer requests (redirect, retries). + */ + DETAILED, + + /** + * Only application level request is gonna be traced. + * In this mode the Datadog SDK still able to link trace spans to Rum.Resources making possible to navigate + * from one to another but the internal requests like redirects and retries ( if networking library allows that) + * will not be traced. + */ + APPLICATION_LEVEL_REQUESTS_ONLY +} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/DeterministicTraceSampler.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/DeterministicTraceSampler.kt new file mode 100644 index 0000000000..3481222e33 --- /dev/null +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/DeterministicTraceSampler.kt @@ -0,0 +1,44 @@ +/* + * 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.trace + +import androidx.annotation.FloatRange +import com.datadog.android.core.sampling.DeterministicSampler +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.internal.net.SpanSamplingIdProvider + +/** + * A [com.datadog.android.core.sampling.DeterministicSampler] using the TraceID of a Span to compute the sampling decision. + * + * @param sampleRateProvider Provider for the sample rate value which will be called each time + * the sampling decision needs to be made. All the values should be in the range [0;100]. + */ +open class DeterministicTraceSampler( + sampleRateProvider: () -> Float +) : DeterministicSampler( + SpanSamplingIdProvider::provideId, + sampleRateProvider +) { + + /** + * Creates a new instance of [DeterministicSampler] with the given sample rate. + * + * @param sampleRate Sample rate to use. + */ + constructor( + @FloatRange(from = 0.0, to = 100.0) sampleRate: Float + ) : this({ sampleRate }) + + /** + * Creates a new instance of [DeterministicSampler] with the given sample rate. + * + * @param sampleRate Sample rate to use. + */ + constructor( + @FloatRange(from = 0.0, to = 100.0) sampleRate: Double + ) : this(sampleRate.toFloat()) +} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/NetworkTracedRequestListener.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/NetworkTracedRequestListener.kt new file mode 100644 index 0000000000..4c914409ff --- /dev/null +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/NetworkTracedRequestListener.kt @@ -0,0 +1,34 @@ +/* + * 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.trace + +import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpResponseInfo +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.tools.annotation.NoOpImplementation + +/** + * Listener for automatically created [com.datadog.android.trace.api.span.DatadogSpan] around [com.datadog.android.api.instrumentation.network.HttpRequestInfo]. + */ +@NoOpImplementation +interface NetworkTracedRequestListener { + /** + * Notifies that a span was automatically created around an OkHttp [com.datadog.android.api.instrumentation.network.HttpRequestInfo]. + * You can update the given [com.datadog.android.trace.api.span.DatadogSpan] (e.g.: add custom tags / baggage items) before it + * is persisted. Won't be called if [com.datadog.android.api.instrumentation.network.HttpRequestInfo] wasn't sampled. + * @param request the intercepted [com.datadog.android.api.instrumentation.network.HttpRequestInfo] + * @param span the [com.datadog.android.trace.api.span.DatadogSpan] created around the intercepted [com.datadog.android.api.instrumentation.network.HttpRequestInfo] + * @param response the [com.datadog.android.api.instrumentation.network.HttpRequestInfo] response in case of any + * @param throwable in case an error occurred during the [com.datadog.android.api.instrumentation.network.HttpRequestInfo] + */ + fun onRequestIntercepted( + request: HttpRequestInfo, + span: DatadogSpan, + response: HttpResponseInfo?, + throwable: Throwable? + ) +} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/TraceContextInjection.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/TraceContextInjection.kt new file mode 100644 index 0000000000..e2ce246df2 --- /dev/null +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/TraceContextInjection.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.trace + +/** + * Defines whether the trace context should be injected into all requests or only sampled ones. + */ +enum class TraceContextInjection { + /** + * Injects trace context into all requests irrespective of the sampling decision. + * For example if the request trace is sampled out, the trace context will still be injected in your request + * headers but the sampling priority will be `0`. This will mean that the client will dictate the sampling priority + * on the server side and no trace will be created no matter the sampling rate at the server side. + */ + ALL, + + /** + * Injects trace context only into sampled requests. + * For example if the request trace is sampled out neither the trace context or the sampling priority will + * be injected into the request headers leaving the server side to make the sampling decision. + * This will mean that if the server side sampling rate is higher than the client side sampling rate there will + * be a chance that a trace will be created down the stream. + */ + SAMPLED +} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/ApmNetworkInstrumentation.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/ApmNetworkInstrumentation.kt new file mode 100644 index 0000000000..a7ced58f7a --- /dev/null +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/ApmNetworkInstrumentation.kt @@ -0,0 +1,292 @@ +/* + * 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.trace.internal + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.api.instrumentation.network.HttpResponseInfo +import com.datadog.android.api.instrumentation.network.MutableHttpRequestInfo +import com.datadog.android.api.logToUser +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.SdkReference +import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver +import com.datadog.android.core.sampling.Sampler +import com.datadog.android.internal.utils.loggableStackTrace +import com.datadog.android.lint.InternalApi +import com.datadog.android.trace.ApmNetworkTracingScope +import com.datadog.android.trace.NetworkTracedRequestListener +import com.datadog.android.trace.TraceContextInjection +import com.datadog.android.trace.api.DatadogTracingConstants +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.trace.internal.RumContextPropagator.Companion.extractRumContext +import com.datadog.android.trace.internal.net.RequestTraceState +import com.datadog.android.trace.internal.net.TracerProvider +import com.datadog.android.trace.internal.net.applyPriority +import com.datadog.android.trace.internal.net.buildSpan +import com.datadog.android.trace.internal.net.finishRumAware +import com.datadog.android.trace.internal.net.isRumEnabled +import com.datadog.android.trace.internal.net.sample +import java.net.HttpURLConnection +import java.util.Locale + +/** + * For internal usage only. + * + * Provides APM (Application Performance Monitoring) tracing instrumentation for network requests. + * This class handles the creation and management of trace spans for HTTP requests, including + * header injection, sampling decisions, and span lifecycle management. + * + * @param sdkInstanceName the name of the SDK instance to bind to, or null for the default instance. + * @param traceOrigin optional origin tag to add to traces. + * @param tracerProvider provider for obtaining tracer instances. + * @param redacted404ResourceName whether to redact resource names for 404 responses. + * @param traceSampler the sampler used to determine which traces should be sampled. + * @param injectionType defines whether trace context should be injected into all requests or only sampled ones. + * @param tracedRequestListener listener to be notified when a request is intercepted. + * @param localFirstPartyHostHeaderTypeResolver resolver for determining header types for first-party hosts. + * @param networkingLibraryName the name identifying the network instrumentation (e.g., "OkHttp", "Cronet"). + * @param networkTracingScope Tracing scope for the instrumentation. See [ApmNetworkTracingScope] enum for more details. + */ +@Suppress("LongParameterList") +@InternalApi +class ApmNetworkInstrumentation internal constructor( + internal val sdkInstanceName: String?, + internal val traceOrigin: String?, + internal val tracerProvider: TracerProvider, + internal val redacted404ResourceName: Boolean, + internal val traceSampler: Sampler, + internal val injectionType: TraceContextInjection, + internal val tracedRequestListener: NetworkTracedRequestListener, + internal val localFirstPartyHostHeaderTypeResolver: DefaultFirstPartyHostHeaderTypeResolver, + private val networkingLibraryName: String, + // TODO RUM-13974 will be used in the following tickets + @Suppress("unused") + private val networkTracingScope: ApmNetworkTracingScope = ApmNetworkTracingScope.DETAILED +) { + private val rumContextPropagator = RumContextPropagator { internalSdkCore } + private val sdkCoreReference = SdkReference(sdkInstanceName) { + val sdkCore = it as InternalSdkCore + if (localFirstPartyHostHeaderTypeResolver.isEmpty() && sdkCore.firstPartyHostResolver.isEmpty()) { + sdkCore.internalLogger.logToUser(InternalLogger.Level.WARN, onlyOnce = true) { + WARNING_TRACING_NO_HOSTS.format(Locale.US, networkingLibraryName) + } + } + } + private val internalSdkCore: InternalSdkCore? + get() = sdkCoreReference.get() as? InternalSdkCore + + /** + * Called when a network request is about to be sent. + * This method creates a trace span, applies sampling decisions, and injects tracing headers. + * + * @param request the HTTP request information. + * @return the tracing state containing the request modifier, sampling decision, and span. + */ + @Suppress("ReturnCount") + fun onRequest(request: HttpRequestInfo): RequestTraceState? { + val sdkCore = getSdkCoreOrNull(request.url) + val requestInfoBuilder = request.newBuilder(sdkCore?.internalLogger ?: InternalLogger.UNBOUND) + if (requestInfoBuilder == null) { + return null + } + + if (sdkCore == null) { + return RequestTraceState(requestInfoBuilder) + } + + val tracer = tracerProvider.provideTracer( + sdkCore, + localFirstPartyHostHeaderTypeResolver.getAllHeaderTypes(), + networkingLibraryName + ) + + if (tracer == null || !request.isTraceable(sdkCore)) { + return RequestTraceState(requestInfoBuilder) + } + + val span = tracer.buildSpan(request, networkingLibraryName, traceOrigin) + val isSampled = span.isSampled(request) + if (span.isRootSpan) { + span.applyPriority(isSampled, traceSampler) + } + + val updatedRequest = try { + updateRequest(request.url, sdkCore, requestInfoBuilder, tracer, span, isSampled) + } catch (e: IllegalStateException) { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), + { "Failed to update intercepted $networkingLibraryName request" }, + e + ) + requestInfoBuilder + } + + return RequestTraceState( + span = span, + isSampled = isSampled, + requestBuilder = updatedRequest, + sampleRate = traceSampler.getSampleRate() + ) + } + + /** + * Called when a network request succeeds. + * This method updates the span with response information and finishes it. + * + * @param tracingState the tracing state from [onRequest]. + * @param response the HTTP response information. + */ + fun onResponseSucceeded(tracingState: RequestTraceState, response: HttpResponseInfo) { + if (tracingState.isSampled) { + tracingState.span?.setTag(DatadogTracingConstants.Tags.KEY_HTTP_STATUS, response.statusCode) + if (response.statusCode in HttpURLConnection.HTTP_BAD_REQUEST until HttpURLConnection.HTTP_INTERNAL_ERROR) { + tracingState.span?.isError = true + } + if (response.statusCode == HttpURLConnection.HTTP_NOT_FOUND && redacted404ResourceName) { + tracingState.span?.resourceName = RESOURCE_NAME_404 + } + } + tracingState.onRequestCompleted(response, null) + tracingState.span?.finishRumAware(tracingState.isSampled, !internalSdkCore.isRumEnabled) + } + + /** + * Called when a network request fails. + * This method marks the span as errored, adds error details, and finishes it. + * + * @param tracingState the tracing state from [onRequest]. + * @param throwable the exception that caused the failure. + */ + fun onResponseFailed(tracingState: RequestTraceState, throwable: Throwable) { + if (!tracingState.isSampled) { + tracingState.onRequestCompleted(null, throwable) + } else { + tracingState.span?.isError = true + tracingState.span?.setTag(DatadogTracingConstants.Tags.KEY_ERROR_MSG, throwable.message) + tracingState.span?.setTag(DatadogTracingConstants.Tags.KEY_ERROR_TYPE, throwable.javaClass.name) + tracingState.span?.setTag(DatadogTracingConstants.Tags.KEY_ERROR_STACK, throwable.loggableStackTrace()) + tracingState.onRequestCompleted(null, throwable) + } + tracingState.span?.finishRumAware(tracingState.isSampled, !internalSdkCore.isRumEnabled) + } + + private fun DatadogSpan.isSampled(request: HttpRequestInfo): Boolean = + extractRumContext(rumContextPropagator, block = true) + .sample(request, traceSampler) + + private fun RequestTraceState.onRequestCompleted( + response: HttpResponseInfo?, + throwable: Throwable? + ) { + if (!isSampled || span == null) return + val request = requestBuilder.build() + try { + tracedRequestListener.onRequestIntercepted(request, span, response, throwable) + } catch (e: StackOverflowError) { + internalSdkCore?.internalLogger?.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.USER, + { "$ERROR_STACK_OVERFLOW\nRequest: ${request.method}:${request.url}" }, + e + ) + @Suppress("ThrowingInternalException") + throw e + } + } + + private fun updateRequest( + url: String, + sdkCore: InternalSdkCore, + requestModifier: HttpRequestInfoBuilder, + tracer: DatadogTracer, + span: DatadogSpan, + isSampled: Boolean + ): HttpRequestInfoBuilder = requestModifier.also { modifier -> + val tracingHeaderTypes = localFirstPartyHostHeaderTypeResolver.headerTypesForUrl(url) + .ifEmpty { sdkCore.firstPartyHostResolver.headerTypesForUrl(url) } + + if (isSampled) { + DatadogTracingToolkit.propagationHelper.propagateSampledHeaders( + requestModifier, + tracer, + span, + tracingHeaderTypes + ) + } else { + DatadogTracingToolkit.propagationHelper.propagateNotSampledHeaders( + modifier, + tracer, + span, + tracingHeaderTypes, + injectionType, + traceOrigin + ) + } + } + + private fun HttpRequestInfo.isTraceable(sdkCore: InternalSdkCore): Boolean = + sdkCore.firstPartyHostResolver.isFirstPartyUrl(url) || + localFirstPartyHostHeaderTypeResolver.isFirstPartyUrl(url) + + private fun getSdkCoreOrNull(url: String? = null): InternalSdkCore? { + if (internalSdkCore == null) { + InternalLogger.UNBOUND.logToUser(InternalLogger.Level.INFO) { + buildString { + append( + if (sdkInstanceName == null) { + "Default SDK instance" + } else { + "SDK instance with name=$sdkInstanceName" + } + ) + append(" for ").append(networkingLibraryName).append(" instrumentation is not found") + if (url != null) append(", skipping tracking of request with url=").append(url) + } + } + } + + return internalSdkCore + } + + internal companion object { + internal const val ZERO_SAMPLE_RATE: Float = 0.0f + internal const val ALL_IN_SAMPLE_RATE: Double = 100.0 + internal const val SPAN_NAME = "%s.request" + internal const val RESOURCE_NAME_404 = "404" + internal const val AGENT_PSR_ATTRIBUTE = "_dd.agent_psr" + internal const val URL_QUERY_PARAMS_BLOCK_SEPARATOR = '?' + internal const val WARNING_TRACING_NO_HOSTS = + "You added a ApmNetworkInstrumentation to your %s instrumentation, " + + "but you did not specify any first party hosts. " + + "Your requests won't be traced.\n" + + "To set a list of known hosts, you can use the " + + "Configuration.Builder.setFirstPartyHosts() method." + internal const val ERROR_STACK_OVERFLOW = + "StackOverflowError detected in TracedRequestListener. " + + "This is likely caused by retrying the same request within the " + + "onRequestIntercepted callback, leading to infinite recursion." + + internal const val ERROR_REQUEST_INFO_IS_NOT_MUTABLE = + "RequestInfo is not mutable. Your requests won't be traced." + + private fun HttpRequestInfo.newBuilder(internalLogger: InternalLogger): HttpRequestInfoBuilder? { + return if (this is MutableHttpRequestInfo) { + newBuilder() + } else { + internalLogger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { ERROR_REQUEST_INFO_IS_NOT_MUTABLE } + ) + null + } + } + } +} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelper.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelper.kt index 67bee9a3f6..dd4fd89606 100644 --- a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelper.kt +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelper.kt @@ -5,11 +5,23 @@ */ package com.datadog.android.trace.internal +import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.api.instrumentation.network.tag import com.datadog.android.lint.InternalApi +import com.datadog.android.trace.TraceContextInjection +import com.datadog.android.trace.TracingHeaderType +import com.datadog.android.trace.api.DatadogTracingConstants.PrioritySampling +import com.datadog.android.trace.api.span.DatadogSpan import com.datadog.android.trace.api.span.DatadogSpanContext +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.trace.internal.net.TraceContext import com.datadog.trace.api.DDSpanId import com.datadog.trace.api.DDTraceId +import com.datadog.trace.core.propagation.B3HttpCodec +import com.datadog.trace.core.propagation.DatadogHttpCodec import com.datadog.trace.core.propagation.ExtractedContext +import com.datadog.trace.core.propagation.W3CHttpCodec /** * For internal usage only. @@ -23,11 +35,73 @@ class DatadogPropagationHelper internal constructor() { * @param context The [DatadogSpanContext] to be evaluated. * @return True if the context is identified as an extracted context, otherwise false. */ + // TODO RUM-13441: This method should be private after refactor of TracingInterceptor. fun isExtractedContext(context: DatadogSpanContext): Boolean { if (context !is DatadogSpanContextAdapter) return false return context.delegate is ExtractedContext } + /** + * Sets the trace context information as a tag on the request. + * + * @param requestInfoBuilder the request modifier to add the trace context to. + * @param traceId the trace ID to set. + * @param spanId the span ID to set. + * @param samplingPriority the sampling priority for the trace. + */ + fun setTraceContext( + requestInfoBuilder: HttpRequestInfoBuilder, + traceId: String, + spanId: String, + samplingPriority: Int + ) { + requestInfoBuilder.addTag( + TraceContext::class.java, + TraceContext( + traceId, + spanId, + samplingPriority + ) + ) + } + + /** + * Extracts the parent span context from the request. + * Checks both the request tags and headers for trace context information. + * + * @param tracer the tracer to use for context extraction. + * @param request the HTTP request info to extract context from. + * @return the extracted parent context, or null if none found. + */ + fun extractParentContext(tracer: DatadogTracer, request: HttpRequestInfo): DatadogSpanContext? { + val tagContext = request.tag(DatadogSpan::class.java)?.context() ?: extractTraceContext(request) + + val headerContext: DatadogSpanContext? = tracer.propagate().extract(request) { carrier, classifier -> + val headers = carrier.headers + .map { it.key to it.value.joinToString(";") } + .toMap() + + // there is no actual classification here, values just got cached + for ((key, value) in headers) classifier(key, value) + } + + return if (headerContext != null && isExtractedContext(headerContext)) { + headerContext + } else { + tagContext + } + } + + private fun extractTraceContext(request: HttpRequestInfo): DatadogSpanContext? = + request.tag(DatadogSpan::class.java)?.context() + ?: request.tag(TraceContext::class.java)?.let { + createExtractedContext( + it.traceId, + it.spanId, + it.samplingPriority + ) + } + /** * Creates a [DatadogSpanContext] object that represents an extracted context from the given parameters. * @@ -36,6 +110,7 @@ class DatadogPropagationHelper internal constructor() { * @param samplingPriority The sampling priority value for determining the trace's sampling behavior. * @return A [DatadogSpanContext] instance containing the extracted context. */ + // TODO RUM-13441: this class should be internal after refactor of TracingInterceptor. fun createExtractedContext( traceId: String, spanId: String, @@ -50,4 +125,290 @@ class DatadogPropagationHelper internal constructor() { null ) ) + + /** + * Injects trace context headers for a sampled request. + * + * @param modifier the request modifier to add headers to. + * @param tracer the tracer to use for context injection. + * @param span the span containing the trace context. + * @param tracingHeaderTypes the set of header types to inject. + * @return the modified request info builder. + */ + fun propagateSampledHeaders( + modifier: HttpRequestInfoBuilder, + tracer: DatadogTracer, + span: DatadogSpan, + tracingHeaderTypes: Set + ) = modifier.apply { + tracer.propagate().inject( + span.context(), + modifier + ) { carrier: HttpRequestInfoBuilder, key: String, value: String -> + when (key) { + DatadogHttpCodec.ORIGIN_KEY, + DatadogHttpCodec.SPAN_ID_KEY, + DatadogHttpCodec.TRACE_ID_KEY, + DatadogHttpCodec.DATADOG_TAGS_KEY, + DatadogHttpCodec.SAMPLING_PRIORITY_KEY -> if (tracingHeaderTypes.contains(TracingHeaderType.DATADOG)) { + carrier.replaceHeader(key, value) + } else { + carrier.removeHeader(key) + } + + B3HttpCodec.B3_KEY -> if (tracingHeaderTypes.contains(TracingHeaderType.B3)) { + carrier.replaceHeader(key, value) + } else { + carrier.removeHeader(key) + } + + B3HttpCodec.SPAN_ID_KEY, + B3HttpCodec.TRACE_ID_KEY, + B3HttpCodec.SAMPLING_PRIORITY_KEY -> if (tracingHeaderTypes.contains(TracingHeaderType.B3MULTI)) { + carrier.replaceHeader(key, value) + } else { + carrier.removeHeader(key) + } + + W3CHttpCodec.BAGGAGE_KEY -> carrier.replaceHeader( + key, + DatadogTracingToolkit.mergeBaggage( + carrier.resolveExistingBaggageHeaderValue(), + value + ) + ) + + W3CHttpCodec.TRACE_PARENT_KEY, + W3CHttpCodec.TRACE_STATE_KEY -> if (tracingHeaderTypes.contains(TracingHeaderType.TRACECONTEXT)) { + carrier.replaceHeader(key, value) + } else { + carrier.removeHeader(key) + } + + else -> carrier.replaceHeader(key, value) + } + } + } + + /** + * Handles trace context headers for a non-sampled request. + * Depending on the injection type, may remove or inject headers with drop sampling priority. + * + * @param modifier the request modifier to modify headers on. + * @param tracer the tracer to use for context injection. + * @param span the span containing the trace context. + * @param tracingHeaderTypes the set of header types to handle. + * @param injectionType whether to inject headers for non-sampled requests. + * @param traceOrigin optional trace origin to include in headers. + * @return the modified request info builder. + */ + @Suppress("NestedBlockDepth") + fun propagateNotSampledHeaders( + modifier: HttpRequestInfoBuilder, + tracer: DatadogTracer, + span: DatadogSpan, + tracingHeaderTypes: Set, + injectionType: TraceContextInjection, + traceOrigin: String? + ) = modifier.apply { + for (headerType in tracingHeaderTypes) { + when (headerType) { + TracingHeaderType.DATADOG -> { + DATADOG_CODEC_HEADERS.forEach { removeHeader(it) } + if (TraceContextInjection.ALL == injectionType) resetDatadogHeaders(span, tracer) + } + + TracingHeaderType.B3 -> { + removeHeader(B3HttpCodec.B3_KEY) + if (TraceContextInjection.ALL == injectionType) resetB3Headers() + } + + TracingHeaderType.B3MULTI -> { + B3M_CODEC_HEADERS.forEach { removeHeader(it) } + if (TraceContextInjection.ALL == injectionType) resetB3MultiHeaders() + } + + TracingHeaderType.TRACECONTEXT -> { + W3C_CODEC_HEADERS.forEach { removeHeader(it) } + if (TraceContextInjection.ALL == injectionType) { + resetW3CHeaders( + span, + traceOrigin + ) + } + } + } + } + } + + /** + * Extracts the sampling decision from the request. + * Checks headers and tags for existing sampling decisions. + * + * @param request the HTTP request info to extract sampling decision from. + * @return true if sampled, false if not sampled, null if no decision found. + */ + fun extractSamplingDecision(request: HttpRequestInfo): Boolean? { + val datadogSpan = request.tag(DatadogSpan::class.java) + val headerSamplingPriority = extractSamplingDecisionFromHeader(request) + val openTelemetrySpanSamplingPriority = request.tag(TraceContext::class.java)?.samplingPriority + + return when { + headerSamplingPriority != null -> headerSamplingPriority + + datadogSpan != null -> { + DatadogTracingToolkit.setTracingSamplingPriorityIfNecessary(datadogSpan.context()) + datadogSpan.context().samplingPriority > 0 + } + + openTelemetrySpanSamplingPriority == PrioritySampling.UNSET -> null + + else -> openTelemetrySpanSamplingPriority?.let { samplingPriority -> samplingPriority > 0 } + } + } + + @Suppress("ReturnCount") + private fun extractSamplingDecisionFromHeader(request: HttpRequestInfo): Boolean? { + val datadogSamplingPriority = + request.headers[DatadogHttpCodec.SAMPLING_PRIORITY_KEY]?.firstOrNull()?.toIntOrNull() + if (datadogSamplingPriority != null) { + if (datadogSamplingPriority == PrioritySampling.UNSET) return null + return datadogSamplingPriority == PrioritySampling.USER_KEEP || + datadogSamplingPriority == PrioritySampling.SAMPLER_KEEP + } + val b3MSamplingPriority = request.headers[B3HttpCodec.SAMPLING_PRIORITY_KEY]?.firstOrNull() + if (b3MSamplingPriority != null) { + return when (b3MSamplingPriority) { + "1" -> true + "0" -> false + else -> null + } + } + + val b3HeaderValue = request.headers[B3HttpCodec.B3_KEY]?.firstOrNull() + if (b3HeaderValue != null) { + if (b3HeaderValue == "0") { + return false + } + val b3HeaderParts = b3HeaderValue.split("-") + if (b3HeaderParts.size >= B3_SAMPLING_DECISION_INDEX + 1) { + return when (b3HeaderParts[B3_SAMPLING_DECISION_INDEX]) { + "1", "d" -> true + "0" -> false + else -> null + } + } + } + + val w3cHeaderValue = request.headers[W3CHttpCodec.TRACE_PARENT_KEY]?.firstOrNull() + if (w3cHeaderValue != null) { + val w3CHeaderParts = w3cHeaderValue.split("-") + if (w3CHeaderParts.size >= W3C_SAMPLING_DECISION_INDEX + 1) { + return when (w3CHeaderParts[W3C_SAMPLING_DECISION_INDEX].toIntOrNull()) { + 1 -> true + 0 -> false + else -> null + } + } + } + + return null + } + + companion object { + internal const val B3_DROP_SAMPLING_DECISION = "0" + internal const val B3M_DROP_SAMPLING_DECISION = "0" + internal const val DATADOG_DROP_SAMPLING_DECISION = "0" + internal const val W3C_TRACE_PARENT_DROP_SAMPLING_DECISION = "00-%s-%s-00" + internal const val W3C_TRACE_STATE_DROP_SAMPLING_DECISION = "dd=p:%s;s:0" + internal const val W3C_TRACE_ID_LENGTH = 32 + internal const val W3C_PARENT_ID_LENGTH = 16 + internal const val B3_SAMPLING_DECISION_INDEX = 2 + internal const val W3C_SAMPLING_DECISION_INDEX = 3 + + internal val DATADOG_CODEC_HEADERS = setOf( + DatadogHttpCodec.ORIGIN_KEY, + DatadogHttpCodec.SPAN_ID_KEY, + DatadogHttpCodec.TRACE_ID_KEY, + DatadogHttpCodec.DATADOG_TAGS_KEY, + DatadogHttpCodec.SAMPLING_PRIORITY_KEY + ) + + internal val B3M_CODEC_HEADERS = setOf( + B3HttpCodec.TRACE_ID_KEY, + B3HttpCodec.SPAN_ID_KEY, + B3HttpCodec.SAMPLING_PRIORITY_KEY + ) + + internal val W3C_CODEC_HEADERS = setOf( + W3CHttpCodec.TRACE_PARENT_KEY, + W3CHttpCodec.TRACE_STATE_KEY + ) + + private fun HttpRequestInfoBuilder.resetDatadogHeaders( + span: DatadogSpan, + tracer: DatadogTracer + ) { + tracer.propagate().inject( + span.context(), + this + ) { carrier, key, value -> + carrier.removeHeader(key) + if (key in DATADOG_CODEC_HEADERS) carrier.addHeader(key, value) + } + + replaceHeader(DatadogHttpCodec.SAMPLING_PRIORITY_KEY, DATADOG_DROP_SAMPLING_DECISION) + } + + private fun HttpRequestInfoBuilder.resetB3Headers() { + addHeader(B3HttpCodec.B3_KEY, B3_DROP_SAMPLING_DECISION) + } + + private fun HttpRequestInfoBuilder.resetB3MultiHeaders() { + addHeader(B3HttpCodec.SAMPLING_PRIORITY_KEY, B3M_DROP_SAMPLING_DECISION) + } + + private fun HttpRequestInfoBuilder.resetW3CHeaders( + span: DatadogSpan, + traceOrigin: String? + ) { + val traceId = span.context().traceId.toHexString() + val spanId = span.context().spanId.toString() + addHeader( + W3CHttpCodec.TRACE_PARENT_KEY, + // TODO RUM-11445 InvalidStringFormat false alarm + @Suppress("UnsafeThirdPartyFunctionCall", "InvalidStringFormat") // Format string is static + W3C_TRACE_PARENT_DROP_SAMPLING_DECISION.format( + traceId.padStart(length = W3C_TRACE_ID_LENGTH, padChar = '0'), + spanId.padStart(length = W3C_PARENT_ID_LENGTH, padChar = '0') + ) + ) + // TODO RUM-2121 3rd party vendor information will be erased + // TODO RUM-11445 InvalidStringFormat false alarm + @Suppress("UnsafeThirdPartyFunctionCall", "InvalidStringFormat") // Format string is static + var traceStateHeader = W3C_TRACE_STATE_DROP_SAMPLING_DECISION + .format(spanId.padStart(length = W3C_PARENT_ID_LENGTH, padChar = '0')) + if (traceOrigin != null) { + traceStateHeader += ";o:$traceOrigin" + } + addHeader(W3CHttpCodec.TRACE_STATE_KEY, traceStateHeader) + } + + @Suppress("UnsafeThirdPartyFunctionCall") // exceptions are caught + private fun HttpRequestInfoBuilder.resolveExistingBaggageHeaderValue(): String? = try { + // w3 HTTP specification allows multiple baggage headers and in such case they should be combined + // https://www.w3.org/TR/baggage/ + build() + .headers[W3CHttpCodec.BAGGAGE_KEY].orEmpty() + .reduce { acc, header -> + DatadogTracingToolkit.mergeBaggage(acc, header) + } + } catch (_: UnsupportedOperationException) { + // Header values collection is empty + null + } catch (_: IllegalStateException) { + // Failed to compose baggage header + null + } + } } diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogTracingToolkit.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogTracingToolkit.kt index 70e12ffd98..3941d16efa 100644 --- a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogTracingToolkit.kt +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogTracingToolkit.kt @@ -6,6 +6,8 @@ package com.datadog.android.trace.internal import com.datadog.android.lint.InternalApi +import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration +import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration.Companion.createInstrumentation import com.datadog.android.trace.api.scope.DatadogScope import com.datadog.android.trace.api.span.DatadogSpan import com.datadog.android.trace.api.span.DatadogSpanContext @@ -104,4 +106,17 @@ object DatadogTracingToolkit { .mergeWith(Baggage.from(newHeader)) .toString() } + + /** + * Creates an [ApmNetworkInstrumentation] instance from the provided configuration. + * + * @param name the name identifying the network instrumentation (e.g., "OkHttp", "Cronet"). + * @param configuration the configuration containing tracing settings such as traced hosts, + * sample rate, trace context injection behavior, and tracing scope. + * @return a new [ApmNetworkInstrumentation] instance configured with the provided settings. + */ + fun createApmNetworkInstrumentation( + name: String, + configuration: ApmNetworkInstrumentationConfiguration + ): ApmNetworkInstrumentation = configuration.createInstrumentation(name) } diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/ApmNetworkInstrumentationExt.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/ApmNetworkInstrumentationExt.kt new file mode 100644 index 0000000000..7c759c08e2 --- /dev/null +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/ApmNetworkInstrumentationExt.kt @@ -0,0 +1,78 @@ +/* + * 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.trace.internal.net + +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.core.sampling.Sampler +import com.datadog.android.trace.api.DatadogTracingConstants.PrioritySampling +import com.datadog.android.trace.api.DatadogTracingConstants.Tags +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.trace.internal.ApmNetworkInstrumentation.Companion.AGENT_PSR_ATTRIBUTE +import com.datadog.android.trace.internal.ApmNetworkInstrumentation.Companion.ALL_IN_SAMPLE_RATE +import com.datadog.android.trace.internal.ApmNetworkInstrumentation.Companion.SPAN_NAME +import com.datadog.android.trace.internal.ApmNetworkInstrumentation.Companion.URL_QUERY_PARAMS_BLOCK_SEPARATOR +import com.datadog.android.trace.internal.ApmNetworkInstrumentation.Companion.ZERO_SAMPLE_RATE +import com.datadog.android.trace.internal.DatadogTracingToolkit.propagationHelper +import java.util.Locale + +internal val FeatureSdkCore?.isRumEnabled: Boolean + get() = this?.getFeature(Feature.RUM_FEATURE_NAME) != null + +internal fun DatadogSpan.applyPriority(isSampled: Boolean, traceSampler: Sampler) { + val samplingPriority = if (isSampled) { + PrioritySampling.SAMPLER_KEEP + } else { + PrioritySampling.SAMPLER_DROP + } + + val spanContext = context() + if (spanContext.setSamplingPriority(samplingPriority)) { + spanContext.setMetric( + AGENT_PSR_ATTRIBUTE, + (traceSampler.getSampleRate() ?: ZERO_SAMPLE_RATE) / ALL_IN_SAMPLE_RATE + ) + } +} + +internal fun DatadogSpan.sample(request: HttpRequestInfo, traceSampler: Sampler): Boolean { + val samplingPriority = samplingPriority + return if (samplingPriority != null) { + samplingPriority > 0 + } else { + propagationHelper.extractSamplingDecision(request) ?: traceSampler.sample(this) + } +} + +internal fun DatadogSpan.finishRumAware(isSampled: Boolean, canSendSpan: Boolean) { + if (canSendSpan) { + if (isSampled) finish() else drop() + } else { + drop() + } +} + +internal fun DatadogTracer.buildSpan( + request: HttpRequestInfo, + networkInstrumentationName: String, + traceOrigin: String? +): DatadogSpan { + val parentContext = propagationHelper.extractParentContext(this, request) + + val span = buildSpan(SPAN_NAME.format(Locale.US, networkInstrumentationName)) + .withOrigin(traceOrigin) + .withParentContext(parentContext) + .start() + + span.resourceName = request.url.substringBefore(URL_QUERY_PARAMS_BLOCK_SEPARATOR) + span.setTag(Tags.KEY_HTTP_URL, request.url) + span.setTag(Tags.KEY_HTTP_METHOD, request.method) + span.setTag(Tags.KEY_SPAN_KIND, Tags.VALUE_SPAN_KIND_CLIENT) + + return span +} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/RequestTraceState.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/RequestTraceState.kt new file mode 100644 index 0000000000..76f9f7d055 --- /dev/null +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/RequestTraceState.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.trace.internal.net + +import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.lint.InternalApi +import com.datadog.android.trace.api.span.DatadogSpan + +/** + * For internal usage only. + * + * Holds the tracing state for a network request. + * This state is created by [com.datadog.android.trace.internal.ApmNetworkInstrumentation.onRequest] and should be passed + * to [com.datadog.android.trace.internal.ApmNetworkInstrumentation.onResponseSucceeded] or [com.datadog.android.trace.internal.ApmNetworkInstrumentation.onResponseFailed]. + * + * @property requestBuilder the modifier for the HTTP request info, containing any added headers. + * @property isSampled whether the request trace was sampled. + * @property span the trace span created for this request, or null if not sampled or tracing is disabled. + * @property sampleRate the sample rate used for the sampling decision. + */ +@InternalApi +data class RequestTraceState( + internal val requestBuilder: HttpRequestInfoBuilder, + val isSampled: Boolean = false, + val span: DatadogSpan? = null, + val sampleRate: Float? = null +) { + + /** + * Returns the [HttpRequestInfo] with any modifications applied during tracing setup. + * This includes tracing headers that were added to the original request. + */ + val requestInfo: HttpRequestInfo + get() = requestBuilder.build() +} diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/utils/SpanSamplingIdProvider.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/SpanSamplingIdProvider.kt similarity index 94% rename from integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/utils/SpanSamplingIdProvider.kt rename to features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/SpanSamplingIdProvider.kt index 375236a403..1699140d96 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/utils/SpanSamplingIdProvider.kt +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/SpanSamplingIdProvider.kt @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.okhttp.internal.utils +package com.datadog.android.trace.internal.net import com.datadog.android.log.LogAttributes import com.datadog.android.trace.api.span.DatadogSpan diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/TraceContext.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TraceContext.kt similarity index 82% rename from integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/TraceContext.kt rename to features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TraceContext.kt index b7b49840bd..240abce454 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/TraceContext.kt +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TraceContext.kt @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.okhttp +package com.datadog.android.trace.internal.net import com.datadog.android.lint.InternalApi @@ -12,6 +12,7 @@ import com.datadog.android.lint.InternalApi * The context of a trace to be propagated through the OkHttp requests for Datadog tracing. */ @InternalApi +// TODO RUM-13441: this class should be internal after refactor of TracingInterceptor. data class TraceContext( /** * The trace id. diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TracerProvider.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TracerProvider.kt new file mode 100644 index 0000000000..85c513d418 --- /dev/null +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TracerProvider.kt @@ -0,0 +1,73 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +package com.datadog.android.trace.internal.net + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.logToUser +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.trace.TracingHeaderType +import com.datadog.android.trace.api.tracer.DatadogTracer +import java.util.Locale +import java.util.concurrent.atomic.AtomicReference + +internal class TracerProvider internal constructor( + private val localTracerFactory: (SdkCore, Set) -> DatadogTracer, + private val globalTracerProvider: () -> DatadogTracer? +) { + private val localTracerReference: AtomicReference = AtomicReference() + + @Synchronized + fun provideTracer( + sdkCore: InternalSdkCore, + localHeaderTypes: Set, + networkingLibraryName: String + ): DatadogTracer? { + val tracingFeature = sdkCore.getFeature(Feature.TRACING_FEATURE_NAME) + val globalTracerInstance = globalTracerProvider.invoke() + return when { + tracingFeature == null -> { + sdkCore.internalLogger.logToUser( + level = InternalLogger.Level.WARN, + onlyOnce = true + ) { WARNING_TRACING_DISABLED.format(Locale.US, networkingLibraryName) } + null + } + + globalTracerInstance != null -> { + // clear the localTracer reference if any + localTracerReference.set(null) + globalTracerInstance + } + + else -> { + // only register once + if (localTracerReference.get() == null) { + @Suppress("UnsafeThirdPartyFunctionCall") // internal safe call + val globalHeaderTypes = sdkCore.firstPartyHostResolver.getAllHeaderTypes() + val allHeaders = localHeaderTypes.plus(globalHeaderTypes) + localTracerReference.compareAndSet(null, localTracerFactory(sdkCore, allHeaders)) + sdkCore.internalLogger.logToUser(InternalLogger.Level.WARN) { + WARNING_DEFAULT_TRACER.format(Locale.US, networkingLibraryName) + } + } + return localTracerReference.get() + } + } + } + + internal companion object { + const val WARNING_TRACING_DISABLED = "You added a ApmNetworkInstrumentation to your %s, " + + "but you did not enable the TracingFeature. " + + "Your requests won't be traced." + + const val WARNING_DEFAULT_TRACER = + "You added a ApmNetworkInstrumentation to your %s instrumentation, " + + "but you didn't register any DatadogTracer in . " + + "We automatically created a local tracer for you." + } +} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TracesRequestFactory.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TracesRequestFactory.kt index f8149539fa..493af5c7fc 100644 --- a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TracesRequestFactory.kt +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/net/TracesRequestFactory.kt @@ -25,7 +25,7 @@ internal class TracesRequestFactory( executionContext: RequestExecutionContext, batchData: List, batchMetadata: ByteArray? - ): Request? { + ): Request { val requestId = UUID.randomUUID().toString() val baseUrl = customEndpointUrl ?: (context.site.intakeEndpoint + "/api/v2/spans") diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelperTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelperTest.kt new file mode 100644 index 0000000000..524abd0654 --- /dev/null +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelperTest.kt @@ -0,0 +1,912 @@ +/* + * 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.trace.internal + +import com.datadog.android.api.instrumentation.network.ExtendedRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.trace.TraceContextInjection +import com.datadog.android.trace.TracingHeaderType +import com.datadog.android.trace.api.DatadogTracingConstants.PrioritySampling +import com.datadog.android.trace.api.propagation.DatadogPropagation +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.span.DatadogSpanContext +import com.datadog.android.trace.api.trace.DatadogTraceId +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.trace.internal.net.TraceContext +import com.datadog.android.utils.forge.Configurator +import com.datadog.trace.core.propagation.B3HttpCodec +import com.datadog.trace.core.propagation.DatadogHttpCodec +import com.datadog.trace.core.propagation.ExtractedContext +import com.datadog.trace.core.propagation.W3CHttpCodec +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +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.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.stream.Stream + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogPropagationHelperTest { + + private lateinit var testedHelper: DatadogPropagationHelper + + @Mock + lateinit var mockTracer: DatadogTracer + + @Mock + lateinit var mockSpan: DatadogSpan + + @Mock + lateinit var mockSpanContext: DatadogSpanContext + + @Mock + lateinit var mockTraceId: DatadogTraceId + + @Mock + lateinit var mockRequestModifier: HttpRequestInfoBuilder + + @Mock + lateinit var mockPropagation: DatadogPropagation + + @BeforeEach + fun `set up`() { + testedHelper = DatadogPropagationHelper() + + whenever(mockSpan.context()) doReturn mockSpanContext + whenever(mockSpanContext.traceId) doReturn mockTraceId + whenever(mockTracer.propagate()) doReturn mockPropagation + whenever(mockRequestModifier.build()) doReturn mock() + } + + @Test + fun `M return true W isExtractedContext() {extracted context}`(forge: Forge) { + // Given + val extractedContext = ExtractedContext( + forge.getForgery(), + forge.aLong(), + forge.anInt(), + null, + null, + null + ) + val context = DatadogSpanContextAdapter(extractedContext) + + // When + val result = testedHelper.isExtractedContext(context) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return false W isExtractedContext() {non-extracted DatadogSpanContextAdapter}`() { + // Given + val mockAgentSpanContext: com.datadog.trace.bootstrap.instrumentation.api.AgentSpan.Context = mock() + val context = DatadogSpanContextAdapter(mockAgentSpanContext) + + // When + val result = testedHelper.isExtractedContext(context) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return false W isExtractedContext() {non-DatadogSpanContextAdapter}`( + @Forgery fakeContext: DatadogSpanContext + ) { + // When + val result = testedHelper.isExtractedContext(fakeContext) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M add TraceContext tag W setTraceContext()`( + @StringForgery fakeTraceId: String, + @StringForgery fakeSpanId: String, + @IntForgery fakeSamplingPriority: Int + ) { + // When + testedHelper.setTraceContext(mockRequestModifier, fakeTraceId, fakeSpanId, fakeSamplingPriority) + + // Then + verify(mockRequestModifier).addTag( + eq(TraceContext::class.java), + eq(TraceContext(fakeTraceId, fakeSpanId, fakeSamplingPriority)) + ) + } + + @Test + fun `M create extracted context W createExtractedContext()`( + @StringForgery(regex = "[a-f0-9]{32}") fakeTraceId: String, + @StringForgery(regex = "[a-f0-9]{16}") fakeSpanId: String, + @IntForgery fakeSamplingPriority: Int + ) { + // When + val result = testedHelper.createExtractedContext(fakeTraceId, fakeSpanId, fakeSamplingPriority) + + // Then + assertThat(result).isInstanceOf(DatadogSpanContextAdapter::class.java) + assertThat(testedHelper.isExtractedContext(result)).isTrue() + } + + @Test + fun `M return context with correct traceId W createExtractedContext()`( + @StringForgery(regex = "[a-f0-9]{32}") fakeTraceId: String, + @StringForgery(regex = "[a-f0-9]{16}") fakeSpanId: String, + @IntForgery fakeSamplingPriority: Int + ) { + // When + val result = testedHelper.createExtractedContext(fakeTraceId, fakeSpanId, fakeSamplingPriority) + + // Then + assertThat(result.traceId.toHexString()).isEqualTo(fakeTraceId) + } + + @Test + fun `M return context with correct samplingPriority W createExtractedContext()`( + @StringForgery(regex = "[a-f0-9]{32}") fakeTraceId: String, + @StringForgery(regex = "[a-f0-9]{16}") fakeSpanId: String, + @IntForgery fakeSamplingPriority: Int + ) { + // When + val result = testedHelper.createExtractedContext(fakeTraceId, fakeSpanId, fakeSamplingPriority) + + // Then + assertThat(result.samplingPriority).isEqualTo(fakeSamplingPriority) + } + + @Test + fun `M return span context from tag W extractParentContext() {DatadogSpan tag}`() { + // Given + val mockRequest = createMockRequestWithTags(datadogSpan = mockSpan) + whenever( + mockPropagation.extract( + eq(mockRequest), + any< + ( + HttpRequestInfo, + (String, String) -> Boolean + ) -> Unit + >() + ) + ) + .doReturn(null) + + // When + val result = testedHelper.extractParentContext(mockTracer, mockRequest) + + // Then + assertThat(result).isSameAs(mockSpanContext) + } + + @Test + fun `M return extracted header context W extractParentContext() {extracted context from headers}`( + forge: Forge + ) { + // Given + val mockRequest = createMockRequestWithTags() + val extractedContext = ExtractedContext( + forge.getForgery(), + forge.aLong(), + forge.anInt(), + null, + null, + null + ) + val extractedSpanContext = DatadogSpanContextAdapter(extractedContext) + + whenever( + mockPropagation.extract(eq(mockRequest), any<(HttpRequestInfo, (String, String) -> Boolean) -> Unit>()) + ) + .doReturn(extractedSpanContext) + + // When + val result = testedHelper.extractParentContext(mockTracer, mockRequest) + + // Then + assertThat(result).isSameAs(extractedSpanContext) + } + + @Test + fun `M return tag context W extractParentContext() {non-extracted header context}`() { + // Given + val mockRequest = createMockRequestWithTags(datadogSpan = mockSpan) + val nonExtractedContext: DatadogSpanContext = mock() + + whenever( + mockPropagation.extract(eq(mockRequest), any<(HttpRequestInfo, (String, String) -> Boolean) -> Unit>()) + ) + .doReturn(nonExtractedContext) + + // When + val result = testedHelper.extractParentContext(mockTracer, mockRequest) + + // Then + assertThat(result).isSameAs(mockSpanContext) + } + + @Test + fun `M return TraceContext W extractParentContext() {TraceContext tag, no span}`( + @StringForgery(regex = "[a-f0-9]{32}") fakeTraceId: String, + @StringForgery(regex = "[a-f0-9]{16}") fakeSpanId: String, + @IntForgery fakeSamplingPriority: Int + ) { + // Given + val traceContext = TraceContext(fakeTraceId, fakeSpanId, fakeSamplingPriority) + val mockRequest = createMockRequestWithTags(traceContext = traceContext) + + whenever( + mockPropagation.extract(eq(mockRequest), any<(HttpRequestInfo, (String, String) -> Boolean) -> Unit>()) + ) + .doReturn(null) + + // When + val result = testedHelper.extractParentContext(mockTracer, mockRequest) + + // Then + assertThat(result).isNotNull + assertThat(result!!.traceId.toHexString()).isEqualTo(fakeTraceId) + assertThat(result.samplingPriority).isEqualTo(fakeSamplingPriority) + } + + @Test + fun `M return null W extractParentContext() {no tags, no headers}`() { + // Given + val mockRequest = createMockRequestWithTags() + whenever( + mockPropagation.extract( + eq(mockRequest), + any< + ( + HttpRequestInfo, + (String, String) -> Boolean + ) -> Unit + >() + ) + ) + .doReturn(null) + + // When + val result = testedHelper.extractParentContext(mockTracer, mockRequest) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return true W extractSamplingDecision() {Datadog header sampler keep}`() { + // Given + val headers = mapOf( + DatadogHttpCodec.SAMPLING_PRIORITY_KEY to listOf(PrioritySampling.SAMPLER_KEEP.toString()) + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return true W extractSamplingDecision() {Datadog header user keep}`() { + // Given + val headers = mapOf( + DatadogHttpCodec.SAMPLING_PRIORITY_KEY to listOf(PrioritySampling.USER_KEEP.toString()) + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return false W extractSamplingDecision() {Datadog header sampler drop}`() { + // Given + val headers = mapOf( + DatadogHttpCodec.SAMPLING_PRIORITY_KEY to listOf(PrioritySampling.SAMPLER_DROP.toString()) + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return null W extractSamplingDecision() {Datadog header unset}`() { + // Given + val headers = mapOf( + DatadogHttpCodec.SAMPLING_PRIORITY_KEY to listOf(PrioritySampling.UNSET.toString()) + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return true W extractSamplingDecision() {B3Multi header sampled}`() { + // Given + val headers = mapOf( + B3HttpCodec.SAMPLING_PRIORITY_KEY to listOf("1") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return false W extractSamplingDecision() {B3Multi header not sampled}`() { + // Given + val headers = mapOf( + B3HttpCodec.SAMPLING_PRIORITY_KEY to listOf("0") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return null W extractSamplingDecision() {B3Multi header invalid value}`() { + // Given + val headers = mapOf( + B3HttpCodec.SAMPLING_PRIORITY_KEY to listOf("invalid") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return true W extractSamplingDecision() {B3 single header sampled}`() { + // Given + val headers = mapOf( + B3HttpCodec.B3_KEY to listOf("80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return true W extractSamplingDecision() {B3 single header debug}`() { + // Given + val headers = mapOf( + B3HttpCodec.B3_KEY to listOf("80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-d") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return false W extractSamplingDecision() {B3 single header not sampled}`() { + // Given + val headers = mapOf( + B3HttpCodec.B3_KEY to listOf("80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-0") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return false W extractSamplingDecision() {B3 single header deny all}`() { + // Given + val headers = mapOf( + B3HttpCodec.B3_KEY to listOf("0") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return true W extractSamplingDecision() {W3C traceparent sampled}`() { + // Given + val headers = mapOf( + W3CHttpCodec.TRACE_PARENT_KEY to listOf("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return false W extractSamplingDecision() {W3C traceparent not sampled}`() { + // Given + val headers = mapOf( + W3CHttpCodec.TRACE_PARENT_KEY to listOf("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return null W extractSamplingDecision() {W3C traceparent invalid flags}`() { + // Given + val headers = mapOf( + W3CHttpCodec.TRACE_PARENT_KEY to listOf("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-xx") + ) + val mockRequest = createMockRequestWithTags(headers = headers) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return sampled from span W extractSamplingDecision() {DatadogSpan tag with positive priority}`() { + // Given + whenever(mockSpanContext.samplingPriority) doReturn PrioritySampling.SAMPLER_KEEP + val mockRequest = createMockRequestWithTags(datadogSpan = mockSpan) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return not sampled from span W extractSamplingDecision() {DatadogSpan tag with zero priority}`() { + // Given + whenever(mockSpanContext.samplingPriority) doReturn PrioritySampling.SAMPLER_DROP + val mockRequest = createMockRequestWithTags(datadogSpan = mockSpan) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return sampled from TraceContext W extractSamplingDecision() {TraceContext tag positive priority}`() { + // Given + val traceContext = TraceContext("traceId", "spanId", PrioritySampling.USER_KEEP) + val mockRequest = createMockRequestWithTags(traceContext = traceContext) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return not sampled W extractSamplingDecision() {TraceContext tag zero priority}`() { + // Given + val traceContext = TraceContext("traceId", "spanId", PrioritySampling.SAMPLER_DROP) + val mockRequest = createMockRequestWithTags(traceContext = traceContext) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M return null W extractSamplingDecision() {TraceContext tag unset priority}`() { + // Given + val traceContext = TraceContext("traceId", "spanId", PrioritySampling.UNSET) + val mockRequest = createMockRequestWithTags(traceContext = traceContext) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return null W extractSamplingDecision() {no headers, no tags}`() { + // Given + val mockRequest = createMockRequestWithTags() + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M prioritize header over tag W extractSamplingDecision() {both present, different decisions}`() { + // Given + val headers = mapOf( + DatadogHttpCodec.SAMPLING_PRIORITY_KEY to listOf(PrioritySampling.SAMPLER_DROP.toString()) + ) + whenever(mockSpanContext.samplingPriority) doReturn PrioritySampling.SAMPLER_KEEP + val mockRequest = createMockRequestWithTags(headers = headers, datadogSpan = mockSpan) + + // When + val result = testedHelper.extractSamplingDecision(mockRequest) + + // Then + assertThat(result).isFalse() + } + + @Test + fun `M call tracer inject W propagateSampledHeaders()`() { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.DATADOG) + + // When + testedHelper.propagateSampledHeaders(mockRequestModifier, mockTracer, mockSpan, tracingHeaderTypes) + + // Then + verify(mockPropagation).inject(eq(mockSpanContext), eq(mockRequestModifier), any()) + } + + @ParameterizedTest + @MethodSource("datadogHeaderKeys") + fun `M replace Datadog headers W propagateSampledHeaders() {DATADOG header type}`( + headerKey: String + ) { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.DATADOG) + val fakeValue = "test-value" + setupInjectorCallback(headerKey, fakeValue) + + // When + testedHelper.propagateSampledHeaders(mockRequestModifier, mockTracer, mockSpan, tracingHeaderTypes) + + // Then + verify(mockRequestModifier).replaceHeader(headerKey, fakeValue) + } + + @ParameterizedTest + @MethodSource("datadogHeaderKeys") + fun `M remove Datadog headers W propagateSampledHeaders() {no DATADOG header type}`( + headerKey: String + ) { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.B3) + val fakeValue = "test-value" + setupInjectorCallback(headerKey, fakeValue) + + // When + testedHelper.propagateSampledHeaders(mockRequestModifier, mockTracer, mockSpan, tracingHeaderTypes) + + // Then + verify(mockRequestModifier).removeHeader(headerKey) + verify(mockRequestModifier, never()).replaceHeader(eq(headerKey), any()) + } + + @Test + fun `M replace B3 header W propagateSampledHeaders() {B3 header type}`() { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.B3) + val fakeValue = "test-b3-value" + setupInjectorCallback(B3HttpCodec.B3_KEY, fakeValue) + + // When + testedHelper.propagateSampledHeaders(mockRequestModifier, mockTracer, mockSpan, tracingHeaderTypes) + + // Then + verify(mockRequestModifier).replaceHeader(B3HttpCodec.B3_KEY, fakeValue) + } + + @Test + fun `M remove B3 header W propagateSampledHeaders() {no B3 header type}`() { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.DATADOG) + val fakeValue = "test-b3-value" + setupInjectorCallback(B3HttpCodec.B3_KEY, fakeValue) + + // When + testedHelper.propagateSampledHeaders(mockRequestModifier, mockTracer, mockSpan, tracingHeaderTypes) + + // Then + verify(mockRequestModifier).removeHeader(B3HttpCodec.B3_KEY) + } + + @ParameterizedTest + @MethodSource("b3MultiHeaderKeys") + fun `M replace B3Multi headers W propagateSampledHeaders() {B3MULTI header type}`( + headerKey: String + ) { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.B3MULTI) + val fakeValue = "test-value" + setupInjectorCallback(headerKey, fakeValue) + + // When + testedHelper.propagateSampledHeaders(mockRequestModifier, mockTracer, mockSpan, tracingHeaderTypes) + + // Then + verify(mockRequestModifier).replaceHeader(headerKey, fakeValue) + } + + @ParameterizedTest + @MethodSource("w3cHeaderKeys") + fun `M replace W3C headers W propagateSampledHeaders() {TRACECONTEXT header type}`( + headerKey: String + ) { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.TRACECONTEXT) + val fakeValue = "test-value" + setupInjectorCallback(headerKey, fakeValue) + + // When + testedHelper.propagateSampledHeaders(mockRequestModifier, mockTracer, mockSpan, tracingHeaderTypes) + + // Then + verify(mockRequestModifier).replaceHeader(headerKey, fakeValue) + } + + @ParameterizedTest + @MethodSource("w3cHeaderKeys") + fun `M remove W3C headers W propagateSampledHeaders() {no TRACECONTEXT header type}`( + headerKey: String + ) { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.DATADOG) + val fakeValue = "test-value" + setupInjectorCallback(headerKey, fakeValue) + + // When + testedHelper.propagateSampledHeaders(mockRequestModifier, mockTracer, mockSpan, tracingHeaderTypes) + + // Then + verify(mockRequestModifier).removeHeader(headerKey) + } + + @Test + fun `M remove Datadog headers W propagateNotSampledHeaders() {SAMPLED injection type}`() { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.DATADOG) + + // When + testedHelper.propagateNotSampledHeaders( + mockRequestModifier, + mockTracer, + mockSpan, + tracingHeaderTypes, + TraceContextInjection.SAMPLED, + null + ) + + // Then + DatadogPropagationHelper.DATADOG_CODEC_HEADERS.forEach { headerKey -> + verify(mockRequestModifier).removeHeader(headerKey) + } + } + + @Test + fun `M remove B3 header W propagateNotSampledHeaders() {SAMPLED injection type}`() { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.B3) + + // When + testedHelper.propagateNotSampledHeaders( + mockRequestModifier, + mockTracer, + mockSpan, + tracingHeaderTypes, + TraceContextInjection.SAMPLED, + null + ) + + // Then + verify(mockRequestModifier).removeHeader(B3HttpCodec.B3_KEY) + } + + @Test + fun `M remove B3Multi headers W propagateNotSampledHeaders() {SAMPLED injection type}`() { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.B3MULTI) + + // When + testedHelper.propagateNotSampledHeaders( + mockRequestModifier, + mockTracer, + mockSpan, + tracingHeaderTypes, + TraceContextInjection.SAMPLED, + null + ) + + // Then + DatadogPropagationHelper.B3M_CODEC_HEADERS.forEach { headerKey -> + verify(mockRequestModifier).removeHeader(headerKey) + } + } + + @Test + fun `M remove W3C headers W propagateNotSampledHeaders() {SAMPLED injection type}`() { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.TRACECONTEXT) + + // When + testedHelper.propagateNotSampledHeaders( + mockRequestModifier, + mockTracer, + mockSpan, + tracingHeaderTypes, + TraceContextInjection.SAMPLED, + null + ) + + // Then + DatadogPropagationHelper.W3C_CODEC_HEADERS.forEach { headerKey -> + verify(mockRequestModifier).removeHeader(headerKey) + } + } + + @Test + fun `M add drop sampling B3 header W propagateNotSampledHeaders() {ALL injection type, B3}`() { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.B3) + + // When + testedHelper.propagateNotSampledHeaders( + mockRequestModifier, + mockTracer, + mockSpan, + tracingHeaderTypes, + TraceContextInjection.ALL, + null + ) + + // Then + verify(mockRequestModifier).addHeader(B3HttpCodec.B3_KEY, DatadogPropagationHelper.B3_DROP_SAMPLING_DECISION) + } + + @Test + fun `M add drop sampling B3Multi header W propagateNotSampledHeaders() {ALL injection type, B3MULTI}`() { + // Given + val tracingHeaderTypes = setOf(TracingHeaderType.B3MULTI) + + // When + testedHelper.propagateNotSampledHeaders( + mockRequestModifier, + mockTracer, + mockSpan, + tracingHeaderTypes, + TraceContextInjection.ALL, + null + ) + + // Then + verify(mockRequestModifier).addHeader( + B3HttpCodec.SAMPLING_PRIORITY_KEY, + DatadogPropagationHelper.B3M_DROP_SAMPLING_DECISION + ) + } + + private fun createMockRequestWithTags( + headers: Map> = emptyMap(), + datadogSpan: DatadogSpan? = null, + traceContext: TraceContext? = null + ): HttpRequestInfo { + return mock { + on { this.headers } doReturn headers + on { tag(DatadogSpan::class.java) } doReturn datadogSpan + on { tag(TraceContext::class.java) } doReturn traceContext + } + } + + private fun setupInjectorCallback(expectedKey: String, expectedValue: String) { + whenever( + mockPropagation.inject( + eq(mockSpanContext), + eq(mockRequestModifier), + any<(HttpRequestInfoBuilder, String, String) -> Unit>() + ) + ).doAnswer { invocation -> + @Suppress("UNCHECKED_CAST") + val injector = invocation.arguments[2] as (HttpRequestInfoBuilder, String, String) -> Unit + injector(mockRequestModifier, expectedKey, expectedValue) + } + } + + private interface HttpRequestInfoWithTags : HttpRequestInfo, ExtendedRequestInfo + + companion object { + @JvmStatic + fun datadogHeaderKeys(): Stream = Stream.of( + Arguments.of(DatadogHttpCodec.ORIGIN_KEY), + Arguments.of(DatadogHttpCodec.SPAN_ID_KEY), + Arguments.of(DatadogHttpCodec.TRACE_ID_KEY), + Arguments.of(DatadogHttpCodec.DATADOG_TAGS_KEY), + Arguments.of(DatadogHttpCodec.SAMPLING_PRIORITY_KEY) + ) + + @JvmStatic + fun b3MultiHeaderKeys(): Stream = Stream.of( + Arguments.of(B3HttpCodec.SPAN_ID_KEY), + Arguments.of(B3HttpCodec.TRACE_ID_KEY), + Arguments.of(B3HttpCodec.SAMPLING_PRIORITY_KEY) + ) + + @JvmStatic + fun w3cHeaderKeys(): Stream = Stream.of( + Arguments.of(W3CHttpCodec.TRACE_PARENT_KEY), + Arguments.of(W3CHttpCodec.TRACE_STATE_KEY) + ) + } +} diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/DatadogTracingToolkitTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/DatadogTracingToolkitTest.kt new file mode 100644 index 0000000000..625414634d --- /dev/null +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/DatadogTracingToolkitTest.kt @@ -0,0 +1,82 @@ +/* + * 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.trace.internal + +import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration +import com.datadog.android.trace.TracingHeaderType +import com.datadog.android.utils.forge.Configurator +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.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogTracingToolkitTest { + + private lateinit var fakeTracedHosts: Map> + + @StringForgery + lateinit var fakeInstrumentationName: String + + @BeforeEach + fun `set up`(forge: Forge) { + fakeTracedHosts = forge.aMap { + aStringMatching("[a-z]+\\.[a-z]{2,3}") to forge.aList { + aValueFrom(TracingHeaderType::class.java) + }.toSet() + } + } + + @Test + fun `M build TracingInstrumentation W build() extension`() { + // Given + val builder = ApmNetworkInstrumentationConfiguration(fakeTracedHosts) + + // When + val result: ApmNetworkInstrumentation = with(DatadogTracingToolkit) { + DatadogTracingToolkit.createApmNetworkInstrumentation(fakeInstrumentationName, builder) + } + + // Then + assertThat(result).isNotNull + assertThat(result.sdkInstanceName).isNull() + assertThat(result.traceOrigin).isNull() + } + + @Test + fun `M build TracingInstrumentation with configured values W build() extension`( + @StringForgery fakeSdkInstanceName: String, + @StringForgery fakeTraceOrigin: String + ) { + // Given + val builder = ApmNetworkInstrumentationConfiguration(fakeTracedHosts) + .setSdkInstanceName(fakeSdkInstanceName) + .setTraceOrigin(fakeTraceOrigin) + + // When + val result: ApmNetworkInstrumentation = + DatadogTracingToolkit.createApmNetworkInstrumentation(fakeInstrumentationName, builder) + + // Then + assertThat(result).isNotNull + assertThat(result.sdkInstanceName).isEqualTo(fakeSdkInstanceName) + assertThat(result.traceOrigin).isEqualTo(fakeTraceOrigin) + } +} diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/ApmInstrumentationConfigurationTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/ApmInstrumentationConfigurationTest.kt new file mode 100644 index 0000000000..d6d2f5ec48 --- /dev/null +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/ApmInstrumentationConfigurationTest.kt @@ -0,0 +1,254 @@ +/* + * 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.trace.internal.net + +import com.datadog.android.api.SdkCore +import com.datadog.android.core.sampling.Sampler +import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration +import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration.Companion.createInstrumentation +import com.datadog.android.trace.ApmNetworkTracingScope +import com.datadog.android.trace.DeterministicTraceSampler +import com.datadog.android.trace.NetworkTracedRequestListener +import com.datadog.android.trace.TraceContextInjection +import com.datadog.android.trace.TracingHeaderType +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.BoolForgery +import fr.xgouchet.elmyr.annotation.FloatForgery +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.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) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ApmInstrumentationConfigurationTest { + + private lateinit var testedBuilder: ApmNetworkInstrumentationConfiguration + + private lateinit var fakeTracedHosts: Map> + + @Mock + lateinit var mockNetworkTracedRequestListener: NetworkTracedRequestListener + + @Mock + lateinit var mockTraceSampler: Sampler + + @Mock + lateinit var mockGlobalTracer: DatadogTracer + + @Mock + lateinit var mockLocalTracer: DatadogTracer + + @StringForgery + lateinit var fakeNetworkLibraryName: String + + @BeforeEach + fun `set up`(forge: Forge) { + fakeTracedHosts = forge.aMap { + aStringMatching("[a-z]+\\.[a-z]{2,3}") to forge.aList { + aValueFrom(TracingHeaderType::class.java) + }.toSet() + } + testedBuilder = ApmNetworkInstrumentationConfiguration(fakeTracedHosts) + } + + @Test + fun `M build with default values W build()`() { + // When + val result = testedBuilder.createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.sdkInstanceName).isNull() + assertThat(result.traceOrigin).isNull() + assertThat(result.redacted404ResourceName).isTrue() + assertThat(result.injectionType).isEqualTo(TraceContextInjection.SAMPLED) + } + + @Test + fun `M set trace origin W setTraceOrigin()`( + @StringForgery fakeTraceOrigin: String + ) { + // When + val result = testedBuilder.setTraceOrigin(fakeTraceOrigin).createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.traceOrigin).isEqualTo(fakeTraceOrigin) + } + + @Test + fun `M set SDK instance name W setSdkInstanceName()`( + @StringForgery fakeSdkInstanceName: String + ) { + // When + val result = testedBuilder.setSdkInstanceName(fakeSdkInstanceName).createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.sdkInstanceName).isEqualTo(fakeSdkInstanceName) + } + + @Test + fun `M set traced request listener W setTracedRequestListener()`() { + // When + val result = testedBuilder.setTracedRequestListener(mockNetworkTracedRequestListener) + .createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.tracedRequestListener).isSameAs(mockNetworkTracedRequestListener) + } + + @Test + fun `M set trace sample rate W setTraceSampleRate()`( + @FloatForgery(min = 0f, max = 100f) fakeSampleRate: Float + ) { + // When + val result = testedBuilder.setTraceSampleRate(fakeSampleRate).createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.traceSampler).isInstanceOf(DeterministicTraceSampler::class.java) + assertThat(result.traceSampler.getSampleRate()).isEqualTo(fakeSampleRate) + } + + @Test + fun `M set trace sampler W setTraceSampler()`() { + // When + val result = testedBuilder.setTraceSampler(mockTraceSampler).createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.traceSampler).isSameAs(mockTraceSampler) + } + + @Test + fun `M set trace context injection W setTraceContextInjection() {ALL}`() { + // When + val result = testedBuilder.setTraceContextInjection(TraceContextInjection.ALL) + .createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.injectionType).isEqualTo(TraceContextInjection.ALL) + } + + @Test + fun `M set trace context injection W setTraceContextInjection() {SAMPLED}`() { + // When + val result = testedBuilder.setTraceContextInjection(TraceContextInjection.SAMPLED) + .createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.injectionType).isEqualTo(TraceContextInjection.SAMPLED) + } + + @Test + fun `M set redacted 404 resource name W set404ResourcesRedacted()`( + @BoolForgery fakeRedacted: Boolean + ) { + // When + val result = testedBuilder.set404ResourcesRedacted(fakeRedacted).createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.redacted404ResourceName).isEqualTo(fakeRedacted) + } + + @Test + fun `M return self W chaining builder methods()`( + @StringForgery fakeTraceOrigin: String, + @StringForgery fakeSdkInstanceName: String, + @FloatForgery(min = 0f, max = 100f) fakeSampleRate: Float, + @BoolForgery fakeRedacted: Boolean + ) { + // When + val result = testedBuilder + .setTraceOrigin(fakeTraceOrigin) + .setSdkInstanceName(fakeSdkInstanceName) + .setTracedRequestListener(mockNetworkTracedRequestListener) + .setTraceSampleRate(fakeSampleRate) + .setTraceContextInjection(TraceContextInjection.ALL) + .set404ResourcesRedacted(fakeRedacted) + .setTracingScope(ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY) + + // Then + assertThat(result).isSameAs(testedBuilder) + } + + @Test + fun `M set local tracer factory W setLocalTracerFactory()`() { + // Given + val fakeFactory: (SdkCore, Set) -> DatadogTracer = { _, _ -> mockLocalTracer } + + // When + val result = testedBuilder.setLocalTracerFactory(fakeFactory) + + // Then + assertThat(result).isSameAs(testedBuilder) + assertThat(testedBuilder.localTracerFactory).isSameAs(fakeFactory) + } + + @Test + fun `M set global tracer provider W setGlobalTracerProvider()`() { + // Given + val fakeProvider: () -> DatadogTracer? = { mockGlobalTracer } + + // When + val result = testedBuilder.setGlobalTracerProvider(fakeProvider) + + // Then + assertThat(result).isSameAs(testedBuilder) + assertThat(testedBuilder.globalTracerProvider === fakeProvider).isTrue() + } + + @Test + fun `M sanitize hosts W build() {valid and invalid hosts}`() { + // Given + val validHost = "example.com" + val invalidHost = "not a valid host!" + val mixedHosts = mapOf( + validHost to setOf(TracingHeaderType.DATADOG), + invalidHost to setOf(TracingHeaderType.TRACECONTEXT) + ) + testedBuilder = ApmNetworkInstrumentationConfiguration(mixedHosts) + + // When + val result = testedBuilder.createInstrumentation(fakeNetworkLibraryName) + + // Then + assertThat(result.localFirstPartyHostHeaderTypeResolver.isFirstPartyUrl("https://$validHost/path")).isTrue() + } + + @Test + fun `M set APPLICATION_LEVEL_REQUESTS_ONLY scope W setTracingScope()`() { + // When + val result = testedBuilder.setTracingScope(ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY) + + // Then + assertThat(result).isSameAs(testedBuilder) + assertThat(testedBuilder.networkTracingScope).isEqualTo(ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY) + } + + @Test + fun `M set DETAILED scope W setTracingScope()`() { + // When + val result = testedBuilder.setTracingScope(ApmNetworkTracingScope.DETAILED) + + // Then + assertThat(result).isSameAs(testedBuilder) + assertThat(testedBuilder.networkTracingScope).isEqualTo(ApmNetworkTracingScope.DETAILED) + } +} diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/ApmNetworkInstrumentationTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/ApmNetworkInstrumentationTest.kt new file mode 100644 index 0000000000..6bbfed67aa --- /dev/null +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/ApmNetworkInstrumentationTest.kt @@ -0,0 +1,956 @@ +/* + * 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.trace.internal.net + +import com.datadog.android.Datadog +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.SdkCore +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.api.instrumentation.network.HttpResponseInfo +import com.datadog.android.api.instrumentation.network.MutableHttpRequestInfo +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver +import com.datadog.android.core.sampling.Sampler +import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration +import com.datadog.android.trace.ApmNetworkTracingScope +import com.datadog.android.trace.NetworkTracedRequestListener +import com.datadog.android.trace.TraceContextInjection +import com.datadog.android.trace.TracingHeaderType +import com.datadog.android.trace.api.DatadogTracingConstants.Tags +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.span.DatadogSpanBuilder +import com.datadog.android.trace.api.span.DatadogSpanContext +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.trace.api.withMockPropagationHelper +import com.datadog.android.trace.internal.ApmNetworkInstrumentation +import com.datadog.android.trace.internal.DatadogPropagationHelper +import com.datadog.android.trace.internal.DatadogTracingToolkit +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.FloatForgery +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +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.AfterEach +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.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.IOException +import java.net.HttpURLConnection + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class ApmNetworkInstrumentationTest { + + private lateinit var testedInstrumentation: ApmNetworkInstrumentation + + @Mock + lateinit var mockSdkCore: InternalSdkCore + + @Mock + lateinit var mockTracerProvider: TracerProvider + + @Mock + lateinit var mockTracer: DatadogTracer + + @Mock + lateinit var mockSpan: DatadogSpan + + @Mock + lateinit var mockSpanBuilder: DatadogSpanBuilder + + @Mock + lateinit var mockSpanContext: DatadogSpanContext + + @Mock + lateinit var mockTraceSampler: Sampler + + @Mock + lateinit var mockNetworkTracedRequestListener: NetworkTracedRequestListener + + @Mock + lateinit var mockLocalFirstPartyHostResolver: DefaultFirstPartyHostHeaderTypeResolver + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockTracingFeature: FeatureScope + + @Mock + lateinit var mockRumFeature: FeatureScope + + @Mock(extraInterfaces = [MutableHttpRequestInfo::class]) + lateinit var mockRequestInfo: HttpRequestInfo + + @Mock + lateinit var mockRequestBuilder: HttpRequestInfoBuilder + + @Mock + lateinit var mockResponseInfo: HttpResponseInfo + + @Mock + lateinit var mockPropagationHelper: DatadogPropagationHelper + + @StringForgery + lateinit var fakeNetworkInstrumentationName: String + + @StringForgery(regex = "https://[a-z]+\\.[a-z]{2,3}/[a-z]+") + lateinit var fakeUrl: String + + @StringForgery + lateinit var fakeMethod: String + + @FloatForgery(min = 0f, max = 100f) + var fakeSampleRate: Float = 0f + + private lateinit var fakeTracedHosts: Map> + + @BeforeEach + fun `set up`(forge: Forge) { + fakeTracedHosts = forge.aMap { + aStringMatching("[a-z]+\\.[a-z]{2,3}") to forge.aList { + aValueFrom(TracingHeaderType::class.java) + }.toSet() + } + + datadogRegistryRegisterMethod.invoke(datadogRegistryField.get(null), null, mockSdkCore) + + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn mockTracingFeature + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn mockRumFeature + whenever(mockSdkCore.firstPartyHostResolver) doReturn mock() + + whenever(mockRequestInfo.url) doReturn fakeUrl + whenever(mockRequestInfo.method) doReturn fakeMethod + whenever(mockRequestInfo.headers) doReturn emptyMap() + whenever((mockRequestInfo as MutableHttpRequestInfo).newBuilder()) doReturn mockRequestBuilder + whenever(mockRequestBuilder.build()) doReturn mockRequestInfo + + whenever(mockSpanBuilder.withOrigin(anyOrNull())) doReturn mockSpanBuilder + whenever(mockSpanBuilder.withParentContext(anyOrNull())) doReturn mockSpanBuilder + whenever(mockSpanBuilder.start()) doReturn mockSpan + + whenever(mockSpan.context()) doReturn mockSpanContext + whenever(mockSpan.samplingPriority) doReturn null + whenever(mockSpan.isRootSpan) doReturn true + + whenever(mockTraceSampler.sample(mockSpan)) doReturn true + whenever(mockTraceSampler.getSampleRate()) doReturn fakeSampleRate + + whenever(mockTracer.buildSpan(any())) doReturn mockSpanBuilder + + whenever( + mockTracerProvider.provideTracer( + any(), + any(), + any() + ) + ) doReturn mockTracer + + whenever(mockLocalFirstPartyHostResolver.isFirstPartyUrl(fakeUrl)) doReturn true + whenever(mockLocalFirstPartyHostResolver.headerTypesForUrl(fakeUrl)) doReturn setOf(TracingHeaderType.DATADOG) + whenever(mockLocalFirstPartyHostResolver.getAllHeaderTypes()) doReturn setOf(TracingHeaderType.DATADOG) + whenever(mockLocalFirstPartyHostResolver.isEmpty()) doReturn false + + // Set up mock propagation helper to return null for sampling decision (falls through to sampler) + whenever(mockPropagationHelper.extractSamplingDecision(any())) doReturn null + + testedInstrumentation = ApmNetworkInstrumentation( + sdkInstanceName = null, + traceOrigin = null, + tracerProvider = mockTracerProvider, + redacted404ResourceName = true, + traceSampler = mockTraceSampler, + injectionType = TraceContextInjection.ALL, + tracedRequestListener = mockNetworkTracedRequestListener, + localFirstPartyHostHeaderTypeResolver = mockLocalFirstPartyHostResolver, + networkingLibraryName = fakeNetworkInstrumentationName, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + } + + @AfterEach + fun `tear down`() { + datadogRegistryClearMethod.invoke(datadogRegistryField.get(null)) + } + + // region onRequest + + @Test + fun `M return RequestTraceState with span W onRequest() {traceable first party url}`() { + // Given + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + val result = checkNotNull(testedInstrumentation.onRequest(mockRequestInfo)) + + // Then + assertThat(result.span).isEqualTo(mockSpan) + assertThat(result.isSampled).isTrue() + assertThat(result.sampleRate).isEqualTo(fakeSampleRate) + assertThat(result.networkTracingScope).isEqualTo(ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY) + } + } + + @Test + fun `M set span resource name W onRequest()`() { + // Given + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onRequest(mockRequestInfo) + + // Then + verify(mockSpan).resourceName = fakeUrl.substringBefore('?') + } + } + + @Test + fun `M set span tags W onRequest()`() { + // Given + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onRequest(mockRequestInfo) + + // Then + verify(mockSpan).setTag(Tags.KEY_HTTP_URL, fakeUrl) + verify(mockSpan).setTag(Tags.KEY_HTTP_METHOD, fakeMethod) + verify(mockSpan).setTag(Tags.KEY_SPAN_KIND, Tags.VALUE_SPAN_KIND_CLIENT) + } + } + + @Test + fun `M return RequestTraceState without span W onRequest() {not first party url}`() { + // Given + whenever(mockLocalFirstPartyHostResolver.isFirstPartyUrl(fakeUrl)) doReturn false + whenever(mockSdkCore.firstPartyHostResolver.isFirstPartyUrl(fakeUrl)) doReturn false + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + val result = checkNotNull(testedInstrumentation.onRequest(mockRequestInfo)) + + // Then + assertThat(result.span).isNull() + assertThat(result.isSampled).isFalse() + } + } + + @Test + fun `M return RequestTraceState with span W onRequest() {APPLICATION_LEVEL_REQUESTS_ONLY scope}`() { + // Given + testedInstrumentation = ApmNetworkInstrumentation( + sdkInstanceName = null, + traceOrigin = null, + tracerProvider = mockTracerProvider, + redacted404ResourceName = true, + traceSampler = mockTraceSampler, + injectionType = TraceContextInjection.ALL, + tracedRequestListener = mockNetworkTracedRequestListener, + localFirstPartyHostHeaderTypeResolver = mockLocalFirstPartyHostResolver, + networkingLibraryName = fakeNetworkInstrumentationName, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + val result = checkNotNull(testedInstrumentation.onRequest(mockRequestInfo)) + + // Then + assertThat(result.span).isNotNull() + assertThat(result.isSampled).isTrue() + } + } + + @Test + fun `M return RequestTraceState without span W onRequest() {tracer not available}`() { + // Given + whenever(mockTracerProvider.provideTracer(any(), any(), any())) doReturn null + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + val result = checkNotNull(testedInstrumentation.onRequest(mockRequestInfo)) + + // Then + assertThat(result.span).isNull() + assertThat(result.isSampled).isFalse() + } + } + + @Test + fun `M return not sampled state W onRequest() {span not sampled}`() { + // Given + whenever(mockTraceSampler.sample(mockSpan)) doReturn false + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + val result = checkNotNull(testedInstrumentation.onRequest(mockRequestInfo)) + + // Then + assertThat(result.span).isEqualTo(mockSpan) + assertThat(result.isSampled).isFalse() + } + } + + @Test + fun `M propagate sampled headers W onRequest() {sampled}`() { + // Given + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onRequest(mockRequestInfo) + + // Then + verify(mockPropagationHelper).propagateSampledHeaders( + eq(mockRequestBuilder), + eq(mockTracer), + eq(mockSpan), + any() + ) + } + } + + @Test + fun `M propagate not sampled headers W onRequest() {not sampled}`() { + // Given + whenever(mockTraceSampler.sample(mockSpan)) doReturn false + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onRequest(mockRequestInfo) + + // Then + verify(mockPropagationHelper).propagateNotSampledHeaders( + eq(mockRequestBuilder), + eq(mockTracer), + eq(mockSpan), + any(), + eq(TraceContextInjection.ALL), + anyOrNull() + ) + } + } + + @Test + fun `M use global first party resolver W onRequest() {local resolver has no headers}`() { + // Given + val mockGlobalResolver: DefaultFirstPartyHostHeaderTypeResolver = mock() + whenever(mockLocalFirstPartyHostResolver.headerTypesForUrl(fakeUrl)) doReturn emptySet() + whenever(mockSdkCore.firstPartyHostResolver) doReturn mockGlobalResolver + whenever(mockGlobalResolver.headerTypesForUrl(fakeUrl)) doReturn setOf(TracingHeaderType.TRACECONTEXT) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onRequest(mockRequestInfo) + + // Then + verify(mockGlobalResolver).headerTypesForUrl(fakeUrl) + } + } + + // endregion + + // region onResponseSucceeded + + @Test + fun `M set http status code tag W onResponseSucceeded()`( + @IntForgery(min = 200, max = 299) fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockSpan).setTag(Tags.KEY_HTTP_STATUS, fakeStatusCode) + } + } + + @ParameterizedTest + @ValueSource(ints = [400, 401, 403, 404, 499]) + fun `M mark span as error W onResponseSucceeded() {4xx status code}`( + fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockSpan).isError = true + } + } + + @ParameterizedTest + @ValueSource(ints = [200, 201, 204, 301, 302, 500, 502, 503]) + fun `M not mark span as error W onResponseSucceeded() {non-4xx status code}`( + fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockSpan, never()).isError = true + } + } + + @Test + fun `M redact 404 resource name W onResponseSucceeded() {404 status code, redaction enabled}`() { + // Given + whenever(mockResponseInfo.statusCode) doReturn HttpURLConnection.HTTP_NOT_FOUND + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockSpan).resourceName = ApmNetworkInstrumentation.RESOURCE_NAME_404 + } + } + + @Test + fun `M not redact 404 resource name W onResponseSucceeded() {404 status code, redaction disabled}`() { + // Given + testedInstrumentation = ApmNetworkInstrumentation( + sdkInstanceName = null, + traceOrigin = null, + tracerProvider = mockTracerProvider, + redacted404ResourceName = false, + traceSampler = mockTraceSampler, + injectionType = TraceContextInjection.ALL, + tracedRequestListener = mockNetworkTracedRequestListener, + localFirstPartyHostHeaderTypeResolver = mockLocalFirstPartyHostResolver, + networkingLibraryName = fakeNetworkInstrumentationName, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + whenever(mockResponseInfo.statusCode) doReturn HttpURLConnection.HTTP_NOT_FOUND + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockSpan, never()).resourceName = ApmNetworkInstrumentation.RESOURCE_NAME_404 + } + } + + @Test + fun `M call traced request listener W onResponseSucceeded() {sampled}`( + @IntForgery(min = 200, max = 299) fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockNetworkTracedRequestListener).onRequestIntercepted( + mockRequestInfo, + mockSpan, + mockResponseInfo, + null + ) + } + } + + @Test + fun `M not call traced request listener W onResponseSucceeded() {not sampled}`( + @IntForgery(min = 200, max = 299) fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = false, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verifyNoInteractions(mockNetworkTracedRequestListener) + } + } + + @Test + fun `M finish span W onResponseSucceeded() {sampled, RUM disabled}`( + @IntForgery(min = 200, max = 299) fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockSpan).finish() + } + } + + @Test + fun `M drop span W onResponseSucceeded() {not sampled, RUM disabled}`( + @IntForgery(min = 200, max = 299) fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = false, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockSpan).drop() + } + } + + // endregion + + // region onResponseFailed + + @Test + fun `M mark span as error W onResponseFailed()`( + @Forgery fakeThrowable: Throwable + ) { + // Given + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseFailed(traceState, fakeThrowable) + + // Then + verify(mockSpan).isError = true + } + } + + @Test + fun `M set error tags W onResponseFailed()`( + @StringForgery fakeErrorMessage: String + ) { + // Given + val fakeThrowable = IOException(fakeErrorMessage) + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseFailed(traceState, fakeThrowable) + + // Then + verify(mockSpan).setTag(Tags.KEY_ERROR_MSG, fakeErrorMessage) + verify(mockSpan).setTag(Tags.KEY_ERROR_TYPE, IOException::class.java.name) + verify(mockSpan).setTag(eq(Tags.KEY_ERROR_STACK), any()) + } + } + + @Test + fun `M call traced request listener W onResponseFailed() {sampled}`( + @Forgery fakeThrowable: Throwable + ) { + // Given + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseFailed(traceState, fakeThrowable) + + // Then + verify(mockNetworkTracedRequestListener).onRequestIntercepted( + mockRequestInfo, + mockSpan, + null, + fakeThrowable + ) + } + } + + @Test + fun `M not call traced request listener W onResponseFailed() {not sampled}`( + @Forgery fakeThrowable: Throwable + ) { + // Given + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = false, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseFailed(traceState, fakeThrowable) + + // Then + verifyNoInteractions(mockNetworkTracedRequestListener) + } + } + + @Test + fun `M not set error tags W onResponseFailed() {not sampled}`( + @Forgery fakeThrowable: Throwable + ) { + // Given + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = false, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseFailed(traceState, fakeThrowable) + + // Then + verify(mockSpan, never()).isError = true + verify(mockSpan, never()).setTag(eq(Tags.KEY_ERROR_MSG), any()) + verify(mockSpan, never()).setTag(eq(Tags.KEY_ERROR_TYPE), any()) + verify(mockSpan, never()).setTag(eq(Tags.KEY_ERROR_STACK), any()) + } + } + + @Test + fun `M finish span W onResponseFailed() {sampled, RUM disabled}`( + @Forgery fakeThrowable: Throwable + ) { + // Given + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseFailed(traceState, fakeThrowable) + + // Then + verify(mockSpan).finish() + } + } + + @Test + fun `M drop span W onResponseFailed() {not sampled, RUM disabled}`( + @Forgery fakeThrowable: Throwable + ) { + // Given + whenever(mockSdkCore.getFeature(Feature.RUM_FEATURE_NAME)) doReturn null + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = false, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseFailed(traceState, fakeThrowable) + + // Then + verify(mockSpan).drop() + } + } + + // endregion + + // region internalSdkCore null scenarios + + @Test + fun `M return RequestTraceState without span W onRequest() { sdkCore not registered }`() { + // Given + datadogRegistryClearMethod.invoke(datadogRegistryField.get(null)) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + val result = checkNotNull(testedInstrumentation.onRequest(mockRequestInfo)) + + // Then + assertThat(result.span).isNull() + assertThat(result.isSampled).isFalse() + verifyNoInteractions(mockTracerProvider) + } + } + + @Test + fun `M finish span W onResponseSucceeded() { sdkCore not registered, sampled }`( + @IntForgery(min = 200, max = 299) fakeStatusCode: Int + ) { + // Given + // When sdkCore is null, isRumEnabled returns false + // Since !isRumEnabled = true, canSendSpan = true + // With isSampled = true, span.finish() should be called + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + datadogRegistryClearMethod.invoke(datadogRegistryField.get(null)) + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockSpan).finish() + } + } + + @Test + fun `M drop span W onResponseSucceeded() { sdkCore not registered, not sampled }`( + @IntForgery(min = 200, max = 299) fakeStatusCode: Int + ) { + // Given + whenever(mockResponseInfo.statusCode) doReturn fakeStatusCode + datadogRegistryClearMethod.invoke(datadogRegistryField.get(null)) + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = false, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseSucceeded(traceState, mockResponseInfo) + + // Then + verify(mockSpan).drop() + } + } + + @Test + fun `M finish span W onResponseFailed() { sdkCore not registered, sampled }`( + @Forgery fakeThrowable: Throwable + ) { + // Given + datadogRegistryClearMethod.invoke(datadogRegistryField.get(null)) + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = true, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseFailed(traceState, fakeThrowable) + + // Then + verify(mockSpan).finish() + } + } + + @Test + fun `M drop span W onResponseFailed() { sdkCore not registered, not sampled }`( + @Forgery fakeThrowable: Throwable + ) { + // Given + datadogRegistryClearMethod.invoke(datadogRegistryField.get(null)) + + val traceState = RequestTraceState( + requestBuilder = mockRequestBuilder, + isSampled = false, + span = mockSpan, + sampleRate = fakeSampleRate, + networkTracingScope = ApmNetworkTracingScope.APPLICATION_LEVEL_REQUESTS_ONLY + ) + + DatadogTracingToolkit.withMockPropagationHelper(mockPropagationHelper) { + // When + testedInstrumentation.onResponseFailed(traceState, fakeThrowable) + + // Then + verify(mockSpan).drop() + } + } + + // endregion + + // region Builder companion methods + + @Test + fun `M create builder W Builder() {with hosts map}`(forge: Forge) { + // Given + val fakeHosts = forge.aMap { + forge.aStringMatching("[a-z]+\\.[a-z]{2,3}") to forge.aList { + aValueFrom(TracingHeaderType::class.java) + }.toSet() + } + + // When + val builder = ApmNetworkInstrumentationConfiguration(fakeHosts) + + // Then + assertThat(builder).isInstanceOf(ApmNetworkInstrumentationConfiguration::class.java) + } + + @Test + fun `M create builder with default headers W Builder() {with hosts list}`(forge: Forge) { + // Given + val fakeHosts = forge.aList { forge.aStringMatching("[a-z]+\\.[a-z]{2,3}") } + + // When + val builder = ApmNetworkInstrumentationConfiguration(fakeHosts) + + // Then + assertThat(builder).isInstanceOf(ApmNetworkInstrumentationConfiguration::class.java) + } + + // endregion + + companion object Companion { + private val datadogRegistryField = Datadog::class.java.getDeclaredField("registry").apply { + isAccessible = true + } + private val datadogRegistryRegisterMethod = datadogRegistryField.type.getMethod( + "register", + String::class.java, + SdkCore::class.java + ) + private val datadogRegistryClearMethod = datadogRegistryField.type.getMethod("clear") + } +} diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/DeterministicTraceSamplerTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/DeterministicTraceSamplerTest.kt new file mode 100644 index 0000000000..9bf98da18a --- /dev/null +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/DeterministicTraceSamplerTest.kt @@ -0,0 +1,205 @@ +/* + * 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.trace.internal.net + +import com.datadog.android.trace.DeterministicTraceSampler +import com.datadog.android.trace.api.span.DatadogSpan +import com.datadog.android.trace.api.span.DatadogSpanContext +import com.datadog.android.trace.api.trace.DatadogTraceId +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.FloatForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.data.Offset +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.RepeatedTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DeterministicTraceSamplerTest { + + private lateinit var testedSampler: DeterministicTraceSampler + + @Mock + lateinit var mockSpanContext: DatadogSpanContext + + private lateinit var fakeSpans: List + + @BeforeEach + fun `set up`(forge: Forge) { + val listSize = forge.anInt(256, 1024) + fakeSpans = forge.aList(listSize) { + val traceId = mock { + on { toLong() } doReturn aLong() + } + val context = mock { + on { this.traceId } doReturn traceId + on { tags } doReturn emptyMap() + } + mock { + on { context() } doReturn context + } + } + } + + @RepeatedTest(32) + fun `M sample spans based on rate W sample() {float constructor}`( + @FloatForgery(min = 0f, max = 100f) fakeSampleRate: Float + ) { + // Given + testedSampler = DeterministicTraceSampler(fakeSampleRate) + var sampledIn = 0 + + // When + fakeSpans.forEach { + if (testedSampler.sample(it)) { + sampledIn++ + } + } + + // Then + val offset = 2.5f * fakeSpans.size + assertThat(sampledIn.toFloat()).isCloseTo( + fakeSpans.size * fakeSampleRate / 100f, + Offset.offset(offset) + ) + } + + @RepeatedTest(32) + fun `M sample spans based on rate W sample() {double constructor}`( + @FloatForgery(min = 0f, max = 100f) fakeSampleRate: Float + ) { + // Given + testedSampler = DeterministicTraceSampler(fakeSampleRate.toDouble()) + var sampledIn = 0 + + // When + fakeSpans.forEach { + if (testedSampler.sample(it)) { + sampledIn++ + } + } + + // Then + val offset = 2.5f * fakeSpans.size + assertThat(sampledIn.toFloat()).isCloseTo( + fakeSpans.size * fakeSampleRate / 100f, + Offset.offset(offset) + ) + } + + @RepeatedTest(32) + fun `M sample spans based on rate W sample() {provider constructor}`( + @FloatForgery(min = 0f, max = 100f) fakeSampleRate: Float + ) { + // Given + testedSampler = DeterministicTraceSampler { fakeSampleRate } + var sampledIn = 0 + + // When + fakeSpans.forEach { + if (testedSampler.sample(it)) { + sampledIn++ + } + } + + // Then + val offset = 2.5f * fakeSpans.size + assertThat(sampledIn.toFloat()).isCloseTo( + fakeSpans.size * fakeSampleRate / 100f, + Offset.offset(offset) + ) + } + + @Test + fun `M drop all spans W sample() {rate is 0}`() { + // Given + testedSampler = DeterministicTraceSampler(0f) + var sampledIn = 0 + + // When + fakeSpans.forEach { + if (testedSampler.sample(it)) { + sampledIn++ + } + } + + // Then + assertThat(sampledIn).isEqualTo(0) + } + + @Test + fun `M keep all spans W sample() {rate is 100}`() { + // Given + testedSampler = DeterministicTraceSampler(100f) + var sampledIn = 0 + + // When + fakeSpans.forEach { + if (testedSampler.sample(it)) { + sampledIn++ + } + } + + // Then + assertThat(sampledIn).isEqualTo(fakeSpans.size) + } + + @Test + fun `M return sample rate W getSampleRate()`( + @FloatForgery(min = 0f, max = 100f) fakeSampleRate: Float + ) { + // Given + testedSampler = DeterministicTraceSampler(fakeSampleRate) + + // When + val result = testedSampler.getSampleRate() + + // Then + assertThat(result).isEqualTo(fakeSampleRate) + } + + @RepeatedTest(16) + fun `M return consistent result W sample() {same span}`( + @LongForgery fakeTraceIdLong: Long + ) { + // Given + testedSampler = DeterministicTraceSampler(50f) + val traceId = mock { + on { toLong() } doReturn fakeTraceIdLong + } + val context = mock { + on { this.traceId } doReturn traceId + on { tags } doReturn emptyMap() + } + val span = mock { + on { context() } doReturn context + } + + // When + val results = (1..10).map { testedSampler.sample(span) } + + // Then + assertThat(results.distinct()).hasSize(1) + } +} diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/utils/SpanSamplingIdProviderTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/SpanSamplingIdProviderTest.kt similarity index 96% rename from integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/utils/SpanSamplingIdProviderTest.kt rename to features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/SpanSamplingIdProviderTest.kt index 49f3d6ac95..909ef8d166 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/utils/SpanSamplingIdProviderTest.kt +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/SpanSamplingIdProviderTest.kt @@ -4,11 +4,10 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.okhttp.internal.utils +package com.datadog.android.trace.internal.net import com.datadog.android.internal.utils.toHexString import com.datadog.android.log.LogAttributes -import com.datadog.android.okhttp.trace.newSpanMock import com.datadog.android.trace.api.ZERO import com.datadog.android.trace.api.from import com.datadog.android.trace.api.span.DatadogSpan @@ -51,7 +50,7 @@ internal class SpanSamplingIdProviderTest { fun `set up`(forge: Forge) { fakeTags = forge.aMap { anAlphabeticalString() to aString() } whenever(mockSpanContext.tags) doReturn fakeTags - mockSpan = forge.newSpanMock(mockSpanContext) + whenever(mockSpan.context()) doReturn mockSpanContext } @Test diff --git a/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/TracerProviderTest.kt b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/TracerProviderTest.kt new file mode 100644 index 0000000000..305c320ec0 --- /dev/null +++ b/features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/TracerProviderTest.kt @@ -0,0 +1,385 @@ +/* + * 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.trace.internal.net + +import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureScope +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.trace.TracingHeaderType +import com.datadog.android.trace.api.tracer.DatadogTracer +import com.datadog.android.utils.forge.Configurator +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.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class TracerProviderTest { + + private lateinit var testedTracerProvider: TracerProvider + + @Mock + lateinit var mockSdkCore: InternalSdkCore + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockTracingFeature: FeatureScope + + @Mock + lateinit var mockGlobalTracer: DatadogTracer + + @Mock + lateinit var mockLocalTracer: DatadogTracer + + @Mock + lateinit var mockFirstPartyHostResolver: FirstPartyHostHeaderTypeResolver + + @StringForgery + lateinit var fakeNetworkInstrumentationName: String + + private lateinit var fakeLocalHeaderTypes: Set + private lateinit var fakeGlobalHeaderTypes: Set + + @BeforeEach + fun `set up`(forge: Forge) { + fakeLocalHeaderTypes = forge.aList { aValueFrom(TracingHeaderType::class.java) }.toSet() + fakeGlobalHeaderTypes = forge.aList { aValueFrom(TracingHeaderType::class.java) }.toSet() + + whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger + whenever(mockSdkCore.firstPartyHostResolver) doReturn mockFirstPartyHostResolver + whenever(mockFirstPartyHostResolver.getAllHeaderTypes()) doReturn fakeGlobalHeaderTypes + + testedTracerProvider = TracerProvider( + localTracerFactory = { _, _ -> mockLocalTracer }, + globalTracerProvider = { null } + ) + } + + @Test + fun `M return null and log W provideTracer() {tracing feature not registered}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn null + + // When + val result = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + assertThat(result).isNull() + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + capture(), + eq(null), + eq(true), + eq(null) + ) + assertThat(firstValue()).contains(fakeNetworkInstrumentationName) + assertThat(firstValue()).contains("TracingFeature") + } + } + + @Test + fun `M return global tracer W provideTracer() {global tracer available}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn mockTracingFeature + testedTracerProvider = TracerProvider( + localTracerFactory = { _, _ -> mockLocalTracer }, + globalTracerProvider = { mockGlobalTracer } + ) + + // When + val result = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + assertThat(result).isSameAs(mockGlobalTracer) + } + + @Test + fun `M create and return local tracer W provideTracer() {no global tracer}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn mockTracingFeature + var capturedHeaderTypes: Set? = null + testedTracerProvider = TracerProvider( + localTracerFactory = { _, headerTypes -> + capturedHeaderTypes = headerTypes + mockLocalTracer + }, + globalTracerProvider = { null } + ) + + // When + val result = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + assertThat(result).isSameAs(mockLocalTracer) + assertThat(capturedHeaderTypes).isEqualTo(fakeLocalHeaderTypes + fakeGlobalHeaderTypes) + argumentCaptor<() -> String> { + verify(mockInternalLogger).log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + capture(), + eq(null), + eq(false), + eq(null) + ) + assertThat(firstValue()).contains(fakeNetworkInstrumentationName) + assertThat(firstValue()).contains("local tracer") + } + } + + @Test + fun `M reuse local tracer W provideTracer() called multiple times {no global tracer}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn mockTracingFeature + var factoryCallCount = 0 + testedTracerProvider = TracerProvider( + localTracerFactory = { _, _ -> + factoryCallCount++ + mockLocalTracer + }, + globalTracerProvider = { null } + ) + + // When + val result1 = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + val result2 = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + assertThat(result1).isSameAs(mockLocalTracer) + assertThat(result2).isSameAs(mockLocalTracer) + assertThat(factoryCallCount).isEqualTo(1) + } + + @Test + fun `M clear local tracer W provideTracer() {global tracer becomes available}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn mockTracingFeature + var globalTracerAvailable = false + testedTracerProvider = TracerProvider( + localTracerFactory = { _, _ -> mockLocalTracer }, + globalTracerProvider = { if (globalTracerAvailable) mockGlobalTracer else null } + ) + + // When - first call without global tracer + val result1 = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + assertThat(result1).isSameAs(mockLocalTracer) + + // When - second call with global tracer available + globalTracerAvailable = true + val result2 = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + assertThat(result2).isSameAs(mockGlobalTracer) + } + + @Test + fun `M create new local tracer W provideTracer() {global tracer becomes unavailable after being available}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn mockTracingFeature + var globalTracerAvailable = true + var factoryCallCount = 0 + testedTracerProvider = TracerProvider( + localTracerFactory = { _, _ -> + factoryCallCount++ + mockLocalTracer + }, + globalTracerProvider = { if (globalTracerAvailable) mockGlobalTracer else null } + ) + + // When - first call with global tracer + val result1 = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + assertThat(result1).isSameAs(mockGlobalTracer) + assertThat(factoryCallCount).isEqualTo(0) + + // When - second call without global tracer + globalTracerAvailable = false + val result2 = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + assertThat(result2).isSameAs(mockLocalTracer) + assertThat(factoryCallCount).isEqualTo(1) + } + + @Test + fun `M log warning only once W provideTracer() called multiple times {tracing feature not registered}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn null + + // When + testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + verify(mockInternalLogger, times(2)).log( + eq(InternalLogger.Level.WARN), + eq(InternalLogger.Target.USER), + any<() -> String>(), + eq(null), + eq(true), + eq(null) + ) + } + + @Test + fun `M create local tracer only once W provideTracer() called concurrently {no global tracer}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn mockTracingFeature + val factoryCallCount = AtomicInteger(0) + testedTracerProvider = TracerProvider( + localTracerFactory = { _, _ -> + factoryCallCount.incrementAndGet() + mockLocalTracer + }, + globalTracerProvider = { null } + ) + + val threadCount = 10 + val executor = Executors.newFixedThreadPool(threadCount) + val startLatch = CountDownLatch(1) + val doneLatch = CountDownLatch(threadCount) + val results = mutableListOf() + + // When + repeat(threadCount) { + executor.submit { + startLatch.await() + val result = testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + synchronized(results) { + results.add(result) + } + doneLatch.countDown() + } + } + startLatch.countDown() + doneLatch.await(5, TimeUnit.SECONDS) + executor.shutdown() + + // Then + assertThat(factoryCallCount.get()).isEqualTo(1) + assertThat(results).hasSize(threadCount) + results.forEach { assertThat(it).isSameAs(mockLocalTracer) } + } + + @Test + fun `M not interact with firstPartyHostResolver W provideTracer() {tracing feature not registered}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn null + + // When + testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + verifyNoInteractions(mockFirstPartyHostResolver) + } + + @Test + fun `M not interact with firstPartyHostResolver W provideTracer() {global tracer available}`() { + // Given + whenever(mockSdkCore.getFeature(Feature.TRACING_FEATURE_NAME)) doReturn mockTracingFeature + testedTracerProvider = TracerProvider( + localTracerFactory = { _, _ -> mockLocalTracer }, + globalTracerProvider = { mockGlobalTracer } + ) + + // When + testedTracerProvider.provideTracer( + mockSdkCore, + fakeLocalHeaderTypes, + fakeNetworkInstrumentationName + ) + + // Then + verifyNoInteractions(mockFirstPartyHostResolver) + } +} diff --git a/integrations/dd-sdk-android-cronet/api/apiSurface b/integrations/dd-sdk-android-cronet/api/apiSurface index f3ef126d9a..acf00c8132 100644 --- a/integrations/dd-sdk-android-cronet/api/apiSurface +++ b/integrations/dd-sdk-android-cronet/api/apiSurface @@ -24,9 +24,9 @@ class com.datadog.android.cronet.DatadogCronetEngine : org.chromium.net.CronetEn override fun removeThroughputListener(org.chromium.net.NetworkQualityThroughputListener?) class Builder : org.chromium.net.CronetEngine.Builder constructor(android.content.Context, org.chromium.net.CronetEngine.Builder = CronetEngine.Builder(context)) - fun setRumResourceAttributesProvider(com.datadog.android.rum.RumResourceAttributesProvider) - fun setSdkInstanceName(String) + fun enableRumResourceInstrumentation(com.datadog.android.rum.configuration.RumResourceInstrumentationConfiguration = RumResourceInstrumentationConfiguration()) fun setListenerExecutor(java.util.concurrent.Executor) + fun enableApmInstrumentation(com.datadog.android.trace.ApmNetworkInstrumentationConfiguration) override fun getDefaultUserAgent(): String override fun enableQuic(Boolean) override fun setStoragePath(String?) diff --git a/integrations/dd-sdk-android-cronet/api/dd-sdk-android-cronet.api b/integrations/dd-sdk-android-cronet/api/dd-sdk-android-cronet.api index c3ffc5334f..a5b478c8d8 100644 --- a/integrations/dd-sdk-android-cronet/api/dd-sdk-android-cronet.api +++ b/integrations/dd-sdk-android-cronet/api/dd-sdk-android-cronet.api @@ -32,6 +32,7 @@ public final class com/datadog/android/cronet/DatadogCronetEngine$Builder : org/ public fun addQuicHint (Ljava/lang/String;II)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; public synthetic fun addQuicHint (Ljava/lang/String;II)Lorg/chromium/net/CronetEngine$Builder; public fun build ()Lorg/chromium/net/CronetEngine; + public final fun enableApmInstrumentation (Lcom/datadog/android/trace/ApmNetworkInstrumentationConfiguration;)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; public fun enableBrotli (Z)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; public synthetic fun enableBrotli (Z)Lorg/chromium/net/CronetEngine$Builder; public fun enableHttp2 (Z)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; @@ -44,6 +45,8 @@ public final class com/datadog/android/cronet/DatadogCronetEngine$Builder : org/ public synthetic fun enablePublicKeyPinningBypassForLocalTrustAnchors (Z)Lorg/chromium/net/CronetEngine$Builder; public fun enableQuic (Z)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; public synthetic fun enableQuic (Z)Lorg/chromium/net/CronetEngine$Builder; + public final fun enableRumResourceInstrumentation (Lcom/datadog/android/rum/configuration/RumResourceInstrumentationConfiguration;)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; + public static synthetic fun enableRumResourceInstrumentation$default (Lcom/datadog/android/cronet/DatadogCronetEngine$Builder;Lcom/datadog/android/rum/configuration/RumResourceInstrumentationConfiguration;ILjava/lang/Object;)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; public fun enableSdch (Z)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; public synthetic fun enableSdch (Z)Lorg/chromium/net/CronetEngine$Builder; public fun getDefaultUserAgent ()Ljava/lang/String; @@ -64,8 +67,6 @@ public final class com/datadog/android/cronet/DatadogCronetEngine$Builder : org/ public synthetic fun setQuicOptions (Lorg/chromium/net/QuicOptions$Builder;)Lorg/chromium/net/CronetEngine$Builder; public fun setQuicOptions (Lorg/chromium/net/QuicOptions;)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; public synthetic fun setQuicOptions (Lorg/chromium/net/QuicOptions;)Lorg/chromium/net/CronetEngine$Builder; - public final fun setRumResourceAttributesProvider (Lcom/datadog/android/rum/RumResourceAttributesProvider;)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; - public final fun setSdkInstanceName (Ljava/lang/String;)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; public fun setStoragePath (Ljava/lang/String;)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; public synthetic fun setStoragePath (Ljava/lang/String;)Lorg/chromium/net/CronetEngine$Builder; public fun setThreadPriority (I)Lcom/datadog/android/cronet/DatadogCronetEngine$Builder; diff --git a/integrations/dd-sdk-android-cronet/build.gradle.kts b/integrations/dd-sdk-android-cronet/build.gradle.kts index 49074ded7b..b1ae6a6dc5 100644 --- a/integrations/dd-sdk-android-cronet/build.gradle.kts +++ b/integrations/dd-sdk-android-cronet/build.gradle.kts @@ -40,6 +40,11 @@ plugins { android { namespace = "com.datadog.android.cronet" + lint { + // Cronet library has experimental annotations that AndroidX lint checker + // cannot properly parse, causing "Failed to extract attribute 'level'" error + disable += "UnsafeOptInUsageError" + } } dependencies { @@ -49,6 +54,7 @@ dependencies { implementation(project(":dd-sdk-android-internal")) implementation(project(":features:dd-sdk-android-rum")) + implementation(project(":features:dd-sdk-android-trace")) unmock(libs.robolectric) // Trying to add most recent lib version in order to test the instrumentation, see CronetApiInstrumentationTest @@ -56,6 +62,7 @@ dependencies { testImplementation(testFixtures(project(":dd-sdk-android-core"))) testImplementation(testFixtures(project(":dd-sdk-android-internal"))) testImplementation(testFixtures(project(":features:dd-sdk-android-rum"))) + testImplementation(libs.elmyrJUnit4) testImplementation(libs.bundles.jUnit5) testImplementation(libs.bundles.testTools) diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/DatadogCronetEngine.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/DatadogCronetEngine.kt index 37085a9b6a..438aa17dad 100644 --- a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/DatadogCronetEngine.kt +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/DatadogCronetEngine.kt @@ -6,12 +6,17 @@ package com.datadog.android.cronet import android.content.Context +import com.datadog.android.cronet.internal.DatadogCronetRequestContext +import com.datadog.android.cronet.internal.DatadogRequestCallback import com.datadog.android.cronet.internal.DatadogRequestFinishedInfoListener import com.datadog.android.cronet.internal.DatadogUrlRequestBuilder import com.datadog.android.rum.ExperimentalRumApi -import com.datadog.android.rum.NoOpRumResourceAttributesProvider -import com.datadog.android.rum.RumResourceAttributesProvider +import com.datadog.android.rum._RumInternalProxy +import com.datadog.android.rum.configuration.RumResourceInstrumentationConfiguration import com.datadog.android.rum.internal.net.RumResourceInstrumentation +import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration +import com.datadog.android.trace.internal.ApmNetworkInstrumentation +import com.datadog.android.trace.internal.DatadogTracingToolkit import org.chromium.net.BidirectionalStream import org.chromium.net.ConnectionMigrationOptions import org.chromium.net.CronetEngine @@ -34,14 +39,21 @@ import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit /** - * Datadog-instrumented wrapper for [CronetEngine] that adds RUM monitoring. + * Datadog-instrumented wrapper for [CronetEngine] that adds RUM and APM monitoring. * This wrapper delegates all Cronet functionality to the underlying engine while - * intercepting network requests to report them as RUM resources. + * intercepting network requests to report them as RUM resources and create APM trace spans. + * + * Use [DatadogCronetEngine.Builder] to create instances of this class. + * + * @param delegate the underlying CronetEngine to delegate calls to. + * @param apmNetworkInstrumentation optional APM tracing instrumentation for creating trace spans. + * @param rumResourceInstrumentation optional RUM instrumentation for tracking network resources. */ @Suppress("TooManyFunctions") // The number of functions depends on Cronet implementation. class DatadogCronetEngine internal constructor( internal val delegate: CronetEngine, - internal val rumResourceInstrumentation: RumResourceInstrumentation + internal val apmNetworkInstrumentation: ApmNetworkInstrumentation?, + internal val rumResourceInstrumentation: RumResourceInstrumentation? ) : CronetEngine() { /** @inheritDoc */ @@ -49,11 +61,25 @@ class DatadogCronetEngine internal constructor( url: String, callback: UrlRequest.Callback, executor: Executor - ): UrlRequest.Builder = DatadogUrlRequestBuilder( - url = url, - delegate = delegate.newUrlRequestBuilder(url, callback, executor), - rumResourceInstrumentation = rumResourceInstrumentation - ) + ): UrlRequest.Builder { + val datadogCallback = DatadogRequestCallback(callback, apmNetworkInstrumentation) + val requestContext = DatadogCronetRequestContext( + url = url, + engine = this, + datadogRequestCallback = datadogCallback, + executor = executor + ) + return DatadogUrlRequestBuilder( + requestContext = requestContext, + cronetInstrumentationStateHolder = datadogCallback + ) + } + + internal fun newDelegateUrlRequestBuilder( + url: String, + callback: UrlRequest.Callback, + executor: Executor + ): UrlRequest.Builder = delegate.newUrlRequestBuilder(url, callback, executor) // region simple delegation @@ -70,11 +96,9 @@ class DatadogCronetEngine internal constructor( override fun stopNetLog() = delegate.stopNetLog() /** @inheritDoc */ + @Suppress("DEPRECATION") @Deprecated("Deprecated in Java") - override fun getGlobalMetricsDeltas(): ByteArray? { - @Suppress("DEPRECATION") - return delegate.globalMetricsDeltas - } + override fun getGlobalMetricsDeltas(): ByteArray? = delegate.globalMetricsDeltas /** @inheritDoc */ @Throws(IOException::class) @@ -95,12 +119,14 @@ class DatadogCronetEngine internal constructor( override fun getActiveRequestCount(): Int = delegate.activeRequestCount /** @inheritDoc */ - override fun addRequestFinishedListener(listener: RequestFinishedInfo.Listener?) = - delegate.addRequestFinishedListener(listener) + override fun addRequestFinishedListener(listener: RequestFinishedInfo.Listener?) { + listener?.let { delegate.addRequestFinishedListener(it) } + } /** @inheritDoc */ - override fun removeRequestFinishedListener(listener: RequestFinishedInfo.Listener?) = - delegate.removeRequestFinishedListener(listener) + override fun removeRequestFinishedListener( + listener: RequestFinishedInfo.Listener? + ) = delegate.removeRequestFinishedListener(listener) /** @inheritDoc */ override fun getHttpRttMs(): Int = delegate.httpRttMs @@ -112,8 +138,11 @@ class DatadogCronetEngine internal constructor( override fun getDownstreamThroughputKbps(): Int = delegate.downstreamThroughputKbps /** @inheritDoc */ - override fun startNetLogToDisk(dirPath: String?, logAll: Boolean, maxSize: Int) = - delegate.startNetLogToDisk(dirPath, logAll, maxSize) + override fun startNetLogToDisk( + dirPath: String?, + logAll: Boolean, + maxSize: Int + ) = delegate.startNetLogToDisk(dirPath, logAll, maxSize) /** @inheritDoc */ override fun bindToNetwork(networkHandle: Long) = delegate.bindToNetwork(networkHandle) @@ -139,8 +168,9 @@ class DatadogCronetEngine internal constructor( override fun removeRttListener(listener: NetworkQualityRttListener?) = delegate.removeRttListener(listener) /** @inheritDoc */ - override fun addThroughputListener(listener: NetworkQualityThroughputListener?) = - delegate.addThroughputListener(listener) + override fun addThroughputListener( + listener: NetworkQualityThroughputListener? + ) = delegate.addThroughputListener(listener) /** @inheritDoc */ override fun removeThroughputListener(listener: NetworkQualityThroughputListener?) = @@ -153,15 +183,20 @@ class DatadogCronetEngine internal constructor( internal const val CRONET_NETWORK_INSTRUMENTATION_NAME = "Cronet" } - /** @inheritDoc */ + /** + * Builder for creating [DatadogCronetEngine] instances with Datadog instrumentation. + * This builder wraps the standard [CronetEngine.Builder] and adds options for RUM and APM monitoring. + * + * By default, RUM resource tracking is enabled. APM tracing can be enabled via [enableApmInstrumentation]. + */ @Suppress("TooManyFunctions") // The amount of functions is depend on Cronet class Builder : CronetEngine.Builder { /** * This constructor is made only for the testing purposes. * - * @param iCronetEngineBuilder - an instance [ICronetEngineBuilder] usually made from [Context]. - * @param delegate - the delegate builder to wrap, defaults to a new CronetEngine.Builder + * @param iCronetEngineBuilder an instance [ICronetEngineBuilder] usually made from [Context]. + * @param delegate the delegate builder to wrap, defaults to a new CronetEngine.Builder. */ internal constructor( iCronetEngineBuilder: ICronetEngineBuilder, @@ -170,7 +205,12 @@ class DatadogCronetEngine internal constructor( this.delegate = delegate } - /** @inheritDoc */ + /** + * Creates a new Builder for [DatadogCronetEngine]. + * + * @param context the Android context to use for creating the underlying CronetEngine. + * @param delegate optional delegate builder to wrap, defaults to a new CronetEngine.Builder. + */ @ExperimentalRumApi constructor( context: Context, @@ -179,31 +219,23 @@ class DatadogCronetEngine internal constructor( this.delegate = delegate } - private val delegate: CronetEngine.Builder - private var sdkInstanceName: String? = null - private var listenerExecutor: Executor? = null - private var rumResourceAttributesProvider: RumResourceAttributesProvider = - NoOpRumResourceAttributesProvider() - /** - * Sets the [RumResourceAttributesProvider] to use to provide custom attributes to the RUM. - * By default it won't attach any custom attributes. - * @param rumResourceAttributesProvider the [RumResourceAttributesProvider] to use. + * Sets a custom RUM instrumentation builder. + * Use this to customize how RUM resources are tracked, or pass null to disable RUM tracking. + * + * @param configuration the RUM instrumentation builder, or null to disable RUM tracking. */ @ExperimentalRumApi - fun setRumResourceAttributesProvider(rumResourceAttributesProvider: RumResourceAttributesProvider) = apply { - this.rumResourceAttributesProvider = rumResourceAttributesProvider + fun enableRumResourceInstrumentation( + configuration: RumResourceInstrumentationConfiguration = RumResourceInstrumentationConfiguration() + ) = apply { + rumResourceInstrumentationConfiguration = configuration } - /** - * Set the SDK instance name to bind to, the default value is null. - * @param sdkInstanceName SDK instance name to bind to, the default value is null. - * Instrumentation won't be working until SDK instance is ready. - */ - @ExperimentalRumApi - fun setSdkInstanceName(sdkInstanceName: String) = apply { - this.sdkInstanceName = sdkInstanceName - } + private val delegate: CronetEngine.Builder + private var listenerExecutor: Executor? = null + private var tracingInstrumentationConfiguration: ApmNetworkInstrumentationConfiguration? = null + private var rumResourceInstrumentationConfiguration: RumResourceInstrumentationConfiguration? = null /** * Sets the executor for request finished listeners. @@ -215,6 +247,18 @@ class DatadogCronetEngine internal constructor( this.listenerExecutor = executor } + /** + * Enables APM tracing for network requests made through this Cronet engine. + * When enabled, trace spans will be created for HTTP requests and tracing headers + * will be injected according to the instrumentation configuration. + * + * @param configuration the tracing instrumentation configuration to configure APM tracing behavior. + */ + @ExperimentalRumApi + fun enableApmInstrumentation(configuration: ApmNetworkInstrumentationConfiguration) = apply { + this.tracingInstrumentationConfiguration = configuration + } + /** @inheritDoc */ override fun getDefaultUserAgent(): String = delegate.defaultUserAgent @@ -325,24 +369,30 @@ class DatadogCronetEngine internal constructor( /** @inheritDoc */ override fun build(): CronetEngine { - val rumResourceInstrumentation = RumResourceInstrumentation( - sdkInstanceName, - networkInstrumentationName = CRONET_NETWORK_INSTRUMENTATION_NAME, - rumResourceAttributesProvider = rumResourceAttributesProvider - ) + val rumResourceInstrumentation = rumResourceInstrumentationConfiguration?.let { + _RumInternalProxy.createRumResourceInstrumentation(CRONET_NETWORK_INSTRUMENTATION_NAME, it) + } - val engine = DatadogCronetEngine(delegate.build(), rumResourceInstrumentation) + val tracingInstrumentation = tracingInstrumentationConfiguration?.let { + DatadogTracingToolkit.createApmNetworkInstrumentation(CRONET_NETWORK_INSTRUMENTATION_NAME, it) + } - engine.addRequestFinishedListener( + val requestFinishedListener = rumResourceInstrumentation?.let { instrumentation -> DatadogRequestFinishedInfoListener( - executor = listenerExecutor ?: newListenerExecutor(), - rumResourceInstrumentation = rumResourceInstrumentation + rumResourceInstrumentation = instrumentation, + executor = listenerExecutor ?: newListenerExecutor() ) - ) - return engine + } + + return if (rumResourceInstrumentation == null && tracingInstrumentation == null) { + delegate.build() + } else { + DatadogCronetEngine(delegate.build(), tracingInstrumentation, rumResourceInstrumentation) + .also { it.addRequestFinishedListener(requestFinishedListener) } + } } - private companion object { + internal companion object { // Exception thrown only for wrong arguments, but those ones are correct @Suppress("UnsafeThirdPartyFunctionCall") private fun newListenerExecutor(): ThreadPoolExecutor = ThreadPoolExecutor( diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetHttpRequestInfo.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetHttpRequestInfo.kt index 252bb7b895..96b5b3a1bb 100644 --- a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetHttpRequestInfo.kt +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetHttpRequestInfo.kt @@ -7,26 +7,50 @@ package com.datadog.android.cronet.internal import com.datadog.android.api.instrumentation.network.ExtendedRequestInfo import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.api.instrumentation.network.MutableHttpRequestInfo import com.datadog.android.core.internal.net.HttpSpec +import com.datadog.android.trace.internal.net.RequestTraceState import org.chromium.net.UploadDataProvider +import org.chromium.net.UrlRequest import java.io.IOException internal data class CronetHttpRequestInfo( - override val url: String, - override val method: String, - override val headers: Map>, - private val uploadDataProvider: UploadDataProvider?, + private val requestContext: DatadogCronetRequestContext +) : HttpRequestInfo, ExtendedRequestInfo, MutableHttpRequestInfo { + + override val url: String + get() = requestContext.url + + override val method: String + get() = requestContext.method + private val annotations: List -) : HttpRequestInfo, ExtendedRequestInfo { + get() = requestContext.annotations - override val contentType: String? get() = headers[HttpSpec.Headers.CONTENT_TYPE]?.firstOrNull() + override val headers: Map> + get() = requestContext.headers + + override val contentType: String? + get() = headers[HttpSpec.Headers.CONTENT_TYPE]?.firstOrNull() @Suppress("UNCHECKED_CAST") override fun tag(type: Class): T? = annotations.firstOrNull { type.isInstanceOf(it) } as? T override fun contentLength(): Long? = headers[HttpSpec.Headers.CONTENT_LENGTH] ?.firstOrNull()?.toLongOrNull() - ?: uploadDataProvider?.contentLength() + ?: requestContext.uploadDataProvider?.contentLength() + + override fun newBuilder() = CronetHttpRequestInfoBuilder(requestContext.copy()) + + /** + * Builds the underlying delegate UrlRequest. + * This applies all accumulated headers to the delegate and calls delegate.build(). + * Should be called from DatadogUrlRequest.start() after tracing headers have been added. + */ + internal fun buildCronetRequest( + tracingState: RequestTraceState? + ): UrlRequest = requestContext.buildCronetRequest(this, tracingState) // We have to override toString in order to prevent StackOverflowException, // as annotations could hold a link to this CronetHttpRequestInfo instance itself. @@ -34,7 +58,7 @@ internal data class CronetHttpRequestInfo( "url='$url', " + "method='$method', " + "headers=$headers, " + - "provider=$uploadDataProvider, " + + "provider=${requestContext.uploadDataProvider}, " + "annotations=${annotations.size}" + ")" @@ -56,3 +80,27 @@ internal data class CronetHttpRequestInfo( else -> this.isInstance(value) } } + +internal class CronetHttpRequestInfoBuilder( + private val requestContext: DatadogCronetRequestContext +) : HttpRequestInfoBuilder { + + override fun setUrl(url: String): HttpRequestInfoBuilder = apply { + requestContext.url = url + } + + override fun addHeader(key: String, vararg values: String) = apply { + requestContext.addHeader(key, *values) + } + + override fun removeHeader(key: String) = apply { + // Cronet doesn't support removing headers, but we track it in requestContext + requestContext.removeHeader(key) + } + + override fun addTag(type: Class, tag: T?) = apply { + requestContext.setTag(type, tag) + } + + override fun build() = requestContext.buildRequestInfo() +} diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetInstrumentationStateHolder.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetInstrumentationStateHolder.kt new file mode 100644 index 0000000000..7072360d6a --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetInstrumentationStateHolder.kt @@ -0,0 +1,12 @@ +/* + * 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.cronet.internal + +import com.datadog.android.trace.internal.net.RequestTraceState + +internal interface CronetInstrumentationStateHolder { + var traceState: RequestTraceState? +} diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogCronetRequestContext.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogCronetRequestContext.kt new file mode 100644 index 0000000000..e27eee656d --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogCronetRequestContext.kt @@ -0,0 +1,214 @@ +/* + * 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.cronet.internal + +import com.datadog.android.core.internal.net.HttpSpec +import com.datadog.android.cronet.DatadogCronetEngine +import com.datadog.android.rum.internal.net.RumResourceInstrumentation +import com.datadog.android.rum.internal.net.RumResourceInstrumentation.Companion.buildResourceId +import com.datadog.android.trace.internal.ApmNetworkInstrumentation +import com.datadog.android.trace.internal.net.RequestTraceState +import org.chromium.net.RequestFinishedInfo +import org.chromium.net.UploadDataProvider +import org.chromium.net.UrlRequest +import java.nio.ByteBuffer +import java.util.concurrent.Executor + +@Suppress("TooManyFunctions") +internal class DatadogCronetRequestContext internal constructor( + internal var url: String, + private val executor: Executor, + private val engine: DatadogCronetEngine, + private val datadogRequestCallback: DatadogRequestCallback, + private val requestParams: CronetRequestParams = CronetRequestParams(), + private val additionalAnnotations: MutableMap, Any> = mutableMapOf() +) { + internal val method: String + get() = requestParams.method + + internal val uploadDataProvider: UploadDataProvider? + get() = requestParams.uploadDataProviderParams?.uploadDataProvider + + internal val headers: Map> + get() = requestParams.headers.toMap() + + internal val apmNetworkInstrumentation: ApmNetworkInstrumentation? + get() = engine.apmNetworkInstrumentation + + internal val rumResourceInstrumentation: RumResourceInstrumentation? + get() = engine.rumResourceInstrumentation + + internal val annotations: List + get() = additionalAnnotations.values.toList() + + internal fun addHeader(key: String, vararg values: String) { + values.forEach { value -> + requestParams.headers.getOrPut(key) { mutableListOf() } + .add(value) + } + } + + internal fun removeHeader(key: String) = requestParams.headers.remove(key) + + internal fun setTag(type: Class, tag: T?) { + if (tag == null) { + additionalAnnotations.remove(type) + } else { + additionalAnnotations[type] = tag + } + } + + internal fun disableCache() { + requestParams.disableCache = true + } + + internal fun allowDirectExecutor() { + requestParams.allowDirectExecutor = true + } + + internal fun setPriority(priority: Int) { + requestParams.priority = priority + } + + internal fun bindToNetwork(networkHandle: Long) { + requestParams.networkHandle = networkHandle + } + + internal fun setTrafficStatsTag(trafficStatsTag: Int) { + requestParams.trafficStatsTag = trafficStatsTag + } + + internal fun setTrafficStatsUid(trafficStatsUid: Int) { + requestParams.trafficStatsUid = trafficStatsUid + } + + internal fun setRawCompressionDictionary( + dictionarySha256Hash: ByteArray?, + dictionary: ByteBuffer?, + dictionaryId: String? + ) { + requestParams.rawCompressionDictionary = CronetRequestParams.RawCompressionDictionary( + dictionarySha256Hash, + dictionary, + dictionaryId + ) + } + + internal fun setRequestFinishedListener(listener: RequestFinishedInfo.Listener?) { + requestParams.listener = listener + } + + internal fun addRequestAnnotation(annotation: Any) { + additionalAnnotations[annotation::class.java] = annotation + } + + internal fun setUploadDataProvider(uploadDataProvider: UploadDataProvider?, executor: Executor?) { + requestParams.uploadDataProviderParams = CronetRequestParams.UploadDataProviderParams( + uploadDataProvider, + executor + ) + } + + internal fun setHttpMethod(method: String) { + requestParams.method = method + } + + internal fun copy() = DatadogCronetRequestContext( + url = url, + engine = engine, + executor = executor, + requestParams = requestParams.deepCopy(), + datadogRequestCallback = datadogRequestCallback, + additionalAnnotations = additionalAnnotations.toMutableMap() + ) + + internal fun buildCronetRequest(requestInfo: CronetHttpRequestInfo, tracingState: RequestTraceState?): UrlRequest = + engine.newDelegateUrlRequestBuilder(url, datadogRequestCallback, executor) + .applyRequestParams(requestParams) + .applyAnnotations(annotations) + .also { + it.addRequestAnnotation(requestInfo) + it.addRequestAnnotation(buildResourceId(requestInfo, generateUuid = true)) + if (tracingState != null) it.addRequestAnnotation(tracingState) + } + .build() + + internal fun buildRequestInfo() = CronetHttpRequestInfo(this) +} + +private fun UrlRequest.Builder.applyRequestParams(params: CronetRequestParams) = apply { + setHttpMethod(params.method) + addHeaders(params.headers) + if (params.disableCache) disableCache() + if (params.allowDirectExecutor) allowDirectExecutor() + params.priority?.let(::setPriority) + params.networkHandle?.let(::bindToNetwork) + params.trafficStatsTag?.let(::setTrafficStatsTag) + params.trafficStatsUid?.let(::setTrafficStatsUid) + params.listener?.let(::setRequestFinishedListener) + params.rawCompressionDictionary?.let { + setRawCompressionDictionary( + it.dictionarySha256Hash, + it.dictionary, + it.dictionaryId + ) + } + params.uploadDataProviderParams?.let { setUploadDataProvider(it.uploadDataProvider, it.executor) } +} + +internal data class CronetRequestParams( + var method: String = HttpSpec.Method.GET, + var headers: MutableMap> = mutableMapOf(), + var disableCache: Boolean = false, + var allowDirectExecutor: Boolean = false, + var priority: Int? = null, + var networkHandle: Long? = null, + var trafficStatsTag: Int? = null, + var trafficStatsUid: Int? = null, + var rawCompressionDictionary: RawCompressionDictionary? = null, + var listener: RequestFinishedInfo.Listener? = null, + var uploadDataProviderParams: UploadDataProviderParams? = null +) { + + fun deepCopy() = copy( + headers = headers.mapValues { it.value.toMutableList() }.toMutableMap() + ) + + data class UploadDataProviderParams( + val uploadDataProvider: UploadDataProvider?, + val executor: Executor? + ) + + class RawCompressionDictionary( + val dictionarySha256Hash: ByteArray?, + val dictionary: ByteBuffer?, + val dictionaryId: String? + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RawCompressionDictionary + + if (!dictionarySha256Hash.contentEquals(other.dictionarySha256Hash)) return false + if (dictionary != other.dictionary) return false + if (dictionaryId != other.dictionaryId) return false + + return true + } + + override fun hashCode(): Int { + var result = dictionarySha256Hash?.contentHashCode() ?: 0 + result = 31 * result + (dictionary?.hashCode() ?: 0) + result = 31 * result + (dictionaryId?.hashCode() ?: 0) + return result + } + } +} + +private fun UrlRequest.Builder.applyAnnotations(annotations: List) = apply { + annotations.forEach { addRequestAnnotation(it) } +} diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogRequestCallback.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogRequestCallback.kt new file mode 100644 index 0000000000..61babe6355 --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogRequestCallback.kt @@ -0,0 +1,74 @@ +/* + * 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.cronet.internal + +import com.datadog.android.trace.internal.ApmNetworkInstrumentation +import com.datadog.android.trace.internal.net.RequestTraceState +import org.chromium.net.CronetException +import org.chromium.net.UrlRequest +import org.chromium.net.UrlResponseInfo +import java.io.IOException +import java.nio.ByteBuffer + +@Suppress("UnsafeThirdPartyFunctionCall") // Cronet callback delegation is safe +internal class DatadogRequestCallback( + private val delegate: UrlRequest.Callback, + private val apmNetworkInstrumentation: ApmNetworkInstrumentation? +) : UrlRequest.Callback(), CronetInstrumentationStateHolder { + + @Volatile + override var traceState: RequestTraceState? = null + override fun onRedirectReceived( + request: UrlRequest?, + info: UrlResponseInfo?, + newLocationUrl: String? + ) = delegate.onRedirectReceived(request, info, newLocationUrl) + + override fun onResponseStarted( + request: UrlRequest?, + info: UrlResponseInfo? + ) = delegate.onResponseStarted(request, info) + + override fun onReadCompleted( + request: UrlRequest?, + info: UrlResponseInfo?, + byteBuffer: ByteBuffer? + ) = delegate.onReadCompleted(request, info, byteBuffer) + + override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) { + traceState?.let { + apmNetworkInstrumentation?.onResponseSucceeded( + it, + CronetHttpResponseInfo(info) + ) + } + delegate.onSucceeded(request, info) + } + + override fun onCanceled(request: UrlRequest?, info: UrlResponseInfo?) { + traceState?.let { + apmNetworkInstrumentation?.onResponseFailed( + it, + IOException("Response cancelled") + ) + } + delegate.onCanceled(request, info) + } + + override fun onFailed( + request: UrlRequest?, + info: UrlResponseInfo?, + error: CronetException? + ) { + traceState?.let { + apmNetworkInstrumentation?.onResponseFailed( + it, + error ?: IOException("Response failed") + ) + } + delegate.onFailed(request, info, error) + } +} diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogRequestFinishedInfoListener.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogRequestFinishedInfoListener.kt index 6f62c4e0df..9d9995527b 100644 --- a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogRequestFinishedInfoListener.kt +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogRequestFinishedInfoListener.kt @@ -6,8 +6,10 @@ package com.datadog.android.cronet.internal import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.internal.domain.event.ResourceTiming import com.datadog.android.rum.internal.net.RumResourceInstrumentation +import com.datadog.android.trace.internal.net.RequestTraceState import org.chromium.net.RequestFinishedInfo import java.io.IOException import java.util.Date @@ -21,7 +23,7 @@ internal class DatadogRequestFinishedInfoListener( override fun onRequestFinished(finishedInfo: RequestFinishedInfo) { val requestInfo = finishedInfo.annotations?.filterIsInstance()?.firstOrNull() - + val traceInfo = finishedInfo.annotations?.filterIsInstance()?.firstOrNull() if (requestInfo == null) { rumResourceInstrumentation.reportInstrumentationError( "Unable to instrument RUM resource without the request info" @@ -58,13 +60,24 @@ internal class DatadogRequestFinishedInfoListener( } else { rumResourceInstrumentation.stopResource( requestInfo = requestInfo, - responseInfo = responseInfo + responseInfo = responseInfo, + attributes = traceInfo?.toAttributes().orEmpty() ) } } } } + private fun RequestTraceState.toAttributes(): Map? = span + ?.takeIf { isSampled } + ?.let { span -> + buildMap { + put(RumAttributes.TRACE_ID, span.context().traceId.toHexString()) + put(RumAttributes.SPAN_ID, span.context().spanId.toString()) + put(RumAttributes.RULE_PSR, (sampleRate ?: ZERO_SAMPLE_RATE) / ALL_IN_SAMPLE_RATE) + } + } + private fun buildTiming(metrics: RequestFinishedInfo.Metrics): ResourceTiming { val connectStartMs = metrics.connectStart - metrics.requestStart val connectDurationMs = metrics.connectEnd - metrics.connectStart @@ -113,13 +126,12 @@ internal class DatadogRequestFinishedInfoListener( companion object { - internal operator fun Long?.plus(other: Long?): Long? { - return if (this == null || other == null) null else this + other - } + private const val ALL_IN_SAMPLE_RATE: Float = 100f + private const val ZERO_SAMPLE_RATE: Float = 0f - internal operator fun Long?.minus(other: Long?): Long { - return if (this == null || other == null) 0L else this - other - } + internal operator fun Long?.plus(other: Long?) = if (this == null || other == null) null else this + other + + internal operator fun Long?.minus(other: Long?) = if (this == null || other == null) 0L else this - other internal operator fun Date?.minus(other: Date?): Long { val thisTime = this?.time diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequest.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequest.kt index dba2fb7971..620db4992a 100644 --- a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequest.kt +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequest.kt @@ -6,29 +6,50 @@ package com.datadog.android.cronet.internal import com.datadog.android.api.instrumentation.network.HttpRequestInfo -import com.datadog.android.rum.internal.net.RumResourceInstrumentation import org.chromium.net.UrlRequest import java.nio.ByteBuffer internal class DatadogUrlRequest( - private val info: HttpRequestInfo, - private val delegate: UrlRequest, - private val rumResourceInstrumentation: RumResourceInstrumentation + private val requestContext: DatadogCronetRequestContext, + private val cronetInstrumentationStateHolder: CronetInstrumentationStateHolder ) : UrlRequest() { + @Volatile + private var delegatedRequest: UrlRequest? = null + override fun start() { - rumResourceInstrumentation.startResource(info) - rumResourceInstrumentation.sendWaitForResourceTimingEvent(info) - delegate.start() + val requestInfo: CronetHttpRequestInfo = requestContext.buildRequestInfo() + requestContext.rumResourceInstrumentation?.apply { + startResource(requestInfo) + sendWaitForResourceTimingEvent(requestInfo) + } + + val traceState = requestContext.apmNetworkInstrumentation?.onRequest(requestInfo) + ?.also { traceState -> cronetInstrumentationStateHolder.traceState = traceState } + + val finalRequestInfo: HttpRequestInfo = traceState?.requestInfo ?: requestInfo + + (finalRequestInfo as? CronetHttpRequestInfo) + ?.buildCronetRequest(traceState) + ?.also { delegatedRequest = it } + ?.start() } - override fun followRedirect() = delegate.followRedirect() + override fun cancel() { + delegatedRequest?.cancel() + } - override fun read(buffer: ByteBuffer?) = delegate.read(buffer) + override fun followRedirect() { + delegatedRequest?.followRedirect() + } - override fun cancel() = delegate.cancel() + override fun read(buffer: ByteBuffer?) { + delegatedRequest?.read(buffer) + } - override fun isDone(): Boolean = delegate.isDone + override fun getStatus(listener: StatusListener?) { + delegatedRequest?.getStatus(listener) + } - override fun getStatus(listener: StatusListener?) = delegate.getStatus(listener) + override fun isDone(): Boolean = delegatedRequest?.isDone ?: false } diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestBuilder.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestBuilder.kt index 5139f64268..74e7fc191c 100644 --- a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestBuilder.kt +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestBuilder.kt @@ -5,9 +5,6 @@ */ package com.datadog.android.cronet.internal -import com.datadog.android.core.internal.net.HttpSpec -import com.datadog.android.rum.internal.net.RumResourceInstrumentation -import com.datadog.android.rum.internal.net.RumResourceInstrumentation.Companion.buildResourceId import org.chromium.net.RequestFinishedInfo import org.chromium.net.UploadDataProvider import org.chromium.net.UrlRequest @@ -16,65 +13,55 @@ import java.util.concurrent.Executor @Suppress("TooManyFunctions") // The number of functions depends on Cronet implementation. internal class DatadogUrlRequestBuilder( - internal val url: String, - internal val delegate: UrlRequest.Builder, - internal val rumResourceInstrumentation: RumResourceInstrumentation + private val requestContext: DatadogCronetRequestContext, + internal val cronetInstrumentationStateHolder: CronetInstrumentationStateHolder ) : UrlRequest.Builder() { - internal var method: String = HttpSpec.Method.GET - internal val annotations = mutableListOf() - internal val headers: MutableMap> = mutableMapOf() - internal var uploadDataProvider: UploadDataProvider? = null - override fun setHttpMethod(method: String): UrlRequest.Builder = apply { - this.method = method - delegate.setHttpMethod(method) + requestContext.setHttpMethod(method) } - override fun addHeader(header: String, value: String?): UrlRequest.Builder = apply { - headers[header] = listOfNotNull(value) - delegate.addHeader(header, value) + override fun addHeader(header: String, value: String): UrlRequest.Builder = apply { + requestContext.addHeader(header, value) } override fun setUploadDataProvider( uploadDataProvider: UploadDataProvider?, executor: Executor? ): UrlRequest.Builder = apply { - this.uploadDataProvider = uploadDataProvider - delegate.setUploadDataProvider(uploadDataProvider, executor) + requestContext.setUploadDataProvider(uploadDataProvider, executor) } override fun disableCache(): UrlRequest.Builder = apply { - delegate.disableCache() + requestContext.disableCache() } override fun setPriority(priority: Int): UrlRequest.Builder = apply { - delegate.setPriority(priority) + requestContext.setPriority(priority) } override fun allowDirectExecutor(): UrlRequest.Builder = apply { - delegate.allowDirectExecutor() + requestContext.allowDirectExecutor() } override fun addRequestAnnotation(annotation: Any?): UrlRequest.Builder = apply { - annotations.add(annotation ?: return@apply) - delegate.addRequestAnnotation(annotation) + requestContext.addRequestAnnotation(annotation ?: return@apply) } override fun bindToNetwork(networkHandle: Long): UrlRequest.Builder = apply { - delegate.bindToNetwork(networkHandle) + requestContext.bindToNetwork(networkHandle) } override fun setTrafficStatsTag(tag: Int): UrlRequest.Builder = apply { - delegate.setTrafficStatsTag(tag) + requestContext.setTrafficStatsTag(tag) } override fun setTrafficStatsUid(uid: Int): UrlRequest.Builder = apply { - delegate.setTrafficStatsUid(uid) + requestContext.setTrafficStatsUid(uid) } override fun setRequestFinishedListener(listener: RequestFinishedInfo.Listener?): UrlRequest.Builder = apply { - delegate.setRequestFinishedListener(listener) + requestContext.setRequestFinishedListener(listener) } @UrlRequest.Experimental @@ -83,25 +70,15 @@ internal class DatadogUrlRequestBuilder( dictionary: ByteBuffer?, dictionaryId: String? ): UrlRequest.Builder = apply { - delegate.setRawCompressionDictionary(dictionarySha256Hash, dictionary, dictionaryId) - } - - override fun build(): UrlRequest { - val requestInfo = CronetHttpRequestInfo( - url = url, - method = method, - headers = headers, - uploadDataProvider = uploadDataProvider, - annotations = annotations - ) - - addRequestAnnotation(requestInfo) - addRequestAnnotation(buildResourceId(requestInfo, generateUuid = true)) - - return DatadogUrlRequest( - info = requestInfo, - delegate = delegate.build(), - rumResourceInstrumentation = rumResourceInstrumentation + requestContext.setRawCompressionDictionary( + dictionarySha256Hash, + dictionary, + dictionaryId ) } + + override fun build() = DatadogUrlRequest( + requestContext = requestContext, + cronetInstrumentationStateHolder = cronetInstrumentationStateHolder + ) } diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/UrlRequestBuilderExt.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/UrlRequestBuilderExt.kt new file mode 100644 index 0000000000..3f0104e965 --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/UrlRequestBuilderExt.kt @@ -0,0 +1,14 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ +package com.datadog.android.cronet.internal + +import org.chromium.net.UrlRequest + +internal fun UrlRequest.Builder.addHeaders(headers: Map>) = apply { + headers.forEach { (key, values) -> + values.forEach { addHeader(key, it) } + } +} diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/DatadogCronetEngineBuilderTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/DatadogCronetEngineBuilderTest.kt index b91830f978..2e6a3cd970 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/DatadogCronetEngineBuilderTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/DatadogCronetEngineBuilderTest.kt @@ -10,6 +10,7 @@ import com.datadog.android.cronet.DatadogCronetEngine.Companion.CRONET_NETWORK_I import com.datadog.android.cronet.internal.DatadogRequestFinishedInfoListener import com.datadog.android.rum.ExperimentalRumApi import com.datadog.android.rum.RumResourceAttributesProvider +import com.datadog.android.rum.configuration.RumResourceInstrumentationConfiguration import com.datadog.android.rum.internal.net.RumResourceInstrumentationAssert import com.datadog.android.utils.forge.Configurator import fr.xgouchet.elmyr.Forge @@ -338,47 +339,55 @@ internal class DatadogCronetEngineBuilderTest { @OptIn(ExperimentalRumApi::class) @Test - fun `M propagate RumResourceAttributesProvider W setRumResourceAttributesProvider()`() { + fun `M propagate RumResourceAttributesProvider W setCustomRumInstrumentation()`() { // Given val customProvider = mock() whenever(mockBuilderDelegate.build()).thenReturn(mock()) + val customConfig = RumResourceInstrumentationConfiguration() + .setRumResourceAttributesProvider(customProvider) // When - val builder = testedBuilder.setRumResourceAttributesProvider(customProvider) + val builder = testedBuilder.enableRumResourceInstrumentation(customConfig) val engine = builder.build() // Then assertThat(builder).isSameAs(testedBuilder) check(engine is DatadogCronetEngine) - RumResourceInstrumentationAssert.assertThat(engine.rumResourceInstrumentation) + RumResourceInstrumentationAssert.assertThat(engine.rumResourceInstrumentation!!) .hasRumResourceAttributesProvider(customProvider) } @OptIn(ExperimentalRumApi::class) @Test - fun `M propagate sdkInstanceName W setSdkInstanceName()`(@StringForgery sdkInstanceName: String) { + fun `M propagate sdkInstanceName W setCustomRumInstrumentation()`(@StringForgery sdkInstanceName: String) { + // Given + val customConfig = RumResourceInstrumentationConfiguration() + .setSdkInstanceName(sdkInstanceName) + // When - val builder = testedBuilder.setSdkInstanceName(sdkInstanceName) + val builder = testedBuilder.enableRumResourceInstrumentation(customConfig) val engine = builder.build() // Then check(engine is DatadogCronetEngine) assertThat(builder).isSameAs(testedBuilder) - RumResourceInstrumentationAssert.assertThat(engine.rumResourceInstrumentation) + RumResourceInstrumentationAssert.assertThat(engine.rumResourceInstrumentation!!) .hasSdkInstanceName(sdkInstanceName) } + @OptIn(ExperimentalRumApi::class) @Test fun `M use Cronet as network layer name W build()`() { // Given whenever(mockBuilderDelegate.build()).thenReturn(mock()) + testedBuilder.enableRumResourceInstrumentation(RumResourceInstrumentationConfiguration()) // When val engine = testedBuilder.build() // Then check(engine is DatadogCronetEngine) - RumResourceInstrumentationAssert.assertThat(engine.rumResourceInstrumentation) + RumResourceInstrumentationAssert.assertThat(engine.rumResourceInstrumentation!!) .hasNetworkLayerName(CRONET_NETWORK_INSTRUMENTATION_NAME) } @@ -387,6 +396,7 @@ internal class DatadogCronetEngineBuilderTest { fun `M propagate executor W setListenerExecutor()`() { // Given val customExecutor = mock() + testedBuilder.enableRumResourceInstrumentation(RumResourceInstrumentationConfiguration()) // When val builder = testedBuilder.setListenerExecutor(customExecutor) diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetHttpRequestInfoModifierTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetHttpRequestInfoModifierTest.kt new file mode 100644 index 0000000000..fa831487da --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetHttpRequestInfoModifierTest.kt @@ -0,0 +1,250 @@ +/* + * 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.cronet.internal + +import com.datadog.android.core.internal.net.HttpSpec +import com.datadog.android.cronet.DatadogCronetEngine +import com.datadog.android.utils.forge.Configurator +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.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness +import java.util.concurrent.Executor + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class CronetHttpRequestInfoModifierTest { + + @Mock + lateinit var mockExecutor: Executor + + @Mock + lateinit var mockEngine: DatadogCronetEngine + + @Mock + lateinit var mockCallback: DatadogRequestCallback + + @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") + lateinit var fakeUrl: String + + private lateinit var fakeRequestInfo: CronetHttpRequestInfo + private lateinit var testedRequestInfoBuilder: CronetHttpRequestInfoBuilder + + @BeforeEach + fun `set up`(forge: Forge) { + val requestContext = DatadogCronetRequestContext( + url = fakeUrl, + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(forge.anElementFrom(HttpSpec.Method.values())) } + fakeRequestInfo = CronetHttpRequestInfo(requestContext) + testedRequestInfoBuilder = fakeRequestInfo.newBuilder() + } + + @Test + fun `M update url W setUrl()`( + @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") newUrl: String + ) { + // When + testedRequestInfoBuilder.setUrl(newUrl) + val result = testedRequestInfoBuilder.build() + + // Then + assertThat(result.url).isEqualTo(newUrl) + } + + @Test + fun `M add single header W addHeader() { single value }`( + @StringForgery headerKey: String, + @StringForgery headerValue: String + ) { + // When + testedRequestInfoBuilder.addHeader(headerKey, headerValue) + val result = testedRequestInfoBuilder.build() + + // Then + assertThat(result.headers[headerKey]).containsExactly(headerValue) + } + + @Test + fun `M add multiple values W addHeader() { multiple values }`( + @StringForgery headerKey: String, + @StringForgery headerValue1: String, + @StringForgery headerValue2: String, + @StringForgery headerValue3: String + ) { + // When + testedRequestInfoBuilder.addHeader(headerKey, headerValue1, headerValue2, headerValue3) + val result = testedRequestInfoBuilder.build() + + // Then + assertThat(result.headers[headerKey]).containsExactly(headerValue1, headerValue2, headerValue3) + } + + @Test + fun `M append values W addHeader() { called multiple times }`( + @StringForgery headerKey: String, + @StringForgery headerValue1: String, + @StringForgery headerValue2: String + ) { + // When + testedRequestInfoBuilder.addHeader(headerKey, headerValue1) + testedRequestInfoBuilder.addHeader(headerKey, headerValue2) + val result = testedRequestInfoBuilder.build() + + // Then + assertThat(result.headers[headerKey]).containsExactly(headerValue1, headerValue2) + } + + @Test + fun `M remove header W removeHeader()`( + @StringForgery headerKey: String, + @StringForgery headerValue: String + ) { + // Given + testedRequestInfoBuilder.addHeader(headerKey, headerValue) + + // When + val result = testedRequestInfoBuilder.removeHeader(headerKey).build() + + // Then + assertThat(result.headers[headerKey]).isNull() + } + + @Test + fun `M remove all values W removeHeader() { multiple values }`( + @StringForgery headerKey: String, + @StringForgery headerValue1: String, + @StringForgery headerValue2: String + ) { + // Given + testedRequestInfoBuilder.addHeader(headerKey, headerValue1, headerValue2) + + // When + val result = testedRequestInfoBuilder.removeHeader(headerKey).build() + + // Then + assertThat(result.headers[headerKey]).isNull() + } + + @Test + fun `M not affect other headers W removeHeader()`( + @StringForgery headerKey1: String, + @StringForgery headerValue1: String, + @StringForgery headerKey2: String, + @StringForgery headerValue2: String + ) { + // Given + testedRequestInfoBuilder.addHeader(headerKey1, headerValue1) + testedRequestInfoBuilder.addHeader(headerKey2, headerValue2) + + // When + val result = testedRequestInfoBuilder.removeHeader(headerKey1).build() + + // Then + assertThat(result.headers[headerKey1]).isNull() + assertThat(result.headers[headerKey2]).containsExactly(headerValue2) + } + + @Test + fun `M add tag W addTag()`(forge: Forge) { + // Given + val fakeTag = FakeTag(forge.anAlphabeticalString()) + + // When + testedRequestInfoBuilder.addTag(FakeTag::class.java, fakeTag) + val result = testedRequestInfoBuilder.build() + + // Then + assertThat(result.tag(FakeTag::class.java)).isEqualTo(fakeTag) + } + + @Test + fun `M return CronetHttpRequestInfo W result()`() { + // When + val result = testedRequestInfoBuilder.build() + + // Then + assertThat(result).isInstanceOf(CronetHttpRequestInfo::class.java) + } + + @Test + fun `M preserve original url W result() { no modifications }`() { + // When + val result = testedRequestInfoBuilder.build() + + // Then + assertThat(result.url).isEqualTo(fakeUrl) + } + + @Test + fun `M preserve original method W result() { no modifications }`() { + // When + val result = testedRequestInfoBuilder.build() + + // Then + assertThat(result.method).isEqualTo(fakeRequestInfo.method) + } + + @Test + fun `M preserve original annotations W result()`(forge: Forge) { + // Given + val existingAnnotation = FakeTag(forge.anAlphabeticalString()) + testedRequestInfoBuilder.addTag(FakeTag::class.java, existingAnnotation) + + // When + val intermediateBuilder = testedRequestInfoBuilder.build().newBuilder() + intermediateBuilder.addTag(OtherFakeTag::class.java, OtherFakeTag(forge.anAlphabeticalString())) + val result = intermediateBuilder.build() + + // Then + assertThat(result.tag(FakeTag::class.java)).isEqualTo(existingAnnotation) + } + + @Test + fun `M replace header W replaceHeader()`( + @StringForgery headerKey: String, + @StringForgery oldValue: String, + @StringForgery newValue: String + ) { + // Given + testedRequestInfoBuilder.addHeader(headerKey, oldValue) + + // When + val result = testedRequestInfoBuilder.replaceHeader(headerKey, newValue).build() + + // Then + assertThat(result.headers[headerKey]).containsExactly(newValue) + } + + @Test + fun `M not add tag W addTag() { null value }`() { + // When + testedRequestInfoBuilder.addTag(FakeTag::class.java, null) + val result = testedRequestInfoBuilder.build() + + // Then + assertThat(result.tag(FakeTag::class.java)).isNull() + } + + private data class FakeTag(val value: String) + private data class OtherFakeTag(val value: String) +} diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestInfoTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestInfoTest.kt index e6f11a671f..d7f8d8cf2e 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestInfoTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestInfoTest.kt @@ -7,6 +7,7 @@ package com.datadog.android.cronet.internal import com.datadog.android.core.internal.net.HttpSpec +import com.datadog.android.cronet.DatadogCronetEngine import com.datadog.android.utils.forge.Configurator import com.datadog.tools.unit.forge.exhaustiveAttributes import fr.xgouchet.elmyr.Forge @@ -25,6 +26,7 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import java.util.concurrent.Executor @Extensions( ExtendWith(MockitoExtension::class), @@ -36,9 +38,17 @@ internal class CronetRequestInfoTest { @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") lateinit var fakeUrl: String - lateinit var fakeMethod: String - lateinit var fakeHeaders: Map> + lateinit var fakeHeaders: MutableMap> + + @Mock + lateinit var mockExecutor: Executor + + @Mock + lateinit var mockEngine: DatadogCronetEngine + + @Mock + lateinit var mockCallback: DatadogRequestCallback @Mock lateinit var mockUploadDataProvider: UploadDataProvider @@ -46,19 +56,19 @@ internal class CronetRequestInfoTest { @BeforeEach fun `set up`(forge: Forge) { fakeMethod = forge.anElementFrom(HttpSpec.Method.values()) - fakeHeaders = forge.exhaustiveAttributes().mapValues { listOf(it.value.toString()) } + fakeHeaders = forge.exhaustiveAttributes().mapValues { listOf(it.value.toString()) }.toMutableMap() } @Test fun `M return url W url property`() { // Given - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders, - uploadDataProvider = null, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.url @@ -70,13 +80,13 @@ internal class CronetRequestInfoTest { @Test fun `M return method W method property`() { // Given - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders, - uploadDataProvider = null, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.method @@ -88,13 +98,18 @@ internal class CronetRequestInfoTest { @Test fun `M return headers W headers property`() { // Given - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders, - uploadDataProvider = null, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + fakeHeaders.forEach { (key, values) -> + values.forEach { value -> + requestContext.addHeader(key, value) + } + } + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.headers @@ -104,37 +119,45 @@ internal class CronetRequestInfoTest { } @Test - fun `M return annotations W annotations property`( + fun `M return annotations W tag() property`( forge: Forge ) { // Given - val fakeAnnotations = listOf( - forge.anAlphabeticalString(), - forge.anInt(), - forge.aBool(), - forge.aChar(), - forge.anInt().toByte(), - forge.aLong(), - forge.aFloat(), - forge.aDouble() - ) - val requestInfo = CronetHttpRequestInfo( + val fakeString = forge.anAlphabeticalString() + val fakeInt = forge.anInt() + val fakeBool = forge.aBool() + val fakeChar = forge.aChar() + val fakeByte = forge.anInt().toByte() + val fakeLong = forge.aLong() + val fakeFloat = forge.aFloat() + val fakeDouble = forge.aDouble() + + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders, - uploadDataProvider = null, - annotations = fakeAnnotations - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + requestContext.setTag(String::class.java, fakeString) + requestContext.setTag(Int::class.java, fakeInt) + requestContext.setTag(Boolean::class.java, fakeBool) + requestContext.setTag(Char::class.java, fakeChar) + requestContext.setTag(Byte::class.java, fakeByte) + requestContext.setTag(Long::class.java, fakeLong) + requestContext.setTag(Float::class.java, fakeFloat) + requestContext.setTag(Double::class.java, fakeDouble) + + val requestInfo = CronetHttpRequestInfo(requestContext) // Then - assertThat(requestInfo.tag(String::class.java)).isEqualTo(fakeAnnotations[0]) - assertThat(requestInfo.tag(Int::class.java)).isEqualTo(fakeAnnotations[1]) - assertThat(requestInfo.tag(Boolean::class.java)).isEqualTo(fakeAnnotations[2]) - assertThat(requestInfo.tag(Char::class.java)).isEqualTo(fakeAnnotations[3]) - assertThat(requestInfo.tag(Byte::class.java)).isEqualTo(fakeAnnotations[4]) - assertThat(requestInfo.tag(Long::class.java)).isEqualTo(fakeAnnotations[5]) - assertThat(requestInfo.tag(Float::class.java)).isEqualTo(fakeAnnotations[6]) - assertThat(requestInfo.tag(Double::class.java)).isEqualTo(fakeAnnotations[7]) + assertThat(requestInfo.tag(String::class.java)).isEqualTo(fakeString) + assertThat(requestInfo.tag(Int::class.java)).isEqualTo(fakeInt) + assertThat(requestInfo.tag(Boolean::class.java)).isEqualTo(fakeBool) + assertThat(requestInfo.tag(Char::class.java)).isEqualTo(fakeChar) + assertThat(requestInfo.tag(Byte::class.java)).isEqualTo(fakeByte) + assertThat(requestInfo.tag(Long::class.java)).isEqualTo(fakeLong) + assertThat(requestInfo.tag(Float::class.java)).isEqualTo(fakeFloat) + assertThat(requestInfo.tag(Double::class.java)).isEqualTo(fakeDouble) } @Test @@ -142,13 +165,14 @@ internal class CronetRequestInfoTest { @StringForgery fakeContentType: String ) { // Given - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders + mapOf(HttpSpec.Headers.CONTENT_TYPE to listOf(fakeContentType)), - uploadDataProvider = null, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + requestContext.addHeader(HttpSpec.Headers.CONTENT_TYPE, fakeContentType) + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.contentType @@ -160,13 +184,13 @@ internal class CronetRequestInfoTest { @Test fun `M return null W contentType property { no content type header }`() { // Given - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders, - uploadDataProvider = null, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + val requestInfo = CronetHttpRequestInfo(requestContext) // When val actual = requestInfo.contentType @@ -178,13 +202,16 @@ internal class CronetRequestInfoTest { @Test fun `M return null W tag() { annotation type does not match }`(forge: Forge) { // Given - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders, - uploadDataProvider = null, - annotations = listOf(forge.aString(), forge.anInt(), forge.aBool()) - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + requestContext.setTag(String::class.java, forge.aString()) + requestContext.setTag(Int::class.java, forge.anInt()) + requestContext.setTag(Boolean::class.java, forge.aBool()) + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.tag(Double::class.java) @@ -196,13 +223,13 @@ internal class CronetRequestInfoTest { @Test fun `M return null W tag() { no annotations }`() { // Given - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders, - uploadDataProvider = null, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.tag(String::class.java) @@ -217,13 +244,14 @@ internal class CronetRequestInfoTest { ) { // Given whenever(mockUploadDataProvider.length).thenReturn(fakeLength) - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders, - uploadDataProvider = mockUploadDataProvider, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + requestContext.setUploadDataProvider(mockUploadDataProvider, mockExecutor) + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.contentLength() @@ -238,13 +266,15 @@ internal class CronetRequestInfoTest { ) { // Given whenever(mockUploadDataProvider.length).thenReturn(fakeLength) - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders + mapOf(HttpSpec.Headers.CONTENT_LENGTH to listOf(fakeLength.toString())), - uploadDataProvider = mockUploadDataProvider, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + requestContext.addHeader(HttpSpec.Headers.CONTENT_LENGTH, fakeLength.toString()) + requestContext.setUploadDataProvider(mockUploadDataProvider, mockExecutor) + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.contentLength() @@ -257,13 +287,14 @@ internal class CronetRequestInfoTest { fun `M return null W contentLength() { upload data provider length is unknown }`() { // Given whenever(mockUploadDataProvider.length).thenReturn(-1L) - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = emptyMap(), - uploadDataProvider = mockUploadDataProvider, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + requestContext.setUploadDataProvider(mockUploadDataProvider, mockExecutor) + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.contentLength() @@ -275,13 +306,13 @@ internal class CronetRequestInfoTest { @Test fun `M return null W contentLength() { no upload data provider }`() { // Given - val requestInfo = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = fakeMethod, - headers = fakeHeaders, - uploadDataProvider = null, - annotations = emptyList() - ) + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(fakeMethod) } + val requestInfo = CronetHttpRequestInfo(requestContext) // When val result = requestInfo.contentLength() diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestResourceIdentifierTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestResourceIdentifierTest.kt index 1406507e6b..ffdb0d7f56 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestResourceIdentifierTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestResourceIdentifierTest.kt @@ -7,6 +7,7 @@ package com.datadog.android.cronet.internal import com.datadog.android.core.internal.net.HttpSpec +import com.datadog.android.cronet.DatadogCronetEngine import com.datadog.android.rum.internal.net.RumResourceInstrumentation.Companion.buildResourceId import com.datadog.android.utils.forge.Configurator import fr.xgouchet.elmyr.annotation.StringForgery @@ -20,6 +21,7 @@ import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource +import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.doReturn @@ -27,6 +29,7 @@ import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.quality.Strictness import java.io.IOException +import java.util.concurrent.Executor @Extensions( ExtendWith(MockitoExtension::class), @@ -45,6 +48,15 @@ internal class CronetRequestResourceIdentifierTest { @StringForgery private lateinit var fakeBody: String + @Mock + lateinit var mockEngine: DatadogCronetEngine + + @Mock + lateinit var mockCallback: DatadogRequestCallback + + @Mock + lateinit var mockExecutor: Executor + private var fakeContentLength: Long = 0L @BeforeEach @@ -215,14 +227,21 @@ internal class CronetRequestResourceIdentifierTest { method: String ) { // Given - val request = CronetHttpRequestInfo( + val requestContext = DatadogCronetRequestContext( url = fakeUrl, - method = method, - headers = mapOf(HttpSpec.Headers.CONTENT_TYPE to listOf(fakeContentType)), - uploadDataProvider = mock { on { length } doThrow IOException("") }, - annotations = emptyList() + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(method) } + + requestContext.addHeader(HttpSpec.Headers.CONTENT_TYPE, fakeContentType) + requestContext.setUploadDataProvider( + mock { on { length } doThrow IOException("") }, + mockExecutor ) + val request = CronetHttpRequestInfo(requestContext) + // When val actual = request.uniqueId @@ -235,17 +254,23 @@ internal class CronetRequestResourceIdentifierTest { url: String = fakeUrl, method: String = HttpSpec.Method.GET, contentLength: Long? = null, - contentType: String? = null, - annotations: List = emptyList() - ) = CronetHttpRequestInfo( - url = url, - method = method, - headers = contentType?.let { mapOf(HttpSpec.Headers.CONTENT_TYPE to listOf(it)) } ?: emptyMap(), - uploadDataProvider = contentLength?.let { - mock { on { length } doReturn contentLength } - }, - annotations = annotations - ) + contentType: String? = null + ): CronetHttpRequestInfo { + val requestContext = DatadogCronetRequestContext( + url = url, + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(method) } + contentType?.let { requestContext.addHeader(HttpSpec.Headers.CONTENT_TYPE, it) } + contentLength?.let { + requestContext.setUploadDataProvider( + mock { on { length } doReturn contentLength }, + mockExecutor + ) + } + return CronetHttpRequestInfo(requestContext) + } private val CronetHttpRequestInfo.uniqueId: String get() = buildResourceId(this, false).key diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogCronetEngineTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogCronetEngineTest.kt index caf51613e9..bbf3ded229 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogCronetEngineTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogCronetEngineTest.kt @@ -8,11 +8,13 @@ package com.datadog.android.cronet.internal import com.datadog.android.cronet.DatadogCronetEngine import com.datadog.android.rum.internal.net.RumResourceInstrumentation +import com.datadog.android.trace.internal.ApmNetworkInstrumentation import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat import org.chromium.net.BidirectionalStream import org.chromium.net.CronetEngine import org.chromium.net.NetworkQualityRttListener @@ -47,13 +49,17 @@ internal class DatadogCronetEngineTest { @Mock lateinit var mockRumResourceInstrumentation: RumResourceInstrumentation + @Mock + lateinit var mockApmNetworkInstrumentation: ApmNetworkInstrumentation + lateinit var testedEngine: DatadogCronetEngine @BeforeEach fun setup() { testedEngine = DatadogCronetEngine( mockDelegate, - mockRumResourceInstrumentation + rumResourceInstrumentation = mockRumResourceInstrumentation, + apmNetworkInstrumentation = null ) } @@ -282,11 +288,65 @@ internal class DatadogCronetEngineTest { val result = testedEngine.newUrlRequestBuilder(url, mockCallback, mockExecutor) // Then - verify(mockDelegate).newUrlRequestBuilder( - eq(url), - any(), - eq(mockExecutor) + assertThat(result).isInstanceOf(DatadogUrlRequestBuilder::class.java) + } + + @Test + fun `M return DatadogUrlRequestBuilder with tracingInstrumentation W newUrlRequestBuilder() {tracing enabled}`( + @StringForgery url: String + ) { + // Given + testedEngine = DatadogCronetEngine( + mockDelegate, + rumResourceInstrumentation = mockRumResourceInstrumentation, + apmNetworkInstrumentation = mockApmNetworkInstrumentation ) - check(result is DatadogUrlRequestBuilder) + val mockCallback = mock() + val mockExecutor = mock() + val mockBuilder = mock() + whenever( + mockDelegate.newUrlRequestBuilder( + eq(url), + any(), + eq(mockExecutor) + ) + ).thenReturn(mockBuilder) + + // When + val result = testedEngine.newUrlRequestBuilder(url, mockCallback, mockExecutor) + + // Then + assertThat(result).isInstanceOf(DatadogUrlRequestBuilder::class.java) + assertThat(testedEngine.apmNetworkInstrumentation) + .isSameAs(mockApmNetworkInstrumentation) + } + + @Test + fun `M return DatadogUrlRequestBuilder with null tracingInstrumentation W newUrlRequestBuilder()`( + @StringForgery url: String + ) { + // Given + testedEngine = DatadogCronetEngine( + mockDelegate, + rumResourceInstrumentation = mockRumResourceInstrumentation, + apmNetworkInstrumentation = null + ) + val mockCallback = mock() + val mockExecutor = mock() + val mockBuilder = mock() + whenever( + mockDelegate.newUrlRequestBuilder( + eq(url), + any(), + eq(mockExecutor) + ) + ).thenReturn(mockBuilder) + + // When + val result = testedEngine.newUrlRequestBuilder(url, mockCallback, mockExecutor) + + // Then + assertThat(result).isInstanceOf(DatadogUrlRequestBuilder::class.java) + assertThat(testedEngine.apmNetworkInstrumentation).isNull() } } diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogCronetRequestContextTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogCronetRequestContextTest.kt new file mode 100644 index 0000000000..7b7152a187 --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogCronetRequestContextTest.kt @@ -0,0 +1,572 @@ +/* + * 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.cronet.internal + +import com.datadog.android.core.internal.net.HttpSpec +import com.datadog.android.cronet.DatadogCronetEngine +import com.datadog.android.rum.internal.net.RumResourceInstrumentation +import com.datadog.android.trace.internal.ApmNetworkInstrumentation +import com.datadog.android.trace.internal.net.RequestTraceState +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +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.chromium.net.RequestFinishedInfo +import org.chromium.net.UploadDataProvider +import org.chromium.net.UrlRequest +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.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.nio.ByteBuffer +import java.util.concurrent.Executor + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogCronetRequestContextTest { + + @Mock + lateinit var mockEngine: DatadogCronetEngine + + @Mock + lateinit var mockCallback: DatadogRequestCallback + + @Mock + lateinit var mockExecutor: Executor + + @Mock + lateinit var mockDelegateBuilder: UrlRequest.Builder + + @Mock + lateinit var mockUrlRequest: UrlRequest + + @Mock + lateinit var mockTracingState: RequestTraceState + + @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") + lateinit var fakeUrl: String + + lateinit var testedContext: DatadogCronetRequestContext + + @BeforeEach + fun `set up`() { + whenever(mockEngine.newDelegateUrlRequestBuilder(any(), any(), any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.build()) doReturn mockUrlRequest + whenever(mockDelegateBuilder.setHttpMethod(any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.addHeader(any(), any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.disableCache()) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.allowDirectExecutor()) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.setPriority(any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.bindToNetwork(any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.setTrafficStatsTag(any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.setTrafficStatsUid(any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.setUploadDataProvider(any(), any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.setRequestFinishedListener(any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.setRawCompressionDictionary(any(), any(), any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.addRequestAnnotation(any())) doReturn mockDelegateBuilder + + testedContext = DatadogCronetRequestContext( + url = fakeUrl, + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ) + } + + @Test + fun `M store url W initialization`() { + // When/Then + assertThat(testedContext.url).isEqualTo(fakeUrl) + } + + @Test + fun `M use GET as default method W initialization`() { + // Then + assertThat(testedContext.method).isEqualTo(HttpSpec.Method.GET) + } + + @Test + fun `M update method W setHttpMethod()`() { + // Given + val newMethod = HttpSpec.Method.POST + + // When + testedContext.setHttpMethod(newMethod) + + // Then + assertThat(testedContext.method).isEqualTo(newMethod) + } + + @Test + fun `M add single header W addHeader() { single value }`( + @StringForgery headerKey: String, + @StringForgery headerValue: String + ) { + // When + testedContext.addHeader(headerKey, headerValue) + + // Then + assertThat(testedContext.headers[headerKey]).containsExactly(headerValue) + } + + @Test + fun `M add multiple values W addHeader() { multiple values }`( + @StringForgery headerKey: String, + @StringForgery headerValue1: String, + @StringForgery headerValue2: String, + @StringForgery headerValue3: String + ) { + // When + testedContext.addHeader(headerKey, headerValue1, headerValue2, headerValue3) + + // Then + assertThat(testedContext.headers[headerKey]) + .containsExactly(headerValue1, headerValue2, headerValue3) + } + + @Test + fun `M append values W addHeader() { called multiple times }`( + @StringForgery headerKey: String, + @StringForgery headerValue1: String, + @StringForgery headerValue2: String + ) { + // When + testedContext.addHeader(headerKey, headerValue1) + testedContext.addHeader(headerKey, headerValue2) + + // Then + assertThat(testedContext.headers[headerKey]).containsExactly(headerValue1, headerValue2) + } + + @Test + fun `M remove header W removeHeader()`( + @StringForgery headerKey: String, + @StringForgery headerValue: String + ) { + // Given + testedContext.addHeader(headerKey, headerValue) + + // When + testedContext.removeHeader(headerKey) + + // Then + assertThat(testedContext.headers[headerKey]).isNull() + } + + @Test + fun `M not affect other headers W removeHeader()`( + @StringForgery headerKey1: String, + @StringForgery headerValue1: String, + @StringForgery headerKey2: String, + @StringForgery headerValue2: String + ) { + // Given + testedContext.addHeader(headerKey1, headerValue1) + testedContext.addHeader(headerKey2, headerValue2) + + // When + testedContext.removeHeader(headerKey1) + + // Then + assertThat(testedContext.headers[headerKey1]).isNull() + assertThat(testedContext.headers[headerKey2]).containsExactly(headerValue2) + } + + @Test + fun `M return empty map W headers { no headers added }`() { + // Then + assertThat(testedContext.headers).isEmpty() + } + + @Test + fun `M store tag W setTag()`( + @StringForgery fakeTag: String + ) { + // When + testedContext.setTag(String::class.java, fakeTag) + + // Then + assertThat(testedContext.annotations).contains(fakeTag) + } + + @Test + fun `M remove tag W setTag() { null value }`( + @StringForgery fakeTag: String + ) { + // Given + testedContext.setTag(String::class.java, fakeTag) + + // When + testedContext.setTag(String::class.java, null) + + // Then + assertThat(testedContext.annotations).doesNotContain(fakeTag) + } + + @Test + fun `M store multiple tags W setTag() { different types }`( + @StringForgery fakeStringTag: String, + @IntForgery fakeIntTag: Int + ) { + // When + testedContext.setTag(String::class.java, fakeStringTag) + testedContext.setTag(Int::class.java, fakeIntTag) + + // Then + assertThat(testedContext.annotations).contains(fakeStringTag) + assertThat(testedContext.annotations).contains(fakeIntTag) + } + + @Test + fun `M return empty list W annotations { no tags added }`() { + // Then + assertThat(testedContext.annotations).isEmpty() + } + + @Test + fun `M add annotation W addRequestAnnotation()`( + @StringForgery fakeAnnotation: String + ) { + // When + testedContext.addRequestAnnotation(fakeAnnotation) + + // Then + assertThat(testedContext.annotations).contains(fakeAnnotation) + } + + @Test + fun `M store uploadDataProvider W setUploadDataProvider()`() { + // Given + val mockUploadProvider = mock() + val mockUploadExecutor = mock() + + // When + testedContext.setUploadDataProvider(mockUploadProvider, mockUploadExecutor) + + // Then + assertThat(testedContext.uploadDataProvider).isSameAs(mockUploadProvider) + } + + @Test + fun `M return null W uploadDataProvider { not set }`() { + // Then + assertThat(testedContext.uploadDataProvider).isNull() + } + + @Test + fun `M create independent copy W copy()`( + @StringForgery headerKey: String, + @StringForgery headerValue: String, + @StringForgery tagValue: String + ) { + // Given + testedContext.addHeader(headerKey, headerValue) + testedContext.setTag(String::class.java, tagValue) + + // When + val copiedContext = testedContext.copy() + + // Then + assertThat(copiedContext.url).isEqualTo(testedContext.url) + assertThat(copiedContext.method).isEqualTo(testedContext.method) + assertThat(copiedContext.headers).isEqualTo(testedContext.headers) + } + + @Test + fun `M not affect original W copy() + modify copy`( + @StringForgery headerKey: String, + @StringForgery headerValue: String, + @StringForgery newHeaderKey: String, + @StringForgery newHeaderValue: String + ) { + // Given + testedContext.addHeader(headerKey, headerValue) + val copiedContext = testedContext.copy() + + // When + copiedContext.addHeader(newHeaderKey, newHeaderValue) + + // Then + assertThat(testedContext.headers[newHeaderKey]).isNull() + assertThat(copiedContext.headers[newHeaderKey]).containsExactly(newHeaderValue) + } + + @Test + fun `M build CronetHttpRequestInfo W buildRequestInfo()`() { + // When + val requestInfo = testedContext.buildRequestInfo() + + // Then + assertThat(requestInfo).isInstanceOf(CronetHttpRequestInfo::class.java) + assertThat(requestInfo.url).isEqualTo(fakeUrl) + assertThat(requestInfo.method).isEqualTo(HttpSpec.Method.GET) + } + + @Test + fun `M build UrlRequest W buildCronetRequest()`() { + // Given + val requestInfo = testedContext.buildRequestInfo() + + // When + val request = testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockEngine).newDelegateUrlRequestBuilder(fakeUrl, mockCallback, mockExecutor) + verify(mockDelegateBuilder).build() + assertThat(request).isSameAs(mockUrlRequest) + } + + @Test + fun `M add requestInfo as annotation W buildCronetRequest()`() { + // Given + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).addRequestAnnotation(requestInfo) + } + + @Test + fun `M add tracingState as annotation W buildCronetRequest() { tracingState not null }`() { + // Given + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).addRequestAnnotation(mockTracingState) + } + + @Test + fun `M apply disableCache W buildCronetRequest() { cache disabled }`() { + // Given + testedContext.disableCache() + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).disableCache() + } + + @Test + fun `M apply allowDirectExecutor W buildCronetRequest() { direct executor allowed }`() { + // Given + testedContext.allowDirectExecutor() + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).allowDirectExecutor() + } + + @Test + fun `M apply priority W buildCronetRequest() { priority set }`( + @IntForgery fakePriority: Int + ) { + // Given + testedContext.setPriority(fakePriority) + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).setPriority(fakePriority) + } + + @Test + fun `M apply networkHandle W buildCronetRequest() { network bound }`( + @LongForgery fakeNetworkHandle: Long + ) { + // Given + testedContext.bindToNetwork(fakeNetworkHandle) + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).bindToNetwork(fakeNetworkHandle) + } + + @Test + fun `M apply trafficStatsTag W buildCronetRequest() { traffic stats tag set }`( + @IntForgery fakeTag: Int + ) { + // Given + testedContext.setTrafficStatsTag(fakeTag) + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).setTrafficStatsTag(fakeTag) + } + + @Test + fun `M apply trafficStatsUid W buildCronetRequest() { traffic stats uid set }`( + @IntForgery fakeUid: Int + ) { + // Given + testedContext.setTrafficStatsUid(fakeUid) + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).setTrafficStatsUid(fakeUid) + } + + @Test + fun `M apply uploadDataProvider W buildCronetRequest() { upload data provider set }`() { + // Given + val mockUploadProvider = mock() + val mockUploadExecutor = mock() + testedContext.setUploadDataProvider(mockUploadProvider, mockUploadExecutor) + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).setUploadDataProvider(mockUploadProvider, mockUploadExecutor) + } + + @Test + fun `M apply requestFinishedListener W buildCronetRequest() { listener set }`() { + // Given + val mockListener = mock() + testedContext.setRequestFinishedListener(mockListener) + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).setRequestFinishedListener(mockListener) + } + + @Test + fun `M apply rawCompressionDictionary W buildCronetRequest() { dictionary set }`( + @StringForgery fakeDictionaryId: String, + forge: Forge + ) { + // Given + val fakeHash = ByteArray(5) { forge.anInt().toByte() } + val mockDictionary = mock() + testedContext.setRawCompressionDictionary(fakeHash, mockDictionary, fakeDictionaryId) + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).setRawCompressionDictionary(fakeHash, mockDictionary, fakeDictionaryId) + } + + @Test + fun `M return networkTracingInstrumentation W networkTracingInstrumentation { tracing enabled }`() { + // Given + val mockTracingInstrumentation = mock() + whenever(mockEngine.apmNetworkInstrumentation) doReturn mockTracingInstrumentation + + // When + val result = testedContext.apmNetworkInstrumentation + + // Then + assertThat(result).isSameAs(mockTracingInstrumentation) + } + + @Test + fun `M return null W networkTracingInstrumentation { tracing disabled }`() { + // Given + whenever(mockEngine.apmNetworkInstrumentation) doReturn null + + // When + val result = testedContext.apmNetworkInstrumentation + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return rumResourceInstrumentation W rumResourceInstrumentation { rum enabled }`() { + // Given + val mockRumInstrumentation = mock() + whenever(mockEngine.rumResourceInstrumentation) doReturn mockRumInstrumentation + + // When + val result = testedContext.rumResourceInstrumentation + + // Then + assertThat(result).isSameAs(mockRumInstrumentation) + } + + @Test + fun `M return null W rumResourceInstrumentation { rum disabled }`() { + // Given + whenever(mockEngine.rumResourceInstrumentation) doReturn null + + // When + val result = testedContext.rumResourceInstrumentation + + // Then + assertThat(result).isNull() + } + + @Test + fun `M update url W url setter`( + @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") newUrl: String + ) { + // When + testedContext.url = newUrl + + // Then + assertThat(testedContext.url).isEqualTo(newUrl) + } + + @Test + fun `M apply additional annotations W buildCronetRequest() { annotations added }`( + @StringForgery fakeAnnotation: String + ) { + // Given + testedContext.addRequestAnnotation(fakeAnnotation) + val requestInfo = testedContext.buildRequestInfo() + + // When + testedContext.buildCronetRequest(requestInfo, mockTracingState) + + // Then + verify(mockDelegateBuilder).addRequestAnnotation(fakeAnnotation) + } +} diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogRequestCallbackTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogRequestCallbackTest.kt new file mode 100644 index 0000000000..3f6f2a98d8 --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogRequestCallbackTest.kt @@ -0,0 +1,224 @@ +/* + * 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.cronet.internal + +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.trace.ApmNetworkTracingScope +import com.datadog.android.trace.internal.ApmNetworkInstrumentation +import com.datadog.android.trace.internal.net.RequestTraceState +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.IntForgery +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.chromium.net.CronetException +import org.chromium.net.UrlRequest +import org.chromium.net.UrlResponseInfo +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.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.IOException +import java.nio.ByteBuffer + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DatadogRequestCallbackTest { + + private lateinit var testedCallback: DatadogRequestCallback + + @Mock + lateinit var mockDelegate: UrlRequest.Callback + + @Mock + lateinit var mockApmNetworkInstrumentation: ApmNetworkInstrumentation + + @Mock + lateinit var mockUrlRequest: UrlRequest + + @Mock + lateinit var mockUrlResponseInfo: UrlResponseInfo + + @Mock + lateinit var mockRequestModifier: HttpRequestInfoBuilder + + @Mock + lateinit var mockByteBuffer: ByteBuffer + + @Mock + lateinit var mockCronetException: CronetException + + @BeforeEach + fun `set up`() { + testedCallback = DatadogRequestCallback(mockDelegate, mockApmNetworkInstrumentation) + } + + @Test + fun `M delegate onRedirectReceived W onRedirectReceived()`( + @StringForgery fakeNewLocationUrl: String + ) { + // When + testedCallback.onRedirectReceived(mockUrlRequest, mockUrlResponseInfo, fakeNewLocationUrl) + + // Then + verify(mockDelegate).onRedirectReceived(mockUrlRequest, mockUrlResponseInfo, fakeNewLocationUrl) + } + + @Test + fun `M delegate onResponseStarted W onResponseStarted()`() { + // When + testedCallback.onResponseStarted(mockUrlRequest, mockUrlResponseInfo) + + // Then + verify(mockDelegate).onResponseStarted(mockUrlRequest, mockUrlResponseInfo) + } + + @Test + fun `M delegate onReadCompleted W onReadCompleted()`() { + // When + testedCallback.onReadCompleted(mockUrlRequest, mockUrlResponseInfo, mockByteBuffer) + + // Then + verify(mockDelegate).onReadCompleted(mockUrlRequest, mockUrlResponseInfo, mockByteBuffer) + } + + @Test + fun `M delegate onSucceeded and call apmNetworkInstrumentation W onSucceeded() {traceState set}`( + @IntForgery(min = 100, max = 599) fakeStatusCode: Int, + @StringForgery fakeUrl: String + ) { + // Given + whenever(mockUrlResponseInfo.httpStatusCode) doReturn fakeStatusCode + whenever(mockUrlResponseInfo.url) doReturn fakeUrl + whenever(mockUrlResponseInfo.allHeaders) doReturn emptyMap() + val fakeTracingState = RequestTraceState(mockRequestModifier, ApmNetworkTracingScope.DETAILED, isSampled = true) + testedCallback.traceState = fakeTracingState + + // When + testedCallback.onSucceeded(mockUrlRequest, mockUrlResponseInfo) + + // Then + verify(mockDelegate).onSucceeded(mockUrlRequest, mockUrlResponseInfo) + argumentCaptor { + verify(mockApmNetworkInstrumentation).onResponseSucceeded(any(), capture()) + org.assertj.core.api.Assertions.assertThat(firstValue.statusCode).isEqualTo(fakeStatusCode) + } + } + + @Test + fun `M only delegate onSucceeded W onSucceeded() {traceState null}`() { + // Given + testedCallback.traceState = null + + // When + testedCallback.onSucceeded(mockUrlRequest, mockUrlResponseInfo) + + // Then + verify(mockDelegate).onSucceeded(mockUrlRequest, mockUrlResponseInfo) + verifyNoInteractions(mockApmNetworkInstrumentation) + } + + @Test + fun `M only delegate onSucceeded W onSucceeded() {apmNetworkInstrumentation null}`() { + // Given + testedCallback = DatadogRequestCallback(mockDelegate, null) + val fakeTracingState = RequestTraceState(mockRequestModifier, ApmNetworkTracingScope.DETAILED, isSampled = true) + testedCallback.traceState = fakeTracingState + + // When + testedCallback.onSucceeded(mockUrlRequest, mockUrlResponseInfo) + + // Then + verify(mockDelegate).onSucceeded(mockUrlRequest, mockUrlResponseInfo) + } + + @Test + fun `M delegate onFailed and call apmNetworkInstrumentation W onFailed() {traceState set}`() { + // Given + val fakeTracingState = RequestTraceState(mockRequestModifier, ApmNetworkTracingScope.DETAILED, isSampled = true) + testedCallback.traceState = fakeTracingState + + // When + testedCallback.onFailed(mockUrlRequest, mockUrlResponseInfo, mockCronetException) + + // Then + verify(mockDelegate).onFailed(mockUrlRequest, mockUrlResponseInfo, mockCronetException) + verify(mockApmNetworkInstrumentation).onResponseFailed(fakeTracingState, mockCronetException) + } + + @Test + fun `M delegate onFailed with IOException W onFailed() {traceState set, error null}`() { + // Given + val fakeTracingState = RequestTraceState(mockRequestModifier, ApmNetworkTracingScope.DETAILED, isSampled = true) + testedCallback.traceState = fakeTracingState + + // When + testedCallback.onFailed(mockUrlRequest, mockUrlResponseInfo, null) + + // Then + verify(mockDelegate).onFailed(mockUrlRequest, mockUrlResponseInfo, null) + argumentCaptor { + verify(mockApmNetworkInstrumentation).onResponseFailed(any(), capture()) + assertThat(firstValue).isInstanceOf(IOException::class.java) + assertThat(firstValue.message).isEqualTo("Response failed") + } + } + + @Test + fun `M only delegate onFailed W onFailed() {traceState null}`() { + // Given + testedCallback.traceState = null + + // When + testedCallback.onFailed(mockUrlRequest, mockUrlResponseInfo, mockCronetException) + + // Then + verify(mockDelegate).onFailed(mockUrlRequest, mockUrlResponseInfo, mockCronetException) + verifyNoInteractions(mockApmNetworkInstrumentation) + } + + @Test + fun `M only delegate onFailed W onFailed() {apmNetworkInstrumentation null}`() { + // Given + testedCallback = DatadogRequestCallback(mockDelegate, null) + val fakeTracingState = RequestTraceState(mockRequestModifier, ApmNetworkTracingScope.DETAILED, isSampled = true) + testedCallback.traceState = fakeTracingState + + // When + testedCallback.onFailed(mockUrlRequest, mockUrlResponseInfo, mockCronetException) + + // Then + verify(mockDelegate).onFailed(mockUrlRequest, mockUrlResponseInfo, mockCronetException) + } + + @Test + fun `M store and retrieve traceState W traceState property`() { + // Given + val fakeTracingState = RequestTraceState(mockRequestModifier, ApmNetworkTracingScope.DETAILED, isSampled = true) + + // When + testedCallback.traceState = fakeTracingState + + // Then + assertThat(testedCallback.traceState).isSameAs(fakeTracingState) + } +} diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestBuilderTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestBuilderTest.kt index 5611762fc3..1a1e796525 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestBuilderTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestBuilderTest.kt @@ -6,10 +6,9 @@ package com.datadog.android.cronet.internal -import com.datadog.android.api.instrumentation.network.HttpRequestInfo import com.datadog.android.api.instrumentation.network.RequestInfoAssert import com.datadog.android.core.internal.net.HttpSpec -import com.datadog.android.rum.internal.net.RumResourceInstrumentation +import com.datadog.android.cronet.DatadogCronetEngine import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.LongForgery @@ -26,10 +25,7 @@ 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.any -import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock -import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.nio.ByteBuffer @@ -49,230 +45,217 @@ internal class DatadogUrlRequestBuilderTest { lateinit var mockDelegate: UrlRequest.Builder @Mock - lateinit var mockRumResourceInstrumentation: RumResourceInstrumentation + lateinit var mockEngine: DatadogCronetEngine + + @Mock + lateinit var mockCallback: DatadogRequestCallback + + @Mock + lateinit var mockExecutor: Executor lateinit var fakeUrl: String + lateinit var requestContext: DatadogCronetRequestContext + lateinit var testedBuilder: DatadogUrlRequestBuilder @BeforeEach fun setup(forge: Forge) { - fakeUrl = forge.aStringMatching("https://[a-z0-9]+\\.com") - testedBuilder = DatadogUrlRequestBuilder( + fakeUrl = forge.aStringMatching("http(s?)://[a-z]+\\.com/[a-z]+") + requestContext = DatadogCronetRequestContext( url = fakeUrl, - delegate = mockDelegate, - rumResourceInstrumentation = mockRumResourceInstrumentation + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ) + testedBuilder = DatadogUrlRequestBuilder( + cronetInstrumentationStateHolder = mockCallback, + requestContext = requestContext ) whenever(mockDelegate.build()).thenReturn(mockRequest) } @Test - fun `M delegate to builder W setHttpMethod()`( + fun `M store method locally W setHttpMethod()`( @StringForgery method: String ) { - // Given - whenever(mockDelegate.setHttpMethod(method)).thenReturn(mockDelegate) - // When testedBuilder.setHttpMethod(method) // Then - verify(mockDelegate).setHttpMethod(method) + val requestInfo = requestContext.buildRequestInfo() + assertThat(requestInfo.method).isEqualTo(method) } @Test - fun `M delegate to builder W addHeader()`( + fun `M store header locally W addHeader()`( @StringForgery header: String, @StringForgery value: String ) { - // Given - whenever(mockDelegate.addHeader(header, value)).thenReturn(mockDelegate) - // When testedBuilder.addHeader(header, value) // Then - verify(mockDelegate).addHeader(header, value) + val requestInfo = requestContext.buildRequestInfo() + assertThat(requestInfo.headers[header]).contains(value) } @Test - fun `M delegate to builder W disableCache()`() { - // Given - whenever(mockDelegate.disableCache()).thenReturn(mockDelegate) + fun `M return DatadogUrlRequest W build()`() { + // When + val result = testedBuilder.build() + // Then + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) + } + + @Test + fun `M create request info with annotations W build`( + @StringForgery key: String, + @StringForgery value: String, + @StringForgery tag: String + ) { // When - testedBuilder.disableCache() + testedBuilder.setHttpMethod(HttpSpec.Method.POST) + .addHeader(key, value) + .addHeader(HttpSpec.Headers.CONTENT_TYPE, HttpSpec.ContentType.APPLICATION_GRPC_JSON) + .addRequestAnnotation(tag) + + val requestInfo = requestContext.buildRequestInfo() // Then - verify(mockDelegate).disableCache() + RequestInfoAssert.assertThat(requestInfo) + .hasUrl(fakeUrl) + .hasHeader(key, value) + .hasHeader(HttpSpec.Headers.CONTENT_TYPE, HttpSpec.ContentType.APPLICATION_GRPC_JSON) + .hasContentType(HttpSpec.ContentType.APPLICATION_GRPC_JSON) + .hasTag(String::class.java, tag) + .hasMethod(HttpSpec.Method.POST) } @Test - fun `M delegate to builder W setPriority()`( + fun `M store priority W setPriority()`( @IntForgery priority: Int ) { - // Given - whenever(mockDelegate.setPriority(priority)).thenReturn(mockDelegate) - // When testedBuilder.setPriority(priority) + val result = testedBuilder.build() // Then - verify(mockDelegate).setPriority(priority) + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) } @Test - fun `M delegate to builder W setUploadDataProvider()`() { + fun `M store uploadDataProvider W setUploadDataProvider()`() { // Given val mockUploadDataProvider = mock() - val mockExecutor = mock() - whenever(mockDelegate.setUploadDataProvider(mockUploadDataProvider, mockExecutor)) - .thenReturn(mockDelegate) + val mockUploadExecutor = mock() // When - testedBuilder.setUploadDataProvider(mockUploadDataProvider, mockExecutor) + testedBuilder.setUploadDataProvider(mockUploadDataProvider, mockUploadExecutor) + val result = testedBuilder.build() // Then - verify(mockDelegate).setUploadDataProvider(mockUploadDataProvider, mockExecutor) + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) } @Test - fun `M delegate to builder W allowDirectExecutor()`() { - // Given - whenever(mockDelegate.allowDirectExecutor()).thenReturn(mockDelegate) + fun `M store disableCache W disableCache()`() { + // When + testedBuilder.disableCache() + val result = testedBuilder.build() + // Then + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) + } + + @Test + fun `M store allowDirectExecutor W allowDirectExecutor()`() { // When testedBuilder.allowDirectExecutor() + val result = testedBuilder.build() // Then - verify(mockDelegate).allowDirectExecutor() + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) } @Test - fun `M delegate to builder W addRequestAnnotation()`() { + fun `M store annotation W addRequestAnnotation()`() { // Given val mockAnnotation = mock() - whenever(mockDelegate.addRequestAnnotation(mockAnnotation)).thenReturn(mockDelegate) // When testedBuilder.addRequestAnnotation(mockAnnotation) + val result = testedBuilder.build() // Then - verify(mockDelegate).addRequestAnnotation(mockAnnotation) + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) } @Test - fun `M delegate to builder W bindToNetwork()`( + fun `M store networkHandle W bindToNetwork()`( @LongForgery networkHandle: Long ) { - // Given - whenever(mockDelegate.bindToNetwork(networkHandle)).thenReturn(mockDelegate) - // When testedBuilder.bindToNetwork(networkHandle) + val result = testedBuilder.build() // Then - verify(mockDelegate).bindToNetwork(networkHandle) + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) } @Test - fun `M delegate to builder W setTrafficStatsTag()`( + fun `M store trafficStatsTag W setTrafficStatsTag()`( @IntForgery tag: Int ) { - // Given - whenever(mockDelegate.setTrafficStatsTag(tag)).thenReturn(mockDelegate) - // When testedBuilder.setTrafficStatsTag(tag) + val result = testedBuilder.build() // Then - verify(mockDelegate).setTrafficStatsTag(tag) + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) } @Test - fun `M delegate to builder W setTrafficStatsUid()`( + fun `M store trafficStatsUid W setTrafficStatsUid()`( @IntForgery uid: Int ) { - // Given - whenever(mockDelegate.setTrafficStatsUid(uid)).thenReturn(mockDelegate) - // When testedBuilder.setTrafficStatsUid(uid) + val result = testedBuilder.build() // Then - verify(mockDelegate).setTrafficStatsUid(uid) + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) } @Test - fun `M delegate to builder W setRequestFinishedListener()`() { + fun `M store requestFinishedListener W setRequestFinishedListener()`() { // Given val mockListener = mock() - whenever(mockDelegate.setRequestFinishedListener(mockListener)).thenReturn(mockDelegate) // When testedBuilder.setRequestFinishedListener(mockListener) + val result = testedBuilder.build() // Then - verify(mockDelegate).setRequestFinishedListener(mockListener) + assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) } @Test - fun `M delegate to builder W setRawCompressionDictionary()`( + fun `M store rawCompressionDictionary W setRawCompressionDictionary()`( @StringForgery dictionaryId: String, forge: Forge ) { // Given val hash = ByteArray(5) { forge.anInt().toByte() } val mockDictionary = mock() - whenever(mockDelegate.setRawCompressionDictionary(hash, mockDictionary, dictionaryId)) - .thenReturn(mockDelegate) // When testedBuilder.setRawCompressionDictionary(hash, mockDictionary, dictionaryId) - - // Then - verify(mockDelegate).setRawCompressionDictionary(hash, mockDictionary, dictionaryId) - } - - @Test - fun `M return DatadogUrlRequest W build()`() { - // Given - val mockUrlRequest = mock() - whenever(mockDelegate.build()).thenReturn(mockUrlRequest) - - // When val result = testedBuilder.build() // Then - verify(mockDelegate).build() assertThat(result).isInstanceOf(DatadogUrlRequest::class.java) } - - @Test - fun `M addAnnotation W build`( - @StringForgery key: String, - @StringForgery value: String, - @StringForgery tag: String - ) { - // When - testedBuilder.setHttpMethod(HttpSpec.Method.POST) - .addHeader(key, value) - .addHeader(HttpSpec.Headers.CONTENT_TYPE, HttpSpec.ContentType.APPLICATION_GRPC_JSON) - .addRequestAnnotation(tag) - .build() - - // Then - verify(mockDelegate).addRequestAnnotation(any()) - argumentCaptor { - verify(mockDelegate).addRequestAnnotation(capture()) - RequestInfoAssert.assertThat(firstValue) - .hasUrl(fakeUrl) - .hasHeader(key, value) - .hasHeader(HttpSpec.Headers.CONTENT_TYPE, HttpSpec.ContentType.APPLICATION_GRPC_JSON) - .hasContentType(HttpSpec.ContentType.APPLICATION_GRPC_JSON) - .hasTag(String::class.java, tag) - .hasMethod(HttpSpec.Method.POST) - } - } } diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestTest.kt index 0c00262715..fa38cd415c 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/DatadogUrlRequestTest.kt @@ -6,11 +6,12 @@ package com.datadog.android.cronet.internal -import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.core.internal.net.HttpSpec +import com.datadog.android.cronet.DatadogCronetEngine import com.datadog.android.rum.internal.net.RumResourceInstrumentation import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.BoolForgery -import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat @@ -22,11 +23,15 @@ 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.any +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.nio.ByteBuffer +import java.util.concurrent.Executor @Extensions( ExtendWith(MockitoExtension::class), @@ -37,22 +42,45 @@ import java.nio.ByteBuffer internal class DatadogUrlRequestTest { @Mock - lateinit var mockDelegate: UrlRequest + lateinit var mockBuiltRequest: UrlRequest @Mock lateinit var mockRumResourceInstrumentation: RumResourceInstrumentation - @Forgery - lateinit var fakeRequestInfo: HttpRequestInfo + @Mock + lateinit var mockEngine: DatadogCronetEngine + + @Mock + lateinit var mockCallback: DatadogRequestCallback + + @Mock + lateinit var mockExecutor: Executor + + @Mock + lateinit var mockDelegateBuilder: UrlRequest.Builder lateinit var testedRequest: DatadogUrlRequest @BeforeEach - fun setup() { + fun setup(forge: Forge) { + whenever(mockEngine.rumResourceInstrumentation) doReturn mockRumResourceInstrumentation + whenever(mockEngine.apmNetworkInstrumentation) doReturn null + whenever(mockEngine.newDelegateUrlRequestBuilder(any(), any(), any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.setHttpMethod(any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.addHeader(any(), any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.addRequestAnnotation(any())) doReturn mockDelegateBuilder + whenever(mockDelegateBuilder.build()) doReturn mockBuiltRequest + + val requestContext = DatadogCronetRequestContext( + url = forge.aStringMatching("http(s?)://[a-z]+\\.com/[a-z]+"), + engine = mockEngine, + datadogRequestCallback = mockCallback, + executor = mockExecutor + ).apply { setHttpMethod(forge.anElementFrom(HttpSpec.Method.values())) } + testedRequest = DatadogUrlRequest( - info = fakeRequestInfo, - delegate = mockDelegate, - rumResourceInstrumentation = mockRumResourceInstrumentation + requestContext = requestContext, + cronetInstrumentationStateHolder = mockCallback ) } @@ -62,62 +90,71 @@ internal class DatadogUrlRequestTest { testedRequest.start() // Then - verify(mockDelegate).start() + verify(mockBuiltRequest).start() } @Test fun `M delegate to request W followRedirect()`() { + // Given + testedRequest.start() + // When testedRequest.followRedirect() // Then - verify(mockDelegate).followRedirect() + verify(mockBuiltRequest).followRedirect() } @Test fun `M delegate to request W read()`() { // Given + testedRequest.start() val mockBuffer = mock() // When testedRequest.read(mockBuffer) // Then - verify(mockDelegate).read(mockBuffer) + verify(mockBuiltRequest).read(mockBuffer) } @Test fun `M delegate to request W cancel()`() { + // Given + testedRequest.start() + // When testedRequest.cancel() // Then - verify(mockDelegate).cancel() + verify(mockBuiltRequest).cancel() } @Test fun `M delegate to request W isDone`(@BoolForgery fakeDone: Boolean) { // Given - whenever(mockDelegate.isDone).thenReturn(fakeDone) + testedRequest.start() + whenever(mockBuiltRequest.isDone).thenReturn(fakeDone) // When val result = testedRequest.isDone // Then - verify(mockDelegate).isDone + verify(mockBuiltRequest).isDone assertThat(result).isEqualTo(fakeDone) } @Test fun `M delegate to request W getStatus()`() { // Given + testedRequest.start() val mockListener = mock() // When testedRequest.getStatus(mockListener) // Then - verify(mockDelegate).getStatus(mockListener) + verify(mockBuiltRequest).getStatus(mockListener) } @Test @@ -126,7 +163,7 @@ internal class DatadogUrlRequestTest { testedRequest.start() // Then - verify(mockRumResourceInstrumentation).startResource(fakeRequestInfo) + verify(mockRumResourceInstrumentation).startResource(any()) } @Test @@ -135,6 +172,62 @@ internal class DatadogUrlRequestTest { testedRequest.start() // Then - mockRumResourceInstrumentation.sendWaitForResourceTimingEvent(fakeRequestInfo) + verify(mockRumResourceInstrumentation).sendWaitForResourceTimingEvent(any()) + } + + // region Edge cases: methods called before start() + + @Test + fun `M do nothing W cancel() { before start }`() { + // When + testedRequest.cancel() + + // Then + verifyNoInteractions(mockBuiltRequest) + } + + @Test + fun `M do nothing W followRedirect() { before start }`() { + // When + testedRequest.followRedirect() + + // Then + verifyNoInteractions(mockBuiltRequest) + } + + @Test + fun `M do nothing W read() { before start }`() { + // Given + val mockBuffer = mock() + + // When + testedRequest.read(mockBuffer) + + // Then + verifyNoInteractions(mockBuiltRequest) + } + + @Test + fun `M do nothing W getStatus() { before start }`() { + // Given + val mockListener = mock() + + // When + testedRequest.getStatus(mockListener) + + // Then + verifyNoInteractions(mockBuiltRequest) } + + @Test + fun `M return false W isDone { before start }`() { + // When + val result = testedRequest.isDone + + // Then + assertThat(result).isFalse() + verifyNoInteractions(mockBuiltRequest) + } + + // endregion } diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/UrlRequestBuilderExtTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/UrlRequestBuilderExtTest.kt new file mode 100644 index 0000000000..b645a6df73 --- /dev/null +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/UrlRequestBuilderExtTest.kt @@ -0,0 +1,135 @@ +/* + * 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.cronet.internal + +import com.datadog.android.utils.forge.Configurator +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.chromium.net.UrlRequest +import org.junit.jupiter.api.Test +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.inOrder +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class UrlRequestBuilderExtTest { + + @Mock + lateinit var mockBuilder: UrlRequest.Builder + + @Test + fun `M add single header W addHeaders() {single key single value}`( + @StringForgery fakeKey: String, + @StringForgery fakeValue: String + ) { + // Given + val headers = mapOf(fakeKey to listOf(fakeValue)) + + // When + mockBuilder.addHeaders(headers) + + // Then + verify(mockBuilder).addHeader(fakeKey, fakeValue) + } + + @Test + fun `M add multiple values for same key W addHeaders() {single key multiple values}`( + @StringForgery fakeKey: String, + @StringForgery fakeValue1: String, + @StringForgery fakeValue2: String, + @StringForgery fakeValue3: String + ) { + // Given + val headers = mapOf(fakeKey to listOf(fakeValue1, fakeValue2, fakeValue3)) + + // When + mockBuilder.addHeaders(headers) + + // Then + val inOrder = inOrder(mockBuilder) + inOrder.verify(mockBuilder).addHeader(fakeKey, fakeValue1) + inOrder.verify(mockBuilder).addHeader(fakeKey, fakeValue2) + inOrder.verify(mockBuilder).addHeader(fakeKey, fakeValue3) + } + + @Test + fun `M add headers for multiple keys W addHeaders() {multiple keys}`( + @StringForgery fakeKey1: String, + @StringForgery fakeKey2: String, + @StringForgery fakeValue1: String, + @StringForgery fakeValue2: String + ) { + // Given + val headers = mapOf( + fakeKey1 to listOf(fakeValue1), + fakeKey2 to listOf(fakeValue2) + ) + + // When + mockBuilder.addHeaders(headers) + + // Then + verify(mockBuilder).addHeader(fakeKey1, fakeValue1) + verify(mockBuilder).addHeader(fakeKey2, fakeValue2) + } + + @Test + fun `M not add any headers W addHeaders() {empty map}`() { + // When + mockBuilder.addHeaders(emptyMap>()) + + // Then + verifyNoInteractions(mockBuilder) + } + + @Test + fun `M skip empty value lists W addHeaders() {key with empty list}`( + @StringForgery fakeKey: String + ) { + // Given + val headers = mapOf(fakeKey to emptyList()) + + // When + mockBuilder.addHeaders(headers) + + // Then + verifyNoInteractions(mockBuilder) + } + + @Test + fun `M add all headers from forge W addHeaders() {random headers}`(forge: Forge) { + // Given + val fakeHeaders = forge.aMap(size = forge.anInt(1, 5)) { + forge.anAlphabeticalString() to forge.aList(size = forge.anInt(1, 3)) { + forge.anAlphabeticalString() + } + } + + // When + mockBuilder.addHeaders(fakeHeaders) + + // Then + fakeHeaders.forEach { (key, values) -> + values.forEach { value -> + verify(mockBuilder).addHeader(key, value) + } + } + } +} diff --git a/integrations/dd-sdk-android-okhttp-otel/src/main/kotlin/com/datadog/android/okhttp/otel/OkHttpExt.kt b/integrations/dd-sdk-android-okhttp-otel/src/main/kotlin/com/datadog/android/okhttp/otel/OkHttpExt.kt index 894504a23f..0eb2302644 100644 --- a/integrations/dd-sdk-android-okhttp-otel/src/main/kotlin/com/datadog/android/okhttp/otel/OkHttpExt.kt +++ b/integrations/dd-sdk-android-okhttp-otel/src/main/kotlin/com/datadog/android/okhttp/otel/OkHttpExt.kt @@ -6,7 +6,7 @@ package com.datadog.android.okhttp.otel -import com.datadog.android.okhttp.TraceContext +import com.datadog.android.okhttp.internal.OkHttpHttpRequestInfoBuilder import com.datadog.android.trace.api.DatadogTracingConstants import com.datadog.android.trace.internal.DatadogTracingToolkit import com.datadog.opentelemetry.trace.OtelSpan @@ -21,15 +21,18 @@ import okhttp3.Request fun Request.Builder.addParentSpan(span: Span): Request.Builder = apply { // very fragile and assumes that Datadog Tracer is used // we need to trigger sampling decision at this point, because we are doing context propagation out of OpenTelemetry - val tracingContext = if (span is OtelSpan) { + val builder = OkHttpHttpRequestInfoBuilder(this) + if (span is OtelSpan) { DatadogTracingToolkit.setTracingSamplingPriorityIfNecessary(span.datadogSpanContext) - TraceContext( + DatadogTracingToolkit.propagationHelper.setTraceContext( + builder, span.spanContext.traceId, span.spanContext.spanId, span.datadogSpanContext.samplingPriority ) } else { - TraceContext( + DatadogTracingToolkit.propagationHelper.setTraceContext( + builder, span.spanContext.traceId, span.spanContext.spanId, if (span.spanContext.isSampled) { @@ -39,6 +42,4 @@ fun Request.Builder.addParentSpan(span: Span): Request.Builder = apply { } ) } - @Suppress("UnsafeThirdPartyFunctionCall") // the context will always be a TraceContext - tag(TraceContext::class.java, tracingContext) } diff --git a/integrations/dd-sdk-android-okhttp-otel/src/test/kotlin/com/datadog/android/okhttp/otel/OkHttpExtTest.kt b/integrations/dd-sdk-android-okhttp-otel/src/test/kotlin/com/datadog/android/okhttp/otel/OkHttpExtTest.kt index b686d4d9b0..f95d34c853 100644 --- a/integrations/dd-sdk-android-okhttp-otel/src/test/kotlin/com/datadog/android/okhttp/otel/OkHttpExtTest.kt +++ b/integrations/dd-sdk-android-okhttp-otel/src/test/kotlin/com/datadog/android/okhttp/otel/OkHttpExtTest.kt @@ -6,8 +6,8 @@ package com.datadog.android.okhttp.otel -import com.datadog.android.okhttp.TraceContext import com.datadog.android.trace.api.DatadogTracingConstants +import com.datadog.android.trace.internal.net.TraceContext import com.datadog.tools.unit.forge.BaseConfigurator import fr.xgouchet.elmyr.annotation.BoolForgery import fr.xgouchet.elmyr.annotation.StringForgery diff --git a/integrations/dd-sdk-android-okhttp/api/apiSurface b/integrations/dd-sdk-android-okhttp/api/apiSurface index 881b826646..0ea4bc1b36 100644 --- a/integrations/dd-sdk-android-okhttp/api/apiSurface +++ b/integrations/dd-sdk-android-okhttp/api/apiSurface @@ -25,15 +25,15 @@ open class com.datadog.android.okhttp.DatadogInterceptor : com.datadog.android.o constructor(List) override fun build(): DatadogInterceptor fun setRumResourceAttributesProvider(com.datadog.android.rum.RumResourceAttributesProvider): Builder -data class com.datadog.android.okhttp.TraceContext - constructor(String, String, Int) -enum com.datadog.android.okhttp.TraceContextInjection - - ALL - - SAMPLED -open class com.datadog.android.okhttp.trace.DeterministicTraceSampler : com.datadog.android.core.sampling.DeterministicSampler - constructor(() -> Float) - constructor(Float) - constructor(Double) +typealias TraceContextInjection = com.datadog.android.trace.TraceContextInjection +class com.datadog.android.okhttp.internal.OkHttpHttpRequestInfoBuilder : com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder + constructor(okhttp3.Request.Builder) + override fun setUrl(String) + override fun addHeader(String, String) + override fun removeHeader(String) + override fun addTag(Class, T?) + override fun build(): com.datadog.android.api.instrumentation.network.HttpRequestInfo +typealias DeterministicTraceSampler = com.datadog.android.trace.DeterministicTraceSampler fun okhttp3.Request.Builder.parentSpan(com.datadog.android.trace.api.span.DatadogSpan): okhttp3.Request.Builder interface com.datadog.android.okhttp.trace.TracedRequestListener fun onRequestIntercepted(okhttp3.Request, com.datadog.android.trace.api.span.DatadogSpan, okhttp3.Response?, Throwable?) @@ -52,6 +52,6 @@ open class com.datadog.android.okhttp.trace.TracingInterceptor : okhttp3.Interce fun setTracedRequestListener(TracedRequestListener): R fun setTraceSampleRate(Float): R fun setTraceSampler(com.datadog.android.core.sampling.Sampler): R - fun setTraceContextInjection(com.datadog.android.okhttp.TraceContextInjection): R + fun setTraceContextInjection(com.datadog.android.trace.TraceContextInjection): R fun set404ResourcesRedacted(Boolean): R abstract fun build(): T diff --git a/integrations/dd-sdk-android-okhttp/api/dd-sdk-android-okhttp.api b/integrations/dd-sdk-android-okhttp/api/dd-sdk-android-okhttp.api index 3b71970de1..d9b23432cb 100644 --- a/integrations/dd-sdk-android-okhttp/api/dd-sdk-android-okhttp.api +++ b/integrations/dd-sdk-android-okhttp/api/dd-sdk-android-okhttp.api @@ -35,32 +35,18 @@ public final class com/datadog/android/okhttp/DatadogInterceptor$Builder : com/d public final fun setRumResourceAttributesProvider (Lcom/datadog/android/rum/RumResourceAttributesProvider;)Lcom/datadog/android/okhttp/DatadogInterceptor$Builder; } -public final class com/datadog/android/okhttp/TraceContext { - public fun (Ljava/lang/String;Ljava/lang/String;I)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()I - public final fun copy (Ljava/lang/String;Ljava/lang/String;I)Lcom/datadog/android/okhttp/TraceContext; - public static synthetic fun copy$default (Lcom/datadog/android/okhttp/TraceContext;Ljava/lang/String;Ljava/lang/String;IILjava/lang/Object;)Lcom/datadog/android/okhttp/TraceContext; - public fun equals (Ljava/lang/Object;)Z - public final fun getSamplingPriority ()I - public final fun getSpanId ()Ljava/lang/String; - public final fun getTraceId ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/datadog/android/okhttp/TraceContextInjection : java/lang/Enum { - public static final field ALL Lcom/datadog/android/okhttp/TraceContextInjection; - public static final field SAMPLED Lcom/datadog/android/okhttp/TraceContextInjection; - public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/okhttp/TraceContextInjection; - public static fun values ()[Lcom/datadog/android/okhttp/TraceContextInjection; -} - -public class com/datadog/android/okhttp/trace/DeterministicTraceSampler : com/datadog/android/core/sampling/DeterministicSampler { - public fun (D)V - public fun (F)V - public fun (Lkotlin/jvm/functions/Function0;)V +public final class com/datadog/android/okhttp/internal/OkHttpHttpRequestInfoBuilder : com/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder { + public fun (Lokhttp3/Request$Builder;)V + public synthetic fun addHeader (Ljava/lang/String;[Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public fun addHeader (Ljava/lang/String;[Ljava/lang/String;)Lcom/datadog/android/okhttp/internal/OkHttpHttpRequestInfoBuilder; + public synthetic fun addTag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public fun addTag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/datadog/android/okhttp/internal/OkHttpHttpRequestInfoBuilder; + public fun build ()Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo; + public synthetic fun removeHeader (Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public fun removeHeader (Ljava/lang/String;)Lcom/datadog/android/okhttp/internal/OkHttpHttpRequestInfoBuilder; + public fun replaceHeader (Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public synthetic fun setUrl (Ljava/lang/String;)Lcom/datadog/android/api/instrumentation/network/HttpRequestInfoBuilder; + public fun setUrl (Ljava/lang/String;)Lcom/datadog/android/okhttp/internal/OkHttpHttpRequestInfoBuilder; } public final class com/datadog/android/okhttp/trace/OkHttpRequestExtKt { @@ -82,7 +68,7 @@ public abstract class com/datadog/android/okhttp/trace/TracingInterceptor$BaseBu public abstract fun build ()Lcom/datadog/android/okhttp/trace/TracingInterceptor; public final fun set404ResourcesRedacted (Z)Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder; public final fun setSdkInstanceName (Ljava/lang/String;)Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder; - public final fun setTraceContextInjection (Lcom/datadog/android/okhttp/TraceContextInjection;)Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder; + public final fun setTraceContextInjection (Lcom/datadog/android/trace/TraceContextInjection;)Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder; public final fun setTraceSampleRate (F)Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder; public final fun setTraceSampler (Lcom/datadog/android/core/sampling/Sampler;)Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder; public final fun setTracedRequestListener (Lcom/datadog/android/okhttp/trace/TracedRequestListener;)Lcom/datadog/android/okhttp/trace/TracingInterceptor$BaseBuilder; diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/DatadogInterceptor.kt b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/DatadogInterceptor.kt index 128deadd28..6c1bfd836a 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/DatadogInterceptor.kt +++ b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/DatadogInterceptor.kt @@ -29,6 +29,7 @@ import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.monitor.AdvancedNetworkRumMonitor import com.datadog.android.rum.tracking.ViewTrackingStrategy +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.span.DatadogSpan import com.datadog.android.trace.api.tracer.DatadogTracer diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/TraceContextInjection.kt b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/TraceContextInjection.kt index 66a1a3f501..3942a8a557 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/TraceContextInjection.kt +++ b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/TraceContextInjection.kt @@ -6,24 +6,16 @@ package com.datadog.android.okhttp +import com.datadog.android.trace.TraceContextInjection + /** * Defines whether the trace context should be injected into all requests or only sampled ones. */ -enum class TraceContextInjection { - /** - * Injects trace context into all requests irrespective of the sampling decision. - * For example if the request trace is sampled out, the trace context will still be injected in your request - * headers but the sampling priority will be `0`. This will mean that the client will dictate the sampling priority - * on the server side and no trace will be created no matter the sampling rate at the server side. - */ - ALL, - - /** - * Injects trace context only into sampled requests. - * For example if the request trace is sampled out neither the trace context or the sampling priority will - * be injected into the request headers leaving the server side to make the sampling decision. - * This will mean that if the server side sampling rate is higher than the client side sampling rate there will - * be a chance that a trace will be created down the stream. - */ - SAMPLED -} +@Deprecated( + "Use com.datadog.android.trace.TraceContextInjection instead.", + replaceWith = ReplaceWith( + "TraceContextInjection", + imports = ["com.datadog.android.trace.TraceContextInjection"] + ) +) +typealias TraceContextInjection = TraceContextInjection diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/OkHttpHttpRequestInfo.kt b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/OkHttpHttpRequestInfo.kt index 97562e4d8e..373f3c68ad 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/OkHttpHttpRequestInfo.kt +++ b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/OkHttpHttpRequestInfo.kt @@ -7,6 +7,9 @@ package com.datadog.android.okhttp.internal import com.datadog.android.api.instrumentation.network.ExtendedRequestInfo import com.datadog.android.api.instrumentation.network.HttpRequestInfo +import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder +import com.datadog.android.api.instrumentation.network.MutableHttpRequestInfo +import com.datadog.android.lint.InternalApi import com.datadog.android.rum.internal.net.RumResourceInstrumentation import okhttp3.Request import okio.IOException @@ -25,7 +28,10 @@ internal fun Request.buildResourceId(generateUuid: Boolean) = generateUuid = generateUuid ) -internal class OkHttpHttpRequestInfo(internal val request: Request) : HttpRequestInfo, ExtendedRequestInfo { +internal class OkHttpHttpRequestInfo(internal val request: Request) : + HttpRequestInfo, + ExtendedRequestInfo, + MutableHttpRequestInfo { override val method: String get() = request.method override val url: String get() = request.url.toString() @@ -37,4 +43,33 @@ internal class OkHttpHttpRequestInfo(internal val request: Request) : HttpReques } catch (@Suppress("SwallowedException") _: IOException) { null } + + override fun newBuilder() = OkHttpHttpRequestInfoBuilder(request.newBuilder()) +} + +/** + * For internal usage only. + * + * [HttpRequestInfoBuilder] implementation for OkHttp requests. + * Allows modifying request properties such as URL, headers, and tags. + * + * @param requestBuilder the OkHttp request builder to modify. + */ +@InternalApi +@Suppress("UnsafeThirdPartyFunctionCall") // OkHttp builder methods are safe +class OkHttpHttpRequestInfoBuilder(private val requestBuilder: Request.Builder) : HttpRequestInfoBuilder { + + override fun setUrl(url: String) = apply { requestBuilder.url(url) } + + override fun addHeader(key: String, vararg values: String) = apply { + values.forEach { value -> + requestBuilder.addHeader(key, value) + } + } + + override fun removeHeader(key: String) = apply { requestBuilder.removeHeader(key) } + + override fun addTag(type: Class, tag: T?) = apply { requestBuilder.tag(type, tag) } + + override fun build(): HttpRequestInfo = OkHttpHttpRequestInfo(requestBuilder.build()) } diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/DeterministicTraceSampler.kt b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/DeterministicTraceSampler.kt index 661601baa4..1e79f56fe9 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/DeterministicTraceSampler.kt +++ b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/DeterministicTraceSampler.kt @@ -3,42 +3,15 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ - package com.datadog.android.okhttp.trace -import androidx.annotation.FloatRange -import com.datadog.android.core.sampling.DeterministicSampler -import com.datadog.android.okhttp.internal.utils.SpanSamplingIdProvider -import com.datadog.android.trace.api.span.DatadogSpan - -/** - * A [DeterministicSampler] using the TraceID of a Span to compute the sampling decision. - * - * @param sampleRateProvider Provider for the sample rate value which will be called each time - * the sampling decision needs to be made. All the values should be in the range [0;100]. - */ -open class DeterministicTraceSampler( - sampleRateProvider: () -> Float -) : DeterministicSampler( - SpanSamplingIdProvider::provideId, - sampleRateProvider -) { - - /** - * Creates a new instance lof [DeterministicSampler] with the given sample rate. - * - * @param sampleRate Sample rate to use. - */ - constructor( - @FloatRange(from = 0.0, to = 100.0) sampleRate: Float - ) : this({ sampleRate }) +import com.datadog.android.trace.DeterministicTraceSampler - /** - * Creates a new instance of [DeterministicSampler] with the given sample rate. - * - * @param sampleRate Sample rate to use. - */ - constructor( - @FloatRange(from = 0.0, to = 100.0) sampleRate: Double - ) : this(sampleRate.toFloat()) -} +@Deprecated( + "Use com.datadog.android.trace.DeterministicTraceSampler instead.", + replaceWith = ReplaceWith( + "DeterministicTraceSampler", + imports = ["com.datadog.android.trace.DeterministicTraceSampler"] + ) +) +typealias DeterministicTraceSampler = DeterministicTraceSampler diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracedRequestListener.kt b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracedRequestListener.kt index 4248f461ff..7cf4f5d343 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracedRequestListener.kt +++ b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracedRequestListener.kt @@ -12,18 +12,17 @@ import okhttp3.Request import okhttp3.Response /** - * Listener for automatically created [Span] around OkHttp [Request]. + * Listener for automatically created [DatadogSpan] around OkHttp [Request]. */ - @NoOpImplementation interface TracedRequestListener { /** * Notifies that a span was automatically created around an OkHttp [Request]. - * You can update the given [Span] (e.g.: add custom tags / baggage items) before it + * You can update the given [DatadogSpan] (e.g.: add custom tags / baggage items) before it * is persisted. Won't be called if [Request] wasn't sampled. * @param request the intercepted [Request] - * @param span the [Span] created around the intercepted [Request] + * @param span the [DatadogSpan] created around the intercepted [Request] * @param response the [Request] response in case of any * @param throwable in case an error occurred during the [Request] */ diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracingInterceptor.kt b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracingInterceptor.kt index e6b92db4a9..660dba9f5f 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracingInterceptor.kt +++ b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracingInterceptor.kt @@ -20,11 +20,11 @@ import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.telemetry.TracingHeaderTypesSet import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.lint.InternalApi -import com.datadog.android.okhttp.TraceContext -import com.datadog.android.okhttp.TraceContextInjection import com.datadog.android.okhttp.internal.trace.toInternalTracingHeaderType import com.datadog.android.trace.DatadogTracing +import com.datadog.android.trace.DeterministicTraceSampler import com.datadog.android.trace.GlobalDatadogTracer +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.DatadogTracingConstants.PrioritySampling import com.datadog.android.trace.api.DatadogTracingConstants.Tags @@ -34,6 +34,7 @@ import com.datadog.android.trace.api.tracer.DatadogTracer import com.datadog.android.trace.internal.DatadogTracingToolkit import com.datadog.android.trace.internal.RumContextPropagator import com.datadog.android.trace.internal.RumContextPropagator.Companion.extractRumContext +import com.datadog.android.trace.internal.net.TraceContext import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request @@ -867,14 +868,14 @@ internal constructor( "but you did not specify any first party hosts. " + "Your requests won't be traced.\n" + "To set a list of known hosts, you can use the " + - "Configuration.Builder::setFirstPartyHosts() method." + "Configuration.Builder.setFirstPartyHosts() method." internal const val WARNING_TRACING_DISABLED = "You added a TracingInterceptor to your OkHttpClient, " + "but you did not enable the TracingFeature. " + "Your requests won't be traced." internal const val WARNING_DEFAULT_TRACER = "You added a TracingInterceptor to your OkHttpClient, " + - "but you didn't register any AgentTracer.TracerAPI. " + + "but you didn't register any DatadogTracer. " + "We automatically created a local tracer for you." internal const val ERROR_STACK_OVERFLOW = diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorBuilderTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorBuilderTest.kt index 73a5ce0412..69128a235a 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorBuilderTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorBuilderTest.kt @@ -8,11 +8,12 @@ package com.datadog.android.okhttp import com.datadog.android.core.sampling.Sampler import com.datadog.android.okhttp.internal.RumResourceAttributesProviderCompatibilityAdapter -import com.datadog.android.okhttp.trace.DeterministicTraceSampler import com.datadog.android.okhttp.trace.NoOpTracedRequestListener import com.datadog.android.okhttp.trace.TracedRequestListener import com.datadog.android.rum.NoOpRumResourceAttributesProvider import com.datadog.android.rum.RumResourceAttributesProvider +import com.datadog.android.trace.DeterministicTraceSampler +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.span.DatadogSpan import com.datadog.tools.unit.extensions.TestConfigurationExtension diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorTest.kt index bb32588d6c..08711b2377 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorTest.kt @@ -11,7 +11,6 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.SdkCore import com.datadog.android.api.feature.Feature import com.datadog.android.internal.network.GraphQLHeaders -import com.datadog.android.okhttp.trace.DeterministicTraceSampler import com.datadog.android.okhttp.trace.NoOpTracedRequestListener import com.datadog.android.okhttp.trace.TracingInterceptor import com.datadog.android.okhttp.trace.TracingInterceptorNotSendingSpanTest @@ -23,6 +22,8 @@ import com.datadog.android.rum.RumResourceAttributesProvider import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.resource.ResourceId +import com.datadog.android.trace.DeterministicTraceSampler +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.tracer.DatadogTracer import com.datadog.tools.unit.extensions.TestConfigurationExtension diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutRumTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutRumTest.kt index d2332642e8..80eff7004f 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutRumTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutRumTest.kt @@ -13,6 +13,7 @@ import com.datadog.android.okhttp.trace.TracingInterceptor import com.datadog.android.okhttp.trace.TracingInterceptorTest import com.datadog.android.okhttp.utils.verifyLog import com.datadog.android.rum.RumResourceAttributesProvider +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.tracer.DatadogTracer import com.datadog.tools.unit.extensions.TestConfigurationExtension diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt index 9bd86dec41..6e7c4e2ae5 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/DatadogInterceptorWithoutTracesTest.kt @@ -27,6 +27,7 @@ import com.datadog.android.rum.RumResourceAttributesProvider import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.resource.ResourceId +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.api.propagation.DatadogPropagation import com.datadog.android.trace.api.span.DatadogSpan import com.datadog.android.trace.api.span.DatadogSpanBuilder diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/TracingInterceptorBuilderTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/TracingInterceptorBuilderTest.kt index 3fb360666a..b4a4d9bccc 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/TracingInterceptorBuilderTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/TracingInterceptorBuilderTest.kt @@ -7,10 +7,11 @@ package com.datadog.android.okhttp import com.datadog.android.core.sampling.Sampler -import com.datadog.android.okhttp.trace.DeterministicTraceSampler import com.datadog.android.okhttp.trace.NoOpTracedRequestListener import com.datadog.android.okhttp.trace.TracedRequestListener import com.datadog.android.okhttp.trace.TracingInterceptor +import com.datadog.android.trace.DeterministicTraceSampler +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.span.DatadogSpan import com.datadog.tools.unit.extensions.TestConfigurationExtension diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpHttpRequestInfoModifierTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpHttpRequestInfoModifierTest.kt new file mode 100644 index 0000000000..95b42d913b --- /dev/null +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpHttpRequestInfoModifierTest.kt @@ -0,0 +1,210 @@ +/* + * 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.okhttp.internal + +import com.datadog.android.api.instrumentation.network.tag +import com.datadog.tools.unit.forge.BaseConfigurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import okhttp3.Request +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.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(BaseConfigurator::class) +internal class OkHttpHttpRequestInfoModifierTest { + + @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") + lateinit var fakeUrl: String + + private lateinit var testedModifier: OkHttpHttpRequestInfoBuilder + + @BeforeEach + fun `set up`() { + val requestBuilder = Request.Builder().url(fakeUrl) + testedModifier = OkHttpHttpRequestInfoBuilder(requestBuilder) + } + + @Test + fun `M update url W setUrl()`( + @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") newUrl: String + ) { + // When + testedModifier.setUrl(newUrl) + val result = testedModifier.build() + + // Then + assertThat(result.url).isEqualTo(newUrl) + } + + @Test + fun `M add single header W addHeader() { single value }`( + @StringForgery headerKey: String, + @StringForgery headerValue: String + ) { + // When + testedModifier.addHeader(headerKey, headerValue) + val result = testedModifier.build() + + // Then + assertThat(result.headers[headerKey]).containsExactly(headerValue) + } + + @Test + fun `M add multiple values W addHeader() { multiple values }`( + @StringForgery headerKey: String, + @StringForgery headerValue1: String, + @StringForgery headerValue2: String, + @StringForgery headerValue3: String + ) { + // When + testedModifier.addHeader(headerKey, headerValue1, headerValue2, headerValue3) + val result = testedModifier.build() + + // Then + assertThat(result.headers[headerKey]).containsExactly(headerValue1, headerValue2, headerValue3) + } + + @Test + fun `M append values W addHeader() { called multiple times }`( + @StringForgery headerKey: String, + @StringForgery headerValue1: String, + @StringForgery headerValue2: String + ) { + // When + testedModifier.addHeader(headerKey, headerValue1) + testedModifier.addHeader(headerKey, headerValue2) + val result = testedModifier.build() + + // Then + assertThat(result.headers[headerKey]).containsExactly(headerValue1, headerValue2) + } + + @Test + fun `M remove header W removeHeader()`( + @StringForgery headerKey: String, + @StringForgery headerValue: String + ) { + // Given + testedModifier.addHeader(headerKey, headerValue) + + // When + val result = testedModifier.removeHeader(headerKey).build() + + // Then + assertThat(result.headers[headerKey]).isNull() + } + + @Test + fun `M remove all values W removeHeader() { multiple values }`( + @StringForgery headerKey: String, + @StringForgery headerValue1: String, + @StringForgery headerValue2: String + ) { + // Given + testedModifier.addHeader(headerKey, headerValue1, headerValue2) + + // When + val result = testedModifier.removeHeader(headerKey).build() + + // Then + assertThat(result.headers[headerKey]).isNull() + } + + @Test + fun `M not affect other headers W removeHeader()`( + @StringForgery headerKey1: String, + @StringForgery headerValue1: String, + @StringForgery headerKey2: String, + @StringForgery headerValue2: String + ) { + // Given + testedModifier.addHeader(headerKey1, headerValue1) + testedModifier.addHeader(headerKey2, headerValue2) + + // When + val result = testedModifier.removeHeader(headerKey1).build() + + // Then + assertThat(result.headers[headerKey1]).isNull() + assertThat(result.headers[headerKey2]).containsExactly(headerValue2) + } + + @Test + fun `M add tag W addTag()`(forge: Forge) { + // Given + val fakeTag = FakeTag(forge.anAlphabeticalString()) + + // When + testedModifier.addTag(FakeTag::class.java, fakeTag) + val result = testedModifier.build() + + // Then + assertThat(result.tag(FakeTag::class.java)).isEqualTo(fakeTag) + } + + @Test + fun `M remove tag W addTag() { null value }`( + @StringForgery fakeTag: String + ) { + // Given + testedModifier.addTag(String::class.java, fakeTag) + + // When + val result = testedModifier.addTag(String::class.java, null).build() + + // Then + assertThat(result.tag(String::class.java)).isNull() + } + + @Test + fun `M return OkHttpHttpRequestInfo W result()`() { + // When + val result = testedModifier.build() + + // Then + assertThat(result).isInstanceOf(OkHttpHttpRequestInfo::class.java) + } + + @Test + fun `M preserve original url W result() { no modifications }`() { + // When + val result = testedModifier.build() + + // Then + assertThat(result.url).isEqualTo(fakeUrl) + } + + @Test + fun `M replace header W replaceHeader()`( + @StringForgery headerKey: String, + @StringForgery oldValue: String, + @StringForgery newValue: String + ) { + // Given + testedModifier.addHeader(headerKey, oldValue) + + // When + val result = testedModifier.replaceHeader(headerKey, newValue).build() + + // Then + assertThat(result.headers[headerKey]).containsExactly(newValue) + } + private data class FakeTag(val value: String) +} diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/utils/forge/TraceContextFactory.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/utils/forge/TraceContextFactory.kt index 06132f8b62..dcc1ccadd1 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/utils/forge/TraceContextFactory.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/utils/forge/TraceContextFactory.kt @@ -6,7 +6,7 @@ package com.datadog.android.okhttp.internal.utils.forge -import com.datadog.android.okhttp.TraceContext +import com.datadog.android.trace.internal.net.TraceContext import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.ForgeryFactory diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt index 69d373bb74..fce0e542ee 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorContextInjectionSampledTest.kt @@ -11,12 +11,12 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.utils.loggableStackTrace -import com.datadog.android.okhttp.TraceContext -import com.datadog.android.okhttp.TraceContextInjection import com.datadog.android.okhttp.internal.utils.forge.OkHttpConfigurator import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.okhttp.utils.verifyLog +import com.datadog.android.trace.DeterministicTraceSampler +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.DatadogTracingConstants import com.datadog.android.trace.api.propagation.DatadogPropagation @@ -29,6 +29,7 @@ import com.datadog.android.trace.api.withMockPropagationHelper import com.datadog.android.trace.internal.DatadogPropagationHelper import com.datadog.android.trace.internal.DatadogTracingToolkit import com.datadog.android.trace.internal.fromHex +import com.datadog.android.trace.internal.net.TraceContext import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt index b346edf51f..ff4fe80305 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerNotSendingSpanTest.kt @@ -11,10 +11,10 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.utils.loggableStackTrace -import com.datadog.android.okhttp.TraceContextInjection import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.verifyLog +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.DatadogTracingConstants import com.datadog.android.trace.api.propagation.DatadogPropagation diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt index cd8cbc132c..87f4bcf22f 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNonDdTracerTest.kt @@ -11,10 +11,11 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.utils.loggableStackTrace -import com.datadog.android.okhttp.TraceContextInjection import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.verifyLog +import com.datadog.android.trace.DeterministicTraceSampler +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.DatadogTracingConstants import com.datadog.android.trace.api.propagation.DatadogPropagation diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt index aed9bd9102..bca57d7a29 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorNotSendingSpanTest.kt @@ -11,12 +11,12 @@ import com.datadog.android.api.feature.Feature import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.utils.loggableStackTrace -import com.datadog.android.okhttp.TraceContextInjection import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.okhttp.utils.verifyLog import com.datadog.android.rum.RumResourceMethod +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.DatadogTracingConstants import com.datadog.android.trace.api.propagation.DatadogPropagation diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt index 8ede39c70e..c716114d19 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/trace/TracingInterceptorTest.kt @@ -13,8 +13,6 @@ import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeReso import com.datadog.android.core.sampling.Sampler import com.datadog.android.internal.telemetry.TracingHeaderTypesSet import com.datadog.android.internal.utils.loggableStackTrace -import com.datadog.android.okhttp.TraceContext -import com.datadog.android.okhttp.TraceContextInjection import com.datadog.android.okhttp.internal.trace.toInternalTracingHeaderType import com.datadog.android.okhttp.internal.utils.forge.OkHttpConfigurator import com.datadog.android.okhttp.trace.TracingInterceptor.Companion.OKHTTP_INTERCEPTOR_HEADER_TYPES @@ -23,6 +21,8 @@ import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertTh import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration import com.datadog.android.okhttp.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.okhttp.utils.verifyLog +import com.datadog.android.trace.DeterministicTraceSampler +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.DatadogTracingConstants.PrioritySampling import com.datadog.android.trace.api.propagation.DatadogPropagation @@ -35,6 +35,7 @@ import com.datadog.android.trace.api.withMockPropagationHelper import com.datadog.android.trace.internal.DatadogPropagationHelper import com.datadog.android.trace.internal.DatadogTracingToolkit import com.datadog.android.trace.internal.fromHex +import com.datadog.android.trace.internal.net.TraceContext import com.datadog.tools.unit.annotations.TestConfigurationsProvider import com.datadog.tools.unit.extensions.TestConfigurationExtension import com.datadog.tools.unit.extensions.config.TestConfiguration diff --git a/reliability/single-fit/okhttp/src/test/kotlin/com/datadog/android/okhttp/HeadBasedSamplingTest.kt b/reliability/single-fit/okhttp/src/test/kotlin/com/datadog/android/okhttp/HeadBasedSamplingTest.kt index 44a0c894ef..1613a72d60 100644 --- a/reliability/single-fit/okhttp/src/test/kotlin/com/datadog/android/okhttp/HeadBasedSamplingTest.kt +++ b/reliability/single-fit/okhttp/src/test/kotlin/com/datadog/android/okhttp/HeadBasedSamplingTest.kt @@ -20,6 +20,7 @@ import com.datadog.android.trace.DatadogTracing import com.datadog.android.trace.GlobalDatadogTracer import com.datadog.android.trace.Trace import com.datadog.android.trace.TraceConfiguration +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.DatadogTracingConstants import com.datadog.android.trace.api.span.DatadogSpan diff --git a/reliability/single-fit/okhttp/src/test/kotlin/com/datadog/android/okhttp/RumContextPropagationTest.kt b/reliability/single-fit/okhttp/src/test/kotlin/com/datadog/android/okhttp/RumContextPropagationTest.kt index 1b76cb68c7..778b63b85d 100644 --- a/reliability/single-fit/okhttp/src/test/kotlin/com/datadog/android/okhttp/RumContextPropagationTest.kt +++ b/reliability/single-fit/okhttp/src/test/kotlin/com/datadog/android/okhttp/RumContextPropagationTest.kt @@ -23,6 +23,7 @@ import com.datadog.android.trace.DatadogTracing import com.datadog.android.trace.GlobalDatadogTracer import com.datadog.android.trace.Trace import com.datadog.android.trace.TraceConfiguration +import com.datadog.android.trace.TraceContextInjection import com.datadog.android.trace.TracingHeaderType import com.datadog.android.trace.api.TestIdGenerationStrategy import com.datadog.android.trace.api.replace diff --git a/sample/benchmark/transitiveDependencies b/sample/benchmark/transitiveDependencies index 4e5d3e3d52..6545a94913 100644 --- a/sample/benchmark/transitiveDependencies +++ b/sample/benchmark/transitiveDependencies @@ -34,8 +34,9 @@ androidx.compose.ui:ui-util-android:1.6.0 : 13 Kb androidx.constraintlayout:constraintlayout-solver:2.0.4 : 225 Kb androidx.constraintlayout:constraintlayout:2.0.4 : 375 Kb androidx.coordinatorlayout:coordinatorlayout:1.1.0 : 43 Kb -androidx.core:core-ktx:1.12.0 : 169 Kb -androidx.core:core:1.12.0 : 1291 Kb +androidx.core:core-ktx:1.17.0 : 168 Kb +androidx.core:core-viewtree:1.0.0 : 6 Kb +androidx.core:core:1.17.0 : 1376 Kb androidx.cursoradapter:cursoradapter:1.0.0 : 10 Kb androidx.customview:customview:1.1.0 : 32 Kb androidx.databinding:viewbinding:8.13.2 : 1860 b @@ -86,6 +87,7 @@ com.github.bumptech.glide:glide:4.11.0 : 614 Kb com.github.bumptech.glide:okhttp3-integration:4.11.0 : 8 Kb com.google.android.material:material:1.3.0 : 1535 Kb com.google.dagger:dagger:2.56.2 : 47 Kb +com.google.guava:listenablefuture:1.0 : 3 Kb com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2: 12 Kb com.hannesdorfmann:adapterdelegates4:4.3.2 : 12 Kb com.jakewharton.timber:timber:5.0.1 : 31 Kb @@ -116,9 +118,9 @@ org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.21 : 6 Kb org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 : 963 b org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 : 969 b org.jetbrains.kotlin:kotlin-stdlib:2.0.21 : 1706 Kb -org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 : 20 Kb -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3 : 1514 Kb -org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.3 : 953 b +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 : 19 Kb +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1 : 1510 Kb +org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.8.1 : 953 b org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.3 : 410 Kb org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3 : 258 Kb org.jetbrains:annotations:23.0.0 : 28 Kb diff --git a/sample/kotlin/src/main/kotlin/com/datadog/android/sample/cronet/CronetImageFragment.kt b/sample/kotlin/src/main/kotlin/com/datadog/android/sample/cronet/CronetImageFragment.kt index b234a68b2a..ed31bea80e 100644 --- a/sample/kotlin/src/main/kotlin/com/datadog/android/sample/cronet/CronetImageFragment.kt +++ b/sample/kotlin/src/main/kotlin/com/datadog/android/sample/cronet/CronetImageFragment.kt @@ -16,7 +16,9 @@ import android.widget.Toast import androidx.fragment.app.Fragment import com.datadog.android.cronet.DatadogCronetEngine import com.datadog.android.rum.ExperimentalRumApi +import com.datadog.android.rum.configuration.RumResourceInstrumentationConfiguration import com.datadog.android.sample.R +import com.datadog.android.trace.ApmNetworkInstrumentationConfiguration import org.chromium.net.CronetEngine import org.chromium.net.CronetException import org.chromium.net.UrlRequest @@ -41,6 +43,12 @@ internal class CronetImageFragment : Fragment() { "https://storage.googleapis.com/cronet/walnut.jpg" ) + private val tracedHosts = listOf( + "datadoghq.com", + "127.0.0.1", + "storage.googleapis.com" + ) + @OptIn(ExperimentalRumApi::class) override fun onCreateView( inflater: LayoutInflater, @@ -53,12 +61,14 @@ internal class CronetImageFragment : Fragment() { ).also { rootView -> imageView = rootView.findViewById(R.id.cronet_image_view) loadButton = rootView.findViewById(R.id.cronet_load_button) + loadButton.setOnClickListener { loadRandomImage() } cronetEngine = DatadogCronetEngine.Builder(requireContext()) .enableQuic(true) .enableHttp2(true) + .enableRumResourceInstrumentation() + .enableApmInstrumentation(ApmNetworkInstrumentationConfiguration(tracedHosts)) .build() - loadButton.setOnClickListener { loadRandomImage() } } private fun loadRandomImage() {