Skip to content

Commit

Permalink
Use SDK source value in Session Replay MobileSegment.source property
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnm committed Sep 27, 2024
1 parent e8272f9 commit d041c0a
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,13 +26,16 @@ import com.google.gson.JsonParser
*/
internal class BatchesToSegmentsMapper(private val internalLogger: InternalLogger) {

fun map(batchData: List<ByteArray>): List<Pair<MobileSegment, JsonObject>> {
return groupBatchDataIntoSegments(batchData)
fun map(datadogContext: DatadogContext, batchData: List<ByteArray>): List<Pair<MobileSegment, JsonObject>> {
return groupBatchDataIntoSegments(datadogContext, batchData)
}

// region Internal

private fun groupBatchDataIntoSegments(batchData: List<ByteArray>): List<Pair<MobileSegment, JsonObject>> {
private fun groupBatchDataIntoSegments(
datadogContext: DatadogContext,
batchData: List<ByteArray>
): List<Pair<MobileSegment, JsonObject>> {
return batchData
.asSequence()
.mapNotNull {
Expand Down Expand Up @@ -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<MobileSegment, JsonObject>? {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal class SegmentRequestFactory(
batchData: List<RawBatchEvent>,
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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."
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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 ->
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -249,7 +262,7 @@ internal class BatchesToSegmentsMapperTest {
.map { it.toString().toByteArray() }

// When
assertThat(testedMapper.map(fakeBatchData)).isEmpty()
assertThat(testedMapper.map(datadogContext, fakeBatchData)).isEmpty()
}

@Test
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<NoSuchElementException>(),
onlyOnce = eq(false),
additionalProperties = isNull()
)

assertThat(firstValue()).isEqualTo(
UNKNOWN_MOBILE_SEGMENT_SOURCE_WARNING_MESSAGE_FORMAT.format(
Locale.US,
fakeInvalidSource
)
)
}
}

// endregion
}

0 comments on commit d041c0a

Please sign in to comment.