Skip to content
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ This is the first official production version of SDK v3 containing the new archi
* [MAINTENANCE] Bump Kotlin version used to `2.0.21`. See [#2766](https://github.com/DataDog/dd-sdk-android/pull/2766)
* [MAINTENANCE] Bump `minSdk` version to 23. See [#2844](https://github.com/DataDog/dd-sdk-android/pull/2844)

# 2.26.2 / 2025-10-09

* [IMPROVEMENT] Extend resource handling to support multiple MIME types in RN. See [#2914](https://github.com/DataDog/dd-sdk-android/pull/2914)

# 2.26.1 / 2025-09-11

* [BUGFIX] RUM: Move session properties to `ddtags` over query parameters. See [#2812](https://github.com/DataDog/dd-sdk-android/pull/2812)

# 2.26.0 / 2025-08-27

* [FEATURE] RUM: Add battery and display attributes. See [#2815](https://github.com/DataDog/dd-sdk-android/pull/2815)
Expand Down
4 changes: 4 additions & 0 deletions features/dd-sdk-android-session-replay/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ data class com.datadog.android.sessionreplay.SessionReplayConfiguration
fun build(): SessionReplayConfiguration
interface com.datadog.android.sessionreplay.SessionReplayInternalCallback
fun getCurrentActivity(): android.app.Activity?
fun addResourceItem(String, ByteArray, String? = null)
fun setResourceQueue(SessionReplayInternalResourceQueue)
interface com.datadog.android.sessionreplay.SessionReplayInternalResourceQueue
fun addResourceItem(String, ByteArray, String? = null)
enum com.datadog.android.sessionreplay.SessionReplayPrivacy
- ALLOW
- MASK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,21 @@ public final class com/datadog/android/sessionreplay/SessionReplayConfiguration$
}

public abstract interface class com/datadog/android/sessionreplay/SessionReplayInternalCallback {
public abstract fun addResourceItem (Ljava/lang/String;[BLjava/lang/String;)V
public abstract fun getCurrentActivity ()Landroid/app/Activity;
public abstract fun setResourceQueue (Lcom/datadog/android/sessionreplay/SessionReplayInternalResourceQueue;)V
}

public final class com/datadog/android/sessionreplay/SessionReplayInternalCallback$DefaultImpls {
public static synthetic fun addResourceItem$default (Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;Ljava/lang/String;[BLjava/lang/String;ILjava/lang/Object;)V
}

public abstract interface class com/datadog/android/sessionreplay/SessionReplayInternalResourceQueue {
public abstract fun addResourceItem (Ljava/lang/String;[BLjava/lang/String;)V
}

public final class com/datadog/android/sessionreplay/SessionReplayInternalResourceQueue$DefaultImpls {
public static synthetic fun addResourceItem$default (Lcom/datadog/android/sessionreplay/SessionReplayInternalResourceQueue;Ljava/lang/String;[BLjava/lang/String;ILjava/lang/Object;)V
}

public final class com/datadog/android/sessionreplay/SessionReplayPrivacy : java/lang/Enum {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,22 @@ interface SessionReplayInternalCallback {
* that were missed because the client was initialized after the `Application.onCreate` phase.
*/
fun getCurrentActivity(): Activity?

/**
* Adds a resource item to the current resource queue.
*
* @param identifier A unique identifier for the resource.
* @param resourceData The raw content of the resource.
* @param mimeType Optional MIME type describing the resource data (e.g. `"image/png"` or `"image/svg+xml"`).
*/
fun addResourceItem(identifier: String, resourceData: ByteArray, mimeType: String? = null)

/**
* Sets the resource queue used by this callback. This allows resource management to also be handled
* on a platform-specific level.
*
* @param resourceQueue The [SessionReplayInternalResourceQueue] instance that will
* collect and manage resource items submitted through [addResourceItem].
*/
fun setResourceQueue(resourceQueue: SessionReplayInternalResourceQueue)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 com.datadog.android.lint.InternalApi

/**
* Internal API for queuing custom resources in Session Replay.
*
* This interface provides access to the Session Replay resource queue, allowing clients
* to manually add custom resources (e.g., SVG images, custom assets) that will be
* uploaded alongside standard Session Replay data.
*
* Note: This is an internal API intended for advanced use cases where standard image
* capture mechanisms don't apply. For regular image resources, the SDK handles
* resource capture automatically.
*/
@InternalApi
interface SessionReplayInternalResourceQueue {
/**
* Adds a custom resource item to the Session Replay upload queue.
*
* Resources are deduplicated based on their identifier. If a resource with the same
* identifier has already been queued or uploaded in this session, it will not be
* queued again.
*
* @param identifier A unique identifier for this resource. Typically an MD5 hash of
* the resource content to ensure deduplication across identical resources.
* @param resourceData The raw binary data of the resource to upload.
* @param mimeType Optional MIME type of the resource (e.g., "image/svg+xml").
* If null, the resource will be uploaded without a specific content type.
*/
fun addResourceItem(
identifier: String,
resourceData: ByteArray,
mimeType: String? = null
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import com.datadog.android.sessionreplay.recorder.SystemInformation
internal interface DataQueueHandler {
fun addResourceItem(
identifier: String,
resourceData: ByteArray
resourceData: ByteArray,
mimeType: String? = null
): ResourceRecordedDataQueueItem?
fun addTouchEventItem(
pointerInteractions: List<MobileSegment.MobileRecord>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,17 @@ internal class RecordedDataQueueHandler(
@MainThread
override fun addResourceItem(
identifier: String,
resourceData: ByteArray
resourceData: ByteArray,
mimeType: String?
): ResourceRecordedDataQueueItem? {
val rumContextData = rumContextDataHandler.createRumContextData()
?: return null

val item = ResourceRecordedDataQueueItem(
recordedQueuedItemContext = rumContextData,
identifier = identifier,
resourceData = resourceData
resourceData = resourceData,
mimeType
)

insertIntoRecordedDataQueue(item)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import com.datadog.android.sessionreplay.internal.processor.RecordedQueuedItemCo
internal class ResourceRecordedDataQueueItem(
recordedQueuedItemContext: RecordedQueuedItemContext,
val identifier: String,
val resourceData: ByteArray
val resourceData: ByteArray,
val mimeType: String? = null
) : RecordedDataQueueItem(recordedQueuedItemContext) {

override fun isValid(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ package com.datadog.android.sessionreplay.internal.net
internal data class ResourceEvent(
val applicationId: String,
val identifier: String,
val resourceData: ByteArray
val resourceData: ByteArray,
val mimeType: String? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.datadog.android.sessionreplay.internal.processor.EnrichedResource.Com
import com.datadog.android.sessionreplay.internal.processor.EnrichedResource.Companion.APPLICATION_KEY
import com.datadog.android.sessionreplay.internal.processor.EnrichedResource.Companion.FILENAME_KEY
import com.datadog.android.sessionreplay.internal.processor.EnrichedResource.Companion.ID_KEY
import com.datadog.android.sessionreplay.internal.processor.EnrichedResource.Companion.MIME_TYPE
import com.datadog.android.sessionreplay.internal.utils.MiscUtils
import com.google.gson.JsonObject
import okhttp3.MediaType.Companion.toMediaTypeOrNull
Expand Down Expand Up @@ -99,12 +100,18 @@ internal class ResourceRequestBodyFactory(
resourceMetadata,
FILENAME_KEY
)
val mimeType = MiscUtils.safeGetStringFromJsonObject(
internalLogger,
resourceMetadata,
MIME_TYPE
)

if (applicationId != null && filename != null) {
ResourceEvent(
applicationId = applicationId,
identifier = filename,
it.data
it.data,
mimeType = mimeType
)
} else {
null
Expand All @@ -119,7 +126,8 @@ internal class ResourceRequestBodyFactory(
resources.forEach {
val filename = it.identifier
val resourceData = it.resourceData
addResourceRequestBody(builder, filename, resourceData)
val mimeType = it.mimeType
addResourceRequestBody(builder, filename, resourceData, mimeType)
}
}

Expand Down Expand Up @@ -162,9 +170,14 @@ internal class ResourceRequestBodyFactory(
}

@Suppress("TooGenericExceptionCaught")
private fun addResourceRequestBody(builder: MultipartBody.Builder, filename: String, resourceData: ByteArray) {
private fun addResourceRequestBody(
builder: MultipartBody.Builder,
filename: String,
resourceData: ByteArray,
mimeType: String? = null
) {
val body = try {
resourceData.toRequestBody(CONTENT_TYPE_IMAGE)
resourceData.toRequestBody(mimeType?.toMediaTypeOrNull() ?: CONTENT_TYPE_IMAGE)
} catch (e: ArrayIndexOutOfBoundsException) {
// this should never happen because we aren't specifying an offset
internalLogger.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import com.google.gson.JsonObject

internal data class EnrichedResource(
internal val resource: ByteArray,
internal val filename: String
internal val filename: String,
internal val mimeType: String? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand All @@ -33,6 +34,7 @@ internal data class EnrichedResource(
internal const val APPLICATION_KEY = "application"
internal const val ID_KEY = "id"
internal const val FILENAME_KEY = "filename"
internal const val MIME_TYPE = "mimeType"
}
}

Expand All @@ -41,5 +43,8 @@ internal fun EnrichedResource.asBinaryMetadata(rumApplicationId: String): ByteAr
val jsonObject = JsonObject()
jsonObject.addProperty(EnrichedResource.APPLICATION_ID_KEY, rumApplicationId)
jsonObject.addProperty(EnrichedResource.FILENAME_KEY, filename)
if (this.mimeType != null) {
jsonObject.addProperty(EnrichedResource.MIME_TYPE, this.mimeType)
}
return jsonObject.toString().toByteArray(Charsets.UTF_8)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ internal class RecordedDataProcessor(

val enrichedResource = EnrichedResource(
resource = item.resourceData,
filename = resourceHash
filename = resourceHash,
mimeType = item.mimeType
)

resourcesWriter.write(enrichedResource)
Expand Down
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.processor

import androidx.annotation.MainThread
import com.datadog.android.sessionreplay.SessionReplayInternalResourceQueue
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler

internal class ResourceQueueImpl(
private val internalHandler: RecordedDataQueueHandler
) : SessionReplayInternalResourceQueue {
@MainThread
override fun addResourceItem(identifier: String, resourceData: ByteArray, mimeType: String?) {
internalHandler.addResourceItem(identifier, resourceData, mimeType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.datadog.android.sessionreplay.internal.TouchPrivacyManager
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler
import com.datadog.android.sessionreplay.internal.processor.MutationResolver
import com.datadog.android.sessionreplay.internal.processor.RecordedDataProcessor
import com.datadog.android.sessionreplay.internal.processor.ResourceQueueImpl
import com.datadog.android.sessionreplay.internal.processor.RumContextDataHandler
import com.datadog.android.sessionreplay.internal.recorder.callback.OnWindowRefreshedCallback
import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper
Expand Down Expand Up @@ -202,6 +203,9 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
sessionReplayLifecycleCallback.registerFragmentLifecycleCallbacks(it)
}

// Expose this object so it can be used to dynamically add resources
internalCallback.setResourceQueue(ResourceQueueImpl(this.recordedDataQueueHandler))

this.uiHandler = Handler(Looper.getMainLooper())
this.internalLogger = internalLogger
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,8 @@ internal class ResourceResolverTest {

verify(mockRecordedDataQueueHandler, times(1)).addResourceItem(
identifier = eq(fakeResourceId),
resourceData = eq(fakeByteArray)
resourceData = eq(fakeByteArray),
mimeType = anyOrNull()
)

// second time
Expand All @@ -950,7 +951,8 @@ internal class ResourceResolverTest {

verify(mockRecordedDataQueueHandler, times(1)).addResourceItem(
identifier = eq(fakeResourceId),
resourceData = eq(fakeByteArray)
resourceData = eq(fakeByteArray),
mimeType = anyOrNull()
)
}

Expand Down
Loading