Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/189/session replay min duration #199

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ val config = PostHogAndroidConfig(apiKey).apply {
sessionReplayConfig.screenshot = false
// debouncerDelayMs is 500ms by default
sessionReplayConfig.debouncerDelayMs = 1000
// minSessionDurationMs is 0ms by default
sessionReplayConfig.minSessionDurationMs = 2000
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -118,6 +119,11 @@ public class PostHogReplayIntegration(
private val isSessionReplayEnabled: Boolean
get() = PostHog.isSessionReplayActive()

private val sessionStartTime by lazy { config.dateProvider.currentTimeMillis() }
private val events = mutableListOf<RREvent>()
private val mouseInteractions = mutableListOf<RRIncrementalMouseInteractionEvent>()
private var minSessionThresholdCrossed = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This flag has to be cached per session id, so ideally this is part of ViewTreeSnapshotStatus I think, or it has to be reset when the session_id changes as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not very clear to me. What we are doing here is setting the threshold once for 1 launch session of the user.

The behavior we have currently is: User launches the app, we start caching events & wait for the minimum threshold, if the user closes the app before the threshold the events are not captured / dropped. Once the threshold is crossed, all cached events are captured immediately and subsequent events are captured right away. Is this the expected behavior?

I am not sure of ViewTreeSnapshotStatus and where the session_id is set / unset. Could you help me with more info on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So all this information should be related to a specific sessionId.
Right now the sessionStartTime is gonna be used even if the session rotates, etc.
So we need a way of Map<SessionId, Object> where Object contains the properties (sessionStartTime, events, mouseInteractions, etc...)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example is that if the app is in the background for more than 30 minutes, the sessionId rotates, and the new session will start once the app is in the background, but we are still using the sessionStartTime from the last session.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Looking at PostHogSessionManager right now. Thanks!


private fun addView(
view: View,
added: Boolean = true,
Expand Down Expand Up @@ -251,7 +257,6 @@ public class PostHogReplayIntegration(
motionEvent: MotionEvent,
type: RRMouseInteraction,
) {
val mouseInteractions = mutableListOf<RRIncrementalMouseInteractionEvent>()
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)
Expand All @@ -269,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()
karntrehan marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -312,6 +319,7 @@ public class PostHogReplayIntegration(

// workaround for react native that is started after the window is added
// Curtains.rootViews should be empty for normal apps yet

Curtains.rootViews.forEach { view ->
addView(view)
}
Expand Down Expand Up @@ -373,8 +381,6 @@ public class PostHogReplayIntegration(
}
}

val events = mutableListOf<RREvent>()

if (!status.sentMetaEvent) {
val title = view.phoneWindow?.attributes?.title?.toString()?.substringAfter("/") ?: ""
// TODO: cache and compare, if size changes, we send a ViewportResize event
Expand Down Expand Up @@ -453,13 +459,29 @@ public class PostHogReplayIntegration(
events.add(it)
}

if (events.isNotEmpty()) {
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

config.logger.log("Minimum replay session duration set to: $finalMinimumDuration")

minSessionThresholdCrossed =
config.dateProvider.currentTimeMillis() - sessionStartTime >= finalMinimumDuration
}
return minSessionThresholdCrossed
}

private fun View.isVisible(): Boolean {
// TODO: also check for getGlobalVisibleRect intersects the display
val visible = isShown && width >= 0 && height >= 0 && this !is ViewStub
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,11 @@ public class PostHogSessionReplayConfig
*/
@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 0ms
*/
@PostHogExperimental
public var minSessionDurationMs: Long = 0,
karntrehan marked this conversation as resolved.
Show resolved Hide resolved
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class MyApp : Application() {
sessionReplayConfig.maskAllImages = false
sessionReplayConfig.captureLogcat = true
sessionReplayConfig.screenshot = true
sessionReplayConfig.minSessionDurationMs = 5000
}
PostHogAndroid.setup(this, config)
}
Expand Down
3 changes: 2 additions & 1 deletion posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +856 to +857
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rollback

}

override fun isSessionReplayActive(): Boolean {
Expand Down
3 changes: 3 additions & 0 deletions posthog/src/main/java/com/posthog/PostHogConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ internal class PostHogFeatureFlags(
config.snapshotEndpoint = it["endpoint"] as? String
?: config.snapshotEndpoint

config.minReplaySessionDurationMs = (it["minimumDurationMilliseconds"] as? Number)?.toLong() ?: config.minReplaySessionDurationMs

sessionReplayFlagActive = isRecordingActive(this.featureFlags ?: mapOf(), it)
config.cachePreferences?.setValue(SESSION_REPLAY, it)

Expand Down Expand Up @@ -178,6 +180,8 @@ internal class PostHogFeatureFlags(

config.snapshotEndpoint = sessionRecording["endpoint"] as? String
?: config.snapshotEndpoint

config.minReplaySessionDurationMs = (sessionRecording["minimumDurationMilliseconds"] as? Number)?.toLong() ?: config.minReplaySessionDurationMs
}
}
}
Expand Down