diff --git a/CHANGELOG.md b/CHANGELOG.md index bba30a929d..e597bc20c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index 67fbbfab01..2bcdab33db 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -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 diff --git a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api index 6c31d4c40c..e6ccf73eb5 100644 --- a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api +++ b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api @@ -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 { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalCallback.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalCallback.kt index 23d468a738..0674c2e41d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalCallback.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalCallback.kt @@ -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) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalResourceQueue.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalResourceQueue.kt new file mode 100644 index 0000000000..a471b1bb1f --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalResourceQueue.kt @@ -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 + ) +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/DataQueueHandler.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/DataQueueHandler.kt index 590aea3825..559df2e7c0 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/DataQueueHandler.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/DataQueueHandler.kt @@ -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 diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/RecordedDataQueueHandler.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/RecordedDataQueueHandler.kt index 28313ba823..14eac29127 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/RecordedDataQueueHandler.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/RecordedDataQueueHandler.kt @@ -44,7 +44,8 @@ internal class RecordedDataQueueHandler( @MainThread override fun addResourceItem( identifier: String, - resourceData: ByteArray + resourceData: ByteArray, + mimeType: String? ): ResourceRecordedDataQueueItem? { val rumContextData = rumContextDataHandler.createRumContextData() ?: return null @@ -52,7 +53,8 @@ internal class RecordedDataQueueHandler( val item = ResourceRecordedDataQueueItem( recordedQueuedItemContext = rumContextData, identifier = identifier, - resourceData = resourceData + resourceData = resourceData, + mimeType ) insertIntoRecordedDataQueue(item) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/ResourceRecordedDataQueueItem.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/ResourceRecordedDataQueueItem.kt index 22870eff1c..4b68fbbb19 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/ResourceRecordedDataQueueItem.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/async/ResourceRecordedDataQueueItem.kt @@ -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 { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceEvent.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceEvent.kt index 8ceee3fb37..c02396580a 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceEvent.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceEvent.kt @@ -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 diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceRequestBodyFactory.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceRequestBodyFactory.kt index acb2428e24..637b9d4248 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceRequestBodyFactory.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/net/ResourceRequestBodyFactory.kt @@ -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 @@ -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 @@ -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) } } @@ -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( diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/EnrichedResource.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/EnrichedResource.kt index fa43f5bb1d..43861453b8 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/EnrichedResource.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/EnrichedResource.kt @@ -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 @@ -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" } } @@ -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) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/RecordedDataProcessor.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/RecordedDataProcessor.kt index f9a3232504..12db3af535 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/RecordedDataProcessor.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/RecordedDataProcessor.kt @@ -50,7 +50,8 @@ internal class RecordedDataProcessor( val enrichedResource = EnrichedResource( resource = item.resourceData, - filename = resourceHash + filename = resourceHash, + mimeType = item.mimeType ) resourcesWriter.write(enrichedResource) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/ResourceQueueImpl.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/ResourceQueueImpl.kt new file mode 100644 index 0000000000..519b18eb5b --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/ResourceQueueImpl.kt @@ -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) + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt index 94e3f6bea8..9fba608ef8 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt @@ -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 @@ -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 } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt index 4d40afbc84..7dda732788 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/ResourceResolverTest.kt @@ -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 @@ -950,7 +951,8 @@ internal class ResourceResolverTest { verify(mockRecordedDataQueueHandler, times(1)).addResourceItem( identifier = eq(fakeResourceId), - resourceData = eq(fakeByteArray) + resourceData = eq(fakeByteArray), + mimeType = anyOrNull() ) }