Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUMM-2273 Create the Session Replay Writer component #1041

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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

import android.app.Activity
import android.app.Application
import android.os.Bundle
import com.datadog.android.sessionreplay.LifecycleCallback

internal class NoOpLifecycleCallback : LifecycleCallback {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you use the automatic NoOpFactory?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because the LifecycleCallback lives in the dd-dk-android-session-replay module and NoOpLifecycleCallback is only relevant in the dd-sdk-android module where is actually being used. If I used the NoOpFactory it will be generated in the dd-sdk-android-session-replay and I want to avoid that.

override fun register(appContext: Application) {}

override fun unregisterAndStopRecorders(appContext: Application) {}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}

override fun onActivityPaused(activity: Activity) {}

override fun onActivityResumed(activity: Activity) {}

override fun onActivityDestroyed(activity: Activity) {}

override fun onActivityStarted(activity: Activity) {}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}

override fun onActivityStopped(activity: Activity) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,28 @@ import com.datadog.android.core.internal.net.DataUploader
import com.datadog.android.core.internal.net.NoOpDataUploader
import com.datadog.android.core.internal.persistence.PersistenceStrategy
import com.datadog.android.core.internal.utils.sdkLogger
import com.datadog.android.sessionreplay.LifecycleCallback
import com.datadog.android.sessionreplay.SessionReplayLifecycleCallback
import com.datadog.android.sessionreplay.internal.domain.SessionReplayRecordPersistenceStrategy
import com.datadog.android.sessionreplay.internal.domain.SessionReplaySerializedRecordWriter
import java.util.concurrent.atomic.AtomicBoolean

internal class SessionReplayFeature(
coreFeature: CoreFeature,
private val sessionReplayCallback: SessionReplayLifecycleCallback
) : SdkFeature<Any, Configuration.Feature.SessionReplay>(coreFeature) {
private val configuration: Configuration.Feature.SessionReplay,
private val sessionReplayCallbackProvider: (PersistenceStrategy<String>) ->
LifecycleCallback = {
SessionReplayLifecycleCallback(
SessionReplayContextProvider(),
configuration.privacy,
SessionReplaySerializedRecordWriter(it.getWriter())
)
}
) : SdkFeature<String, Configuration.Feature.SessionReplay>(coreFeature) {

internal lateinit var appContext: Context
private var isRecording = AtomicBoolean(false)
internal var sessionReplayCallback: LifecycleCallback = NoOpLifecycleCallback()

// region SDKFeature

Expand All @@ -35,18 +46,20 @@ internal class SessionReplayFeature(
) {
super.onInitialize(context, configuration)
appContext = context
sessionReplayCallback = sessionReplayCallbackProvider(persistenceStrategy)
startRecording()
}

override fun onStop() {
super.onStop()
stopRecording()
sessionReplayCallback = NoOpLifecycleCallback()
}

override fun createPersistenceStrategy(
context: Context,
configuration: Configuration.Feature.SessionReplay
): PersistenceStrategy<Any> {
): PersistenceStrategy<String> {
return SessionReplayRecordPersistenceStrategy(
coreFeature.trackingConsentProvider,
coreFeature.storageDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal class SessionReplayRecordPersistenceStrategy(
executorService: ExecutorService,
internalLogger: Logger,
localDataEncryption: Encryption?
) : BatchFilePersistenceStrategy<Any>(
) : BatchFilePersistenceStrategy<String>(
FeatureFileOrchestrator(
consentProvider,
storageDir,
Expand All @@ -33,7 +33,7 @@ internal class SessionReplayRecordPersistenceStrategy(
internalLogger
),
executorService,
RecordSerializer(),
SessionReplayRecordSerializer(),
PayloadDecoration.NEW_LINE_DECORATION,
internalLogger,
BatchFileReaderWriter.create(internalLogger, localDataEncryption),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ package com.datadog.android.sessionreplay.internal.domain

import com.datadog.android.core.internal.persistence.Serializer

internal class RecordSerializer : Serializer<Any> {
override fun serialize(model: Any): String? {
// TODO: This will be switched to a Serializer<Record> once the models
// will be in place. RUMM-2330"
return null
internal class SessionReplayRecordSerializer : Serializer<String> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by some reason github doesn't show any syntax highlight for this file. Is this file ok?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I will recreate this file, have the same issue in my editor.

override fun serialize(model: String): String {
return model
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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.domain

import com.datadog.android.core.internal.persistence.DataWriter
import com.datadog.android.sessionreplay.SerializedRecordWriter

internal class SessionReplaySerializedRecordWriter(private val dataWriter: DataWriter<String>) :
SerializedRecordWriter {
// This method is being called from the `SnapshotProcessor` in Session Replay module which
// runs on a WorkerThread already.
@Suppress("ThreadSafety")
override fun write(serializedRecord: String) {
dataWriter.write(serializedRecord)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import com.datadog.android.log.internal.LogsFeature
import com.datadog.android.log.model.LogEvent
import com.datadog.android.privacy.TrackingConsent
import com.datadog.android.rum.internal.RumFeature
import com.datadog.android.sessionreplay.SessionReplayLifecycleCallback
import com.datadog.android.sessionreplay.internal.SessionReplayContextProvider
import com.datadog.android.sessionreplay.internal.SessionReplayFeature
import com.datadog.android.tracing.internal.TracingFeature
import com.datadog.android.v2.api.FeatureScope
Expand Down Expand Up @@ -65,7 +63,8 @@ internal class DatadogCore(
null
internal var webViewLogsFeature: SdkFeature<JsonObject, Configuration.Feature.Logs>? = null
internal var webViewRumFeature: SdkFeature<Any, Configuration.Feature.RUM>? = null
internal var sessionReplayFeature: SdkFeature<Any, Configuration.Feature.SessionReplay>? = null
internal var sessionReplayFeature: SdkFeature<String, Configuration.Feature.SessionReplay>? =
null

// TODO RUMM-0000 handle context
internal var contextProvider: ContextProvider? = null
Expand Down Expand Up @@ -303,10 +302,7 @@ internal class DatadogCore(
if (configuration != null) {
sessionReplayFeature = SessionReplayFeature(
coreFeature,
SessionReplayLifecycleCallback(
SessionReplayContextProvider(),
configuration.privacy
)
configuration
)
sessionReplayFeature?.initialize(appContext, configuration)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@ import org.mockito.quality.Strictness
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(Configurator::class)
internal class SessionReplayFeatureTest : SdkFeatureTest<Any, Configuration.Feature.SessionReplay,
SessionReplayFeature>() {
internal class SessionReplayFeatureTest :
SdkFeatureTest<String, Configuration.Feature.SessionReplay, SessionReplayFeature>() {

@Mock
lateinit var mockSessionReplayLifecycleCallback: SessionReplayLifecycleCallback

override fun createTestedFeature(): SessionReplayFeature {
return SessionReplayFeature(coreFeature.mockInstance, mockSessionReplayLifecycleCallback)
return SessionReplayFeature(
coreFeature.mockInstance,
fakeConfigurationFeature
) { mockSessionReplayLifecycleCallback }
}

override fun forgeConfiguration(forge: Forge): Configuration.Feature.SessionReplay {
Expand All @@ -66,6 +69,22 @@ internal class SessionReplayFeatureTest : SdkFeatureTest<Any, Configuration.Feat
.isInstanceOf(SessionReplayRecordPersistenceStrategy::class.java)
}

@Test
fun `𝕄 initialize session replay callback 𝕎 initialize()`() {
// Given
testedFeature = SessionReplayFeature(
coreFeature.mockInstance,
fakeConfigurationFeature
)

// When
testedFeature.initialize(appContext.mockInstance, fakeConfigurationFeature)

// Then
assertThat(testedFeature.sessionReplayCallback)
.isInstanceOf(SessionReplayLifecycleCallback::class.java)
}

@Test
fun `M register the Session Replay lifecycle callback W initialize()`() {
// When
Expand All @@ -89,6 +108,19 @@ internal class SessionReplayFeatureTest : SdkFeatureTest<Any, Configuration.Feat
.unregisterAndStopRecorders(appContext.mockInstance)
}

@Test
fun `M reset the Session Replay lifecycle callback W stop()`() {
// Given
testedFeature.initialize(appContext.mockInstance, fakeConfigurationFeature)

// When
testedFeature.stop()

// Then
assertThat(testedFeature.sessionReplayCallback)
.isInstanceOf(NoOpLifecycleCallback::class.java)
}

@Test
fun `M unregister the SessionReplayCallback W stopRecording() { was recording }`() {
// Given
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.domain

import com.datadog.android.utils.forge.Configurator
import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
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)
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(Configurator::class)
internal class SessionReplayRecordSerializerTest {

lateinit var testedRecordSerializer: SessionReplayRecordSerializer

@BeforeEach
fun `set up`() {
testedRecordSerializer = SessionReplayRecordSerializer()
}

@Test
fun `M do nothing and return the serializedRecord W serialize`(
@StringForgery fakeSerializedRecord: String
) {
// When
val serializedRecord = testedRecordSerializer.serialize(fakeSerializedRecord)

// Then
assertThat(serializedRecord).isSameAs(fakeSerializedRecord)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.domain

import com.datadog.android.core.internal.persistence.DataWriter
import com.datadog.android.utils.forge.Configurator
import com.nhaarman.mockitokotlin2.verify
import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.junit.jupiter.api.BeforeEach
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.junit.jupiter.MockitoSettings
import org.mockito.quality.Strictness

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(Configurator::class)
internal class SessionReplaySerializedRecordWriterTest {

lateinit var testedSessionReplayRecordWriter: SessionReplaySerializedRecordWriter

@Mock
lateinit var mockDataWriter: DataWriter<String>

@BeforeEach
fun `set up`() {
testedSessionReplayRecordWriter = SessionReplaySerializedRecordWriter(mockDataWriter)
}

@Test
fun `M delegate to the dataWriter W write`(@StringForgery fakeSerializedRecord: String) {
// When
testedSessionReplayRecordWriter.write(fakeSerializedRecord)

// Then
verify(mockDataWriter).write(fakeSerializedRecord)
}
}
13 changes: 9 additions & 4 deletions library/dd-sdk-android-session-replay/apiSurface
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
class com.datadog.android.sessionreplay.SessionReplayLifecycleCallback : android.app.Application.ActivityLifecycleCallbacks
constructor(com.datadog.android.sessionreplay.utils.RumContextProvider, SessionReplayPrivacy)
interface com.datadog.android.sessionreplay.LifecycleCallback : android.app.Application.ActivityLifecycleCallbacks
fun register(android.app.Application)
fun unregisterAndStopRecorders(android.app.Application)
interface com.datadog.android.sessionreplay.SerializedRecordWriter
fun write(String)
class com.datadog.android.sessionreplay.SessionReplayLifecycleCallback : LifecycleCallback
constructor(com.datadog.android.sessionreplay.utils.RumContextProvider, SessionReplayPrivacy, SerializedRecordWriter)
override fun onActivityCreated(android.app.Activity, android.os.Bundle?)
override fun onActivityStarted(android.app.Activity)
override fun onActivityResumed(android.app.Activity)
override fun onActivityPaused(android.app.Activity)
override fun onActivityStopped(android.app.Activity)
override fun onActivitySaveInstanceState(android.app.Activity, android.os.Bundle)
override fun onActivityDestroyed(android.app.Activity)
fun register(android.app.Application)
fun unregisterAndStopRecorders(android.app.Application)
override fun register(android.app.Application)
override fun unregisterAndStopRecorders(android.app.Application)
companion object
enum com.datadog.android.sessionreplay.SessionReplayPrivacy
- ALLOW_ALL
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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

import android.app.Application

/**
* The Session Replay related LifecycleCallback interface.
* It will be registered as `Application.ActivityLifecycleCallbacks` and will decide when the
* activity can be recorded or not based on the `onActivityResume`, `onActivityPause` callbacks.
* This is only meant for internal usage and later will change visibility from public to internal.
*/
interface LifecycleCallback : Application.ActivityLifecycleCallbacks {

/**
* Registers the callback on the Application lifecycle.
* @param appContext
*/
fun register(appContext: Application)

/**
* Unregister the callback and stops any related recorders that were previously started.
* @param appContext
*/
fun unregisterAndStopRecorders(appContext: Application)
}
Loading