From ca15b03079884b1c0bcb591391a7900f5a5562a7 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Thu, 17 Oct 2024 11:57:13 +0530 Subject: [PATCH 01/13] [#189] Adding minSessionDuration to config and update usage --- USAGE.md | 2 + .../replay/PostHogSessionReplayConfig.kt | 97 ++++++++++--------- .../java/com/posthog/android/sample/MyApp.kt | 1 + 3 files changed, 55 insertions(+), 45 deletions(-) diff --git a/USAGE.md b/USAGE.md index 196fc180..e42e9b45 100644 --- a/USAGE.md +++ b/USAGE.md @@ -218,6 +218,8 @@ val config = PostHogAndroidConfig(apiKey).apply { sessionReplayConfig.screenshot = false // debouncerDelayMs is 500ms by default sessionReplayConfig.debouncerDelayMs = 1000 + // minSessionDurationMs is 1000ms by default + sessionReplayConfig.minSessionDurationMs = 2000 } ``` 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 a94ea35f..5111c2f9 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 @@ -4,48 +4,55 @@ import com.posthog.PostHogExperimental @PostHogExperimental public class PostHogSessionReplayConfig - @JvmOverloads - constructor( - /** - * Enable masking of all text input fields - * Defaults to true - * This isn't supported if using Jetpack Compose views, use with caution - */ - @PostHogExperimental - public var maskAllTextInputs: Boolean = true, - /** - * Enable masking of all images to a placeholder - * Defaults to true - * This isn't supported if using Jetpack Compose views, use with caution - */ - @PostHogExperimental - public var maskAllImages: Boolean = true, - /** - * Enable capturing of logcat as console events - * Defaults to true - */ - @PostHogExperimental - 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 - */ - @PostHogExperimental - 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. - */ - @PostHogExperimental - public var screenshot: Boolean = false, - /** - * Deboucer 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 more snapshots will be captured but higher the performance impact - * Defaults to 500ms - */ - @PostHogExperimental - public var debouncerDelayMs: Long = 500, - ) +@JvmOverloads +constructor( + /** + * Enable masking of all text input fields + * Defaults to true + * This isn't supported if using Jetpack Compose views, use with caution + */ + @PostHogExperimental + public var maskAllTextInputs: Boolean = true, + /** + * Enable masking of all images to a placeholder + * Defaults to true + * This isn't supported if using Jetpack Compose views, use with caution + */ + @PostHogExperimental + public var maskAllImages: Boolean = true, + /** + * Enable capturing of logcat as console events + * Defaults to true + */ + @PostHogExperimental + 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 + */ + @PostHogExperimental + 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. + */ + @PostHogExperimental + public var screenshot: Boolean = false, + /** + * Deboucer 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 more snapshots will be captured but higher the performance impact + * Defaults to 500ms + */ + @PostHogExperimental + public var debouncerDelayMs: Long = 500, + /** + * Define the minimum duration for sessions to be recorded. + * This is useful if you want to exclude sessions that are too short to be useful. + * Defaults to 1000ms + */ + @PostHogExperimental + public var minSessionDurationMs: Long = 1000, +) 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 7f3d3798..63f85048 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 @@ -40,6 +40,7 @@ class MyApp : Application() { sessionReplayConfig.maskAllImages = false sessionReplayConfig.captureLogcat = true sessionReplayConfig.screenshot = true + sessionReplayConfig.minSessionDurationMs = 2000 } PostHogAndroid.setup(this, config) } From 478cd56ad6c5141492ecdaa663657ce6a02082d1 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Thu, 17 Oct 2024 12:55:04 +0530 Subject: [PATCH 02/13] [#189] WIP: Delete recording if less than min session length. --- .../replay/PostHogReplayIntegration.kt | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) 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 70488127..57a70f14 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 @@ -118,6 +118,10 @@ public class PostHogReplayIntegration( private val isSessionReplayEnabled: Boolean get() = PostHog.isSessionReplayActive() + private var sessionStartTime = 0L + private var sessionEndTime = 0L + + private fun addView( view: View, added: Boolean = true, @@ -148,7 +152,11 @@ public class PostHogReplayIntegration( executor.submit { try { - generateSnapshot(WeakReference(decorView), WeakReference(window), timestamp) + generateSnapshot( + WeakReference(decorView), + WeakReference(window), + timestamp + ) } catch (e: Throwable) { config.logger.log("Session Replay generateSnapshot failed: $e.") } @@ -229,10 +237,19 @@ public class PostHogReplayIntegration( } when (motionEvent.action.and(MotionEvent.ACTION_MASK)) { MotionEvent.ACTION_DOWN -> { - generateMouseInteractions(timestamp, motionEvent, RRMouseInteraction.TouchStart) + generateMouseInteractions( + timestamp, + motionEvent, + RRMouseInteraction.TouchStart + ) } + MotionEvent.ACTION_UP -> { - generateMouseInteractions(timestamp, motionEvent, RRMouseInteraction.TouchEnd) + generateMouseInteractions( + timestamp, + motionEvent, + RRMouseInteraction.TouchEnd + ) } } } catch (e: Throwable) { @@ -265,7 +282,8 @@ public class PostHogReplayIntegration( x = absX, y = absY, ) - val mouseInteraction = RRIncrementalMouseInteractionEvent(mouseInteractionData, timestamp) + val mouseInteraction = + RRIncrementalMouseInteractionEvent(mouseInteractionData, timestamp) mouseInteractions.add(mouseInteraction) } @@ -312,6 +330,9 @@ public class PostHogReplayIntegration( // workaround for react native that is started after the window is added // Curtains.rootViews should be empty for normal apps yet + + sessionStartTime = config.dateProvider.currentTimeMillis() + Curtains.rootViews.forEach { view -> addView(view) } @@ -325,6 +346,13 @@ public class PostHogReplayIntegration( override fun uninstall() { try { + sessionEndTime = config.dateProvider.currentTimeMillis() + + //TODO improve this + if (sessionEndTime - sessionStartTime <= config.sessionReplayConfig.minSessionDurationMs) { + //Delete existing session recording? + } + Curtains.onRootViewsChangedListeners -= onRootViewsChangedListener decorViews.entries.forEach { @@ -742,14 +770,17 @@ public class PostHogReplayIntegration( 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)) { @@ -769,6 +800,7 @@ public class PostHogReplayIntegration( } style.verticalAlign = verticalAlignment } + else -> { style.verticalAlign = "center" style.horizontalAlign = "left" From 69a30562b5b5fb064421fa380c7ba15951e76c58 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Thu, 17 Oct 2024 13:40:51 +0530 Subject: [PATCH 03/13] Move to capture events only when min duration passed. --- .../replay/PostHogReplayIntegration.kt | 20 +++++++++---------- .../java/com/posthog/android/sample/MyApp.kt | 2 +- posthog/src/main/java/com/posthog/PostHog.kt | 3 ++- 3 files changed, 12 insertions(+), 13 deletions(-) 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 57a70f14..c808ee6f 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 @@ -119,8 +119,7 @@ public class PostHogReplayIntegration( get() = PostHog.isSessionReplayActive() private var sessionStartTime = 0L - private var sessionEndTime = 0L - + private val events = mutableListOf() private fun addView( view: View, @@ -346,12 +345,6 @@ public class PostHogReplayIntegration( override fun uninstall() { try { - sessionEndTime = config.dateProvider.currentTimeMillis() - - //TODO improve this - if (sessionEndTime - sessionStartTime <= config.sessionReplayConfig.minSessionDurationMs) { - //Delete existing session recording? - } Curtains.onRootViewsChangedListeners -= onRootViewsChangedListener @@ -401,8 +394,6 @@ public class PostHogReplayIntegration( } } - val events = mutableListOf() - if (!status.sentMetaEvent) { val title = view.phoneWindow?.attributes?.title?.toString()?.substringAfter("/") ?: "" // TODO: cache and compare, if size changes, we send a ViewportResize event @@ -481,13 +472,20 @@ public class PostHogReplayIntegration( events.add(it) } - if (events.isNotEmpty()) { + if (events.isNotEmpty() && sessionLongerThanMinDuration()) { events.capture() + events.clear() } status.lastSnapshot = wireframe } + private fun sessionLongerThanMinDuration(): Boolean { + //TODO improve this -> Consider BE variable as well + return config.dateProvider.currentTimeMillis() - sessionStartTime >= + config.sessionReplayConfig.minSessionDurationMs + } + private fun View.isVisible(): Boolean { // TODO: also check for getGlobalVisibleRect intersects the display val visible = isShown && width >= 0 && height >= 0 && this !is ViewStub 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 63f85048..9470d957 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 @@ -40,7 +40,7 @@ class MyApp : Application() { sessionReplayConfig.maskAllImages = false sessionReplayConfig.captureLogcat = true sessionReplayConfig.screenshot = true - sessionReplayConfig.minSessionDurationMs = 2000 + sessionReplayConfig.minSessionDurationMs = 5000 } PostHogAndroid.setup(this, config) } diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index 1ecc29ee..76f20754 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -853,7 +853,8 @@ public class PostHog private constructor( // this is used in cases where we know the session is already active // so we spare another locker private fun isSessionReplayFlagActive(): Boolean { - return config?.sessionReplay == true && featureFlags?.isSessionReplayFlagActive() == true + //FIXME -> Remove this true hardcoding + return true || config?.sessionReplay == true && featureFlags?.isSessionReplayFlagActive() == true } override fun isSessionReplayActive(): Boolean { From e9d0a2190f93a3ac3cdd4938d668b63e56c01985 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Thu, 17 Oct 2024 14:28:38 +0530 Subject: [PATCH 04/13] Prevent frequent calculations if minthreshold is crossed once. --- .../android/replay/PostHogReplayIntegration.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 c808ee6f..ce8e1f28 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 @@ -120,6 +120,7 @@ public class PostHogReplayIntegration( private var sessionStartTime = 0L private val events = mutableListOf() + private var minSessionThresholdCrossed = false private fun addView( view: View, @@ -473,6 +474,7 @@ public class PostHogReplayIntegration( } if (events.isNotEmpty() && sessionLongerThanMinDuration()) { + config.logger.log("Session replay events captured: " + events.size) events.capture() events.clear() } @@ -481,9 +483,12 @@ public class PostHogReplayIntegration( } private fun sessionLongerThanMinDuration(): Boolean { - //TODO improve this -> Consider BE variable as well - return config.dateProvider.currentTimeMillis() - sessionStartTime >= - config.sessionReplayConfig.minSessionDurationMs + if (!minSessionThresholdCrossed) { + minSessionThresholdCrossed = + config.dateProvider.currentTimeMillis() - sessionStartTime >= + config.sessionReplayConfig.minSessionDurationMs + } + return minSessionThresholdCrossed } private fun View.isVisible(): Boolean { From d846f980569a4229dfbede414c39f2fd1fa7c110 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Thu, 17 Oct 2024 15:01:45 +0530 Subject: [PATCH 05/13] Read values from server config for min duration --- .../replay/PostHogReplayIntegration.kt | 21 +++++++++++++++++-- .../posthog/internal/PostHogPreferences.kt | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) 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 ce8e1f28..a78a4d01 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 @@ -58,6 +58,7 @@ import com.posthog.android.internal.screenSize import com.posthog.android.replay.internal.NextDrawListener.Companion.onNextDraw import com.posthog.android.replay.internal.ViewTreeSnapshotStatus import com.posthog.android.replay.internal.isAliveAndAttachedToWindow +import com.posthog.internal.PostHogPreferences import com.posthog.internal.PostHogThreadFactory import com.posthog.internal.replay.RRCustomEvent import com.posthog.internal.replay.RREvent @@ -122,6 +123,11 @@ public class PostHogReplayIntegration( private val events = mutableListOf() private var minSessionThresholdCrossed = false + @Suppress("UNCHECKED_CAST") + private val replayPreferenceMap by lazy { + config.cachePreferences?.getValue(PostHogPreferences.SESSION_REPLAY) as? Map + } + private fun addView( view: View, added: Boolean = true, @@ -483,10 +489,21 @@ public class PostHogReplayIntegration( } private fun sessionLongerThanMinDuration(): Boolean { + //Check value only if threshold not crossed. if (!minSessionThresholdCrossed) { + val serverMinDuration = replayPreferenceMap?.let { map -> + (map["minimumDurationMilliseconds"] as Number).toLong() + } ?: 0L + + //Give server min duration a higher priority + val finalMinimumDuration = if (serverMinDuration <= 0) { + serverMinDuration + } else { + config.sessionReplayConfig.minSessionDurationMs + } + minSessionThresholdCrossed = - config.dateProvider.currentTimeMillis() - sessionStartTime >= - config.sessionReplayConfig.minSessionDurationMs + config.dateProvider.currentTimeMillis() - sessionStartTime >= finalMinimumDuration } return minSessionThresholdCrossed } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogPreferences.kt b/posthog/src/main/java/com/posthog/internal/PostHogPreferences.kt index 27fa6d9c..c408ca35 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogPreferences.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogPreferences.kt @@ -33,7 +33,7 @@ public interface PostHogPreferences { internal const val OPT_OUT = "opt-out" internal const val FEATURE_FLAGS = "featureFlags" internal const val FEATURE_FLAGS_PAYLOAD = "featureFlagsPayload" - internal const val SESSION_REPLAY = "sessionReplay" + public const val SESSION_REPLAY: String = "sessionReplay" public const val VERSION: String = "version" public const val BUILD: String = "build" public const val STRINGIFIED_KEYS: String = "stringifiedKeys" From 787a06fb6d06c2f53f3cb0fd28658d43f9ad4fc8 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Thu, 17 Oct 2024 15:04:49 +0530 Subject: [PATCH 06/13] Fix bug of server min duration priority --- .../com/posthog/android/replay/PostHogReplayIntegration.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a78a4d01..572c23b3 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 @@ -495,8 +495,8 @@ public class PostHogReplayIntegration( (map["minimumDurationMilliseconds"] as Number).toLong() } ?: 0L - //Give server min duration a higher priority - val finalMinimumDuration = if (serverMinDuration <= 0) { + //Give server min duration is set, give it a higher priority than locally passed config + val finalMinimumDuration = if (serverMinDuration > 0) { serverMinDuration } else { config.sessionReplayConfig.minSessionDurationMs From 66faf338aa3be3994e452cd6addb9fa960785966 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Mon, 4 Nov 2024 11:21:08 +0530 Subject: [PATCH 07/13] Rollback all formatting changes --- .../replay/PostHogReplayIntegration.kt | 27 +---- .../replay/PostHogSessionReplayConfig.kt | 104 +++++++++--------- 2 files changed, 56 insertions(+), 75 deletions(-) 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 572c23b3..c939d49e 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 @@ -158,11 +158,7 @@ public class PostHogReplayIntegration( executor.submit { try { - generateSnapshot( - WeakReference(decorView), - WeakReference(window), - timestamp - ) + generateSnapshot(WeakReference(decorView), WeakReference(window), timestamp) } catch (e: Throwable) { config.logger.log("Session Replay generateSnapshot failed: $e.") } @@ -243,19 +239,10 @@ public class PostHogReplayIntegration( } when (motionEvent.action.and(MotionEvent.ACTION_MASK)) { MotionEvent.ACTION_DOWN -> { - generateMouseInteractions( - timestamp, - motionEvent, - RRMouseInteraction.TouchStart - ) + generateMouseInteractions(timestamp, motionEvent, RRMouseInteraction.TouchStart) } - MotionEvent.ACTION_UP -> { - generateMouseInteractions( - timestamp, - motionEvent, - RRMouseInteraction.TouchEnd - ) + generateMouseInteractions(timestamp, motionEvent, RRMouseInteraction.TouchEnd) } } } catch (e: Throwable) { @@ -288,8 +275,7 @@ public class PostHogReplayIntegration( x = absX, y = absY, ) - val mouseInteraction = - RRIncrementalMouseInteractionEvent(mouseInteractionData, timestamp) + val mouseInteraction = RRIncrementalMouseInteractionEvent(mouseInteractionData, timestamp) mouseInteractions.add(mouseInteraction) } @@ -352,7 +338,6 @@ public class PostHogReplayIntegration( override fun uninstall() { try { - Curtains.onRootViewsChangedListeners -= onRootViewsChangedListener decorViews.entries.forEach { @@ -790,17 +775,14 @@ public class PostHogReplayIntegration( 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)) { @@ -820,7 +802,6 @@ public class PostHogReplayIntegration( } style.verticalAlign = verticalAlignment } - else -> { style.verticalAlign = "center" style.horizontalAlign = "left" 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 5111c2f9..8d9beb9b 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 @@ -4,55 +4,55 @@ import com.posthog.PostHogExperimental @PostHogExperimental public class PostHogSessionReplayConfig -@JvmOverloads -constructor( - /** - * Enable masking of all text input fields - * Defaults to true - * This isn't supported if using Jetpack Compose views, use with caution - */ - @PostHogExperimental - public var maskAllTextInputs: Boolean = true, - /** - * Enable masking of all images to a placeholder - * Defaults to true - * This isn't supported if using Jetpack Compose views, use with caution - */ - @PostHogExperimental - public var maskAllImages: Boolean = true, - /** - * Enable capturing of logcat as console events - * Defaults to true - */ - @PostHogExperimental - 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 - */ - @PostHogExperimental - 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. - */ - @PostHogExperimental - public var screenshot: Boolean = false, - /** - * Deboucer 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 more snapshots will be captured but higher the performance impact - * Defaults to 500ms - */ - @PostHogExperimental - public var debouncerDelayMs: Long = 500, - /** - * Define the minimum duration for sessions to be recorded. - * This is useful if you want to exclude sessions that are too short to be useful. - * Defaults to 1000ms - */ - @PostHogExperimental - public var minSessionDurationMs: Long = 1000, -) + @JvmOverloads + constructor( + /** + * Enable masking of all text input fields + * Defaults to true + * This isn't supported if using Jetpack Compose views, use with caution + */ + @PostHogExperimental + public var maskAllTextInputs: Boolean = true, + /** + * Enable masking of all images to a placeholder + * Defaults to true + * This isn't supported if using Jetpack Compose views, use with caution + */ + @PostHogExperimental + public var maskAllImages: Boolean = true, + /** + * Enable capturing of logcat as console events + * Defaults to true + */ + @PostHogExperimental + 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 + */ + @PostHogExperimental + 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. + */ + @PostHogExperimental + public var screenshot: Boolean = false, + /** + * Deboucer 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 more snapshots will be captured but higher the performance impact + * Defaults to 500ms + */ + @PostHogExperimental + public var debouncerDelayMs: Long = 500, + /** + * Define the minimum duration for sessions to be recorded. + * This is useful if you want to exclude sessions that are too short to be useful. + * Defaults to 1000ms + */ + @PostHogExperimental + public var minSessionDurationMs: Long = 1000, + ) From a1b0479bacbf88c39a47b101ae90f546eb73c220 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Mon, 4 Nov 2024 11:58:37 +0530 Subject: [PATCH 08/13] PR Feedback: Saving server config in config object, minSessionDurationMs defaulted to 0. --- USAGE.md | 2 +- .../android/replay/PostHogReplayIntegration.kt | 15 +-------------- .../android/replay/PostHogSessionReplayConfig.kt | 4 ++-- .../src/main/java/com/posthog/PostHogConfig.kt | 3 +++ .../com/posthog/internal/PostHogFeatureFlags.kt | 6 ++++++ .../com/posthog/internal/PostHogPreferences.kt | 2 +- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/USAGE.md b/USAGE.md index e42e9b45..6dca70fc 100644 --- a/USAGE.md +++ b/USAGE.md @@ -218,7 +218,7 @@ val config = PostHogAndroidConfig(apiKey).apply { sessionReplayConfig.screenshot = false // debouncerDelayMs is 500ms by default sessionReplayConfig.debouncerDelayMs = 1000 - // minSessionDurationMs is 1000ms by default + // minSessionDurationMs is 0ms by default sessionReplayConfig.minSessionDurationMs = 2000 } ``` 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 c939d49e..efa0da03 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 @@ -123,11 +123,6 @@ public class PostHogReplayIntegration( private val events = mutableListOf() private var minSessionThresholdCrossed = false - @Suppress("UNCHECKED_CAST") - private val replayPreferenceMap by lazy { - config.cachePreferences?.getValue(PostHogPreferences.SESSION_REPLAY) as? Map - } - private fun addView( view: View, added: Boolean = true, @@ -476,16 +471,8 @@ public class PostHogReplayIntegration( private fun sessionLongerThanMinDuration(): Boolean { //Check value only if threshold not crossed. if (!minSessionThresholdCrossed) { - val serverMinDuration = replayPreferenceMap?.let { map -> - (map["minimumDurationMilliseconds"] as Number).toLong() - } ?: 0L - //Give server min duration is set, give it a higher priority than locally passed config - val finalMinimumDuration = if (serverMinDuration > 0) { - serverMinDuration - } else { - config.sessionReplayConfig.minSessionDurationMs - } + val finalMinimumDuration = config.minReplaySessionDurationMs ?: config.sessionReplayConfig.minSessionDurationMs minSessionThresholdCrossed = config.dateProvider.currentTimeMillis() - sessionStartTime >= finalMinimumDuration 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 8d9beb9b..1230998d 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 @@ -51,8 +51,8 @@ public class PostHogSessionReplayConfig /** * Define the minimum duration for sessions to be recorded. * This is useful if you want to exclude sessions that are too short to be useful. - * Defaults to 1000ms + * Defaults to 0ms */ @PostHogExperimental - public var minSessionDurationMs: Long = 1000, + public var minSessionDurationMs: Long = 0, ) diff --git a/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog/src/main/java/com/posthog/PostHogConfig.kt index e498b309..05dc24a9 100644 --- a/posthog/src/main/java/com/posthog/PostHogConfig.kt +++ b/posthog/src/main/java/com/posthog/PostHogConfig.kt @@ -150,6 +150,9 @@ public open class PostHogConfig( @PostHogInternal public var snapshotEndpoint: String = "/s/" + @PostHogInternal + public var minReplaySessionDurationMs: Long? = null + @PostHogInternal public var dateProvider: PostHogDateProvider = PostHogDeviceDateProvider() diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt index b7022fdf..c89ea7d2 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt @@ -127,6 +127,9 @@ internal class PostHogFeatureFlags( config.snapshotEndpoint = it["endpoint"] as? String ?: config.snapshotEndpoint + config.minReplaySessionDurationMs = it["minimumDurationMilliseconds"] as? Long + ?: config.minReplaySessionDurationMs + sessionReplayFlagActive = isRecordingActive(this.featureFlags ?: mapOf(), it) config.cachePreferences?.setValue(SESSION_REPLAY, it) @@ -178,6 +181,9 @@ internal class PostHogFeatureFlags( config.snapshotEndpoint = sessionRecording["endpoint"] as? String ?: config.snapshotEndpoint + + config.minReplaySessionDurationMs = sessionRecording["minimumDurationMilliseconds"] as? Long + ?: config.minReplaySessionDurationMs } } } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogPreferences.kt b/posthog/src/main/java/com/posthog/internal/PostHogPreferences.kt index c408ca35..27fa6d9c 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogPreferences.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogPreferences.kt @@ -33,7 +33,7 @@ public interface PostHogPreferences { internal const val OPT_OUT = "opt-out" internal const val FEATURE_FLAGS = "featureFlags" internal const val FEATURE_FLAGS_PAYLOAD = "featureFlagsPayload" - public const val SESSION_REPLAY: String = "sessionReplay" + internal const val SESSION_REPLAY = "sessionReplay" public const val VERSION: String = "version" public const val BUILD: String = "build" public const val STRINGIFIED_KEYS: String = "stringifiedKeys" From f4d0d2f020cfe53ea3de4f3dda3e885450ab8dbf Mon Sep 17 00:00:00 2001 From: karntrehan Date: Mon, 4 Nov 2024 12:11:05 +0530 Subject: [PATCH 09/13] Formatting fixes --- .../main/java/com/posthog/internal/PostHogFeatureFlags.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt index c89ea7d2..b2a3a01d 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogFeatureFlags.kt @@ -127,8 +127,7 @@ internal class PostHogFeatureFlags( config.snapshotEndpoint = it["endpoint"] as? String ?: config.snapshotEndpoint - config.minReplaySessionDurationMs = it["minimumDurationMilliseconds"] as? Long - ?: config.minReplaySessionDurationMs + config.minReplaySessionDurationMs = (it["minimumDurationMilliseconds"] as? Number)?.toLong() ?: config.minReplaySessionDurationMs sessionReplayFlagActive = isRecordingActive(this.featureFlags ?: mapOf(), it) config.cachePreferences?.setValue(SESSION_REPLAY, it) @@ -182,8 +181,7 @@ internal class PostHogFeatureFlags( config.snapshotEndpoint = sessionRecording["endpoint"] as? String ?: config.snapshotEndpoint - config.minReplaySessionDurationMs = sessionRecording["minimumDurationMilliseconds"] as? Long - ?: config.minReplaySessionDurationMs + config.minReplaySessionDurationMs = (sessionRecording["minimumDurationMilliseconds"] as? Number)?.toLong() ?: config.minReplaySessionDurationMs } } } From 92a4d5bb3d00fc7c5bfdea680bfa681a4c915483 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Mon, 4 Nov 2024 12:29:15 +0530 Subject: [PATCH 10/13] Fix initialization of sessionStartTime --- .../android/replay/PostHogReplayIntegration.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 efa0da03..8eae4e77 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 @@ -119,8 +119,9 @@ public class PostHogReplayIntegration( private val isSessionReplayEnabled: Boolean get() = PostHog.isSessionReplayActive() - private var sessionStartTime = 0L + private val sessionStartTime by lazy { config.dateProvider.currentTimeMillis() } private val events = mutableListOf() + private val mouseInteractions = mutableListOf() private var minSessionThresholdCrossed = false private fun addView( @@ -256,7 +257,6 @@ public class PostHogReplayIntegration( motionEvent: MotionEvent, type: RRMouseInteraction, ) { - val mouseInteractions = mutableListOf() for (index in 0 until motionEvent.pointerCount) { // if the id is 0, BE transformer will set it to the virtual bodyId val id = motionEvent.getPointerId(index) @@ -274,12 +274,14 @@ public class PostHogReplayIntegration( mouseInteractions.add(mouseInteraction) } - if (mouseInteractions.isNotEmpty()) { + if (mouseInteractions.isNotEmpty() && sessionLongerThanMinDuration()) { // TODO: we can probably batch those // if we batch them, we need to be aware that the order of the events matters // also because if we send a mouse interaction later, it might be attached to the wrong // screen + config.logger.log("Session replay mouse events captured: " + mouseInteractions.size) mouseInteractions.capture() + mouseInteractions.clear() } } @@ -318,8 +320,6 @@ public class PostHogReplayIntegration( // workaround for react native that is started after the window is added // Curtains.rootViews should be empty for normal apps yet - sessionStartTime = config.dateProvider.currentTimeMillis() - Curtains.rootViews.forEach { view -> addView(view) } @@ -474,6 +474,8 @@ public class PostHogReplayIntegration( //Give server min duration is set, give it a higher priority than locally passed config val finalMinimumDuration = config.minReplaySessionDurationMs ?: config.sessionReplayConfig.minSessionDurationMs + config.logger.log("Minimum replay session duration set to: $finalMinimumDuration") + minSessionThresholdCrossed = config.dateProvider.currentTimeMillis() - sessionStartTime >= finalMinimumDuration } From ac53a8a96e347a673a3bfde01baa391d9386827a Mon Sep 17 00:00:00 2001 From: karntrehan Date: Thu, 14 Nov 2024 12:11:40 +0530 Subject: [PATCH 11/13] Sync events if threshold is crossed but no new events have come in. --- .../replay/PostHogReplayIntegration.kt | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) 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 8eae4e77..e99f5072 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 @@ -58,7 +58,6 @@ import com.posthog.android.internal.screenSize import com.posthog.android.replay.internal.NextDrawListener.Companion.onNextDraw import com.posthog.android.replay.internal.ViewTreeSnapshotStatus import com.posthog.android.replay.internal.isAliveAndAttachedToWindow -import com.posthog.internal.PostHogPreferences import com.posthog.internal.PostHogThreadFactory import com.posthog.internal.replay.RRCustomEvent import com.posthog.internal.replay.RREvent @@ -154,7 +153,11 @@ public class PostHogReplayIntegration( executor.submit { try { - generateSnapshot(WeakReference(decorView), WeakReference(window), timestamp) + generateSnapshot( + WeakReference(decorView), + WeakReference(window), + timestamp + ) } catch (e: Throwable) { config.logger.log("Session Replay generateSnapshot failed: $e.") } @@ -235,10 +238,19 @@ public class PostHogReplayIntegration( } when (motionEvent.action.and(MotionEvent.ACTION_MASK)) { MotionEvent.ACTION_DOWN -> { - generateMouseInteractions(timestamp, motionEvent, RRMouseInteraction.TouchStart) + generateMouseInteractions( + timestamp, + motionEvent, + RRMouseInteraction.TouchStart + ) } + MotionEvent.ACTION_UP -> { - generateMouseInteractions(timestamp, motionEvent, RRMouseInteraction.TouchEnd) + generateMouseInteractions( + timestamp, + motionEvent, + RRMouseInteraction.TouchEnd + ) } } } catch (e: Throwable) { @@ -270,10 +282,15 @@ public class PostHogReplayIntegration( x = absX, y = absY, ) - val mouseInteraction = RRIncrementalMouseInteractionEvent(mouseInteractionData, timestamp) + val mouseInteraction = + RRIncrementalMouseInteractionEvent(mouseInteractionData, timestamp) mouseInteractions.add(mouseInteraction) } + tryFlushMouseInteractions() + } + + private fun tryFlushMouseInteractions() { if (mouseInteractions.isNotEmpty() && sessionLongerThanMinDuration()) { // TODO: we can probably batch those // if we batch them, we need to be aware that the order of the events matters @@ -332,6 +349,8 @@ public class PostHogReplayIntegration( } override fun uninstall() { + tryFlushEvents() + tryFlushMouseInteractions() try { Curtains.onRootViewsChangedListeners -= onRootViewsChangedListener @@ -459,20 +478,25 @@ public class PostHogReplayIntegration( events.add(it) } + tryFlushEvents() + + status.lastSnapshot = wireframe + } + + private fun tryFlushEvents() { if (events.isNotEmpty() && sessionLongerThanMinDuration()) { config.logger.log("Session replay events captured: " + events.size) events.capture() events.clear() } - - status.lastSnapshot = wireframe } private fun sessionLongerThanMinDuration(): Boolean { //Check value only if threshold not crossed. if (!minSessionThresholdCrossed) { //Give server min duration is set, give it a higher priority than locally passed config - val finalMinimumDuration = config.minReplaySessionDurationMs ?: config.sessionReplayConfig.minSessionDurationMs + val finalMinimumDuration = + config.minReplaySessionDurationMs ?: config.sessionReplayConfig.minSessionDurationMs config.logger.log("Minimum replay session duration set to: $finalMinimumDuration") @@ -764,14 +788,17 @@ public class PostHogReplayIntegration( 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)) { @@ -791,6 +818,7 @@ public class PostHogReplayIntegration( } style.verticalAlign = verticalAlignment } + else -> { style.verticalAlign = "center" style.horizontalAlign = "left" From f7f6ff464eb431db3d626fca84c63a4fcb943281 Mon Sep 17 00:00:00 2001 From: karntrehan Date: Thu, 14 Nov 2024 12:15:02 +0530 Subject: [PATCH 12/13] PR Feedback: minSessionDurationMs default to be null --- .../com/posthog/android/replay/PostHogReplayIntegration.kt | 2 +- .../com/posthog/android/replay/PostHogSessionReplayConfig.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 e99f5072..4948c2fe 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 @@ -496,7 +496,7 @@ public class PostHogReplayIntegration( if (!minSessionThresholdCrossed) { //Give server min duration is set, give it a higher priority than locally passed config val finalMinimumDuration = - config.minReplaySessionDurationMs ?: config.sessionReplayConfig.minSessionDurationMs + (config.minReplaySessionDurationMs ?: config.sessionReplayConfig.minSessionDurationMs) ?: 0 config.logger.log("Minimum replay session duration set to: $finalMinimumDuration") 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 1230998d..52b17a06 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 @@ -51,8 +51,8 @@ public class PostHogSessionReplayConfig /** * Define the minimum duration for sessions to be recorded. * This is useful if you want to exclude sessions that are too short to be useful. - * Defaults to 0ms + * Defaults to null */ @PostHogExperimental - public var minSessionDurationMs: Long = 0, + public var minSessionDurationMs: Long? = null, ) From b6fbed1d55d146dcc515d2dabea42c51279b27dc Mon Sep 17 00:00:00 2001 From: karntrehan Date: Thu, 14 Nov 2024 15:50:31 +0530 Subject: [PATCH 13/13] Move to multiple session ID handling --- .../replay/PostHogReplayIntegration.kt | 118 ++++++++++++------ .../posthog/internal/PostHogSessionManager.kt | 12 ++ 2 files changed, 90 insertions(+), 40 deletions(-) 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 4948c2fe..7c6bb045 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 @@ -58,6 +58,7 @@ import com.posthog.android.internal.screenSize import com.posthog.android.replay.internal.NextDrawListener.Companion.onNextDraw import com.posthog.android.replay.internal.ViewTreeSnapshotStatus import com.posthog.android.replay.internal.isAliveAndAttachedToWindow +import com.posthog.internal.PostHogSessionManager import com.posthog.internal.PostHogThreadFactory import com.posthog.internal.replay.RRCustomEvent import com.posthog.internal.replay.RREvent @@ -82,6 +83,7 @@ import curtains.touchEventInterceptors import curtains.windowAttachCount import java.io.ByteArrayOutputStream import java.lang.ref.WeakReference +import java.util.UUID import java.util.WeakHashMap import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors @@ -118,10 +120,14 @@ public class PostHogReplayIntegration( private val isSessionReplayEnabled: Boolean get() = PostHog.isSessionReplayActive() - private val sessionStartTime by lazy { config.dateProvider.currentTimeMillis() } - private val events = mutableListOf() - private val mouseInteractions = mutableListOf() - private var minSessionThresholdCrossed = false + private val sessionStartTimes = mutableMapOf() + private val events = mutableMapOf>() + private val mouseInteractions = + mutableMapOf>() + private val minSessionDuration by lazy { + config.minReplaySessionDurationMs + ?: config.sessionReplayConfig.minSessionDurationMs ?: 0 + } private fun addView( view: View, @@ -269,6 +275,7 @@ public class PostHogReplayIntegration( motionEvent: MotionEvent, type: RRMouseInteraction, ) { + val currentSessionId = PostHogSessionManager.getActiveSessionId() for (index in 0 until motionEvent.pointerCount) { // if the id is 0, BE transformer will set it to the virtual bodyId val id = motionEvent.getPointerId(index) @@ -284,21 +291,43 @@ public class PostHogReplayIntegration( ) val mouseInteraction = RRIncrementalMouseInteractionEvent(mouseInteractionData, timestamp) - mouseInteractions.add(mouseInteraction) + handleSessionStarts(currentSessionId) + insertMouseInteractions(currentSessionId, mouseInteraction) } tryFlushMouseInteractions() } + private fun handleSessionStarts(currentSessionId: UUID?) { + config.logger.log("sessionstarts: $currentSessionId") + if (sessionStartTimes.containsKey(currentSessionId)) return + + config.logger.log("Session added: $currentSessionId") + sessionStartTimes[currentSessionId] = + PostHogSessionManager.getActiveSessionStartTime() + } + + private fun insertMouseInteractions( + currentSessionId: UUID?, + mouseInteraction: RRIncrementalMouseInteractionEvent + ) { + config.logger.log("session: insertMouseInteraction") + val currentSessionMouseInteractions = + mouseInteractions[currentSessionId] ?: mutableListOf() + currentSessionMouseInteractions.add(mouseInteraction) + mouseInteractions[currentSessionId] = currentSessionMouseInteractions + } + private fun tryFlushMouseInteractions() { - if (mouseInteractions.isNotEmpty() && sessionLongerThanMinDuration()) { - // TODO: we can probably batch those - // if we batch them, we need to be aware that the order of the events matters - // also because if we send a mouse interaction later, it might be attached to the wrong - // screen - config.logger.log("Session replay mouse events captured: " + mouseInteractions.size) - mouseInteractions.capture() - mouseInteractions.clear() + mouseInteractions.forEach { + config.logger.log("session: tryFlushMouseInteractions:" + it.key + ": " + it.value) + val sessionId = it.key + val sessionIdStartTime = sessionStartTimes[sessionId] ?: 0 + if (System.currentTimeMillis() - sessionIdStartTime >= minSessionDuration) { + config.logger.log("Session replay mouse events captured: " + it.value.size) + it.value.capture() + mouseInteractions.remove(sessionId) + } } } @@ -306,6 +335,7 @@ public class PostHogReplayIntegration( view: View, status: ViewTreeSnapshotStatus, ) { + config.logger.log("cleanSessionState") if (view.isAliveAndAttachedToWindow()) { mainHandler.handler.post { // 2nd check to avoid: @@ -327,6 +357,9 @@ public class PostHogReplayIntegration( } decorViews.remove(view) + + tryFlushEvents() + tryFlushMouseInteractions() } override fun install() { @@ -349,8 +382,6 @@ public class PostHogReplayIntegration( } override fun uninstall() { - tryFlushEvents() - tryFlushMouseInteractions() try { Curtains.onRootViewsChangedListeners -= onRootViewsChangedListener @@ -400,8 +431,12 @@ public class PostHogReplayIntegration( } } + val currentSessionId = PostHogSessionManager.getActiveSessionId() + handleSessionStarts(currentSessionId) + if (!status.sentMetaEvent) { - val title = view.phoneWindow?.attributes?.title?.toString()?.substringAfter("/") ?: "" + val title = + view.phoneWindow?.attributes?.title?.toString()?.substringAfter("/") ?: "" // TODO: cache and compare, if size changes, we send a ViewportResize event val screenSizeInfo = view.context.screenSize() ?: return @@ -413,7 +448,7 @@ public class PostHogReplayIntegration( height = screenSizeInfo.height, timestamp = timestamp, ) - events.add(metaEvent) + insertEvent(currentSessionId, metaEvent) status.sentMetaEvent = true } @@ -425,7 +460,7 @@ public class PostHogReplayIntegration( initialOffsetLeft = 0, timestamp = timestamp, ) - events.add(event) + insertEvent(currentSessionId, event) status.sentFullSnapshot = true } else { val lastSnapshot = status.lastSnapshot @@ -467,7 +502,7 @@ public class PostHogReplayIntegration( mutationData = incrementalMutationData, timestamp = timestamp, ) - events.add(incrementalSnapshotEvent) + insertEvent(currentSessionId, incrementalSnapshotEvent) } } @@ -475,7 +510,7 @@ public class PostHogReplayIntegration( val (visible, event) = detectKeyboardVisibility(view, status.keyboardVisible) status.keyboardVisible = visible event?.let { - events.add(it) + insertEvent(currentSessionId, it) } tryFlushEvents() @@ -483,27 +518,28 @@ public class PostHogReplayIntegration( status.lastSnapshot = wireframe } - private fun tryFlushEvents() { - if (events.isNotEmpty() && sessionLongerThanMinDuration()) { - config.logger.log("Session replay events captured: " + events.size) - events.capture() - events.clear() - } + private fun insertEvent( + currentSessionId: UUID?, + event: RREvent + ) { + config.logger.log("session: insertEvent: $currentSessionId") + val currentSessionEvents = + events[currentSessionId] ?: mutableListOf() + currentSessionEvents.add(event) + events[currentSessionId] = currentSessionEvents } - private fun sessionLongerThanMinDuration(): Boolean { - //Check value only if threshold not crossed. - if (!minSessionThresholdCrossed) { - //Give server min duration is set, give it a higher priority than locally passed config - val finalMinimumDuration = - (config.minReplaySessionDurationMs ?: config.sessionReplayConfig.minSessionDurationMs) ?: 0 - - config.logger.log("Minimum replay session duration set to: $finalMinimumDuration") - - minSessionThresholdCrossed = - config.dateProvider.currentTimeMillis() - sessionStartTime >= finalMinimumDuration + private fun tryFlushEvents() { + events.forEach { + config.logger.log("session: tryFlushEvents:" + it.key + ": " + it.value) + val sessionId = it.key + val sessionIdStartTime = sessionStartTimes[sessionId] ?: 0 + if (System.currentTimeMillis() - sessionIdStartTime >= minSessionDuration) { + config.logger.log("Session replay mouse events captured: " + it.value.size) + it.value.capture() + events.remove(sessionId) + } } - return minSessionThresholdCrossed } private fun View.isVisible(): Boolean { @@ -842,7 +878,8 @@ public class PostHogReplayIntegration( // Do not set padding if the text is centered, otherwise the padding will be off if (style.verticalAlign != "center") { style.paddingTop = view.totalPaddingTop.densityValue(displayMetrics.density) - style.paddingBottom = view.totalPaddingBottom.densityValue(displayMetrics.density) + style.paddingBottom = + view.totalPaddingBottom.densityValue(displayMetrics.density) } if (style.horizontalAlign != "center") { style.paddingLeft = view.totalPaddingLeft.densityValue(displayMetrics.density) @@ -1206,7 +1243,8 @@ public class PostHogReplayIntegration( } private fun View.isNoCapture(maskInput: Boolean = false): Boolean { - return maskInput || (tag as? String)?.lowercase()?.contains(PH_NO_CAPTURE_LABEL) == true || + return maskInput || (tag as? String)?.lowercase() + ?.contains(PH_NO_CAPTURE_LABEL) == true || contentDescription?.toString()?.lowercase()?.contains(PH_NO_CAPTURE_LABEL) == true } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogSessionManager.kt b/posthog/src/main/java/com/posthog/internal/PostHogSessionManager.kt index 0f7b08eb..59080575 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogSessionManager.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogSessionManager.kt @@ -15,11 +15,13 @@ public object PostHogSessionManager { private val sessionIdNone = UUID(0, 0) private var sessionId = sessionIdNone + private var sessionStartTime: Long = 0 public fun startSession() { synchronized(sessionLock) { if (sessionId == sessionIdNone) { sessionId = TimeBasedEpochGenerator.generate() + sessionStartTime = System.currentTimeMillis() } } } @@ -27,6 +29,7 @@ public object PostHogSessionManager { public fun endSession() { synchronized(sessionLock) { sessionId = sessionIdNone + sessionStartTime = 0 } } @@ -38,9 +41,18 @@ public object PostHogSessionManager { return tempSessionId } + public fun getActiveSessionStartTime(): Long? { + var tempSessionStartTime: Long? + synchronized(sessionLock) { + tempSessionStartTime = if (sessionStartTime != 0L) sessionStartTime else null + } + return tempSessionStartTime + } + public fun setSessionId(sessionId: UUID) { synchronized(sessionLock) { this.sessionId = sessionId + this.sessionStartTime = System.currentTimeMillis() } }