Skip to content

Commit

Permalink
RUM-886 Provide session replay data in configuration telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusc83 committed Sep 12, 2023
1 parent d8b3962 commit c0a7f5a
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ internal class TelemetryEventHandler(
event.additionalProperties
)
}

TelemetryType.ERROR -> {
createErrorEvent(
datadogContext,
Expand All @@ -67,6 +68,7 @@ internal class TelemetryEventHandler(
event.kind
)
}

TelemetryType.CONFIGURATION -> {
val coreConfiguration = event.coreConfiguration
if (coreConfiguration == null) {
Expand All @@ -85,6 +87,7 @@ internal class TelemetryEventHandler(
)
}
}

TelemetryType.INTERCEPTOR_SETUP -> {
trackNetworkRequests = true
null
Expand Down Expand Up @@ -198,12 +201,21 @@ internal class TelemetryEventHandler(
)
}

@Suppress("LongMethod")
private fun createConfigurationEvent(
datadogContext: DatadogContext,
timestamp: Long,
coreConfiguration: TelemetryCoreConfiguration
): TelemetryConfigurationEvent {
val traceFeature = sdkCore.getFeature(Feature.TRACING_FEATURE_NAME)
val sessionReplayFeatureContext =
sdkCore.getFeatureContext(Feature.SESSION_REPLAY_FEATURE_NAME)
val sessionReplaySampleRate = sessionReplayFeatureContext[SESSION_REPLAY_SAMPLE_RATE_KEY]
as? Long
val startSessionReplayManually =
sessionReplayFeatureContext[SESSION_REPLAY_MANUAL_RECORDING_KEY] as? Boolean
val sessionReplayPrivacy = sessionReplayFeatureContext[SESSION_REPLAY_PRIVACY_KEY]
as? String
val rumConfig = sdkCore.getFeature(Feature.RUM_FEATURE_NAME)
?.unwrap<RumFeature>()
?.configuration
Expand Down Expand Up @@ -247,7 +259,11 @@ internal class TelemetryEventHandler(
batchUploadFrequency = coreConfiguration.batchUploadFrequency,
mobileVitalsUpdatePeriod = rumConfig?.vitalsMonitorUpdateFrequency?.periodInMs,
useTracing = traceFeature != null && isGlobalTracerRegistered(),
trackNetworkRequests = trackNetworkRequests
trackNetworkRequests = trackNetworkRequests,
sessionReplaySampleRate = sessionReplaySampleRate,
defaultPrivacyLevel = sessionReplayPrivacy,
startSessionReplayRecordingManually = startSessionReplayManually

)
)
)
Expand Down Expand Up @@ -298,5 +314,9 @@ internal class TelemetryEventHandler(
const val MAX_EVENT_NUMBER_REACHED_MESSAGE =
"Max number of telemetry events per session reached, rejecting."
const val TELEMETRY_SERVICE_NAME = "dd-sdk-android"
internal const val SESSION_REPLAY_SAMPLE_RATE_KEY = "session_replay_sample_rate"
internal const val SESSION_REPLAY_PRIVACY_KEY = "session_replay_privacy"
internal const val SESSION_REPLAY_MANUAL_RECORDING_KEY =
"session_replay_requires_manual_recording"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,43 @@ internal class TelemetryConfigurationEventAssert(actual: TelemetryConfigurationE

// endregion

// region Session Replay configuration

fun hasSessionReplaySampleRate(expected: Long?): TelemetryConfigurationEventAssert {
assertThat(actual.telemetry.configuration.sessionReplaySampleRate)
.overridingErrorMessage(
"Expected event data to have telemetry.configuration.sessionReplaySampleRate" +
" $expected " +
"but was ${actual.telemetry.configuration.sessionReplaySampleRate}"
)
.isEqualTo(expected)
return this
}

fun hasSessionReplayPrivacy(expected: String?): TelemetryConfigurationEventAssert {
assertThat(actual.telemetry.configuration.defaultPrivacyLevel)
.overridingErrorMessage(
"Expected event data to have telemetry.configuration.defaultPrivacyLevel" +
" $expected " +
"but was ${actual.telemetry.configuration.defaultPrivacyLevel}"
)
.isEqualTo(expected)
return this
}

fun hasSessionReplayStartManually(expected: Boolean?): TelemetryConfigurationEventAssert {
val assertErrorMessage = "Expected event data to have" +
" telemetry.configuration.startSessionReplayRecordingManually" +
" $expected " +
"but was ${actual.telemetry.configuration.startSessionReplayRecordingManually}"
assertThat(actual.telemetry.configuration.startSessionReplayRecordingManually)
.overridingErrorMessage(assertErrorMessage)
.isEqualTo(expected)
return this
}

// endregion

companion object {
fun assertThat(actual: TelemetryConfigurationEvent) =
TelemetryConfigurationEventAssert(actual)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,88 @@ internal class TelemetryEventHandlerTest {
}
}

@Test
fun `𝕄 create config event 𝕎 handleEvent(SendTelemetry) { configuration, no SessionReplay }`(
forge: Forge
) {
// Given
val configRawEvent = forge.createRumRawTelemetryConfigurationEvent()

// When
testedTelemetryHandler.handleEvent(configRawEvent, mockWriter)

// Then
argumentCaptor<TelemetryConfigurationEvent> {
verify(mockWriter).write(eq(mockEventBatchWriter), capture())
assertConfigEventMatchesRawEvent(firstValue, configRawEvent)
assertThat(firstValue).hasSessionReplaySampleRate(null)
assertThat(firstValue).hasSessionReplayStartManually(null)
assertThat(firstValue).hasSessionReplayPrivacy(null)
}
}

@Test
fun `𝕄 create config event 𝕎 handleEvent(SendTelemetry) { configuration, with SessionReplay }`(
forge: Forge
) {
// Given
val fakeSampleRate = forge.aPositiveLong()
val fakeSessionReplayPrivacy = forge.aString()
val fakeSessionReplayIsStartManually = forge.aBool()
val fakeSessionReplayContext = mutableMapOf<String, Any?>(
TelemetryEventHandler.SESSION_REPLAY_PRIVACY_KEY to fakeSessionReplayPrivacy,
TelemetryEventHandler.SESSION_REPLAY_MANUAL_RECORDING_KEY to
fakeSessionReplayIsStartManually,
TelemetryEventHandler.SESSION_REPLAY_SAMPLE_RATE_KEY to fakeSampleRate
)
whenever(mockSdkCore.getFeatureContext(Feature.SESSION_REPLAY_FEATURE_NAME)) doReturn
fakeSessionReplayContext
val configRawEvent = forge.createRumRawTelemetryConfigurationEvent()

// When
testedTelemetryHandler.handleEvent(configRawEvent, mockWriter)

// Then
argumentCaptor<TelemetryConfigurationEvent> {
verify(mockWriter).write(eq(mockEventBatchWriter), capture())
assertConfigEventMatchesRawEvent(firstValue, configRawEvent)
assertThat(firstValue).hasSessionReplaySampleRate(fakeSampleRate)
assertThat(firstValue).hasSessionReplayStartManually(fakeSessionReplayIsStartManually)
assertThat(firstValue).hasSessionReplayPrivacy(fakeSessionReplayPrivacy)
}
}

@Test
fun `𝕄 create config event 𝕎 handleEvent(SendTelemetry) { with SessionReplay, bad format }`(
forge: Forge
) {
// Given
val fakeSampleRate = forge.aNullable { aString() }
val fakeSessionReplayPrivacy = forge.aNullable { aLong() }
val fakeSessionReplayIsStartManually = forge.aNullable { aString() }
val fakeSessionReplayContext = mutableMapOf<String, Any?>(
TelemetryEventHandler.SESSION_REPLAY_PRIVACY_KEY to fakeSessionReplayPrivacy,
TelemetryEventHandler.SESSION_REPLAY_MANUAL_RECORDING_KEY to
fakeSessionReplayIsStartManually,
TelemetryEventHandler.SESSION_REPLAY_SAMPLE_RATE_KEY to fakeSampleRate
)
whenever(mockSdkCore.getFeatureContext(Feature.SESSION_REPLAY_FEATURE_NAME)) doReturn
fakeSessionReplayContext
val configRawEvent = forge.createRumRawTelemetryConfigurationEvent()

// When
testedTelemetryHandler.handleEvent(configRawEvent, mockWriter)

// Then
argumentCaptor<TelemetryConfigurationEvent> {
verify(mockWriter).write(eq(mockEventBatchWriter), capture())
assertConfigEventMatchesRawEvent(firstValue, configRawEvent)
assertThat(firstValue).hasSessionReplaySampleRate(null)
assertThat(firstValue).hasSessionReplayStartManually(null)
assertThat(firstValue).hasSessionReplayPrivacy(null)
}
}

// endregion

// region Sampling
Expand Down Expand Up @@ -797,6 +879,17 @@ internal class TelemetryEventHandlerTest {
.hasActionId(rumContext.actionId)
}

private fun assertConfigEventMatchesRawEvent(
actual: TelemetryConfigurationEvent,
rawEvent: RumRawEvent.SendTelemetry
) {
assertThat(actual)
.hasDate(rawEvent.eventTime.timestamp + fakeServerOffset)
.hasSource(TelemetryConfigurationEvent.Source.ANDROID)
.hasService(TelemetryEventHandler.TELEMETRY_SERVICE_NAME)
.hasVersion(fakeDatadogContext.sdkVersion)
}

// endregion

// region Forgeries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import java.util.concurrent.atomic.AtomicReference
/**
* Session Replay feature class, which needs to be registered with Datadog SDK instance.
*/
internal class SessionReplayFeature constructor(
internal class SessionReplayFeature(
private val sdkCore: FeatureSdkCore,
customEndpointUrl: String?,
internal val privacy: SessionReplayPrivacy,
Expand Down Expand Up @@ -101,6 +101,14 @@ internal class SessionReplayFeature constructor(
@Suppress("ThreadSafety") // TODO REPLAY-1861 can be called from any thread
sessionReplayRecorder.registerCallbacks()
initialized.set(true)
sdkCore.updateFeatureContext(SESSION_REPLAY_FEATURE_NAME) {
it[SESSION_REPLAY_SAMPLE_RATE_KEY] = rateBasedSampler.getSampleRate()?.toLong()
it[SESSION_REPLAY_PRIVACY_KEY] = privacy.toString().lowercase(Locale.US)
// False by default. This will be changed once we will conform to the browser SR
// implementation where a parameter will be passed in the Configuration constructor
// to enable manual recording.
it[SESSION_REPLAY_MANUAL_RECORDING_KEY] = false
}
}

override val requestFactory: RequestFactory =
Expand Down Expand Up @@ -263,5 +271,9 @@ internal class SessionReplayFeature constructor(
const val RUM_SESSION_RENEWED_BUS_MESSAGE = "rum_session_renewed"
const val RUM_KEEP_SESSION_BUS_MESSAGE_KEY = "keepSession"
const val RUM_SESSION_ID_BUS_MESSAGE_KEY = "sessionId"
internal const val SESSION_REPLAY_SAMPLE_RATE_KEY = "session_replay_sample_rate"
internal const val SESSION_REPLAY_PRIVACY_KEY = "session_replay_privacy"
internal const val SESSION_REPLAY_MANUAL_RECORDING_KEY =
"session_replay_requires_manual_recording"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ import org.junit.jupiter.api.extension.Extensions
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
Expand All @@ -49,6 +51,7 @@ import java.util.Locale
import java.util.UUID
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class),
Expand Down Expand Up @@ -77,15 +80,19 @@ internal class SessionReplayFeatureTest {

lateinit var fakeSessionId: String

var fakeSampleRate: Float? = null

@BeforeEach
fun `set up`() {
fun `set up`(forge: Forge) {
fakeSampleRate = forge.aNullable { aFloat() }
whenever(mockSampler.getSampleRate()).thenReturn(fakeSampleRate)
fakeSessionId = UUID.randomUUID().toString()
whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger
testedFeature = SessionReplayFeature(
sdkCore = mockSdkCore,
customEndpointUrl = fakeConfiguration.customEndpointUrl,
privacy = fakeConfiguration.privacy,
mockSampler
rateBasedSampler = mockSampler
) { _, _ -> mockRecorder }
}

Expand Down Expand Up @@ -119,14 +126,46 @@ internal class SessionReplayFeatureTest {
.isInstanceOf(SessionReplayRecorder::class.java)
}

@Test
fun `𝕄 update feature context for telemetry 𝕎 initialize()`() {
// Given
testedFeature = SessionReplayFeature(
sdkCore = mockSdkCore,
customEndpointUrl = fakeConfiguration.customEndpointUrl,
privacy = fakeConfiguration.privacy,
customMappers = emptyList(),
customOptionSelectorDetectors = emptyList(),
sampleRate = fakeConfiguration.sampleRate
)

// When
testedFeature.onInitialize(appContext.mockInstance)

// Then
argumentCaptor<(context: MutableMap<String, Any?>) -> Unit> {
val updatedContext = mutableMapOf<String, Any?>()
verify(mockSdkCore).updateFeatureContext(
eq(SessionReplayFeature.SESSION_REPLAY_FEATURE_NAME),
capture()
)
firstValue.invoke(updatedContext)
assertThat(updatedContext[SessionReplayFeature.SESSION_REPLAY_SAMPLE_RATE_KEY])
.isEqualTo(fakeConfiguration.sampleRate.toLong())
assertThat(updatedContext[SessionReplayFeature.SESSION_REPLAY_PRIVACY_KEY])
.isEqualTo(fakeConfiguration.privacy.toString().lowercase(Locale.US))
assertThat(updatedContext[SessionReplayFeature.SESSION_REPLAY_MANUAL_RECORDING_KEY])
.isEqualTo(false)
}
}

@Test
fun `𝕄 set the feature event receiver 𝕎 initialize()`() {
// Given
testedFeature = SessionReplayFeature(
sdkCore = mockSdkCore,
customEndpointUrl = fakeConfiguration.customEndpointUrl,
privacy = fakeConfiguration.privacy,
mockSampler
rateBasedSampler = mockSampler
) { _, _ -> mockRecorder }

// When
Expand Down

0 comments on commit c0a7f5a

Please sign in to comment.