diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index c6b806b96a..f1547227c9 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -177,6 +177,7 @@ interface com.datadog.android.api.instrumentation.network.HttpResponseInfo val headers: Map> val contentType: String? val contentLength: Long? + val request: HttpRequestInfo? interface com.datadog.android.api.instrumentation.network.MutableHttpRequestInfo fun newBuilder(): HttpRequestInfoBuilder data class com.datadog.android.api.net.Request 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 b206425a2f..370496fc0d 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -499,6 +499,7 @@ public abstract interface class com/datadog/android/api/instrumentation/network/ public abstract fun getContentLength ()Ljava/lang/Long; public abstract fun getContentType ()Ljava/lang/String; public abstract fun getHeaders ()Ljava/util/Map; + public abstract fun getRequest ()Lcom/datadog/android/api/instrumentation/network/HttpRequestInfo; public abstract fun getStatusCode ()I public abstract fun getUrl ()Ljava/lang/String; } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/HttpResponseInfo.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/HttpResponseInfo.kt index 22d3ab71d6..fa3209d344 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/HttpResponseInfo.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/instrumentation/network/HttpResponseInfo.kt @@ -38,4 +38,9 @@ interface HttpResponseInfo { */ @get:WorkerThread val contentLength: Long? + + /** + * Represents the HTTP request associated with the response, or null when not available. + */ + val request: HttpRequestInfo? } diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetHttpResponseInfo.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetHttpResponseInfo.kt index 7cd4613514..52b308c22a 100644 --- a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetHttpResponseInfo.kt +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetHttpResponseInfo.kt @@ -10,7 +10,8 @@ import com.datadog.android.internal.network.HttpSpec import org.chromium.net.UrlResponseInfo internal data class CronetHttpResponseInfo( - private val response: UrlResponseInfo + private val response: UrlResponseInfo, + override val request: CronetHttpRequestInfo? ) : HttpResponseInfo { override val url: String get() = response.url @@ -35,7 +36,7 @@ internal data class CronetHttpResponseInfo( get() { /* * Content-Length is expected to be declared once or in case of multiple repetitions should have same value. - * Otherwise it's assumed that Content-Length is invalid + * Otherwise, it's assumed that Content-Length is invalid * according to https://www.rfc-editor.org/rfc/rfc9110.pdf, page 63: * * Likewise, a sender forward a message with a Content-Length header field value that does not match the ABNF diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetRequestCallback.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetRequestCallback.kt index be0e21d615..cd25c39ac5 100644 --- a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetRequestCallback.kt +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetRequestCallback.kt @@ -102,19 +102,25 @@ internal class CronetRequestCallback( ) = delegate.onReadCompleted(request, info, byteBuffer) private fun finishSuccessRequestTracing(response: UrlResponseInfo?) { - val responseInfo = response?.let { CronetHttpResponseInfo(it) } - apmTracingStateHolder.getAndSet(null)?.let { apmTracingState -> - if (responseInfo != null) { - apmNetworkInstrumentation?.onResponseSucceeded(apmTracingState, responseInfo) + if (response != null) { + val requestInfo = apmTracingState.createRequestInfo() as? CronetHttpRequestInfo + apmNetworkInstrumentation?.onResponseSucceeded( + apmTracingState, + CronetHttpResponseInfo(response, requestInfo) + ) } } ?: apmNetworkInstrumentation?.reportInstrumentationError { "Request tracing state not found for ${response?.url}. Instrumentation may be broken." } distributedTracingStateHolder.getAndSet(null)?.let { distributedTracingState -> - if (responseInfo != null) { - distributedTracingInstrumentation?.onResponseSucceeded(distributedTracingState, responseInfo) + if (response != null) { + val requestInfo = distributedTracingState.createRequestInfo() as? CronetHttpRequestInfo + distributedTracingInstrumentation?.onResponseSucceeded( + distributedTracingState, + CronetHttpResponseInfo(response, requestInfo) + ) } } } diff --git a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetRequestFinishedInfoListener.kt b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetRequestFinishedInfoListener.kt index 584091d7d8..fc78b41768 100644 --- a/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetRequestFinishedInfoListener.kt +++ b/integrations/dd-sdk-android-cronet/src/main/kotlin/com/datadog/android/cronet/internal/CronetRequestFinishedInfoListener.kt @@ -6,7 +6,6 @@ package com.datadog.android.cronet.internal import androidx.annotation.WorkerThread -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.RumNetworkInstrumentation @@ -25,7 +24,7 @@ internal class CronetRequestFinishedInfoListener( @WorkerThread override fun onRequestFinished(finishedInfo: RequestFinishedInfo) { - val requestInfo = finishedInfo.annotations?.filterIsInstance()?.firstOrNull() + val requestInfo = finishedInfo.annotations?.filterIsInstance()?.firstOrNull() val distributingTracingState = finishedInfo.annotations?.filterIsInstance()?.firstOrNull() if (requestInfo == null) { rumNetworkInstrumentation.reportInstrumentationError { @@ -53,7 +52,7 @@ internal class CronetRequestFinishedInfoListener( } RequestFinishedInfo.SUCCEEDED -> { - val responseInfo = finishedInfo.responseInfo?.let { CronetHttpResponseInfo(it) } + val responseInfo = finishedInfo.responseInfo?.let { CronetHttpResponseInfo(it, requestInfo) } if (responseInfo == null) { rumNetworkInstrumentation.stopResourceWithError( diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestFinishedInfoListenerTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestFinishedInfoListenerTest.kt index 0e39811591..a66f7d6cf2 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestFinishedInfoListenerTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetRequestFinishedInfoListenerTest.kt @@ -5,7 +5,6 @@ */ package com.datadog.android.cronet.internal -import com.datadog.android.api.instrumentation.network.HttpRequestInfo import com.datadog.android.cronet.internal.CronetRequestFinishedInfoListener.Companion.minus import com.datadog.android.rum.internal.domain.event.ResourceTiming import com.datadog.android.rum.internal.net.RumNetworkInstrumentation @@ -54,8 +53,8 @@ internal class CronetRequestFinishedInfoListenerTest { @Mock private lateinit var mockResponseInfo: UrlResponseInfo - @Forgery - private lateinit var fakeRequestInfo: HttpRequestInfo + @Mock + private lateinit var fakeRequestInfo: CronetHttpRequestInfo private lateinit var testedListener: CronetRequestFinishedInfoListener @@ -136,7 +135,7 @@ internal class CronetRequestFinishedInfoListenerTest { // Then verify(mockRumNetworkInstrumentation).stopResource( requestInfo = fakeRequestInfo, - responseInfo = CronetHttpResponseInfo(mockResponseInfo) + responseInfo = CronetHttpResponseInfo(mockResponseInfo, fakeRequestInfo) ) } diff --git a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetResponseInfoTest.kt b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetResponseInfoTest.kt index 74e98200da..d4ed5fb425 100644 --- a/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetResponseInfoTest.kt +++ b/integrations/dd-sdk-android-cronet/src/test/kotlin/com/datadog/android/cronet/internal/CronetResponseInfoTest.kt @@ -6,7 +6,6 @@ package com.datadog.android.cronet.internal -import com.datadog.android.api.InternalLogger import com.datadog.android.internal.network.HttpSpec import com.datadog.android.utils.forge.Configurator import com.datadog.tools.unit.forge.exhaustiveAttributes @@ -40,7 +39,7 @@ internal class CronetResponseInfoTest { lateinit var mockUrlResponseInfo: UrlResponseInfo @Mock - lateinit var mockInternalLogger: InternalLogger + lateinit var mockRequestInfo: CronetHttpRequestInfo lateinit var fakeHeaders: Map> @@ -55,7 +54,7 @@ internal class CronetResponseInfoTest { ) { // Given whenever(mockUrlResponseInfo.url).thenReturn(fakeUrl) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.url @@ -70,7 +69,7 @@ internal class CronetResponseInfoTest { ) { // Given whenever(mockUrlResponseInfo.httpStatusCode).thenReturn(fakeStatusCode) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.statusCode @@ -84,7 +83,7 @@ internal class CronetResponseInfoTest { // Given whenever(mockUrlResponseInfo.allHeaders).thenReturn(fakeHeaders) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.headers @@ -100,7 +99,7 @@ internal class CronetResponseInfoTest { // Given val fakeHeaders = fakeHeaders + mapOf(HttpSpec.Header.CONTENT_TYPE to listOf(fakeContentType)) whenever(mockUrlResponseInfo.allHeaders).thenReturn(fakeHeaders) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.contentType @@ -113,7 +112,7 @@ internal class CronetResponseInfoTest { fun `M return null W contentType property { no content type header }`() { // Given whenever(mockUrlResponseInfo.allHeaders).thenReturn(emptyMap()) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.contentType @@ -122,6 +121,30 @@ internal class CronetResponseInfoTest { assertThat(result).isNull() } + @Test + fun `M return request W request property`() { + // Given + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) + + // When + val result = responseInfo.request + + // Then + assertThat(result).isSameAs(mockRequestInfo) + } + + @Test + fun `M return null W request property { null request }`() { + // Given + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, null) + + // When + val result = responseInfo.request + + // Then + assertThat(result).isNull() + } + @Test fun `M return content length from header W computeContentLength() { header present }`( @LongForgery(min = 1) fakeContentLength: Long @@ -129,7 +152,7 @@ internal class CronetResponseInfoTest { // Given val fakeHeaders = fakeHeaders + mapOf(HttpSpec.Header.CONTENT_LENGTH to listOf(fakeContentLength.toString())) whenever(mockUrlResponseInfo.allHeaders).thenReturn(fakeHeaders) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.contentLength @@ -145,7 +168,7 @@ internal class CronetResponseInfoTest { // Given whenever(mockUrlResponseInfo.allHeaders).thenReturn(emptyMap()) whenever(mockUrlResponseInfo.receivedByteCount).thenReturn(fakeByteCount) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.contentLength @@ -159,7 +182,7 @@ internal class CronetResponseInfoTest { // Given whenever(mockUrlResponseInfo.allHeaders).thenReturn(emptyMap()) whenever(mockUrlResponseInfo.receivedByteCount).thenReturn(0L) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.contentLength @@ -173,7 +196,7 @@ internal class CronetResponseInfoTest { // Given whenever(mockUrlResponseInfo.allHeaders).thenReturn(emptyMap()) whenever(mockUrlResponseInfo.receivedByteCount).thenReturn(-1L) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.contentLength @@ -192,7 +215,7 @@ internal class CronetResponseInfoTest { ) whenever(mockUrlResponseInfo.allHeaders).thenReturn(fakeHeaders) whenever(mockUrlResponseInfo.receivedByteCount).thenReturn(100L) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.contentLength @@ -210,7 +233,7 @@ internal class CronetResponseInfoTest { val fakeHeaders = fakeHeaders + mapOf(HttpSpec.Header.CONTENT_LENGTH to listOf(fakeHeaderLength.toString())) whenever(mockUrlResponseInfo.allHeaders).thenReturn(fakeHeaders) whenever(mockUrlResponseInfo.receivedByteCount).thenReturn(fakeByteCount) - val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo) + val responseInfo = CronetHttpResponseInfo(mockUrlResponseInfo, mockRequestInfo) // When val result = responseInfo.contentLength diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfo.kt b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfo.kt index 1f57b78402..b6bbf7eb47 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfo.kt +++ b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfo.kt @@ -7,6 +7,7 @@ package com.datadog.android.okhttp.internal import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger +import com.datadog.android.api.instrumentation.network.HttpRequestInfo import com.datadog.android.api.instrumentation.network.HttpResponseInfo import okhttp3.Response import okhttp3.ResponseBody @@ -65,6 +66,8 @@ internal class OkHttpResponseInfo( null } + override val request: HttpRequestInfo get() = OkHttpRequestInfo(originalResponse.request) + internal companion object { // We need to limit this value as the body will be loaded in memory diff --git a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt index a8d62d2a1a..2c3b33b449 100644 --- a/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt +++ b/integrations/dd-sdk-android-okhttp/src/test/kotlin/com/datadog/android/okhttp/internal/OkHttpResponseInfoTest.kt @@ -90,6 +90,20 @@ internal class OkHttpResponseInfoTest { assertThat(result).isEqualTo(fakeHeaders) } + @Test + fun `M delegate W request property`() { + // Given + val mockRequest = mock() + val mockResponse = mock { on { request } doReturn mockRequest } + + // When + val result = OkHttpResponseInfo(mockResponse, mockInternalLogger).request + + // Then + assertThat(result).isInstanceOf(OkHttpRequestInfo::class.java) + assertThat(result.toOkHttpRequest()).isSameAs(mockRequest) + } + @Test fun `M delegate W contentType property { content type header present }`( @StringForgery fakeContentType: String,