diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt index 4a790cbe4c..e2c0688953 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandler.kt @@ -58,6 +58,7 @@ internal class TelemetryEventHandler( event.additionalProperties ) } + TelemetryType.ERROR -> { createErrorEvent( datadogContext, @@ -67,6 +68,7 @@ internal class TelemetryEventHandler( event.kind ) } + TelemetryType.CONFIGURATION -> { val coreConfiguration = event.coreConfiguration if (coreConfiguration == null) { @@ -85,6 +87,7 @@ internal class TelemetryEventHandler( ) } } + TelemetryType.INTERCEPTOR_SETUP -> { trackNetworkRequests = true null @@ -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() ?.configuration @@ -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 + ) ) ) @@ -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" } } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/assertj/TelemetryConfigurationEventAssert.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/assertj/TelemetryConfigurationEventAssert.kt index 6e15d76fdf..8aaabe851a 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/assertj/TelemetryConfigurationEventAssert.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/assertj/TelemetryConfigurationEventAssert.kt @@ -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) diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt index 3865a0b526..ec2338887a 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/telemetry/internal/TelemetryEventHandlerTest.kt @@ -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 { + 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( + 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 { + 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( + 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 { + verify(mockWriter).write(eq(mockEventBatchWriter), capture()) + assertConfigEventMatchesRawEvent(firstValue, configRawEvent) + assertThat(firstValue).hasSessionReplaySampleRate(null) + assertThat(firstValue).hasSessionReplayStartManually(null) + assertThat(firstValue).hasSessionReplayPrivacy(null) + } + } + // endregion // region Sampling @@ -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 diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeature.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeature.kt index be90c56f74..ef967e9d3d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeature.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeature.kt @@ -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, @@ -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 = @@ -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" } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt index 8a2483c185..4af6ce58a8 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt @@ -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 @@ -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), @@ -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 } } @@ -119,6 +126,38 @@ 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) -> Unit> { + val updatedContext = mutableMapOf() + 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 @@ -126,7 +165,7 @@ internal class SessionReplayFeatureTest { sdkCore = mockSdkCore, customEndpointUrl = fakeConfiguration.customEndpointUrl, privacy = fakeConfiguration.privacy, - mockSampler + rateBasedSampler = mockSampler ) { _, _ -> mockRecorder } // When