diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f87cfbe..87737be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- recording: expose session id ([#166](https://github.com/PostHog/posthog-android/pull/166)) + ## 3.5.1 - 2024-08-26 - recording: capture touch interaction off of main thread to avoid ANRs ([#165](https://github.com/PostHog/posthog-android/pull/165)) diff --git a/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt b/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt index d5c466c9..3e7a6d23 100644 --- a/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt +++ b/posthog-android/src/test/java/com/posthog/android/PostHogFake.kt @@ -3,6 +3,7 @@ package com.posthog.android import com.posthog.PostHogConfig import com.posthog.PostHogInterface import com.posthog.PostHogOnFeatureFlags +import java.util.UUID public class PostHogFake : PostHogInterface { public var event: String? = null @@ -119,6 +120,10 @@ public class PostHogFake : PostHogInterface { return false } + override fun getSessionId(): UUID? { + return null + } + override fun getConfig(): T? { return null } diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index 5e003138..e69973eb 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -11,6 +11,7 @@ public final class com/posthog/PostHog : com/posthog/PostHogInterface { public fun getConfig ()Lcom/posthog/PostHogConfig; public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; + public fun getSessionId ()Ljava/util/UUID; public fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V public fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V public fun isFeatureEnabled (Ljava/lang/String;Z)Z @@ -38,6 +39,7 @@ public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface public fun getConfig ()Lcom/posthog/PostHogConfig; public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; + public fun getSessionId ()Ljava/util/UUID; public fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V public fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V public fun isFeatureEnabled (Ljava/lang/String;Z)Z @@ -178,6 +180,7 @@ public abstract interface class com/posthog/PostHogInterface { public abstract fun getConfig ()Lcom/posthog/PostHogConfig; public abstract fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; public abstract fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; + public abstract fun getSessionId ()Ljava/util/UUID; public abstract fun group (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V public abstract fun identify (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V public abstract fun isFeatureEnabled (Ljava/lang/String;Z)Z @@ -302,6 +305,15 @@ public final class com/posthog/internal/PostHogSerializer { public final fun serializeObject (Ljava/lang/Object;)Ljava/lang/String; } +public final class com/posthog/internal/PostHogSessionManager { + public static final field INSTANCE Lcom/posthog/internal/PostHogSessionManager; + public final fun endSession ()V + public final fun getActiveSessionId ()Ljava/util/UUID; + public final fun isSessionActive ()Z + public final fun setSessionId (Ljava/util/UUID;)V + public final fun startSession ()V +} + public final class com/posthog/internal/PostHogThreadFactory : java/util/concurrent/ThreadFactory { public fun (Ljava/lang/String;)V public fun newThread (Ljava/lang/Runnable;)Ljava/lang/Thread; diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index 58eaf193..97479581 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -17,6 +17,7 @@ import com.posthog.internal.PostHogPrintLogger import com.posthog.internal.PostHogQueue import com.posthog.internal.PostHogSendCachedEventsIntegration import com.posthog.internal.PostHogSerializer +import com.posthog.internal.PostHogSessionManager import com.posthog.internal.PostHogThreadFactory import com.posthog.vendor.uuid.TimeBasedEpochGenerator import java.util.UUID @@ -48,13 +49,8 @@ public class PostHog private constructor( private val setupLock = Any() private val optOutLock = Any() private val anonymousLock = Any() - private val sessionLock = Any() private val groupsLock = Any() - // do not move to companion object, otherwise sessionId will be null - private val sessionIdNone = UUID(0, 0) - - private var sessionId = sessionIdNone private val featureFlagsCalledLock = Any() private var config: PostHogConfig? = null @@ -170,6 +166,10 @@ public class PostHog private constructor( public override fun close() { synchronized(setupLock) { try { + if (!isEnabled()) { + return + } + enabled = false config?.let { config -> @@ -272,15 +272,13 @@ public class PostHog private constructor( } } - synchronized(sessionLock) { - if (sessionId != sessionIdNone) { - val sessionId = sessionId.toString() - props["\$session_id"] = sessionId - if (config?.sessionReplay == true) { - // Session replay requires $window_id, so we set as the same as $session_id. - // the backend might fallback to $session_id if $window_id is not present next. - props["\$window_id"] = sessionId - } + PostHogSessionManager.getActiveSessionId()?.let { sessionId -> + val tempSessionId = sessionId.toString() + props["\$session_id"] = tempSessionId + if (config?.sessionReplay == true) { + // Session replay requires $window_id, so we set as the same as $session_id. + // the backend might fallback to $session_id if $window_id is not present next. + props["\$window_id"] = tempSessionId } } @@ -697,25 +695,35 @@ public class PostHog private constructor( } override fun startSession() { - synchronized(sessionLock) { - if (sessionId == sessionIdNone) { - sessionId = TimeBasedEpochGenerator.generate() - } + if (!isEnabled()) { + return } + + PostHogSessionManager.startSession() } override fun endSession() { - synchronized(sessionLock) { - sessionId = sessionIdNone + if (!isEnabled()) { + return } + + PostHogSessionManager.endSession() } override fun isSessionActive(): Boolean { - var active: Boolean - synchronized(sessionLock) { - active = sessionId != sessionIdNone + if (!isEnabled()) { + return false + } + + return PostHogSessionManager.isSessionActive() + } + + override fun getSessionId(): UUID? { + if (!isEnabled()) { + return null } - return active + + return PostHogSessionManager.getActiveSessionId() } override fun getConfig(): T? { @@ -896,6 +904,10 @@ public class PostHog private constructor( return shared.isSessionActive() } + override fun getSessionId(): UUID? { + return shared.getSessionId() + } + override fun getConfig(): T? { return shared.getConfig() } diff --git a/posthog/src/main/java/com/posthog/PostHogInterface.kt b/posthog/src/main/java/com/posthog/PostHogInterface.kt index a6a49363..8b5c394b 100644 --- a/posthog/src/main/java/com/posthog/PostHogInterface.kt +++ b/posthog/src/main/java/com/posthog/PostHogInterface.kt @@ -1,5 +1,7 @@ package com.posthog +import java.util.UUID + /** * The PostHog SDK entry point */ @@ -189,6 +191,11 @@ public interface PostHogInterface { */ public fun isSessionActive(): Boolean + /** + * Returns the session Id if a session is active + */ + public fun getSessionId(): UUID? + @PostHogInternal public fun getConfig(): T? } diff --git a/posthog/src/main/java/com/posthog/internal/PostHogSessionManager.kt b/posthog/src/main/java/com/posthog/internal/PostHogSessionManager.kt new file mode 100644 index 00000000..0f7b08eb --- /dev/null +++ b/posthog/src/main/java/com/posthog/internal/PostHogSessionManager.kt @@ -0,0 +1,54 @@ +package com.posthog.internal + +import com.posthog.PostHogInternal +import com.posthog.vendor.uuid.TimeBasedEpochGenerator +import java.util.UUID + +/** + * Class that manages the Session ID + */ +@PostHogInternal +public object PostHogSessionManager { + private val sessionLock = Any() + + // do not move to companion object, otherwise sessionId will be null + private val sessionIdNone = UUID(0, 0) + + private var sessionId = sessionIdNone + + public fun startSession() { + synchronized(sessionLock) { + if (sessionId == sessionIdNone) { + sessionId = TimeBasedEpochGenerator.generate() + } + } + } + + public fun endSession() { + synchronized(sessionLock) { + sessionId = sessionIdNone + } + } + + public fun getActiveSessionId(): UUID? { + var tempSessionId: UUID? + synchronized(sessionLock) { + tempSessionId = if (sessionId != sessionIdNone) sessionId else null + } + return tempSessionId + } + + public fun setSessionId(sessionId: UUID) { + synchronized(sessionLock) { + this.sessionId = sessionId + } + } + + public fun isSessionActive(): Boolean { + var active: Boolean + synchronized(sessionLock) { + active = sessionId != sessionIdNone + } + return active + } +}