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 6 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 1000ms 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,15 @@ public class PostHogReplayIntegration(
private val isSessionReplayEnabled: Boolean
get() = PostHog.isSessionReplayActive()

private var sessionStartTime = 0L
private val events = mutableListOf<RREvent>()
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!


@Suppress("UNCHECKED_CAST")
private val replayPreferenceMap by lazy {
config.cachePreferences?.getValue(PostHogPreferences.SESSION_REPLAY) as? Map<String, Any>
}

private fun addView(
view: View,
added: Boolean = true,
Expand Down Expand Up @@ -148,7 +158,11 @@ public class PostHogReplayIntegration(

executor.submit {
try {
generateSnapshot(WeakReference(decorView), WeakReference(window), timestamp)
generateSnapshot(
karntrehan marked this conversation as resolved.
Show resolved Hide resolved
WeakReference(decorView),
WeakReference(window),
timestamp
)
} catch (e: Throwable) {
config.logger.log("Session Replay generateSnapshot failed: $e.")
}
Expand Down Expand Up @@ -229,10 +243,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) {
Expand Down Expand Up @@ -265,7 +288,8 @@ public class PostHogReplayIntegration(
x = absX,
y = absY,
)
val mouseInteraction = RRIncrementalMouseInteractionEvent(mouseInteractionData, timestamp)
val mouseInteraction =
RRIncrementalMouseInteractionEvent(mouseInteractionData, timestamp)
mouseInteractions.add(mouseInteraction)
}

Expand Down Expand Up @@ -312,6 +336,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()
karntrehan marked this conversation as resolved.
Show resolved Hide resolved

Curtains.rootViews.forEach { view ->
addView(view)
}
Expand All @@ -325,6 +352,7 @@ public class PostHogReplayIntegration(

override fun uninstall() {
try {

Curtains.onRootViewsChangedListeners -= onRootViewsChangedListener

decorViews.entries.forEach {
Expand Down Expand Up @@ -373,8 +401,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 +479,35 @@ 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) {
val serverMinDuration = replayPreferenceMap?.let { map ->
(map["minimumDurationMilliseconds"] as Number).toLong()
} ?: 0L
karntrehan marked this conversation as resolved.
Show resolved Hide resolved

//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
}

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 Expand Up @@ -742,14 +790,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)) {
Expand All @@ -769,6 +820,7 @@ public class PostHogReplayIntegration(
}
style.verticalAlign = verticalAlignment
}

else -> {
style.verticalAlign = "center"
style.horizontalAlign = "left"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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
Original file line number Diff line number Diff line change
Expand Up @@ -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"
karntrehan marked this conversation as resolved.
Show resolved Hide resolved
public const val VERSION: String = "version"
public const val BUILD: String = "build"
public const val STRINGIFIED_KEYS: String = "stringifiedKeys"
Expand Down