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

RUM-2707: Add Resources feature #1840

Merged
merged 3 commits into from
Feb 21, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import androidx.annotation.MainThread
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.SessionReplayLifecycleCallback
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler
import com.datadog.android.sessionreplay.internal.processor.MutationResolver
Expand All @@ -30,6 +29,8 @@ import com.datadog.android.sessionreplay.internal.recorder.WindowCallbackInterce
import com.datadog.android.sessionreplay.internal.recorder.WindowInspector
import com.datadog.android.sessionreplay.internal.recorder.callback.OnWindowRefreshedCallback
import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper
import com.datadog.android.sessionreplay.internal.storage.RecordWriter
import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter
import com.datadog.android.sessionreplay.internal.utils.RumContextProvider
import com.datadog.android.sessionreplay.internal.utils.TimeProvider

Expand All @@ -54,6 +55,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {

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

val processor = RecordedDataProcessor(
resourcesWriter,
recordWriter,
MutationResolver(internalLogger)
)
Expand Down
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.net.ResourceRequestFactory
import com.datadog.android.sessionreplay.internal.storage.NoOpResourcesWriter
import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter
import com.datadog.android.sessionreplay.internal.storage.SessionReplayResourcesWriter
import java.util.concurrent.atomic.AtomicBoolean

/**
* Session Replay Resources feature class, which needs to be registered with Session Replay Feature.
*/
internal class ResourcesFeature(
private val sdkCore: FeatureSdkCore,
customEndpointUrl: String?
) : StorageBackedFeature {

internal var dataWriter: ResourcesWriter = NoOpResourcesWriter()
internal val initialized = AtomicBoolean(false)

// region Feature

override val name: String = SESSION_REPLAY_RESOURCES_FEATURE_NAME

override val requestFactory: RequestFactory = ResourceRequestFactory(
customEndpointUrl = customEndpointUrl,
internalLogger = sdkCore.internalLogger
)

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

override val storageConfiguration: FeatureStorageConfiguration =
STORAGE_CONFIGURATION

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

// endregion

internal companion object {

/**
* Session Replay Resources 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"
jonathanmos marked this conversation as resolved.
Show resolved Hide resolved
internal const val RESOURCES_ENDPOINT_ENABLED = false
}
}
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 All @@ -21,11 +22,15 @@ import com.datadog.android.sessionreplay.NoOpRecorder
import com.datadog.android.sessionreplay.Recorder
import com.datadog.android.sessionreplay.SessionReplayPrivacy
import com.datadog.android.sessionreplay.SessionReplayRecorder
import com.datadog.android.sessionreplay.internal.ResourcesFeature.Companion.RESOURCES_ENDPOINT_ENABLED
import com.datadog.android.sessionreplay.internal.net.BatchesToSegmentsMapper
import com.datadog.android.sessionreplay.internal.net.SegmentRequestFactory
import com.datadog.android.sessionreplay.internal.recorder.OptionSelectorDetector
import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper
import com.datadog.android.sessionreplay.internal.storage.NoOpRecordWriter
import com.datadog.android.sessionreplay.internal.storage.NoOpResourcesWriter
import com.datadog.android.sessionreplay.internal.storage.RecordWriter
import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter
import com.datadog.android.sessionreplay.internal.storage.SessionReplayRecordWriter
import com.datadog.android.sessionreplay.internal.time.SessionReplayTimeProvider
import java.util.Locale
Expand All @@ -37,10 +42,10 @@ import java.util.concurrent.atomic.AtomicReference
*/
internal class SessionReplayFeature(
private val sdkCore: FeatureSdkCore,
customEndpointUrl: String?,
private val customEndpointUrl: String?,
internal val privacy: SessionReplayPrivacy,
private val rateBasedSampler: Sampler,
private val sessionReplayRecorderProvider: (RecordWriter, Application) -> Recorder
private val sessionReplayRecorderProvider: (ResourcesWriter, RecordWriter, Application) -> Recorder
) : StorageBackedFeature, FeatureEventReceiver {

private val currentRumSessionId = AtomicReference<String>()
Expand All @@ -57,9 +62,10 @@ internal class SessionReplayFeature(
customEndpointUrl,
privacy,
RateBasedSampler(sampleRate),
{ recordWriter, application ->
{ resourceWriter, recordWriter, application ->
SessionReplayRecorder(
application,
resourcesWriter = resourceWriter,
rumContextProvider = SessionReplayRumContextProvider(sdkCore),
privacy = privacy,
recordWriter = recordWriter,
Expand All @@ -71,7 +77,7 @@ internal class SessionReplayFeature(
}
)

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

this.appContext = appContext

sdkCore.setEventReceiver(SESSION_REPLAY_FEATURE_NAME, this)

val resourcesWriter = if (RESOURCES_ENDPOINT_ENABLED) {
val resourcesFeature = registerResourceFeature(sdkCore)
resourcesFeature.dataWriter
} else {
NoOpResourcesWriter()
}

dataWriter = createDataWriter()
sessionReplayRecorder = sessionReplayRecorderProvider(dataWriter, appContext)
sessionReplayRecorder = sessionReplayRecorderProvider(resourcesWriter, dataWriter, appContext)
jonathanmos marked this conversation as resolved.
Show resolved Hide resolved
@Suppress("ThreadSafety") // TODO REPLAY-1861 can be called from any thread
sessionReplayRecorder.registerCallbacks()
initialized.set(true)
Expand Down Expand Up @@ -244,6 +257,20 @@ internal class SessionReplayFeature(

// endregion

// region resourcesFeature

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

return resourcesFeature
jonathanmos marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 @@ -9,6 +9,8 @@ package com.datadog.android.sessionreplay.internal.net
import androidx.annotation.VisibleForTesting
import com.datadog.android.api.InternalLogger
import com.datadog.android.api.storage.RawBatchEvent
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 @@ -191,8 +193,6 @@ internal class ResourceRequestBodyFactory(
internal val CONTENT_TYPE_IMAGE = "image/png".toMediaTypeOrNull()
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,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.processor

import com.google.gson.JsonObject

internal data class EnrichedResource(
internal val resource: ByteArray,
internal val applicationId: String,
internal val filename: String
) {
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"
}
}

internal fun EnrichedResource.asBinaryMetadata(): ByteArray {
val applicationId = this.applicationId
val filename = this.filename
val jsonObject = JsonObject()
jsonObject.addProperty(EnrichedResource.APPLICATION_ID_KEY, applicationId)
jsonObject.addProperty(EnrichedResource.FILENAME_KEY, filename)
return jsonObject.toString().toByteArray(Charsets.UTF_8)
}
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 @@ -8,18 +8,21 @@ 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.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
import com.datadog.android.sessionreplay.internal.recorder.SystemInformation
import com.datadog.android.sessionreplay.internal.storage.RecordWriter
import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter
import com.datadog.android.sessionreplay.internal.utils.SessionReplayRumContext
import com.datadog.android.sessionreplay.model.MobileSegment
import java.util.LinkedList
import java.util.concurrent.TimeUnit

@Suppress("TooManyFunctions")
internal class RecordedDataProcessor(
private val resourcesWriter: ResourcesWriter,
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
)

resourcesWriter.write(enrichedResource)
}

@WorkerThread
override fun processScreenSnapshots(
item: SnapshotRecordedDataQueueItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

package com.datadog.android.sessionreplay.internal.storage

import com.datadog.android.sessionreplay.internal.RecordWriter
import com.datadog.android.sessionreplay.internal.processor.EnrichedRecord

internal class NoOpRecordWriter : RecordWriter {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* 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.processor.EnrichedResource

internal class NoOpResourcesWriter : ResourcesWriter {
override fun write(enrichedResource: EnrichedResource) {
// no-op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sessionreplay.internal
package com.datadog.android.sessionreplay.internal.storage

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

Expand Down