-
Notifications
You must be signed in to change notification settings - Fork 31
feat(core): Add ability to cache properties for flags #315
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
Changes from 7 commits
c479b5a
50cfb4e
fbb5ef1
69f75bd
9c844e3
63564d4
53ad148
fc775ee
526d530
f917d11
9973f38
4e0afd9
6953f07
75bef00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import com.posthog.internal.PostHogSerializer | |
| import com.posthog.internal.PostHogSessionManager | ||
| import com.posthog.internal.PostHogThreadFactory | ||
| import com.posthog.internal.errortracking.ThrowableCoercer | ||
| import com.posthog.internal.personPropertiesContext | ||
| import com.posthog.internal.replay.PostHogSessionReplayHandler | ||
| import com.posthog.internal.surveys.PostHogSurveysHandler | ||
| import com.posthog.vendor.uuid.TimeBasedEpochGenerator | ||
|
|
@@ -96,7 +97,10 @@ public class PostHog private constructor( | |
| val api = PostHogApi(config) | ||
| val queue = config.queueProvider(config, api, PostHogApiEndpoint.BATCH, config.storagePrefix, queueExecutor) | ||
| val replayQueue = config.queueProvider(config, api, PostHogApiEndpoint.SNAPSHOT, config.replayStoragePrefix, replayExecutor) | ||
| val featureFlags = config.remoteConfigProvider(config, api, remoteConfigExecutor) | ||
| val featureFlags = | ||
| config.remoteConfigProvider(config, api, remoteConfigExecutor) { | ||
| getDefaultPersonProperties() | ||
| } | ||
|
|
||
| // no need to lock optOut here since the setup is locked already | ||
| val optOut = | ||
|
|
@@ -423,6 +427,9 @@ public class PostHog private constructor( | |
| requirePersonProcessing("capture", ignoreMessage = true) | ||
| } | ||
|
|
||
| // Automatically set person properties for feature flags during capture event | ||
| setPersonPropertiesForFlagsIfNeeded(userProperties, userPropertiesSetOnce) | ||
|
|
||
| if (newDistinctId.isBlank()) { | ||
| config?.logger?.log("capture call not allowed, distinctId is invalid: $newDistinctId.") | ||
| return | ||
|
|
@@ -579,6 +586,45 @@ public class PostHog private constructor( | |
| capture(PostHogEventName.CREATE_ALIAS.event, properties = props) | ||
| } | ||
|
|
||
| /** | ||
| * Returns fresh default device and app properties for feature flag evaluation. | ||
| */ | ||
| private fun getDefaultPersonProperties(): Map<String, Any> { | ||
| if (!isEnabled()) return emptyMap() | ||
| if (config?.setDefaultPersonProperties != true) return emptyMap() | ||
|
|
||
| return config?.context?.personPropertiesContext() ?: emptyMap() | ||
| } | ||
|
|
||
| private fun setPersonPropertiesForFlagsIfNeeded( | ||
| userProperties: Map<String, Any>?, | ||
| userPropertiesSetOnce: Map<String, Any>? = null, | ||
| ) { | ||
| if (!hasPersonProcessing()) return | ||
| if (userProperties.isNullOrEmpty() && userPropertiesSetOnce.isNullOrEmpty()) return | ||
|
|
||
| val allProperties = mutableMapOf<String, Any>() | ||
| userPropertiesSetOnce?.let { | ||
| allProperties.putAll(userPropertiesSetOnce) | ||
| } | ||
| userProperties?.let { | ||
| // User properties override setOnce properties | ||
| allProperties.putAll(userProperties) | ||
| } | ||
|
|
||
| remoteConfig?.setPersonPropertiesForFlags(allProperties) | ||
| } | ||
|
|
||
| private fun setGroupPropertiesForFlagsIfNeeded( | ||
| type: String, | ||
| groupProperties: Map<String, Any>?, | ||
| ) { | ||
| if (!hasPersonProcessing()) return | ||
| if (groupProperties.isNullOrEmpty()) return | ||
|
|
||
| remoteConfig?.setGroupPropertiesForFlags(type, groupProperties) | ||
| } | ||
|
|
||
| public override fun identify( | ||
| distinctId: String, | ||
| userProperties: Map<String, Any>?, | ||
|
|
@@ -636,6 +682,9 @@ public class PostHog private constructor( | |
| } | ||
| this.distinctId = distinctId | ||
|
|
||
| // Automatically set person properties for feature flags during identify() call | ||
| setPersonPropertiesForFlagsIfNeeded(userProperties, userPropertiesSetOnce) | ||
|
|
||
| // only because of testing in isolation, this flag is always enabled | ||
| if (reloadFeatureFlags) { | ||
| reloadFeatureFlags(config?.onFeatureFlags) | ||
|
|
@@ -745,6 +794,9 @@ public class PostHog private constructor( | |
|
|
||
| super.groupStateless(this.distinctId, type, key, groupProperties) | ||
|
|
||
| // Automatically set group properties for feature flags | ||
| setGroupPropertiesForFlagsIfNeeded(type, groupProperties) | ||
|
|
||
| // only because of testing in isolation, this flag is always enabled | ||
| if (reloadFeatureFlags && reloadFeatureFlagsIfNewGroup) { | ||
| reloadFeatureFlags(config?.onFeatureFlags) | ||
|
|
@@ -887,6 +939,73 @@ public class PostHog private constructor( | |
| replayQueue?.flush() | ||
| } | ||
|
|
||
| /** | ||
| * Sets person properties that will be included in feature flag evaluation requests. | ||
| * | ||
| * @param properties Dictionary of person properties to include in flag evaluation | ||
| * @param reloadFeatureFlags Whether to automatically reload feature flags after setting properties | ||
| */ | ||
| public fun setPersonPropertiesForFlags( | ||
|
||
| properties: Map<String, Any>, | ||
dustinbyrne marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| reloadFeatureFlags: Boolean = true, | ||
dustinbyrne marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) { | ||
| if (!isEnabled()) return | ||
| if (!hasPersonProcessing()) return | ||
| if (properties.isEmpty()) return | ||
|
|
||
| remoteConfig?.setPersonPropertiesForFlags(properties) | ||
|
|
||
| if (reloadFeatureFlags && this.reloadFeatureFlags) { | ||
| this.reloadFeatureFlags() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Resets all person properties that were set for feature flag evaluation. | ||
| */ | ||
| public fun resetPersonPropertiesForFlags() { | ||
| if (!isEnabled()) return | ||
| if (!hasPersonProcessing()) return | ||
|
|
||
| remoteConfig?.resetPersonPropertiesForFlags() | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you're right. This is a deviation from the iOS implementation. Similar to the other methods added here, I've added a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the user intentionally resets, we should otherwise the flags will be evaluated wrongly. |
||
|
|
||
| /** | ||
| * Sets properties for a specific group type to include when evaluating feature flags. | ||
| * | ||
| * @param groupType The group type identifier (e.g., "organization", "team") | ||
| * @param properties Dictionary of properties to set for this group type | ||
| * @param reloadFeatureFlags Whether to automatically reload feature flags after setting properties | ||
| */ | ||
| public fun setGroupPropertiesForFlags( | ||
| groupType: String, | ||
dustinbyrne marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| properties: Map<String, Any>, | ||
dustinbyrne marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| reloadFeatureFlags: Boolean = true, | ||
| ) { | ||
| if (!isEnabled()) return | ||
| if (!hasPersonProcessing()) return | ||
|
|
||
| if (properties.isEmpty()) return | ||
|
|
||
| remoteConfig?.setGroupPropertiesForFlags(groupType, properties) | ||
|
|
||
| if (reloadFeatureFlags && this.reloadFeatureFlags) { | ||
| this.reloadFeatureFlags() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Clears group properties for feature flag evaluation. | ||
| * | ||
| * @param groupType Optional group type to clear. If null, clears all group properties. | ||
| */ | ||
| public fun resetGroupPropertiesForFlags(groupType: String? = null) { | ||
dustinbyrne marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (!isEnabled()) return | ||
| if (!hasPersonProcessing()) return | ||
|
|
||
| remoteConfig?.resetGroupPropertiesForFlags(groupType) | ||
| } | ||
dustinbyrne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| public override fun reset() { | ||
| if (!isEnabled()) { | ||
| return | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,6 +74,25 @@ public open class PostHogConfig( | |
| * Defaults to null (evaluate all flags) | ||
| */ | ||
| public var evaluationEnvironments: List<String>? = null, | ||
| /** | ||
| * Automatically set common device and app properties as person properties for feature flag evaluation. | ||
| * | ||
| * When enabled, the SDK will automatically set the following person properties: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in f917d11 |
||
| * - $app_version: App version from package info | ||
| * - $app_build: App build number from package info | ||
| * - $os_name: Operating system name (Android) | ||
| * - $os_version: Operating system version | ||
|
Comment on lines
+84
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we decide to add those and the ones mentioned in the comment above, i'd suggest also adding to the iOS SDK https://github.com/PostHog/posthog-ios/blob/ba5dd65d2341ab8ea1949f072594ef67c490af3a/PostHog/PostHogConfig.swift#L98-L110 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an option, though it'd also require another change to set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added these in f917d11. If this is something we want to support, we can keep them in - it does no harm to include them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mm, maybe adding something that isn't supported yet would just add confusion then? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe - I suppose I didn't update the JavaDoc for this to include these additional parameters (I'll do that now!), but to your point, it'd be confusing to see it documented but not usable. I'll pull out the non-default properties. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I do see these properties being sent in test data so maybe I'm wrong. I'll have to dig a little deeper. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the end I ended up with The others ( |
||
| * - $device_type: Device type (Mobile, Tablet, TV, etc.) | ||
| * - $device_manufacturer: Device manufacturer | ||
| * - $device_model: Device model | ||
| * - $locale: User's current locale | ||
| * | ||
| * This helps ensure feature flags that rely on these properties work correctly | ||
| * without waiting for server-side processing of identify() calls. | ||
| * | ||
| * Default: true | ||
| */ | ||
| public var setDefaultPersonProperties: Boolean = true, | ||
| /** | ||
| * Preload PostHog remote config automatically | ||
| * Defaults to true | ||
|
|
@@ -192,8 +211,20 @@ public open class PostHogConfig( | |
| /** | ||
| * Factory to instantiate a custom [com.posthog.internal.PostHogRemoteConfigInterface] implementation. | ||
| */ | ||
| public val remoteConfigProvider: (PostHogConfig, PostHogApi, ExecutorService) -> PostHogFeatureFlagsInterface = | ||
| { config, api, executor -> PostHogRemoteConfig(config, api, executor) }, | ||
| public val remoteConfigProvider: ( | ||
| PostHogConfig, | ||
| PostHogApi, | ||
| ExecutorService, | ||
| (() -> Map<String, Any>)?, | ||
| ) -> PostHogFeatureFlagsInterface = | ||
| { | ||
| config, | ||
| api, | ||
| executor, | ||
| getDefaultPersonProperties, | ||
| -> | ||
| PostHogRemoteConfig(config, api, executor, getDefaultPersonProperties ?: { emptyMap() }) | ||
| }, | ||
| /** | ||
| * Factory to instantiate a custom queue implementation. | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,13 +19,13 @@ internal class PostHogFlagsRequest( | |
| this["\$anon_distinct_id"] = anonymousId | ||
| } | ||
| if (groups?.isNotEmpty() == true) { | ||
| this["\$groups"] = groups | ||
| this["groups"] = groups | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we have to fix this on iOS as well https://github.com/PostHog/posthog-ios/blob/ba5dd65d2341ab8ea1949f072594ef67c490af3a/PostHog/PostHogApi.swift#L221C14-L233 |
||
| } | ||
| if (personProperties?.isNotEmpty() == true) { | ||
| this["\$properties"] = personProperties | ||
| this["person_properties"] = personProperties | ||
| } | ||
| if (groupProperties?.isNotEmpty() == true) { | ||
| this["\$group_properties"] = groupProperties | ||
| this["group_properties"] = groupProperties | ||
| } | ||
| if (evaluationEnvironments?.isNotEmpty() == true) { | ||
| this["evaluation_environments"] = evaluationEnvironments | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after #312
should this be
Map<String, Any?>instead? (Any?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nullable keys are needed for local evaluation. In this case, we're always evaluating remotely so we don't need to handle null values.