From d041c0a9dfd81e56285b0c1eade06dc48f30fd76 Mon Sep 17 00:00:00 2001 From: Nikita Ogorodnikov Date: Fri, 27 Sep 2024 10:16:58 +0200 Subject: [PATCH] Use SDK source value in Session Replay MobileSegment.source property --- .../internal/net/BatchesToSegmentsMapper.kt | 16 ++-- .../internal/net/SegmentRequestFactory.kt | 2 +- .../internal/processor/MobileSegmentExt.kt | 21 +++++ .../net/BatchesToSegmentsMapperTest.kt | 49 ++++++---- .../internal/net/SegmentRequestFactoryTest.kt | 4 +- .../processor/MobileSegmentExtTest.kt | 90 +++++++++++++++++++ 6 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExtTest.kt diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/BatchesToSegmentsMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/BatchesToSegmentsMapper.kt index 72cdd65501..af310a3081 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/BatchesToSegmentsMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/BatchesToSegmentsMapper.kt @@ -7,10 +7,12 @@ package com.datadog.android.sessionreplay.internal.net import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext import com.datadog.android.sessionreplay.internal.gson.safeGetAsJsonArray import com.datadog.android.sessionreplay.internal.gson.safeGetAsJsonObject import com.datadog.android.sessionreplay.internal.gson.safeGetAsLong import com.datadog.android.sessionreplay.internal.processor.EnrichedRecord +import com.datadog.android.sessionreplay.internal.processor.tryFromSource import com.datadog.android.sessionreplay.internal.utils.SessionReplayRumContext import com.datadog.android.sessionreplay.model.MobileSegment import com.google.gson.JsonArray @@ -24,13 +26,16 @@ import com.google.gson.JsonParser */ internal class BatchesToSegmentsMapper(private val internalLogger: InternalLogger) { - fun map(batchData: List): List> { - return groupBatchDataIntoSegments(batchData) + fun map(datadogContext: DatadogContext, batchData: List): List> { + return groupBatchDataIntoSegments(datadogContext, batchData) } // region Internal - private fun groupBatchDataIntoSegments(batchData: List): List> { + private fun groupBatchDataIntoSegments( + datadogContext: DatadogContext, + batchData: List + ): List> { return batchData .asSequence() .mapNotNull { @@ -73,12 +78,13 @@ internal class BatchesToSegmentsMapper(private val internalLogger: InternalLogge } .filter { !it.value.isEmpty } .mapNotNull { - mapToSegment(it.key, it.value) + mapToSegment(datadogContext, it.key, it.value) } } @Suppress("ReturnCount") private fun mapToSegment( + datadogContext: DatadogContext, rumContext: SessionReplayRumContext, records: JsonArray ): Pair? { @@ -132,7 +138,7 @@ internal class BatchesToSegmentsMapper(private val internalLogger: InternalLogge // TODO RUM-861 Find a way or alternative to provide a reliable indexInView indexInView = null, hasFullSnapshot = hasFullSnapshotRecord, - source = MobileSegment.Source.ANDROID, + source = MobileSegment.Source.tryFromSource(datadogContext.source, internalLogger), records = emptyList() ) val segmentAsJsonObject = segment.toJson().safeGetAsJsonObject(internalLogger) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/SegmentRequestFactory.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/SegmentRequestFactory.kt index db8966af37..5abc07e09a 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/SegmentRequestFactory.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/SegmentRequestFactory.kt @@ -27,7 +27,7 @@ internal class SegmentRequestFactory( batchData: List, batchMetadata: ByteArray? ): Request { - val serializedSegmentPair = batchToSegmentsMapper.map(batchData.map { it.data }) + val serializedSegmentPair = batchToSegmentsMapper.map(context, batchData.map { it.data }) if (serializedSegmentPair.isEmpty()) { @Suppress("ThrowingInternalException") throw InvalidPayloadFormatException( diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt index 433ed400ac..554f7e7070 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt @@ -6,6 +6,7 @@ package com.datadog.android.sessionreplay.internal.processor +import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.model.MobileSegment internal fun MobileSegment.Wireframe.copy(clip: MobileSegment.WireframeClip?): MobileSegment.Wireframe { @@ -17,3 +18,23 @@ internal fun MobileSegment.Wireframe.copy(clip: MobileSegment.WireframeClip?): M is MobileSegment.Wireframe.WebviewWireframe -> this.copy(clip = clip) } } + +internal fun MobileSegment.Source.Companion.tryFromSource( + source: String, + internalLogger: InternalLogger +): MobileSegment.Source { + return try { + fromJson(source) + } catch (e: NoSuchElementException) { + internalLogger.log( + InternalLogger.Level.ERROR, + InternalLogger.Target.MAINTAINER, + { UNKNOWN_MOBILE_SEGMENT_SOURCE_WARNING_MESSAGE_FORMAT.format(java.util.Locale.US, source) }, + e + ) + MobileSegment.Source.ANDROID + } +} + +internal const val UNKNOWN_MOBILE_SEGMENT_SOURCE_WARNING_MESSAGE_FORMAT = "You are using an unknown " + + "source %s for MobileSegment.Source enum." diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/net/BatchesToSegmentsMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/net/BatchesToSegmentsMapperTest.kt index 61337ce621..01c1dc6f15 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/net/BatchesToSegmentsMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/net/BatchesToSegmentsMapperTest.kt @@ -7,6 +7,7 @@ package com.datadog.android.sessionreplay.internal.net import com.datadog.android.api.InternalLogger +import com.datadog.android.api.context.DatadogContext import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.internal.processor.EnrichedRecord import com.datadog.android.sessionreplay.internal.utils.SessionReplayRumContext @@ -15,6 +16,7 @@ import com.datadog.android.utils.verifyLog import com.google.gson.JsonParser import com.google.gson.JsonPrimitive import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.annotation.Forgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import fr.xgouchet.elmyr.jvm.ext.aTimestamp @@ -40,11 +42,22 @@ internal class BatchesToSegmentsMapperTest { @Mock lateinit var mockInternalLogger: InternalLogger + @Forgery + lateinit var datadogContext: DatadogContext + private lateinit var testedMapper: BatchesToSegmentsMapper + var fakeSegmentSource: MobileSegment.Source? = null + @BeforeEach - fun `set up`() { + fun `set up`(forge: Forge) { testedMapper = BatchesToSegmentsMapper(mockInternalLogger) + + fakeSegmentSource = forge.aNullable { aValueFrom(MobileSegment.Source::class.java) } + + datadogContext = datadogContext.copy( + source = fakeSegmentSource?.toJson()?.asString ?: forge.aString() + ) } @Test @@ -71,7 +84,7 @@ internal class BatchesToSegmentsMapperTest { val fakeBatchData = fakeEnrichedRecords.map { it.toJson().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) // Then assertThat(mappedSegments.size).isEqualTo(fakeEnrichedRecords.size) @@ -106,7 +119,7 @@ internal class BatchesToSegmentsMapperTest { val fakeBatchData = fakeEnrichedRecords.map { it.toJson().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) // Then mappedSegments.forEachIndexed { index, pair -> @@ -140,7 +153,7 @@ internal class BatchesToSegmentsMapperTest { val fakeBatchData = fakeEnrichedRecords.map { it.toJson().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) // Then mappedSegments.forEachIndexed { index, pair -> @@ -177,7 +190,7 @@ internal class BatchesToSegmentsMapperTest { val fakeBatchData = fakeEnrichedRecords.map { it.toJson().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) // Then mappedSegments.forEachIndexed { index, pair -> @@ -201,7 +214,7 @@ internal class BatchesToSegmentsMapperTest { val fakeBatchData = fakeEnrichedRecords.map { it.toJson().toByteArray() } // When - assertThat(testedMapper.map(fakeBatchData)).isEmpty() + assertThat(testedMapper.map(datadogContext, fakeBatchData)).isEmpty() } @Test @@ -212,7 +225,7 @@ internal class BatchesToSegmentsMapperTest { val fakeBatchData = forge.aList { forge.anAlphabeticalString().toByteArray() } // When - assertThat(testedMapper.map(fakeBatchData)).isEmpty() + assertThat(testedMapper.map(datadogContext, fakeBatchData)).isEmpty() } @Test @@ -249,7 +262,7 @@ internal class BatchesToSegmentsMapperTest { .map { it.toString().toByteArray() } // When - assertThat(testedMapper.map(fakeBatchData)).isEmpty() + assertThat(testedMapper.map(datadogContext, fakeBatchData)).isEmpty() } @Test @@ -292,7 +305,7 @@ internal class BatchesToSegmentsMapperTest { .map { it.toString().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) val expectedRecordsSize = fakeRecords.size - removedRecords assertThat(mappedSegments.size).isEqualTo(1) assertThat(mappedSegments[0].first.recordsCount.toInt()).isEqualTo(expectedRecordsSize) @@ -329,7 +342,7 @@ internal class BatchesToSegmentsMapperTest { } .map { it.toString().toByteArray() } // When - assertThat(testedMapper.map(fakeBatchData)).isEmpty() + assertThat(testedMapper.map(datadogContext, fakeBatchData)).isEmpty() mockInternalLogger.verifyLog( InternalLogger.Level.ERROR, InternalLogger.Target.TELEMETRY, @@ -376,7 +389,7 @@ internal class BatchesToSegmentsMapperTest { .map { it.toString().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) assertThat(mappedSegments.size).isEqualTo(1) val expectedRecordsSize = fakeRecords.size - removedRecords assertThat(mappedSegments[0].first.recordsCount.toInt()).isEqualTo(expectedRecordsSize) @@ -415,7 +428,7 @@ internal class BatchesToSegmentsMapperTest { .map { it.toString().toByteArray() } // When - assertThat(testedMapper.map(fakeBatchData)).isEmpty() + assertThat(testedMapper.map(datadogContext, fakeBatchData)).isEmpty() mockInternalLogger.verifyLog( InternalLogger.Level.ERROR, InternalLogger.Target.TELEMETRY, @@ -462,7 +475,7 @@ internal class BatchesToSegmentsMapperTest { .map { it.toString().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) // Then val expectedRecordsSize = fakeRecords.size - removedRecords @@ -502,7 +515,7 @@ internal class BatchesToSegmentsMapperTest { .map { it.toString().toByteArray() } // Then - assertThat(testedMapper.map(fakeBatchData)).isEmpty() + assertThat(testedMapper.map(datadogContext, fakeBatchData)).isEmpty() mockInternalLogger.verifyLog( InternalLogger.Level.ERROR, InternalLogger.Target.TELEMETRY, @@ -549,7 +562,7 @@ internal class BatchesToSegmentsMapperTest { .map { it.toString().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) // Then val expectedRecordsSize = fakeRecords.size - removedRecords @@ -603,7 +616,7 @@ internal class BatchesToSegmentsMapperTest { .map { it.toString().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) // Then val expectedRecordsSize = fakeRecords.size - removedRecords @@ -651,7 +664,7 @@ internal class BatchesToSegmentsMapperTest { .map { it.toString().toByteArray() } // When - val mappedSegments = testedMapper.map(fakeBatchData) + val mappedSegments = testedMapper.map(datadogContext, fakeBatchData) // Then val expectedRecordsSize = fakeRecords.size - removedRecords @@ -673,7 +686,7 @@ internal class BatchesToSegmentsMapperTest { recordsCount = records.size.toLong(), indexInView = null, hasFullSnapshot = hasFullSnapshot(), - source = MobileSegment.Source.ANDROID, + source = fakeSegmentSource ?: MobileSegment.Source.ANDROID, records = records ) } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/net/SegmentRequestFactoryTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/net/SegmentRequestFactoryTest.kt index 283957695e..6335e2cd1c 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/net/SegmentRequestFactoryTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/net/SegmentRequestFactoryTest.kt @@ -87,7 +87,7 @@ internal class SegmentRequestFactoryTest { fakeBatchMetadata = forge.aNullable { forge.aString().toByteArray() } whenever(mockSegmentRequestBodyFactory.create(fakeDataGroup)) .thenReturn(mockRequestBody) - whenever(mockBatchesToSegmentsMapper.map(fakeBatchData.map { it.data })) + whenever(mockBatchesToSegmentsMapper.map(fakeDatadogContext, fakeBatchData.map { it.data })) .thenReturn(fakeDataGroup) testedRequestFactory = SegmentRequestFactory( customEndpointUrl = null, @@ -160,7 +160,7 @@ internal class SegmentRequestFactoryTest { @Test fun `M throw exception W create(){ payload is broken }`() { // Given - whenever(mockBatchesToSegmentsMapper.map(fakeBatchData.map { it.data })) + whenever(mockBatchesToSegmentsMapper.map(fakeDatadogContext, fakeBatchData.map { it.data })) .thenReturn(emptyList()) // When diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExtTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExtTest.kt new file mode 100644 index 0000000000..91e0b57c1f --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExtTest.kt @@ -0,0 +1,90 @@ +package com.datadog.android.sessionreplay.internal.processor + +import com.datadog.android.api.InternalLogger +import com.datadog.android.sessionreplay.model.MobileSegment +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.isA +import org.mockito.kotlin.isNull +import org.mockito.kotlin.verify +import java.util.Locale + +@Extensions( + ExtendWith(ForgeExtension::class), + ExtendWith(MockitoExtension::class) +) +internal class MobileSegmentExtTest { + + @Mock + lateinit var mockInternalLogger: InternalLogger + + // region MobileSegment.Source + + @Test + fun `M resolve the MobileSegment source W tryFromSource`( + forge: Forge + ) { + // Given + val fakeValidSource = forge.aValueFrom(MobileSegment.Source::class.java) + + // When + val source = MobileSegment.Source.tryFromSource(fakeValidSource.toJson().asString, mockInternalLogger) + + // Then + assertThat(source).isEqualTo(fakeValidSource) + } + + @Test + fun `M return default value W tryFromSource { unknown source }`( + forge: Forge + ) { + // Given + val fakeInvalidSource = forge.aString() + + // When + val source = MobileSegment.Source.tryFromSource(fakeInvalidSource, mockInternalLogger) + + // Then + assertThat(source).isEqualTo(MobileSegment.Source.ANDROID) + } + + @Test + fun `M send an error maintainer log W tryFromSource { unknown source }`( + forge: Forge + ) { + // Given + val fakeInvalidSource = forge.aString() + + // When + MobileSegment.Source.tryFromSource(fakeInvalidSource, mockInternalLogger) + + // Then + argumentCaptor<() -> String>() { + verify(mockInternalLogger).log( + level = eq(InternalLogger.Level.ERROR), + target = eq(InternalLogger.Target.MAINTAINER), + messageBuilder = capture(), + throwable = isA(), + onlyOnce = eq(false), + additionalProperties = isNull() + ) + + assertThat(firstValue()).isEqualTo( + UNKNOWN_MOBILE_SEGMENT_SOURCE_WARNING_MESSAGE_FORMAT.format( + Locale.US, + fakeInvalidSource + ) + ) + } + } + + // endregion +}