diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index f420baab16..10499480e4 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 setLayerTreeRecordingEnabled(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) @@ -192,14 +193,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 +238,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 +273,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 +338,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 +403,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 +419,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 +438,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 +461,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 +547,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 +586,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 +604,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..47d53ee4dc 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; @@ -148,6 +148,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 setInternalCallback (Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; + public final fun setLayerTreeRecordingEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder; } public final class com/datadog/android/sessionreplay/internal/TouchPrivacyManager { @@ -265,20 +266,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 +719,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 +760,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 +955,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 +981,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 +1201,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 +1269,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 +1358,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 +1419,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 +1713,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/SessionReplay.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/SessionReplay.kt index ae1e3420f4..e8ccd524b2 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, + layerTreeRecordingEnabled = sessionReplayConfiguration.layerTreeRecordingEnabled ) 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..a41575d8d4 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 @@ -8,6 +8,7 @@ package com.datadog.android.sessionreplay import androidx.annotation.FloatRange import com.datadog.android.api.InternalLogger +import com.datadog.android.sessionreplay.internal.composition.SessionReplayLayerTreeRecordingFeatureFlag import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector import com.datadog.android.sessionreplay.utils.DrawableToColorMapper @@ -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 layerTreeRecordingEnabled: 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 layerTreeRecordingEnabled = SessionReplayLayerTreeRecordingFeatureFlag.DEFAULT_ENABLED /** * Adds an extension support implementation. This is mostly used when you want to provide @@ -239,6 +242,17 @@ data class SessionReplayConfiguration internal constructor( return this } + /** + * Enables the experimental layer-tree recording pipeline. + * + * This is exposed through [_SessionReplayInternalProxy] and is not part of the stable + * customer-facing configuration API. + */ + internal fun setLayerTreeRecordingEnabled(enabled: Boolean): Builder { + layerTreeRecordingEnabled = enabled + return this + } + /** * Builds a [SessionReplayConfiguration] based on the current state of this Builder. */ @@ -257,7 +271,8 @@ data class SessionReplayConfiguration internal constructor( dynamicOptimizationEnabled = dynamicOptimizationEnabled, systemRequirementsConfiguration = systemRequirementsConfiguration, internalCallback = internalCallback, - heatmapsEnabled = heatmapsEnabled + heatmapsEnabled = heatmapsEnabled, + layerTreeRecordingEnabled = layerTreeRecordingEnabled ) } 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..a4aa25f8c9 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,16 @@ class _SessionReplayInternalProxy(private val builder: SessionReplayConfiguratio ): SessionReplayConfiguration.Builder { return builder.setInternalCallback(internalCallback) } + + /** + * Enables or disables the experimental layer-tree recording pipeline. + * + * This mirrors the iOS `layerTreeRecording` Session Replay feature flag. + * + * @param enabled whether layer-tree recording should be used. + * @return [SessionReplayConfiguration.Builder] instance. + */ + fun setLayerTreeRecordingEnabled(enabled: Boolean): SessionReplayConfiguration.Builder { + return builder.setLayerTreeRecordingEnabled(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..a5ab0d1568 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,9 @@ 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.composition.SessionReplayLayerTreeRecordingFeatureFlag 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 +70,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 layerTreeRecordingEnabled: Boolean = SessionReplayLayerTreeRecordingFeatureFlag.DEFAULT_ENABLED, + private val compositionPipelineFactory: () -> Recorder = { CompositionCapturePipeline() } ) : RecorderProvider { override fun provideSessionReplayRecorder( @@ -77,24 +82,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 = layerTreeRecordingEnabled, + 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..90632374aa 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 @@ -25,6 +25,7 @@ import com.datadog.android.sessionreplay.SessionReplayInternalCallback import com.datadog.android.sessionreplay.SessionReplayPrivacy import com.datadog.android.sessionreplay.TextAndInputPrivacy import com.datadog.android.sessionreplay.TouchPrivacy +import com.datadog.android.sessionreplay.internal.composition.SessionReplayLayerTreeRecordingFeatureFlag import com.datadog.android.sessionreplay.internal.net.BatchesToSegmentsMapper import com.datadog.android.sessionreplay.internal.net.SegmentRequestFactory import com.datadog.android.sessionreplay.internal.recorder.NoOpRecorder @@ -75,7 +76,8 @@ internal class SessionReplayFeature( startRecordingImmediately: Boolean, dynamicOptimizationEnabled: Boolean, internalCallback: SessionReplayInternalCallback, - heatmapsEnabled: Boolean + heatmapsEnabled: Boolean, + layerTreeRecordingEnabled: Boolean = SessionReplayLayerTreeRecordingFeatureFlag.DEFAULT_ENABLED ) : this( sdkCore, customEndpointUrl, @@ -95,7 +97,8 @@ internal class SessionReplayFeature( customDrawableMappers, dynamicOptimizationEnabled, internalCallback, - heatmapsEnabled + heatmapsEnabled, + layerTreeRecordingEnabled ) ) 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..19bf1ed051 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentity.kt @@ -0,0 +1,169 @@ +/* + * 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 java.nio.ByteBuffer +import java.security.MessageDigest + +internal data class RumViewIdentityScope(val value: String) + +internal enum class CapturedIdentityKind { + SCREEN_ROOT, + WINDOW, + VIEW, + COMPOSE_HOST, + COMPOSE_NODE, + LAYER, + WIREFRAME +} + +internal data class CapturedIdentity internal constructor( + val scope: RumViewIdentityScope, + val kind: CapturedIdentityKind, + val namespace: List, + val localId: String, + val wireId: Long +) + +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 wireframe(owner: CapturedIdentity, wireframeId: String): CapturedIdentity +} + +internal class DefaultCapturedIdentityFactory( + override val scope: RumViewIdentityScope +) : CapturedIdentityFactory { + + private val identities = mutableMapOf() + private val keysByWireId = mutableMapOf() + + override fun screenRoot(): CapturedIdentity = + create(CapturedIdentityKind.SCREEN_ROOT, emptyList(), SCREEN_ROOT_LOCAL_ID) + + override fun window(windowId: String): CapturedIdentity = + create(CapturedIdentityKind.WINDOW, emptyList(), windowId) + + override fun view(window: CapturedIdentity, viewId: String): CapturedIdentity { + requireOwner(window, CapturedIdentityKind.WINDOW) + return create(CapturedIdentityKind.VIEW, window.path(), viewId) + } + + override fun composeHost(window: CapturedIdentity, hostId: String): CapturedIdentity { + requireOwner(window, CapturedIdentityKind.WINDOW) + return create(CapturedIdentityKind.COMPOSE_HOST, window.path(), hostId) + } + + override fun composeNode(host: CapturedIdentity, nodeId: String): CapturedIdentity { + requireOwner(host, CapturedIdentityKind.COMPOSE_HOST) + return create(CapturedIdentityKind.COMPOSE_NODE, host.path(), nodeId) + } + + override fun layer(owner: CapturedIdentity, layerId: String): CapturedIdentity { + requireOwner(owner) + return create(CapturedIdentityKind.LAYER, owner.path(), layerId) + } + + override fun wireframe(owner: CapturedIdentity, wireframeId: String): CapturedIdentity { + requireOwner(owner) + return create(CapturedIdentityKind.WIREFRAME, owner.path(), wireframeId) + } + + private fun create( + kind: CapturedIdentityKind, + namespace: List, + localId: String + ): CapturedIdentity { + val key = IdentityKey(kind, namespace.toList(), localId) + identities[key]?.let { return it } + + val wireId = allocateWireId(key) + return CapturedIdentity(scope, kind, key.namespace, localId, wireId).also { + identities[key] = it + keysByWireId[wireId] = key + } + } + + private fun allocateWireId(key: IdentityKey): Long { + var collisionIndex = 0 + while (true) { + val candidate = deterministicWireId(key, collisionIndex) + val existingKey = keysByWireId[candidate] + if (existingKey == null || existingKey == key) return candidate + collisionIndex++ + } + } + + private fun deterministicWireId(key: IdentityKey, collisionIndex: Int): Long { + // SHA-256 is required by the Java and Android runtime specifications. + @Suppress("UnsafeThirdPartyFunctionCall") + val digest = MessageDigest.getInstance(SHA_256) + digest.updateLengthPrefixed(scope.value) + digest.updateLengthPrefixed(key.kind.name) + key.namespace.forEach { digest.updateLengthPrefixed(it) } + digest.updateLengthPrefixed(key.localId) + digest.update(collisionIndex.toBigEndianBytes()) + return ByteBuffer.wrap(digest.digest()).long and MAX_SAFE_WIRE_ID + } + + // 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 fun MessageDigest.updateLengthPrefixed(value: String) { + val bytes = value.toByteArray(Charsets.UTF_8) + update(bytes.size.toBigEndianBytes()) + update(bytes) + } + + // Shifting masks every value into the valid byte range before conversion. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun Int.toBigEndianBytes(): ByteArray = byteArrayOf( + (this ushr BYTE_3_SHIFT).toByte(), + (this ushr BYTE_2_SHIFT).toByte(), + (this ushr BYTE_1_SHIFT).toByte(), + toByte() + ) + + private data class IdentityKey( + val kind: CapturedIdentityKind, + val namespace: List, + val localId: String + ) + + private companion object { + const val SHA_256 = "SHA-256" + const val SCREEN_ROOT_LOCAL_ID = "screen" + const val MAX_SAFE_WIRE_ID = (1L shl 53) - 1 + const val BYTE_3_SHIFT = 24 + const val BYTE_2_SHIFT = 16 + const val BYTE_1_SHIFT = 8 + } +} 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/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..3eff6b33d9 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedMutationValidation.kt @@ -0,0 +1,164 @@ +/* + * 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 } + + fun validate(): CaptureValidationResult { + validateScope() + validateOperationDuplicates() + validateContradictions() + validateTargets() + val replacementRoot = validateReplacementRoot() + + if (failures.isNotEmpty()) return CaptureValidationResult.Invalid(failures) + return CapturedSnapshotValidation(effectiveSnapshot(replacementRoot)).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 || + 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 + 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..f3a90048fe --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedSnapshotValidation.kt @@ -0,0 +1,265 @@ +/* + * 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 failures = mutableListOf() + + 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(root, 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 } + snapshot.wireframes.map { it.identity } + validateIdentityDefinitions(identities) + layers.forEach(::validateLayer) + 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.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) { + 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) { + 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 + ) + } + } + + private fun validateReferences( + layers: List, + layersById: Map, + wireframesById: Map, + parentCounts: MutableMap + ) { + layers.forEach { parent -> + parent.children.forEach { child -> + when (child) { + is CapturedChild.Layer -> validateLayerReference(child, layersById, parentCounts) + is CapturedChild.Wireframe -> validateWireframeReference(child, 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, + wireframesById: Map, + parentCounts: MutableMap + ) { + if (child.identity.kind != CapturedIdentityKind.WIREFRAME) { + failures += validationFailure(CaptureValidationErrorCode.IDENTITY_KIND_MISMATCH, child.identity) + } + validateReferenceScope(child.identity) + val definition = wireframesById[child.identity.wireId] + when { + 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 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) + } + snapshot.wireframes.forEach { + validateParentCount(it.identity, parentCounts, CaptureValidationErrorCode.UNREFERENCED_WIREFRAME) + } + } + + 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 + is CapturedModifier.GaussianBlur -> modifier.radius >= 0 + is CapturedModifier.MaskImage -> modifier.resourceId.isNotBlank() + is CapturedModifier.Opacity -> modifier.value in 0.0..1.0 + is CapturedModifier.Saturate -> modifier.value >= 0 + is CapturedModifier.Shadow -> modifier.radius >= 0 && SHADOW_COLOR.matches(modifier.color) + } + if (!valid) failures += validationFailure(CaptureValidationErrorCode.INVALID_MODIFIER, identity) + } + + // Set membership operations cannot fail for these locally owned mutable sets. + @Suppress("UnsafeThirdPartyFunctionCall") + private fun validateAcyclic(root: CapturedLayer, 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) + } + visit(root) + } + + private fun PixelResource.isResolved(): Boolean = + this is PixelResource.Resolved && resourceId.isNotBlank() + + 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 companion object { + const val COLOR_MATRIX_SIZE = 20 + val SHADOW_COLOR = Regex("^#[A-Fa-f0-9]{6}([A-Fa-f0-9]{2})?$") + } + + private data class IdentityDefinitionKey( + val scope: RumViewIdentityScope, + val kind: CapturedIdentityKind, + val namespace: List, + val localId: String + ) +} 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..69f8258154 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTree.kt @@ -0,0 +1,238 @@ +/* + * 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 + + 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 slotId: String, + 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/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..3ba37f1647 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidator.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 + +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, + 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/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..b7c17c44e1 --- /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 = slotId, + 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/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..ad7649816c --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipeline.kt @@ -0,0 +1,37 @@ +/* + * 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 object SessionReplayLayerTreeRecordingFeatureFlag { + const val DEFAULT_ENABLED: Boolean = false +} + +internal class CapturePipelineSelector( + private val compositionEnabled: Boolean = SessionReplayLayerTreeRecordingFeatureFlag.DEFAULT_ENABLED, + 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/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/SessionReplayConfigurationBuilderTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt index 528b01fd3b..ad75bb7bcc 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.layerTreeRecordingEnabled).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..947c36a64f 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 layer tree recording W setLayerTreeRecordingEnabled()`() { + // Given + testedBuilder = SessionReplayConfiguration.Builder(fakeSampleRate) + testedProxy = _SessionReplayInternalProxy(testedBuilder) + + // When + val result = testedProxy.setLayerTreeRecordingEnabled(true) + val sessionReplayConfiguration = result.build() + + // Then + assertThat(result).isSameAs(testedBuilder) + assertThat(sessionReplayConfiguration.layerTreeRecordingEnabled).isTrue() + } } 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/SessionReplayConfigurationForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SessionReplayConfigurationForgeryFactory.kt index a1d2f81af8..62b21dc48f 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 { 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/DefaultRecorderProviderTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProviderTest.kt new file mode 100644 index 0000000000..1e25a174c7 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProviderTest.kt @@ -0,0 +1,103 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.sessionreplay.internal + +import android.app.Application +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.sessionreplay.ImagePrivacy +import com.datadog.android.sessionreplay.SessionReplayInternalCallback +import com.datadog.android.sessionreplay.TextAndInputPrivacy +import com.datadog.android.sessionreplay.internal.recorder.Recorder +import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder +import com.datadog.android.sessionreplay.internal.resources.ResourceDataStoreManager +import com.datadog.android.sessionreplay.internal.storage.RecordWriter +import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter +import com.datadog.android.sessionreplay.internal.utils.RumContextProvider +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +internal class DefaultRecorderProviderTest { + + @Test + fun `M create only legacy recorder W provideSessionReplayRecorder { flag omitted }`() { + // Given + val sdkCore = mock() + 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, + 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, + layerTreeRecordingEnabled = 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/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..4d6d06db22 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedIdentityTest.kt @@ -0,0 +1,121 @@ +/* + * 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 scope = RumViewIdentityScope("view") + val firstFactory = DefaultCapturedIdentityFactory(scope) + val secondFactory = DefaultCapturedIdentityFactory(scope) + + // When + val first = firstFactory.view(firstFactory.window("window"), "view-id") + val repeated = firstFactory.view(firstFactory.window("window"), "view-id") + val recreated = secondFactory.view(secondFactory.window("window"), "view-id") + + // Then + assertThat(repeated).isSameAs(first) + assertThat(recreated).isEqualTo(first) + } + + @Test + fun `M namespace view identity W view { identical local IDs in different windows }`() { + // Given + val factory = DefaultCapturedIdentityFactory(RumViewIdentityScope("view")) + + // 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 = DefaultCapturedIdentityFactory(RumViewIdentityScope("view")) + 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 namespace identity kind W create identity { identical numeric IDs }`() { + // Given + val factory = DefaultCapturedIdentityFactory(RumViewIdentityScope("view")) + 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.wireframe(window, "42") + + // 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 firstFactory = DefaultCapturedIdentityFactory(RumViewIdentityScope("first-view")) + val secondFactory = DefaultCapturedIdentityFactory(RumViewIdentityScope("second-view")) + + // 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 = DefaultCapturedIdentityFactory(RumViewIdentityScope("first-view")) + val secondFactory = DefaultCapturedIdentityFactory(RumViewIdentityScope("second-view")) + + // When + Then + assertThatThrownBy { secondFactory.view(firstFactory.window("window"), "view") } + .isInstanceOf(IllegalArgumentException::class.java) + } + + @Test + fun `M create player-safe IDs W create identities`() { + // Given + val factory = DefaultCapturedIdentityFactory(RumViewIdentityScope("view")) + val window = factory.window("window") + + // When + val identities = List(1_000) { factory.view(window, "view-$it") } + + // Then + assertThat(identities.map { it.wireId }) + .allMatch { it in 0..MAX_SAFE_JAVASCRIPT_INTEGER } + .doesNotHaveDuplicates() + } + + private companion object { + 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/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..dfb0c65780 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CapturedTreeValidatorTest.kt @@ -0,0 +1,461 @@ +/* + * 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 { 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.wireframe(firstWindowIdentity, "wireframe") + val secondWireframeIdentity = factory.wireframe(secondWindowIdentity, "wireframe") + 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.wireframe(compositionIdentity, "wireframe") + 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.wireframe(tree.layer.identity, "missing-wireframe") + 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 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 pixel = CapturedWireframe.Pixel( + identity = tree.wireframeIdentity, + bounds = CapturedBounds(1, 2, 3, 4), + resource = PixelResource.Unresolved + ) + + // When + val result = testedValidator.validate(tree.snapshot.copy(wireframes = listOf(pixel))) + + // Then + assertThat(result.codes()).contains(CaptureValidationErrorCode.UNRESOLVED_PIXEL_RESOURCE) + } + + @Test + fun `M return valid W validate { resolved pixel and privacy placeholder }`() { + // Given + val tree = compositionTestTree() + val pixelIdentity = tree.factory.wireframe(tree.layer.identity, "pixel") + val placeholderIdentity = tree.factory.wireframe(tree.layer.identity, "placeholder") + 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 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 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 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) + } + + 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 } +} 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..738e46a2ba --- /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.wireframe(tree.layer.identity, "second-wireframe") + val secondWireframe = CapturedWireframe.Shape( + 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.wireframe(tree.layer.identity, "pixel") + val placeholderIdentity = tree.factory.wireframe(tree.layer.identity, "placeholder") + 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 + ) + ) + ) + ) + } +} 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..b9100db02d --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/composition/CompositionCapturePipelineTest.kt @@ -0,0 +1,139 @@ +/* + * 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 default to disabled W read feature flag`() { + // When + Then + assertThat(SessionReplayLayerTreeRecordingFeatureFlag.DEFAULT_ENABLED).isFalse() + } + + @Test + fun `M select only legacy pipeline W create { flag omitted }`() { + // Given + val legacy = mock() + var legacyConstructions = 0 + var compositionConstructions = 0 + val selector = CapturePipelineSelector( + 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( + 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 + } +} 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..4929f96d30 --- /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.wireframe(layerIdentity, "wireframe") + 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) + ) + ) +} 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..2bc6c62e0b 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 @@ -772,6 +772,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 @@ -793,6 +794,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)