From 30ed6787ea48965df1b83c3c6e8f30b24dd3a336 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:11:16 +0300 Subject: [PATCH 1/7] PANA-8500: Sync composition wire schemas --- .../api/apiSurface | 156 ++++- .../api/dd-sdk-android-session-replay.api | 570 +++++++++++++++++- ...layer-brightness-bias-modifier-schema.json | 23 + .../composition-layer-child-schema.json | 21 + ...omposition-layer-clip-modifier-schema.json | 27 + ...on-layer-color-matrix-modifier-schema.json | 26 + ...n-layer-gaussian-blur-modifier-schema.json | 22 + ...tion-layer-mask-image-modifier-schema.json | 21 + .../composition-layer-modifier-schema.json | 33 + ...osition-layer-opacity-modifier-schema.json | 23 + ...sition-layer-saturate-modifier-schema.json | 22 + .../mobile/composition-layer-schema.json | 57 ++ ...position-layer-shadow-modifier-schema.json | 43 ++ .../composition-layer-update-schema.json | 57 ++ ...composition-tree-mutation-data-schema.json | 43 ++ .../mobile/composition-tree-schema.json | 21 + .../embedded-content-wireframe-schema.json | 38 ++ ...edded-content-wireframe-update-schema.json | 33 + .../mobile/full-snapshot-record-schema.json | 10 + .../mobile/incremental-data-schema.json | 3 + .../incremental-snapshot-record-schema.json | 5 + .../mobile/shape-gradient-point-schema.json | 20 + .../mobile/shape-gradient-schema.json | 11 + .../mobile/shape-gradient-stop-schema.json | 23 + .../mobile/shape-linear-gradient-schema.json | 43 ++ .../mobile/shape-style-schema.json | 9 + .../mobile/wireframe-schema.json | 3 + .../wireframe-update-mutation-schema.json | 3 + .../internal/processor/BoundsUtils.kt | 5 + .../internal/processor/HeatmapWireframeExt.kt | 2 + .../internal/processor/MobileSegmentExt.kt | 1 + .../internal/processor/MutationResolver.kt | 32 + .../processor/RecordedDataProcessor.kt | 8 +- .../internal/processor/WireframeUtils.kt | 1 + .../recorder/mapper/RadioButtonMapper.kt | 2 +- .../recorder/mapper/SwitchCompatMapper.kt | 2 +- .../internal/utils/WireframeExt.kt | 3 + .../recorder/mapper/BaseWireframeMapper.kt | 5 +- .../EmbeddedContentWireframeForgeryFactory.kt | 26 + .../sessionreplay/forge/ForgeConfigurator.kt | 1 + .../forge/WireframeForgeryFactory.kt | 4 +- .../internal/processor/BoundUtilsTest.kt | 6 + .../processor/MutationResolverTest.kt | 15 +- .../internal/processor/NodeFlattenerTest.kt | 13 + .../internal/processor/WireframeUtilsTest.kt | 5 + .../recorder/mapper/SwitchCompatMapperTest.kt | 4 +- .../internal/utils/WireframeExtTest.kt | 1 + 47 files changed, 1473 insertions(+), 29 deletions(-) create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-brightness-bias-modifier-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-child-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-clip-modifier-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-color-matrix-modifier-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-gaussian-blur-modifier-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-mask-image-modifier-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-modifier-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-opacity-modifier-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-saturate-modifier-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-shadow-modifier-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-update-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-tree-mutation-data-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-tree-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-update-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-point-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-stop-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-linear-gradient-schema.json create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/EmbeddedContentWireframeForgeryFactory.kt diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index f420baab16..5b8afe1db4 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -192,14 +192,14 @@ data class com.datadog.android.sessionreplay.model.MobileSegment sealed class MobileRecord abstract fun toJson(): com.google.gson.JsonElement data class MobileFullSnapshotRecord : MobileRecord - constructor(kotlin.Long, Data) + constructor(kotlin.Long, Data, kotlin.String? = null) val type: kotlin.Long override fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): MobileFullSnapshotRecord fun fromJsonObject(com.google.gson.JsonObject): MobileFullSnapshotRecord data class MobileIncrementalSnapshotRecord : MobileRecord - constructor(kotlin.Long, MobileIncrementalData) + constructor(kotlin.Long, MobileIncrementalData, kotlin.String? = null) val type: kotlin.Long override fun toJson(): com.google.gson.JsonElement companion object @@ -237,7 +237,7 @@ data class com.datadog.android.sessionreplay.model.MobileSegment fun fromJson(kotlin.String): MobileRecord fun fromJsonElement(com.google.gson.JsonElement): MobileRecord data class Data - constructor(kotlin.collections.List) + constructor(kotlin.collections.List, CompositionTree? = null) fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): Data @@ -272,6 +272,13 @@ data class com.datadog.android.sessionreplay.model.MobileSegment companion object fun fromJson(kotlin.String): PointerInteractionData fun fromJsonObject(com.google.gson.JsonObject): PointerInteractionData + data class CompositionTreeMutationData : MobileIncrementalData + constructor(CompositionLayer? = null, kotlin.collections.List? = null, kotlin.collections.List? = null, kotlin.collections.List? = null) + val source: kotlin.Long + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionTreeMutationData + fun fromJsonObject(com.google.gson.JsonObject): CompositionTreeMutationData companion object fun fromJson(kotlin.String): MobileIncrementalData fun fromJsonElement(com.google.gson.JsonElement): MobileIncrementalData @@ -330,9 +337,22 @@ data class com.datadog.android.sessionreplay.model.MobileSegment companion object fun fromJson(kotlin.String): WebviewWireframe fun fromJsonObject(com.google.gson.JsonObject): WebviewWireframe + data class EmbeddedContentWireframe : Wireframe + constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, kotlin.Boolean? = null, kotlin.String? = null) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): EmbeddedContentWireframe + fun fromJsonObject(com.google.gson.JsonObject): EmbeddedContentWireframe companion object fun fromJson(kotlin.String): Wireframe fun fromJsonElement(com.google.gson.JsonElement): Wireframe + data class CompositionTree + constructor(CompositionLayer, kotlin.collections.List? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionTree + fun fromJsonObject(com.google.gson.JsonObject): CompositionTree data class Add constructor(kotlin.Long? = null, Wireframe) fun toJson(): com.google.gson.JsonElement @@ -382,6 +402,13 @@ data class com.datadog.android.sessionreplay.model.MobileSegment companion object fun fromJson(kotlin.String): WebviewWireframeUpdate fun fromJsonObject(com.google.gson.JsonObject): WebviewWireframeUpdate + data class EmbeddedContentWireframeUpdate : WireframeUpdateMutation + constructor(kotlin.Long, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, kotlin.Boolean? = null) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): EmbeddedContentWireframeUpdate + fun fromJsonObject(com.google.gson.JsonObject): EmbeddedContentWireframeUpdate companion object fun fromJson(kotlin.String): WireframeUpdateMutation fun fromJsonElement(com.google.gson.JsonElement): WireframeUpdateMutation @@ -391,6 +418,18 @@ data class com.datadog.android.sessionreplay.model.MobileSegment companion object fun fromJson(kotlin.String): Position fun fromJsonObject(com.google.gson.JsonObject): Position + data class CompositionLayer + constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.collections.List, kotlin.collections.List? = null, CompositeOperation? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayer + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayer + data class CompositionLayerUpdate + constructor(kotlin.Long, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.collections.List? = null, kotlin.collections.List? = null, CompositeOperation? = null) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerUpdate + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerUpdate data class WireframeClip constructor(kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null) fun toJson(): com.google.gson.JsonElement @@ -398,7 +437,7 @@ data class com.datadog.android.sessionreplay.model.MobileSegment fun fromJson(kotlin.String): WireframeClip fun fromJsonObject(com.google.gson.JsonObject): WireframeClip data class ShapeStyle - constructor(kotlin.String? = null, kotlin.Number? = null, kotlin.Number? = null) + constructor(kotlin.String? = null, BackgroundGradient? = null, kotlin.Number? = null, kotlin.Number? = null) fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): ShapeStyle @@ -421,6 +460,80 @@ data class com.datadog.android.sessionreplay.model.MobileSegment companion object fun fromJson(kotlin.String): TextPosition fun fromJsonObject(com.google.gson.JsonObject): TextPosition + data class CompositionLayerChild + constructor(Type, kotlin.Long) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerChild + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerChild + sealed class CompositionLayerModifier + abstract fun toJson(): com.google.gson.JsonElement + data class CompositionLayerClipModifier : CompositionLayerModifier + constructor(kotlin.String, FillRule? = null) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerClipModifier + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerClipModifier + data class CompositionLayerOpacityModifier : CompositionLayerModifier + constructor(kotlin.Number) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerOpacityModifier + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerOpacityModifier + data class CompositionLayerColorMatrixModifier : CompositionLayerModifier + constructor(kotlin.collections.List) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerColorMatrixModifier + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerColorMatrixModifier + data class CompositionLayerGaussianBlurModifier : CompositionLayerModifier + constructor(kotlin.Number) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerGaussianBlurModifier + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerGaussianBlurModifier + data class CompositionLayerShadowModifier : CompositionLayerModifier + constructor(kotlin.String, kotlin.Number, kotlin.Number, kotlin.Number, kotlin.String? = null) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerShadowModifier + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerShadowModifier + data class CompositionLayerBrightnessBiasModifier : CompositionLayerModifier + constructor(kotlin.Number) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerBrightnessBiasModifier + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerBrightnessBiasModifier + data class CompositionLayerSaturateModifier : CompositionLayerModifier + constructor(kotlin.Number) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerSaturateModifier + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerSaturateModifier + data class CompositionLayerMaskImageModifier : CompositionLayerModifier + constructor(kotlin.String) + val type: kotlin.String + override fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositionLayerMaskImageModifier + fun fromJsonObject(com.google.gson.JsonObject): CompositionLayerMaskImageModifier + companion object + fun fromJson(kotlin.String): CompositionLayerModifier + fun fromJsonElement(com.google.gson.JsonElement): CompositionLayerModifier + data class BackgroundGradient + constructor(kotlin.collections.List, StartPoint, StartPoint) + val type: kotlin.String + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): BackgroundGradient + fun fromJsonObject(com.google.gson.JsonObject): BackgroundGradient data class Padding constructor(kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null) fun toJson(): com.google.gson.JsonElement @@ -433,6 +546,18 @@ data class com.datadog.android.sessionreplay.model.MobileSegment companion object fun fromJson(kotlin.String): Alignment fun fromJsonObject(com.google.gson.JsonObject): Alignment + data class ShapeGradientStop + constructor(kotlin.String, kotlin.Number) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): ShapeGradientStop + fun fromJsonObject(com.google.gson.JsonObject): ShapeGradientStop + data class StartPoint + constructor(kotlin.Number, kotlin.Number) + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): StartPoint + fun fromJsonObject(com.google.gson.JsonObject): StartPoint enum Source constructor(kotlin.String) - ANDROID @@ -460,6 +585,15 @@ data class com.datadog.android.sessionreplay.model.MobileSegment fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): PointerType + enum CompositeOperation + constructor(kotlin.String) + - SOURCEOVER + - DESTINATIONIN + - DESTINATIONOUT + - PLUSDARKER + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): CompositeOperation enum TruncationMode constructor(kotlin.String) - CLIP @@ -469,6 +603,20 @@ data class com.datadog.android.sessionreplay.model.MobileSegment fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): TruncationMode + enum Type + constructor(kotlin.String) + - WIREFRAME + - LAYER + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): Type + enum FillRule + constructor(kotlin.String) + - NONZERO + - EVENODD + fun toJson(): com.google.gson.JsonElement + companion object + fun fromJson(kotlin.String): FillRule enum Horizontal constructor(kotlin.String) - LEFT 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 9881fee8f6..d2fea1c10c 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 @@ -265,20 +265,371 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Applica public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Application; } +public final class com/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient$Companion; + public fun (Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint;Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; + public final fun component3 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; + public final fun copy (Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint;Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint;)Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint;Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient; + public final fun getEndPoint ()Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; + public final fun getStartPoint ()Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; + public final fun getStops ()Ljava/util/List; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient; +} + public final class com/datadog/android/sessionreplay/model/MobileSegment$Companion { public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment; public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment; } +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation : java/lang/Enum { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation$Companion; + public static final field DESTINATIONIN Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public static final field DESTINATIONOUT Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public static final field PLUSDARKER Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public static final field SOURCEOVER Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public final fun toJson ()Lcom/google/gson/JsonElement; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public static fun values ()[Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer$Companion; + public fun (JJJJJLjava/util/List;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation;)V + public synthetic fun (JJJJJLjava/util/List;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()J + public final fun component2 ()J + public final fun component3 ()J + public final fun component4 ()J + public final fun component5 ()J + public final fun component6 ()Ljava/util/List; + public final fun component7 ()Ljava/util/List; + public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public final fun copy (JJJJJLjava/util/List;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer;JJJJJLjava/util/List;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; + public final fun getChildren ()Ljava/util/List; + public final fun getCompositeOperation ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public final fun getHeight ()J + public final fun getId ()J + public final fun getModifiers ()Ljava/util/List; + public final fun getWidth ()J + public final fun getX ()J + public final fun getY ()J + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild$Companion; + public fun (Lcom/datadog/android/sessionreplay/model/MobileSegment$Type;J)V + public final fun component1 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$Type; + public final fun component2 ()J + public final fun copy (Lcom/datadog/android/sessionreplay/model/MobileSegment$Type;J)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild;Lcom/datadog/android/sessionreplay/model/MobileSegment$Type;JILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild; + public final fun getId ()J + public final fun getType ()Lcom/datadog/android/sessionreplay/model/MobileSegment$Type; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerChild; +} + +public abstract class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$Companion; + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier; + public static final fun fromJsonElement (Lcom/google/gson/JsonElement;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier; + public abstract fun toJson ()Lcom/google/gson/JsonElement; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier; + public final fun fromJsonElement (Lcom/google/gson/JsonElement;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier : com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier$Companion; + public fun (Ljava/lang/Number;)V + public final fun component1 ()Ljava/lang/Number; + public final fun copy (Ljava/lang/Number;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier;Ljava/lang/Number;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier; + public final fun getType ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/Number; + public fun hashCode ()I + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerBrightnessBiasModifier; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier : com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier$Companion; + public fun (Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule;)V + public synthetic fun (Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule; + public final fun copy (Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier; + public final fun getFillRule ()Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule; + public final fun getPath ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerClipModifier; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier : com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier$Companion; + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier;Ljava/util/List;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier; + public final fun getMatrix ()Ljava/util/List; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerColorMatrixModifier; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier : com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier$Companion; + public fun (Ljava/lang/Number;)V + public final fun component1 ()Ljava/lang/Number; + public final fun copy (Ljava/lang/Number;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier;Ljava/lang/Number;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier; + public final fun getRadius ()Ljava/lang/Number; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerGaussianBlurModifier; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier : com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier$Companion; + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier; + public final fun getResourceId ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerMaskImageModifier; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier : com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier$Companion; + public fun (Ljava/lang/Number;)V + public final fun component1 ()Ljava/lang/Number; + public final fun copy (Ljava/lang/Number;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier;Ljava/lang/Number;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier; + public final fun getType ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/Number; + public fun hashCode ()I + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerOpacityModifier; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier : com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier$Companion; + public fun (Ljava/lang/Number;)V + public final fun component1 ()Ljava/lang/Number; + public final fun copy (Ljava/lang/Number;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier;Ljava/lang/Number;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier; + public final fun getType ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/Number; + public fun hashCode ()I + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerSaturateModifier; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier : com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier$Companion; + public fun (Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/Number; + public final fun component3 ()Ljava/lang/Number; + public final fun component4 ()Ljava/lang/Number; + public final fun component5 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier;Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/Number;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier; + public final fun getColor ()Ljava/lang/String; + public final fun getOffsetX ()Ljava/lang/Number; + public final fun getOffsetY ()Ljava/lang/Number; + public final fun getPath ()Ljava/lang/String; + public final fun getRadius ()Ljava/lang/Number; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerModifier$CompositionLayerShadowModifier; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate$Companion; + public fun (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation;)V + public synthetic fun (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()J + public final fun component2 ()Ljava/lang/Long; + public final fun component3 ()Ljava/lang/Long; + public final fun component4 ()Ljava/lang/Long; + public final fun component5 ()Ljava/lang/Long; + public final fun component6 ()Ljava/util/List; + public final fun component7 ()Ljava/util/List; + public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public final fun copy (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate;JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/util/List;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate; + public final fun getChildren ()Ljava/util/List; + public final fun getCompositeOperation ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositeOperation; + public final fun getHeight ()Ljava/lang/Long; + public final fun getId ()J + public final fun getModifiers ()Ljava/util/List; + public final fun getWidth ()Ljava/lang/Long; + public final fun getX ()Ljava/lang/Long; + public final fun getY ()Ljava/lang/Long; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayerUpdate; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionTree { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree$Companion; + public fun (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer;Ljava/util/List;)V + public synthetic fun (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; + public final fun component2 ()Ljava/util/List; + public final fun copy (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer;Ljava/util/List;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer;Ljava/util/List;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree; + public final fun getLayers ()Ljava/util/List; + public final fun getRoot ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$CompositionTree$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree; +} + public final class com/datadog/android/sessionreplay/model/MobileSegment$Data { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Data$Companion; - public fun (Ljava/util/List;)V + public fun (Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree;)V + public synthetic fun (Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/util/List; - public final fun copy (Ljava/util/List;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Data; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Data;Ljava/util/List;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Data; + public final fun component2 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree; + public final fun copy (Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Data; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Data;Ljava/util/List;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Data; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Data; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Data; + public final fun getCompositionTree ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionTree; public final fun getWireframes ()Ljava/util/List; public fun hashCode ()I public final fun toJson ()Lcom/google/gson/JsonElement; @@ -367,6 +718,20 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Data3$C public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Data3; } +public final class com/datadog/android/sessionreplay/model/MobileSegment$FillRule : java/lang/Enum { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule$Companion; + public static final field EVENODD Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule; + public static final field NONZERO Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule; + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule; + public final fun toJson ()Lcom/google/gson/JsonElement; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule; + public static fun values ()[Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$FillRule$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$FillRule; +} + public final class com/datadog/android/sessionreplay/model/MobileSegment$Horizontal : java/lang/Enum { public static final field CENTER Lcom/datadog/android/sessionreplay/model/MobileSegment$Horizontal; public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Horizontal$Companion; @@ -394,6 +759,35 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$MobileI public final fun fromJsonElement (Lcom/google/gson/JsonElement;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData; } +public final class com/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData : com/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData$Companion; + public fun ()V + public fun (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer;Ljava/util/List;Ljava/util/List;Ljava/util/List;)V + public synthetic fun (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; + public final fun component2 ()Ljava/util/List; + public final fun component3 ()Ljava/util/List; + public final fun component4 ()Ljava/util/List; + public final fun copy (Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer;Ljava/util/List;Ljava/util/List;Ljava/util/List;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData;Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData; + public final fun getAdds ()Ljava/util/List; + public final fun getRemoves ()Ljava/util/List; + public final fun getRoot ()Lcom/datadog/android/sessionreplay/model/MobileSegment$CompositionLayer; + public final fun getSource ()J + public final fun getUpdates ()Ljava/util/List; + public fun hashCode ()I + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$CompositionTreeMutationData; +} + public final class com/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$MobileMutationData : com/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData$MobileMutationData$Companion; public fun (Ljava/util/List;Ljava/util/List;Ljava/util/List;)V @@ -560,15 +954,18 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$MobileR public final class com/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord : com/datadog/android/sessionreplay/model/MobileSegment$MobileRecord { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord$Companion; - public fun (JLcom/datadog/android/sessionreplay/model/MobileSegment$Data;)V + public fun (JLcom/datadog/android/sessionreplay/model/MobileSegment$Data;Ljava/lang/String;)V + public synthetic fun (JLcom/datadog/android/sessionreplay/model/MobileSegment$Data;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()J public final fun component2 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$Data; - public final fun copy (JLcom/datadog/android/sessionreplay/model/MobileSegment$Data;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord;JLcom/datadog/android/sessionreplay/model/MobileSegment$Data;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord; + public final fun component3 ()Ljava/lang/String; + public final fun copy (JLcom/datadog/android/sessionreplay/model/MobileSegment$Data;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord;JLcom/datadog/android/sessionreplay/model/MobileSegment$Data;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileFullSnapshotRecord; public final fun getData ()Lcom/datadog/android/sessionreplay/model/MobileSegment$Data; + public final fun getSlotId ()Ljava/lang/String; public final fun getTimestamp ()J public final fun getType ()J public fun hashCode ()I @@ -583,15 +980,18 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$MobileR public final class com/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord : com/datadog/android/sessionreplay/model/MobileSegment$MobileRecord { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord$Companion; - public fun (JLcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData;)V + public fun (JLcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData;Ljava/lang/String;)V + public synthetic fun (JLcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()J public final fun component2 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData; - public final fun copy (JLcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord;JLcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord; + public final fun component3 ()Ljava/lang/String; + public final fun copy (JLcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord;JLcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileRecord$MobileIncrementalSnapshotRecord; public final fun getData ()Lcom/datadog/android/sessionreplay/model/MobileSegment$MobileIncrementalData; + public final fun getSlotId ()Ljava/lang/String; public final fun getTimestamp ()J public final fun getType ()J public fun hashCode ()I @@ -800,20 +1200,44 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$ShapeBo public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; } +public final class com/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop$Companion; + public fun (Ljava/lang/String;Ljava/lang/Number;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/Number; + public final fun copy (Ljava/lang/String;Ljava/lang/Number;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop;Ljava/lang/String;Ljava/lang/Number;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop; + public final fun getColor ()Ljava/lang/String; + public final fun getPosition ()Ljava/lang/Number; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeGradientStop; +} + public final class com/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle$Companion; public fun ()V - public fun (Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient;Ljava/lang/Number;Ljava/lang/Number;)V + public synthetic fun (Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient;Ljava/lang/Number;Ljava/lang/Number;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/Number; + public final fun component2 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient; public final fun component3 ()Ljava/lang/Number; - public final fun copy (Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Ljava/lang/String;Ljava/lang/Number;Ljava/lang/Number;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; + public final fun component4 ()Ljava/lang/Number; + public final fun copy (Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient;Ljava/lang/Number;Ljava/lang/Number;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient;Ljava/lang/Number;Ljava/lang/Number;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public final fun getBackgroundColor ()Ljava/lang/String; + public final fun getBackgroundGradient ()Lcom/datadog/android/sessionreplay/model/MobileSegment$BackgroundGradient; public final fun getCornerRadius ()Ljava/lang/Number; public final fun getOpacity ()Ljava/lang/Number; public fun hashCode ()I @@ -844,6 +1268,28 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Source$ public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Source; } +public final class com/datadog/android/sessionreplay/model/MobileSegment$StartPoint { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint$Companion; + public fun (Ljava/lang/Number;Ljava/lang/Number;)V + public final fun component1 ()Ljava/lang/Number; + public final fun component2 ()Ljava/lang/Number; + public final fun copy (Ljava/lang/Number;Ljava/lang/Number;)Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint;Ljava/lang/Number;Ljava/lang/Number;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; + public final fun getX ()Ljava/lang/Number; + public final fun getY ()Ljava/lang/Number; + public fun hashCode ()I + public final fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$StartPoint$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$StartPoint; +} + public final class com/datadog/android/sessionreplay/model/MobileSegment$TextPosition { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition$Companion; public fun ()V @@ -911,6 +1357,20 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Truncat public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$TruncationMode; } +public final class com/datadog/android/sessionreplay/model/MobileSegment$Type : java/lang/Enum { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Type$Companion; + public static final field LAYER Lcom/datadog/android/sessionreplay/model/MobileSegment$Type; + public static final field WIREFRAME Lcom/datadog/android/sessionreplay/model/MobileSegment$Type; + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Type; + public final fun toJson ()Lcom/google/gson/JsonElement; + public static fun valueOf (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Type; + public static fun values ()[Lcom/datadog/android/sessionreplay/model/MobileSegment$Type; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$Type$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Type; +} + public final class com/datadog/android/sessionreplay/model/MobileSegment$Vertical : java/lang/Enum { public static final field BOTTOM Lcom/datadog/android/sessionreplay/model/MobileSegment$Vertical; public static final field CENTER Lcom/datadog/android/sessionreplay/model/MobileSegment$Vertical; @@ -958,6 +1418,48 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun fromJsonElement (Lcom/google/gson/JsonElement;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; } +public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe : com/datadog/android/sessionreplay/model/MobileSegment$Wireframe { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe$Companion; + public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)V + public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()J + public final fun component10 ()Ljava/lang/Boolean; + public final fun component11 ()Ljava/lang/String; + public final fun component2 ()J + public final fun component3 ()J + public final fun component4 ()J + public final fun component5 ()J + public final fun component6 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; + public final fun component7 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; + public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; + public final fun component9 ()Ljava/lang/String; + public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; + public final fun getBorder ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; + public final fun getClip ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; + public final fun getHeight ()J + public final fun getId ()J + public final fun getPermanentId ()Ljava/lang/String; + public final fun getShapeStyle ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; + public final fun getSlotId ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public final fun getWidth ()J + public final fun getX ()J + public final fun getY ()J + public fun hashCode ()I + public final fun isVisible ()Ljava/lang/Boolean; + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; +} + public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe : com/datadog/android/sessionreplay/model/MobileSegment$Wireframe { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe$Companion; public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)V @@ -1210,6 +1712,46 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun fromJsonElement (Lcom/google/gson/JsonElement;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation; } +public final class com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate : com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation { + public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate$Companion; + public fun (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;)V + public synthetic fun (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()J + public final fun component10 ()Ljava/lang/Boolean; + public final fun component2 ()Ljava/lang/Long; + public final fun component3 ()Ljava/lang/Long; + public final fun component4 ()Ljava/lang/Long; + public final fun component5 ()Ljava/lang/Long; + public final fun component6 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; + public final fun component7 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; + public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; + public final fun component9 ()Ljava/lang/String; + public final fun copy (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate;JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; + public fun equals (Ljava/lang/Object;)Z + public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; + public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; + public final fun getBorder ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; + public final fun getClip ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; + public final fun getHeight ()Ljava/lang/Long; + public final fun getId ()J + public final fun getShapeStyle ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; + public final fun getSlotId ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public final fun getWidth ()Ljava/lang/Long; + public final fun getX ()Ljava/lang/Long; + public final fun getY ()Ljava/lang/Long; + public fun hashCode ()I + public final fun isVisible ()Ljava/lang/Boolean; + public fun toJson ()Lcom/google/gson/JsonElement; + public fun toString ()Ljava/lang/String; +} + +public final class com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate$Companion { + public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; + public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; +} + public final class com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$ImageWireframeUpdate : com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$ImageWireframeUpdate$Companion; public fun (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-brightness-bias-modifier-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-brightness-bias-modifier-schema.json new file mode 100644 index 0000000000..fc90620782 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-brightness-bias-modifier-schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-brightness-bias-modifier-schema.json", + "title": "CompositionLayerBrightnessBiasModifier", + "type": "object", + "description": "Adds a signed brightness bias to the rendered layer contents.", + "required": ["type", "value"], + "properties": { + "type": { + "type": "string", + "const": "brightnessBias", + "description": "The type of the modifier.", + "readOnly": true + }, + "value": { + "type": "number", + "minimum": -1, + "maximum": 1, + "description": "Brightness bias from -1 to 1 added to each normalized RGB channel (alpha is unchanged). 0 leaves content unchanged. Positive values brighten; negative values darken. Each channel is clamped to [0, 1] after the bias is applied.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-child-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-child-schema.json new file mode 100644 index 0000000000..a04ce6af42 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-child-schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-child-schema.json", + "title": "CompositionLayerChild", + "type": "object", + "description": "A reference to a child wireframe or child layer in a composition layer.", + "required": ["type", "id"], + "properties": { + "type": { + "type": "string", + "enum": ["wireframe", "layer"], + "description": "The type of the child reference.", + "readOnly": true + }, + "id": { + "type": "integer", + "description": "The id of the referenced wireframe or layer.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-clip-modifier-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-clip-modifier-schema.json new file mode 100644 index 0000000000..060d28c6ec --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-clip-modifier-schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-clip-modifier-schema.json", + "title": "CompositionLayerClipModifier", + "type": "object", + "description": "Geometric clipping applied to the composed layer output, in coordinates local to the layer rectangle.", + "required": ["type", "path"], + "properties": { + "type": { + "type": "string", + "const": "clip", + "description": "The type of the modifier.", + "readOnly": true + }, + "path": { + "type": "string", + "description": "SVG path string defining the clip region, in coordinates local to the layer rectangle.", + "readOnly": true + }, + "fillRule": { + "type": "string", + "enum": ["nonzero", "evenodd"], + "description": "Path fill rule. Defaults to 'nonzero'.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-color-matrix-modifier-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-color-matrix-modifier-schema.json new file mode 100644 index 0000000000..11c07daa85 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-color-matrix-modifier-schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-color-matrix-modifier-schema.json", + "title": "CompositionLayerColorMatrixModifier", + "type": "object", + "description": "Color transformation using a 4x5 matrix applied to the composed layer output.", + "required": ["type", "matrix"], + "properties": { + "type": { + "type": "string", + "const": "colorMatrix", + "description": "The type of the modifier.", + "readOnly": true + }, + "matrix": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 20, + "maxItems": 20, + "description": "4x5 color matrix encoded as 20 numbers in row-major order. Input and output color channels are normalized to [0, 1]. The transform for each output channel is: R' = m[0]*R + m[1]*G + m[2]*B + m[3]*A + m[4], G' = m[5]*R + m[6]*G + m[7]*B + m[8]*A + m[9], B' = m[10]*R + m[11]*G + m[12]*B + m[13]*A + m[14], A' = m[15]*R + m[16]*G + m[17]*B + m[18]*A + m[19]. Each output channel is clamped to [0, 1] after evaluation.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-gaussian-blur-modifier-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-gaussian-blur-modifier-schema.json new file mode 100644 index 0000000000..fe8e228f88 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-gaussian-blur-modifier-schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-gaussian-blur-modifier-schema.json", + "title": "CompositionLayerGaussianBlurModifier", + "type": "object", + "description": "Gaussian blur applied to the composed layer output.", + "required": ["type", "radius"], + "properties": { + "type": { + "type": "string", + "const": "gaussianBlur", + "description": "The type of the modifier.", + "readOnly": true + }, + "radius": { + "type": "number", + "minimum": 0, + "description": "Gaussian blur radius.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-mask-image-modifier-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-mask-image-modifier-schema.json new file mode 100644 index 0000000000..6983c75eb3 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-mask-image-modifier-schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-mask-image-modifier-schema.json", + "title": "CompositionLayerMaskImageModifier", + "type": "object", + "description": "Image mask applied to the composed layer output at this point in the modifier order. The referenced image is mapped to the layer bounds and interpreted as an alpha mask: transparent pixels hide content, opaque pixels keep content, and partial alpha multiplies content alpha. RGB channels are ignored.", + "required": ["type", "resourceId"], + "properties": { + "type": { + "type": "string", + "const": "maskImage", + "description": "The type of the modifier.", + "readOnly": true + }, + "resourceId": { + "type": "string", + "description": "Unique identifier of the image resource used as a bounds-aligned alpha mask.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-modifier-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-modifier-schema.json new file mode 100644 index 0000000000..69d374481f --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-modifier-schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-modifier-schema.json", + "title": "CompositionLayerModifier", + "type": "object", + "description": "A rendering modifier applied to the composed layer output.", + "oneOf": [ + { + "$ref": "composition-layer-clip-modifier-schema.json" + }, + { + "$ref": "composition-layer-opacity-modifier-schema.json" + }, + { + "$ref": "composition-layer-color-matrix-modifier-schema.json" + }, + { + "$ref": "composition-layer-gaussian-blur-modifier-schema.json" + }, + { + "$ref": "composition-layer-shadow-modifier-schema.json" + }, + { + "$ref": "composition-layer-brightness-bias-modifier-schema.json" + }, + { + "$ref": "composition-layer-saturate-modifier-schema.json" + }, + { + "$ref": "composition-layer-mask-image-modifier-schema.json" + } + ] +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-opacity-modifier-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-opacity-modifier-schema.json new file mode 100644 index 0000000000..7a91fd13cf --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-opacity-modifier-schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-opacity-modifier-schema.json", + "title": "CompositionLayerOpacityModifier", + "type": "object", + "description": "Opacity applied to the composed layer output at this point in the modifier order.", + "required": ["type", "value"], + "properties": { + "type": { + "type": "string", + "const": "opacity", + "description": "The type of the modifier.", + "readOnly": true + }, + "value": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Opacity value from 0 to 1.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-saturate-modifier-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-saturate-modifier-schema.json new file mode 100644 index 0000000000..f32ff713c9 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-saturate-modifier-schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-saturate-modifier-schema.json", + "title": "CompositionLayerSaturateModifier", + "type": "object", + "description": "Applies a saturation adjustment to the rendered layer contents.", + "required": ["type", "value"], + "properties": { + "type": { + "type": "string", + "const": "saturate", + "description": "The type of the modifier.", + "readOnly": true + }, + "value": { + "type": "number", + "minimum": 0, + "description": "Saturation multiplier. 1 leaves content unchanged. 0 removes saturation.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-schema.json new file mode 100644 index 0000000000..004cb47d8c --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-schema.json", + "title": "CompositionLayer", + "type": "object", + "description": "A rendering group that groups child wireframes and child layers. Does not draw pixels itself. Ordered rendering modifiers and compositing are applied to its composed output.", + "required": ["id", "x", "y", "width", "height", "children"], + "properties": { + "id": { + "type": "integer", + "description": "Stable layer identifier, persistent throughout the view lifetime.", + "readOnly": true + }, + "x": { + "type": "integer", + "description": "The position in pixels on the X axis of the layer in absolute coordinates. Uses the same coordinate space as mobile wireframes.", + "readOnly": true + }, + "y": { + "type": "integer", + "description": "The position in pixels on the Y axis of the layer in absolute coordinates. Uses the same coordinate space as mobile wireframes.", + "readOnly": true + }, + "width": { + "type": "integer", + "description": "The width in pixels of the layer. Uses the same coordinate space as mobile wireframes.", + "readOnly": true + }, + "height": { + "type": "integer", + "description": "The height in pixels of the layer. Uses the same coordinate space as mobile wireframes.", + "readOnly": true + }, + "children": { + "type": "array", + "items": { + "$ref": "composition-layer-child-schema.json" + }, + "description": "Ordered back-to-front references to child wireframes or child layers.", + "readOnly": true + }, + "modifiers": { + "type": "array", + "items": { + "$ref": "composition-layer-modifier-schema.json" + }, + "description": "Ordered list of rendering modifiers applied to the composed layer output in array order.", + "readOnly": true + }, + "compositeOperation": { + "type": "string", + "enum": ["sourceOver", "destinationIn", "destinationOut", "plusDarker"], + "description": "Operation used when compositing the rendered group into its parent.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-shadow-modifier-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-shadow-modifier-schema.json new file mode 100644 index 0000000000..addb7af767 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-shadow-modifier-schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-shadow-modifier-schema.json", + "title": "CompositionLayerShadowModifier", + "type": "object", + "description": "Drop shadow drawn behind the composed layer output.", + "required": ["type", "color", "offsetX", "offsetY", "radius"], + "properties": { + "type": { + "type": "string", + "const": "shadow", + "description": "The type of the modifier.", + "readOnly": true + }, + "color": { + "type": "string", + "pattern": "^#[A-Fa-f0-9]{6}([A-Fa-f0-9]{2})?$", + "description": "The shadow color as a String hexadecimal. Follows the #RRGGBBAA color format with the alpha value as optional. SDKs should encode the effective shadow alpha in this color and omit the shadow modifier when the effective alpha is 0.", + "readOnly": true + }, + "offsetX": { + "type": "number", + "description": "Horizontal shadow offset in pixels.", + "readOnly": true + }, + "offsetY": { + "type": "number", + "description": "Vertical shadow offset in pixels.", + "readOnly": true + }, + "radius": { + "type": "number", + "minimum": 0, + "description": "Blur radius used to create the shadow.", + "readOnly": true + }, + "path": { + "type": "string", + "description": "Optional SVG path string defining the shadow outline, in coordinates local to the layer rectangle. When present, the path is interpreted using the non-zero winding rule. When omitted, the shadow follows the composed layer alpha.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-update-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-update-schema.json new file mode 100644 index 0000000000..6af443467b --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-layer-update-schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-layer-update-schema.json", + "title": "CompositionLayerUpdate", + "type": "object", + "description": "Sparse update for a composition layer. Omitted fields are unchanged.", + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "description": "The id of the layer to update.", + "readOnly": true + }, + "x": { + "type": "integer", + "description": "Updated X position in absolute coordinates. Uses the same coordinate space as mobile wireframes.", + "readOnly": true + }, + "y": { + "type": "integer", + "description": "Updated Y position in absolute coordinates. Uses the same coordinate space as mobile wireframes.", + "readOnly": true + }, + "width": { + "type": "integer", + "description": "Updated width in pixels. Uses the same coordinate space as mobile wireframes.", + "readOnly": true + }, + "height": { + "type": "integer", + "description": "Updated height in pixels. Uses the same coordinate space as mobile wireframes.", + "readOnly": true + }, + "children": { + "type": "array", + "items": { + "$ref": "composition-layer-child-schema.json" + }, + "description": "When present, replaces the full child list for this layer.", + "readOnly": true + }, + "modifiers": { + "type": "array", + "items": { + "$ref": "composition-layer-modifier-schema.json" + }, + "description": "When present, replaces the full modifier list for this layer.", + "readOnly": true + }, + "compositeOperation": { + "type": "string", + "enum": ["sourceOver", "destinationIn", "destinationOut", "plusDarker"], + "description": "Updated composite operation for this layer.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-tree-mutation-data-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-tree-mutation-data-schema.json new file mode 100644 index 0000000000..f3acda23d0 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-tree-mutation-data-schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-tree-mutation-data-schema.json", + "title": "CompositionTreeMutationData", + "type": "object", + "description": "Mobile-specific. Incremental data carrying composition tree layer mutations.", + "required": ["source"], + "properties": { + "source": { + "type": "integer", + "const": 10, + "description": "The source of this type of incremental data.", + "readOnly": true + }, + "root": { + "$ref": "composition-layer-schema.json" + }, + "adds": { + "type": "array", + "items": { + "$ref": "composition-layer-schema.json" + }, + "description": "Full layer definitions for newly added layers.", + "readOnly": true + }, + "removes": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "Ids of layer definitions to remove. Removing a referenced layer also requires updating the parent or root child list.", + "readOnly": true + }, + "updates": { + "type": "array", + "items": { + "$ref": "composition-layer-update-schema.json" + }, + "description": "Sparse updates for existing layers.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-tree-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-tree-schema.json new file mode 100644 index 0000000000..1514557617 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/composition-tree-schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/composition-tree-schema.json", + "title": "CompositionTree", + "type": "object", + "description": "Optional composition tree describing the rendering hierarchy for a full snapshot. When present, the player uses this tree for rendering order and group operations instead of rendering the wireframes as a flat array.", + "required": ["root"], + "properties": { + "root": { + "$ref": "composition-layer-schema.json" + }, + "layers": { + "type": "array", + "items": { + "$ref": "composition-layer-schema.json" + }, + "description": "Non-root composition layers referenced by the tree.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-schema.json new file mode 100644 index 0000000000..f8ad37f617 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/embedded-content-wireframe-schema.json", + "title": "EmbeddedContentWireframe", + "type": "object", + "description": "Schema of all properties of an EmbeddedContentWireframe.", + "allOf": [ + { + "$ref": "_common-shape-wireframe-schema.json" + }, + { + "required": ["type", "slotId"], + "properties": { + "type": { + "type": "string", + "description": "The type of the wireframe.", + "const": "embedded_content", + "readOnly": true + }, + "slotId": { + "type": "string", + "description": "Unique Id of the slot containing this embedded content.", + "readOnly": true + }, + "isVisible": { + "type": "boolean", + "description": "Whether this embedded content is visible or not.", + "readOnly": true + }, + "permanentId": { + "type": "string", + "description": "A globally unique and stable identifier for this UI element, computed as the hash of the element's path. Used to correlate wireframes with RUM action events.", + "readOnly": true + } + } + } + ] +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-update-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-update-schema.json new file mode 100644 index 0000000000..6b0fa9b772 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-update-schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/embedded-content-wireframe-update-schema.json", + "title": "EmbeddedContentWireframeUpdate", + "type": "object", + "description": "Schema of all properties of an EmbeddedContentWireframeUpdate.", + "allOf": [ + { + "$ref": "_common-shape-wireframe-update-schema.json" + }, + { + "required": ["type", "slotId"], + "properties": { + "type": { + "type": "string", + "description": "The type of the wireframe.", + "const": "embedded_content", + "readOnly": true + }, + "slotId": { + "type": "string", + "description": "Unique Id of the slot containing this embedded content.", + "readOnly": true + }, + "isVisible": { + "type": "boolean", + "description": "Whether this embedded content is visible or not.", + "readOnly": true + } + } + } + ] +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/full-snapshot-record-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/full-snapshot-record-schema.json index 26f708d2b2..c6067f4a62 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/full-snapshot-record-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/full-snapshot-record-schema.json @@ -29,8 +29,18 @@ }, "description": "The Wireframes contained by this Record.", "readOnly": true + }, + "compositionTree": { + "$ref": "composition-tree-schema.json", + "description": "Optional composition tree describing the rendering hierarchy. When present, the player uses this tree for rendering order and group operations.", + "readOnly": true } } + }, + "slotId": { + "type": "string", + "description": "Unique ID of the slot that generated this record.", + "readOnly": true } } } diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/incremental-data-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/incremental-data-schema.json index eb24f66b35..68b5329143 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/incremental-data-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/incremental-data-schema.json @@ -16,6 +16,9 @@ }, { "$ref": "../common/pointer-interaction-data-schema.json" + }, + { + "$ref": "composition-tree-mutation-data-schema.json" } ] } diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/incremental-snapshot-record-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/incremental-snapshot-record-schema.json index 664e02708b..c3d814499b 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/incremental-snapshot-record-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/incremental-snapshot-record-schema.json @@ -19,6 +19,11 @@ }, "data": { "$ref": "incremental-data-schema.json" + }, + "slotId": { + "type": "string", + "description": "Unique ID of the slot that generated this record.", + "readOnly": true } } } diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-point-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-point-schema.json new file mode 100644 index 0000000000..5a05d455c0 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-point-schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/shape-gradient-point-schema.json", + "title": "ShapeGradientPoint", + "type": "object", + "description": "A point in the wireframe's normalized coordinate space. The top-left corner is (0, 0), the bottom-right corner is (1, 1), and values outside this range place the point outside the wireframe bounds.", + "required": ["x", "y"], + "properties": { + "x": { + "type": "number", + "description": "Horizontal position, where 0 is the left edge and 1 is the right edge.", + "readOnly": true + }, + "y": { + "type": "number", + "description": "Vertical position, where 0 is the top edge and 1 is the bottom edge.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-schema.json new file mode 100644 index 0000000000..832a9a1b53 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/shape-gradient-schema.json", + "title": "ShapeGradient", + "description": "A background gradient for a shape wireframe. When backgroundColor is also present, the background color is painted first and the gradient is painted over it. The gradient is clipped to the wireframe shape, the border is painted afterward, and shape opacity applies to the combined result.", + "oneOf": [ + { + "$ref": "shape-linear-gradient-schema.json" + } + ] +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-stop-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-stop-schema.json new file mode 100644 index 0000000000..bff891c27a --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-gradient-stop-schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/shape-gradient-stop-schema.json", + "title": "ShapeGradientStop", + "type": "object", + "description": "A color and its relative position in a shape gradient.", + "required": ["color", "position"], + "properties": { + "color": { + "type": "string", + "pattern": "^#[A-Fa-f0-9]{6}([A-Fa-f0-9]{2})?$", + "description": "The stop color as a hexadecimal string in #RRGGBB or #RRGGBBAA format.", + "readOnly": true + }, + "position": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Relative stop position between 0 and 1. Stops must be ordered by non-decreasing position.", + "readOnly": true + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-linear-gradient-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-linear-gradient-schema.json new file mode 100644 index 0000000000..e7b125fa4f --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-linear-gradient-schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "session-replay/mobile/shape-linear-gradient-schema.json", + "title": "ShapeLinearGradient", + "type": "object", + "description": "A linear background gradient for a shape wireframe. Colors before the first stop and after the last stop are clamped to the nearest stop color.", + "required": ["type", "stops", "startPoint", "endPoint"], + "properties": { + "type": { + "type": "string", + "const": "linear", + "description": "The type of the gradient.", + "readOnly": true + }, + "stops": { + "type": "array", + "items": { + "$ref": "shape-gradient-stop-schema.json" + }, + "minItems": 2, + "description": "Ordered gradient color stops. Positions must be non-decreasing.", + "readOnly": true + }, + "startPoint": { + "description": "The point where position 0 of the gradient is placed.", + "readOnly": true, + "allOf": [ + { + "$ref": "shape-gradient-point-schema.json" + } + ] + }, + "endPoint": { + "description": "The point where position 1 of the gradient is placed.", + "readOnly": true, + "allOf": [ + { + "$ref": "shape-gradient-point-schema.json" + } + ] + } + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-style-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-style-schema.json index 166267be44..4a34d8d807 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-style-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-style-schema.json @@ -13,6 +13,15 @@ "description": "The background color for this wireframe as a String hexadecimal. Follows the #RRGGBBAA color format with the alpha value as optional. The default value is #FFFFFF00.", "readOnly": true }, + "backgroundGradient": { + "description": "The background gradient for this wireframe.", + "readOnly": true, + "allOf": [ + { + "$ref": "shape-gradient-schema.json" + } + ] + }, "opacity": { "type": "number", "description": "The opacity of this wireframe. Takes values from 0 to 1, default value is 1.", diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-schema.json index b3ba3a763f..d8d7b0dbdc 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-schema.json @@ -19,6 +19,9 @@ }, { "$ref": "webview-wireframe-schema.json" + }, + { + "$ref": "embedded-content-wireframe-schema.json" } ] } diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-update-mutation-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-update-mutation-schema.json index 65f921351a..10f3eebb96 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-update-mutation-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-update-mutation-schema.json @@ -19,6 +19,9 @@ }, { "$ref": "webview-wireframe-update-schema.json" + }, + { + "$ref": "embedded-content-wireframe-update-schema.json" } ] } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundsUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundsUtils.kt index 7df20d312b..efba06a43b 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundsUtils.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundsUtils.kt @@ -12,6 +12,7 @@ internal object BoundsUtils { internal fun resolveBounds(wireframe: MobileSegment.Wireframe): WireframeBounds { return when (wireframe) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> wireframe.bounds() is MobileSegment.Wireframe.ShapeWireframe -> wireframe.bounds() is MobileSegment.Wireframe.TextWireframe -> wireframe.bounds() is MobileSegment.Wireframe.ImageWireframe -> wireframe.bounds() @@ -34,6 +35,10 @@ internal object BoundsUtils { return resolveBounds(x, y, width, height, clip) } + private fun MobileSegment.Wireframe.EmbeddedContentWireframe.bounds(): WireframeBounds { + return resolveBounds(x, y, width, height, clip) + } + private fun MobileSegment.Wireframe.TextWireframe.bounds(): WireframeBounds { return resolveBounds(x, y, width, height, clip) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/HeatmapWireframeExt.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/HeatmapWireframeExt.kt index af869514ed..85cd8a5fa1 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/HeatmapWireframeExt.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/HeatmapWireframeExt.kt @@ -10,6 +10,7 @@ import com.datadog.android.sessionreplay.model.MobileSegment internal fun MobileSegment.Wireframe.permanentId(): String? { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.permanentId is MobileSegment.Wireframe.ShapeWireframe -> this.permanentId is MobileSegment.Wireframe.TextWireframe -> this.permanentId is MobileSegment.Wireframe.ImageWireframe -> this.permanentId @@ -22,6 +23,7 @@ internal fun MobileSegment.Wireframe.copyWithPermanentId( permanentId: String? ): MobileSegment.Wireframe { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(permanentId = permanentId) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(permanentId = permanentId) is MobileSegment.Wireframe.TextWireframe -> this.copy(permanentId = permanentId) is MobileSegment.Wireframe.ImageWireframe -> this.copy(permanentId = permanentId) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt index 554f7e7070..7021b8370a 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt @@ -11,6 +11,7 @@ import com.datadog.android.sessionreplay.model.MobileSegment internal fun MobileSegment.Wireframe.copy(clip: MobileSegment.WireframeClip?): MobileSegment.Wireframe { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(clip = clip) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(clip = clip) is MobileSegment.Wireframe.TextWireframe -> this.copy(clip = clip) is MobileSegment.Wireframe.ImageWireframe -> this.copy(clip = clip) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolver.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolver.kt index 39c671c831..ba6272eef9 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolver.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolver.kt @@ -345,6 +345,31 @@ internal class MutationResolver(private val internalLogger: InternalLogger) { return mutation } + private fun resolveEmbeddedContentWireframeMutation( + prevWireframe: MobileSegment.Wireframe.EmbeddedContentWireframe, + currentWireframe: MobileSegment.Wireframe.EmbeddedContentWireframe + ): MobileSegment.WireframeUpdateMutation { + var mutation = MobileSegment.WireframeUpdateMutation.EmbeddedContentWireframeUpdate( + id = currentWireframe.id, + slotId = currentWireframe.slotId + ) + if (prevWireframe.x != currentWireframe.x) mutation = mutation.copy(x = currentWireframe.x) + if (prevWireframe.y != currentWireframe.y) mutation = mutation.copy(y = currentWireframe.y) + if (prevWireframe.width != currentWireframe.width) mutation = mutation.copy(width = currentWireframe.width) + if (prevWireframe.height != currentWireframe.height) mutation = mutation.copy(height = currentWireframe.height) + if (prevWireframe.border != currentWireframe.border) mutation = mutation.copy(border = currentWireframe.border) + if (prevWireframe.shapeStyle != currentWireframe.shapeStyle) { + mutation = mutation.copy(shapeStyle = currentWireframe.shapeStyle) + } + if (prevWireframe.clip != currentWireframe.clip) { + mutation = mutation.copy(clip = currentWireframe.clip ?: MobileSegment.WireframeClip(0, 0, 0, 0)) + } + if (prevWireframe.isVisible != currentWireframe.isVisible) { + mutation = mutation.copy(isVisible = currentWireframe.isVisible) + } + return mutation + } + private fun recordChangedWireframeMutations( oldElement: MobileSegment.Wireframe, newElement: MobileSegment.Wireframe, @@ -390,6 +415,12 @@ internal class MutationResolver(private val internalLogger: InternalLogger) { null } else { when (prevWireframe) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> + resolveEmbeddedContentWireframeMutation( + prevWireframe, + currentWireframe as MobileSegment.Wireframe.EmbeddedContentWireframe + ) + is MobileSegment.Wireframe.TextWireframe -> resolveTextMutation( prevWireframe, currentWireframe as MobileSegment.Wireframe.TextWireframe @@ -467,6 +498,7 @@ internal class MutationResolver(private val internalLogger: InternalLogger) { @Suppress("FunctionMinLength") private fun MobileSegment.Wireframe.id(): Long { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.id is MobileSegment.Wireframe.ShapeWireframe -> this.id is MobileSegment.Wireframe.TextWireframe -> this.id is MobileSegment.Wireframe.ImageWireframe -> this.id 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 65fd6adef4..807f1ce860 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 @@ -142,16 +142,16 @@ internal class RecordedDataProcessor( if (fullSnapshotRequired) { records.add( MobileSegment.MobileRecord.MobileFullSnapshotRecord( - timestamp, - MobileSegment.Data(wireframes) + timestamp = timestamp, + data = MobileSegment.Data(wireframes) ) ) } else { mutationResolver.resolveMutations(prevSnapshot, wireframes)?.let { records.add( MobileSegment.MobileRecord.MobileIncrementalSnapshotRecord( - timestamp, - it + timestamp = timestamp, + data = it ) ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtils.kt index d0460c4f28..c9b2a81267 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtils.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtils.kt @@ -73,6 +73,7 @@ internal class WireframeUtils(private val boundsUtils: BoundsUtils = BoundsUtils private fun MobileSegment.Wireframe.clip(): MobileSegment.WireframeClip? { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.clip is MobileSegment.Wireframe.ShapeWireframe -> this.clip is MobileSegment.Wireframe.TextWireframe -> this.clip is MobileSegment.Wireframe.ImageWireframe -> this.clip diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapper.kt index 685b8da430..40f6555fb0 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/RadioButtonMapper.kt @@ -38,7 +38,7 @@ internal open class RadioButtonMapper( override fun resolveNotCheckedShapeStyle(view: RadioButton, checkBoxColor: String): MobileSegment.ShapeStyle? { return MobileSegment.ShapeStyle( backgroundColor = null, - view.alpha, + opacity = view.alpha, cornerRadius = CORNER_RADIUS ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt index c25c7ce16d..55f3219275 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt @@ -224,7 +224,7 @@ internal open class SwitchCompatMapper( private fun resolveTrackShapeStyle(view: SwitchCompat, checkBoxColor: String): MobileSegment.ShapeStyle { return MobileSegment.ShapeStyle( backgroundColor = checkBoxColor, - view.alpha + opacity = view.alpha ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExt.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExt.kt index 7aeef128f7..2c5d67261b 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExt.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExt.kt @@ -10,6 +10,7 @@ import com.datadog.android.sessionreplay.model.MobileSegment internal fun MobileSegment.Wireframe.hasOpaqueBackground(): Boolean { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.shapeStyle.isOpaque() // we return false from ImageWireframe because we don't know if the image is opaque or not // and ImageWireframe ShapeStyle is always null is MobileSegment.Wireframe.ImageWireframe -> false @@ -34,6 +35,7 @@ private fun MobileSegment.ShapeStyle?.isOpaque(): Boolean { internal fun MobileSegment.Wireframe.shapeStyle(): MobileSegment.ShapeStyle? { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.shapeStyle is MobileSegment.Wireframe.TextWireframe -> this.shapeStyle is MobileSegment.Wireframe.ShapeWireframe -> this.shapeStyle is MobileSegment.Wireframe.ImageWireframe -> this.shapeStyle @@ -44,6 +46,7 @@ internal fun MobileSegment.Wireframe.shapeStyle(): MobileSegment.ShapeStyle? { internal fun MobileSegment.Wireframe.copy(shapeStyle: MobileSegment.ShapeStyle?): MobileSegment.Wireframe { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.TextWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.ImageWireframe -> this.copy(shapeStyle = shapeStyle) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper.kt index aff15ffee3..c4b9d6575f 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper.kt @@ -52,7 +52,10 @@ abstract class BaseWireframeMapper( ): MobileSegment.ShapeStyle? { val color = drawableToColorMapper.mapDrawableToColor(drawable, internalLogger) return if (color != null) { - MobileSegment.ShapeStyle(colorStringFormatter.formatColorAsHexString(color), viewAlpha) + MobileSegment.ShapeStyle( + backgroundColor = colorStringFormatter.formatColorAsHexString(color), + opacity = viewAlpha + ) } else { null } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/EmbeddedContentWireframeForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/EmbeddedContentWireframeForgeryFactory.kt new file mode 100644 index 0000000000..7afb6ac551 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/EmbeddedContentWireframeForgeryFactory.kt @@ -0,0 +1,26 @@ +/* + * 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.model.MobileSegment +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.ForgeryFactory + +internal class EmbeddedContentWireframeForgeryFactory : + ForgeryFactory { + + override fun getForgery(forge: Forge): MobileSegment.Wireframe.EmbeddedContentWireframe { + return MobileSegment.Wireframe.EmbeddedContentWireframe( + id = forge.aLong(), + x = forge.aLong(), + y = forge.aLong(), + width = forge.aPositiveLong(), + height = forge.aPositiveLong(), + slotId = forge.anAlphabeticalString() + ) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/ForgeConfigurator.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/ForgeConfigurator.kt index 996433a772..0ca3b52189 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/ForgeConfigurator.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/ForgeConfigurator.kt @@ -25,6 +25,7 @@ internal class ForgeConfigurator : BaseConfigurator() { forge.addFactory(SessionReplayRumContextForgeryFactory()) forge.addFactory(ShapeWireframeForgeryFactory()) forge.addFactory(TextWireframeForgeryFactory()) + forge.addFactory(EmbeddedContentWireframeForgeryFactory()) forge.addFactory(WireframeForgeryFactory()) forge.addFactory(TextWireframeMutationForgeryFactory()) forge.addFactory(ShapeWireframeMutationForgeryFactory()) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/WireframeForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/WireframeForgeryFactory.kt index 228d8bfcc9..3318f8d7c9 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/WireframeForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/WireframeForgeryFactory.kt @@ -12,10 +12,12 @@ import fr.xgouchet.elmyr.ForgeryFactory internal class WireframeForgeryFactory : ForgeryFactory { override fun getForgery(forge: Forge): MobileSegment.Wireframe { - return when (forge.anInt(min = 0, max = 4)) { + return when (forge.anInt(min = 0, max = 5)) { 0 -> forge.getForgery() 1 -> forge.getForgery() 2 -> forge.getForgery() + 3 -> forge.getForgery() + 4 -> forge.getForgery() else -> forge.getForgery() } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundUtilsTest.kt index be0c8a62ce..c097edb922 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundUtilsTest.kt @@ -143,6 +143,7 @@ internal class BoundUtilsTest { private fun MobileSegment.Wireframe.x(): Long { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.x is MobileSegment.Wireframe.ShapeWireframe -> this.x is MobileSegment.Wireframe.TextWireframe -> this.x is MobileSegment.Wireframe.ImageWireframe -> this.x @@ -153,6 +154,7 @@ internal class BoundUtilsTest { private fun MobileSegment.Wireframe.y(): Long { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.y is MobileSegment.Wireframe.ShapeWireframe -> this.y is MobileSegment.Wireframe.TextWireframe -> this.y is MobileSegment.Wireframe.ImageWireframe -> this.y @@ -163,6 +165,7 @@ internal class BoundUtilsTest { private fun MobileSegment.Wireframe.width(): Long { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.width is MobileSegment.Wireframe.ShapeWireframe -> this.width is MobileSegment.Wireframe.TextWireframe -> this.width is MobileSegment.Wireframe.ImageWireframe -> this.width @@ -173,6 +176,7 @@ internal class BoundUtilsTest { private fun MobileSegment.Wireframe.height(): Long { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.height is MobileSegment.Wireframe.ShapeWireframe -> this.height is MobileSegment.Wireframe.TextWireframe -> this.height is MobileSegment.Wireframe.ImageWireframe -> this.height @@ -189,6 +193,8 @@ internal class BoundUtilsTest { height: Long ): MobileSegment.Wireframe { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> + this.copy(clip = clip, x = x, y = y, width = width, height = height) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(clip = clip, x = x, y = y, width = width, height = height) is MobileSegment.Wireframe.TextWireframe -> diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt index 9d2379d6bd..4796011f14 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt @@ -168,8 +168,11 @@ internal class MutationResolverTest { @Test fun `M identify the removed wireframes W resolveMutations`(forge: Forge) { // Given + // WebviewWireframe is excluded: a removed WebviewWireframe is resolved as a + // visibility update rather than a Remove (see the dedicated webview tests below), + // which this test isn't exercising. val fakePrevSnapshot = forge.aList(size = forge.anInt(min = 2, max = 10)) { - forge.getForgery(MobileSegment.Wireframe::class.java) + forge.nonWebViewWireframe() } val fakeRemovedSize = forge.anInt(min = 1, max = fakePrevSnapshot.size) val fakeCurrentSnapshot = fakePrevSnapshot.drop(fakeRemovedSize) @@ -772,6 +775,7 @@ internal class MutationResolverTest { private fun MobileSegment.Wireframe.id(): Long { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.id is MobileSegment.Wireframe.ShapeWireframe -> this.id is MobileSegment.Wireframe.TextWireframe -> this.id is MobileSegment.Wireframe.ImageWireframe -> this.id @@ -780,6 +784,14 @@ internal class MutationResolverTest { } } + private fun Forge.nonWebViewWireframe(): MobileSegment.Wireframe { + var wireframe: MobileSegment.Wireframe + do { + wireframe = getForgery(MobileSegment.Wireframe::class.java) + } while (wireframe is MobileSegment.Wireframe.WebviewWireframe) + return wireframe + } + // endregion data class MutationTestData( @@ -793,6 +805,7 @@ internal class MutationResolverTest { private fun MobileSegment.Wireframe.id(): Long { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.id is MobileSegment.Wireframe.ShapeWireframe -> this.id is MobileSegment.Wireframe.TextWireframe -> this.id is MobileSegment.Wireframe.ImageWireframe -> this.id diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/NodeFlattenerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/NodeFlattenerTest.kt index 3da3465a17..374a627054 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/NodeFlattenerTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/NodeFlattenerTest.kt @@ -322,6 +322,7 @@ internal class NodeFlattenerTest { private fun MobileSegment.Wireframe.copy(id: Long): MobileSegment.Wireframe { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(id = id) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(id = id) is MobileSegment.Wireframe.TextWireframe -> @@ -371,6 +372,14 @@ internal class NodeFlattenerTest { private fun Forge.forgeWireframe(id: Long, x: Long, y: Long, width: Long, height: Long): MobileSegment.Wireframe { return when (val fakeWireframe = getForgery()) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> fakeWireframe.copy( + id = id, + x = x, + y = y, + width = width, + height = height, + clip = null + ) is MobileSegment.Wireframe.ShapeWireframe -> fakeWireframe.copy( id = id, x = x, @@ -416,6 +425,8 @@ internal class NodeFlattenerTest { private fun MobileSegment.Wireframe.bounds(): Bounds { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> + Bounds(this.x, this.y, this.width, this.height) is MobileSegment.Wireframe.ShapeWireframe -> Bounds(this.x, this.y, this.width, this.height) is MobileSegment.Wireframe.TextWireframe -> @@ -431,6 +442,7 @@ internal class NodeFlattenerTest { private fun MobileSegment.Wireframe.clip(): MobileSegment.WireframeClip? { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> clip is MobileSegment.Wireframe.ShapeWireframe -> clip is MobileSegment.Wireframe.TextWireframe -> @@ -448,6 +460,7 @@ internal class NodeFlattenerTest { private fun MobileSegment.Wireframe.id(): Long { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.id is MobileSegment.Wireframe.ShapeWireframe -> this.id is MobileSegment.Wireframe.TextWireframe -> this.id is MobileSegment.Wireframe.ImageWireframe -> this.id diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt index 14f028df43..e78892d5cc 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt @@ -689,6 +689,7 @@ internal class WireframeUtilsTest { private fun MobileSegment.Wireframe.clip(): MobileSegment.WireframeClip? { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> clip?.normalized() is MobileSegment.Wireframe.ShapeWireframe -> clip?.normalized() is MobileSegment.Wireframe.TextWireframe -> clip?.normalized() is MobileSegment.Wireframe.ImageWireframe -> clip?.normalized() @@ -722,6 +723,10 @@ internal class WireframeUtilsTest { shapeStyle: MobileSegment.ShapeStyle? ): MobileSegment.Wireframe { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> copy( + shapeStyle = shapeStyle + ) + is MobileSegment.Wireframe.ShapeWireframe -> copy( shapeStyle = shapeStyle ) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt index b93ef068a9..b2ac15fb8a 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapperTest.kt @@ -70,7 +70,7 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { border = null, shapeStyle = MobileSegment.ShapeStyle( backgroundColor = fakeCurrentTextColorString, - mockSwitch.alpha + opacity = mockSwitch.alpha ) ) @@ -143,7 +143,7 @@ internal class SwitchCompatMapperTest : BaseSwitchCompatMapperTest() { border = null, shapeStyle = MobileSegment.ShapeStyle( backgroundColor = fakeCurrentTextColorString, - mockSwitch.alpha + opacity = mockSwitch.alpha ) ) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExtTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExtTest.kt index de2e50290f..ec5b1ba048 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExtTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExtTest.kt @@ -202,6 +202,7 @@ internal class WireframeExtTest { private fun MobileSegment.Wireframe.testCopy(shapeStyle: MobileSegment.ShapeStyle?): MobileSegment.Wireframe { return when (this) { + is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.TextWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.ImageWireframe -> this.copy(shapeStyle = shapeStyle) From adc5eeefbcb336dac9f551949172336f53df5f7a Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:16:41 +0300 Subject: [PATCH 2/7] PANA-8500: Add composition capture contracts --- .../internal/composition/CapturedIdentity.kt | 259 ++++++++++++++++++ .../internal/composition/CapturedMutation.kt | 33 +++ .../internal/composition/CapturedTree.kt | 241 ++++++++++++++++ .../composition/CapturedContractsTest.kt | 72 +++++ .../composition/CapturedIdentityTest.kt | 222 +++++++++++++++ .../composition/CompositionTestTree.kt | 59 ++++ 6 files changed, 886 insertions(+) create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutation.kt create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTree.kt create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedContractsTest.kt create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionTestTree.kt diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt new file mode 100644 index 0000000000..bec16028db --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt @@ -0,0 +1,259 @@ +/* + * 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.composition + +internal data class RumViewIdentityScope(val value: String) + +internal enum class CapturedIdentityKind { + SCREEN_ROOT, + WINDOW, + VIEW, + COMPOSE_HOST, + COMPOSE_NODE, + LAYER, + WIREFRAME +} + +/** Matches the wireframe identifier namespaces used by the iOS Core Animation pipeline. */ +internal enum class CapturedWireframeKind(internal val wireIdNamespace: Long) { + SHAPE(1), + TEXT(2), + IMAGE(IMAGE_WIREFRAME_NAMESPACE), + PLACEHOLDER(PLACEHOLDER_WIREFRAME_NAMESPACE), + EMBEDDED_CONTENT(EMBEDDED_CONTENT_WIREFRAME_NAMESPACE), + + /** + * Deliberately namespace `0`, i.e. unshifted: the wire id for a web-view wireframe must equal + * its `slotId` verbatim (not `slotId` shifted into a namespace), matching the pre-existing + * `id == slotId` contract the legacy recorder and iOS both rely on for WebView/JS-bridge + * correlation (see [CapturedIdentityFactory.webViewWireframe]). `slotId` is caller-supplied and, + * once wired to the real view-identity resolver, may be any `Int`-range value (including + * negative), so this is the one identity kind not structurally guaranteed collision-free + * against the rest of the tree. [DefaultCapturedIdentityFactory] offsets every non-wireframe + * layer id by [LAYER_ID_OFFSET] specifically so that raw layer ids can never land in that same + * `Int` range; a residual, validation-caught collision remains possible only between two + * web-view wireframes with a colliding `slotId` in the same view scope. + */ + WEB_VIEW(0) +} + +internal data class CapturedIdentity internal constructor( + val scope: RumViewIdentityScope, + val kind: CapturedIdentityKind, + val wireframeKind: CapturedWireframeKind?, + val namespace: List, + val localId: String, + val wireId: Long +) + +internal fun interface CapturedReplayIdGenerator { + fun next(): Long +} + +internal class AutoIncrementingCapturedReplayIdGenerator( + initialId: Long = 0 +) : CapturedReplayIdGenerator { + private var currentId = initialId + + @Synchronized + override fun next(): Long { + val replayId = currentId + currentId = if (currentId < Int.MAX_VALUE) currentId + 1 else 0 + return replayId + } +} + +@Suppress("TooManyFunctions") // Each function creates one supported capture identity type. +internal interface CapturedIdentityFactory { + val scope: RumViewIdentityScope + + fun screenRoot(): CapturedIdentity + + fun window(windowId: String): CapturedIdentity + + fun view(window: CapturedIdentity, viewId: String): CapturedIdentity + + fun composeHost(window: CapturedIdentity, hostId: String): CapturedIdentity + + fun composeNode(host: CapturedIdentity, nodeId: String): CapturedIdentity + + fun layer(owner: CapturedIdentity, layerId: String): CapturedIdentity + + fun shapeWireframe(owner: CapturedIdentity): CapturedIdentity + + fun textWireframe(owner: CapturedIdentity): CapturedIdentity + + fun imageWireframe(owner: CapturedIdentity): CapturedIdentity + + fun placeholderWireframe(owner: CapturedIdentity): CapturedIdentity + + fun embeddedContentWireframe(owner: CapturedIdentity): CapturedIdentity + + fun webViewWireframe(owner: CapturedIdentity, slotId: Long): CapturedIdentity +} + +internal class DefaultCapturedIdentityFactory( + override val scope: RumViewIdentityScope, + private val replayIdGenerator: CapturedReplayIdGenerator = SHARED_REPLAY_ID_GENERATOR +) : CapturedIdentityFactory { + + private val identities = mutableMapOf() + + override fun screenRoot(): CapturedIdentity = + createLayerIdentity(CapturedIdentityKind.SCREEN_ROOT, emptyList(), SCREEN_ROOT_LOCAL_ID) + + override fun window(windowId: String): CapturedIdentity = + createLayerIdentity(CapturedIdentityKind.WINDOW, emptyList(), windowId) + + override fun view(window: CapturedIdentity, viewId: String): CapturedIdentity { + requireOwner(window, CapturedIdentityKind.WINDOW) + return createLayerIdentity(CapturedIdentityKind.VIEW, window.path(), viewId) + } + + override fun composeHost(window: CapturedIdentity, hostId: String): CapturedIdentity { + requireOwner(window, CapturedIdentityKind.WINDOW) + return createLayerIdentity(CapturedIdentityKind.COMPOSE_HOST, window.path(), hostId) + } + + override fun composeNode(host: CapturedIdentity, nodeId: String): CapturedIdentity { + requireOwner(host, CapturedIdentityKind.COMPOSE_HOST) + return createLayerIdentity(CapturedIdentityKind.COMPOSE_NODE, host.path(), nodeId) + } + + override fun layer(owner: CapturedIdentity, layerId: String): CapturedIdentity { + requireLayerOwner(owner) + return createLayerIdentity(CapturedIdentityKind.LAYER, owner.path(), layerId) + } + + override fun shapeWireframe(owner: CapturedIdentity): CapturedIdentity = + createNamespacedWireframeIdentity(owner, CapturedWireframeKind.SHAPE) + + override fun textWireframe(owner: CapturedIdentity): CapturedIdentity = + createNamespacedWireframeIdentity(owner, CapturedWireframeKind.TEXT) + + override fun imageWireframe(owner: CapturedIdentity): CapturedIdentity = + createNamespacedWireframeIdentity(owner, CapturedWireframeKind.IMAGE) + + override fun placeholderWireframe(owner: CapturedIdentity): CapturedIdentity = + createNamespacedWireframeIdentity(owner, CapturedWireframeKind.PLACEHOLDER) + + override fun embeddedContentWireframe(owner: CapturedIdentity): CapturedIdentity = + createNamespacedWireframeIdentity(owner, CapturedWireframeKind.EMBEDDED_CONTENT) + + override fun webViewWireframe(owner: CapturedIdentity, slotId: Long): CapturedIdentity { + requireLayerOwner(owner) + return createIdentity( + kind = CapturedIdentityKind.WIREFRAME, + wireframeKind = CapturedWireframeKind.WEB_VIEW, + namespace = owner.path(), + localId = slotId.toString(), + wireId = slotId + ) + } + + private fun createLayerIdentity( + kind: CapturedIdentityKind, + namespace: List, + localId: String + ): CapturedIdentity { + val key = IdentityKey(kind, null, namespace.toList(), localId) + identities[key]?.let { return it } + // Offset (not namespace-shifted) so raw layer ids can never collide with an unshifted + // web-view slotId, which is caller-supplied and may fall anywhere in the Int range - while + // staying below 1L shl 32 so createNamespacedWireframeIdentity's `... or owner.wireId` still + // composes cleanly against a namespace shifted into the bits above it. See + // CapturedWireframeKind.WEB_VIEW. + return createIdentity(key, LAYER_ID_OFFSET + replayIdGenerator.next()) + } + + private fun createNamespacedWireframeIdentity( + owner: CapturedIdentity, + wireframeKind: CapturedWireframeKind + ): CapturedIdentity { + requireLayerOwner(owner) + return createIdentity( + kind = CapturedIdentityKind.WIREFRAME, + wireframeKind = wireframeKind, + namespace = owner.path(), + localId = wireframeKind.name, + wireId = (wireframeKind.wireIdNamespace shl NAMESPACE_SHIFT) or owner.wireId + ) + } + + private fun createIdentity( + kind: CapturedIdentityKind, + wireframeKind: CapturedWireframeKind, + namespace: List, + localId: String, + wireId: Long + ): CapturedIdentity = createIdentity( + IdentityKey(kind, wireframeKind, namespace.toList(), localId), + wireId + ) + + private fun createIdentity(key: IdentityKey, wireId: Long): CapturedIdentity { + identities[key]?.let { return it } + return CapturedIdentity( + scope = scope, + kind = key.kind, + wireframeKind = key.wireframeKind, + namespace = key.namespace, + localId = key.localId, + wireId = wireId + ).also { identities[key] = it } + } + + @Suppress("RequireInternal", "UnsafeThirdPartyFunctionCall") + private fun requireLayerOwner(owner: CapturedIdentity) { + requireOwner(owner) + require(owner.kind != CapturedIdentityKind.WIREFRAME) { + "Identity owner must be a captured layer." + } + } + + // These checks guard programmer-only misuse of the internal identity factory. + @Suppress("RequireInternal", "UnsafeThirdPartyFunctionCall") + private fun requireOwner( + owner: CapturedIdentity, + expectedKind: CapturedIdentityKind? = null + ) { + require(owner.scope == scope) { "Identity owner belongs to a different RUM view scope." } + require(expectedKind == null || owner.kind == expectedKind) { + "Identity owner must be a $expectedKind." + } + } + + private fun CapturedIdentity.path(): List = + namespace + kind.name + localId + + private data class IdentityKey( + val kind: CapturedIdentityKind, + val wireframeKind: CapturedWireframeKind?, + val namespace: List, + val localId: String + ) + + private companion object { + const val SCREEN_ROOT_LOCAL_ID = "screen" + const val NAMESPACE_SHIFT = 32 + val SHARED_REPLAY_ID_GENERATOR = AutoIncrementingCapturedReplayIdGenerator() + } +} + +private const val IMAGE_WIREFRAME_NAMESPACE = 3L +private const val PLACEHOLDER_WIREFRAME_NAMESPACE = 4L +private const val EMBEDDED_CONTENT_WIREFRAME_NAMESPACE = 5L + +/** + * Added to every raw layer replay id so that layer wire ids always land above the Int range + * (`[1L shl 31, (1L shl 32) - 1]` given the generator wraps at [Int.MAX_VALUE]) - outside the range + * an unshifted web-view slotId can occupy - while staying below `1L shl 32`, so + * [DefaultCapturedIdentityFactory.createNamespacedWireframeIdentity]'s + * `(wireframeKind.wireIdNamespace shl 32) or owner.wireId` still composes cleanly against a layer + * owner. See [CapturedWireframeKind.WEB_VIEW]. + */ +internal const val LAYER_ID_OFFSET = 1L shl 31 diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutation.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutation.kt new file mode 100644 index 0000000000..a17a6a8e6b --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutation.kt @@ -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.composition + +internal sealed interface CapturedChange { + object Unchanged : CapturedChange + + data class Set(val value: T) : CapturedChange +} + +internal data class CapturedLayerUpdate( + val identity: CapturedIdentity, + val x: CapturedChange = CapturedChange.Unchanged, + val y: CapturedChange = CapturedChange.Unchanged, + val width: CapturedChange = CapturedChange.Unchanged, + val height: CapturedChange = CapturedChange.Unchanged, + val children: CapturedChange> = CapturedChange.Unchanged, + val modifiers: CapturedChange> = CapturedChange.Unchanged, + val compositeOperation: CapturedChange = CapturedChange.Unchanged +) + +internal data class CapturedMutationSet( + val timestamp: Long, + val scope: RumViewIdentityScope, + val root: CapturedChange = CapturedChange.Unchanged, + val adds: CapturedChange> = CapturedChange.Unchanged, + val removes: CapturedChange> = CapturedChange.Unchanged, + val updates: CapturedChange> = CapturedChange.Unchanged +) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTree.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTree.kt new file mode 100644 index 0000000000..0031c3ceaf --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTree.kt @@ -0,0 +1,241 @@ +/* + * 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.composition + +internal fun interface CapturedSnapshotProducer { + fun capture(): CapturedFullSnapshot? +} + +internal data class CapturedBounds( + val x: Long, + val y: Long, + val width: Long, + val height: Long +) + +internal enum class CapturedLayerKind { + SYNTHETIC_SCREEN_ROOT, + WINDOW_ROOT, + NATIVE_VIEW, + COMPOSE_HOST, + COMPOSE_NODE, + COMPOSITION_LAYER +} + +internal data class CapturedLayer( + val identity: CapturedIdentity, + val kind: CapturedLayerKind, + val bounds: CapturedBounds, + val children: List, + val modifiers: List = emptyList(), + val compositeOperation: CapturedCompositeOperation? = null +) + +internal sealed interface CapturedChild { + val identity: CapturedIdentity + + data class Layer( + override val identity: CapturedIdentity + ) : CapturedChild + + data class Wireframe( + override val identity: CapturedIdentity + ) : CapturedChild +} + +internal sealed interface CapturedWireframe { + val identity: CapturedIdentity + val bounds: CapturedBounds + val clip: CapturedClip? + val permanentId: String? + + data class Shape( + override val identity: CapturedIdentity, + override val bounds: CapturedBounds, + override val clip: CapturedClip? = null, + val style: CapturedShapeStyle? = null, + val border: CapturedShapeBorder? = null, + override val permanentId: String? = null + ) : CapturedWireframe + + data class Text( + override val identity: CapturedIdentity, + override val bounds: CapturedBounds, + override val clip: CapturedClip? = null, + val style: CapturedShapeStyle? = null, + val border: CapturedShapeBorder? = null, + val text: String, + val textStyle: CapturedTextStyle, + val textPosition: CapturedTextPosition? = null, + override val permanentId: String? = null + ) : CapturedWireframe + + // No separate slotId field: the identity produced by CapturedIdentityFactory.webViewWireframe + // already carries the slot id verbatim as its wire id, so the wire mapper derives slotId from + // `identity.wireId` directly. A separate field would let the two silently drift apart, breaking + // the id == slotId JS-bridge correlation contract (see CapturedWireframeKind.WEB_VIEW). + data class WebView( + override val identity: CapturedIdentity, + override val bounds: CapturedBounds, + override val clip: CapturedClip? = null, + val style: CapturedShapeStyle? = null, + val border: CapturedShapeBorder? = null, + val isVisible: Boolean? = null, + override val permanentId: String? = null + ) : CapturedWireframe + + data class EmbeddedContent( + override val identity: CapturedIdentity, + override val bounds: CapturedBounds, + override val clip: CapturedClip? = null, + val style: CapturedShapeStyle? = null, + val border: CapturedShapeBorder? = null, + val slotId: String, + val isVisible: Boolean? = null, + override val permanentId: String? = null + ) : CapturedWireframe + + data class Pixel( + override val identity: CapturedIdentity, + override val bounds: CapturedBounds, + override val clip: CapturedClip? = null, + val style: CapturedShapeStyle? = null, + val border: CapturedShapeBorder? = null, + val resource: PixelResource, + val isEmpty: Boolean? = null, + override val permanentId: String? = null + ) : CapturedWireframe + + data class PrivacyPlaceholder( + override val identity: CapturedIdentity, + override val bounds: CapturedBounds, + override val clip: CapturedClip? = null, + val label: String? = null, + override val permanentId: String? = null + ) : CapturedWireframe +} + +internal sealed interface PixelResource { + data class Resolved( + val resourceId: String, + val mimeType: String? = null + ) : PixelResource + + object Unresolved : PixelResource +} + +internal data class CapturedClip( + val top: Long? = null, + val bottom: Long? = null, + val left: Long? = null, + val right: Long? = null +) + +internal data class CapturedShapeStyle( + val backgroundColor: String? = null, + val backgroundGradient: CapturedBackgroundGradient? = null, + val opacity: Number? = null, + val cornerRadius: Number? = null +) + +internal data class CapturedBackgroundGradient( + val stops: List, + val startPoint: CapturedPoint, + val endPoint: CapturedPoint +) + +internal data class CapturedGradientStop( + val color: String, + val offset: Double +) + +internal data class CapturedPoint( + val x: Double, + val y: Double +) + +internal data class CapturedShapeBorder( + val color: String, + val width: Long +) + +internal data class CapturedTextStyle( + val family: String, + val size: Long, + val color: String, + val truncationMode: CapturedTruncationMode? = null +) + +internal enum class CapturedTruncationMode { CLIP, HEAD, TAIL, MIDDLE } + +internal data class CapturedTextPosition( + val padding: CapturedPadding? = null, + val alignment: CapturedAlignment? = null +) + +internal data class CapturedPadding( + val top: Long? = null, + val bottom: Long? = null, + val left: Long? = null, + val right: Long? = null +) + +internal data class CapturedAlignment( + val horizontal: CapturedHorizontalAlignment? = null, + val vertical: CapturedVerticalAlignment? = null +) + +internal enum class CapturedHorizontalAlignment { LEFT, CENTER, RIGHT } + +internal enum class CapturedVerticalAlignment { TOP, CENTER, BOTTOM } + +internal sealed interface CapturedModifier { + data class Clip( + val path: String, + val fillRule: CapturedFillRule? = null + ) : CapturedModifier + + data class Opacity(val value: Double) : CapturedModifier + + data class ColorMatrix(val values: List) : CapturedModifier + + data class GaussianBlur(val radius: Double) : CapturedModifier + + data class Shadow( + val color: String, + val offsetX: Double, + val offsetY: Double, + val radius: Double, + val path: String? = null + ) : CapturedModifier + + data class BrightnessBias(val value: Double) : CapturedModifier + + data class Saturate(val value: Double) : CapturedModifier + + data class MaskImage(val resourceId: String) : CapturedModifier +} + +internal enum class CapturedFillRule { + NON_ZERO, + EVEN_ODD +} + +internal enum class CapturedCompositeOperation { + SOURCE_OVER, + DESTINATION_IN, + DESTINATION_OUT, + PLUS_DARKER +} + +internal data class CapturedFullSnapshot( + val timestamp: Long, + val scope: RumViewIdentityScope, + val root: CapturedLayer?, + val layers: List, + val wireframes: List +) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedContractsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedContractsTest.kt new file mode 100644 index 0000000000..f2b424f54a --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedContractsTest.kt @@ -0,0 +1,72 @@ +/* + * 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.composition + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class CapturedContractsTest { + + @Test + fun `M preserve ordered references W construct captured layer`() { + // Given + val tree = compositionTestTree() + val secondLayer = tree.factory.layer(tree.window, "second-layer") + + // When + val layer = tree.layer.copy( + children = listOf( + CapturedChild.Wireframe(tree.wireframeIdentity), + CapturedChild.Layer(secondLayer), + CapturedChild.Layer(tree.layer.identity) + ), + modifiers = listOf( + CapturedModifier.Opacity(0.5), + CapturedModifier.GaussianBlur(2.0) + ) + ) + + // Then + assertThat(layer.children.map { it.identity }) + .containsExactly(tree.wireframeIdentity, secondLayer, tree.layer.identity) + assertThat(layer.modifiers) + .containsExactly(CapturedModifier.Opacity(0.5), CapturedModifier.GaussianBlur(2.0)) + } + + @Test + fun `M distinguish unchanged from cleared W construct mutation`() { + // Given + val tree = compositionTestTree() + + // When + val unchanged = CapturedLayerUpdate(identity = tree.layer.identity) + val cleared = CapturedLayerUpdate( + identity = tree.layer.identity, + children = CapturedChange.Set(emptyList()), + modifiers = CapturedChange.Set(emptyList()), + compositeOperation = CapturedChange.Set(null) + ) + + // Then + assertThat(unchanged.children).isEqualTo(CapturedChange.Unchanged) + assertThat(unchanged.modifiers).isEqualTo(CapturedChange.Unchanged) + assertThat(cleared.children).isEqualTo(CapturedChange.Set(emptyList())) + assertThat(cleared.modifiers).isEqualTo(CapturedChange.Set(emptyList())) + assertThat(cleared.compositeOperation).isEqualTo(CapturedChange.Set(null)) + } + + @Test + fun `M represent resolved and unresolved resources W construct pixel resource`() { + // Given + When + val resolved: PixelResource = PixelResource.Resolved("resource") + val unresolved: PixelResource = PixelResource.Unresolved + + // Then + assertThat(resolved).isEqualTo(PixelResource.Resolved("resource")) + assertThat(unresolved).isSameAs(PixelResource.Unresolved) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt new file mode 100644 index 0000000000..3cb6f6245c --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt @@ -0,0 +1,222 @@ +/* + * 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.composition + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +internal class CapturedIdentityTest { + + @Test + fun `M return stable identity W create identity { identical inputs }`() { + // Given + val factory = factory() + + // When + val first = factory.view(factory.window("window"), "view-id") + val repeated = factory.view(factory.window("window"), "view-id") + + // Then + assertThat(repeated).isSameAs(first) + } + + @Test + fun `M allocate new identity W create identity { separate factories }`() { + // Given + val generator = AutoIncrementingCapturedReplayIdGenerator() + val scope = RumViewIdentityScope("view") + val firstFactory = DefaultCapturedIdentityFactory(scope, generator) + val secondFactory = DefaultCapturedIdentityFactory(scope, generator) + + // When + val first = firstFactory.window("window") + val second = secondFactory.window("window") + + // Then + assertThat(second).isNotEqualTo(first) + assertThat(second.wireId).isEqualTo(first.wireId + 1) + } + + @Test + fun `M namespace view identity W view { identical local IDs in different windows }`() { + // Given + val factory = factory() + + // When + val first = factory.view(factory.window("first-window"), "shared-id") + val second = factory.view(factory.window("second-window"), "shared-id") + + // Then + assertThat(first).isNotEqualTo(second) + assertThat(first.wireId).isNotEqualTo(second.wireId) + } + + @Test + fun `M namespace compose identity W composeNode { identical local IDs in different hosts }`() { + // Given + val factory = factory() + val window = factory.window("window") + + // When + val first = factory.composeNode(factory.composeHost(window, "first-host"), "shared-id") + val second = factory.composeNode(factory.composeHost(window, "second-host"), "shared-id") + + // Then + assertThat(first).isNotEqualTo(second) + assertThat(first.wireId).isNotEqualTo(second.wireId) + } + + @Test + fun `M allocate distinct identity W create identity { identical numeric IDs }`() { + // Given + val factory = factory() + val window = factory.window("42") + + // When + val view = factory.view(window, "42") + val host = factory.composeHost(window, "42") + val layer = factory.layer(window, "42") + val wireframe = factory.shapeWireframe(window) + + // Then + assertThat(listOf(view, host, layer, wireframe).map { it.wireId }.toSet()).hasSize(4) + } + + @Test + fun `M isolate identity W create identity { different RUM view scopes }`() { + // Given + val generator = AutoIncrementingCapturedReplayIdGenerator() + val firstFactory = DefaultCapturedIdentityFactory(RumViewIdentityScope("first-view"), generator) + val secondFactory = DefaultCapturedIdentityFactory(RumViewIdentityScope("second-view"), generator) + + // When + val first = firstFactory.window("window") + val second = secondFactory.window("window") + + // Then + assertThat(first).isNotEqualTo(second) + assertThat(first.wireId).isNotEqualTo(second.wireId) + } + + @Test + fun `M reject owner W create identity { owner belongs to another scope }`() { + // Given + val firstFactory = factory("first-view") + val secondFactory = factory("second-view") + + // When + Then + assertThatThrownBy { secondFactory.view(firstFactory.window("window"), "view") } + .isInstanceOf(IllegalArgumentException::class.java) + } + + @Test + fun `M create namespaced IDs W create wireframe identities`() { + // Given + val factory = DefaultCapturedIdentityFactory( + scope = RumViewIdentityScope("view"), + replayIdGenerator = AutoIncrementingCapturedReplayIdGenerator(initialId = 42) + ) + val layer = factory.layer(factory.window("window"), "layer") + + // When + val shape = factory.shapeWireframe(layer) + val text = factory.textWireframe(layer) + val image = factory.imageWireframe(layer) + val placeholder = factory.placeholderWireframe(layer) + val embeddedContent = factory.embeddedContentWireframe(layer) + + // Then + assertThat(shape.wireId).isEqualTo((1L shl NAMESPACE_SHIFT) or layer.wireId) + assertThat(text.wireId).isEqualTo((2L shl NAMESPACE_SHIFT) or layer.wireId) + assertThat(image.wireId).isEqualTo((3L shl NAMESPACE_SHIFT) or layer.wireId) + assertThat(placeholder.wireId).isEqualTo((4L shl NAMESPACE_SHIFT) or layer.wireId) + assertThat(embeddedContent.wireId).isEqualTo((5L shl NAMESPACE_SHIFT) or layer.wireId) + } + + @Test + fun `M preserve raw slot ID W create webViewWireframe`() { + // Given + val factory = factory() + val layer = factory.layer(factory.window("window"), "layer") + + // When + val wireframe = factory.webViewWireframe(layer, slotId = 42) + + // Then + assertThat(wireframe.wireId).isEqualTo(42) + assertThat(wireframe.wireframeKind).isEqualTo(CapturedWireframeKind.WEB_VIEW) + } + + @Test + fun `M keep layer IDs outside slot ID range W create identity { any Int slotId }`() { + // Given + val factory = DefaultCapturedIdentityFactory( + scope = RumViewIdentityScope("view"), + replayIdGenerator = AutoIncrementingCapturedReplayIdGenerator(initialId = Int.MAX_VALUE - 1L) + ) + + // When + val layerIds = listOf( + factory.window("before-max").wireId, + factory.window("max").wireId, + factory.window("wrapped").wireId, + factory.window("min").wireId + ) + + // Then + // A web-view slotId is caller-supplied and preserved verbatim as its wire id (see + // `M preserve raw slot ID`), so it may be anywhere in the Int range. Layer ids must never + // land in that range, or a colliding slotId could be mistaken for an unrelated layer. + assertThat(layerIds).allMatch { it < Int.MIN_VALUE || it > Int.MAX_VALUE.toLong() } + } + + @Test + fun `M wrap replay IDs W create identity { maximum ID reached }`() { + // Given + val factory = DefaultCapturedIdentityFactory( + scope = RumViewIdentityScope("view"), + replayIdGenerator = AutoIncrementingCapturedReplayIdGenerator(initialId = Int.MAX_VALUE - 1L) + ) + + // When + val beforeMax = factory.window("before-max") + val max = factory.window("max") + val wrapped = factory.window("wrapped") + + // Then + assertThat(beforeMax.wireId).isEqualTo(LAYER_ID_OFFSET + (Int.MAX_VALUE - 1L)) + assertThat(max.wireId).isEqualTo(LAYER_ID_OFFSET + Int.MAX_VALUE.toLong()) + assertThat(wrapped.wireId).isEqualTo(LAYER_ID_OFFSET) + } + + @Test + fun `M create player-safe IDs W create identities`() { + // Given + val factory = factory() + val window = factory.window("window") + + // When + val identities = List(1_000) { factory.view(window, "view-$it") } + + factory.embeddedContentWireframe(window) + + // Then + assertThat(identities.map { it.wireId }) + .allMatch { it in 0..MAX_SAFE_JAVASCRIPT_INTEGER } + .doesNotHaveDuplicates() + } + + private fun factory(scope: String = "view") = DefaultCapturedIdentityFactory( + scope = RumViewIdentityScope(scope), + replayIdGenerator = AutoIncrementingCapturedReplayIdGenerator() + ) + + private companion object { + const val NAMESPACE_SHIFT = 32 + const val MAX_SAFE_JAVASCRIPT_INTEGER = (1L shl 53) - 1 + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionTestTree.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionTestTree.kt new file mode 100644 index 0000000000..533f78da90 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionTestTree.kt @@ -0,0 +1,59 @@ +/* + * 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.composition + +internal data class CompositionTestTree( + val scope: RumViewIdentityScope, + val factory: DefaultCapturedIdentityFactory, + val window: CapturedIdentity, + val root: CapturedLayer, + val layer: CapturedLayer, + val wireframeIdentity: CapturedIdentity, + val wireframe: CapturedWireframe, + val snapshot: CapturedFullSnapshot +) + +internal fun compositionTestTree(scopeValue: String = "rum-view"): CompositionTestTree { + val scope = RumViewIdentityScope(scopeValue) + val factory = DefaultCapturedIdentityFactory(scope) + val rootIdentity = factory.screenRoot() + val window = factory.window("window") + val layerIdentity = factory.layer(window, "layer") + val wireframeIdentity = factory.shapeWireframe(layerIdentity) + val wireframe = CapturedWireframe.Shape( + identity = wireframeIdentity, + bounds = CapturedBounds(1, 2, 3, 4) + ) + val layer = CapturedLayer( + identity = layerIdentity, + kind = CapturedLayerKind.COMPOSITION_LAYER, + bounds = CapturedBounds(1, 2, 3, 4), + children = listOf(CapturedChild.Wireframe(wireframeIdentity)) + ) + val root = CapturedLayer( + identity = rootIdentity, + kind = CapturedLayerKind.SYNTHETIC_SCREEN_ROOT, + bounds = CapturedBounds(0, 0, 100, 200), + children = listOf(CapturedChild.Layer(layerIdentity)) + ) + return CompositionTestTree( + scope = scope, + factory = factory, + window = window, + root = root, + layer = layer, + wireframeIdentity = wireframeIdentity, + wireframe = wireframe, + snapshot = CapturedFullSnapshot( + timestamp = 123, + scope = scope, + root = root, + layers = listOf(layer), + wireframes = listOf(wireframe) + ) + ) +} From e9001cd9a27f3089cebccd7f27ed8dcfcace7d88 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:18:44 +0300 Subject: [PATCH 3/7] PANA-8500: Validate captured composition trees --- detekt_custom_safe_calls_third_party.yml | 1 + .../composition/CapturedMutationValidation.kt | 175 +++ .../composition/CapturedSnapshotValidation.kt | 442 +++++++ .../composition/CapturedTreeValidator.kt | 74 ++ .../composition/CapturedTreeValidatorTest.kt | 1162 +++++++++++++++++ 5 files changed, 1854 insertions(+) create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutationValidation.kt create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedSnapshotValidation.kt create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidator.kt create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt diff --git a/detekt_custom_safe_calls_third_party.yml b/detekt_custom_safe_calls_third_party.yml index a48a4e71c3..c432ce2639 100644 --- a/detekt_custom_safe_calls_third_party.yml +++ b/detekt_custom_safe_calls_third_party.yml @@ -821,6 +821,7 @@ datadog: - "kotlin.Char.titlecase(java.util.Locale)" - "kotlin.CharArray.constructor(kotlin.Int, kotlin.Function1)" - "kotlin.Double.coerceAtMost(kotlin.Double)" + - "kotlin.Double.isFinite()" - "kotlin.Double.isNaN()" - "kotlin.Double.pow(kotlin.Int)" - "kotlin.Double.pow(kotlin.Double)" diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutationValidation.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutationValidation.kt new file mode 100644 index 0000000000..94e4f427fd --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutationValidation.kt @@ -0,0 +1,175 @@ +/* + * 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.composition + +@Suppress("TooManyFunctions") // Each function validates one independent mutation invariant. +internal class CapturedMutationValidation( + private val mutation: CapturedMutationSet, + private val base: CapturedFullSnapshot +) { + private val failures = mutableListOf() + private val adds = mutation.adds.valueOrEmpty() + private val removes = mutation.removes.valueOrEmpty() + private val updates = mutation.updates.valueOrEmpty() + private val baseLayers = base.layers.associateBy { it.identity.wireId } + private val baseWireframes = base.wireframes.associateBy { it.identity.wireId } + + fun validate(): CaptureValidationResult { + validateScope() + validateOperationDuplicates() + validateContradictions() + validateTargets() + val replacementRoot = validateReplacementRoot() + + if (failures.isNotEmpty()) return CaptureValidationResult.Invalid(failures) + return CapturedSnapshotValidation( + snapshot = effectiveSnapshot(replacementRoot), + validateWireframeDefinitions = false + ).validate() + } + + private fun validateScope() { + if (mutation.scope != base.scope) { + failures += validationFailure(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE) + } + } + + private fun validateOperationDuplicates() { + validateOperationDuplicates(adds.map { it.identity }) + validateOperationDuplicates(removes) + validateOperationDuplicates(updates.map { it.identity }) + } + + private fun validateOperationDuplicates(identities: List) { + identities.groupBy(CapturedIdentity::wireId).values.forEach { operations -> + if (operations.size > 1) { + operations.firstOrNull()?.let { + failures += validationFailure(CaptureValidationErrorCode.DUPLICATE_MUTATION_OPERATION, it) + } + } + } + } + + private fun validateContradictions() { + val addIds = adds.map { it.identity.wireId }.toSet() + val removeIds = removes.map(CapturedIdentity::wireId).toSet() + val updateIds = updates.map { it.identity.wireId }.toSet() + (addIds intersect removeIds).forEach { reportContradiction(it) } + (addIds intersect updateIds).forEach { reportContradiction(it) } + (removeIds intersect updateIds).forEach { reportContradiction(it) } + } + + private fun reportContradiction(wireId: Long) { + val identity = adds.firstOrNull { it.identity.wireId == wireId }?.identity + ?: removes.firstOrNull { it.wireId == wireId } + ?: updates.firstOrNull { it.identity.wireId == wireId }?.identity + failures += validationFailure(CaptureValidationErrorCode.CONTRADICTORY_MUTATION, identity) + } + + private fun validateTargets() { + removes.forEach(::validateRemoval) + updates.forEach(::validateUpdate) + adds.forEach(::validateAddition) + } + + private fun validateRemoval(identity: CapturedIdentity) { + if (identity.scope != mutation.scope) { + failures += validationFailure(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE, identity) + } + validateExistingTarget(identity) + } + + private fun validateUpdate(update: CapturedLayerUpdate) { + if (update.identity.scope != mutation.scope) { + failures += validationFailure(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE, update.identity) + } + validateExistingTarget(update.identity) + } + + private fun validateExistingTarget(identity: CapturedIdentity) { + val target = baseLayers[identity.wireId] + when { + target == null -> failures += validationFailure( + CaptureValidationErrorCode.UNKNOWN_MUTATION_TARGET, + identity + ) + + target.identity != identity -> failures += validationFailure( + CaptureValidationErrorCode.REFERENCE_IDENTITY_MISMATCH, + identity + ) + } + } + + private fun validateAddition(layer: CapturedLayer) { + if (layer.identity.scope != mutation.scope) { + failures += validationFailure(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE, layer.identity) + } + val identityAlreadyExists = baseLayers[layer.identity.wireId] != null || + baseWireframes[layer.identity.wireId] != null || + base.root?.identity?.wireId == layer.identity.wireId + if (identityAlreadyExists) { + failures += validationFailure(CaptureValidationErrorCode.CONTRADICTORY_MUTATION, layer.identity) + } + } + + private fun validateReplacementRoot(): CapturedLayer? { + val replacement = (mutation.root as? CapturedChange.Set)?.value ?: return null + val isValid = replacement.kind == CapturedLayerKind.SYNTHETIC_SCREEN_ROOT && + replacement.identity.kind == CapturedIdentityKind.SCREEN_ROOT && + replacement.identity.scope == mutation.scope && + (base.root == null || replacement.identity == base.root.identity) && + // Only meaningful when base.root == null (a stable root's own wireId can't newly + // collide with a wireframe): establishing the root for the first time via a mutation + // must not claim a wireId an existing wireframe already owns, or full-snapshot + // validation of the equivalent state would reject it as DUPLICATE_IDENTITY. + baseWireframes[replacement.identity.wireId] == null + if (!isValid) { + failures += validationFailure(CaptureValidationErrorCode.INVALID_ROOT_REPLACEMENT, replacement.identity) + } + return replacement + } + + // The map is a mutable copy owned by this validation operation. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun effectiveSnapshot(replacementRoot: CapturedLayer?): CapturedFullSnapshot { + val effectiveLayers = baseLayers.toMutableMap() + removes.forEach { effectiveLayers.remove(it.wireId) } + adds.forEach { effectiveLayers[it.identity.wireId] = it } + updates.forEach { update -> + effectiveLayers[update.identity.wireId]?.let { + effectiveLayers[update.identity.wireId] = it.apply(update) + } + } + return base.copy( + timestamp = mutation.timestamp, + root = replacementRoot ?: base.root, + layers = effectiveLayers.values.toList() + ) + } + + private fun CapturedLayer.apply(update: CapturedLayerUpdate): CapturedLayer = copy( + bounds = bounds.copy( + x = update.x.valueOr(bounds.x), + y = update.y.valueOr(bounds.y), + width = update.width.valueOr(bounds.width), + height = update.height.valueOr(bounds.height) + ), + children = update.children.valueOr(children), + modifiers = update.modifiers.valueOr(modifiers), + compositeOperation = when (val operation = update.compositeOperation) { + is CapturedChange.Set -> operation.value + CapturedChange.Unchanged -> compositeOperation + } + ) + + private fun CapturedChange>.valueOrEmpty(): List = + (this as? CapturedChange.Set)?.value.orEmpty() + + private fun CapturedChange.valueOr(default: T): T = + (this as? CapturedChange.Set)?.value ?: default +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedSnapshotValidation.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedSnapshotValidation.kt new file mode 100644 index 0000000000..71baff1704 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedSnapshotValidation.kt @@ -0,0 +1,442 @@ +/* + * 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.composition + +@Suppress("TooManyFunctions") // Each function validates one independent snapshot invariant. +internal class CapturedSnapshotValidation( + private val snapshot: CapturedFullSnapshot, + private val validateWireframeDefinitions: Boolean = true +) { + private val failures = mutableListOf() + + // Wireframe identities referenced by a layer child but absent from wireframesById while + // validateWireframeDefinitions is false - i.e. delivered outside this mutation. Tracked + // separately from snapshot.wireframes so validateParentCounts can still catch two references + // to the same not-yet-delivered wireframe, without resurrecting UNREFERENCED_WIREFRAME/ + // DANGLING_WIREFRAME_REFERENCE for wireframes this validation pass has no definition for. + private val referencedUnknownWireframes = mutableMapOf() + + fun validate(): CaptureValidationResult { + val root = snapshot.root ?: return missingRootResult() + validateRoot(root) + + // All collections are local captured values and are not mutated while validating. + @Suppress("UnsafeThirdPartyFunctionCall") + val layers = listOf(root) + snapshot.layers + validateDefinitions(layers) + + val layersById = snapshot.layers.associateBy { it.identity.wireId } + val wireframesById = snapshot.wireframes.associateBy { it.identity.wireId } + val parentCounts = mutableMapOf() + validateReferences(layers, layersById, wireframesById, parentCounts) + validateParentCounts(parentCounts) + validateAcyclic(layers, layersById) + return failures.toValidationResult() + } + + // This result always contains the single missing-root failure constructed here. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun missingRootResult(): CaptureValidationResult = CaptureValidationResult.Invalid( + listOf(validationFailure(CaptureValidationErrorCode.MISSING_ROOT)) + ) + + private fun validateRoot(root: CapturedLayer) { + if (root.kind != CapturedLayerKind.SYNTHETIC_SCREEN_ROOT || + root.identity.kind != CapturedIdentityKind.SCREEN_ROOT + ) { + failures += validationFailure(CaptureValidationErrorCode.INVALID_ROOT, root.identity) + } + } + + private fun validateDefinitions(layers: List) { + val identities = layers.map { it.identity } + if (validateWireframeDefinitions) { + snapshot.wireframes.map { it.identity } + } else { + emptyList() + } + validateIdentityDefinitions(identities) + layers.forEach(::validateLayer) + if (validateWireframeDefinitions) snapshot.wireframes.forEach(::validateWireframe) + } + + private fun validateIdentityDefinitions(identities: List) { + identities.filter { it.scope != snapshot.scope }.forEach { + failures += validationFailure(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE, it) + } + identities.groupBy(CapturedIdentity::wireId).values.forEach { matchingWireIds -> + if (matchingWireIds.size > 1) { + matchingWireIds.firstOrNull()?.let { + failures += validationFailure(CaptureValidationErrorCode.DUPLICATE_IDENTITY, it) + } + } + } + identities.groupBy { + IdentityDefinitionKey(it.scope, it.kind, it.wireframeKind, it.namespace, it.localId) + } + .values + .forEach { matchingIdentities -> + if (matchingIdentities.size > 1) { + matchingIdentities.firstOrNull()?.let { + failures += validationFailure(CaptureValidationErrorCode.DUPLICATE_IDENTITY, it) + } + } + } + } + + private fun validateLayer(layer: CapturedLayer) { + if (layer.identity.scope != snapshot.scope) { + failures += validationFailure(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE, layer.identity) + } + if (layer.identity.kind != layer.kind.identityKind || layer.identity.wireframeKind != null) { + failures += validationFailure(CaptureValidationErrorCode.IDENTITY_KIND_MISMATCH, layer.identity) + } + if (layer.bounds.width < 0 || layer.bounds.height < 0) { + failures += validationFailure(CaptureValidationErrorCode.INVALID_BOUNDS, layer.identity) + } + layer.modifiers.forEach { validateModifier(it, layer.identity) } + } + + private fun validateWireframe(captured: CapturedWireframe) { + if (captured.identity.scope != snapshot.scope) { + failures += validationFailure(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE, captured.identity) + } + if (captured.identity.kind != CapturedIdentityKind.WIREFRAME || + captured.identity.wireframeKind != captured.identityKind + ) { + failures += validationFailure(CaptureValidationErrorCode.IDENTITY_KIND_MISMATCH, captured.identity) + } + if (captured.bounds.width < 0 || captured.bounds.height < 0) { + failures += validationFailure(CaptureValidationErrorCode.INVALID_BOUNDS, captured.identity) + } + if (captured is CapturedWireframe.Pixel && !captured.resource.isResolved()) { + failures += validationFailure( + CaptureValidationErrorCode.UNRESOLVED_PIXEL_RESOURCE, + captured.identity + ) + } + validateStyle(captured.style, captured.identity) + validateBackgroundGradient(captured.style, captured.identity) + validateBorder(captured.border, captured.identity) + if (captured is CapturedWireframe.Text) validateTextStyle(captured.textStyle, captured.identity) + } + + private fun validateReferences( + layers: List, + layersById: Map, + wireframesById: Map, + parentCounts: MutableMap + ) { + layers.forEach { parent -> + parent.children.forEach { child -> + validateReference(child, layersById, wireframesById, parentCounts) + } + } + } + + private fun validateReference( + child: CapturedChild, + layersById: Map, + wireframesById: Map, + parentCounts: MutableMap + ) { + when (child) { + is CapturedChild.Layer -> validateLayerReference(child, layersById, parentCounts) + is CapturedChild.Wireframe -> validateWireframeReference(child, layersById, wireframesById, parentCounts) + } + } + + private fun validateLayerReference( + child: CapturedChild.Layer, + layersById: Map, + parentCounts: MutableMap + ) { + if (child.identity.kind == CapturedIdentityKind.WIREFRAME) { + failures += validationFailure(CaptureValidationErrorCode.IDENTITY_KIND_MISMATCH, child.identity) + } + validateReferenceScope(child.identity) + val definition = layersById[child.identity.wireId] + when { + definition == null -> failures += validationFailure( + CaptureValidationErrorCode.DANGLING_LAYER_REFERENCE, + child.identity + ) + + definition.identity != child.identity -> failures += validationFailure( + CaptureValidationErrorCode.REFERENCE_IDENTITY_MISMATCH, + child.identity + ) + + else -> parentCounts.increment(child.identity.wireId) + } + } + + private fun validateWireframeReference( + child: CapturedChild.Wireframe, + layersById: Map, + wireframesById: Map, + parentCounts: MutableMap + ) { + validateWireframeReferenceIdentity(child) + val definition = wireframesById[child.identity.wireId] + when { + // Only a wholly absent definition is tolerated during mutation validation - the + // wireframe may be delivered outside this mutation - but it's still counted, or a + // second reference introduced by this mutation's layer changes would silently give + // the same wireframe multiple parents. See validateParentCounts. A definition that + // DOES exist under this wireId but doesn't match is a real collision (e.g. two + // WebViews from different owners landing on the same unshifted slot id) and is + // rejected below regardless of validateWireframeDefinitions. An undelivered wireframe + // id colliding with an existing layer or the root is also a real collision - full + // snapshot validation would reject the equivalent state as DUPLICATE_IDENTITY - so it + // isn't waved through just because no wireframe claims that id yet. + definition == null && !validateWireframeDefinitions -> { + val collidesWithLayer = layersById[child.identity.wireId] != null || + snapshot.root?.identity?.wireId == child.identity.wireId + if (collidesWithLayer) { + failures += validationFailure(CaptureValidationErrorCode.DUPLICATE_IDENTITY, child.identity) + } else { + parentCounts.increment(child.identity.wireId) + referencedUnknownWireframes[child.identity.wireId] = child.identity + } + } + + definition == null -> failures += validationFailure( + CaptureValidationErrorCode.DANGLING_WIREFRAME_REFERENCE, + child.identity + ) + + definition.identity != child.identity -> failures += validationFailure( + CaptureValidationErrorCode.REFERENCE_IDENTITY_MISMATCH, + child.identity + ) + + else -> parentCounts.increment(child.identity.wireId) + } + } + + private fun validateWireframeReferenceIdentity(child: CapturedChild.Wireframe) { + if (child.identity.kind != CapturedIdentityKind.WIREFRAME) { + failures += validationFailure(CaptureValidationErrorCode.IDENTITY_KIND_MISMATCH, child.identity) + } + validateReferenceScope(child.identity) + } + + private fun validateReferenceScope(identity: CapturedIdentity) { + if (identity.scope != snapshot.scope) { + failures += validationFailure(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE, identity) + } + } + + private fun validateParentCounts(parentCounts: Map) { + snapshot.layers.forEach { + validateParentCount(it.identity, parentCounts, CaptureValidationErrorCode.UNREFERENCED_LAYER) + } + // Multiple parents on a wireframe is invalid regardless of validateWireframeDefinitions - a + // mutation's layer additions/updates can introduce a second reference to an existing base + // wireframe even though wireframes themselves aren't part of the mutation. Zero parents, by + // contrast, is expected during mutations: CapturedMutationSet has no way to represent a + // wireframe addition, so a layer's children can legitimately reference a wireframe that isn't + // in base.wireframes yet (delivered separately), and only get counted once it is. + snapshot.wireframes.forEach { + validateWireframeParentCount(it, parentCounts) + } + // Same invariant as above, for wireframe identities this pass has no definition for: still + // referenced ≥1 time by construction (that's how they ended up in this map), so only the + // multiple-parents case is meaningful here. + referencedUnknownWireframes.forEach { (wireId, identity) -> + if ((parentCounts[wireId] ?: 0) > 1) { + failures += validationFailure(CaptureValidationErrorCode.MULTIPLE_PARENTS, identity) + } + } + } + + private fun validateWireframeParentCount( + wireframe: CapturedWireframe, + parentCounts: Map + ) { + when (parentCounts[wireframe.identity.wireId] ?: 0) { + 0 -> if (validateWireframeDefinitions && !wireframe.isHiddenSlot) { + failures += validationFailure( + CaptureValidationErrorCode.UNREFERENCED_WIREFRAME, + wireframe.identity + ) + } + + 1 -> Unit + else -> failures += validationFailure( + CaptureValidationErrorCode.MULTIPLE_PARENTS, + wireframe.identity + ) + } + } + + private fun validateParentCount( + identity: CapturedIdentity, + parentCounts: Map, + unreferencedCode: CaptureValidationErrorCode + ) { + when (parentCounts[identity.wireId] ?: 0) { + 0 -> failures += validationFailure(unreferencedCode, identity) + 1 -> Unit + else -> failures += validationFailure(CaptureValidationErrorCode.MULTIPLE_PARENTS, identity) + } + } + + // The regular expression is compiled from a constant controlled by this class. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun validateModifier(modifier: CapturedModifier, identity: CapturedIdentity) { + val valid = when (modifier) { + is CapturedModifier.BrightnessBias -> modifier.value in -1.0..1.0 + is CapturedModifier.Clip -> modifier.path.isNotBlank() + is CapturedModifier.ColorMatrix -> + modifier.values.size == COLOR_MATRIX_SIZE && modifier.values.all { it.isFinite() } + is CapturedModifier.GaussianBlur -> modifier.radius >= 0 && modifier.radius.isFinite() + is CapturedModifier.MaskImage -> modifier.resourceId.isNotBlank() + is CapturedModifier.Opacity -> modifier.value in 0.0..1.0 + is CapturedModifier.Saturate -> modifier.value >= 0 && modifier.value.isFinite() + is CapturedModifier.Shadow -> + modifier.radius >= 0 && modifier.radius.isFinite() && + modifier.offsetX.isFinite() && modifier.offsetY.isFinite() && + HEX_COLOR.matches(modifier.color) + } + if (!valid) failures += validationFailure(CaptureValidationErrorCode.INVALID_MODIFIER, identity) + } + + // The regular expression is compiled from a constant controlled by this class. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun validateStyle(style: CapturedShapeStyle?, identity: CapturedIdentity) { + if (style == null) return + val valid = (style.opacity == null || style.opacity.toDouble() in 0.0..1.0) && + style.cornerRadius.isFiniteOrNull() && + (style.backgroundColor == null || HEX_COLOR.matches(style.backgroundColor)) + if (!valid) failures += validationFailure(CaptureValidationErrorCode.INVALID_STYLE, identity) + } + + // The regular expression is compiled from a constant controlled by this class. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun validateBorder(border: CapturedShapeBorder?, identity: CapturedIdentity) { + if (border == null) return + if (!HEX_COLOR.matches(border.color)) { + failures += validationFailure(CaptureValidationErrorCode.INVALID_STYLE, identity) + } + } + + // The regular expression is compiled from a constant controlled by this class. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun validateTextStyle(textStyle: CapturedTextStyle, identity: CapturedIdentity) { + if (!HEX_COLOR.matches(textStyle.color)) { + failures += validationFailure(CaptureValidationErrorCode.INVALID_STYLE, identity) + } + } + + private fun Number?.isFiniteOrNull(): Boolean = this == null || toDouble().isFinite() + + // The regular expression is compiled from a constant controlled by this class. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun validateBackgroundGradient(style: CapturedShapeStyle?, identity: CapturedIdentity) { + val gradient = style?.backgroundGradient ?: return + val stops = gradient.stops + val endpoints = listOf(gradient.startPoint.x, gradient.startPoint.y, gradient.endPoint.x, gradient.endPoint.y) + val valid = stops.size >= MIN_GRADIENT_STOPS && + stops.all { it.offset in 0.0..1.0 && HEX_COLOR.matches(it.color) } && + stops.zipWithNext().all { (previous, next) -> previous.offset <= next.offset } && + endpoints.all { it.isFinite() } + if (!valid) failures += validationFailure(CaptureValidationErrorCode.INVALID_GRADIENT, identity) + } + + // Set membership operations cannot fail for these locally owned mutable sets. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun validateAcyclic(layers: List, layersById: Map) { + val visiting = mutableSetOf() + val visited = mutableSetOf() + + fun visit(layer: CapturedLayer) { + if (!visiting.add(layer.identity.wireId)) { + failures += validationFailure(CaptureValidationErrorCode.CYCLE, layer.identity) + return + } + if (visited.add(layer.identity.wireId)) { + layer.children.filterIsInstance().forEach { + layersById[it.identity.wireId]?.let(::visit) + } + } + visiting.remove(layer.identity.wireId) + } + // Every layer is checked as a potential DFS root, not just the tree root, so a cycle among + // layers unreachable from root (e.g. two layers that only reference each other) is still + // caught - such a component would otherwise have every node reporting a valid one-parent + // count without ever being visited by a root-only traversal. + layers.filterNot { it.identity.wireId in visited }.forEach(::visit) + } + + private fun PixelResource.isResolved(): Boolean = + this is PixelResource.Resolved && resourceId.isNotBlank() + + private val CapturedWireframe.isHiddenSlot: Boolean + get() = when (this) { + is CapturedWireframe.EmbeddedContent -> isVisible == false + is CapturedWireframe.WebView -> isVisible == false + else -> false + } + + private fun MutableMap.increment(id: Long) { + this[id] = (this[id] ?: 0) + 1 + } + + private val CapturedLayerKind.identityKind: CapturedIdentityKind + get() = when (this) { + CapturedLayerKind.SYNTHETIC_SCREEN_ROOT -> CapturedIdentityKind.SCREEN_ROOT + CapturedLayerKind.WINDOW_ROOT -> CapturedIdentityKind.WINDOW + CapturedLayerKind.NATIVE_VIEW -> CapturedIdentityKind.VIEW + CapturedLayerKind.COMPOSE_HOST -> CapturedIdentityKind.COMPOSE_HOST + CapturedLayerKind.COMPOSE_NODE -> CapturedIdentityKind.COMPOSE_NODE + CapturedLayerKind.COMPOSITION_LAYER -> CapturedIdentityKind.LAYER + } + + private val CapturedWireframe.identityKind: CapturedWireframeKind + get() = when (this) { + is CapturedWireframe.Shape -> CapturedWireframeKind.SHAPE + is CapturedWireframe.Text -> CapturedWireframeKind.TEXT + is CapturedWireframe.Pixel -> CapturedWireframeKind.IMAGE + is CapturedWireframe.PrivacyPlaceholder -> CapturedWireframeKind.PLACEHOLDER + is CapturedWireframe.WebView -> CapturedWireframeKind.WEB_VIEW + is CapturedWireframe.EmbeddedContent -> CapturedWireframeKind.EMBEDDED_CONTENT + } + + private val CapturedWireframe.style: CapturedShapeStyle? + get() = when (this) { + is CapturedWireframe.Shape -> style + is CapturedWireframe.Text -> style + is CapturedWireframe.WebView -> style + is CapturedWireframe.EmbeddedContent -> style + is CapturedWireframe.Pixel -> style + is CapturedWireframe.PrivacyPlaceholder -> null + } + + private val CapturedWireframe.border: CapturedShapeBorder? + get() = when (this) { + is CapturedWireframe.Shape -> border + is CapturedWireframe.Text -> border + is CapturedWireframe.WebView -> border + is CapturedWireframe.Pixel -> border + is CapturedWireframe.PrivacyPlaceholder -> null + } + + private companion object { + const val COLOR_MATRIX_SIZE = 20 + const val MIN_GRADIENT_STOPS = 2 + val HEX_COLOR = Regex("^#[A-Fa-f0-9]{6}([A-Fa-f0-9]{2})?$") + } + + private data class IdentityDefinitionKey( + val scope: RumViewIdentityScope, + val kind: CapturedIdentityKind, + val wireframeKind: CapturedWireframeKind?, + val namespace: List, + val localId: String + ) +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidator.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidator.kt new file mode 100644 index 0000000000..cd6a7fb94f --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidator.kt @@ -0,0 +1,74 @@ +/* + * 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.composition + +internal enum class CaptureValidationErrorCode { + MISSING_ROOT, + INVALID_ROOT, + DUPLICATE_IDENTITY, + WRONG_IDENTITY_SCOPE, + IDENTITY_KIND_MISMATCH, + REFERENCE_IDENTITY_MISMATCH, + DANGLING_LAYER_REFERENCE, + DANGLING_WIREFRAME_REFERENCE, + UNREFERENCED_LAYER, + UNREFERENCED_WIREFRAME, + MULTIPLE_PARENTS, + CYCLE, + INVALID_BOUNDS, + UNRESOLVED_PIXEL_RESOURCE, + INVALID_MODIFIER, + INVALID_GRADIENT, + INVALID_STYLE, + DUPLICATE_MUTATION_OPERATION, + CONTRADICTORY_MUTATION, + UNKNOWN_MUTATION_TARGET, + INVALID_ROOT_REPLACEMENT +} + +internal data class CaptureValidationFailure( + val code: CaptureValidationErrorCode, + val identity: CapturedIdentity? = null, + val detail: String? = null +) + +internal sealed interface CaptureValidationResult { + object Valid : CaptureValidationResult + + data class Invalid( + val failures: List + ) : CaptureValidationResult +} + +internal interface CapturedTreeValidator { + fun validate(snapshot: CapturedFullSnapshot): CaptureValidationResult + + fun validate( + mutation: CapturedMutationSet, + base: CapturedFullSnapshot + ): CaptureValidationResult +} + +internal class DefaultCapturedTreeValidator : CapturedTreeValidator { + + override fun validate(snapshot: CapturedFullSnapshot): CaptureValidationResult = + CapturedSnapshotValidation(snapshot).validate() + + override fun validate( + mutation: CapturedMutationSet, + base: CapturedFullSnapshot + ): CaptureValidationResult = CapturedMutationValidation(mutation, base).validate() +} + +internal fun validationFailure( + code: CaptureValidationErrorCode, + identity: CapturedIdentity? = null, + detail: String? = null +) = CaptureValidationFailure(code, identity, detail) + +internal fun List.toValidationResult(): CaptureValidationResult = + if (isEmpty()) CaptureValidationResult.Valid else CaptureValidationResult.Invalid(this) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt new file mode 100644 index 0000000000..507776134c --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt @@ -0,0 +1,1162 @@ +/* + * 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.composition + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class CapturedTreeValidatorTest { + + private val testedValidator = DefaultCapturedTreeValidator() + + @Test + fun `M return valid W validate { minimal full snapshot }`() { + // Given + val snapshot = compositionTestTree().snapshot + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + } + + @Test + fun `M return valid W validate { valid background gradient }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle( + backgroundGradient = CapturedBackgroundGradient( + stops = listOf( + CapturedGradientStop("#000000", 0.0), + CapturedGradientStop("#FFFFFFFF", 1.0) + ), + startPoint = CapturedPoint(0.0, 0.0), + endPoint = CapturedPoint(1.0, 1.0) + ) + ) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + } + + @Test + fun `M report invalid gradient W validate { fewer than two stops }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle( + backgroundGradient = CapturedBackgroundGradient( + stops = listOf(CapturedGradientStop("#000000", 0.0)), + startPoint = CapturedPoint(0.0, 0.0), + endPoint = CapturedPoint(1.0, 1.0) + ) + ) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_GRADIENT) + } + + @Test + fun `M report invalid gradient W validate { offset out of range }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle( + backgroundGradient = CapturedBackgroundGradient( + stops = listOf( + CapturedGradientStop("#000000", 0.0), + CapturedGradientStop("#FFFFFF", 5.0) + ), + startPoint = CapturedPoint(0.0, 0.0), + endPoint = CapturedPoint(1.0, 1.0) + ) + ) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_GRADIENT) + } + + @Test + fun `M report invalid gradient W validate { invalid color }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle( + backgroundGradient = CapturedBackgroundGradient( + stops = listOf( + CapturedGradientStop("not-a-color", 0.0), + CapturedGradientStop("#FFFFFF", 1.0) + ), + startPoint = CapturedPoint(0.0, 0.0), + endPoint = CapturedPoint(1.0, 1.0) + ) + ) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_GRADIENT) + } + + @Test + fun `M report invalid gradient W validate { decreasing offsets }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle( + backgroundGradient = CapturedBackgroundGradient( + stops = listOf( + CapturedGradientStop("#000000", 0.6), + CapturedGradientStop("#FFFFFF", 0.4) + ), + startPoint = CapturedPoint(0.0, 0.0), + endPoint = CapturedPoint(1.0, 1.0) + ) + ) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_GRADIENT) + } + + @Test + fun `M report invalid gradient W validate { non-finite endpoint coordinate }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle( + backgroundGradient = CapturedBackgroundGradient( + stops = listOf( + CapturedGradientStop("#000000", 0.0), + CapturedGradientStop("#FFFFFF", 1.0) + ), + startPoint = CapturedPoint(Double.NaN, 0.0), + endPoint = CapturedPoint(1.0, Double.POSITIVE_INFINITY) + ) + ) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_GRADIENT) + } + + @Test + fun `M report invalid modifier W validate { non-finite color matrix entry }`() { + // Given + val tree = compositionTestTree() + val matrix = List(19) { 0.0 } + Double.NaN + val layer = tree.layer.copy(modifiers = listOf(CapturedModifier.ColorMatrix(matrix))) + val snapshot = tree.snapshot.copy(layers = listOf(layer)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_MODIFIER) + } + + @Test + fun `M report invalid modifier W validate { non-finite shadow offset }`() { + // Given + val tree = compositionTestTree() + val shadow = CapturedModifier.Shadow( + color = "#000000FF", + offsetX = Double.NaN, + offsetY = 0.0, + radius = 4.0 + ) + val layer = tree.layer.copy(modifiers = listOf(shadow)) + val snapshot = tree.snapshot.copy(layers = listOf(layer)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_MODIFIER) + } + + @Test + fun `M report invalid modifier W validate { infinite shadow radius }`() { + // Given + val tree = compositionTestTree() + val shadow = CapturedModifier.Shadow( + color = "#000000FF", + offsetX = 0.0, + offsetY = 0.0, + radius = Double.POSITIVE_INFINITY + ) + val layer = tree.layer.copy(modifiers = listOf(shadow)) + val snapshot = tree.snapshot.copy(layers = listOf(layer)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_MODIFIER) + } + + @Test + fun `M report invalid modifier W validate { infinite gaussian blur radius }`() { + // Given + val tree = compositionTestTree() + val layer = tree.layer.copy( + modifiers = listOf(CapturedModifier.GaussianBlur(radius = Double.POSITIVE_INFINITY)) + ) + val snapshot = tree.snapshot.copy(layers = listOf(layer)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_MODIFIER) + } + + @Test + fun `M report invalid modifier W validate { infinite saturate value }`() { + // Given + val tree = compositionTestTree() + val layer = tree.layer.copy( + modifiers = listOf(CapturedModifier.Saturate(value = Double.POSITIVE_INFINITY)) + ) + val snapshot = tree.snapshot.copy(layers = listOf(layer)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_MODIFIER) + } + + @Test + fun `M report invalid style W validate { non-finite opacity }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle(opacity = Double.NaN) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_STYLE) + } + + @Test + fun `M report invalid style W validate { out of range opacity }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle(opacity = 1.5) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_STYLE) + } + + @Test + fun `M report invalid style W validate { infinite corner radius }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle(cornerRadius = Double.POSITIVE_INFINITY) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_STYLE) + } + + @Test + fun `M report invalid style W validate { malformed background color }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + style = CapturedShapeStyle(backgroundColor = "red") + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_STYLE) + } + + @Test + fun `M report invalid style W validate { malformed border color }`() { + // Given + val tree = compositionTestTree() + val wireframe = (tree.wireframe as CapturedWireframe.Shape).copy( + border = CapturedShapeBorder(color = "red", width = 1) + ) + val snapshot = tree.snapshot.copy(wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_STYLE) + } + + @Test + fun `M report invalid style W validate { malformed text style color }`() { + // Given + val tree = compositionTestTree() + val textIdentity = tree.factory.textWireframe(tree.layer.identity) + val wireframe = CapturedWireframe.Text( + identity = textIdentity, + bounds = tree.wireframe.bounds, + text = "hello", + textStyle = CapturedTextStyle(family = "Roboto", size = 12, color = "red") + ) + val layer = tree.layer.copy(children = listOf(CapturedChild.Wireframe(textIdentity))) + val snapshot = tree.snapshot.copy(layers = listOf(layer), wireframes = listOf(wireframe)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_STYLE) + } + + @Test + fun `M return valid W validate { multiple windows below synthetic root }`() { + // Given + val scope = RumViewIdentityScope("view") + val factory = DefaultCapturedIdentityFactory(scope) + val firstWindowIdentity = factory.window("first") + val secondWindowIdentity = factory.window("second") + val firstWireframeIdentity = factory.shapeWireframe(firstWindowIdentity) + val secondWireframeIdentity = factory.shapeWireframe(secondWindowIdentity) + val firstWindow = layer( + firstWindowIdentity, + CapturedLayerKind.WINDOW_ROOT, + listOf(CapturedChild.Wireframe(firstWireframeIdentity)) + ) + val secondWindow = layer( + secondWindowIdentity, + CapturedLayerKind.WINDOW_ROOT, + listOf(CapturedChild.Wireframe(secondWireframeIdentity)) + ) + val root = layer( + factory.screenRoot(), + CapturedLayerKind.SYNTHETIC_SCREEN_ROOT, + listOf( + CapturedChild.Layer(firstWindowIdentity), + CapturedChild.Layer(secondWindowIdentity) + ) + ) + val snapshot = CapturedFullSnapshot( + timestamp = 1, + scope = scope, + root = root, + layers = listOf(firstWindow, secondWindow), + wireframes = listOf( + semanticWireframe(firstWireframeIdentity), + semanticWireframe(secondWireframeIdentity) + ) + ) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + assertThat(root.children.map { it.identity }) + .containsExactly(firstWindowIdentity, secondWindowIdentity) + } + + @Test + fun `M return valid W validate { mixed native compose and composition layers }`() { + // Given + val scope = RumViewIdentityScope("view") + val factory = DefaultCapturedIdentityFactory(scope) + val windowIdentity = factory.window("window") + val viewIdentity = factory.view(windowIdentity, "view") + val hostIdentity = factory.composeHost(windowIdentity, "host") + val nodeIdentity = factory.composeNode(hostIdentity, "node") + val compositionIdentity = factory.layer(nodeIdentity, "layer") + val wireframeIdentity = factory.shapeWireframe(compositionIdentity) + val layers = listOf( + layer(windowIdentity, CapturedLayerKind.WINDOW_ROOT, listOf(CapturedChild.Layer(viewIdentity))), + layer(viewIdentity, CapturedLayerKind.NATIVE_VIEW, listOf(CapturedChild.Layer(hostIdentity))), + layer(hostIdentity, CapturedLayerKind.COMPOSE_HOST, listOf(CapturedChild.Layer(nodeIdentity))), + layer(nodeIdentity, CapturedLayerKind.COMPOSE_NODE, listOf(CapturedChild.Layer(compositionIdentity))), + layer( + compositionIdentity, + CapturedLayerKind.COMPOSITION_LAYER, + listOf(CapturedChild.Wireframe(wireframeIdentity)) + ) + ) + val root = layer( + factory.screenRoot(), + CapturedLayerKind.SYNTHETIC_SCREEN_ROOT, + listOf(CapturedChild.Layer(windowIdentity)) + ) + + // When + val result = testedValidator.validate( + CapturedFullSnapshot( + timestamp = 1, + scope = scope, + root = root, + layers = layers, + wireframes = listOf(semanticWireframe(wireframeIdentity)) + ) + ) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + } + + @Test + fun `M report missing root W validate`() { + // Given + val snapshot = compositionTestTree().snapshot.copy(root = null) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).containsExactly(CaptureValidationErrorCode.MISSING_ROOT) + } + + @Test + fun `M report duplicate identity W validate`() { + // Given + val tree = compositionTestTree() + val snapshot = tree.snapshot.copy(layers = listOf(tree.layer, tree.layer)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.DUPLICATE_IDENTITY) + } + + @Test + fun `M report dangling references W validate`() { + // Given + val tree = compositionTestTree() + val missingLayer = tree.factory.layer(tree.window, "missing-layer") + val missingWireframe = tree.factory.textWireframe(tree.layer.identity) + val root = tree.root.copy(children = listOf(CapturedChild.Layer(missingLayer))) + val layer = tree.layer.copy(children = listOf(CapturedChild.Wireframe(missingWireframe))) + + // When + val result = testedValidator.validate(tree.snapshot.copy(root = root, layers = listOf(layer))) + + // Then + assertThat(result.codes()).contains( + CaptureValidationErrorCode.DANGLING_LAYER_REFERENCE, + CaptureValidationErrorCode.DANGLING_WIREFRAME_REFERENCE + ) + } + + @Test + fun `M report cycle W validate`() { + // Given + val tree = compositionTestTree() + val secondIdentity = tree.factory.layer(tree.window, "second-layer") + val first = tree.layer.copy(children = listOf(CapturedChild.Layer(secondIdentity))) + val second = layer( + secondIdentity, + CapturedLayerKind.COMPOSITION_LAYER, + listOf(CapturedChild.Layer(first.identity)) + ) + val snapshot = tree.snapshot.copy(layers = listOf(first, second), wireframes = emptyList()) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.CYCLE) + } + + @Test + fun `M report cycle W validate { disconnected island not reachable from root }`() { + // Given + val tree = compositionTestTree() + val aIdentity = tree.factory.layer(tree.window, "island-a") + val bIdentity = tree.factory.layer(tree.window, "island-b") + val a = layer(aIdentity, CapturedLayerKind.COMPOSITION_LAYER, listOf(CapturedChild.Layer(bIdentity))) + val b = layer(bIdentity, CapturedLayerKind.COMPOSITION_LAYER, listOf(CapturedChild.Layer(aIdentity))) + val snapshot = tree.snapshot.copy(layers = listOf(tree.layer, a, b)) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.CYCLE) + } + + @Test + fun `M report multiple parents W validate`() { + // Given + val tree = compositionTestTree() + val secondIdentity = tree.factory.layer(tree.window, "second-layer") + val second = layer( + secondIdentity, + CapturedLayerKind.COMPOSITION_LAYER, + listOf(CapturedChild.Wireframe(tree.wireframeIdentity)) + ) + val root = tree.root.copy( + children = listOf( + CapturedChild.Layer(tree.layer.identity), + CapturedChild.Layer(secondIdentity) + ) + ) + + // When + val result = testedValidator.validate( + tree.snapshot.copy(root = root, layers = listOf(tree.layer, second)) + ) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.MULTIPLE_PARENTS) + } + + @Test + fun `M report unresolved resource W validate { pixel wireframe }`() { + // Given + val tree = compositionTestTree() + val pixelIdentity = tree.factory.imageWireframe(tree.layer.identity) + val pixel = CapturedWireframe.Pixel( + identity = pixelIdentity, + bounds = CapturedBounds(1, 2, 3, 4), + resource = PixelResource.Unresolved + ) + val layer = tree.layer.copy( + children = listOf(CapturedChild.Wireframe(pixelIdentity)) + ) + + // When + val result = testedValidator.validate( + tree.snapshot.copy(layers = listOf(layer), wireframes = listOf(pixel)) + ) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.UNRESOLVED_PIXEL_RESOURCE) + } + + @Test + fun `M report identity kind mismatch W validate { wrong wireframe namespace }`() { + // Given + val tree = compositionTestTree() + val pixel = CapturedWireframe.Pixel( + identity = tree.wireframeIdentity, + bounds = CapturedBounds(1, 2, 3, 4), + resource = PixelResource.Resolved("resource") + ) + + // When + val result = testedValidator.validate(tree.snapshot.copy(wireframes = listOf(pixel))) + + // Then + assertThat(result.codes()).containsExactly(CaptureValidationErrorCode.IDENTITY_KIND_MISMATCH) + } + + @Test + fun `M return valid W validate { resolved pixel and privacy placeholder }`() { + // Given + val tree = compositionTestTree() + val pixelIdentity = tree.factory.imageWireframe(tree.layer.identity) + val placeholderIdentity = tree.factory.placeholderWireframe(tree.layer.identity) + val layer = tree.layer.copy( + children = listOf( + CapturedChild.Wireframe(pixelIdentity), + CapturedChild.Wireframe(placeholderIdentity) + ) + ) + val snapshot = tree.snapshot.copy( + layers = listOf(layer), + wireframes = listOf( + CapturedWireframe.Pixel( + identity = pixelIdentity, + bounds = CapturedBounds(0, 0, 5, 5), + resource = PixelResource.Resolved("resource", "image/webp") + ), + CapturedWireframe.PrivacyPlaceholder( + identity = placeholderIdentity, + bounds = CapturedBounds(5, 0, 5, 5), + label = "Image" + ) + ) + ) + + // When + val result = testedValidator.validate(snapshot) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + } + + @Test + fun `M return valid W validate { unreferenced hidden slot wireframes }`() { + // Given + val tree = compositionTestTree() + val webViewIdentity = tree.factory.webViewWireframe(tree.layer.identity, 42) + val embeddedIdentity = tree.factory.embeddedContentWireframe(tree.layer.identity) + val hiddenWebView = CapturedWireframe.WebView( + identity = webViewIdentity, + bounds = CapturedBounds(0, 0, 0, 0), + isVisible = false + ) + val hiddenEmbeddedContent = CapturedWireframe.EmbeddedContent( + identity = embeddedIdentity, + bounds = CapturedBounds(0, 0, 0, 0), + slotId = "embedded-slot", + isVisible = false + ) + + // When + val result = testedValidator.validate( + tree.snapshot.copy( + wireframes = listOf(tree.wireframe, hiddenWebView, hiddenEmbeddedContent) + ) + ) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + } + + @Test + fun `M report unreferenced wireframe W validate { visible slot wireframe }`() { + // Given + val tree = compositionTestTree() + val identity = tree.factory.embeddedContentWireframe(tree.layer.identity) + val visibleEmbeddedContent = CapturedWireframe.EmbeddedContent( + identity = identity, + bounds = CapturedBounds(0, 0, 10, 10), + slotId = "embedded-slot", + isVisible = true + ) + + // When + val result = testedValidator.validate( + tree.snapshot.copy(wireframes = listOf(tree.wireframe, visibleEmbeddedContent)) + ) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.UNREFERENCED_WIREFRAME) + } + + @Test + fun `M report mismatched reference W validate { same wire ID from another scope }`() { + // Given + val tree = compositionTestTree() + val wrongIdentity = tree.wireframeIdentity.copy(scope = RumViewIdentityScope("other-view")) + val layer = tree.layer.copy(children = listOf(CapturedChild.Wireframe(wrongIdentity))) + + // When + val result = testedValidator.validate(tree.snapshot.copy(layers = listOf(layer))) + + // Then + assertThat(result.codes()).contains( + CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE, + CaptureValidationErrorCode.REFERENCE_IDENTITY_MISMATCH + ) + } + + @Test + fun `M report identity kind mismatch W validate { layer kind does not match identity }`() { + // Given + val tree = compositionTestTree() + val layer = tree.layer.copy(kind = CapturedLayerKind.NATIVE_VIEW) + + // When + val result = testedValidator.validate(tree.snapshot.copy(layers = listOf(layer))) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.IDENTITY_KIND_MISMATCH) + } + + @Test + fun `M report wrong scope W validate { layer belongs to another RUM view }`() { + // Given + val tree = compositionTestTree() + val otherFactory = DefaultCapturedIdentityFactory(RumViewIdentityScope("other-view")) + val otherLayerIdentity = otherFactory.layer(otherFactory.window("window"), "layer") + val otherLayer = layer(otherLayerIdentity, CapturedLayerKind.COMPOSITION_LAYER, emptyList()) + val root = tree.root.copy(children = listOf(CapturedChild.Layer(otherLayerIdentity))) + + // When + val result = testedValidator.validate( + tree.snapshot.copy(root = root, layers = listOf(otherLayer), wireframes = emptyList()) + ) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE) + } + + @Test + fun `M return valid W validate mutation { sparse bounds update }`() { + // Given + val tree = compositionTestTree() + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + updates = CapturedChange.Set( + listOf(CapturedLayerUpdate(identity = tree.layer.identity, x = CapturedChange.Set(42))) + ) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + } + + @Test + fun `M return valid W validate mutation { wireframe child removed separately }`() { + // Given + val tree = compositionTestTree() + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + updates = CapturedChange.Set( + listOf( + CapturedLayerUpdate( + identity = tree.layer.identity, + children = CapturedChange.Set(emptyList()) + ) + ) + ) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + } + + @Test + fun `M return valid W validate mutation { wireframe child added separately }`() { + // Given + val tree = compositionTestTree() + val addedWireframe = tree.factory.textWireframe(tree.layer.identity) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + updates = CapturedChange.Set( + listOf( + CapturedLayerUpdate( + identity = tree.layer.identity, + children = CapturedChange.Set( + listOf(CapturedChild.Wireframe(addedWireframe)) + ) + ) + ) + ) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + } + + @Test + fun `M report wrong scope W validate mutation { wireframe child added separately }`() { + // Given + val tree = compositionTestTree() + val wrongIdentity = tree.factory.textWireframe(tree.layer.identity) + .copy(scope = RumViewIdentityScope("other-view")) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + updates = CapturedChange.Set( + listOf( + CapturedLayerUpdate( + identity = tree.layer.identity, + children = CapturedChange.Set( + listOf(CapturedChild.Wireframe(wrongIdentity)) + ) + ) + ) + ) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE) + } + + @Test + fun `M report contradiction W validate mutation { same layer removed and updated }`() { + // Given + val tree = compositionTestTree() + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + removes = CapturedChange.Set(listOf(tree.layer.identity)), + updates = CapturedChange.Set(listOf(CapturedLayerUpdate(tree.layer.identity))) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.CONTRADICTORY_MUTATION) + } + + @Test + fun `M report contradiction W validate mutation { added layer collides with existing wireframe }`() { + // Given + val tree = compositionTestTree() + val addedLayer = CapturedLayer( + identity = tree.wireframeIdentity, + kind = CapturedLayerKind.COMPOSITION_LAYER, + bounds = CapturedBounds(0, 0, 1, 1), + children = emptyList() + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + adds = CapturedChange.Set(listOf(addedLayer)) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.CONTRADICTORY_MUTATION) + } + + @Test + fun `M report multiple parents W validate mutation { added layer references existing wireframe }`() { + // Given + val tree = compositionTestTree() + val secondLayerIdentity = tree.factory.layer(tree.window, "second-layer") + val secondLayer = CapturedLayer( + identity = secondLayerIdentity, + kind = CapturedLayerKind.COMPOSITION_LAYER, + bounds = CapturedBounds(0, 0, 1, 1), + children = listOf(CapturedChild.Wireframe(tree.wireframeIdentity)) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + adds = CapturedChange.Set(listOf(secondLayer)) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.MULTIPLE_PARENTS) + } + + @Test + fun `M report multiple parents W validate mutation { two added layers reference same undelivered wireframe }`() { + // Given + val tree = compositionTestTree() + val undeliveredWireframeIdentity = tree.factory.textWireframe(tree.layer.identity) + val firstLayerIdentity = tree.factory.layer(tree.window, "first-layer") + val secondLayerIdentity = tree.factory.layer(tree.window, "second-layer") + val firstLayer = CapturedLayer( + identity = firstLayerIdentity, + kind = CapturedLayerKind.COMPOSITION_LAYER, + bounds = CapturedBounds(0, 0, 1, 1), + children = listOf(CapturedChild.Wireframe(undeliveredWireframeIdentity)) + ) + val secondLayer = CapturedLayer( + identity = secondLayerIdentity, + kind = CapturedLayerKind.COMPOSITION_LAYER, + bounds = CapturedBounds(0, 0, 1, 1), + children = listOf(CapturedChild.Wireframe(undeliveredWireframeIdentity)) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + adds = CapturedChange.Set(listOf(firstLayer, secondLayer)) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.MULTIPLE_PARENTS) + } + + @Test + fun `M report reference identity mismatch W validate mutation { known wireframe id under mismatched identity }`() { + // Given + val tree = compositionTestTree() + val collidingIdentity = tree.wireframeIdentity.copy(localId = "different-local-id") + val secondLayerIdentity = tree.factory.layer(tree.window, "second-layer") + val secondLayer = CapturedLayer( + identity = secondLayerIdentity, + kind = CapturedLayerKind.COMPOSITION_LAYER, + bounds = CapturedBounds(0, 0, 1, 1), + children = listOf(CapturedChild.Wireframe(collidingIdentity)) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + adds = CapturedChange.Set(listOf(secondLayer)) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.REFERENCE_IDENTITY_MISMATCH) + } + + @Test + fun `M report duplicate identity W validate mutation { undelivered wireframe id collides with existing layer }`() { + // Given + val tree = compositionTestTree() + val collidingIdentity = tree.factory.textWireframe(tree.layer.identity) + .copy(wireId = tree.layer.identity.wireId) + val secondLayerIdentity = tree.factory.layer(tree.window, "second-layer") + val secondLayer = CapturedLayer( + identity = secondLayerIdentity, + kind = CapturedLayerKind.COMPOSITION_LAYER, + bounds = CapturedBounds(0, 0, 1, 1), + children = listOf(CapturedChild.Wireframe(collidingIdentity)) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + adds = CapturedChange.Set(listOf(secondLayer)) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.DUPLICATE_IDENTITY) + } + + @Test + fun `M report duplicate identity W validate mutation { undelivered wireframe id collides with root }`() { + // Given + val tree = compositionTestTree() + val collidingIdentity = tree.factory.textWireframe(tree.layer.identity) + .copy(wireId = tree.root.identity.wireId) + val secondLayerIdentity = tree.factory.layer(tree.window, "second-layer") + val secondLayer = CapturedLayer( + identity = secondLayerIdentity, + kind = CapturedLayerKind.COMPOSITION_LAYER, + bounds = CapturedBounds(0, 0, 1, 1), + children = listOf(CapturedChild.Wireframe(collidingIdentity)) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + adds = CapturedChange.Set(listOf(secondLayer)) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.DUPLICATE_IDENTITY) + } + + @Test + fun `M report duplicate operations W validate mutation`() { + // Given + val tree = compositionTestTree() + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + updates = CapturedChange.Set( + listOf( + CapturedLayerUpdate(tree.layer.identity), + CapturedLayerUpdate(tree.layer.identity) + ) + ) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.DUPLICATE_MUTATION_OPERATION) + } + + @Test + fun `M report every pairwise contradiction W validate mutation { add remove and update }`() { + // Given + val tree = compositionTestTree() + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + adds = CapturedChange.Set(listOf(tree.layer)), + removes = CapturedChange.Set(listOf(tree.layer.identity)), + updates = CapturedChange.Set(listOf(CapturedLayerUpdate(tree.layer.identity))) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes().count { it == CaptureValidationErrorCode.CONTRADICTORY_MUTATION }) + .isGreaterThanOrEqualTo(3) + } + + @Test + fun `M report mismatched target W validate mutation { same wire ID from another scope }`() { + // Given + val tree = compositionTestTree() + val wrongIdentity = tree.layer.identity.copy(scope = RumViewIdentityScope("other-view")) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + updates = CapturedChange.Set(listOf(CapturedLayerUpdate(wrongIdentity))) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains( + CaptureValidationErrorCode.WRONG_IDENTITY_SCOPE, + CaptureValidationErrorCode.REFERENCE_IDENTITY_MISMATCH + ) + } + + @Test + fun `M return valid W validate mutation { root replacement }`() { + // Given + val tree = compositionTestTree() + val replacement = tree.root.copy(bounds = CapturedBounds(0, 0, 200, 400)) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + root = CapturedChange.Set(replacement) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result).isEqualTo(CaptureValidationResult.Valid) + } + + @Test + fun `M report invalid root replacement W validate mutation`() { + // Given + val tree = compositionTestTree() + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + root = CapturedChange.Set(tree.layer) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_ROOT_REPLACEMENT) + } + + @Test + fun `M report invalid root replacement W validate mutation { different identity from base root }`() { + // Given + val tree = compositionTestTree() + val replacement = tree.root.copy( + identity = tree.root.identity.copy(wireId = tree.root.identity.wireId + 1) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + root = CapturedChange.Set(replacement) + ) + + // When + val result = testedValidator.validate(mutation, tree.snapshot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_ROOT_REPLACEMENT) + } + + @Test + fun `M report invalid root replacement W validate mutation { new root collides with existing wireframe }`() { + // Given + val tree = compositionTestTree() + val baseWithoutRoot = tree.snapshot.copy(root = null) + val collidingRoot = tree.root.copy( + identity = tree.root.identity.copy(wireId = tree.wireframeIdentity.wireId) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + root = CapturedChange.Set(collidingRoot) + ) + + // When + val result = testedValidator.validate(mutation, baseWithoutRoot) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.INVALID_ROOT_REPLACEMENT) + } + + private fun layer( + identity: CapturedIdentity, + kind: CapturedLayerKind, + children: List + ) = CapturedLayer( + identity = identity, + kind = kind, + bounds = CapturedBounds(0, 0, 10, 10), + children = children + ) + + private fun semanticWireframe(identity: CapturedIdentity) = CapturedWireframe.Shape( + identity = identity, + bounds = CapturedBounds(0, 0, 10, 10) + ) + + private fun CaptureValidationResult.codes(): List = + (this as CaptureValidationResult.Invalid).failures.map { it.code } +} From 56e31aab8e51e6e932ffe2e5ea3b5b10db8b4bfe Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:20:08 +0300 Subject: [PATCH 4/7] PANA-8500: Map captured trees to wire models --- .../composition/CapturedTreeWireMapper.kt | 342 ++++++++++++++++ .../composition/CapturedTreeWireMapperTest.kt | 371 ++++++++++++++++++ 2 files changed, 713 insertions(+) create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapper.kt create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapperTest.kt diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapper.kt new file mode 100644 index 0000000000..ad18f4992c --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapper.kt @@ -0,0 +1,342 @@ +/* + * 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. + */ + +@file:Suppress("TooManyFunctions") // This file contains stateless wire-model conversion extensions. + +package com.datadog.android.sessionreplay.internal.composition + +import com.datadog.android.sessionreplay.model.MobileSegment + +internal sealed interface CaptureWireMappingResult { + data class Success(val value: T) : CaptureWireMappingResult + + data class Invalid( + val failures: List + ) : CaptureWireMappingResult +} + +internal interface CapturedTreeWireMapper { + fun mapFullSnapshot( + snapshot: CapturedFullSnapshot + ): CaptureWireMappingResult + + fun mapMutation( + mutation: CapturedMutationSet, + base: CapturedFullSnapshot + ): CaptureWireMappingResult +} + +internal class DefaultCapturedTreeWireMapper( + private val validator: CapturedTreeValidator = DefaultCapturedTreeValidator() +) : CapturedTreeWireMapper { + + @Suppress("ReturnCount") // Invalid capture data is returned at the point where it is detected. + override fun mapFullSnapshot( + snapshot: CapturedFullSnapshot + ): CaptureWireMappingResult { + val validation = validator.validate(snapshot) + if (validation is CaptureValidationResult.Invalid) { + return CaptureWireMappingResult.Invalid(validation.failures) + } + + val root = snapshot.root + ?: return invalidMapping(CaptureValidationErrorCode.MISSING_ROOT) + val wireframes = snapshot.wireframes.map { captured -> + captured.toWireframeOrNull() + ?: return invalidMapping( + CaptureValidationErrorCode.UNRESOLVED_PIXEL_RESOURCE, + captured.identity + ) + } + val tree = MobileSegment.CompositionTree( + root = root.toWireLayer(), + layers = snapshot.layers.map { it.toWireLayer() }.ifEmpty { null } + ) + return CaptureWireMappingResult.Success( + MobileSegment.MobileRecord.MobileFullSnapshotRecord( + timestamp = snapshot.timestamp, + data = MobileSegment.Data( + wireframes = wireframes, + compositionTree = tree + ) + ) + ) + } + + override fun mapMutation( + mutation: CapturedMutationSet, + base: CapturedFullSnapshot + ): CaptureWireMappingResult { + val validation = validator.validate(mutation, base) + if (validation is CaptureValidationResult.Invalid) { + return CaptureWireMappingResult.Invalid(validation.failures) + } + + val data = MobileSegment.MobileIncrementalData.CompositionTreeMutationData( + root = mutation.root.mapOrNull { it.toWireLayer() }, + adds = mutation.adds.mapOrNull { layers -> layers.map { it.toWireLayer() } }, + removes = mutation.removes.mapOrNull { identities -> identities.map(CapturedIdentity::wireId) }, + updates = mutation.updates.mapOrNull { updates -> updates.map { it.toWireUpdate() } } + ) + return CaptureWireMappingResult.Success( + MobileSegment.MobileRecord.MobileIncrementalSnapshotRecord( + timestamp = mutation.timestamp, + data = data + ) + ) + } +} + +private fun CapturedLayer.toWireLayer(): MobileSegment.CompositionLayer = + MobileSegment.CompositionLayer( + id = identity.wireId, + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + children = children.map { it.toWireChild() }, + modifiers = modifiers.map { it.toWireModifier() }.ifEmpty { null }, + compositeOperation = compositeOperation?.toWireCompositeOperation() + ) + +private fun CapturedLayerUpdate.toWireUpdate(): MobileSegment.CompositionLayerUpdate = + MobileSegment.CompositionLayerUpdate( + id = identity.wireId, + x = x.mapOrNull { it }, + y = y.mapOrNull { it }, + width = width.mapOrNull { it }, + height = height.mapOrNull { it }, + children = children.mapOrNull { children -> children.map { it.toWireChild() } }, + modifiers = modifiers.mapOrNull { modifiers -> modifiers.map { it.toWireModifier() } }, + compositeOperation = compositeOperation.mapOrNull { + it?.toWireCompositeOperation() ?: MobileSegment.CompositeOperation.SOURCEOVER + } + ) + +private fun CapturedWireframe.toWireframeOrNull(): MobileSegment.Wireframe? = when (this) { + is CapturedWireframe.Shape -> toWireframe() + is CapturedWireframe.Text -> toWireframe() + is CapturedWireframe.Pixel -> toWireframeOrNull() + is CapturedWireframe.PrivacyPlaceholder -> toWireframe() + is CapturedWireframe.WebView -> toWireframe() + is CapturedWireframe.EmbeddedContent -> toWireframe() +} + +private fun CapturedWireframe.Shape.toWireframe() = MobileSegment.Wireframe.ShapeWireframe( + id = identity.wireId, + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + clip = clip?.toWireClip(), + shapeStyle = style?.toWireShapeStyle(), + border = border?.toWireBorder(), + permanentId = permanentId +) + +private fun CapturedWireframe.Text.toWireframe() = MobileSegment.Wireframe.TextWireframe( + id = identity.wireId, + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + clip = clip?.toWireClip(), + shapeStyle = style?.toWireShapeStyle(), + border = border?.toWireBorder(), + text = text, + textStyle = textStyle.toWireTextStyle(), + textPosition = textPosition?.toWireTextPosition(), + permanentId = permanentId +) + +private fun CapturedWireframe.Pixel.toWireframeOrNull(): MobileSegment.Wireframe.ImageWireframe? = + (resource as? PixelResource.Resolved) + ?.takeIf { it.resourceId.isNotBlank() } + ?.let { resolvedResource -> + MobileSegment.Wireframe.ImageWireframe( + id = identity.wireId, + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + clip = clip?.toWireClip(), + shapeStyle = style?.toWireShapeStyle(), + border = border?.toWireBorder(), + resourceId = resolvedResource.resourceId, + mimeType = resolvedResource.mimeType, + isEmpty = isEmpty, + permanentId = permanentId + ) + } + +private fun CapturedWireframe.PrivacyPlaceholder.toWireframe() = + MobileSegment.Wireframe.PlaceholderWireframe( + id = identity.wireId, + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + clip = clip?.toWireClip(), + label = label, + permanentId = permanentId + ) + +private fun CapturedWireframe.WebView.toWireframe() = MobileSegment.Wireframe.WebviewWireframe( + id = identity.wireId, + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + clip = clip?.toWireClip(), + shapeStyle = style?.toWireShapeStyle(), + border = border?.toWireBorder(), + slotId = identity.wireId.toString(), + isVisible = isVisible, + permanentId = permanentId +) + +private fun CapturedWireframe.EmbeddedContent.toWireframe() = + MobileSegment.Wireframe.EmbeddedContentWireframe( + id = identity.wireId, + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + clip = clip?.toWireClip(), + shapeStyle = style?.toWireShapeStyle(), + border = border?.toWireBorder(), + slotId = slotId, + isVisible = isVisible, + permanentId = permanentId + ) + +private fun CapturedClip.toWireClip() = MobileSegment.WireframeClip(top, bottom, left, right) + +private fun CapturedShapeStyle.toWireShapeStyle() = MobileSegment.ShapeStyle( + backgroundColor = backgroundColor, + backgroundGradient = backgroundGradient?.toWireGradient(), + opacity = opacity, + cornerRadius = cornerRadius +) + +private fun CapturedBackgroundGradient.toWireGradient() = MobileSegment.BackgroundGradient( + stops = stops.map { MobileSegment.ShapeGradientStop(it.color, it.offset) }, + startPoint = MobileSegment.StartPoint(startPoint.x, startPoint.y), + endPoint = MobileSegment.StartPoint(endPoint.x, endPoint.y) +) + +private fun CapturedShapeBorder.toWireBorder() = MobileSegment.ShapeBorder(color, width) + +private fun CapturedTextStyle.toWireTextStyle() = MobileSegment.TextStyle( + family = family, + size = size, + color = color, + truncationMode = truncationMode?.toWireTruncationMode() +) + +private fun CapturedTruncationMode.toWireTruncationMode() = when (this) { + CapturedTruncationMode.CLIP -> MobileSegment.TruncationMode.CLIP + CapturedTruncationMode.HEAD -> MobileSegment.TruncationMode.HEAD + CapturedTruncationMode.TAIL -> MobileSegment.TruncationMode.TAIL + CapturedTruncationMode.MIDDLE -> MobileSegment.TruncationMode.MIDDLE +} + +private fun CapturedTextPosition.toWireTextPosition() = MobileSegment.TextPosition( + padding = padding?.let { MobileSegment.Padding(it.top, it.bottom, it.left, it.right) }, + alignment = alignment?.let { + MobileSegment.Alignment( + horizontal = it.horizontal?.toWireHorizontal(), + vertical = it.vertical?.toWireVertical() + ) + } +) + +private fun CapturedHorizontalAlignment.toWireHorizontal() = when (this) { + CapturedHorizontalAlignment.LEFT -> MobileSegment.Horizontal.LEFT + CapturedHorizontalAlignment.CENTER -> MobileSegment.Horizontal.CENTER + CapturedHorizontalAlignment.RIGHT -> MobileSegment.Horizontal.RIGHT +} + +private fun CapturedVerticalAlignment.toWireVertical() = when (this) { + CapturedVerticalAlignment.TOP -> MobileSegment.Vertical.TOP + CapturedVerticalAlignment.CENTER -> MobileSegment.Vertical.CENTER + CapturedVerticalAlignment.BOTTOM -> MobileSegment.Vertical.BOTTOM +} + +private fun CapturedChild.toWireChild(): MobileSegment.CompositionLayerChild = when (this) { + is CapturedChild.Layer -> MobileSegment.CompositionLayerChild( + type = MobileSegment.Type.LAYER, + id = identity.wireId + ) + + is CapturedChild.Wireframe -> MobileSegment.CompositionLayerChild( + type = MobileSegment.Type.WIREFRAME, + id = identity.wireId + ) +} + +private fun CapturedModifier.toWireModifier(): MobileSegment.CompositionLayerModifier = when (this) { + is CapturedModifier.BrightnessBias -> + MobileSegment.CompositionLayerModifier.CompositionLayerBrightnessBiasModifier(value) + + is CapturedModifier.Clip -> + MobileSegment.CompositionLayerModifier.CompositionLayerClipModifier( + path = path, + fillRule = fillRule?.toWireFillRule() + ) + + is CapturedModifier.ColorMatrix -> + MobileSegment.CompositionLayerModifier.CompositionLayerColorMatrixModifier(values) + + is CapturedModifier.GaussianBlur -> + MobileSegment.CompositionLayerModifier.CompositionLayerGaussianBlurModifier(radius) + + is CapturedModifier.MaskImage -> + MobileSegment.CompositionLayerModifier.CompositionLayerMaskImageModifier(resourceId) + + is CapturedModifier.Opacity -> + MobileSegment.CompositionLayerModifier.CompositionLayerOpacityModifier(value) + + is CapturedModifier.Saturate -> + MobileSegment.CompositionLayerModifier.CompositionLayerSaturateModifier(value) + + is CapturedModifier.Shadow -> + MobileSegment.CompositionLayerModifier.CompositionLayerShadowModifier( + color = color, + offsetX = offsetX, + offsetY = offsetY, + radius = radius, + path = path + ) +} + +private fun CapturedFillRule.toWireFillRule(): MobileSegment.FillRule = when (this) { + CapturedFillRule.EVEN_ODD -> MobileSegment.FillRule.EVENODD + CapturedFillRule.NON_ZERO -> MobileSegment.FillRule.NONZERO +} + +private fun CapturedCompositeOperation.toWireCompositeOperation(): MobileSegment.CompositeOperation = + when (this) { + CapturedCompositeOperation.DESTINATION_IN -> MobileSegment.CompositeOperation.DESTINATIONIN + CapturedCompositeOperation.DESTINATION_OUT -> MobileSegment.CompositeOperation.DESTINATIONOUT + CapturedCompositeOperation.PLUS_DARKER -> MobileSegment.CompositeOperation.PLUSDARKER + CapturedCompositeOperation.SOURCE_OVER -> MobileSegment.CompositeOperation.SOURCEOVER + } + +private fun CapturedChange.mapOrNull(transform: (T) -> R): R? = + when (this) { + is CapturedChange.Set -> transform(value) + CapturedChange.Unchanged -> null + } + +// The list contains exactly the single failure constructed here. +@Suppress("UnsafeThirdPartyFunctionCall") +private fun invalidMapping( + code: CaptureValidationErrorCode, + identity: CapturedIdentity? = null +): CaptureWireMappingResult.Invalid = CaptureWireMappingResult.Invalid( + listOf(CaptureValidationFailure(code, identity)) +) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapperTest.kt new file mode 100644 index 0000000000..900faa8d4a --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapperTest.kt @@ -0,0 +1,371 @@ +/* + * 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.composition + +import com.datadog.android.sessionreplay.model.MobileSegment +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class CapturedTreeWireMapperTest { + + private val testedMapper = DefaultCapturedTreeWireMapper() + + @Test + fun `M preserve wireframes and tree W mapFullSnapshot`() { + // Given + val tree = compositionTestTree() + + // When + val result = testedMapper.mapFullSnapshot(tree.snapshot) + + // Then + val record = (result as CaptureWireMappingResult.Success).value + assertThat(record.timestamp).isEqualTo(tree.snapshot.timestamp) + assertThat(record.data.wireframes.single()).isEqualTo( + MobileSegment.Wireframe.ShapeWireframe( + id = tree.wireframeIdentity.wireId, + x = 1, + y = 2, + width = 3, + height = 4 + ) + ) + assertThat(record.data.compositionTree?.root?.id).isEqualTo(tree.root.identity.wireId) + assertThat(record.data.compositionTree?.layers?.map { it.id }) + .containsExactly(tree.layer.identity.wireId) + assertThat(record.toJson().asJsonObject.getAsJsonObject("data").has("compositionTree")).isTrue + } + + @Test + fun `M preserve modifier ordering W mapFullSnapshot`() { + // Given + val tree = compositionTestTree() + val modifiers = listOf( + CapturedModifier.Clip("M0 0", CapturedFillRule.EVEN_ODD), + CapturedModifier.Opacity(0.5), + CapturedModifier.ColorMatrix(List(20) { it.toDouble() }), + CapturedModifier.GaussianBlur(2.0), + CapturedModifier.Shadow("#AABBCC", 1.0, 2.0, 3.0), + CapturedModifier.BrightnessBias(0.25), + CapturedModifier.Saturate(1.5), + CapturedModifier.MaskImage("resource") + ) + val layer = tree.layer.copy( + modifiers = modifiers, + compositeOperation = CapturedCompositeOperation.DESTINATION_IN + ) + + // When + val result = testedMapper.mapFullSnapshot(tree.snapshot.copy(layers = listOf(layer))) + + // Then + val record = (result as CaptureWireMappingResult.Success).value + val wireLayer = record.data.compositionTree?.layers?.single() + assertThat(wireLayer?.modifiers).containsExactly( + MobileSegment.CompositionLayerModifier.CompositionLayerClipModifier( + path = "M0 0", + fillRule = MobileSegment.FillRule.EVENODD + ), + MobileSegment.CompositionLayerModifier.CompositionLayerOpacityModifier(0.5), + MobileSegment.CompositionLayerModifier.CompositionLayerColorMatrixModifier( + List(20) { it.toDouble() } + ), + MobileSegment.CompositionLayerModifier.CompositionLayerGaussianBlurModifier(2.0), + MobileSegment.CompositionLayerModifier.CompositionLayerShadowModifier( + color = "#AABBCC", + offsetX = 1.0, + offsetY = 2.0, + radius = 3.0 + ), + MobileSegment.CompositionLayerModifier.CompositionLayerBrightnessBiasModifier(0.25), + MobileSegment.CompositionLayerModifier.CompositionLayerSaturateModifier(1.5), + MobileSegment.CompositionLayerModifier.CompositionLayerMaskImageModifier("resource") + ) + assertThat(wireLayer?.compositeOperation).isEqualTo(MobileSegment.CompositeOperation.DESTINATIONIN) + } + + @Test + fun `M preserve ordered children W mapMutation`() { + // Given + val tree = compositionTestTree() + val secondIdentity = tree.factory.placeholderWireframe(tree.layer.identity) + val secondWireframe = CapturedWireframe.PrivacyPlaceholder( + identity = secondIdentity, + bounds = CapturedBounds(5, 6, 7, 8) + ) + val baseLayer = tree.layer.copy( + children = listOf( + CapturedChild.Wireframe(tree.wireframeIdentity), + CapturedChild.Wireframe(secondIdentity) + ) + ) + val base = tree.snapshot.copy( + layers = listOf(baseLayer), + wireframes = listOf(tree.wireframe, secondWireframe) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + updates = CapturedChange.Set( + listOf( + CapturedLayerUpdate( + identity = tree.layer.identity, + children = CapturedChange.Set( + listOf( + CapturedChild.Wireframe(secondIdentity), + CapturedChild.Wireframe(tree.wireframeIdentity) + ) + ), + modifiers = CapturedChange.Set(emptyList()) + ) + ) + ) + ) + + // When + val result = testedMapper.mapMutation(mutation, base) + + // Then + val record = (result as CaptureWireMappingResult.Success).value + val data = record.data as MobileSegment.MobileIncrementalData.CompositionTreeMutationData + val update = data.updates?.single() + assertThat(update?.children?.map { it.id }) + .containsExactly(secondIdentity.wireId, tree.wireframeIdentity.wireId) + assertThat(update?.modifiers).isEmpty() + assertThat(update?.x).isNull() + assertThat(data.adds).isNull() + assertThat(data.removes).isNull() + } + + @Test + fun `M preserve absent and explicit empty fields W mapMutation`() { + // Given + val tree = compositionTestTree() + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + adds = CapturedChange.Set(emptyList()), + removes = CapturedChange.Set(emptyList()) + ) + + // When + val result = testedMapper.mapMutation(mutation, tree.snapshot) + + // Then + val record = (result as CaptureWireMappingResult.Success).value + val data = record.data as MobileSegment.MobileIncrementalData.CompositionTreeMutationData + assertThat(data.adds).isEmpty() + assertThat(data.removes).isEmpty() + assertThat(data.updates).isNull() + assertThat(data.root).isNull() + } + + @Test + fun `M map resolved pixels and privacy placeholders W mapFullSnapshot`() { + // Given + val tree = compositionTestTree() + val pixelIdentity = tree.factory.imageWireframe(tree.layer.identity) + val placeholderIdentity = tree.factory.placeholderWireframe(tree.layer.identity) + val layer = tree.layer.copy( + children = listOf( + CapturedChild.Wireframe(pixelIdentity), + CapturedChild.Wireframe(placeholderIdentity) + ) + ) + val snapshot = tree.snapshot.copy( + layers = listOf(layer), + wireframes = listOf( + CapturedWireframe.Pixel( + identity = pixelIdentity, + bounds = CapturedBounds(1, 2, 3, 4), + resource = PixelResource.Resolved("resource", "image/webp") + ), + CapturedWireframe.PrivacyPlaceholder( + identity = placeholderIdentity, + bounds = CapturedBounds(5, 6, 7, 8), + label = "Image" + ) + ) + ) + + // When + val result = testedMapper.mapFullSnapshot(snapshot) + + // Then + val wireframes = (result as CaptureWireMappingResult.Success).value.data.wireframes + assertThat(wireframes).containsExactly( + MobileSegment.Wireframe.ImageWireframe( + id = pixelIdentity.wireId, + x = 1, + y = 2, + width = 3, + height = 4, + resourceId = "resource", + mimeType = "image/webp" + ), + MobileSegment.Wireframe.PlaceholderWireframe( + id = placeholderIdentity.wireId, + x = 5, + y = 6, + width = 7, + height = 8, + label = "Image" + ) + ) + } + + @Test + fun `M map cleared composite operation to source over W mapMutation`() { + // Given + val tree = compositionTestTree() + val base = tree.snapshot.copy( + layers = listOf( + tree.layer.copy(compositeOperation = CapturedCompositeOperation.DESTINATION_IN) + ) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + updates = CapturedChange.Set( + listOf( + CapturedLayerUpdate( + identity = tree.layer.identity, + compositeOperation = CapturedChange.Set(null) + ) + ) + ) + ) + + // When + val result = testedMapper.mapMutation(mutation, base) + + // Then + val record = (result as CaptureWireMappingResult.Success).value + val data = record.data as MobileSegment.MobileIncrementalData.CompositionTreeMutationData + assertThat(data.updates?.single()?.compositeOperation) + .isEqualTo(MobileSegment.CompositeOperation.SOURCEOVER) + } + + @Test + fun `M map complete added layers W mapMutation`() { + // Given + val tree = compositionTestTree() + val addedIdentity = tree.factory.layer(tree.window, "added") + val addedLayer = tree.layer.copy(identity = addedIdentity) + val replacementRoot = tree.root.copy( + children = listOf( + CapturedChild.Layer(tree.layer.identity), + CapturedChild.Layer(addedIdentity) + ) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + root = CapturedChange.Set(replacementRoot), + adds = CapturedChange.Set(listOf(addedLayer)), + updates = CapturedChange.Set( + listOf( + CapturedLayerUpdate( + identity = tree.layer.identity, + children = CapturedChange.Set(emptyList()) + ) + ) + ) + ) + + // When + val result = testedMapper.mapMutation(mutation, tree.snapshot) + + // Then + val record = (result as CaptureWireMappingResult.Success).value + val data = record.data as MobileSegment.MobileIncrementalData.CompositionTreeMutationData + assertThat(data.adds?.single()?.id).isEqualTo(addedIdentity.wireId) + assertThat(data.adds?.single()?.children?.single()?.id) + .isEqualTo(tree.wireframeIdentity.wireId) + } + + @Test + fun `M map removed layer identities W mapMutation`() { + // Given + val tree = compositionTestTree() + val removedIdentity = tree.factory.layer(tree.window, "removed") + val removedLayer = tree.layer.copy(identity = removedIdentity, children = emptyList()) + val base = tree.snapshot.copy( + root = tree.root.copy( + children = listOf( + CapturedChild.Layer(tree.layer.identity), + CapturedChild.Layer(removedIdentity) + ) + ), + layers = listOf(tree.layer, removedLayer) + ) + val mutation = CapturedMutationSet( + timestamp = 456, + scope = tree.scope, + root = CapturedChange.Set(tree.root), + removes = CapturedChange.Set(listOf(removedIdentity)) + ) + + // When + val result = testedMapper.mapMutation(mutation, base) + + // Then + val record = (result as CaptureWireMappingResult.Success).value + val data = record.data as MobileSegment.MobileIncrementalData.CompositionTreeMutationData + assertThat(data.removes).containsExactly(removedIdentity.wireId) + } + + @Test + fun `M return structured failures W mapFullSnapshot { invalid snapshot }`() { + // Given + val snapshot = compositionTestTree().snapshot.copy(root = null) + + // When + val result = testedMapper.mapFullSnapshot(snapshot) + + // Then + assertThat(result).isInstanceOf(CaptureWireMappingResult.Invalid::class.java) + assertThat((result as CaptureWireMappingResult.Invalid).failures.map { it.code }) + .containsExactly(CaptureValidationErrorCode.MISSING_ROOT) + } + + @Test + fun `M return structured failure W mapFullSnapshot { validator accepts unresolved pixel }`() { + // Given + val tree = compositionTestTree() + val unresolvedPixel = CapturedWireframe.Pixel( + identity = tree.wireframeIdentity, + bounds = CapturedBounds(1, 2, 3, 4), + resource = PixelResource.Unresolved + ) + val mapper = DefaultCapturedTreeWireMapper( + validator = object : CapturedTreeValidator { + override fun validate(snapshot: CapturedFullSnapshot) = CaptureValidationResult.Valid + + override fun validate( + mutation: CapturedMutationSet, + base: CapturedFullSnapshot + ) = CaptureValidationResult.Valid + } + ) + + // When + val result = mapper.mapFullSnapshot(tree.snapshot.copy(wireframes = listOf(unresolvedPixel))) + + // Then + assertThat(result).isEqualTo( + CaptureWireMappingResult.Invalid( + listOf( + CaptureValidationFailure( + CaptureValidationErrorCode.UNRESOLVED_PIXEL_RESOURCE, + tree.wireframeIdentity + ) + ) + ) + ) + } +} From 26e0ac44f9721706a8c1db9a4b2f7d8e554c903a Mon Sep 17 00:00:00 2001 From: Jonathan Moskovich <48201295+jonathanmos@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:49:18 +0300 Subject: [PATCH 5/7] PANA-8500: Add composition pipeline selection --- .../api/apiSurface | 1 + .../api/dd-sdk-android-session-replay.api | 5 +- .../android/sessionreplay/SessionReplay.kt | 3 +- .../SessionReplayConfiguration.kt | 29 +++- .../_SessionReplayInternalProxy.kt | 13 ++ .../internal/DefaultRecorderProvider.kt | 52 ++++--- .../internal/SessionReplayFeature.kt | 6 +- .../composition/CompositionCapturePipeline.kt | 33 +++++ .../SessionReplayConfigurationBuilderTest.kt | 1 + .../SessionReplayInternalProxyTest.kt | 15 ++ ...essionReplayConfigurationForgeryFactory.kt | 3 +- .../internal/DefaultRecorderProviderTest.kt | 104 ++++++++++++++ .../internal/SessionReplayFeatureTest.kt | 6 +- .../CompositionCapturePipelineTest.kt | 135 ++++++++++++++++++ 14 files changed, 377 insertions(+), 29 deletions(-) create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipeline.kt create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProviderTest.kt create mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipelineTest.kt diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index 5b8afe1db4..8bcc032b2d 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -77,6 +77,7 @@ enum com.datadog.android.sessionreplay.TouchPrivacy : PrivacyLevel class com.datadog.android.sessionreplay._SessionReplayInternalProxy constructor(SessionReplayConfiguration.Builder) fun setInternalCallback(SessionReplayInternalCallback): SessionReplayConfiguration.Builder + fun setCompositionTreeRecordingEnabled(Boolean): SessionReplayConfiguration.Builder class com.datadog.android.sessionreplay.internal.TouchPrivacyManager constructor(com.datadog.android.sessionreplay.TouchPrivacy) fun addTouchOverrideArea(android.graphics.Rect, com.datadog.android.sessionreplay.TouchPrivacy) 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 d2fea1c10c..a9edf39a1f 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 @@ -64,8 +64,8 @@ public final class com/datadog/android/sessionreplay/SessionReplay { } public final class com/datadog/android/sessionreplay/SessionReplayConfiguration { - public final fun copy (Ljava/lang/String;Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;Ljava/util/List;Ljava/util/List;Ljava/util/List;FLcom/datadog/android/sessionreplay/ImagePrivacy;ZLcom/datadog/android/sessionreplay/TouchPrivacy;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;ZLcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;Ljava/lang/String;Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;Ljava/util/List;Ljava/util/List;Ljava/util/List;FLcom/datadog/android/sessionreplay/ImagePrivacy;ZLcom/datadog/android/sessionreplay/TouchPrivacy;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;ZLcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;ZILjava/lang/Object;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration; + public final fun copy (Ljava/lang/String;Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;Ljava/util/List;Ljava/util/List;Ljava/util/List;FLcom/datadog/android/sessionreplay/ImagePrivacy;ZLcom/datadog/android/sessionreplay/TouchPrivacy;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;ZLcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;ZZ)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;Ljava/lang/String;Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;Ljava/util/List;Ljava/util/List;Ljava/util/List;FLcom/datadog/android/sessionreplay/ImagePrivacy;ZLcom/datadog/android/sessionreplay/TouchPrivacy;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;ZLcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;ZZILjava/lang/Object;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -147,6 +147,7 @@ public final class com/datadog/android/sessionreplay/TouchPrivacy : java/lang/En public final class com/datadog/android/sessionreplay/_SessionReplayInternalProxy { public fun (Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder;)V + public final fun setCompositionTreeRecordingEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setInternalCallback (Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplay.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplay.kt index ae1e3420f4..967694c168 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplay.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplay.kt @@ -61,7 +61,8 @@ object SessionReplay { startRecordingImmediately = sessionReplayConfiguration.startRecordingImmediately, dynamicOptimizationEnabled = sessionReplayConfiguration.dynamicOptimizationEnabled, internalCallback = sessionReplayConfiguration.internalCallback, - heatmapsEnabled = sessionReplayConfiguration.heatmapsEnabled + heatmapsEnabled = sessionReplayConfiguration.heatmapsEnabled, + compositionTreeRecordingEnabled = sessionReplayConfiguration.compositionTreeRecordingEnabled ) if (isAlreadyRegistered()) { diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt index c34c6688c1..cd83004589 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt @@ -6,6 +6,7 @@ package com.datadog.android.sessionreplay +import android.view.View import androidx.annotation.FloatRange import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder @@ -30,7 +31,8 @@ data class SessionReplayConfiguration internal constructor( internal val dynamicOptimizationEnabled: Boolean, internal val systemRequirementsConfiguration: SystemRequirementsConfiguration, internal val internalCallback: SessionReplayInternalCallback, - internal val heatmapsEnabled: Boolean + internal val heatmapsEnabled: Boolean, + internal val compositionTreeRecordingEnabled: Boolean ) { /** @@ -78,6 +80,7 @@ data class SessionReplayConfiguration internal constructor( private var systemRequirementsConfiguration = SystemRequirementsConfiguration.NONE private var internalCallback: SessionReplayInternalCallback = NoOpSessionReplayInternalCallback() private var heatmapsEnabled = false + private var compositionTreeRecordingEnabled = false /** * Adds an extension support implementation. This is mostly used when you want to provide @@ -239,6 +242,27 @@ data class SessionReplayConfiguration internal constructor( return this } + /** + * Enables the experimental composition-tree recording pipeline. + * + * The default recorder builds replay wireframes from semantic information extracted from + * Android [View] properties. This pipeline uses the rendered composition tree as its primary + * source, preserving its structure and rendering effects to improve replay fidelity in + * Android View and Jetpack Compose applications. + * + * The underlying capture pipeline is not implemented yet: enabling this flag currently + * disables Session Replay recording entirely instead of falling back to the default + * recorder. Kept internal until the pipeline is functional; see [_SessionReplayInternalProxy]. + * + * Disabled by default. + * + * @param enabled whether composition-tree recording should be used. + */ + internal fun setCompositionTreeRecordingEnabled(enabled: Boolean): Builder { + compositionTreeRecordingEnabled = enabled + return this + } + /** * Builds a [SessionReplayConfiguration] based on the current state of this Builder. */ @@ -257,7 +281,8 @@ data class SessionReplayConfiguration internal constructor( dynamicOptimizationEnabled = dynamicOptimizationEnabled, systemRequirementsConfiguration = systemRequirementsConfiguration, internalCallback = internalCallback, - heatmapsEnabled = heatmapsEnabled + heatmapsEnabled = heatmapsEnabled, + compositionTreeRecordingEnabled = compositionTreeRecordingEnabled ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/_SessionReplayInternalProxy.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/_SessionReplayInternalProxy.kt index 9c8525a138..b8568c6d82 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/_SessionReplayInternalProxy.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/_SessionReplayInternalProxy.kt @@ -35,4 +35,17 @@ class _SessionReplayInternalProxy(private val builder: SessionReplayConfiguratio ): SessionReplayConfiguration.Builder { return builder.setInternalCallback(internalCallback) } + + /** + * Enables the experimental composition-tree recording pipeline. + * + * The underlying capture pipeline is not implemented yet: enabling this flag currently + * disables Session Replay recording entirely instead of falling back to the default recorder. + * + * @param enabled whether composition-tree recording should be used. + * @return [SessionReplayConfiguration.Builder] instance. + */ + fun setCompositionTreeRecordingEnabled(enabled: Boolean): SessionReplayConfiguration.Builder { + return builder.setCompositionTreeRecordingEnabled(enabled) + } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt index df12318dc8..e7edb7ac4a 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt @@ -27,6 +27,8 @@ import com.datadog.android.sessionreplay.ImagePrivacy import com.datadog.android.sessionreplay.MapperTypeWrapper import com.datadog.android.sessionreplay.SessionReplayInternalCallback import com.datadog.android.sessionreplay.TextAndInputPrivacy +import com.datadog.android.sessionreplay.internal.composition.CapturePipelineSelector +import com.datadog.android.sessionreplay.internal.composition.CompositionCapturePipeline import com.datadog.android.sessionreplay.internal.recorder.Recorder import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder import com.datadog.android.sessionreplay.internal.recorder.mapper.ActionBarContainerMapper @@ -67,7 +69,9 @@ internal class DefaultRecorderProvider( private val customDrawableMappers: List, private val dynamicOptimizationEnabled: Boolean, private val internalCallback: SessionReplayInternalCallback, - private val heatmapsEnabled: Boolean + private val heatmapsEnabled: Boolean, + private val compositionTreeRecordingEnabled: Boolean, + private val compositionPipelineFactory: () -> Recorder = { CompositionCapturePipeline() } ) : RecorderProvider { override fun provideSessionReplayRecorder( @@ -77,24 +81,34 @@ internal class DefaultRecorderProvider( rumContextProvider: RumContextProvider, application: Application ): Recorder { - return SessionReplayRecorder( - application, - resourceDataStoreManager = resourceDataStoreManager, - resourcesWriter = resourceWriter, - rumContextProvider = rumContextProvider, - imagePrivacy = imagePrivacy, - touchPrivacyManager = touchPrivacyManager, - textAndInputPrivacy = textAndInputPrivacy, - recordWriter = recordWriter, - timeProvider = sdkCore.timeProvider, - mappers = customMappers + builtInMappers(), - customOptionSelectorDetectors = customOptionSelectorDetectors, - customDrawableMappers = customDrawableMappers, - sdkCore = sdkCore, - dynamicOptimizationEnabled = dynamicOptimizationEnabled, - internalCallback = internalCallback, - heatmapIdentifierRegistry = if (heatmapsEnabled) LazyHeatmapIdentifierRegistry(sdkCore) else null - ) + return CapturePipelineSelector( + compositionEnabled = compositionTreeRecordingEnabled, + compositionFactory = compositionPipelineFactory, + legacyFactory = { + SessionReplayRecorder( + application, + resourceDataStoreManager = resourceDataStoreManager, + resourcesWriter = resourceWriter, + rumContextProvider = rumContextProvider, + imagePrivacy = imagePrivacy, + touchPrivacyManager = touchPrivacyManager, + textAndInputPrivacy = textAndInputPrivacy, + recordWriter = recordWriter, + timeProvider = sdkCore.timeProvider, + mappers = customMappers + builtInMappers(), + customOptionSelectorDetectors = customOptionSelectorDetectors, + customDrawableMappers = customDrawableMappers, + sdkCore = sdkCore, + dynamicOptimizationEnabled = dynamicOptimizationEnabled, + internalCallback = internalCallback, + heatmapIdentifierRegistry = if (heatmapsEnabled) { + LazyHeatmapIdentifierRegistry(sdkCore) + } else { + null + } + ) + } + ).create() } @Suppress("LongMethod") diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeature.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeature.kt index e970c94b90..d65484fc30 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeature.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeature.kt @@ -75,7 +75,8 @@ internal class SessionReplayFeature( startRecordingImmediately: Boolean, dynamicOptimizationEnabled: Boolean, internalCallback: SessionReplayInternalCallback, - heatmapsEnabled: Boolean + heatmapsEnabled: Boolean, + compositionTreeRecordingEnabled: Boolean ) : this( sdkCore, customEndpointUrl, @@ -95,7 +96,8 @@ internal class SessionReplayFeature( customDrawableMappers, dynamicOptimizationEnabled, internalCallback, - heatmapsEnabled + heatmapsEnabled, + compositionTreeRecordingEnabled ) ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipeline.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipeline.kt new file mode 100644 index 0000000000..c0183581bb --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipeline.kt @@ -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.composition + +import com.datadog.android.sessionreplay.internal.recorder.Recorder + +internal class CapturePipelineSelector( + private val compositionEnabled: Boolean, + private val legacyFactory: () -> Recorder, + private val compositionFactory: () -> Recorder +) { + fun create(): Recorder = if (compositionEnabled) compositionFactory() else legacyFactory() +} + +/** + * Lifecycle entry point for the composition capture pipeline. Traversal and capture are added by + * later workstreams; keeping this recorder isolated prevents legacy state from crossing pipelines. + */ +internal class CompositionCapturePipeline : Recorder { + override fun registerCallbacks() = Unit + + override fun unregisterCallbacks() = Unit + + override fun stopProcessingRecords() = Unit + + override fun resumeRecorders() = Unit + + override fun stopRecorders() = Unit +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt index 528b01fd3b..c9b0ea5cac 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt @@ -76,6 +76,7 @@ internal class SessionReplayConfigurationBuilderTest { assertThat(sessionReplayConfiguration.customOptionSelectorDetectors).isEmpty() assertThat(sessionReplayConfiguration.dynamicOptimizationEnabled).isEqualTo(true) assertThat(sessionReplayConfiguration.heatmapsEnabled).isFalse() + assertThat(sessionReplayConfiguration.compositionTreeRecordingEnabled).isFalse() } @Test diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalProxyTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalProxyTest.kt index 5862b6b24d..c73ec9a945 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalProxyTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalProxyTest.kt @@ -48,4 +48,19 @@ internal class SessionReplayInternalProxyTest { assertThat(result).isEqualTo(testedBuilder) assertThat(sessionReplayConfiguration.internalCallback).isEqualTo(mockInternalCallback) } + + @Test + fun `M enable composition tree recording W setCompositionTreeRecordingEnabled`() { + // Given + testedBuilder = SessionReplayConfiguration.Builder(fakeSampleRate) + testedProxy = _SessionReplayInternalProxy(testedBuilder) + + // When + val result = testedProxy.setCompositionTreeRecordingEnabled(true) + val sessionReplayConfiguration = result.build() + + // Then + assertThat(result).isEqualTo(testedBuilder) + assertThat(sessionReplayConfiguration.compositionTreeRecordingEnabled).isTrue() + } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SessionReplayConfigurationForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SessionReplayConfigurationForgeryFactory.kt index a1d2f81af8..37a8463e6c 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SessionReplayConfigurationForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SessionReplayConfigurationForgeryFactory.kt @@ -36,7 +36,8 @@ class SessionReplayConfigurationForgeryFactory : ForgeryFactory() + whenever(sdkCore.timeProvider).thenReturn(mock()) + whenever(sdkCore.internalLogger).thenReturn(mock()) + whenever(sdkCore.createSingleThreadExecutorService(any())).thenReturn(mock()) + var compositionConstructions = 0 + val provider = DefaultRecorderProvider( + sdkCore = sdkCore, + textAndInputPrivacy = TextAndInputPrivacy.MASK_ALL, + imagePrivacy = ImagePrivacy.MASK_ALL, + touchPrivacyManager = mock(), + customMappers = emptyList(), + customOptionSelectorDetectors = emptyList(), + customDrawableMappers = emptyList(), + dynamicOptimizationEnabled = false, + internalCallback = mock(), + heatmapsEnabled = false, + compositionTreeRecordingEnabled = false, + compositionPipelineFactory = { + compositionConstructions++ + mock() + } + ) + + // When + val result = provider.provideSessionReplayRecorder( + resourceDataStoreManager = mock(), + resourceWriter = mock(), + recordWriter = mock(), + rumContextProvider = mock(), + application = mock() + ) + + // Then + assertThat(result).isInstanceOf(SessionReplayRecorder::class.java) + assertThat(compositionConstructions).isZero() + } + + @Test + fun `M create only composition recorder W provideSessionReplayRecorder { flag enabled }`() { + // Given + val compositionRecorder = mock() + var compositionConstructions = 0 + val provider = DefaultRecorderProvider( + sdkCore = mock(), + textAndInputPrivacy = TextAndInputPrivacy.MASK_ALL, + imagePrivacy = ImagePrivacy.MASK_ALL, + touchPrivacyManager = mock(), + customMappers = emptyList(), + customOptionSelectorDetectors = emptyList(), + customDrawableMappers = emptyList(), + dynamicOptimizationEnabled = false, + internalCallback = mock(), + heatmapsEnabled = false, + compositionTreeRecordingEnabled = true, + compositionPipelineFactory = { + compositionConstructions++ + compositionRecorder + } + ) + + // When + val result = provider.provideSessionReplayRecorder( + resourceDataStoreManager = mock(), + resourceWriter = mock(), + recordWriter = mock(), + rumContextProvider = mock(), + application = mock() + ) + + // Then + assertThat(result).isSameAs(compositionRecorder) + assertThat(compositionConstructions).isEqualTo(1) + } +} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt index 97a524e794..02a6d4508c 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt @@ -150,7 +150,8 @@ internal class SessionReplayFeatureTest { sampleRate = fakeConfiguration.sampleRate, dynamicOptimizationEnabled = fakeConfiguration.dynamicOptimizationEnabled, internalCallback = NoOpSessionReplayInternalCallback(), - heatmapsEnabled = fakeConfiguration.heatmapsEnabled + heatmapsEnabled = fakeConfiguration.heatmapsEnabled, + compositionTreeRecordingEnabled = false ) // When @@ -179,7 +180,8 @@ internal class SessionReplayFeatureTest { startRecordingImmediately = true, dynamicOptimizationEnabled = fakeConfiguration.dynamicOptimizationEnabled, internalCallback = NoOpSessionReplayInternalCallback(), - heatmapsEnabled = fakeConfiguration.heatmapsEnabled + heatmapsEnabled = fakeConfiguration.heatmapsEnabled, + compositionTreeRecordingEnabled = false ) // When diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipelineTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipelineTest.kt new file mode 100644 index 0000000000..d3f86cba3d --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipelineTest.kt @@ -0,0 +1,135 @@ +/* + * 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.composition + +import com.datadog.android.sessionreplay.internal.recorder.Recorder +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +internal class CompositionCapturePipelineTest { + + @Test + fun `M select only legacy pipeline W create { flag disabled }`() { + // Given + val legacy = mock() + var legacyConstructions = 0 + var compositionConstructions = 0 + val selector = CapturePipelineSelector( + compositionEnabled = false, + legacyFactory = { + legacyConstructions++ + legacy + }, + compositionFactory = { + compositionConstructions++ + mock() + } + ) + + // When + val result = selector.create() + + // Then + assertThat(result).isSameAs(legacy) + assertThat(legacyConstructions).isEqualTo(1) + assertThat(compositionConstructions).isZero() + } + + @Test + fun `M select only composition pipeline W create { flag enabled }`() { + // Given + val composition = mock() + var legacyConstructions = 0 + var compositionConstructions = 0 + val selector = CapturePipelineSelector( + compositionEnabled = true, + legacyFactory = { + legacyConstructions++ + mock() + }, + compositionFactory = { + compositionConstructions++ + composition + } + ) + + // When + val result = selector.create() + + // Then + assertThat(result).isSameAs(composition) + assertThat(legacyConstructions).isZero() + assertThat(compositionConstructions).isEqualTo(1) + } + + @Test + fun `M keep recording cycle on selected pipeline W invoke recorder lifecycle`() { + // Given + val legacy = mock() + var compositionConstructions = 0 + val recorder = CapturePipelineSelector( + compositionEnabled = false, + legacyFactory = { legacy }, + compositionFactory = { + compositionConstructions++ + mock() + } + ).create() + + // When + recorder.registerCallbacks() + recorder.stopProcessingRecords() + recorder.resumeRecorders() + recorder.stopRecorders() + recorder.unregisterCallbacks() + + // Then + verify(legacy).registerCallbacks() + verify(legacy).stopProcessingRecords() + verify(legacy).resumeRecorders() + verify(legacy).stopRecorders() + verify(legacy).unregisterCallbacks() + assertThat(compositionConstructions).isZero() + } + + @Test + fun `M keep composition state isolated W create multiple pipelines`() { + // Given + val selector = CapturePipelineSelector( + compositionEnabled = true, + legacyFactory = { mock() }, + compositionFactory = { StatefulRecorder() } + ) + val first = selector.create() as StatefulRecorder + val second = selector.create() as StatefulRecorder + + // When + first.registerCallbacks() + + // Then + assertThat(first.callbacksRegistered).isTrue() + assertThat(second.callbacksRegistered).isFalse() + } + + private class StatefulRecorder : Recorder { + var callbacksRegistered = false + + override fun registerCallbacks() { + callbacksRegistered = true + } + + override fun unregisterCallbacks() = Unit + + override fun stopProcessingRecords() = Unit + + override fun resumeRecorders() = Unit + + override fun stopRecorders() = Unit + } +} From 737d49a6bfd4f8d8e26da23abeb4f0eebd53cc29 Mon Sep 17 00:00:00 2001 From: Jonathan Moskovich <48201295+jonathanmos@users.noreply.github.com> Date: Sun, 16 Aug 2026 13:37:37 +0300 Subject: [PATCH 6/7] PANA-8500: Remove embedded content from the scope --- .../api/apiSurface | 14 ---- .../api/dd-sdk-android-session-replay.api | 82 ------------------- .../embedded-content-wireframe-schema.json | 38 --------- ...edded-content-wireframe-update-schema.json | 33 -------- .../mobile/wireframe-schema.json | 3 - .../wireframe-update-mutation-schema.json | 3 - .../internal/DefaultRecorderProvider.kt | 7 +- .../internal/composition/CapturedIdentity.kt | 7 -- .../composition/CapturedMutationValidation.kt | 25 +++--- .../composition/CapturedSnapshotValidation.kt | 6 +- .../internal/composition/CapturedTree.kt | 11 --- .../composition/CapturedTreeWireMapper.kt | 16 ---- .../internal/processor/BoundsUtils.kt | 5 -- .../internal/processor/HeatmapWireframeExt.kt | 2 - .../internal/processor/MobileSegmentExt.kt | 1 - .../internal/processor/MutationResolver.kt | 32 -------- .../internal/processor/WireframeUtils.kt | 1 - .../internal/utils/WireframeExt.kt | 3 - .../EmbeddedContentWireframeForgeryFactory.kt | 26 ------ .../sessionreplay/forge/ForgeConfigurator.kt | 1 - .../forge/WireframeForgeryFactory.kt | 5 +- .../composition/CapturedIdentityTest.kt | 4 +- .../composition/CapturedTreeValidatorTest.kt | 16 +--- .../internal/processor/BoundUtilsTest.kt | 6 -- .../processor/MutationResolverTest.kt | 2 - .../internal/processor/NodeFlattenerTest.kt | 13 --- .../internal/processor/WireframeUtilsTest.kt | 5 -- .../internal/utils/WireframeExtTest.kt | 1 - 28 files changed, 25 insertions(+), 343 deletions(-) delete mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-schema.json delete mode 100644 features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-update-schema.json delete mode 100644 features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/EmbeddedContentWireframeForgeryFactory.kt diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index 8bcc032b2d..edee468432 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -338,13 +338,6 @@ data class com.datadog.android.sessionreplay.model.MobileSegment companion object fun fromJson(kotlin.String): WebviewWireframe fun fromJsonObject(com.google.gson.JsonObject): WebviewWireframe - data class EmbeddedContentWireframe : Wireframe - constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, kotlin.Boolean? = null, kotlin.String? = null) - val type: kotlin.String - override fun toJson(): com.google.gson.JsonElement - companion object - fun fromJson(kotlin.String): EmbeddedContentWireframe - fun fromJsonObject(com.google.gson.JsonObject): EmbeddedContentWireframe companion object fun fromJson(kotlin.String): Wireframe fun fromJsonElement(com.google.gson.JsonElement): Wireframe @@ -403,13 +396,6 @@ data class com.datadog.android.sessionreplay.model.MobileSegment companion object fun fromJson(kotlin.String): WebviewWireframeUpdate fun fromJsonObject(com.google.gson.JsonObject): WebviewWireframeUpdate - data class EmbeddedContentWireframeUpdate : WireframeUpdateMutation - constructor(kotlin.Long, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, kotlin.Long? = null, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, kotlin.Boolean? = null) - val type: kotlin.String - override fun toJson(): com.google.gson.JsonElement - companion object - fun fromJson(kotlin.String): EmbeddedContentWireframeUpdate - fun fromJsonObject(com.google.gson.JsonObject): EmbeddedContentWireframeUpdate companion object fun fromJson(kotlin.String): WireframeUpdateMutation fun fromJsonElement(com.google.gson.JsonElement): WireframeUpdateMutation 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 a9edf39a1f..e59cb3e4f4 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 @@ -1419,48 +1419,6 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun fromJsonElement (Lcom/google/gson/JsonElement;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; } -public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe : com/datadog/android/sessionreplay/model/MobileSegment$Wireframe { - public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe$Companion; - public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)V - public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()J - public final fun component10 ()Ljava/lang/Boolean; - public final fun component11 ()Ljava/lang/String; - public final fun component2 ()J - public final fun component3 ()J - public final fun component4 ()J - public final fun component5 ()J - public final fun component6 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; - public final fun component7 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; - public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; - public final fun component9 ()Ljava/lang/String; - public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; - public fun equals (Ljava/lang/Object;)Z - public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; - public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; - public final fun getBorder ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; - public final fun getClip ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; - public final fun getHeight ()J - public final fun getId ()J - public final fun getPermanentId ()Ljava/lang/String; - public final fun getShapeStyle ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; - public final fun getSlotId ()Ljava/lang/String; - public final fun getType ()Ljava/lang/String; - public final fun getWidth ()J - public final fun getX ()J - public final fun getY ()J - public fun hashCode ()I - public final fun isVisible ()Ljava/lang/Boolean; - public fun toJson ()Lcom/google/gson/JsonElement; - public fun toString ()Ljava/lang/String; -} - -public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe$Companion { - public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; - public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$EmbeddedContentWireframe; -} - public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe : com/datadog/android/sessionreplay/model/MobileSegment$Wireframe { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe$Companion; public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)V @@ -1713,46 +1671,6 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun fromJsonElement (Lcom/google/gson/JsonElement;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation; } -public final class com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate : com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation { - public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate$Companion; - public fun (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;)V - public synthetic fun (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()J - public final fun component10 ()Ljava/lang/Boolean; - public final fun component2 ()Ljava/lang/Long; - public final fun component3 ()Ljava/lang/Long; - public final fun component4 ()Ljava/lang/Long; - public final fun component5 ()Ljava/lang/Long; - public final fun component6 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; - public final fun component7 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; - public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; - public final fun component9 ()Ljava/lang/String; - public final fun copy (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate;JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; - public fun equals (Ljava/lang/Object;)Z - public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; - public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; - public final fun getBorder ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; - public final fun getClip ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; - public final fun getHeight ()Ljava/lang/Long; - public final fun getId ()J - public final fun getShapeStyle ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; - public final fun getSlotId ()Ljava/lang/String; - public final fun getType ()Ljava/lang/String; - public final fun getWidth ()Ljava/lang/Long; - public final fun getX ()Ljava/lang/Long; - public final fun getY ()Ljava/lang/Long; - public fun hashCode ()I - public final fun isVisible ()Ljava/lang/Boolean; - public fun toJson ()Lcom/google/gson/JsonElement; - public fun toString ()Ljava/lang/String; -} - -public final class com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate$Companion { - public final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; - public final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$EmbeddedContentWireframeUpdate; -} - public final class com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$ImageWireframeUpdate : com/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeUpdateMutation$ImageWireframeUpdate$Companion; public fun (JLjava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Ljava/lang/Long;Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-schema.json deleted file mode 100644 index f8ad37f617..0000000000 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-schema.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "session-replay/mobile/embedded-content-wireframe-schema.json", - "title": "EmbeddedContentWireframe", - "type": "object", - "description": "Schema of all properties of an EmbeddedContentWireframe.", - "allOf": [ - { - "$ref": "_common-shape-wireframe-schema.json" - }, - { - "required": ["type", "slotId"], - "properties": { - "type": { - "type": "string", - "description": "The type of the wireframe.", - "const": "embedded_content", - "readOnly": true - }, - "slotId": { - "type": "string", - "description": "Unique Id of the slot containing this embedded content.", - "readOnly": true - }, - "isVisible": { - "type": "boolean", - "description": "Whether this embedded content is visible or not.", - "readOnly": true - }, - "permanentId": { - "type": "string", - "description": "A globally unique and stable identifier for this UI element, computed as the hash of the element's path. Used to correlate wireframes with RUM action events.", - "readOnly": true - } - } - } - ] -} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-update-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-update-schema.json deleted file mode 100644 index 6b0fa9b772..0000000000 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/embedded-content-wireframe-update-schema.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "session-replay/mobile/embedded-content-wireframe-update-schema.json", - "title": "EmbeddedContentWireframeUpdate", - "type": "object", - "description": "Schema of all properties of an EmbeddedContentWireframeUpdate.", - "allOf": [ - { - "$ref": "_common-shape-wireframe-update-schema.json" - }, - { - "required": ["type", "slotId"], - "properties": { - "type": { - "type": "string", - "description": "The type of the wireframe.", - "const": "embedded_content", - "readOnly": true - }, - "slotId": { - "type": "string", - "description": "Unique Id of the slot containing this embedded content.", - "readOnly": true - }, - "isVisible": { - "type": "boolean", - "description": "Whether this embedded content is visible or not.", - "readOnly": true - } - } - } - ] -} diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-schema.json index d8d7b0dbdc..b3ba3a763f 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-schema.json @@ -19,9 +19,6 @@ }, { "$ref": "webview-wireframe-schema.json" - }, - { - "$ref": "embedded-content-wireframe-schema.json" } ] } diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-update-mutation-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-update-mutation-schema.json index 10f3eebb96..65f921351a 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-update-mutation-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/wireframe-update-mutation-schema.json @@ -19,9 +19,6 @@ }, { "$ref": "webview-wireframe-update-schema.json" - }, - { - "$ref": "embedded-content-wireframe-update-schema.json" } ] } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt index e7edb7ac4a..65130b324b 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt @@ -81,6 +81,7 @@ internal class DefaultRecorderProvider( rumContextProvider: RumContextProvider, application: Application ): Recorder { + val heatmapIdentifierRegistry = if (heatmapsEnabled) LazyHeatmapIdentifierRegistry(sdkCore) else null return CapturePipelineSelector( compositionEnabled = compositionTreeRecordingEnabled, compositionFactory = compositionPipelineFactory, @@ -101,11 +102,7 @@ internal class DefaultRecorderProvider( sdkCore = sdkCore, dynamicOptimizationEnabled = dynamicOptimizationEnabled, internalCallback = internalCallback, - heatmapIdentifierRegistry = if (heatmapsEnabled) { - LazyHeatmapIdentifierRegistry(sdkCore) - } else { - null - } + heatmapIdentifierRegistry = heatmapIdentifierRegistry ) } ).create() diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt index bec16028db..64bea2309d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt @@ -24,7 +24,6 @@ internal enum class CapturedWireframeKind(internal val wireIdNamespace: Long) { TEXT(2), IMAGE(IMAGE_WIREFRAME_NAMESPACE), PLACEHOLDER(PLACEHOLDER_WIREFRAME_NAMESPACE), - EMBEDDED_CONTENT(EMBEDDED_CONTENT_WIREFRAME_NAMESPACE), /** * Deliberately namespace `0`, i.e. unshifted: the wire id for a web-view wireframe must equal @@ -91,8 +90,6 @@ internal interface CapturedIdentityFactory { fun placeholderWireframe(owner: CapturedIdentity): CapturedIdentity - fun embeddedContentWireframe(owner: CapturedIdentity): CapturedIdentity - fun webViewWireframe(owner: CapturedIdentity, slotId: Long): CapturedIdentity } @@ -141,9 +138,6 @@ internal class DefaultCapturedIdentityFactory( override fun placeholderWireframe(owner: CapturedIdentity): CapturedIdentity = createNamespacedWireframeIdentity(owner, CapturedWireframeKind.PLACEHOLDER) - override fun embeddedContentWireframe(owner: CapturedIdentity): CapturedIdentity = - createNamespacedWireframeIdentity(owner, CapturedWireframeKind.EMBEDDED_CONTENT) - override fun webViewWireframe(owner: CapturedIdentity, slotId: Long): CapturedIdentity { requireLayerOwner(owner) return createIdentity( @@ -246,7 +240,6 @@ internal class DefaultCapturedIdentityFactory( private const val IMAGE_WIREFRAME_NAMESPACE = 3L private const val PLACEHOLDER_WIREFRAME_NAMESPACE = 4L -private const val EMBEDDED_CONTENT_WIREFRAME_NAMESPACE = 5L /** * Added to every raw layer replay id so that layer wire ids always land above the Int range diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutationValidation.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutationValidation.kt index 94e4f427fd..675b8b50fd 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutationValidation.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutationValidation.kt @@ -152,20 +152,23 @@ internal class CapturedMutationValidation( ) } - private fun CapturedLayer.apply(update: CapturedLayerUpdate): CapturedLayer = copy( - bounds = bounds.copy( - x = update.x.valueOr(bounds.x), - y = update.y.valueOr(bounds.y), - width = update.width.valueOr(bounds.width), - height = update.height.valueOr(bounds.height) - ), - children = update.children.valueOr(children), - modifiers = update.modifiers.valueOr(modifiers), - compositeOperation = when (val operation = update.compositeOperation) { + private fun CapturedLayer.apply(update: CapturedLayerUpdate): CapturedLayer { + val effectiveCompositeOperation = when (val operation = update.compositeOperation) { is CapturedChange.Set -> operation.value CapturedChange.Unchanged -> compositeOperation } - ) + return copy( + bounds = bounds.copy( + x = update.x.valueOr(bounds.x), + y = update.y.valueOr(bounds.y), + width = update.width.valueOr(bounds.width), + height = update.height.valueOr(bounds.height) + ), + children = update.children.valueOr(children), + modifiers = update.modifiers.valueOr(modifiers), + compositeOperation = effectiveCompositeOperation + ) + } private fun CapturedChange>.valueOrEmpty(): List = (this as? CapturedChange.Set)?.value.orEmpty() diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedSnapshotValidation.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedSnapshotValidation.kt index 71baff1704..37472dce57 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedSnapshotValidation.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedSnapshotValidation.kt @@ -53,11 +53,12 @@ internal class CapturedSnapshotValidation( } private fun validateDefinitions(layers: List) { - val identities = layers.map { it.identity } + if (validateWireframeDefinitions) { + val wireframeIdentities = if (validateWireframeDefinitions) { snapshot.wireframes.map { it.identity } } else { emptyList() } + val identities = layers.map { it.identity } + wireframeIdentities validateIdentityDefinitions(identities) layers.forEach(::validateLayer) if (validateWireframeDefinitions) snapshot.wireframes.forEach(::validateWireframe) @@ -378,7 +379,6 @@ internal class CapturedSnapshotValidation( private val CapturedWireframe.isHiddenSlot: Boolean get() = when (this) { - is CapturedWireframe.EmbeddedContent -> isVisible == false is CapturedWireframe.WebView -> isVisible == false else -> false } @@ -404,7 +404,6 @@ internal class CapturedSnapshotValidation( is CapturedWireframe.Pixel -> CapturedWireframeKind.IMAGE is CapturedWireframe.PrivacyPlaceholder -> CapturedWireframeKind.PLACEHOLDER is CapturedWireframe.WebView -> CapturedWireframeKind.WEB_VIEW - is CapturedWireframe.EmbeddedContent -> CapturedWireframeKind.EMBEDDED_CONTENT } private val CapturedWireframe.style: CapturedShapeStyle? @@ -412,7 +411,6 @@ internal class CapturedSnapshotValidation( is CapturedWireframe.Shape -> style is CapturedWireframe.Text -> style is CapturedWireframe.WebView -> style - is CapturedWireframe.EmbeddedContent -> style is CapturedWireframe.Pixel -> style is CapturedWireframe.PrivacyPlaceholder -> null } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTree.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTree.kt index 0031c3ceaf..a0df331812 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTree.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTree.kt @@ -88,17 +88,6 @@ internal sealed interface CapturedWireframe { override val permanentId: String? = null ) : CapturedWireframe - data class EmbeddedContent( - override val identity: CapturedIdentity, - override val bounds: CapturedBounds, - override val clip: CapturedClip? = null, - val style: CapturedShapeStyle? = null, - val border: CapturedShapeBorder? = null, - val slotId: String, - val isVisible: Boolean? = null, - override val permanentId: String? = null - ) : CapturedWireframe - data class Pixel( override val identity: CapturedIdentity, override val bounds: CapturedBounds, diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapper.kt index ad18f4992c..59cfd752bb 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapper.kt @@ -122,7 +122,6 @@ private fun CapturedWireframe.toWireframeOrNull(): MobileSegment.Wireframe? = wh is CapturedWireframe.Pixel -> toWireframeOrNull() is CapturedWireframe.PrivacyPlaceholder -> toWireframe() is CapturedWireframe.WebView -> toWireframe() - is CapturedWireframe.EmbeddedContent -> toWireframe() } private fun CapturedWireframe.Shape.toWireframe() = MobileSegment.Wireframe.ShapeWireframe( @@ -198,21 +197,6 @@ private fun CapturedWireframe.WebView.toWireframe() = MobileSegment.Wireframe.We permanentId = permanentId ) -private fun CapturedWireframe.EmbeddedContent.toWireframe() = - MobileSegment.Wireframe.EmbeddedContentWireframe( - id = identity.wireId, - x = bounds.x, - y = bounds.y, - width = bounds.width, - height = bounds.height, - clip = clip?.toWireClip(), - shapeStyle = style?.toWireShapeStyle(), - border = border?.toWireBorder(), - slotId = slotId, - isVisible = isVisible, - permanentId = permanentId - ) - private fun CapturedClip.toWireClip() = MobileSegment.WireframeClip(top, bottom, left, right) private fun CapturedShapeStyle.toWireShapeStyle() = MobileSegment.ShapeStyle( diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundsUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundsUtils.kt index efba06a43b..7df20d312b 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundsUtils.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundsUtils.kt @@ -12,7 +12,6 @@ internal object BoundsUtils { internal fun resolveBounds(wireframe: MobileSegment.Wireframe): WireframeBounds { return when (wireframe) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> wireframe.bounds() is MobileSegment.Wireframe.ShapeWireframe -> wireframe.bounds() is MobileSegment.Wireframe.TextWireframe -> wireframe.bounds() is MobileSegment.Wireframe.ImageWireframe -> wireframe.bounds() @@ -35,10 +34,6 @@ internal object BoundsUtils { return resolveBounds(x, y, width, height, clip) } - private fun MobileSegment.Wireframe.EmbeddedContentWireframe.bounds(): WireframeBounds { - return resolveBounds(x, y, width, height, clip) - } - private fun MobileSegment.Wireframe.TextWireframe.bounds(): WireframeBounds { return resolveBounds(x, y, width, height, clip) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/HeatmapWireframeExt.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/HeatmapWireframeExt.kt index 85cd8a5fa1..af869514ed 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/HeatmapWireframeExt.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/HeatmapWireframeExt.kt @@ -10,7 +10,6 @@ import com.datadog.android.sessionreplay.model.MobileSegment internal fun MobileSegment.Wireframe.permanentId(): String? { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.permanentId is MobileSegment.Wireframe.ShapeWireframe -> this.permanentId is MobileSegment.Wireframe.TextWireframe -> this.permanentId is MobileSegment.Wireframe.ImageWireframe -> this.permanentId @@ -23,7 +22,6 @@ internal fun MobileSegment.Wireframe.copyWithPermanentId( permanentId: String? ): MobileSegment.Wireframe { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(permanentId = permanentId) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(permanentId = permanentId) is MobileSegment.Wireframe.TextWireframe -> this.copy(permanentId = permanentId) is MobileSegment.Wireframe.ImageWireframe -> this.copy(permanentId = permanentId) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt index 7021b8370a..554f7e7070 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MobileSegmentExt.kt @@ -11,7 +11,6 @@ import com.datadog.android.sessionreplay.model.MobileSegment internal fun MobileSegment.Wireframe.copy(clip: MobileSegment.WireframeClip?): MobileSegment.Wireframe { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(clip = clip) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(clip = clip) is MobileSegment.Wireframe.TextWireframe -> this.copy(clip = clip) is MobileSegment.Wireframe.ImageWireframe -> this.copy(clip = clip) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolver.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolver.kt index ba6272eef9..39c671c831 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolver.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolver.kt @@ -345,31 +345,6 @@ internal class MutationResolver(private val internalLogger: InternalLogger) { return mutation } - private fun resolveEmbeddedContentWireframeMutation( - prevWireframe: MobileSegment.Wireframe.EmbeddedContentWireframe, - currentWireframe: MobileSegment.Wireframe.EmbeddedContentWireframe - ): MobileSegment.WireframeUpdateMutation { - var mutation = MobileSegment.WireframeUpdateMutation.EmbeddedContentWireframeUpdate( - id = currentWireframe.id, - slotId = currentWireframe.slotId - ) - if (prevWireframe.x != currentWireframe.x) mutation = mutation.copy(x = currentWireframe.x) - if (prevWireframe.y != currentWireframe.y) mutation = mutation.copy(y = currentWireframe.y) - if (prevWireframe.width != currentWireframe.width) mutation = mutation.copy(width = currentWireframe.width) - if (prevWireframe.height != currentWireframe.height) mutation = mutation.copy(height = currentWireframe.height) - if (prevWireframe.border != currentWireframe.border) mutation = mutation.copy(border = currentWireframe.border) - if (prevWireframe.shapeStyle != currentWireframe.shapeStyle) { - mutation = mutation.copy(shapeStyle = currentWireframe.shapeStyle) - } - if (prevWireframe.clip != currentWireframe.clip) { - mutation = mutation.copy(clip = currentWireframe.clip ?: MobileSegment.WireframeClip(0, 0, 0, 0)) - } - if (prevWireframe.isVisible != currentWireframe.isVisible) { - mutation = mutation.copy(isVisible = currentWireframe.isVisible) - } - return mutation - } - private fun recordChangedWireframeMutations( oldElement: MobileSegment.Wireframe, newElement: MobileSegment.Wireframe, @@ -415,12 +390,6 @@ internal class MutationResolver(private val internalLogger: InternalLogger) { null } else { when (prevWireframe) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> - resolveEmbeddedContentWireframeMutation( - prevWireframe, - currentWireframe as MobileSegment.Wireframe.EmbeddedContentWireframe - ) - is MobileSegment.Wireframe.TextWireframe -> resolveTextMutation( prevWireframe, currentWireframe as MobileSegment.Wireframe.TextWireframe @@ -498,7 +467,6 @@ internal class MutationResolver(private val internalLogger: InternalLogger) { @Suppress("FunctionMinLength") private fun MobileSegment.Wireframe.id(): Long { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.id is MobileSegment.Wireframe.ShapeWireframe -> this.id is MobileSegment.Wireframe.TextWireframe -> this.id is MobileSegment.Wireframe.ImageWireframe -> this.id diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtils.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtils.kt index c9b2a81267..d0460c4f28 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtils.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtils.kt @@ -73,7 +73,6 @@ internal class WireframeUtils(private val boundsUtils: BoundsUtils = BoundsUtils private fun MobileSegment.Wireframe.clip(): MobileSegment.WireframeClip? { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.clip is MobileSegment.Wireframe.ShapeWireframe -> this.clip is MobileSegment.Wireframe.TextWireframe -> this.clip is MobileSegment.Wireframe.ImageWireframe -> this.clip diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExt.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExt.kt index 2c5d67261b..7aeef128f7 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExt.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExt.kt @@ -10,7 +10,6 @@ import com.datadog.android.sessionreplay.model.MobileSegment internal fun MobileSegment.Wireframe.hasOpaqueBackground(): Boolean { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.shapeStyle.isOpaque() // we return false from ImageWireframe because we don't know if the image is opaque or not // and ImageWireframe ShapeStyle is always null is MobileSegment.Wireframe.ImageWireframe -> false @@ -35,7 +34,6 @@ private fun MobileSegment.ShapeStyle?.isOpaque(): Boolean { internal fun MobileSegment.Wireframe.shapeStyle(): MobileSegment.ShapeStyle? { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.shapeStyle is MobileSegment.Wireframe.TextWireframe -> this.shapeStyle is MobileSegment.Wireframe.ShapeWireframe -> this.shapeStyle is MobileSegment.Wireframe.ImageWireframe -> this.shapeStyle @@ -46,7 +44,6 @@ internal fun MobileSegment.Wireframe.shapeStyle(): MobileSegment.ShapeStyle? { internal fun MobileSegment.Wireframe.copy(shapeStyle: MobileSegment.ShapeStyle?): MobileSegment.Wireframe { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.TextWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.ImageWireframe -> this.copy(shapeStyle = shapeStyle) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/EmbeddedContentWireframeForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/EmbeddedContentWireframeForgeryFactory.kt deleted file mode 100644 index 7afb6ac551..0000000000 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/EmbeddedContentWireframeForgeryFactory.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.model.MobileSegment -import fr.xgouchet.elmyr.Forge -import fr.xgouchet.elmyr.ForgeryFactory - -internal class EmbeddedContentWireframeForgeryFactory : - ForgeryFactory { - - override fun getForgery(forge: Forge): MobileSegment.Wireframe.EmbeddedContentWireframe { - return MobileSegment.Wireframe.EmbeddedContentWireframe( - id = forge.aLong(), - x = forge.aLong(), - y = forge.aLong(), - width = forge.aPositiveLong(), - height = forge.aPositiveLong(), - slotId = forge.anAlphabeticalString() - ) - } -} diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/ForgeConfigurator.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/ForgeConfigurator.kt index 0ca3b52189..996433a772 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/ForgeConfigurator.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/ForgeConfigurator.kt @@ -25,7 +25,6 @@ internal class ForgeConfigurator : BaseConfigurator() { forge.addFactory(SessionReplayRumContextForgeryFactory()) forge.addFactory(ShapeWireframeForgeryFactory()) forge.addFactory(TextWireframeForgeryFactory()) - forge.addFactory(EmbeddedContentWireframeForgeryFactory()) forge.addFactory(WireframeForgeryFactory()) forge.addFactory(TextWireframeMutationForgeryFactory()) forge.addFactory(ShapeWireframeMutationForgeryFactory()) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/WireframeForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/WireframeForgeryFactory.kt index 3318f8d7c9..79ad4fd3a5 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/WireframeForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/WireframeForgeryFactory.kt @@ -12,12 +12,11 @@ import fr.xgouchet.elmyr.ForgeryFactory internal class WireframeForgeryFactory : ForgeryFactory { override fun getForgery(forge: Forge): MobileSegment.Wireframe { - return when (forge.anInt(min = 0, max = 5)) { + return when (forge.anInt(min = 0, max = 4)) { 0 -> forge.getForgery() 1 -> forge.getForgery() 2 -> forge.getForgery() - 3 -> forge.getForgery() - 4 -> forge.getForgery() + 3 -> forge.getForgery() else -> forge.getForgery() } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt index 3cb6f6245c..7fbdb7c9f1 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt @@ -128,14 +128,12 @@ internal class CapturedIdentityTest { val text = factory.textWireframe(layer) val image = factory.imageWireframe(layer) val placeholder = factory.placeholderWireframe(layer) - val embeddedContent = factory.embeddedContentWireframe(layer) // Then assertThat(shape.wireId).isEqualTo((1L shl NAMESPACE_SHIFT) or layer.wireId) assertThat(text.wireId).isEqualTo((2L shl NAMESPACE_SHIFT) or layer.wireId) assertThat(image.wireId).isEqualTo((3L shl NAMESPACE_SHIFT) or layer.wireId) assertThat(placeholder.wireId).isEqualTo((4L shl NAMESPACE_SHIFT) or layer.wireId) - assertThat(embeddedContent.wireId).isEqualTo((5L shl NAMESPACE_SHIFT) or layer.wireId) } @Test @@ -202,7 +200,7 @@ internal class CapturedIdentityTest { // When val identities = List(1_000) { factory.view(window, "view-$it") } + - factory.embeddedContentWireframe(window) + factory.placeholderWireframe(window) // Then assertThat(identities.map { it.wireId }) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt index 507776134c..0c1637652c 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt @@ -637,23 +637,16 @@ internal class CapturedTreeValidatorTest { // Given val tree = compositionTestTree() val webViewIdentity = tree.factory.webViewWireframe(tree.layer.identity, 42) - val embeddedIdentity = tree.factory.embeddedContentWireframe(tree.layer.identity) val hiddenWebView = CapturedWireframe.WebView( identity = webViewIdentity, bounds = CapturedBounds(0, 0, 0, 0), isVisible = false ) - val hiddenEmbeddedContent = CapturedWireframe.EmbeddedContent( - identity = embeddedIdentity, - bounds = CapturedBounds(0, 0, 0, 0), - slotId = "embedded-slot", - isVisible = false - ) // When val result = testedValidator.validate( tree.snapshot.copy( - wireframes = listOf(tree.wireframe, hiddenWebView, hiddenEmbeddedContent) + wireframes = listOf(tree.wireframe, hiddenWebView) ) ) @@ -665,17 +658,16 @@ internal class CapturedTreeValidatorTest { fun `M report unreferenced wireframe W validate { visible slot wireframe }`() { // Given val tree = compositionTestTree() - val identity = tree.factory.embeddedContentWireframe(tree.layer.identity) - val visibleEmbeddedContent = CapturedWireframe.EmbeddedContent( + val identity = tree.factory.webViewWireframe(tree.layer.identity, 42) + val visibleWebView = CapturedWireframe.WebView( identity = identity, bounds = CapturedBounds(0, 0, 10, 10), - slotId = "embedded-slot", isVisible = true ) // When val result = testedValidator.validate( - tree.snapshot.copy(wireframes = listOf(tree.wireframe, visibleEmbeddedContent)) + tree.snapshot.copy(wireframes = listOf(tree.wireframe, visibleWebView)) ) // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundUtilsTest.kt index c097edb922..be0c8a62ce 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/BoundUtilsTest.kt @@ -143,7 +143,6 @@ internal class BoundUtilsTest { private fun MobileSegment.Wireframe.x(): Long { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.x is MobileSegment.Wireframe.ShapeWireframe -> this.x is MobileSegment.Wireframe.TextWireframe -> this.x is MobileSegment.Wireframe.ImageWireframe -> this.x @@ -154,7 +153,6 @@ internal class BoundUtilsTest { private fun MobileSegment.Wireframe.y(): Long { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.y is MobileSegment.Wireframe.ShapeWireframe -> this.y is MobileSegment.Wireframe.TextWireframe -> this.y is MobileSegment.Wireframe.ImageWireframe -> this.y @@ -165,7 +163,6 @@ internal class BoundUtilsTest { private fun MobileSegment.Wireframe.width(): Long { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.width is MobileSegment.Wireframe.ShapeWireframe -> this.width is MobileSegment.Wireframe.TextWireframe -> this.width is MobileSegment.Wireframe.ImageWireframe -> this.width @@ -176,7 +173,6 @@ internal class BoundUtilsTest { private fun MobileSegment.Wireframe.height(): Long { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.height is MobileSegment.Wireframe.ShapeWireframe -> this.height is MobileSegment.Wireframe.TextWireframe -> this.height is MobileSegment.Wireframe.ImageWireframe -> this.height @@ -193,8 +189,6 @@ internal class BoundUtilsTest { height: Long ): MobileSegment.Wireframe { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> - this.copy(clip = clip, x = x, y = y, width = width, height = height) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(clip = clip, x = x, y = y, width = width, height = height) is MobileSegment.Wireframe.TextWireframe -> diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt index 4796011f14..a125cf58ab 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/MutationResolverTest.kt @@ -775,7 +775,6 @@ internal class MutationResolverTest { private fun MobileSegment.Wireframe.id(): Long { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.id is MobileSegment.Wireframe.ShapeWireframe -> this.id is MobileSegment.Wireframe.TextWireframe -> this.id is MobileSegment.Wireframe.ImageWireframe -> this.id @@ -805,7 +804,6 @@ internal class MutationResolverTest { private fun MobileSegment.Wireframe.id(): Long { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.id is MobileSegment.Wireframe.ShapeWireframe -> this.id is MobileSegment.Wireframe.TextWireframe -> this.id is MobileSegment.Wireframe.ImageWireframe -> this.id diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/NodeFlattenerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/NodeFlattenerTest.kt index 374a627054..3da3465a17 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/NodeFlattenerTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/NodeFlattenerTest.kt @@ -322,7 +322,6 @@ internal class NodeFlattenerTest { private fun MobileSegment.Wireframe.copy(id: Long): MobileSegment.Wireframe { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(id = id) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(id = id) is MobileSegment.Wireframe.TextWireframe -> @@ -372,14 +371,6 @@ internal class NodeFlattenerTest { private fun Forge.forgeWireframe(id: Long, x: Long, y: Long, width: Long, height: Long): MobileSegment.Wireframe { return when (val fakeWireframe = getForgery()) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> fakeWireframe.copy( - id = id, - x = x, - y = y, - width = width, - height = height, - clip = null - ) is MobileSegment.Wireframe.ShapeWireframe -> fakeWireframe.copy( id = id, x = x, @@ -425,8 +416,6 @@ internal class NodeFlattenerTest { private fun MobileSegment.Wireframe.bounds(): Bounds { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> - Bounds(this.x, this.y, this.width, this.height) is MobileSegment.Wireframe.ShapeWireframe -> Bounds(this.x, this.y, this.width, this.height) is MobileSegment.Wireframe.TextWireframe -> @@ -442,7 +431,6 @@ internal class NodeFlattenerTest { private fun MobileSegment.Wireframe.clip(): MobileSegment.WireframeClip? { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> clip is MobileSegment.Wireframe.ShapeWireframe -> clip is MobileSegment.Wireframe.TextWireframe -> @@ -460,7 +448,6 @@ internal class NodeFlattenerTest { private fun MobileSegment.Wireframe.id(): Long { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.id is MobileSegment.Wireframe.ShapeWireframe -> this.id is MobileSegment.Wireframe.TextWireframe -> this.id is MobileSegment.Wireframe.ImageWireframe -> this.id diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt index e78892d5cc..14f028df43 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/processor/WireframeUtilsTest.kt @@ -689,7 +689,6 @@ internal class WireframeUtilsTest { private fun MobileSegment.Wireframe.clip(): MobileSegment.WireframeClip? { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> clip?.normalized() is MobileSegment.Wireframe.ShapeWireframe -> clip?.normalized() is MobileSegment.Wireframe.TextWireframe -> clip?.normalized() is MobileSegment.Wireframe.ImageWireframe -> clip?.normalized() @@ -723,10 +722,6 @@ internal class WireframeUtilsTest { shapeStyle: MobileSegment.ShapeStyle? ): MobileSegment.Wireframe { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> copy( - shapeStyle = shapeStyle - ) - is MobileSegment.Wireframe.ShapeWireframe -> copy( shapeStyle = shapeStyle ) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExtTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExtTest.kt index ec5b1ba048..de2e50290f 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExtTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/utils/WireframeExtTest.kt @@ -202,7 +202,6 @@ internal class WireframeExtTest { private fun MobileSegment.Wireframe.testCopy(shapeStyle: MobileSegment.ShapeStyle?): MobileSegment.Wireframe { return when (this) { - is MobileSegment.Wireframe.EmbeddedContentWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.TextWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.ShapeWireframe -> this.copy(shapeStyle = shapeStyle) is MobileSegment.Wireframe.ImageWireframe -> this.copy(shapeStyle = shapeStyle) From 2ebd855669ec327afb90ceba6c67b510e3b107ef Mon Sep 17 00:00:00 2001 From: Jonathan Moskovich <48201295+jonathanmos@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:43:02 +0300 Subject: [PATCH 7/7] PANA-8500: PR review fixes --- .../api/apiSurface | 3 +- .../api/dd-sdk-android-session-replay.api | 5 +- .../ExperimentalSessionReplayApi.kt | 18 +++++ .../SessionReplayConfiguration.kt | 7 +- .../_SessionReplayInternalProxy.kt | 13 --- .../internal/composition/CapturedIdentity.kt | 80 +++++++++++------- .../SessionReplayConfigurationBuilderTest.kt | 12 +++ .../SessionReplayInternalProxyTest.kt | 15 ---- .../internal/SessionReplayFeatureTest.kt | 4 +- .../composition/CapturedIdentityTest.kt | 81 ++++++++++++++----- .../composition/CapturedTreeValidatorTest.kt | 19 ++++- .../composition/CapturedTreeWireMapperTest.kt | 31 +++---- 12 files changed, 185 insertions(+), 103 deletions(-) create mode 100644 features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/ExperimentalSessionReplayApi.kt diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index edee468432..517d3c6f3c 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -1,3 +1,4 @@ +annotation com.datadog.android.sessionreplay.ExperimentalSessionReplayApi interface com.datadog.android.sessionreplay.ExtensionSupport fun name(): String fun getCustomViewMappers(): List> @@ -48,6 +49,7 @@ data class com.datadog.android.sessionreplay.SessionReplayConfiguration fun setDynamicOptimizationEnabled(Boolean): Builder fun setSystemRequirements(SystemRequirementsConfiguration): Builder fun setHeatmapsEnabled(Boolean): Builder + fun setCompositionTreeRecordingEnabled(Boolean): Builder fun build(): SessionReplayConfiguration interface com.datadog.android.sessionreplay.SessionReplayInternalCallback fun getCurrentActivity(): android.app.Activity? @@ -77,7 +79,6 @@ enum com.datadog.android.sessionreplay.TouchPrivacy : PrivacyLevel class com.datadog.android.sessionreplay._SessionReplayInternalProxy constructor(SessionReplayConfiguration.Builder) fun setInternalCallback(SessionReplayInternalCallback): SessionReplayConfiguration.Builder - fun setCompositionTreeRecordingEnabled(Boolean): SessionReplayConfiguration.Builder class com.datadog.android.sessionreplay.internal.TouchPrivacyManager constructor(com.datadog.android.sessionreplay.TouchPrivacy) fun addTouchOverrideArea(android.graphics.Rect, com.datadog.android.sessionreplay.TouchPrivacy) 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 e59cb3e4f4..74a42f6473 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 @@ -1,3 +1,6 @@ +public abstract interface annotation class com/datadog/android/sessionreplay/ExperimentalSessionReplayApi : java/lang/annotation/Annotation { +} + public abstract interface class com/datadog/android/sessionreplay/ExtensionSupport { public abstract fun getCustomDrawableMapper ()Ljava/util/List; public abstract fun getCustomViewMappers ()Ljava/util/List; @@ -77,6 +80,7 @@ public final class com/datadog/android/sessionreplay/SessionReplayConfiguration$ public synthetic fun (FILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addExtensionSupport (Lcom/datadog/android/sessionreplay/ExtensionSupport;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun build ()Lcom/datadog/android/sessionreplay/SessionReplayConfiguration; + public final fun setCompositionTreeRecordingEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setDynamicOptimizationEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setHeatmapsEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setImagePrivacy (Lcom/datadog/android/sessionreplay/ImagePrivacy;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; @@ -147,7 +151,6 @@ public final class com/datadog/android/sessionreplay/TouchPrivacy : java/lang/En public final class com/datadog/android/sessionreplay/_SessionReplayInternalProxy { public fun (Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder;)V - public final fun setCompositionTreeRecordingEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; public final fun setInternalCallback (Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/ExperimentalSessionReplayApi.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/ExperimentalSessionReplayApi.kt new file mode 100644 index 0000000000..0072aba2da --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/ExperimentalSessionReplayApi.kt @@ -0,0 +1,18 @@ +/* + * 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 + +/** + * Marker for the experimental Session Replay API. + */ +@RequiresOptIn( + message = "This is an experimental Session Replay API." + + " It may change in the future.", + level = RequiresOptIn.Level.WARNING +) +@Retention(AnnotationRetention.BINARY) +annotation class ExperimentalSessionReplayApi diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt index cd83004589..48c28ba670 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplayConfiguration.kt @@ -250,15 +250,12 @@ data class SessionReplayConfiguration internal constructor( * source, preserving its structure and rendering effects to improve replay fidelity in * Android View and Jetpack Compose applications. * - * The underlying capture pipeline is not implemented yet: enabling this flag currently - * disables Session Replay recording entirely instead of falling back to the default - * recorder. Kept internal until the pipeline is functional; see [_SessionReplayInternalProxy]. - * * Disabled by default. * * @param enabled whether composition-tree recording should be used. */ - internal fun setCompositionTreeRecordingEnabled(enabled: Boolean): Builder { + @ExperimentalSessionReplayApi + fun setCompositionTreeRecordingEnabled(enabled: Boolean): Builder { compositionTreeRecordingEnabled = enabled return this } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/_SessionReplayInternalProxy.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/_SessionReplayInternalProxy.kt index b8568c6d82..9c8525a138 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/_SessionReplayInternalProxy.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/_SessionReplayInternalProxy.kt @@ -35,17 +35,4 @@ class _SessionReplayInternalProxy(private val builder: SessionReplayConfiguratio ): SessionReplayConfiguration.Builder { return builder.setInternalCallback(internalCallback) } - - /** - * Enables the experimental composition-tree recording pipeline. - * - * The underlying capture pipeline is not implemented yet: enabling this flag currently - * disables Session Replay recording entirely instead of falling back to the default recorder. - * - * @param enabled whether composition-tree recording should be used. - * @return [SessionReplayConfiguration.Builder] instance. - */ - fun setCompositionTreeRecordingEnabled(enabled: Boolean): SessionReplayConfiguration.Builder { - return builder.setCompositionTreeRecordingEnabled(enabled) - } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt index 64bea2309d..76973423b6 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt @@ -6,6 +6,8 @@ package com.datadog.android.sessionreplay.internal.composition +import com.datadog.android.api.InternalLogger + internal data class RumViewIdentityScope(val value: String) internal enum class CapturedIdentityKind { @@ -18,13 +20,14 @@ internal enum class CapturedIdentityKind { WIREFRAME } +private const val WEB_VIEW_WIREFRAME_NAMESPACE = 0L +private const val SHAPE_WIREFRAME_NAMESPACE = 1L +private const val TEXT_WIREFRAME_NAMESPACE = 2L +private const val IMAGE_WIREFRAME_NAMESPACE = 3L +private const val PLACEHOLDER_WIREFRAME_NAMESPACE = 4L + /** Matches the wireframe identifier namespaces used by the iOS Core Animation pipeline. */ internal enum class CapturedWireframeKind(internal val wireIdNamespace: Long) { - SHAPE(1), - TEXT(2), - IMAGE(IMAGE_WIREFRAME_NAMESPACE), - PLACEHOLDER(PLACEHOLDER_WIREFRAME_NAMESPACE), - /** * Deliberately namespace `0`, i.e. unshifted: the wire id for a web-view wireframe must equal * its `slotId` verbatim (not `slotId` shifted into a namespace), matching the pre-existing @@ -37,7 +40,11 @@ internal enum class CapturedWireframeKind(internal val wireIdNamespace: Long) { * `Int` range; a residual, validation-caught collision remains possible only between two * web-view wireframes with a colliding `slotId` in the same view scope. */ - WEB_VIEW(0) + WEB_VIEW(WEB_VIEW_WIREFRAME_NAMESPACE), + SHAPE(SHAPE_WIREFRAME_NAMESPACE), + TEXT(TEXT_WIREFRAME_NAMESPACE), + IMAGE(IMAGE_WIREFRAME_NAMESPACE), + PLACEHOLDER(PLACEHOLDER_WIREFRAME_NAMESPACE) } internal data class CapturedIdentity internal constructor( @@ -47,7 +54,9 @@ internal data class CapturedIdentity internal constructor( val namespace: List, val localId: String, val wireId: Long -) +) { + fun path(): List = namespace + kind.name + localId +} internal fun interface CapturedReplayIdGenerator { fun next(): Long @@ -95,7 +104,8 @@ internal interface CapturedIdentityFactory { internal class DefaultCapturedIdentityFactory( override val scope: RumViewIdentityScope, - private val replayIdGenerator: CapturedReplayIdGenerator = SHARED_REPLAY_ID_GENERATOR + private val replayIdGenerator: CapturedReplayIdGenerator = SHARED_REPLAY_ID_GENERATOR, + private val internalLogger: InternalLogger = InternalLogger.UNBOUND ) : CapturedIdentityFactory { private val identities = mutableMapOf() @@ -107,22 +117,22 @@ internal class DefaultCapturedIdentityFactory( createLayerIdentity(CapturedIdentityKind.WINDOW, emptyList(), windowId) override fun view(window: CapturedIdentity, viewId: String): CapturedIdentity { - requireOwner(window, CapturedIdentityKind.WINDOW) + validateOwner(window, CapturedIdentityKind.WINDOW) return createLayerIdentity(CapturedIdentityKind.VIEW, window.path(), viewId) } override fun composeHost(window: CapturedIdentity, hostId: String): CapturedIdentity { - requireOwner(window, CapturedIdentityKind.WINDOW) + validateOwner(window, CapturedIdentityKind.WINDOW) return createLayerIdentity(CapturedIdentityKind.COMPOSE_HOST, window.path(), hostId) } override fun composeNode(host: CapturedIdentity, nodeId: String): CapturedIdentity { - requireOwner(host, CapturedIdentityKind.COMPOSE_HOST) + validateOwner(host, CapturedIdentityKind.COMPOSE_HOST) return createLayerIdentity(CapturedIdentityKind.COMPOSE_NODE, host.path(), nodeId) } override fun layer(owner: CapturedIdentity, layerId: String): CapturedIdentity { - requireLayerOwner(owner) + validateLayerOwner(owner) return createLayerIdentity(CapturedIdentityKind.LAYER, owner.path(), layerId) } @@ -139,7 +149,7 @@ internal class DefaultCapturedIdentityFactory( createNamespacedWireframeIdentity(owner, CapturedWireframeKind.PLACEHOLDER) override fun webViewWireframe(owner: CapturedIdentity, slotId: Long): CapturedIdentity { - requireLayerOwner(owner) + validateLayerOwner(owner) return createIdentity( kind = CapturedIdentityKind.WIREFRAME, wireframeKind = CapturedWireframeKind.WEB_VIEW, @@ -168,7 +178,7 @@ internal class DefaultCapturedIdentityFactory( owner: CapturedIdentity, wireframeKind: CapturedWireframeKind ): CapturedIdentity { - requireLayerOwner(owner) + validateLayerOwner(owner) return createIdentity( kind = CapturedIdentityKind.WIREFRAME, wireframeKind = wireframeKind, @@ -201,29 +211,40 @@ internal class DefaultCapturedIdentityFactory( ).also { identities[key] = it } } - @Suppress("RequireInternal", "UnsafeThirdPartyFunctionCall") - private fun requireLayerOwner(owner: CapturedIdentity) { - requireOwner(owner) - require(owner.kind != CapturedIdentityKind.WIREFRAME) { - "Identity owner must be a captured layer." + // These checks guard against programmer-only misuse of the internal identity factory. They + // must never throw: a bug tripping them would otherwise crash the host application, so any + // violation is reported to the maintainer telemetry instead and construction proceeds. + private fun validateLayerOwner(owner: CapturedIdentity) { + validateOwner(owner) + if (owner.kind == CapturedIdentityKind.WIREFRAME) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { "Identity owner must be a captured layer." } + ) } } - // These checks guard programmer-only misuse of the internal identity factory. - @Suppress("RequireInternal", "UnsafeThirdPartyFunctionCall") - private fun requireOwner( + private fun validateOwner( owner: CapturedIdentity, expectedKind: CapturedIdentityKind? = null ) { - require(owner.scope == scope) { "Identity owner belongs to a different RUM view scope." } - require(expectedKind == null || owner.kind == expectedKind) { - "Identity owner must be a $expectedKind." + if (owner.scope != scope) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { "Identity owner belongs to a different RUM view scope." } + ) + } + if (expectedKind != null && owner.kind != expectedKind) { + internalLogger.log( + InternalLogger.Level.WARN, + InternalLogger.Target.MAINTAINER, + { "Identity owner must be a $expectedKind." } + ) } } - private fun CapturedIdentity.path(): List = - namespace + kind.name + localId - private data class IdentityKey( val kind: CapturedIdentityKind, val wireframeKind: CapturedWireframeKind?, @@ -238,9 +259,6 @@ internal class DefaultCapturedIdentityFactory( } } -private const val IMAGE_WIREFRAME_NAMESPACE = 3L -private const val PLACEHOLDER_WIREFRAME_NAMESPACE = 4L - /** * Added to every raw layer replay id so that layer wire ids always land above the Int range * (`[1L shl 31, (1L shl 32) - 1]` given the generator wraps at [Int.MAX_VALUE]) - outside the range diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt index c9b0ea5cac..9a67579d14 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt @@ -90,6 +90,18 @@ internal class SessionReplayConfigurationBuilderTest { assertThat(sessionReplayConfiguration.heatmapsEnabled).isTrue() } + @Test + @OptIn(ExperimentalSessionReplayApi::class) + fun `M set compositionTreeRecordingEnabled to true W setCompositionTreeRecordingEnabled()`() { + // When + val sessionReplayConfiguration = testedBuilder + .setCompositionTreeRecordingEnabled(true) + .build() + + // Then + assertThat(sessionReplayConfiguration.compositionTreeRecordingEnabled).isTrue() + } + @Test fun `M build config with custom site W useCustomEndpoint() and build()`( @StringForgery(regex = "https://[a-z]+\\.com(/[a-z]+)+") sessionReplayUrl: String diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalProxyTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalProxyTest.kt index c73ec9a945..5862b6b24d 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalProxyTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayInternalProxyTest.kt @@ -48,19 +48,4 @@ internal class SessionReplayInternalProxyTest { assertThat(result).isEqualTo(testedBuilder) assertThat(sessionReplayConfiguration.internalCallback).isEqualTo(mockInternalCallback) } - - @Test - fun `M enable composition tree recording W setCompositionTreeRecordingEnabled`() { - // Given - testedBuilder = SessionReplayConfiguration.Builder(fakeSampleRate) - testedProxy = _SessionReplayInternalProxy(testedBuilder) - - // When - val result = testedProxy.setCompositionTreeRecordingEnabled(true) - val sessionReplayConfiguration = result.build() - - // Then - assertThat(result).isEqualTo(testedBuilder) - assertThat(sessionReplayConfiguration.compositionTreeRecordingEnabled).isTrue() - } } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt index 02a6d4508c..f68013d561 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeatureTest.kt @@ -151,6 +151,8 @@ internal class SessionReplayFeatureTest { dynamicOptimizationEnabled = fakeConfiguration.dynamicOptimizationEnabled, internalCallback = NoOpSessionReplayInternalCallback(), heatmapsEnabled = fakeConfiguration.heatmapsEnabled, + // The default (legacy) recorder is only selected when composition-tree recording is + // disabled; this test asserts on that recorder type, so the flag must stay false. compositionTreeRecordingEnabled = false ) @@ -181,7 +183,7 @@ internal class SessionReplayFeatureTest { dynamicOptimizationEnabled = fakeConfiguration.dynamicOptimizationEnabled, internalCallback = NoOpSessionReplayInternalCallback(), heatmapsEnabled = fakeConfiguration.heatmapsEnabled, - compositionTreeRecordingEnabled = false + compositionTreeRecordingEnabled = fakeConfiguration.compositionTreeRecordingEnabled ) // When diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt index 7fbdb7c9f1..bbafb66bf4 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt @@ -6,27 +6,57 @@ package com.datadog.android.sessionreplay.internal.composition +import com.datadog.android.api.InternalLogger +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.LongForgery +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.assertj.core.api.Assertions.assertThatThrownBy 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.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.verify +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(ForgeConfigurator::class) internal class CapturedIdentityTest { + @Mock + lateinit var mockInternalLogger: InternalLogger + @Test - fun `M return stable identity W create identity { identical inputs }`() { + fun `M return stable identity W create identity { identical inputs }`( + @StringForgery fakeWindowId: String, + @StringForgery fakeViewId: String + ) { // Given val factory = factory() // When - val first = factory.view(factory.window("window"), "view-id") - val repeated = factory.view(factory.window("window"), "view-id") + val first = factory.view(factory.window(fakeWindowId), fakeViewId) + val repeated = factory.view(factory.window(fakeWindowId), fakeViewId) // Then assertThat(repeated).isSameAs(first) } @Test - fun `M allocate new identity W create identity { separate factories }`() { + fun `M allocate new identity W create identity { separate factories }`( + @StringForgery fakeWindowId: String + ) { // Given val generator = AutoIncrementingCapturedReplayIdGenerator() val scope = RumViewIdentityScope("view") @@ -34,8 +64,8 @@ internal class CapturedIdentityTest { val secondFactory = DefaultCapturedIdentityFactory(scope, generator) // When - val first = firstFactory.window("window") - val second = secondFactory.window("window") + val first = firstFactory.window(fakeWindowId) + val second = secondFactory.window(fakeWindowId) // Then assertThat(second).isNotEqualTo(first) @@ -104,22 +134,34 @@ internal class CapturedIdentityTest { } @Test - fun `M reject owner W create identity { owner belongs to another scope }`() { + fun `M log warning and still create identity W create identity { owner belongs to another scope }`() { // Given val firstFactory = factory("first-view") val secondFactory = factory("second-view") - // When + Then - assertThatThrownBy { secondFactory.view(firstFactory.window("window"), "view") } - .isInstanceOf(IllegalArgumentException::class.java) + // When + val view = secondFactory.view(firstFactory.window("window"), "view") + + // Then + assertThat(view).isNotNull() + verify(mockInternalLogger).log( + level = eq(InternalLogger.Level.WARN), + target = eq(InternalLogger.Target.MAINTAINER), + messageBuilder = any(), + throwable = isNull(), + onlyOnce = eq(false), + additionalProperties = isNull() + ) } @Test - fun `M create namespaced IDs W create wireframe identities`() { + fun `M create namespaced IDs W create wireframe identities`( + @LongForgery(min = 0, max = Int.MAX_VALUE.toLong()) fakeInitialId: Long + ) { // Given val factory = DefaultCapturedIdentityFactory( scope = RumViewIdentityScope("view"), - replayIdGenerator = AutoIncrementingCapturedReplayIdGenerator(initialId = 42) + replayIdGenerator = AutoIncrementingCapturedReplayIdGenerator(initialId = fakeInitialId) ) val layer = factory.layer(factory.window("window"), "layer") @@ -137,16 +179,18 @@ internal class CapturedIdentityTest { } @Test - fun `M preserve raw slot ID W create webViewWireframe`() { + fun `M preserve raw slot ID W create webViewWireframe`( + @IntForgery fakeSlotId: Int + ) { // Given val factory = factory() val layer = factory.layer(factory.window("window"), "layer") // When - val wireframe = factory.webViewWireframe(layer, slotId = 42) + val wireframe = factory.webViewWireframe(layer, slotId = fakeSlotId.toLong()) // Then - assertThat(wireframe.wireId).isEqualTo(42) + assertThat(wireframe.wireId).isEqualTo(fakeSlotId.toLong()) assertThat(wireframe.wireframeKind).isEqualTo(CapturedWireframeKind.WEB_VIEW) } @@ -210,7 +254,8 @@ internal class CapturedIdentityTest { private fun factory(scope: String = "view") = DefaultCapturedIdentityFactory( scope = RumViewIdentityScope(scope), - replayIdGenerator = AutoIncrementingCapturedReplayIdGenerator() + replayIdGenerator = AutoIncrementingCapturedReplayIdGenerator(), + internalLogger = mockInternalLogger ) private companion object { diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt index 0c1637652c..e811114dbc 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt @@ -6,9 +6,16 @@ package com.datadog.android.sessionreplay.internal.composition +import com.datadog.android.sessionreplay.forge.ForgeConfigurator +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +@ExtendWith(ForgeExtension::class) +@ForgeConfiguration(ForgeConfigurator::class) internal class CapturedTreeValidatorTest { private val testedValidator = DefaultCapturedTreeValidator() @@ -633,10 +640,12 @@ internal class CapturedTreeValidatorTest { } @Test - fun `M return valid W validate { unreferenced hidden slot wireframes }`() { + fun `M return valid W validate { unreferenced hidden slot wireframes }`( + @IntForgery fakeSlotId: Int + ) { // Given val tree = compositionTestTree() - val webViewIdentity = tree.factory.webViewWireframe(tree.layer.identity, 42) + val webViewIdentity = tree.factory.webViewWireframe(tree.layer.identity, fakeSlotId.toLong()) val hiddenWebView = CapturedWireframe.WebView( identity = webViewIdentity, bounds = CapturedBounds(0, 0, 0, 0), @@ -655,10 +664,12 @@ internal class CapturedTreeValidatorTest { } @Test - fun `M report unreferenced wireframe W validate { visible slot wireframe }`() { + fun `M report unreferenced wireframe W validate { visible slot wireframe }`( + @IntForgery fakeSlotId: Int + ) { // Given val tree = compositionTestTree() - val identity = tree.factory.webViewWireframe(tree.layer.identity, 42) + val identity = tree.factory.webViewWireframe(tree.layer.identity, fakeSlotId.toLong()) val visibleWebView = CapturedWireframe.WebView( identity = identity, bounds = CapturedBounds(0, 0, 10, 10), diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapperTest.kt index 900faa8d4a..1eb0877ff6 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeWireMapperTest.kt @@ -25,13 +25,14 @@ internal class CapturedTreeWireMapperTest { // Then val record = (result as CaptureWireMappingResult.Success).value assertThat(record.timestamp).isEqualTo(tree.snapshot.timestamp) + val bounds = (tree.wireframe as CapturedWireframe.Shape).bounds assertThat(record.data.wireframes.single()).isEqualTo( MobileSegment.Wireframe.ShapeWireframe( id = tree.wireframeIdentity.wireId, - x = 1, - y = 2, - width = 3, - height = 4 + x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height ) ) assertThat(record.data.compositionTree?.root?.id).isEqualTo(tree.root.identity.wireId) @@ -176,17 +177,19 @@ internal class CapturedTreeWireMapperTest { CapturedChild.Wireframe(placeholderIdentity) ) ) + val pixelBounds = CapturedBounds(1, 2, 3, 4) + val placeholderBounds = CapturedBounds(5, 6, 7, 8) val snapshot = tree.snapshot.copy( layers = listOf(layer), wireframes = listOf( CapturedWireframe.Pixel( identity = pixelIdentity, - bounds = CapturedBounds(1, 2, 3, 4), + bounds = pixelBounds, resource = PixelResource.Resolved("resource", "image/webp") ), CapturedWireframe.PrivacyPlaceholder( identity = placeholderIdentity, - bounds = CapturedBounds(5, 6, 7, 8), + bounds = placeholderBounds, label = "Image" ) ) @@ -200,19 +203,19 @@ internal class CapturedTreeWireMapperTest { assertThat(wireframes).containsExactly( MobileSegment.Wireframe.ImageWireframe( id = pixelIdentity.wireId, - x = 1, - y = 2, - width = 3, - height = 4, + x = pixelBounds.x, + y = pixelBounds.y, + width = pixelBounds.width, + height = pixelBounds.height, resourceId = "resource", mimeType = "image/webp" ), MobileSegment.Wireframe.PlaceholderWireframe( id = placeholderIdentity.wireId, - x = 5, - y = 6, - width = 7, - height = 8, + x = placeholderBounds.x, + y = placeholderBounds.y, + width = placeholderBounds.width, + height = placeholderBounds.height, label = "Image" ) )