From e13532145ef2b05909563329a8669ee6c1d039aa Mon Sep 17 00:00:00 2001 From: Jonathan Moskovich <48201295+jonathanmos@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:07:13 +0200 Subject: [PATCH 1/5] Revert "RUM-1002 Implement base64 for imageviews" This reverts commit 3100b414e02641ba8e53f0a84051d8cf5e3bafd4. --- .../sessionreplay/SessionReplayPrivacy.kt | 22 ++- .../recorder/mapper/EditTextViewMapper.kt | 86 +++++++++ ...mageViewMapper.kt => ImageButtonMapper.kt} | 8 +- .../sessionreplay/SessionReplayPrivacyTest.kt | 14 +- .../mapper/BaseEditTextViewMapperTest.kt | 167 ++++++++++++++++++ .../recorder/mapper/EditTextViewMapperTest.kt | 36 ++++ ...MapperTest.kt => ImageButtonMapperTest.kt} | 70 ++++---- 7 files changed, 358 insertions(+), 45 deletions(-) create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/EditTextViewMapper.kt rename features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/{ImageViewMapper.kt => ImageButtonMapper.kt} (95%) create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseEditTextViewMapperTest.kt create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/EditTextViewMapperTest.kt rename features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/{ImageViewMapperTest.kt => ImageButtonMapperTest.kt} (84%) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacy.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacy.kt index 97623aa27b..5887aa4a96 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacy.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacy.kt @@ -11,6 +11,8 @@ import android.view.View import android.widget.Button import android.widget.CheckBox import android.widget.CheckedTextView +import android.widget.EditText +import android.widget.ImageButton import android.widget.ImageView import android.widget.NumberPicker import android.widget.RadioButton @@ -26,7 +28,8 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.BasePickerMapp import com.datadog.android.sessionreplay.internal.recorder.mapper.ButtonMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckBoxMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckedTextViewMapper -import com.datadog.android.sessionreplay.internal.recorder.mapper.ImageViewMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.EditTextViewMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.ImageButtonMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskCheckBoxMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskCheckedTextViewMapper @@ -42,6 +45,8 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.SeekBarWirefra import com.datadog.android.sessionreplay.internal.recorder.mapper.SwitchCompatMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.UnsupportedViewMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewScreenshotWireframeMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator import androidx.appcompat.widget.Toolbar as AppCompatToolbar @@ -81,14 +86,17 @@ enum class SessionReplayPrivacy { val imageWireframeHelper = ImageWireframeHelper(base64Serializer = base64Serializer) val uniqueIdentifierGenerator = UniqueIdentifierGenerator + val viewWireframeMapper = ViewWireframeMapper() val unsupportedViewMapper = UnsupportedViewMapper() - val imageViewMapper = ImageViewMapper( + val imageButtonMapper = ImageButtonMapper( base64Serializer = base64Serializer, imageWireframeHelper = imageWireframeHelper, uniqueIdentifierGenerator = uniqueIdentifierGenerator ) + val imageMapper: ViewScreenshotWireframeMapper val textMapper: TextViewMapper val buttonMapper: ButtonMapper + val editTextViewMapper: EditTextViewMapper val checkedTextViewMapper: CheckedTextViewMapper val checkBoxMapper: CheckBoxMapper val radioButtonMapper: RadioButtonMapper @@ -97,11 +105,13 @@ enum class SessionReplayPrivacy { val numberPickerMapper: BasePickerMapper? when (this) { ALLOW -> { + imageMapper = ViewScreenshotWireframeMapper(viewWireframeMapper) textMapper = TextViewMapper( imageWireframeHelper = imageWireframeHelper, uniqueIdentifierGenerator = uniqueIdentifierGenerator ) buttonMapper = ButtonMapper(textMapper) + editTextViewMapper = EditTextViewMapper(textMapper) checkedTextViewMapper = CheckedTextViewMapper(textMapper) checkBoxMapper = CheckBoxMapper(textMapper) radioButtonMapper = RadioButtonMapper(textMapper) @@ -110,11 +120,13 @@ enum class SessionReplayPrivacy { numberPickerMapper = getNumberPickerMapper() } MASK -> { + imageMapper = ViewScreenshotWireframeMapper(viewWireframeMapper) textMapper = MaskTextViewMapper( imageWireframeHelper = imageWireframeHelper, uniqueIdentifierGenerator = uniqueIdentifierGenerator ) buttonMapper = ButtonMapper(textMapper) + editTextViewMapper = EditTextViewMapper(textMapper) checkedTextViewMapper = MaskCheckedTextViewMapper(textMapper) checkBoxMapper = MaskCheckBoxMapper(textMapper) radioButtonMapper = MaskRadioButtonMapper(textMapper) @@ -123,11 +135,13 @@ enum class SessionReplayPrivacy { numberPickerMapper = getMaskNumberPickerMapper() } MASK_USER_INPUT -> { + imageMapper = ViewScreenshotWireframeMapper(viewWireframeMapper) textMapper = MaskInputTextViewMapper( imageWireframeHelper = imageWireframeHelper, uniqueIdentifierGenerator = uniqueIdentifierGenerator ) buttonMapper = ButtonMapper(textMapper) + editTextViewMapper = EditTextViewMapper(textMapper) checkedTextViewMapper = MaskCheckedTextViewMapper(textMapper) checkBoxMapper = MaskCheckBoxMapper(textMapper) radioButtonMapper = MaskRadioButtonMapper(textMapper) @@ -142,8 +156,10 @@ enum class SessionReplayPrivacy { MapperTypeWrapper(CheckBox::class.java, checkBoxMapper.toGenericMapper()), MapperTypeWrapper(CheckedTextView::class.java, checkedTextViewMapper.toGenericMapper()), MapperTypeWrapper(Button::class.java, buttonMapper.toGenericMapper()), + MapperTypeWrapper(ImageButton::class.java, imageButtonMapper.toGenericMapper()), + MapperTypeWrapper(EditText::class.java, editTextViewMapper.toGenericMapper()), MapperTypeWrapper(TextView::class.java, textMapper.toGenericMapper()), - MapperTypeWrapper(ImageView::class.java, imageViewMapper.toGenericMapper()), + MapperTypeWrapper(ImageView::class.java, imageMapper.toGenericMapper()), MapperTypeWrapper(AppCompatToolbar::class.java, unsupportedViewMapper.toGenericMapper()) ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/EditTextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/EditTextViewMapper.kt new file mode 100644 index 0000000000..1387402499 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/EditTextViewMapper.kt @@ -0,0 +1,86 @@ +/* + * 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.sessionreplay.internal.recorder.mapper + +import android.widget.EditText +import com.datadog.android.sessionreplay.SessionReplayPrivacy +import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback +import com.datadog.android.sessionreplay.internal.recorder.MappingContext +import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.utils.StringUtils +import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.ViewUtils + +/** + * A [WireframeMapper] implementation to map a [EditText] component in case the + * [SessionReplayPrivacy.ALLOW] rule was used in the configuration. + * In this case the mapper will use the provided [textViewMapper] used for the current privacy + * level and will only mask the [EditText] for which the input type is considered sensible + * (password, email, address, postal address, numeric password) with the static mask: [***]. + */ +internal open class EditTextViewMapper( + internal val textViewMapper: TextViewMapper, + private val uniqueIdentifierGenerator: UniqueIdentifierGenerator = UniqueIdentifierGenerator, + viewUtils: ViewUtils = ViewUtils, + stringUtils: StringUtils = StringUtils +) : BaseWireframeMapper( + viewUtils = viewUtils, + stringUtils = stringUtils +) { + + override fun map( + view: EditText, + mappingContext: MappingContext, + asyncJobStatusCallback: AsyncJobStatusCallback + ): + List { + val mainWireframeList = textViewMapper.map(view, mappingContext, asyncJobStatusCallback) + resolveUnderlineWireframe(view, mappingContext.systemInformation.screenDensity) + ?.let { wireframe -> + return mainWireframeList + wireframe + } + return mainWireframeList + } + + private fun resolveUnderlineWireframe( + parent: EditText, + pixelsDensity: Float + ): MobileSegment.Wireframe? { + val identifier = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( + parent, + UNDERLINE_KEY_NAME + ) ?: return null + val viewGlobalBounds = resolveViewGlobalBounds(parent, pixelsDensity) + val fieldUnderlineColor = resolveUnderlineColor(parent) + return MobileSegment.Wireframe.ShapeWireframe( + identifier, + viewGlobalBounds.x, + viewGlobalBounds.y + viewGlobalBounds.height - UNDERLINE_HEIGHT_IN_PIXELS, + viewGlobalBounds.width, + UNDERLINE_HEIGHT_IN_PIXELS, + shapeStyle = MobileSegment.ShapeStyle( + backgroundColor = fieldUnderlineColor, + opacity = parent.alpha + ) + ) + } + + private fun resolveUnderlineColor(view: EditText): String { + view.backgroundTintList?.let { + return colorAndAlphaAsStringHexa( + it.defaultColor, + OPAQUE_ALPHA_VALUE + ) + } + return colorAndAlphaAsStringHexa(view.currentTextColor, OPAQUE_ALPHA_VALUE) + } + + companion object { + internal const val UNDERLINE_HEIGHT_IN_PIXELS = 1L + internal const val UNDERLINE_KEY_NAME = "underline" + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageButtonMapper.kt similarity index 95% rename from features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapper.kt rename to features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageButtonMapper.kt index 6105423e67..1ca0dec8a9 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageButtonMapper.kt @@ -6,7 +6,7 @@ package com.datadog.android.sessionreplay.internal.recorder.mapper -import android.widget.ImageView +import android.widget.ImageButton import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.MappingContext import com.datadog.android.sessionreplay.internal.recorder.base64.Base64Serializer @@ -16,16 +16,16 @@ import com.datadog.android.sessionreplay.internal.recorder.densityNormalized import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator -internal class ImageViewMapper( +internal class ImageButtonMapper( private val base64Serializer: Base64Serializer, private val imageWireframeHelper: ImageWireframeHelper, uniqueIdentifierGenerator: UniqueIdentifierGenerator -) : BaseAsyncBackgroundWireframeMapper( +) : BaseAsyncBackgroundWireframeMapper( imageWireframeHelper = imageWireframeHelper, uniqueIdentifierGenerator = uniqueIdentifierGenerator ) { override fun map( - view: ImageView, + view: ImageButton, mappingContext: MappingContext, asyncJobStatusCallback: AsyncJobStatusCallback ): List { diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacyTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacyTest.kt index c8803ce2d4..6571daae18 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacyTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayPrivacyTest.kt @@ -11,6 +11,8 @@ import android.view.View import android.widget.Button import android.widget.CheckBox import android.widget.CheckedTextView +import android.widget.EditText +import android.widget.ImageButton import android.widget.ImageView import android.widget.NumberPicker import android.widget.RadioButton @@ -21,7 +23,8 @@ import androidx.appcompat.widget.SwitchCompat import com.datadog.android.sessionreplay.internal.recorder.mapper.ButtonMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckBoxMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckedTextViewMapper -import com.datadog.android.sessionreplay.internal.recorder.mapper.ImageViewMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.EditTextViewMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.ImageButtonMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskCheckBoxMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskCheckedTextViewMapper @@ -37,6 +40,7 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.SeekBarWirefra import com.datadog.android.sessionreplay.internal.recorder.mapper.SwitchCompatMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.UnsupportedViewMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewScreenshotWireframeMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.tools.unit.setStaticValue import org.assertj.core.api.Assertions.assertThat @@ -106,12 +110,16 @@ internal class SessionReplayPrivacyTest { // BASE private val mockButtonMapper: ButtonMapper = mock() + private val mockEditTextViewMapper: EditTextViewMapper = mock() + private val mockImageMapper: ViewScreenshotWireframeMapper = mock() private val mockUnsupportedViewMapper: UnsupportedViewMapper = mock() - private val mockImageViewMapper: ImageViewMapper = mock() + private val mockImageButtonViewMapper: ImageButtonMapper = mock() private val baseMappers = listOf( MapperTypeWrapper(Button::class.java, mockButtonMapper.toGenericMapper()), - MapperTypeWrapper(ImageView::class.java, mockImageViewMapper.toGenericMapper()), + MapperTypeWrapper(EditText::class.java, mockEditTextViewMapper.toGenericMapper()), + MapperTypeWrapper(ImageView::class.java, mockImageMapper.toGenericMapper()), + MapperTypeWrapper(ImageButton::class.java, mockImageButtonViewMapper.toGenericMapper()), MapperTypeWrapper(AppCompatToolbar::class.java, mockUnsupportedViewMapper.toGenericMapper()) ) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseEditTextViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseEditTextViewMapperTest.kt new file mode 100644 index 0000000000..bfed1faafc --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/BaseEditTextViewMapperTest.kt @@ -0,0 +1,167 @@ +/* + * 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.sessionreplay.internal.recorder.mapper + +import android.content.res.ColorStateList +import android.widget.EditText +import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds +import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.utils.StringUtils +import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator +import com.datadog.android.sessionreplay.utils.ViewUtils +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever + +internal abstract class BaseEditTextViewMapperTest : BaseWireframeMapperTest() { + + private lateinit var testedEditTextViewMapper: EditTextViewMapper + + @Mock + lateinit var mockuniqueIdentifierGenerator: UniqueIdentifierGenerator + + @Mock + lateinit var mockTextWireframeMapper: TextViewMapper + + @Forgery + lateinit var fakeTextWireframes: List + + @Mock + lateinit var mockEditText: EditText + + @Mock + lateinit var mockBackgroundTintList: ColorStateList + + @LongForgery + var fakeGeneratedIdentifier: Long = 0L + + @IntForgery + var fakeBackgroundTintColor: Int = 0 + + @IntForgery + var fakeTextColor: Int = 0 + + @Mock + lateinit var mockViewUtils: ViewUtils + + @Forgery + lateinit var fakeViewGlobalBounds: GlobalBounds + + @Mock + lateinit var mockStringUtils: StringUtils + + @BeforeEach + fun `set up`() { + whenever(mockEditText.currentTextColor).thenReturn(fakeTextColor) + whenever(mockBackgroundTintList.defaultColor).thenReturn(fakeBackgroundTintColor) + whenever( + mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockEditText, + EditTextViewMapper.UNDERLINE_KEY_NAME + ) + ).thenReturn(fakeGeneratedIdentifier) + whenever(mockEditText.backgroundTintList).thenReturn(mockBackgroundTintList) + whenever(mockTextWireframeMapper.map(eq(mockEditText), eq(fakeMappingContext), any())) + .thenReturn(fakeTextWireframes) + whenever( + mockViewUtils.resolveViewGlobalBounds( + mockEditText, + fakeMappingContext.systemInformation.screenDensity + ) + ) + .thenReturn(fakeViewGlobalBounds) + testedEditTextViewMapper = initTestInstance() + } + + abstract fun initTestInstance(): EditTextViewMapper + + @Test + fun `M resolve the underline as ShapeWireframe W map()`(forge: Forge) { + // Given + val fakeExpectedUnderlineColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") + whenever( + mockStringUtils.formatColorAndAlphaAsHexa( + fakeBackgroundTintColor, + OPAQUE_ALPHA_VALUE + ) + ) + .thenReturn(fakeExpectedUnderlineColor) + val expectedUnderlineShapeWireframe = MobileSegment.Wireframe.ShapeWireframe( + id = fakeGeneratedIdentifier, + x = fakeViewGlobalBounds.x, + y = fakeViewGlobalBounds.y + + fakeViewGlobalBounds.height - + EditTextViewMapper.UNDERLINE_HEIGHT_IN_PIXELS, + width = fakeViewGlobalBounds.width, + height = EditTextViewMapper.UNDERLINE_HEIGHT_IN_PIXELS, + shapeStyle = MobileSegment.ShapeStyle( + backgroundColor = fakeExpectedUnderlineColor, + opacity = mockEditText.alpha + ) + ) + + // When + assertThat(testedEditTextViewMapper.map(mockEditText, fakeMappingContext)) + .isEqualTo(fakeTextWireframes + expectedUnderlineShapeWireframe) + } + + @Test + fun `M resolve the underline color from textColor W map{backgroundTint is null}`( + forge: Forge + ) { + // Given + whenever(mockEditText.backgroundTintList).thenReturn(null) + val fakeExpectedUnderlineColor = forge.aStringMatching("#[0-9A-Fa-f]{8}") + whenever( + mockStringUtils.formatColorAndAlphaAsHexa( + fakeTextColor, + OPAQUE_ALPHA_VALUE + ) + ) + .thenReturn(fakeExpectedUnderlineColor) + val expectedUnderlineShapeWireframe = MobileSegment.Wireframe.ShapeWireframe( + id = fakeGeneratedIdentifier, + x = fakeViewGlobalBounds.x, + y = fakeViewGlobalBounds.y + + fakeViewGlobalBounds.height - + EditTextViewMapper.UNDERLINE_HEIGHT_IN_PIXELS, + width = fakeViewGlobalBounds.width, + height = EditTextViewMapper.UNDERLINE_HEIGHT_IN_PIXELS, + shapeStyle = MobileSegment.ShapeStyle( + backgroundColor = fakeExpectedUnderlineColor, + opacity = mockEditText.alpha + ) + ) + + // When + assertThat(testedEditTextViewMapper.map(mockEditText, fakeMappingContext)) + .isEqualTo(fakeTextWireframes + expectedUnderlineShapeWireframe) + } + + @Test + fun `M ignore the underline W map() { unique id could not be generated }`() { + // Given + whenever( + mockuniqueIdentifierGenerator.resolveChildUniqueIdentifier( + mockEditText, + EditTextViewMapper.UNDERLINE_KEY_NAME + ) + ).thenReturn(null) + + // Then + assertThat(testedEditTextViewMapper.map(mockEditText, fakeMappingContext)) + .isEqualTo(fakeTextWireframes) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/EditTextViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/EditTextViewMapperTest.kt new file mode 100644 index 0000000000..923a84053d --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/EditTextViewMapperTest.kt @@ -0,0 +1,36 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.sessionreplay.internal.recorder.mapper + +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import com.datadog.tools.unit.extensions.ApiLevelExtension +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +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), + ExtendWith(ApiLevelExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(ForgeConfigurator::class) +internal class EditTextViewMapperTest : BaseEditTextViewMapperTest() { + + override fun initTestInstance(): EditTextViewMapper { + return EditTextViewMapper( + mockTextWireframeMapper, + mockuniqueIdentifierGenerator, + mockViewUtils, + mockStringUtils + ) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageButtonMapperTest.kt similarity index 84% rename from features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapperTest.kt rename to features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageButtonMapperTest.kt index da0d1a56a3..1b0df9ef4b 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ImageButtonMapperTest.kt @@ -12,7 +12,7 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable.ConstantState import android.util.DisplayMetrics -import android.widget.ImageView +import android.widget.ImageButton import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback import com.datadog.android.sessionreplay.internal.recorder.GlobalBounds @@ -56,12 +56,12 @@ import org.mockito.quality.Strictness ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(ForgeConfigurator::class) -internal class ImageViewMapperTest { +internal class ImageButtonMapperTest { - private lateinit var testedMapper: ImageViewMapper + private lateinit var testedMapper: ImageButtonMapper @Mock - lateinit var mockImageView: ImageView + lateinit var mockImageButton: ImageButton @Mock lateinit var mockImageWireframeHelper: ImageWireframeHelper @@ -116,15 +116,15 @@ internal class ImageViewMapperTest { @BeforeEach fun setup(forge: Forge) { - whenever(mockImageView.background).thenReturn(null) + whenever(mockImageButton.background).thenReturn(null) whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(any(), any())) .thenReturn(fakeId) whenever(mockConstantState.newDrawable(any())).thenReturn(mockDrawable) whenever(mockDrawable.constantState).thenReturn(mockConstantState) - whenever(mockImageView.drawable).thenReturn(mockDrawable) - whenever(mockImageView.drawable.current).thenReturn(mockDrawable) + whenever(mockImageButton.drawable).thenReturn(mockDrawable) + whenever(mockImageButton.drawable.current).thenReturn(mockDrawable) whenever(mockDrawable.intrinsicWidth).thenReturn(forge.aPositiveInt()) whenever(mockDrawable.intrinsicHeight).thenReturn(forge.aPositiveInt()) @@ -135,10 +135,10 @@ internal class ImageViewMapperTest { whenever(mockMappingContext.systemInformation).thenReturn(mockSystemInformation) whenever(mockResources.displayMetrics).thenReturn(mockDisplayMetrics) - whenever(mockImageView.resources).thenReturn(mockResources) + whenever(mockImageButton.resources).thenReturn(mockResources) whenever(mockContext.applicationContext).thenReturn(mockContext) - whenever(mockImageView.context).thenReturn(mockContext) + whenever(mockImageButton.context).thenReturn(mockContext) whenever(mockBackground.current).thenReturn(mockBackground) whenever(mockViewUtils.resolveViewGlobalBounds(any(), any())).thenReturn(mockGlobalBounds) @@ -147,8 +147,8 @@ internal class ImageViewMapperTest { id = fakeId, x = mockGlobalBounds.x, y = mockGlobalBounds.y, - width = mockImageView.width.toLong(), - height = mockImageView.height.toLong(), + width = mockImageButton.width.toLong(), + height = mockImageButton.height.toLong(), shapeStyle = null, border = null, base64 = "", @@ -159,7 +159,7 @@ internal class ImageViewMapperTest { whenever(mockBase64Serializer.getDrawableScaledDimensions(any(), any(), any())) .thenReturn(DrawableDimensions(0, 0)) - testedMapper = ImageViewMapper( + testedMapper = ImageButtonMapper( base64Serializer = mockBase64Serializer, imageWireframeHelper = mockImageWireframeHelper, uniqueIdentifierGenerator = mockUniqueIdentifierGenerator @@ -169,11 +169,11 @@ internal class ImageViewMapperTest { @Test fun `M return foreground wireframe W map() { no background }`() { // Given - whenever(mockImageView.background).thenReturn(null) + whenever(mockImageButton.background).thenReturn(null) val fakeViewDrawable = mockDrawable.constantState?.newDrawable(mockResources) whenever( mockImageWireframeHelper.createImageWireframe( - eq(mockImageView), + eq(mockImageButton), any(), any(), any(), @@ -188,7 +188,7 @@ internal class ImageViewMapperTest { ).thenReturn(expectedWireframe) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageButton, mockMappingContext) // Then assertThat(wireframes.size).isEqualTo(1) @@ -204,8 +204,8 @@ internal class ImageViewMapperTest { id = id, x = mockGlobalBounds.x, y = mockGlobalBounds.y, - width = mockImageView.width.toLong(), - height = mockImageView.height.toLong(), + width = mockImageButton.width.toLong(), + height = mockImageButton.height.toLong(), shapeStyle = null, border = null, base64 = "", @@ -213,14 +213,14 @@ internal class ImageViewMapperTest { isEmpty = true ) - whenever(mockImageView.background).thenReturn(mockBackground) + whenever(mockImageButton.background).thenReturn(mockBackground) mockCreateImageWireframe( expectedBackgroundWireframe, expectedWireframe ) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageButton, mockMappingContext) // Then assertThat(wireframes.size).isEqualTo(2) @@ -231,7 +231,7 @@ internal class ImageViewMapperTest { @Test fun `M call async callback W map() { }`() { // Given - whenever(mockImageView.background).thenReturn(mockBackground) + whenever(mockImageButton.background).thenReturn(mockBackground) val argumentCaptor = argumentCaptor() whenever( @@ -251,7 +251,7 @@ internal class ImageViewMapperTest { ).thenReturn(expectedWireframe) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext, mockCallback) + val wireframes = testedMapper.map(mockImageButton, mockMappingContext, mockCallback) // Then assertThat(wireframes.size).isEqualTo(2) @@ -287,15 +287,15 @@ internal class ImageViewMapperTest { id = id, x = mockGlobalBounds.x, y = mockGlobalBounds.y, - width = mockImageView.width.toLong(), - height = mockImageView.height.toLong(), + width = mockImageButton.width.toLong(), + height = mockImageButton.height.toLong(), shapeStyle = null, border = null, base64 = "", mimeType = fakeMimeType, isEmpty = true ) - whenever(mockImageView.background).thenReturn(mockBackground) + whenever(mockImageButton.background).thenReturn(mockBackground) mockCreateImageWireframe( expectedBackgroundWireframe, @@ -303,7 +303,7 @@ internal class ImageViewMapperTest { ) // When - testedMapper.map(mockImageView, mockMappingContext) + testedMapper.map(mockImageButton, mockMappingContext) // Then val captor = argumentCaptor() @@ -328,7 +328,7 @@ internal class ImageViewMapperTest { @Test fun `M set index to 0 W map() { no background wireframe }`() { // Given - whenever(mockImageView.background).thenReturn(mockBackground) + whenever(mockImageButton.background).thenReturn(mockBackground) mockCreateImageWireframe( null, @@ -336,7 +336,7 @@ internal class ImageViewMapperTest { ) // When - testedMapper.map(mockImageView, mockMappingContext) + testedMapper.map(mockImageButton, mockMappingContext) // Then val captor = argumentCaptor() @@ -363,14 +363,14 @@ internal class ImageViewMapperTest { @LongForgery id: Long ) { // Given - whenever(mockImageView.background).thenReturn(mockBackground) + whenever(mockImageButton.background).thenReturn(mockBackground) val expectedBackgroundWireframe = MobileSegment.Wireframe.ImageWireframe( id = id, x = mockGlobalBounds.x, y = mockGlobalBounds.y, - width = mockImageView.width.toLong(), - height = mockImageView.height.toLong(), + width = mockImageButton.width.toLong(), + height = mockImageButton.height.toLong(), shapeStyle = null, border = null, base64 = "", @@ -384,7 +384,7 @@ internal class ImageViewMapperTest { ) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageButton, mockMappingContext) // Then assertThat(wireframes[0]::class.java).isEqualTo(MobileSegment.Wireframe.ImageWireframe::class.java) @@ -395,10 +395,10 @@ internal class ImageViewMapperTest { @Mock mockColorDrawable: ColorDrawable ) { // Given - whenever(mockImageView.background).thenReturn(mockColorDrawable) + whenever(mockImageButton.background).thenReturn(mockColorDrawable) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageButton, mockMappingContext) // Then assertThat(wireframes[0]::class.java).isEqualTo(MobileSegment.Wireframe.ShapeWireframe::class.java) @@ -409,7 +409,7 @@ internal class ImageViewMapperTest { @Mock mockColorDrawable: ColorDrawable ) { // Given - whenever(mockImageView.background).thenReturn(mockColorDrawable) + whenever(mockImageButton.background).thenReturn(mockColorDrawable) whenever(mockUniqueIdentifierGenerator.resolveChildUniqueIdentifier(any(), any())) .thenReturn(null) @@ -420,7 +420,7 @@ internal class ImageViewMapperTest { ) // When - val wireframes = testedMapper.map(mockImageView, mockMappingContext) + val wireframes = testedMapper.map(mockImageButton, mockMappingContext) // Then assertThat(wireframes.size).isEqualTo(1) From c1827491ed68bd5db40e057a5c0a972183d0abc0 Mon Sep 17 00:00:00 2001 From: Nikita Ogorodnikov Date: Tue, 3 Oct 2023 13:26:59 +0200 Subject: [PATCH 2/5] RUM-1236: Support overloaded methods with same arguments for no-op implementation generation --- .../noopfactory/NoOpFactorySymbolProcessor.kt | 44 +++++++++++++++---- .../noopfactory/NoOpFactoryProviderTest.kt | 3 +- .../resources/gen/NoOpOverloadedInterface.kt | 19 ++++++++ .../test/resources/src/OverloadedInterface.kt | 12 +++++ 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 tools/noopfactory/src/test/resources/gen/NoOpOverloadedInterface.kt create mode 100644 tools/noopfactory/src/test/resources/src/OverloadedInterface.kt diff --git a/tools/noopfactory/src/main/kotlin/com/datadog/tools/noopfactory/NoOpFactorySymbolProcessor.kt b/tools/noopfactory/src/main/kotlin/com/datadog/tools/noopfactory/NoOpFactorySymbolProcessor.kt index 81548a0893..909e6d6030 100644 --- a/tools/noopfactory/src/main/kotlin/com/datadog/tools/noopfactory/NoOpFactorySymbolProcessor.kt +++ b/tools/noopfactory/src/main/kotlin/com/datadog/tools/noopfactory/NoOpFactorySymbolProcessor.kt @@ -7,6 +7,8 @@ package com.datadog.tools.noopfactory import com.datadog.tools.annotation.NoOpImplementation +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getAnnotationsByType import com.google.devtools.ksp.processing.CodeGenerator import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.KSPLogger @@ -225,11 +227,11 @@ class NoOpFactorySymbolProcessor( val interfaces = mutableListOf(declaration) val functions: MutableMap = mutableMapOf() val properties = mutableMapOf() + val typeParamResolver = declaration.typeParameters.toTypeParameterResolver() while (interfaces.isNotEmpty()) { - generateFirstInterfaceImplementation(interfaces, functions, properties) + generateFirstInterfaceImplementation(interfaces, functions, properties, typeParamResolver) } - val typeParamResolver = declaration.typeParameters.toTypeParameterResolver() functions.values.forEach { typeSpecBuilder.addFunction(generateFunctionImplementation(it, typeParamResolver)) } @@ -242,11 +244,12 @@ class NoOpFactorySymbolProcessor( private fun generateFirstInterfaceImplementation( interfaces: MutableList, functions: MutableMap, - properties: MutableMap + properties: MutableMap, + typeParamResolver: TypeParameterResolver ) { val interfaceType = interfaces.removeAt(0) if (interfaceType.classKind == ClassKind.INTERFACE) { - fetchInterfaceFunctions(interfaceType, functions) + fetchInterfaceFunctions(interfaceType, functions, typeParamResolver) fetchInterfaceProperties(interfaceType, properties) interfaceType.superTypes.forEach { @@ -266,10 +269,18 @@ class NoOpFactorySymbolProcessor( */ private fun fetchInterfaceFunctions( declaration: KSClassDeclaration, - executableElements: MutableMap + executableElements: MutableMap, + typeParamResolver: TypeParameterResolver ) { declaration.getAllFunctions().forEach { - val id = it.identifier() + // hack for the case when we process a function from super definition, which was already + // seen and which has a generic parameters - it will need a typeResolver from super + // and will have a different ID, but essentially we already saw everything + // needed to implement this function + if (executableElements.values.any { seen -> seen.findOverridee() == it }) { + return@forEach + } + val id = it.identifier(typeParamResolver) if ((id !in ignoredFunctions) && !executableElements.containsKey(id)) { executableElements[id] = it } @@ -295,6 +306,7 @@ class NoOpFactorySymbolProcessor( /** * Generates the implementation for a given method. */ + @OptIn(KspExperimental::class) private fun generateFunctionImplementation( functionDeclaration: KSFunctionDeclaration, typeParamResolver: TypeParameterResolver = TypeParameterResolver.EMPTY @@ -312,6 +324,18 @@ class NoOpFactorySymbolProcessor( ) } + // add Deprecated annotation, which has a special handling during compile-time + val deprecatedAnnotation = functionDeclaration + .getAnnotationsByType(Deprecated::class) + .firstOrNull() + if (deprecatedAnnotation != null) { + funSpecBuilder.addAnnotation( + AnnotationSpec.builder(Deprecated::class) + .addMember("%S", deprecatedAnnotation.message) + .build() + ) + } + val returnType = functionDeclaration.returnType?.resolve() if (returnType != null) { generateFunctionReturnStatement(funSpecBuilder, returnType, params, typeParamResolver) @@ -536,9 +560,11 @@ class NoOpFactorySymbolProcessor( /** * @return the identifier name of the [KSFunctionDeclaration] */ - private fun KSFunctionDeclaration.identifier(): String { + private fun KSFunctionDeclaration.identifier(typeParamResolver: TypeParameterResolver): String { return simpleName.asString() + parameters.joinToString(",", "(", ")") { - it.name?.asString() ?: "?" + val name = it.name?.asString() ?: "?" + val type = it.type.resolve().toTypeName(typeParamResolver) + "$name:$type" } } @@ -550,7 +576,7 @@ class NoOpFactorySymbolProcessor( // endregion companion object { - private val ignoredFunctions = arrayOf("equals(other)", "hashCode()", "toString()") + private val ignoredFunctions = arrayOf("equals(other:kotlin.Any?)", "hashCode()", "toString()") private const val KOTLIN_COLLECTIONS_PACKAGE = "kotlin.collections" } } diff --git a/tools/noopfactory/src/test/kotlin/com/datadog/tools/noopfactory/NoOpFactoryProviderTest.kt b/tools/noopfactory/src/test/kotlin/com/datadog/tools/noopfactory/NoOpFactoryProviderTest.kt index 2caf77edc7..27150bd2ce 100644 --- a/tools/noopfactory/src/test/kotlin/com/datadog/tools/noopfactory/NoOpFactoryProviderTest.kt +++ b/tools/noopfactory/src/test/kotlin/com/datadog/tools/noopfactory/NoOpFactoryProviderTest.kt @@ -24,7 +24,8 @@ internal class NoOpFactoryProviderTest { "GenericInterface.kt:NoOpGenericInterface.kt", "InheritedInterface.kt:NoOpInheritedInterface.kt", "AnyGenericInterface.kt:NoOpAnyGenericInterface.kt", - "EnumInterface.kt:NoOpEnumInterface.kt" + "EnumInterface.kt:NoOpEnumInterface.kt", + "OverloadedInterface.kt:NoOpOverloadedInterface.kt" ] ) fun `implement a NoOp class from interface`(srcFileName: String, genFileName: String) { diff --git a/tools/noopfactory/src/test/resources/gen/NoOpOverloadedInterface.kt b/tools/noopfactory/src/test/resources/gen/NoOpOverloadedInterface.kt new file mode 100644 index 0000000000..5d1bc94f9a --- /dev/null +++ b/tools/noopfactory/src/test/resources/gen/NoOpOverloadedInterface.kt @@ -0,0 +1,19 @@ +@file:Suppress("ktlint") + +package com.example + +import kotlin.Deprecated +import kotlin.Int +import kotlin.String +import kotlin.Suppress +import kotlin.Unit + +@Suppress("RedundantUnitReturnType") +internal class NoOpOverloadedInterface : OverloadedInterface { + @Deprecated("foobar") + public override fun doSomething(i: Int): Unit { + } + + public override fun doSomething(i: String): Unit { + } +} diff --git a/tools/noopfactory/src/test/resources/src/OverloadedInterface.kt b/tools/noopfactory/src/test/resources/src/OverloadedInterface.kt new file mode 100644 index 0000000000..9b602274b0 --- /dev/null +++ b/tools/noopfactory/src/test/resources/src/OverloadedInterface.kt @@ -0,0 +1,12 @@ +package com.example + +import com.datadog.tools.annotation.NoOpImplementation + +@NoOpImplementation +interface OverloadedInterface { + + @Deprecated("foobar") + fun doSomething(i: Int) + + fun doSomething(i: String) +} From 5bcb22399cb34e7102eeb1fd6909f896f23c9440 Mon Sep 17 00:00:00 2001 From: Nikita Ogorodnikov Date: Tue, 3 Oct 2023 11:11:47 +0200 Subject: [PATCH 3/5] RUM-1236: Use enum for HTTP method parameter of RumMonitor#startResource API --- features/dd-sdk-android-rum/api/apiSurface | 10 ++- .../api/dd-sdk-android-rum.api | 13 ++++ .../com/datadog/android/rum/RumMonitor.kt | 22 ++++++ .../datadog/android/rum/RumResourceMethod.kt | 43 ++++++++++++ .../rum/internal/domain/scope/RumEventExt.kt | 39 +++++------ .../rum/internal/domain/scope/RumRawEvent.kt | 3 +- .../internal/domain/scope/RumResourceScope.kt | 7 +- .../rum/internal/monitor/DatadogRumMonitor.kt | 36 ++++++++++ .../rum/resource/RumResourceInputStream.kt | 3 +- .../android/rum/assertj/ErrorEventAssert.kt | 6 +- .../rum/assertj/ResourceEventAssert.kt | 8 +-- .../domain/scope/RumActionScopeTest.kt | 19 +++--- .../scope/RumContinuousActionScopeTest.kt | 17 ++--- .../internal/domain/scope/RumEventExtTest.kt | 51 +++----------- .../internal/domain/scope/RumRawEventExt.kt | 3 +- .../domain/scope/RumResourceScopeTest.kt | 6 +- .../internal/domain/scope/RumViewScopeTest.kt | 9 +-- .../internal/monitor/DatadogRumMonitorTest.kt | 58 +++++++++++++++- .../integration/security/EncryptionTest.kt | 3 +- .../android/nightly/rum/RumMonitorE2ETests.kt | 2 +- .../android/nightly/utils/ForgeExtensions.kt | 5 +- .../UserInteractionCustomTargetActivity.kt | 3 +- .../UserInteractionTrackingActivity.kt | 3 +- .../android/okhttp/DatadogInterceptor.kt | 25 ++++++- .../android/okhttp/DatadogInterceptorTest.kt | 55 +++++++++++++++ .../DatadogInterceptorWithoutTracesTest.kt | 67 +++++++++++++++++-- .../TracingInterceptorNotSendingSpanTest.kt | 43 +++++++----- .../sample/service/LogsForegroundService.kt | 3 +- 28 files changed, 432 insertions(+), 130 deletions(-) create mode 100644 features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumResourceMethod.kt diff --git a/features/dd-sdk-android-rum/api/apiSurface b/features/dd-sdk-android-rum/api/apiSurface index e45e68974f..9b16b6482d 100644 --- a/features/dd-sdk-android-rum/api/apiSurface +++ b/features/dd-sdk-android-rum/api/apiSurface @@ -93,7 +93,8 @@ interface com.datadog.android.rum.RumMonitor fun addAction(RumActionType, String, Map) fun startAction(RumActionType, String, Map) fun stopAction(RumActionType, String, Map = emptyMap()) - fun startResource(String, String, String, Map = emptyMap()) + DEPRECATED fun startResource(String, String, String, Map = emptyMap()) + fun startResource(String, RumResourceMethod, String, Map = emptyMap()) fun stopResource(String, Int?, Long?, RumResourceKind, Map) fun stopResourceWithError(String, Int?, String, RumErrorSource, Throwable, Map = emptyMap()) fun stopResourceWithError(String, Int?, String, RumErrorSource, String, String?, Map = emptyMap()) @@ -130,6 +131,13 @@ enum com.datadog.android.rum.RumResourceKind - OTHER companion object fun fromMimeType(String): RumResourceKind +enum com.datadog.android.rum.RumResourceMethod + - POST + - GET + - HEAD + - PUT + - DELETE + - PATCH interface com.datadog.android.rum.RumSessionListener fun onSessionStarted(String, Boolean) class com.datadog.android.rum._RumInternalProxy 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 46b5c0667b..ab7c7df157 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 @@ -144,6 +144,7 @@ public abstract interface class com/datadog/android/rum/RumMonitor { public abstract fun removeAttribute (Ljava/lang/String;)V public abstract fun setDebug (Z)V public abstract fun startAction (Lcom/datadog/android/rum/RumActionType;Ljava/lang/String;Ljava/util/Map;)V + public abstract fun startResource (Ljava/lang/String;Lcom/datadog/android/rum/RumResourceMethod;Ljava/lang/String;Ljava/util/Map;)V public abstract fun startResource (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V public abstract fun startView (Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;)V public abstract fun stopAction (Lcom/datadog/android/rum/RumActionType;Ljava/lang/String;Ljava/util/Map;)V @@ -155,6 +156,7 @@ public abstract interface class com/datadog/android/rum/RumMonitor { } public final class com/datadog/android/rum/RumMonitor$DefaultImpls { + public static synthetic fun startResource$default (Lcom/datadog/android/rum/RumMonitor;Ljava/lang/String;Lcom/datadog/android/rum/RumResourceMethod;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V public static synthetic fun startResource$default (Lcom/datadog/android/rum/RumMonitor;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V public static synthetic fun startView$default (Lcom/datadog/android/rum/RumMonitor;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V public static synthetic fun stopAction$default (Lcom/datadog/android/rum/RumMonitor;Lcom/datadog/android/rum/RumActionType;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V @@ -197,6 +199,17 @@ public final class com/datadog/android/rum/RumResourceKind$Companion { public final fun fromMimeType (Ljava/lang/String;)Lcom/datadog/android/rum/RumResourceKind; } +public final class com/datadog/android/rum/RumResourceMethod : java/lang/Enum { + public static final field DELETE Lcom/datadog/android/rum/RumResourceMethod; + public static final field GET Lcom/datadog/android/rum/RumResourceMethod; + public static final field HEAD Lcom/datadog/android/rum/RumResourceMethod; + public static final field PATCH Lcom/datadog/android/rum/RumResourceMethod; + public static final field POST Lcom/datadog/android/rum/RumResourceMethod; + public static final field PUT Lcom/datadog/android/rum/RumResourceMethod; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/rum/RumResourceMethod; + public static fun values ()[Lcom/datadog/android/rum/RumResourceMethod; +} + public abstract interface class com/datadog/android/rum/RumSessionListener { public abstract fun onSessionStarted (Ljava/lang/String;Z)V } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt index ce9dae6648..ffa6abd682 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumMonitor.kt @@ -111,6 +111,10 @@ interface RumMonitor { * @see [stopResource] * @see [stopResourceWithError] */ + @Deprecated( + "This method is deprecated and will be removed in the future versions." + + " Use `startResource` method which takes `RumHttpMethod` as `method` parameter instead." + ) fun startResource( key: String, method: String, @@ -118,6 +122,24 @@ interface RumMonitor { attributes: Map = emptyMap() ) + /** + * Notify that a new Resource is being loaded, linked with the [key] instance. + * @param key the instance that represents the resource being loaded (usually your + * request or network call instance). + * @param method the method used to load the resource (E.g., for network: "GET" or "POST") + * @param url the url or local path of the resource being loaded + * @param attributes additional custom attributes to attach to the resource. Attributes can be + * nested up to 9 levels deep. Keys using more than 9 levels will be sanitized by SDK. + * @see [stopResource] + * @see [stopResourceWithError] + */ + fun startResource( + key: String, + method: RumResourceMethod, + url: String, + attributes: Map = emptyMap() + ) + /** * Stops a previously started Resource, linked with the [key] instance. * @param key the instance that represents the active view (usually your diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumResourceMethod.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumResourceMethod.kt new file mode 100644 index 0000000000..07a96e1cd2 --- /dev/null +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumResourceMethod.kt @@ -0,0 +1,43 @@ +/* + * 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 + +/** + * Describes the type of a method associated with resource call. + * @see [RumMonitor] + */ +enum class RumResourceMethod { + /** + * POST Method. + */ + POST, + + /** + * GET Method. + */ + GET, + + /** + * HEAD Method. + */ + HEAD, + + /** + * PUT Method. + */ + PUT, + + /** + * DELETE Method. + */ + DELETE, + + /** + * PATCH Method. + */ + PATCH +} diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExt.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExt.kt index 66acb47952..3c418fafc5 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExt.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExt.kt @@ -13,6 +13,7 @@ import com.datadog.android.api.context.NetworkInfo import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.domain.event.ResourceTiming import com.datadog.android.rum.model.ActionEvent @@ -22,31 +23,25 @@ import com.datadog.android.rum.model.ResourceEvent import com.datadog.android.rum.model.ViewEvent import java.util.Locale -internal fun String.toMethod(internalLogger: InternalLogger): ResourceEvent.Method { - return try { - ResourceEvent.Method.valueOf(this.uppercase(Locale.US)) - } catch (e: IllegalArgumentException) { - internalLogger.log( - InternalLogger.Level.ERROR, - listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), - { "Unable to convert [$this] to a valid http method" }, - e - ) - ResourceEvent.Method.GET +internal fun RumResourceMethod.toResourceMethod(): ResourceEvent.Method { + return when (this) { + RumResourceMethod.GET -> ResourceEvent.Method.GET + RumResourceMethod.POST -> ResourceEvent.Method.POST + RumResourceMethod.HEAD -> ResourceEvent.Method.HEAD + RumResourceMethod.PUT -> ResourceEvent.Method.PUT + RumResourceMethod.DELETE -> ResourceEvent.Method.DELETE + RumResourceMethod.PATCH -> ResourceEvent.Method.PATCH } } -internal fun String.toErrorMethod(internalLogger: InternalLogger): ErrorEvent.Method { - return try { - ErrorEvent.Method.valueOf(this.uppercase(Locale.US)) - } catch (e: IllegalArgumentException) { - internalLogger.log( - InternalLogger.Level.ERROR, - listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY), - { "Unable to convert [$this] to a valid http method" }, - e - ) - ErrorEvent.Method.GET +internal fun RumResourceMethod.toErrorMethod(): ErrorEvent.Method { + return when (this) { + RumResourceMethod.GET -> ErrorEvent.Method.GET + RumResourceMethod.POST -> ErrorEvent.Method.POST + RumResourceMethod.HEAD -> ErrorEvent.Method.HEAD + RumResourceMethod.PUT -> ErrorEvent.Method.PUT + RumResourceMethod.DELETE -> ErrorEvent.Method.DELETE + RumResourceMethod.PATCH -> ErrorEvent.Method.PATCH } } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt index fb6ed56339..021732b939 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEvent.kt @@ -10,6 +10,7 @@ import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.domain.Time import com.datadog.android.rum.internal.domain.event.ResourceTiming @@ -51,7 +52,7 @@ internal sealed class RumRawEvent { internal data class StartResource( val key: String, val url: String, - val method: String, + val method: RumResourceMethod, val attributes: Map, override val eventTime: Time = Time() ) : RumRawEvent() diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt index 930718e5ae..4574fa7cec 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScope.kt @@ -17,6 +17,7 @@ import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time @@ -34,7 +35,7 @@ internal class RumResourceScope( internal val parentScope: RumScope, internal val sdkCore: InternalSdkCore, internal val url: String, - internal val method: String, + internal val method: RumResourceMethod, internal val key: String, eventTime: Time, initialAttributes: Map, @@ -201,7 +202,7 @@ internal class RumResourceScope( type = kind.toSchemaType(), url = url, duration = duration, - method = method.toMethod(sdkCore.internalLogger), + method = method.toResourceMethod(), statusCode = statusCode, size = size, dns = finalTiming?.dns(), @@ -323,7 +324,7 @@ internal class RumResourceScope( isCrash = false, resource = ErrorEvent.Resource( url = url, - method = method.toErrorMethod(sdkCore.internalLogger), + method = method.toErrorMethod(), statusCode = statusCode ?: 0, provider = resolveErrorProvider() ), 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 f4da27f870..54db148f0b 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 @@ -20,6 +20,7 @@ import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum._RumInternalProxy import com.datadog.android.rum.internal.CombinedRumSessionListener @@ -161,11 +162,46 @@ internal class DatadogRumMonitor( ) } + @Deprecated( + "This method is deprecated and will be removed in the future versions." + + " Use `startResource` method which takes `RumHttpMethod` as `method` parameter instead." + ) override fun startResource( key: String, method: String, url: String, attributes: Map + ) { + // enum value names may be changed if obfuscation is aggressive + val rumResourceMethod = when (method.uppercase(Locale.US)) { + "POST" -> RumResourceMethod.POST + "GET" -> RumResourceMethod.GET + "HEAD" -> RumResourceMethod.HEAD + "PUT" -> RumResourceMethod.PUT + "DELETE" -> RumResourceMethod.DELETE + "PATCH" -> RumResourceMethod.PATCH + else -> { + sdkCore.internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.USER, + { + "Unsupported HTTP method %s reported, using GET instead".format( + Locale.US, + method + ) + } + ) + RumResourceMethod.GET + } + } + startResource(key, rumResourceMethod, url, attributes) + } + + override fun startResource( + key: String, + method: RumResourceMethod, + url: String, + attributes: Map ) { val eventTime = getEventTime(attributes) handleEvent( diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/resource/RumResourceInputStream.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/resource/RumResourceInputStream.kt index 2dddcf3406..85d4eda80a 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/resource/RumResourceInputStream.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/resource/RumResourceInputStream.kt @@ -11,6 +11,7 @@ import com.datadog.android.api.SdkCore import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.domain.event.ResourceTiming import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor import java.io.InputStream @@ -176,7 +177,7 @@ class RumResourceInputStream @JvmOverloads constructor( // endregion internal companion object { - internal const val METHOD: String = "GET" + internal val METHOD: RumResourceMethod = RumResourceMethod.GET internal const val ERROR_CLOSE = "Error closing input stream" internal const val ERROR_MARK = "Error marking input stream" diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt index 563644fc13..9a6b02b8e6 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ErrorEventAssert.kt @@ -9,8 +9,10 @@ package com.datadog.android.rum.assertj import com.datadog.android.api.context.NetworkInfo import com.datadog.android.api.context.UserInfo import com.datadog.android.rum.RumErrorSource +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.scope.isConnected +import com.datadog.android.rum.internal.domain.scope.toErrorMethod import com.datadog.android.rum.internal.domain.scope.toSchemaSource import com.datadog.android.rum.model.ErrorEvent import org.assertj.core.api.AbstractObjectAssert @@ -76,7 +78,7 @@ internal class ErrorEventAssert(actual: ErrorEvent) : fun hasResource( expectedUrl: String, - expectedMethod: String, + expectedMethod: RumResourceMethod, expectedStatusCode: Long ): ErrorEventAssert { assertThat(actual.error.resource?.url) @@ -90,7 +92,7 @@ internal class ErrorEventAssert(actual: ErrorEvent) : "Expected event data to have error.resource.method $expectedMethod " + "but was ${actual.error.resource?.method}" ) - .isEqualTo(ErrorEvent.Method.valueOf(expectedMethod)) + .isEqualTo(expectedMethod.toErrorMethod()) assertThat(actual.error.resource?.statusCode) .overridingErrorMessage( "Expected event data to have error.resource.statusCode $expectedStatusCode " + diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ResourceEventAssert.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ResourceEventAssert.kt index 25ed3a20f9..d74394c4cd 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ResourceEventAssert.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ResourceEventAssert.kt @@ -9,16 +9,16 @@ package com.datadog.android.rum.assertj import com.datadog.android.api.context.NetworkInfo import com.datadog.android.api.context.UserInfo import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.event.ResourceTiming import com.datadog.android.rum.internal.domain.scope.isConnected -import com.datadog.android.rum.internal.domain.scope.toMethod +import com.datadog.android.rum.internal.domain.scope.toResourceMethod import com.datadog.android.rum.internal.domain.scope.toSchemaType import com.datadog.android.rum.model.ResourceEvent import org.assertj.core.api.AbstractObjectAssert import org.assertj.core.api.Assertions.assertThat import org.assertj.core.data.Offset -import org.mockito.kotlin.mock internal class ResourceEventAssert(actual: ResourceEvent) : AbstractObjectAssert( @@ -68,13 +68,13 @@ internal class ResourceEventAssert(actual: ResourceEvent) : return this } - fun hasMethod(expected: String): ResourceEventAssert { + fun hasMethod(expected: RumResourceMethod): ResourceEventAssert { assertThat(actual.resource.method) .overridingErrorMessage( "Expected event data to have resource.method $expected " + "but was ${actual.resource.method}" ) - .isEqualTo(expected.toMethod(mock())) + .isEqualTo(expected.toResourceMethod()) return this } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt index c088e4f89a..bfd2778f8e 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt @@ -16,6 +16,7 @@ import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.assertj.ActionEventAssert.Companion.assertThat import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.domain.RumContext @@ -203,7 +204,7 @@ internal class RumActionScopeTest { @Test fun `𝕄 send Action after threshold 𝕎 handleEvent(StartResource+StopResource+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, @LongForgery(200, 600) statusCode: Long, @LongForgery(0, 1024) size: Long, @@ -270,7 +271,7 @@ internal class RumActionScopeTest { fun `𝕄 do nothing 𝕎 handleEvent(StartResource+StopResource+any) {unknown key}`( @StringForgery key: String, @StringForgery key2: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, @LongForgery(200, 600) statusCode: Long, @LongForgery(0, 1024) size: Long, @@ -296,7 +297,7 @@ internal class RumActionScopeTest { @Test fun `𝕄 send Action after threshold 𝕎 handleEvent(StartResource+StopResourceWithError+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, @LongForgery(200, 600) statusCode: Long, @StringForgery message: String, @@ -374,7 +375,7 @@ internal class RumActionScopeTest { @Test fun `𝕄 send Action after threshold 𝕎 handleEvent(StartResource+StopResourceWithStackTrace)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, @LongForgery(200, 600) statusCode: Long, @StringForgery message: String, @@ -457,7 +458,7 @@ internal class RumActionScopeTest { fun `𝕄 send Action 𝕎 handleEvent(StartResource+StopResourceWithError+any) {unknown key}`( @StringForgery key: String, @StringForgery key2: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, @LongForgery(200, 600) statusCode: Long, @StringForgery message: String, @@ -491,7 +492,7 @@ internal class RumActionScopeTest { fun `𝕄 send Action 𝕎 handleEvent(StartResource+StopResourceWithStackTrace+any) {unknown key}`( @StringForgery key: String, @StringForgery key2: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, @LongForgery(200, 600) statusCode: Long, @StringForgery message: String, @@ -528,7 +529,7 @@ internal class RumActionScopeTest { @Test fun `𝕄 send Action after threshold 𝕎 handleEvent(StartResource+any) missing resource key`( - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { // Given @@ -2053,7 +2054,7 @@ internal class RumActionScopeTest { @Test fun `𝕄 doNothing 𝕎 handleEvent(StartResource+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { // When @@ -2071,7 +2072,7 @@ internal class RumActionScopeTest { @Test fun `𝕄 send Action after timeout 𝕎 handleEvent(StartResource+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { // When diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt index ac203b4072..0387769e72 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumContinuousActionScopeTest.kt @@ -16,6 +16,7 @@ import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.assertj.ActionEventAssert.Companion.assertThat import com.datadog.android.rum.internal.domain.RumContext import com.datadog.android.rum.internal.domain.Time @@ -395,7 +396,7 @@ internal class RumContinuousActionScopeTest { @Test fun `𝕄 send Action after threshold 𝕎 handleEvent(StartResource+StopAction+StopResource+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, @LongForgery(200, 600) statusCode: Long, @LongForgery(0, 1024) size: Long, @@ -463,7 +464,7 @@ internal class RumContinuousActionScopeTest { @Test fun `𝕄 send Action 𝕎 handleEvent(StartResource+StopAction+StopResourceWithError+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, @LongForgery(200, 600) statusCode: Long, @StringForgery message: String, @@ -543,7 +544,7 @@ internal class RumContinuousActionScopeTest { @Test fun `𝕄 send Action 𝕎 handleEvent(StartResource+StopAction+StopResourceWithStackTrace+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, @LongForgery(200, 600) statusCode: Long, @StringForgery message: String, @@ -626,7 +627,7 @@ internal class RumContinuousActionScopeTest { @Test fun `𝕄 send Action 𝕎 handleEvent(StartResource+StopAction+any) missing resource key`( - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { // Given @@ -1774,7 +1775,7 @@ internal class RumContinuousActionScopeTest { @Test fun `𝕄 do nothing 𝕎 handleEvent(StartResource+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { // When @@ -1792,7 +1793,7 @@ internal class RumContinuousActionScopeTest { @Test fun `𝕄 do nothing 𝕎 handleEvent(StartResource+StopAction+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { // When @@ -1814,7 +1815,7 @@ internal class RumContinuousActionScopeTest { @Test fun `𝕄 send Action after timeout 𝕎 handleEvent(StartResource+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { // When @@ -1872,7 +1873,7 @@ internal class RumContinuousActionScopeTest { @Test fun `𝕄 send Action after timeout 𝕎 handleEvent(StartResource+StopAction+any)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { // When diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExtTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExtTest.kt index 071fd295a0..9a941f6cba 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExtTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumEventExtTest.kt @@ -11,6 +11,7 @@ import com.datadog.android.api.context.NetworkInfo import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.model.ActionEvent import com.datadog.android.rum.model.ErrorEvent @@ -20,7 +21,6 @@ import com.datadog.android.rum.model.ViewEvent import com.datadog.android.rum.utils.forge.Configurator import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.annotation.StringForgeryType import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -29,7 +29,6 @@ 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.EnumSource -import org.mockito.kotlin.mock @Extensions( ExtendWith(ForgeExtension::class) @@ -38,55 +37,27 @@ import org.mockito.kotlin.mock internal class RumEventExtTest { @ParameterizedTest - @EnumSource(ResourceEvent.Method::class) - fun `𝕄 return method 𝕎 toMethod() {valid name}`( - method: ResourceEvent.Method - ) { - // Given - val name = method.name - - // When - val result = name.toMethod(internalLogger = mock()) - - // Then - assertThat(result).isEqualTo(method) - } - - @Test - fun `𝕄 return GET 𝕎 toMethod() {invalid name}`( - @StringForgery(type = StringForgeryType.NUMERICAL) name: String + @EnumSource(RumResourceMethod::class) + fun `𝕄 return method 𝕎 toMethod()`( + method: RumResourceMethod ) { // When - val result = name.toMethod(internalLogger = mock()) + val result = method.toResourceMethod() // Then - assertThat(result).isEqualTo(ResourceEvent.Method.GET) + assertThat(result.name).isEqualTo(method.name) } @ParameterizedTest - @EnumSource(ErrorEvent.Method::class) - fun `𝕄 return method 𝕎 toErrorMethod() {valid name}`( - method: ErrorEvent.Method - ) { - // Given - val name = method.name - - // When - val result = name.toErrorMethod(internalLogger = mock()) - - // Then - assertThat(result).isEqualTo(method) - } - - @Test - fun `𝕄 return GET 𝕎 toErrorMethod() {invalid name}`( - @StringForgery(type = StringForgeryType.NUMERICAL) name: String + @EnumSource(RumResourceMethod::class) + fun `𝕄 return method 𝕎 toErrorMethod()`( + method: RumResourceMethod ) { // When - val result = name.toErrorMethod(internalLogger = mock()) + val result = method.toErrorMethod() // Then - assertThat(result).isEqualTo(ErrorEvent.Method.GET) + assertThat(result.name).isEqualTo(method.name) } @ParameterizedTest diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt index 842b34c0e0..a55074222c 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumRawEventExt.kt @@ -9,6 +9,7 @@ package com.datadog.android.rum.internal.domain.scope import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.internal.domain.Time import com.datadog.tools.unit.forge.aThrowable import com.datadog.tools.unit.forge.exhaustiveAttributes @@ -60,7 +61,7 @@ internal fun Forge.startResourceEvent(): RumRawEvent.StartResource { return RumRawEvent.StartResource( key = anAlphabeticalString(), url = getForgery().toString(), - method = anElementFrom("POST", "GET", "PUT", "DELETE", "HEAD"), + method = aValueFrom(RumResourceMethod::class.java), attributes = exhaustiveAttributes() ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt index 8ef16365c2..1319420eb4 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumResourceScopeTest.kt @@ -18,6 +18,7 @@ import com.datadog.android.core.internal.utils.loggableStackTrace import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.assertj.ErrorEventAssert.Companion.assertThat import com.datadog.android.rum.assertj.ResourceEventAssert.Companion.assertThat import com.datadog.android.rum.internal.FeaturesContextResolver @@ -104,7 +105,9 @@ internal class RumResourceScopeTest { @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") lateinit var fakeUrl: String lateinit var fakeKey: String - lateinit var fakeMethod: String + + @Forgery + lateinit var fakeMethod: RumResourceMethod lateinit var fakeAttributes: Map @Forgery @@ -164,7 +167,6 @@ internal class RumResourceScopeTest { forge.aLong(min = minLimit, max = maxLimit) fakeAttributes = forge.exhaustiveAttributes() fakeKey = forge.anAsciiString() - fakeMethod = forge.anElementFrom("PUT", "POST", "GET", "DELETE") mockEvent = mockEvent() fakeSampleRate = forge.aFloat(min = 0.0f, max = 100.0f) diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt index 2e6f019d98..c46052be58 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt @@ -20,6 +20,7 @@ import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumPerformanceMetric +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.assertj.ActionEventAssert.Companion.assertThat import com.datadog.android.rum.assertj.ErrorEventAssert.Companion.assertThat import com.datadog.android.rum.assertj.LongTaskEventAssert.Companion.assertThat @@ -3808,7 +3809,7 @@ internal class RumViewScopeTest { @Test fun `𝕄 create ResourceScope 𝕎 handleEvent(StartResource)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, forge: Forge ) { @@ -3844,7 +3845,7 @@ internal class RumViewScopeTest { @Test fun `𝕄 create ResourceScope with active actionId 𝕎 handleEvent(StartResource)`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, forge: Forge ) { @@ -3931,7 +3932,7 @@ internal class RumViewScopeTest { @Test fun `𝕄 wait for pending Resource 𝕎 handleEvent(StartResource) on active view`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { // Given @@ -6470,7 +6471,7 @@ internal class RumViewScopeTest { RumRawEvent.StartResource( forge.anAlphabeticalString(), forge.anAlphabeticalString(), - forge.anAlphabeticalString(), + forge.aValueFrom(RumResourceMethod::class.java), emptyMap() ), RumRawEvent.ResourceSent( diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt index d89483c54a..8f4e74ab1f 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt @@ -19,6 +19,7 @@ import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumPerformanceMetric import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.internal.RumErrorSourceType import com.datadog.android.rum.internal.RumFeature @@ -77,6 +78,7 @@ import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import java.util.Locale import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future @@ -287,10 +289,62 @@ internal class DatadogRumMonitorTest { } @Test - fun `M delegate event to rootScope W startResource()`( + fun `M delegate event to rootScope W startResource() { deprecated, known http method }`( + @StringForgery key: String, + @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String, + forge: Forge + ) { + val method = forge.anElementFrom( + "GeT", + "PoSt", + "pUt", + "HeAd", + "DeLeTe", + "pAtCh" + ) + @Suppress("DEPRECATION") + testedMonitor.startResource(key, method, url, fakeAttributes) + Thread.sleep(PROCESSING_DELAY) + + argumentCaptor { + verify(mockScope).handleEvent(capture(), same(mockWriter)) + + val event = firstValue as RumRawEvent.StartResource + assertThat(event.key).isEqualTo(key) + assertThat(event.method.name).isEqualTo(method.uppercase(Locale.US)) + assertThat(event.url).isEqualTo(url) + assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) + } + verifyNoMoreInteractions(mockScope, mockWriter) + } + + @Test + fun `M delegate event to rootScope W startResource() { deprecated, unknown http method }`( @StringForgery key: String, @StringForgery method: String, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String + ) { + @Suppress("DEPRECATION") + testedMonitor.startResource(key, method, url, fakeAttributes) + Thread.sleep(PROCESSING_DELAY) + + argumentCaptor { + verify(mockScope).handleEvent(capture(), same(mockWriter)) + + val event = firstValue as RumRawEvent.StartResource + assertThat(event.key).isEqualTo(key) + assertThat(event.method).isEqualTo(RumResourceMethod.GET) + assertThat(event.url).isEqualTo(url) + assertThat(event.attributes).containsAllEntriesOf(fakeAttributes) + } + verifyNoMoreInteractions(mockScope, mockWriter) + } + + @Test + fun `M delegate event to rootScope W startResource()`( + @StringForgery key: String, + @Forgery method: RumResourceMethod, + @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { testedMonitor.startResource(key, method, url, fakeAttributes) Thread.sleep(PROCESSING_DELAY) @@ -698,7 +752,7 @@ internal class DatadogRumMonitorTest { @Test fun `M delegate event to rootScope with timestamp W startResource()`( @StringForgery key: String, - @StringForgery method: String, + @Forgery method: RumResourceMethod, @StringForgery(regex = "http(s?)://[a-z]+\\.com/[a-z]+") url: String ) { val attributes = fakeAttributes + (RumAttributes.INTERNAL_TIMESTAMP to fakeTimestamp) diff --git a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/security/EncryptionTest.kt b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/security/EncryptionTest.kt index 50ce0e1b40..03c6ef09ec 100644 --- a/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/security/EncryptionTest.kt +++ b/instrumented/integration/src/androidTest/kotlin/com/datadog/android/sdk/integration/security/EncryptionTest.kt @@ -21,6 +21,7 @@ import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumConfiguration import com.datadog.android.rum.RumMonitor import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import com.datadog.android.security.Encryption import com.datadog.android.sessionreplay.SessionReplay import com.datadog.android.sessionreplay.SessionReplayConfiguration @@ -170,7 +171,7 @@ internal class EncryptionTest { rumMonitor.startResource( resourceName, - "GET", + RumResourceMethod.GET, "https://${forge.anAlphaNumericalString()}.com" ) diff --git a/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/rum/RumMonitorE2ETests.kt b/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/rum/RumMonitorE2ETests.kt index a26aa348ea..52c2df2b38 100644 --- a/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/rum/RumMonitorE2ETests.kt +++ b/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/rum/RumMonitorE2ETests.kt @@ -639,7 +639,7 @@ class RumMonitorE2ETests { // region Resource /** - * apiMethodSignature: com.datadog.android.rum.RumMonitor#fun startResource(String, String, String, Map = emptyMap()) + * apiMethodSignature: com.datadog.android.rum.RumMonitor#fun startResource(String, HttpMethod, String, Map = emptyMap()) */ @Test fun rum_rummonitor_start_resource() { diff --git a/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/utils/ForgeExtensions.kt b/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/utils/ForgeExtensions.kt index 5c37e74a59..92b0a6e098 100644 --- a/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/utils/ForgeExtensions.kt +++ b/instrumented/nightly-tests/src/androidTest/kotlin/com/datadog/android/nightly/utils/ForgeExtensions.kt @@ -13,6 +13,7 @@ import com.datadog.android.nightly.rum.RUM_RESOURCE_URL_PREFIX import com.datadog.android.nightly.rum.RUM_VIEW_PREFIX import com.datadog.android.nightly.rum.RUM_VIEW_URL_PREFIX import com.datadog.android.nightly.rum.TAG_VALUE_PREFIX +import com.datadog.android.rum.RumResourceMethod import fr.xgouchet.elmyr.Forge fun Forge.exhaustiveAttributes(): Map { @@ -52,8 +53,8 @@ fun Forge.anErrorMessage(prefix: String = RUM_ERROR_MESSAGE_PREFIX): String { return prefix + this.aStringMatching("[a-zA-z](.+)") } -fun Forge.aResourceMethod(): String { - return this.anElementFrom(listOf("GET", "POST")) +fun Forge.aResourceMethod(): RumResourceMethod { + return this.aValueFrom(RumResourceMethod::class.java) } fun Forge.aTagValue(prefix: String = TAG_VALUE_PREFIX): String { diff --git a/instrumented/nightly-tests/src/main/kotlin/com/datadog/android/nightly/activities/UserInteractionCustomTargetActivity.kt b/instrumented/nightly-tests/src/main/kotlin/com/datadog/android/nightly/activities/UserInteractionCustomTargetActivity.kt index 418a47e295..679bcecf9f 100644 --- a/instrumented/nightly-tests/src/main/kotlin/com/datadog/android/nightly/activities/UserInteractionCustomTargetActivity.kt +++ b/instrumented/nightly-tests/src/main/kotlin/com/datadog/android/nightly/activities/UserInteractionCustomTargetActivity.kt @@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity import com.datadog.android.nightly.R import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumResourceKind +import com.datadog.android.rum.RumResourceMethod import java.net.HttpURLConnection internal class UserInteractionCustomTargetActivity : AppCompatActivity() { @@ -23,7 +24,7 @@ internal class UserInteractionCustomTargetActivity : AppCompatActivity() { findViewById