Skip to content

Commit

Permalink
RUM-2707: Add Resources feature
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Jan 29, 2024
1 parent ef5a186 commit 03d02e0
Show file tree
Hide file tree
Showing 21 changed files with 628 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.annotation.VisibleForTesting
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.LifecycleCallback
import com.datadog.android.sessionreplay.internal.RecordWriter
import com.datadog.android.sessionreplay.internal.ResourcesFeature
import com.datadog.android.sessionreplay.internal.SessionReplayLifecycleCallback
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler
import com.datadog.android.sessionreplay.internal.processor.MutationResolver
Expand Down Expand Up @@ -54,6 +55,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {

constructor(
appContext: Application,
resourcesFeature: ResourcesFeature,
rumContextProvider: RumContextProvider,
privacy: SessionReplayPrivacy,
recordWriter: RecordWriter,
Expand All @@ -70,6 +72,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
)

val processor = RecordedDataProcessor(
resourcesFeature,
recordWriter,
MutationResolver(internalLogger)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 com.datadog.android.sessionreplay.internal.processor.EnrichedResource

internal interface ResourceWriter {
/**
* Writes the resource to disk.
* @param enrichedResource to write
*/
fun write(enrichedResource: EnrichedResource)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.content.Context
import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.api.feature.StorageBackedFeature
import com.datadog.android.api.net.RequestFactory
import com.datadog.android.api.storage.FeatureStorageConfiguration
import com.datadog.android.sessionreplay.internal.domain.ResourceRequestFactory
import com.datadog.android.sessionreplay.internal.storage.NoOpResourceWriter
import com.datadog.android.sessionreplay.internal.storage.SessionReplayResourceWriter
import java.util.concurrent.atomic.AtomicBoolean

/**
* Session Replay feature class, which needs to be registered with Datadog SDK instance.
*/
internal class ResourcesFeature(
private val sdkCore: FeatureSdkCore
) : StorageBackedFeature {

internal var dataWriter: ResourceWriter = NoOpResourceWriter()
internal val initialized = AtomicBoolean(false)

// region Feature

override val name: String = SESSION_REPLAY_RESOURCES_FEATURE_NAME

override val requestFactory: RequestFactory = ResourceRequestFactory(
customEndpointUrl = null
)

override fun onInitialize(appContext: Context) {
dataWriter = createResourceWriter()
initialized.set(true)
}

override val storageConfiguration: FeatureStorageConfiguration =
STORAGE_CONFIGURATION

override fun onStop() {
dataWriter = NoOpResourceWriter()
initialized.set(false)
}

private fun createResourceWriter(): ResourceWriter {
return SessionReplayResourceWriter(sdkCore)
}

// endregion

internal companion object {

/**
* Session Replay storage configuration with the following parameters:
* max item size = 10 MB,
* max items per batch = 500,
* max batch size = 10 MB, SR intake batch limit is 10MB
* old batch threshold = 18 hours.
*/
internal val STORAGE_CONFIGURATION: FeatureStorageConfiguration =
FeatureStorageConfiguration.DEFAULT.copy(
maxItemSize = 10 * 1024 * 1024,
maxBatchSize = 10 * 1024 * 1024
)

internal const val SESSION_REPLAY_RESOURCES_FEATURE_NAME = "session-replay-resources"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package com.datadog.android.sessionreplay.internal
import android.app.Application
import android.content.Context
import com.datadog.android.api.InternalLogger
import com.datadog.android.api.SdkCore
import com.datadog.android.api.feature.Feature
import com.datadog.android.api.feature.FeatureEventReceiver
import com.datadog.android.api.feature.FeatureSdkCore
Expand Down Expand Up @@ -40,7 +41,7 @@ internal class SessionReplayFeature(
customEndpointUrl: String?,
internal val privacy: SessionReplayPrivacy,
private val rateBasedSampler: Sampler,
private val sessionReplayRecorderProvider: (RecordWriter, Application) -> Recorder
private val sessionReplayRecorderProvider: (ResourcesFeature, RecordWriter, Application) -> Recorder
) : StorageBackedFeature, FeatureEventReceiver {

private val currentRumSessionId = AtomicReference<String>()
Expand All @@ -57,9 +58,10 @@ internal class SessionReplayFeature(
customEndpointUrl,
privacy,
RateBasedSampler(sampleRate),
{ recordWriter, application ->
{ resourcesFeature, recordWriter, application ->
SessionReplayRecorder(
application,
resourcesFeature = resourcesFeature,
rumContextProvider = SessionReplayRumContextProvider(sdkCore),
privacy = privacy,
recordWriter = recordWriter,
Expand All @@ -72,6 +74,7 @@ internal class SessionReplayFeature(
)

internal lateinit var appContext: Context
internal lateinit var resourcesFeature: ResourcesFeature
private var isRecording = AtomicBoolean(false)
internal var sessionReplayRecorder: Recorder = NoOpRecorder()
internal var dataWriter: RecordWriter = NoOpRecordWriter()
Expand All @@ -92,10 +95,10 @@ internal class SessionReplayFeature(
}

this.appContext = appContext

this.resourcesFeature = registerResourceFeature(sdkCore)
sdkCore.setEventReceiver(SESSION_REPLAY_FEATURE_NAME, this)
dataWriter = createDataWriter()
sessionReplayRecorder = sessionReplayRecorderProvider(dataWriter, appContext)
sessionReplayRecorder = sessionReplayRecorderProvider(resourcesFeature, dataWriter, appContext)
@Suppress("ThreadSafety") // TODO REPLAY-1861 can be called from any thread
sessionReplayRecorder.registerCallbacks()
initialized.set(true)
Expand Down Expand Up @@ -244,6 +247,19 @@ internal class SessionReplayFeature(

// endregion

// region resourcesFeature

private fun registerResourceFeature(sdkCore: SdkCore): ResourcesFeature {
val resourcesFeature = ResourcesFeature(
sdkCore = sdkCore as FeatureSdkCore
)
sdkCore.registerFeature(resourcesFeature)

return resourcesFeature
}

// endregion

internal companion object {

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.async

import com.datadog.android.sessionreplay.internal.processor.RecordedQueuedItemContext

internal class ResourceRecordedDataQueueItem(
recordedQueuedItemContext: RecordedQueuedItemContext,
val identifier: String,
val applicationId: String,
val resourceData: ByteArray
) : RecordedDataQueueItem(recordedQueuedItemContext) {

override fun isValid(): Boolean {
return resourceData.isNotEmpty()
}

override fun isReady(): Boolean {
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package com.datadog.android.sessionreplay.internal.domain

import com.datadog.android.api.storage.RawBatchEvent
import com.datadog.android.sessionreplay.internal.exception.InvalidPayloadFormatException
import com.datadog.android.sessionreplay.internal.processor.EnrichedResource.Companion.APPLICATION_ID_KEY
import com.datadog.android.sessionreplay.internal.processor.EnrichedResource.Companion.FILENAME_KEY
import com.datadog.android.sessionreplay.internal.utils.MiscUtils
import com.google.gson.JsonObject
import okhttp3.MediaType.Companion.toMediaTypeOrNull
Expand Down Expand Up @@ -122,8 +124,6 @@ internal class ResourceRequestBodyFactory {
@Suppress("UnsafeThirdPartyFunctionCall") // if malformed returns null
internal val CONTENT_TYPE_APPLICATION = "application/json".toMediaTypeOrNull()

internal const val APPLICATION_ID_KEY = "application_id"
internal const val FILENAME_KEY = "filename"
internal const val TYPE_KEY = "type"
internal const val TYPE_RESOURCE = "resource"
internal const val NAME_IMAGE = "image"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.processor

import com.google.gson.JsonObject

internal data class EnrichedResource(
internal val resource: ByteArray,
internal val applicationId: String,
internal val filename: String,
internal val metadata: JsonObject = JsonObject().apply {
this.addProperty(APPLICATION_ID_KEY, applicationId)
this.addProperty(FILENAME_KEY, filename)
}
) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as EnrichedResource

if (!resource.contentEquals(other.resource)) return false
if (applicationId != other.applicationId) return false
return filename == other.filename
}

override fun hashCode(): Int {
var result = resource.contentHashCode()
result = 31 * result + applicationId.hashCode()
result = 31 * result + filename.hashCode()
return result
}

internal companion object {
internal const val APPLICATION_ID_KEY = "application_id"
internal const val FILENAME_KEY = "filename"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@

package com.datadog.android.sessionreplay.internal.processor

import com.datadog.android.sessionreplay.internal.async.ResourceRecordedDataQueueItem
import com.datadog.android.sessionreplay.internal.async.SnapshotRecordedDataQueueItem
import com.datadog.android.sessionreplay.internal.async.TouchEventRecordedDataQueueItem

internal interface Processor {

fun processResources(item: ResourceRecordedDataQueueItem)

fun processScreenSnapshots(item: SnapshotRecordedDataQueueItem)

fun processTouchEventsRecords(item: TouchEventRecordedDataQueueItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ package com.datadog.android.sessionreplay.internal.processor
import android.content.res.Configuration
import androidx.annotation.WorkerThread
import com.datadog.android.sessionreplay.internal.RecordWriter
import com.datadog.android.sessionreplay.internal.ResourcesFeature
import com.datadog.android.sessionreplay.internal.async.ResourceRecordedDataQueueItem
import com.datadog.android.sessionreplay.internal.async.SnapshotRecordedDataQueueItem
import com.datadog.android.sessionreplay.internal.async.TouchEventRecordedDataQueueItem
import com.datadog.android.sessionreplay.internal.recorder.Node
Expand All @@ -20,6 +22,7 @@ import java.util.concurrent.TimeUnit

@Suppress("TooManyFunctions")
internal class RecordedDataProcessor(
private val resourcesFeature: ResourcesFeature,
private val writer: RecordWriter,
private val mutationResolver: MutationResolver,
private val nodeFlattener: NodeFlattener = NodeFlattener()
Expand All @@ -29,6 +32,19 @@ internal class RecordedDataProcessor(
private var previousOrientation = Configuration.ORIENTATION_UNDEFINED
private var prevRumContext: SessionReplayRumContext = SessionReplayRumContext()

@WorkerThread
override fun processResources(
item: ResourceRecordedDataQueueItem
) {
val enrichedResource = EnrichedResource(
resource = item.resourceData,
applicationId = item.applicationId,
filename = item.identifier
)

resourcesFeature.dataWriter.write(enrichedResource)
}

@WorkerThread
override fun processScreenSnapshots(
item: SnapshotRecordedDataQueueItem
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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.storage

import com.datadog.android.sessionreplay.internal.ResourceWriter
import com.datadog.android.sessionreplay.internal.processor.EnrichedResource

internal class NoOpResourceWriter : ResourceWriter {
override fun write(enrichedResource: EnrichedResource) {
// no-op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.storage

import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.api.storage.RawBatchEvent
import com.datadog.android.sessionreplay.internal.ResourceWriter
import com.datadog.android.sessionreplay.internal.ResourcesFeature.Companion.SESSION_REPLAY_RESOURCES_FEATURE_NAME
import com.datadog.android.sessionreplay.internal.processor.EnrichedResource

internal class SessionReplayResourceWriter(
private val sdkCore: FeatureSdkCore
) : ResourceWriter {
override fun write(enrichedResource: EnrichedResource) {
sdkCore.getFeature(SESSION_REPLAY_RESOURCES_FEATURE_NAME)?.withWriteContext() { _, eventBatchWriter ->
synchronized(this) {
val serializedMetadata = enrichedResource.metadata.toString().toByteArray(Charsets.UTF_8)
@Suppress("ThreadSafety") // called from the worker thread
eventBatchWriter.write(
event = RawBatchEvent(
data = enrichedResource.resource,
metadata = serializedMetadata
),
batchMetadata = null
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.forge

import com.datadog.android.sessionreplay.internal.processor.EnrichedResource
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.ForgeryFactory
import java.util.UUID

internal class EnrichedResourceForgeryFactory : ForgeryFactory<EnrichedResource> {
override fun getForgery(forge: Forge): EnrichedResource {
return EnrichedResource(
applicationId = forge.getForgery<UUID>().toString(),
filename = forge.getForgery<UUID>().toString(),
resource = forge.getForgery<UUID>().toString().toByteArray()
)
}
}

0 comments on commit 03d02e0

Please sign in to comment.