Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/device-bucketing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog": minor
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@dmarticus you also need to release the android sdk here otherwise it wont have any effect

---

feat: add device bucketing support for stable feature flag assignment across identity changes
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ public class PostHogFake : PostHogInterface {
return ""
}

override fun getDeviceId(): String {
return ""
}

override fun debug(enable: Boolean) {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,14 @@ internal class PostHogFeatureFlags(
}

return try {
val response = api.flags(distinctId, null, groups, personProperties, groupProperties)
val response =
api.flags(
distinctId,
anonymousId = null,
groups = groups,
personProperties = personProperties,
groupProperties = groupProperties,
)
val flags = response?.flags
cache.put(
cacheKey,
Expand Down
9 changes: 7 additions & 2 deletions posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public final class com/posthog/PostHog : com/posthog/PostHogStateless, com/posth
public fun endSession ()V
public fun flush ()V
public fun getConfig ()Lcom/posthog/PostHogConfig;
public fun getDeviceId ()Ljava/lang/String;
public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Boolean;)Ljava/lang/Object;
public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
public fun getFeatureFlagResult (Ljava/lang/String;Ljava/lang/Boolean;)Lcom/posthog/FeatureFlagResult;
Expand Down Expand Up @@ -73,6 +74,7 @@ public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface
public fun endSession ()V
public fun flush ()V
public fun getConfig ()Lcom/posthog/PostHogConfig;
public fun getDeviceId ()Ljava/lang/String;
public fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Boolean;)Ljava/lang/Object;
public fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
public fun getFeatureFlagResult (Ljava/lang/String;Ljava/lang/Boolean;)Lcom/posthog/FeatureFlagResult;
Expand Down Expand Up @@ -311,6 +313,7 @@ public abstract interface class com/posthog/PostHogInterface : com/posthog/PostH
public abstract fun distinctId ()Ljava/lang/String;
public abstract fun endSession ()V
public abstract fun getConfig ()Lcom/posthog/PostHogConfig;
public abstract fun getDeviceId ()Ljava/lang/String;
public abstract fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Boolean;)Ljava/lang/Object;
public abstract fun getFeatureFlagPayload (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
public abstract fun getFeatureFlagResult (Ljava/lang/String;Ljava/lang/Boolean;)Lcom/posthog/FeatureFlagResult;
Expand Down Expand Up @@ -634,8 +637,8 @@ public final class com/posthog/internal/MultiVariateConfig {
public final class com/posthog/internal/PostHogApi {
public fun <init> (Lcom/posthog/PostHogConfig;)V
public final fun batch (Ljava/util/List;)V
public final fun flags (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lcom/posthog/internal/PostHogFlagsResponse;
public static synthetic fun flags$default (Lcom/posthog/internal/PostHogApi;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lcom/posthog/internal/PostHogFlagsResponse;
public final fun flags (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lcom/posthog/internal/PostHogFlagsResponse;
public static synthetic fun flags$default (Lcom/posthog/internal/PostHogApi;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lcom/posthog/internal/PostHogFlagsResponse;
public final fun localEvaluation (Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/internal/LocalEvaluationApiResponse;
public static synthetic fun localEvaluation$default (Lcom/posthog/internal/PostHogApi;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/posthog/internal/LocalEvaluationApiResponse;
public final fun remoteConfig ()Lcom/posthog/internal/PostHogRemoteConfigResponse;
Expand Down Expand Up @@ -781,6 +784,7 @@ public abstract interface class com/posthog/internal/PostHogPreferences {
public static final field ANONYMOUS_ID Ljava/lang/String;
public static final field BUILD Ljava/lang/String;
public static final field Companion Lcom/posthog/internal/PostHogPreferences$Companion;
public static final field DEVICE_ID Ljava/lang/String;
public static final field DISTINCT_ID Ljava/lang/String;
public static final field GROUPS Ljava/lang/String;
public static final field LAST_SEEN_SURVEY_DATE Ljava/lang/String;
Expand All @@ -797,6 +801,7 @@ public abstract interface class com/posthog/internal/PostHogPreferences {
public final class com/posthog/internal/PostHogPreferences$Companion {
public static final field ANONYMOUS_ID Ljava/lang/String;
public static final field BUILD Ljava/lang/String;
public static final field DEVICE_ID Ljava/lang/String;
public static final field DISTINCT_ID Ljava/lang/String;
public static final field GROUPS Ljava/lang/String;
public static final field LAST_SEEN_SURVEY_DATE Ljava/lang/String;
Expand Down
35 changes: 32 additions & 3 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.posthog.internal.PostHogOnRemoteConfigLoaded
import com.posthog.internal.PostHogPreferences.Companion.ALL_INTERNAL_KEYS
import com.posthog.internal.PostHogPreferences.Companion.ANONYMOUS_ID
import com.posthog.internal.PostHogPreferences.Companion.BUILD
import com.posthog.internal.PostHogPreferences.Companion.DEVICE_ID
import com.posthog.internal.PostHogPreferences.Companion.DISTINCT_ID
import com.posthog.internal.PostHogPreferences.Companion.GROUPS
import com.posthog.internal.PostHogPreferences.Companion.IS_IDENTIFIED
Expand Down Expand Up @@ -53,6 +54,7 @@ public class PostHog private constructor(
private val reloadFeatureFlags: Boolean = true,
) : PostHogInterface, PostHogStateless() {
private val anonymousLock = Any()
private val deviceIdLock = Any()
private val identifiedLock = Any()
private val groupsLock = Any()
private val personProcessingLock: Any = Any()
Expand Down Expand Up @@ -175,6 +177,11 @@ public class PostHog private constructor(

legacyPreferences(config, config.serializer)

// Initialize device_id if not already set. getDeviceId() handles lazy init
// by seeding from the anonymous ID, providing a stable identifier for
// device-level feature flag bucketing that survives identify() and reset().
getDeviceId()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@dmarticus does this have any effect since the sdk is still not enabled? This should be called after enabled=true


super.enabled = true

queue.start()
Expand Down Expand Up @@ -1224,9 +1231,10 @@ public class PostHog private constructor(
return
}

// only remove properties, preserve BUILD and VERSION keys in order to fix over-sending
// of 'Application Installed' events and under-sending of 'Application Updated' events
val except = mutableListOf(VERSION, BUILD)
// Preserve BUILD and VERSION to prevent over-sending "Application Installed" events
// and under-sending "Application Updated" events. Preserve DEVICE_ID to maintain
// stable feature flag bucketing across identity changes.
val except = mutableListOf(VERSION, BUILD, DEVICE_ID)
// preserve the ANONYMOUS_ID if reuseAnonymousId is enabled (for preserving a guest user
// account on the device)
if (config?.reuseAnonymousId == true) {
Expand Down Expand Up @@ -1283,6 +1291,25 @@ public class PostHog private constructor(
return distinctId
}

override fun getDeviceId(): String {
if (!isEnabled()) {
return ""
}
synchronized(deviceIdLock) {
val deviceId = getPreferences().getValue(DEVICE_ID) as? String
if (deviceId.isNullOrBlank()) {
// Lazy init for upgrades: existing installs won't have a device_id yet
val anonId = anonymousId
if (anonId.isNotBlank()) {
getPreferences().setValue(DEVICE_ID, anonId)
return anonId
}
return ""
}
return deviceId
}
}

override fun startSession() {
if (!isEnabled()) {
return
Expand Down Expand Up @@ -1627,6 +1654,8 @@ public class PostHog private constructor(

override fun distinctId(): String = shared.distinctId()

override fun getDeviceId(): String = shared.getDeviceId()

override fun debug(enable: Boolean) {
shared.debug(enable)
}
Expand Down
9 changes: 9 additions & 0 deletions posthog/src/main/java/com/posthog/PostHogInterface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ public interface PostHogInterface : PostHogCoreInterface {
*/
public fun distinctId(): String

/**
* Returns the stable device identifier used for device-level feature flag bucketing.
* This ID persists across [identify] and [reset] calls, only changing on a fresh
* app install, manual cache clearing, or OS-initiated storage cleanup.
*
* @return The device ID, or an empty string if not yet initialized
*/
public fun getDeviceId(): String

/**
* Starts a session
* The SDK will automatically start a session when you call [setup]
Expand Down
2 changes: 2 additions & 0 deletions posthog/src/main/java/com/posthog/internal/PostHogApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public class PostHogApi(
public fun flags(
distinctId: String,
anonymousId: String? = null,
deviceId: String? = null,
groups: Map<String, String>? = null,
personProperties: Map<String, Any?>? = null,
groupProperties: Map<String, Map<String, Any?>>? = null,
Expand All @@ -143,6 +144,7 @@ public class PostHogApi(
config.apiKey,
distinctId,
anonymousId = anonymousId,
deviceId = deviceId,
groups,
personProperties,
groupProperties,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal class PostHogFlagsRequest(
apiKey: String,
distinctId: String,
anonymousId: String? = null,
deviceId: String? = null,
groups: Map<String, String>? = null,
personProperties: Map<String, Any?>? = null,
groupProperties: Map<String, Map<String, Any?>>? = null,
Expand All @@ -21,6 +22,9 @@ internal class PostHogFlagsRequest(
if (!anonymousId.isNullOrBlank()) {
this["\$anon_distinct_id"] = anonymousId
}
if (!deviceId.isNullOrBlank()) {
this["\$device_id"] = deviceId
}
if (groups?.isNotEmpty() == true) {
this["groups"] = groups
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public interface PostHogPreferences {
public const val LAST_SEEN_SURVEY_DATE: String = "lastSeenSurveyDate"
public const val VERSION: String = "version"
public const val BUILD: String = "build"
public const val DEVICE_ID: String = "deviceId"
public const val STRINGIFIED_KEYS: String = "stringifiedKeys"

public val ALL_INTERNAL_KEYS: Set<String> =
Expand All @@ -64,6 +65,7 @@ public interface PostHogPreferences {
LAST_SEEN_SURVEY_DATE,
VERSION,
BUILD,
DEVICE_ID,
STRINGIFIED_KEYS,
FEATURE_FLAG_REQUEST_ID,
FEATURE_FLAG_EVALUATED_AT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.posthog.PostHogConfig
import com.posthog.PostHogInternal
import com.posthog.PostHogOnFeatureFlags
import com.posthog.internal.PostHogPreferences.Companion.CAPTURE_PERFORMANCE
import com.posthog.internal.PostHogPreferences.Companion.DEVICE_ID
import com.posthog.internal.PostHogPreferences.Companion.ERROR_TRACKING
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAGS
import com.posthog.internal.PostHogPreferences.Companion.FEATURE_FLAGS_PAYLOAD
Expand Down Expand Up @@ -529,10 +530,13 @@ public class PostHogRemoteConfig(
}

try {
val deviceId = config.cachePreferences?.getValue(DEVICE_ID) as? String

val response =
api.flags(
distinctId,
anonymousId = anonymousId,
deviceId = deviceId,
groups = groups,
personProperties = getPersonPropertiesForFlags(),
groupProperties = getGroupPropertiesForFlags(),
Expand Down
Loading
Loading