diff --git a/.changeset/android-v4-major.md b/.changeset/android-v4-major.md index b24de8f7e..07371f860 100644 --- a/.changeset/android-v4-major.md +++ b/.changeset/android-v4-major.md @@ -13,3 +13,7 @@ - **Breaking:** remove the deprecated `remoteConfig` config option; the SDK now always loads remote config at setup - **Breaking:** replace `PostHogOnFeatureFlags` with `PostHogFeatureFlagsCallback` in `PostHogConfig.onFeatureFlags` and `reloadFeatureFlags()`; it receives a `PostHogFeatureFlagsLoaded` with the enabled flags, their values, and `errorsLoading` - Add `onFeatureFlags()` to get notified after every feature flags load (on the main thread on Android), returning a `PostHogFeatureFlagsSubscription` you can `unsubscribe()` +- **Breaking:** remove wireframe session replay; replay always records masked screenshots, the mode `screenshot = true` already used. `PostHogSessionReplayConfig.screenshot` is removed, and so are `drawableConverter` and `PostHogDrawableConverter`, which only wireframe capture read + - Apps that relied on the default now upload screenshots. Masking still covers text inputs, images, `WebView`s, Compose semantics and `ph-no-capture` views and `Modifier.postHogMask()`, but custom-drawn views and other content the SDK doesn't recognize (for example maps or video) can now appear; mask them, or turn replay off with `sessionReplay = false` + - Jetpack Compose screens, which wireframe captured as empty boxes, are now recorded + - Wrapper SDKs: the internal `RRWireframe` keeps only `id`, `x`, `y`, `width`, `height`, `type` and `base64`, and `RRStyle` is removed; screenshot frames no longer send an empty `style` diff --git a/posthog-android/api/posthog-android.api b/posthog-android/api/posthog-android.api index 282d623b6..6e2efaee4 100644 --- a/posthog-android/api/posthog-android.api +++ b/posthog-android/api/posthog-android.api @@ -72,10 +72,6 @@ public final class com/posthog/android/internal/PostHogAndroidUtilsKt { public static final fun getApplicationInfo (Landroid/content/Context;)Landroid/content/pm/ApplicationInfo; } -public abstract interface class com/posthog/android/replay/PostHogDrawableConverter { - public abstract fun convert (Landroid/graphics/drawable/Drawable;)Landroid/graphics/Bitmap; -} - public abstract interface annotation class com/posthog/android/replay/PostHogInternalReplayApi : java/lang/annotation/Annotation { } @@ -118,18 +114,14 @@ public final class com/posthog/android/replay/PostHogSessionReplayConfig { public fun (Z)V public fun (ZZ)V public fun (ZZZ)V - public fun (ZZZLcom/posthog/android/replay/PostHogDrawableConverter;)V - public fun (ZZZLcom/posthog/android/replay/PostHogDrawableConverter;Z)V - public fun (ZZZLcom/posthog/android/replay/PostHogDrawableConverter;ZJ)V - public fun (ZZZLcom/posthog/android/replay/PostHogDrawableConverter;ZJLjava/lang/Double;)V - public synthetic fun (ZZZLcom/posthog/android/replay/PostHogDrawableConverter;ZJLjava/lang/Double;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZZJ)V + public fun (ZZZJLjava/lang/Double;)V + public synthetic fun (ZZZJLjava/lang/Double;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getCaptureLogcat ()Z public final fun getCaptureTouches ()Z - public final fun getDrawableConverter ()Lcom/posthog/android/replay/PostHogDrawableConverter; public final fun getMaskAllImages ()Z public final fun getMaskAllTextInputs ()Z public final fun getSampleRate ()Ljava/lang/Double; - public final fun getScreenshot ()Z public final fun getScreenshotColorMode ()Lcom/posthog/android/replay/PostHogScreenshotColorMode; public final fun getScreenshotCompressionQuality ()I public final fun getScreenshotScale ()Ljava/lang/Float; @@ -137,11 +129,9 @@ public final class com/posthog/android/replay/PostHogSessionReplayConfig { public final fun getVerifyScreenshotMaskAlignment ()Z public final fun setCaptureLogcat (Z)V public final fun setCaptureTouches (Z)V - public final fun setDrawableConverter (Lcom/posthog/android/replay/PostHogDrawableConverter;)V public final fun setMaskAllImages (Z)V public final fun setMaskAllTextInputs (Z)V public final fun setSampleRate (Ljava/lang/Double;)V - public final fun setScreenshot (Z)V public final fun setScreenshotColorMode (Lcom/posthog/android/replay/PostHogScreenshotColorMode;)V public final fun setScreenshotCompressionQuality (I)V public final fun setScreenshotScale (Ljava/lang/Float;)V diff --git a/posthog-android/src/main/java/com/posthog/android/replay/PostHogDrawableConverter.kt b/posthog-android/src/main/java/com/posthog/android/replay/PostHogDrawableConverter.kt deleted file mode 100644 index 7f6a30aa1..000000000 --- a/posthog-android/src/main/java/com/posthog/android/replay/PostHogDrawableConverter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.posthog.android.replay - -import android.graphics.Bitmap -import android.graphics.drawable.Drawable - -/** - * Converts Android [Drawable] instances that the SDK cannot render by default into [Bitmap]s for - * session replay screenshots or wireframes. - */ -public fun interface PostHogDrawableConverter { - /** - * Converts [drawable] to a bitmap representation. - * - * @param drawable Drawable to convert. - * @return A bitmap to capture, or `null` when this converter cannot handle the drawable. - */ - public fun convert(drawable: Drawable): Bitmap? -} diff --git a/posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt b/posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt index 8e863062e..4121b2dde 100644 --- a/posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt +++ b/posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt @@ -2,31 +2,25 @@ package com.posthog.android.replay import android.annotation.SuppressLint import android.content.Context -import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Point -import android.graphics.PorterDuff import android.graphics.Rect import android.graphics.RectF -import android.graphics.Typeface import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.InsetDrawable import android.graphics.drawable.LayerDrawable -import android.graphics.drawable.RippleDrawable import android.graphics.drawable.VectorDrawable import android.os.Build import android.os.Handler import android.os.HandlerThread import android.os.Looper import android.text.InputType -import android.util.TypedValue -import android.view.Gravity import android.view.MotionEvent import android.view.PixelCopy import android.view.SurfaceView @@ -39,16 +33,9 @@ import android.view.WindowManager import android.view.WindowManager.LayoutParams.FLAG_SECURE import android.webkit.WebView import android.widget.Button -import android.widget.CheckBox -import android.widget.CompoundButton import android.widget.EditText import android.widget.ImageView -import android.widget.ProgressBar -import android.widget.RadioButton -import android.widget.RadioGroup -import android.widget.RatingBar import android.widget.Spinner -import android.widget.Switch import android.widget.TextView import androidx.compose.ui.node.RootForTest import androidx.compose.ui.semantics.SemanticsNode @@ -96,7 +83,6 @@ import com.posthog.internal.replay.RRMetaEvent import com.posthog.internal.replay.RRMouseInteraction import com.posthog.internal.replay.RRMutatedNode import com.posthog.internal.replay.RRRemovedNode -import com.posthog.internal.replay.RRStyle import com.posthog.internal.replay.RRWireframe import com.posthog.internal.replay.capture import curtains.Curtains @@ -210,9 +196,6 @@ public class PostHogReplayIntegration( private val isNativeSdk: Boolean get() = (config.sdkName != "posthog-flutter") - private val isScreenshotCapable: Boolean - get() = config.sessionReplayConfig.screenshot || !isNativeSdk - @Volatile private var postHog: PostHogInterface? = null @@ -273,9 +256,13 @@ public class PostHogReplayIntegration( drawState.isLegacyCaptureActive if (classifyLegacyDraw) { val isOnlyAnimationRedraw = - isScreenshotCapable && - !drawState.didLayoutSinceReset && - (view.hasTransientState() || view.hasActiveSurfaceRendering()) && + !drawState.didLayoutSinceReset && + // The surface scan walks the whole tree, so it runs only while a legacy + // capture can read the verdict; beginLegacyCapture resets it anyway. + ( + view.hasTransientState() || + (drawState.isLegacyCaptureActive && view.hasActiveSurfaceRendering()) + ) && !view.isAnimationRunning() drawState.recordLegacyAnimationRedraw(isOnlyAnimationRedraw) } @@ -755,7 +742,7 @@ public class PostHogReplayIntegration( } } } - val delivered = generateSnapshot(WeakReference(decorView), WeakReference(window), forceScreenshot = true) + val delivered = generateSnapshot(WeakReference(decorView), WeakReference(window)) if (!delivered) { config.logger.log("Session Replay bridge capture produced no frame (will retry next tick).") } @@ -771,25 +758,12 @@ public class PostHogReplayIntegration( } } - private fun Resources.Theme.toRGBColor(): String? { - val value = TypedValue() - resolveAttribute(android.R.attr.windowBackground, value, true) - return if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && - value.type <= TypedValue.TYPE_LAST_COLOR_INT - ) { - value.data - } else { - null - }?.toRGBColor() - } - // internal (not private) so tests can drive a snapshot pass directly. // Returns whether a frame was actually produced (false on every early bail), // so the bridge caller can tell "captured" from "silently skipped". internal fun generateSnapshot( viewRef: WeakReference, windowRef: WeakReference, - forceScreenshot: Boolean = false, ): Boolean { if (!isActive()) return false val postHog = postHog ?: return false @@ -808,28 +782,11 @@ public class PostHogReplayIntegration( val timestamp = config.dateProvider.currentTimeMillis() - val useScreenshot = config.sessionReplayConfig.screenshot || forceScreenshot val wireframe = - if (useScreenshot) { - view.toScreenshotWireframe( - window, - status.drawState, - ) ?: return false - } else if (window.isSecure()) { - view.toSecureWireframe() ?: return false - } else { - val tree = view.toWireframe() ?: return false - // The window can turn secure while the tree is walked. - if (window.isSecure()) view.toSecureWireframe() ?: return false else tree - } - - // if the decorView has no backgroundColor, we use the theme color - // no need to do this if we are capturing a screenshot - if (wireframe.style?.backgroundColor == null && !useScreenshot) { - context.theme?.toRGBColor()?.let { - wireframe.style?.backgroundColor = it - } - } + view.toScreenshotWireframe( + window, + status.drawState, + ) ?: return false val events = synchronized(decorViews) { @@ -895,40 +852,21 @@ public class PostHogReplayIntegration( events.add(event) status.sentFullSnapshot = true } else { + // Each window sends one screenshot node, so a frame is an update to that node. val lastSnapshot = status.lastSnapshot - val lastSnapshots = if (lastSnapshot != null) listOf(lastSnapshot) else emptyList() - val (addedItems, removedItems, updatedItems) = - findAddedAndRemovedItems( - lastSnapshots.flattenChildren(), - listOf(wireframe).flattenChildren(), - ) - - val addedNodes = mutableListOf() - addedItems.forEach { - val item = RRMutatedNode(it, parentId = it.parentId) - addedNodes.add(item) - } - - val removedNodes = mutableListOf() - removedItems.forEach { - val item = RRRemovedNode(it.id, parentId = it.parentId) - removedNodes.add(item) - } - - val updatedNodes = mutableListOf() - updatedItems.forEach { - val item = RRMutatedNode(it, parentId = it.parentId) - updatedNodes.add(item) - } - - if (addedNodes.isNotEmpty() || removedNodes.isNotEmpty() || updatedNodes.isNotEmpty()) { - val incrementalMutationData = - RRIncrementalMutationData( - adds = addedNodes.ifEmpty { null }, - removes = removedNodes.ifEmpty { null }, - updates = updatedNodes.ifEmpty { null }, - ) + val incrementalMutationData = + when { + lastSnapshot == null -> RRIncrementalMutationData(adds = listOf(RRMutatedNode(wireframe))) + lastSnapshot.id != wireframe.id -> + RRIncrementalMutationData( + adds = listOf(RRMutatedNode(wireframe)), + removes = listOf(RRRemovedNode(lastSnapshot.id)), + ) + lastSnapshot != wireframe -> RRIncrementalMutationData(updates = listOf(RRMutatedNode(wireframe))) + else -> null + } + if (incrementalMutationData != null) { val incrementalSnapshotEvent = RRIncrementalSnapshotEvent( mutationData = incrementalMutationData, @@ -1003,8 +941,8 @@ public class PostHogReplayIntegration( /** * Checks whether the view has a non-empty visible rect. Walk callers pass their walk so - * its scratch objects are reused; other callers allocate per call (see the wireframe - * vs delayed-screenshot-walk race test). + * its scratch objects are reused; other callers allocate per call (see the concurrent + * capture vs delayed-screenshot-walk race test). */ private fun View.hasGlobalVisibleRect(walk: MaskWalk? = null): Boolean { return if (isViewStateStableForMatrixOperations()) { @@ -1748,7 +1686,7 @@ public class PostHogReplayIntegration( findMaskableWidgets(this, it) } } ?: return false - if (walk.aborted || unsafeRedraw()) { + if (walk.aborted || walk.poisoned || unsafeRedraw()) { config.logger.log("Session Replay screenshot discarded due to screen changes.") return false } @@ -1910,7 +1848,6 @@ public class PostHogReplayIntegration( height = height, type = "screenshot", base64 = base64, - style = RRStyle(), ) } @@ -2084,7 +2021,6 @@ public class PostHogReplayIntegration( height = height, type = "screenshot", base64 = base64, - style = RRStyle(), ) } @@ -2097,503 +2033,6 @@ public class PostHogReplayIntegration( return this.isTextInputSensitive(ancestorUnmasked) } - // A FLAG_SECURE window is recorded as one black rectangle, with nothing from its view tree. - private fun View.toSecureWireframe(): RRWireframe? { - if (!isVisible()) { - return null - } - val coordinates = IntArray(2) - if (isViewStateStableForMatrixOperations()) { - getLocationOnScreen(coordinates) - } - return RRWireframe( - id = System.identityHashCode(this), - x = coordinates[0].densityValue(screenDensity), - y = coordinates[1].densityValue(screenDensity), - width = width.densityValue(screenDensity), - height = height.densityValue(screenDensity), - style = RRStyle().apply { backgroundColor = "#000000" }, - ) - } - - private fun View.toWireframe( - parentId: Int? = null, - ancestorUnmasked: Boolean = false, - ): RRWireframe? { - val view = this - if (!view.isVisible()) { - return null - } - - val isUnmasked = ancestorUnmasked || view.isUnmasked() - - val viewId = System.identityHashCode(view) - - val coordinates = IntArray(2) - if (view.isViewStateStableForMatrixOperations()) { - view.getLocationOnScreen(coordinates) - } else { - // Use zero coordinates as fallback when view state is unstable - coordinates[0] = 0 - coordinates[1] = 0 - } - val x = coordinates[0].densityValue(screenDensity) - val y = coordinates[1].densityValue(screenDensity) - val width = view.width.densityValue(screenDensity) - val height = view.height.densityValue(screenDensity) - var base64: String? = null - - var type: String? = null - if (view.id == android.R.id.statusBarBackground) { - type = "status_bar" - } - if (view.id == android.R.id.navigationBarBackground) { - type = "navigation_bar" - } - - val style = RRStyle() - view.background?.let { background -> - background.toRGBColor()?.let { color -> - style.backgroundColor = color - } ?: run { - style.backgroundImage = background.base64(view.width, view.height) - } - } - - var checked: Boolean? = null - - var text: String? = null - var inputType: String? = null - var value: Any? = null - // button inherits from textview - if (view is TextView) { - val viewText = view.text?.toString() - if (!viewText.isNullOrEmpty()) { - text = - if (!view.shouldMaskTextView(isUnmasked)) { - viewText - } else { - viewText.mask() - } - } - - val hint = view.hint?.toString() - if (text.isNullOrEmpty() && !hint.isNullOrEmpty()) { - text = - if (!view.shouldMaskTextView(isUnmasked)) { - hint - } else { - hint.mask() - } - } - - type = "text" - style.color = view.currentTextColor.toRGBColor() - - // CompoundButton is a subclass of CheckBox, RadioButton, Switch, etc - if (view is Button && view !is CompoundButton) { - style.borderWidth = 1 - style.borderColor = "#000000" - type = "input" - inputType = "button" - value = text - text = null - } -// TODO: do this when we upgrade API to 34 -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { -// style.fontFamily = view.typeface?.systemFontFamilyName -// } else { - view.typeface?.let { - when (it) { - Typeface.DEFAULT -> style.fontFamily = "sans-serif" - Typeface.DEFAULT_BOLD -> style.fontFamily = "sans-serif-bold" - Typeface.MONOSPACE -> style.fontFamily = "monospace" - Typeface.SERIF -> style.fontFamily = "serif" - } - } -// } - style.fontSize = view.textSize.toInt().densityValue(screenDensity) - when (view.textAlignment) { - View.TEXT_ALIGNMENT_CENTER -> { - style.verticalAlign = "center" - style.horizontalAlign = "center" - } - View.TEXT_ALIGNMENT_TEXT_END, View.TEXT_ALIGNMENT_VIEW_END -> { - style.verticalAlign = "center" - style.horizontalAlign = "right" - } - View.TEXT_ALIGNMENT_TEXT_START, View.TEXT_ALIGNMENT_VIEW_START -> { - style.verticalAlign = "center" - style.horizontalAlign = "left" - } - View.TEXT_ALIGNMENT_GRAVITY -> { - val horizontalAlignment = - when (view.gravity.and(Gravity.HORIZONTAL_GRAVITY_MASK)) { - Gravity.START, Gravity.LEFT -> "left" - Gravity.END, Gravity.RIGHT -> "right" - Gravity.CENTER, Gravity.CENTER_HORIZONTAL -> "center" - else -> "left" - } - style.horizontalAlign = horizontalAlignment - - val verticalAlignment = - when (view.gravity.and(Gravity.VERTICAL_GRAVITY_MASK)) { - Gravity.TOP -> "top" - Gravity.BOTTOM -> "bottom" - Gravity.CENTER_VERTICAL, Gravity.CENTER -> "center" - else -> "center" - } - style.verticalAlign = verticalAlignment - } - else -> { - style.verticalAlign = "center" - style.horizontalAlign = "left" - } - } - - // left, top, right, bottom - view.compoundDrawables.forEachIndexed { index, drawable -> - drawable?.let { - val drawableBase64 = it.base64(view.width, view.height) - // TODO: the 2 other possible drawables (top and bottom are not common) - when (index) { - 0 -> style.iconLeft = drawableBase64 -// 1 -> style.iconTop = drawableBase64 - 2 -> style.iconRight = drawableBase64 -// 3 -> style.iconBottom = drawableBase64 - } - } - } - - // Do not set padding if the text is centered, otherwise the padding will be off - if (style.verticalAlign != "center") { - style.paddingTop = view.totalPaddingTop.densityValue(screenDensity) - style.paddingBottom = view.totalPaddingBottom.densityValue(screenDensity) - } - if (style.horizontalAlign != "center") { - style.paddingLeft = view.totalPaddingLeft.densityValue(screenDensity) - style.paddingRight = view.totalPaddingRight.densityValue(screenDensity) - } - } - - var label: String? = null - if (view is CheckBox) { - type = "input" - inputType = "checkbox" - label = text - text = null - checked = view.isChecked - } - if (view is RadioGroup) { - type = "radio_group" - } - if (view is RadioButton) { - type = "input" - inputType = "radio" - label = text - text = null - checked = view.isChecked - } - - if (view is EditText) { - type = "input" - inputType = "text_area" - value = text - text = null - } - var options: List? = null - if (view is Spinner) { - type = "input" - inputType = "select" - val mask = view.shouldMaskSpinner(isUnmasked) - view.selectedItem?.let { - val theValue = - if (!mask) { - it.toString() - } else { - it.toString().mask() - } - value = theValue - } - - view.adapter?.let { - val items = mutableListOf() - for (i in 0 until it.count) { - val item = it.getItem(i)?.toString() ?: continue - - val theItem = - if (!mask) { - item - } else { - item.mask() - } - - items.add(theItem) - } - options = items.ifEmpty { null } - } - } - - if (view is ImageView) { - type = "image" - if (!view.shouldMaskImage(isUnmasked)) { - // TODO: we can probably do a LRU caching here for already captured images - view.drawable?.let { drawable -> - base64 = drawable.base64(view.width, view.height) -// style.paddingTop = view.paddingTop.densityValue(screenDensity) -// style.paddingBottom = view.paddingBottom.densityValue(screenDensity) -// style.paddingLeft = view.paddingLeft.densityValue(screenDensity) -// style.paddingRight = view.paddingRight.densityValue(screenDensity) - } - } - } - - var max: Int? = null // can be a Int or Float - if (view is ProgressBar) { - inputType = "progress" - type = "input" - val bar = - if (view.isIndeterminate) { - "circular" - } else { - max = view.max - value = view.progress - "horizontal" - } - style.bar = bar - } - if (view is RatingBar) { - style.bar = "rating" - - // since stars allow half stars, we need to divide the max by 2, because - // 5 stars is 10 - max = (view.max / 2) - value = view.rating - } - - if (view is Switch) { - type = "input" - inputType = "toggle" - checked = view.isChecked - label = text - text = null - } - - // TODO: people might be used androidx.webkit:webkit though - if (view is WebView) { - type = "web_view" - } - - val children = mutableListOf() - if (view is ViewGroup && view.childCount > 0) { - for (i in 0 until view.childCount) { - val viewChild = view.getChildAt(i) ?: continue - viewChild.toWireframe(parentId = viewId, ancestorUnmasked = isUnmasked)?.let { - children.add(it) - } - } - } - - return RRWireframe( - id = viewId, - x = x, - y = y, - width = width, - height = height, - text = text, - type = type, - style = style, - childWireframes = children.ifEmpty { null }, - base64 = base64, - parentId = parentId, - disabled = !view.isEnabled, - checked = checked, - inputType = inputType, - value = value, - label = label, - options = options, - max = max, - ) - } - - private fun runDrawableConverter(drawable: Drawable): Bitmap? { - return config.sessionReplayConfig.drawableConverter?.convert(drawable) - } - - @SuppressLint("NewApi") - private fun Drawable.toRGBColor(): String? { - when (this) { - is ColorDrawable -> { - return color.toRGBColor() - } - - is RippleDrawable -> { - try { - return getFirstDrawable()?.toRGBColor() - } catch (e: Throwable) { - // ignore - } - } - - is InsetDrawable -> { - return drawable?.toRGBColor() - } - - is GradientDrawable -> { - colors?.let { rgcColors -> - if (rgcColors.isNotEmpty()) { - // Get the first color from the array - val color = rgcColors[0] - - // Extract RGB values - val red = Color.red(color) - val green = Color.green(color) - val blue = Color.blue(color) - - // Construct the RGB color - val rgb = Color.rgb(red, green, blue) - return rgb.toRGBColor() - } - } - color?.let { - if (it.defaultColor != -1) { - return it.defaultColor.toRGBColor() - } - } - } - } - return null - } - - private fun Drawable.base64( - width: Int, - height: Int, - cloned: Boolean = false, - ): String? { - val convertedBitmap = runDrawableConverter(this) - if (convertedBitmap != null) { - return convertedBitmap.webpBase64() - } - - var clonedDrawable = this - if (!cloned) { - clonedDrawable = copy() ?: return null - } - - when (clonedDrawable) { - is BitmapDrawable -> { - try { - return clonedDrawable.bitmap.webpBase64() - } catch (_: Throwable) { - // ignore - } - } - - is LayerDrawable -> { - clonedDrawable.getFirstDrawable()?.let { - return it.base64(width, height) - } - } - - is InsetDrawable -> { - clonedDrawable.drawable?.let { - return it.base64(width, height) - } - } - } - - try { - val bitmap = clonedDrawable.toBitmap(width, height) - val base64 = bitmap.webpBase64() - bitmap.recycle() - return base64 - } catch (_: Throwable) { - // ignore - } - return null - } - - private fun LayerDrawable.getFirstDrawable(): Drawable? { - for (i in 0 until numberOfLayers) { - getDrawable(i)?.let { - return it - } - } - - return null - } - - private fun Int.toRGBColor(): String { - // TODO: missing alpha - return String.format("#%06X", (0xFFFFFF and this)) - } - - private fun List.flattenChildren(): List { - val result = mutableListOf() - - for (item in this) { - result.add(item) - - item.childWireframes?.let { - result.addAll(it.flattenChildren()) - } - } - - return result - } - - private fun findAddedAndRemovedItems( - oldItems: List, - newItems: List, - ): Triple, List, List> { - val oldMap = oldItems.associateBy { it.id } - val newMap = newItems.associateBy { it.id } - - // Create HashSet to track unique IDs - val oldItemIds = HashSet(oldItems.map { it.id }) - val newItemIds = HashSet(newItems.map { it.id }) - - // Find added items by subtracting oldItemIds from newItemIds - val addedIds = newItemIds - oldItemIds - val addedItems = newItems.filter { it.id in addedIds } - - // Find removed items by subtracting newItemIds from oldItemIds - val removedIds = oldItemIds - newItemIds - val removedItems = oldItems.filter { it.id in removedIds } - - val updatedItems = mutableListOf() - - // Find updated items by finding the intersection of oldItemIds and newItemIds - val sameItems = oldItemIds.intersect(newItemIds) - - for (id in sameItems) { - // we have to copy without the childWireframes, otherwise they all would be different - // if one of the child is different, but we only wanna compare the parent - val oldItem = oldMap[id]?.copy(childWireframes = null) ?: continue - val newItem = newMap[id] ?: continue - val newItemCopy = newItem.copy(childWireframes = null) - - // If the items are different (any property has a different value), add the new item to the updatedItems list - if (oldItem != newItemCopy) { - updatedItems.add(newItem) - } - } - - return Triple(addedItems, removedItems, updatedItems) - } - - private fun Drawable.toBitmap( - width: Int, - height: Int, - ): Bitmap { - val bitmap = Bitmap.createBitmap(displayMetrics, width, height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.MULTIPLY) - setBounds(0, 0, canvas.width, canvas.height) - draw(canvas) - return bitmap - } - private fun MotionEvent.getRawXCompat(index: Int): Float { return if (index < 0 || index >= pointerCount) { rawX // Fallback to single-touch `rawX` to prevent crashes @@ -2628,14 +2067,6 @@ public class PostHogReplayIntegration( contentDescription?.toString()?.contains(PH_NO_MASK_LABEL, ignoreCase = true) == true } - private fun Drawable.copy(): Drawable? { - return constantState?.newDrawable() - } - - private fun String.mask(): String { - return "*".repeat(length) - } - @SuppressLint("AnnotateVersionCheck") private fun isSupported(): Boolean { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O @@ -2776,8 +2207,7 @@ public class PostHogReplayIntegration( props["\$sdk_debug_replay_pending_trigger_conditions"] = pendingConditions } - val captureMode = if (isScreenshotCapable) "screenshot" else "wireframe" - props["\$sdk_debug_replay_capture_mode"] = captureMode + props["\$sdk_debug_replay_capture_mode"] = "screenshot" props["\$sdk_debug_replay_internal_buffer_length"] = if (buffering) replayQueue?.bufferDepth ?: 0 else replayQueue?.size ?: 0 diff --git a/posthog-android/src/main/java/com/posthog/android/replay/PostHogSessionReplayConfig.kt b/posthog-android/src/main/java/com/posthog/android/replay/PostHogSessionReplayConfig.kt index 1d59c7b2e..2667e72f0 100644 --- a/posthog-android/src/main/java/com/posthog/android/replay/PostHogSessionReplayConfig.kt +++ b/posthog-android/src/main/java/com/posthog/android/replay/PostHogSessionReplayConfig.kt @@ -9,6 +9,8 @@ import com.posthog.android.replay.internal.MIN_SCREENSHOT_SCALE * * Use this as `PostHogAndroidConfig.sessionReplayConfig` when enabling session replay through * `PostHogConfig.sessionReplay`. + * + * Windows that set `FLAG_SECURE` are recorded as a solid black frame, and their touches are not recorded. */ public class PostHogSessionReplayConfig @JvmOverloads @@ -28,22 +30,8 @@ public class PostHogSessionReplayConfig * Defaults to true */ public var captureLogcat: Boolean = true, - /** - * Converts custom Drawable to Bitmap - * By default PostHog tries to convert the Drawable to Bitmap, the supported types are - * BitmapDrawable, ColorDrawable, GradientDrawable, InsetDrawable, LayerDrawable, RippleDrawable - */ - public var drawableConverter: PostHogDrawableConverter? = null, - /** - * By default Session replay will capture all the views on the screen as a wireframe, - * By enabling this option, PostHog will capture the screenshot of the screen. - * The screenshot may contain sensitive information, use with caution. - * In both modes, windows that set `FLAG_SECURE` are recorded as a solid black frame, and their touches are not recorded. - */ - public var screenshot: Boolean = false, /** * Throttling delay used to reduce the number of snapshots captured and reduce performance impact. - * This is used for capturing the view as a wireframe or screenshot. * The lower the number, the more snapshots are captured and the higher the performance impact. * Defaults to 1000ms = 1s. */ @@ -79,7 +67,7 @@ public class PostHogSessionReplayConfig * Higher values generally retain more detail and produce larger payloads. * Compression is lossy, except on Android 10 (API 29), where the platform's legacy * WebP encoder uses lossless compression at quality 100. - * Does not change screenshot resolution or enable screenshot capture. + * Does not change screenshot resolution. */ @PostHogExperimental @Volatile @@ -94,7 +82,7 @@ public class PostHogSessionReplayConfig * less capture at full resolution. For example, 0.5 captures half the width and height. * Dimensions are rounded up to at least one pixel; the logical replay viewport is unchanged. * NaN and infinite values reset the scale to `null`. - * Does not change compression quality, color mode, or enable screenshot capture. + * Does not change compression quality or color mode. */ @PostHogExperimental @Volatile @@ -108,7 +96,7 @@ public class PostHogSessionReplayConfig * to preserve alpha and color precision before lossy WebP compression. * [PostHogScreenshotColorMode.RGB_565] uses less bitmap memory but reduces color precision * and removes alpha, making transparent window regions appear black. Devices that reject - * RGB_565 fall back to ARGB_8888. Does not enable screenshot capture. + * RGB_565 fall back to ARGB_8888. */ @PostHogExperimental @Volatile diff --git a/posthog-android/src/test/java/com/posthog/android/PostHogAndroidConfigTest.kt b/posthog-android/src/test/java/com/posthog/android/PostHogAndroidConfigTest.kt index 1aa4837d8..261ed9384 100644 --- a/posthog-android/src/test/java/com/posthog/android/PostHogAndroidConfigTest.kt +++ b/posthog-android/src/test/java/com/posthog/android/PostHogAndroidConfigTest.kt @@ -42,19 +42,6 @@ internal class PostHogAndroidConfigTest { assertEquals(30, config.sessionReplayConfig.screenshotCompressionQuality) assertNull(config.sessionReplayConfig.screenshotScale) assertEquals(PostHogScreenshotColorMode.ARGB_8888, config.sessionReplayConfig.screenshotColorMode) - assertFalse(config.sessionReplayConfig.screenshot) - } - - @Test - fun `screenshot settings are independent of capture enablement`() { - config.sessionReplayConfig.screenshotScale = 0.5f - config.sessionReplayConfig.screenshotCompressionQuality = 60 - config.sessionReplayConfig.screenshotColorMode = PostHogScreenshotColorMode.RGB_565 - - assertEquals(0.5f, config.sessionReplayConfig.screenshotScale) - assertEquals(60, config.sessionReplayConfig.screenshotCompressionQuality) - assertEquals(PostHogScreenshotColorMode.RGB_565, config.sessionReplayConfig.screenshotColorMode) - assertFalse(config.sessionReplayConfig.screenshot) } @Test diff --git a/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt b/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt index 8e6170e70..e09c2c0c3 100644 --- a/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt +++ b/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayIntegrationTest.kt @@ -14,6 +14,7 @@ import android.os.Looper import android.util.Base64 import android.view.MotionEvent import android.view.PixelCopy +import android.view.SurfaceView import android.view.View import android.view.ViewGroup import android.view.Window @@ -107,7 +108,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertNotNull -import kotlin.test.assertNull import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @@ -736,78 +736,6 @@ internal class PostHogReplayIntegrationTest { } } - @Test - fun `a secure window is recorded as one black wireframe in wireframe mode`() { - val h = screenshotCaptureHarness() - try { - h.fx.config.sessionReplayConfig.screenshot = false - h.child.tag = null - h.child.text = "secret" - h.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) - - h.fx.sut.generateSnapshot(WeakReference(h.hookLayout), WeakReference(h.window)) - - @Suppress("UNCHECKED_CAST") - val events = h.fake.properties?.get("\$snapshot_data") as List - val fullSnapshot = events.first { it.type == RREventType.FullSnapshot } - val wireframe = ((fullSnapshot.data as Map<*, *>)["wireframes"] as List<*>).single() as RRWireframe - assertEquals("#000000", wireframe.style?.backgroundColor) - assertNull(wireframe.childWireframes) - assertNull(wireframe.text) - } finally { - h.fx.sut.uninstall() - } - } - - @Test - fun `a window secure when the wireframe walk starts is recorded as one black wireframe`() { - val h = screenshotCaptureHarness() - try { - h.fx.config.sessionReplayConfig.screenshot = false - h.child.tag = null - h.child.text = "secret" - h.window.attributes.flags = h.window.attributes.flags or WindowManager.LayoutParams.FLAG_SECURE - h.hookLayout.onWalkTouch = { - h.window.attributes.flags = h.window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE.inv() - } - - h.fx.sut.generateSnapshot(WeakReference(h.hookLayout), WeakReference(h.window)) - - @Suppress("UNCHECKED_CAST") - val events = h.fake.properties?.get("\$snapshot_data") as List - val fullSnapshot = events.first { it.type == RREventType.FullSnapshot } - val wireframe = ((fullSnapshot.data as Map<*, *>)["wireframes"] as List<*>).single() as RRWireframe - assertEquals("#000000", wireframe.style?.backgroundColor) - assertNull(wireframe.childWireframes) - } finally { - h.fx.sut.uninstall() - } - } - - @Test - fun `a window that turns secure during the wireframe walk is recorded as one black wireframe`() { - val h = screenshotCaptureHarness() - try { - h.fx.config.sessionReplayConfig.screenshot = false - h.child.tag = null - h.child.text = "secret" - h.hookLayout.onWalkTouch = { - h.window.attributes.flags = h.window.attributes.flags or WindowManager.LayoutParams.FLAG_SECURE - } - - h.fx.sut.generateSnapshot(WeakReference(h.hookLayout), WeakReference(h.window)) - - @Suppress("UNCHECKED_CAST") - val events = h.fake.properties?.get("\$snapshot_data") as List - val fullSnapshot = events.first { it.type == RREventType.FullSnapshot } - val wireframe = ((fullSnapshot.data as Map<*, *>)["wireframes"] as List<*>).single() as RRWireframe - assertEquals("#000000", wireframe.style?.backgroundColor) - assertNull(wireframe.childWireframes) - } finally { - h.fx.sut.uninstall() - } - } - private fun dispatchTouch( sut: PostHogReplayIntegration, action: Int = MotionEvent.ACTION_DOWN, @@ -2621,7 +2549,6 @@ internal class PostHogReplayIntegrationTest { integrationContext = ApplicationProvider.getApplicationContext(), ) fx.config.sessionReplayConfig.captureTouches = captureTouches - fx.config.sessionReplayConfig.screenshot = true fx.config.sessionReplayConfig.verifyScreenshotMaskAlignment = enableMaskAlignmentVerification val fake = PostHogFake() PostHogSessionManager.startSession() @@ -2743,7 +2670,6 @@ internal class PostHogReplayIntegrationTest { PostHogSessionManager.startSession() val sessionId = PostHogSessionManager.peekSessionId().toString() val fx = createIntegrationWithRealQueue(true, true, integrationContext = ApplicationProvider.getApplicationContext()) - fx.config.sessionReplayConfig.screenshot = true val client = object : PostHogInterface by core { override fun capture( @@ -4026,7 +3952,7 @@ internal class PostHogReplayIntegrationTest { @Test @Config(sdk = [26], shadows = [ShadowPixelCopy::class]) - fun `wireframe visibility does not race a delayed screenshot mask walk's shared scratch objects`() { + fun `a concurrent capture's visibility check does not race a delayed screenshot mask walk's shared scratch objects`() { val appContext = ApplicationProvider.getApplicationContext() val fx = createIntegrationWithRealQueue( @@ -4048,15 +3974,15 @@ internal class PostHogReplayIntegrationTest { val guardedEntered = CountDownLatch(1) val releaseGuardedCall = CountDownLatch(1) - val wireframeEntered = CountDownLatch(1) + val concurrentEntered = CountDownLatch(1) val guardedTimedOut = AtomicBoolean(false) val guardedRectWasCorrupted = AtomicBoolean(false) val guardedPointWasCorrupted = AtomicBoolean(false) val guardedRect = AtomicReference() val guardedPoint = AtomicReference() val guardedVisibilityCalls = AtomicInteger(0) - val wireframeReusedGuardedRect = AtomicBoolean(false) - val wireframeReusedGuardedPoint = AtomicBoolean(false) + val concurrentReusedGuardedRect = AtomicBoolean(false) + val concurrentReusedGuardedPoint = AtomicBoolean(false) val guardedRectValue = Rect(1, 2, 30, 40) val guardedPointValue = Point(5, 6) @@ -4076,19 +4002,19 @@ internal class PostHogReplayIntegrationTest { guardedPointWasCorrupted.set(point != guardedPointValue) } } - val wireframeView = + val concurrentView = VisibleRectHookView(activity) { rect, point -> - wireframeReusedGuardedRect.set(rect === guardedRect.get()) - wireframeReusedGuardedPoint.set(point === guardedPoint.get()) + concurrentReusedGuardedRect.set(rect === guardedRect.get()) + concurrentReusedGuardedPoint.set(point === guardedPoint.get()) rect.set(100, 200, 300, 400) point?.set(500, 600) - wireframeEntered.countDown() + concurrentEntered.countDown() } val guardedRoot = FrameLayout(activity).apply { addView(guardedView) } val outerRoot = FrameLayout(activity).apply { addView(guardedRoot) - addView(wireframeView) + addView(concurrentView) } activity.setContentView(outerRoot) shadowOf(Looper.getMainLooper()).idle() @@ -4096,18 +4022,18 @@ internal class PostHogReplayIntegrationTest { outerRoot.layout(0, 0, 100, 100) guardedRoot.layout(0, 0, 100, 50) guardedView.layout(0, 0, 50, 50) - wireframeView.layout(50, 0, 100, 50) + concurrentView.layout(50, 0, 100, 50) fx.sut.decorViews[guardedRoot] = ViewTreeSnapshotStatus(mock()) - fx.sut.decorViews[wireframeView] = ViewTreeSnapshotStatus(mock()) + fx.sut.decorViews[concurrentView] = ViewTreeSnapshotStatus(mock()) val executor = Executors.newFixedThreadPool(2) try { - val wireframeFuture = + val concurrentFuture = executor.submit { try { assertTrue(guardedEntered.await(3, TimeUnit.SECONDS)) fx.sut.generateSnapshot( - WeakReference(wireframeView), + WeakReference(concurrentView), WeakReference(activity.window), ) } finally { @@ -4119,7 +4045,6 @@ internal class PostHogReplayIntegrationTest { fx.sut.generateSnapshot( WeakReference(guardedRoot), WeakReference(activity.window), - forceScreenshot = true, ) } assertTrue( @@ -4127,21 +4052,21 @@ internal class PostHogReplayIntegrationTest { "The delayed screenshot callback did not reach its guarded post-copy mask walk", ) - val callsOverlapped = wireframeEntered.count == 0L + val callsOverlapped = concurrentEntered.count == 0L releaseGuardedCall.countDown() awaitDraining(screenshotFuture, 3000) - awaitDraining(wireframeFuture, 3000) + awaitDraining(concurrentFuture, 3000) assertFalse(guardedTimedOut.get(), "The guarded visibility hook timed out") val overlappingCallsWereSafe = - !wireframeReusedGuardedRect.get() && - !wireframeReusedGuardedPoint.get() && + !concurrentReusedGuardedRect.get() && + !concurrentReusedGuardedPoint.get() && !guardedRectWasCorrupted.get() && !guardedPointWasCorrupted.get() assertTrue( !callsOverlapped || overlappingCallsWereSafe, - "An unguarded wireframe visibility call entered during the delayed screenshot mask walk and reused " + - "its Rect=${wireframeReusedGuardedRect.get()} and Point=${wireframeReusedGuardedPoint.get()}; " + + "An unguarded visibility call entered during the delayed screenshot mask walk and reused " + + "its Rect=${concurrentReusedGuardedRect.get()} and Point=${concurrentReusedGuardedPoint.get()}; " + "guarded Rect corrupted=${guardedRectWasCorrupted.get()}, " + "Point corrupted=${guardedPointWasCorrupted.get()}", ) @@ -4647,6 +4572,48 @@ internal class PostHogReplayIntegrationTest { } } + @Test + fun `draws outside a legacy capture skip the surface scan`() { + val (fx, _) = screenshotFixture(enableMaskAlignmentVerification = false) + try { + val activity = Robolectric.buildActivity(Activity::class.java).setup().get() + shadowOf(Looper.getMainLooper()).idle() + val root = CountingChildFrameLayout(activity).apply { addView(View(activity)) } + val drawState = WindowDrawState() + drawState.recordDraw() + fx.sut.onDrawCallback(root, drawState) + val walkCountAfterFirstDraw = root.childWalkCount + + drawState.recordDraw() + fx.sut.onDrawCallback(root, drawState) + + assertEquals(walkCountAfterFirstDraw, root.childWalkCount) + } finally { + fx.sut.uninstall() + } + } + + @Test + fun `a surface redraw during a legacy capture is animation only`() { + val (fx, _) = screenshotFixture(enableMaskAlignmentVerification = false) + try { + val activity = Robolectric.buildActivity(Activity::class.java).setup().get() + shadowOf(Looper.getMainLooper()).idle() + val surface = SurfaceView(activity) + val root = FrameLayout(activity).apply { addView(surface) } + surface.layout(0, 0, 100, 100) + val drawState = WindowDrawState() + drawState.beginLegacyCapture() + drawState.recordDraw() + + fx.sut.onDrawCallback(root, drawState) + + assertTrue(drawState.isOnlyAnimationRedraw) + } finally { + fx.sut.uninstall() + } + } + // An animating view can't be placed, so the walk would otherwise ship its overflow unmasked. @Test fun `legacy mask walk is poisoned by an animating no-capture descendant only`() { @@ -5113,31 +5080,10 @@ internal class PostHogReplayIntegrationTest { } @Test - fun `debugProperties reports screenshot capture mode when the screenshot flag is on`() { - val config = PostHogAndroidConfig(PROJECT_TOKEN) - config.sessionReplayConfig.screenshot = true - val sut = getSut(config) - - assertEquals("screenshot", sut.debugProperties()["\$sdk_debug_replay_capture_mode"]) - } - - @Test - fun `debugProperties reports screenshot capture mode for the flutter host`() { - val config = PostHogAndroidConfig(PROJECT_TOKEN) - config.sdkName = "posthog-flutter" - config.sessionReplayConfig.screenshot = false - val sut = getSut(config) + fun `debugProperties reports screenshot capture mode`() { + val sut = getSut(PostHogAndroidConfig(PROJECT_TOKEN)) assertEquals("screenshot", sut.debugProperties()["\$sdk_debug_replay_capture_mode"]) - } - - @Test - fun `debugProperties reports wireframe capture mode otherwise`() { - val config = PostHogAndroidConfig(PROJECT_TOKEN) - config.sessionReplayConfig.screenshot = false - val sut = getSut(config) - - assertEquals("wireframe", sut.debugProperties()["\$sdk_debug_replay_capture_mode"]) assertFalse(sut.debugProperties().containsKey("\$sdk_debug_replay_throttle_delay_ms")) } @@ -5271,7 +5217,7 @@ internal class PostHogReplayIntegrationTest { val props = sut.debugProperties() assertEquals("disabled", props["\$recording_status"]) assertFalse(props.containsKey("\$sdk_debug_replay_flush_hold_reason")) - assertEquals("wireframe", props["\$sdk_debug_replay_capture_mode"]) + assertEquals("screenshot", props["\$sdk_debug_replay_capture_mode"]) assertEquals(0, props["\$sdk_debug_replay_internal_buffer_length"]) } finally { sut.uninstall() diff --git a/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayMaskWalkBenchmark.kt b/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayMaskWalkBenchmark.kt index 4fc2af9b9..308b721ac 100644 --- a/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayMaskWalkBenchmark.kt +++ b/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayMaskWalkBenchmark.kt @@ -44,7 +44,6 @@ internal class PostHogReplayMaskWalkBenchmark { assumeTrue(System.getenv("POSTHOG_BENCHMARK") == "1") val config = PostHogAndroidConfig(PROJECT_TOKEN).apply { - sessionReplayConfig.screenshot = true sessionReplayConfig.verifyScreenshotMaskAlignment = true } sut = PostHogReplayIntegration(ApplicationProvider.getApplicationContext(), config, MainHandler()) diff --git a/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayTestUtils.kt b/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayTestUtils.kt index fb1b5069a..a564cf726 100644 --- a/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayTestUtils.kt +++ b/posthog-android/src/test/java/com/posthog/android/replay/PostHogReplayTestUtils.kt @@ -13,14 +13,13 @@ import java.util.concurrent.TimeoutException internal fun PostHogReplayIntegration.captureSnapshot( viewRef: WeakReference, windowRef: WeakReference, - forceScreenshot: Boolean = false, ): Boolean { if (Looper.myLooper() != Looper.getMainLooper()) { - return generateSnapshot(viewRef, windowRef, forceScreenshot) + return generateSnapshot(viewRef, windowRef) } val executor = Executors.newSingleThreadExecutor() try { - val future = executor.submit(Callable { generateSnapshot(viewRef, windowRef, forceScreenshot) }) + val future = executor.submit(Callable { generateSnapshot(viewRef, windowRef) }) val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(4) while (System.nanoTime() < deadline) { shadowOf(Looper.getMainLooper()).idle() diff --git a/posthog-android/src/test/resources/json/snapshots/batch-request.json b/posthog-android/src/test/resources/json/snapshots/batch-request.json index c9d3b3997..737b87914 100644 --- a/posthog-android/src/test/resources/json/snapshots/batch-request.json +++ b/posthog-android/src/test/resources/json/snapshots/batch-request.json @@ -104,7 +104,7 @@ "$recording_status": "disabled", "$sdk_debug_replay_event_trigger_status": "trigger_disabled", "$sdk_debug_replay_linked_flag_trigger_status": "trigger_disabled", - "$sdk_debug_replay_capture_mode": "wireframe", + "$sdk_debug_replay_capture_mode": "screenshot", "$sdk_debug_replay_internal_buffer_length": 0, "$sdk_debug_session_start": 1700000000123 }, diff --git a/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt b/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt index bfdb0e45e..015d67cb2 100644 --- a/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt +++ b/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt @@ -50,7 +50,6 @@ class MyApp : Application() { sessionReplayConfig.maskAllTextInputs = false sessionReplayConfig.maskAllImages = false sessionReplayConfig.captureLogcat = true - sessionReplayConfig.screenshot = true // Surveys are opt-in. With :posthog-android-surveys-compose on the classpath the // core SDK auto-discovers the Compose UI delegate — no surveysConfig wiring needed. surveys = true diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index fdf89f65f..9ecba9641 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -1411,68 +1411,6 @@ public final class com/posthog/internal/replay/RRRemovedNode { public final fun getParentId ()Ljava/lang/Integer; } -public final class com/posthog/internal/replay/RRStyle { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component10 ()Ljava/lang/String; - public final fun component11 ()Ljava/lang/Integer; - public final fun component12 ()Ljava/lang/Integer; - public final fun component13 ()Ljava/lang/Integer; - public final fun component14 ()Ljava/lang/Integer; - public final fun component15 ()Ljava/lang/String; - public final fun component16 ()Ljava/lang/String; - public final fun component17 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Ljava/lang/Integer; - public final fun component5 ()Ljava/lang/Integer; - public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Ljava/lang/Integer; - public final fun component8 ()Ljava/lang/String; - public final fun component9 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/internal/replay/RRStyle; - public static synthetic fun copy$default (Lcom/posthog/internal/replay/RRStyle;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/posthog/internal/replay/RRStyle; - public fun equals (Ljava/lang/Object;)Z - public final fun getBackgroundColor ()Ljava/lang/String; - public final fun getBackgroundImage ()Ljava/lang/String; - public final fun getBar ()Ljava/lang/String; - public final fun getBorderColor ()Ljava/lang/String; - public final fun getBorderRadius ()Ljava/lang/Integer; - public final fun getBorderWidth ()Ljava/lang/Integer; - public final fun getColor ()Ljava/lang/String; - public final fun getFontFamily ()Ljava/lang/String; - public final fun getFontSize ()Ljava/lang/Integer; - public final fun getHorizontalAlign ()Ljava/lang/String; - public final fun getIconLeft ()Ljava/lang/String; - public final fun getIconRight ()Ljava/lang/String; - public final fun getPaddingBottom ()Ljava/lang/Integer; - public final fun getPaddingLeft ()Ljava/lang/Integer; - public final fun getPaddingRight ()Ljava/lang/Integer; - public final fun getPaddingTop ()Ljava/lang/Integer; - public final fun getVerticalAlign ()Ljava/lang/String; - public fun hashCode ()I - public final fun setBackgroundColor (Ljava/lang/String;)V - public final fun setBackgroundImage (Ljava/lang/String;)V - public final fun setBar (Ljava/lang/String;)V - public final fun setBorderColor (Ljava/lang/String;)V - public final fun setBorderRadius (Ljava/lang/Integer;)V - public final fun setBorderWidth (Ljava/lang/Integer;)V - public final fun setColor (Ljava/lang/String;)V - public final fun setFontFamily (Ljava/lang/String;)V - public final fun setFontSize (Ljava/lang/Integer;)V - public final fun setHorizontalAlign (Ljava/lang/String;)V - public final fun setIconLeft (Ljava/lang/String;)V - public final fun setIconRight (Ljava/lang/String;)V - public final fun setPaddingBottom (Ljava/lang/Integer;)V - public final fun setPaddingLeft (Ljava/lang/Integer;)V - public final fun setPaddingRight (Ljava/lang/Integer;)V - public final fun setPaddingTop (Ljava/lang/Integer;)V - public final fun setVerticalAlign (Ljava/lang/String;)V - public fun toString ()Ljava/lang/String; -} - public final class com/posthog/internal/replay/RRUtilsKt { public static final fun capture (Ljava/util/List;)V public static final fun capture (Ljava/util/List;Lcom/posthog/PostHogInterface;)V @@ -1480,44 +1418,22 @@ public final class com/posthog/internal/replay/RRUtilsKt { } public final class com/posthog/internal/replay/RRWireframe { - public fun (IIIIILjava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Lcom/posthog/internal/replay/RRStyle;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;)V - public synthetic fun (IIIIILjava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Lcom/posthog/internal/replay/RRStyle;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (IIIIILjava/lang/String;Ljava/lang/String;)V + public synthetic fun (IIIIILjava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()I - public final fun component10 ()Ljava/lang/String; - public final fun component11 ()Ljava/lang/Object; - public final fun component12 ()Ljava/lang/String; - public final fun component13 ()Lcom/posthog/internal/replay/RRStyle; - public final fun component14 ()Ljava/lang/Boolean; - public final fun component15 ()Ljava/lang/Boolean; - public final fun component16 ()Ljava/util/List; - public final fun component17 ()Ljava/lang/Integer; - public final fun component18 ()Ljava/lang/Integer; public final fun component2 ()I public final fun component3 ()I public final fun component4 ()I public final fun component5 ()I - public final fun component6 ()Ljava/util/List; + public final fun component6 ()Ljava/lang/String; public final fun component7 ()Ljava/lang/String; - public final fun component8 ()Ljava/lang/String; - public final fun component9 ()Ljava/lang/String; - public final fun copy (IIIIILjava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Lcom/posthog/internal/replay/RRStyle;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;)Lcom/posthog/internal/replay/RRWireframe; - public static synthetic fun copy$default (Lcom/posthog/internal/replay/RRWireframe;IIIIILjava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Lcom/posthog/internal/replay/RRStyle;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/posthog/internal/replay/RRWireframe; + public final fun copy (IIIIILjava/lang/String;Ljava/lang/String;)Lcom/posthog/internal/replay/RRWireframe; + public static synthetic fun copy$default (Lcom/posthog/internal/replay/RRWireframe;IIIIILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/posthog/internal/replay/RRWireframe; public fun equals (Ljava/lang/Object;)Z public final fun getBase64 ()Ljava/lang/String; - public final fun getChecked ()Ljava/lang/Boolean; - public final fun getChildWireframes ()Ljava/util/List; - public final fun getDisabled ()Ljava/lang/Boolean; public final fun getHeight ()I public final fun getId ()I - public final fun getInputType ()Ljava/lang/String; - public final fun getLabel ()Ljava/lang/String; - public final fun getMax ()Ljava/lang/Integer; - public final fun getOptions ()Ljava/util/List; - public final fun getParentId ()Ljava/lang/Integer; - public final fun getStyle ()Lcom/posthog/internal/replay/RRStyle; - public final fun getText ()Ljava/lang/String; public final fun getType ()Ljava/lang/String; - public final fun getValue ()Ljava/lang/Object; public final fun getWidth ()I public final fun getX ()I public final fun getY ()I diff --git a/posthog/src/main/java/com/posthog/internal/replay/RRStyle.kt b/posthog/src/main/java/com/posthog/internal/replay/RRStyle.kt deleted file mode 100644 index 2f1ea723a..000000000 --- a/posthog/src/main/java/com/posthog/internal/replay/RRStyle.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.posthog.internal.replay - -import com.posthog.PostHogInternal - -@PostHogInternal -public data class RRStyle( - public var color: String? = null, - public var backgroundColor: String? = null, - public var backgroundImage: String? = null, - public var borderWidth: Int? = null, - public var borderRadius: Int? = null, - public var borderColor: String? = null, - public var fontSize: Int? = null, - public var fontFamily: String? = null, - public var horizontalAlign: String? = null, - public var verticalAlign: String? = null, - public var paddingTop: Int? = null, - public var paddingBottom: Int? = null, - public var paddingLeft: Int? = null, - public var paddingRight: Int? = null, - public var bar: String? = null, - public var iconLeft: String? = null, - public var iconRight: String? = null, -) diff --git a/posthog/src/main/java/com/posthog/internal/replay/RRWireframe.kt b/posthog/src/main/java/com/posthog/internal/replay/RRWireframe.kt index c9f7309f0..a51d10f81 100644 --- a/posthog/src/main/java/com/posthog/internal/replay/RRWireframe.kt +++ b/posthog/src/main/java/com/posthog/internal/replay/RRWireframe.kt @@ -3,7 +3,6 @@ package com.posthog.internal.replay import com.posthog.PostHogInternal import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -// TODO: create abstractions on top of RRWireframe, eg RRTextWireframe, etc @IgnoreJRERequirement @PostHogInternal public data class RRWireframe( @@ -12,21 +11,7 @@ public data class RRWireframe( public val y: Int, public val width: Int, public val height: Int, - public val childWireframes: List? = null, - // text|image|rectangle|input|div|screenshot + // screenshot public val type: String? = null, - // checkbox|radio|text|password|email|number|search|tel|url|select|textarea|button - public val inputType: String? = null, - public val text: String? = null, - public val label: String? = null, - // string or number - public val value: Any? = null, public val base64: String? = null, - public val style: RRStyle? = null, - public val disabled: Boolean? = null, - public val checked: Boolean? = null, - public val options: List? = null, - @Transient - public val parentId: Int? = null, - public val max: Int? = null, )