Skip to content

Conversation

@dustinbyrne
Copy link
Contributor

@dustinbyrne dustinbyrne commented Jan 26, 2026

💡 Motivation and Context

Currently, getFeatureFlagPayload() doesn't raise a $feature_flag_called event, which causes confusion when customers only call this method. This pattern will result in zero analytics about feature flag usage. This PR lays the groundwork for a new getFeatureFlagResult() API that returns both value and payload atomically, ensuring a single event captures the complete flag access, without needing to call more than one function.

Related: PostHog/posthog#43520

Changes

New FeatureFlagResult class

  • Contains key, enabled, variant, and payload fields
  • Provides value property (returns variant or enabled boolean)
  • Includes serializedPayload() and getPayloadAs<T>() for payload convenience

Updated PostHogFeatureFlagsInterface:

  • Replaced getFeatureFlag() and getFeatureFlagPayload() with single getFeatureFlagResult() method
  • Updated PostHogRemoteConfig (client-side implementation) to implement getFeatureFlagResult()
  • Updated PostHogFeatureFlags (server-side implementation) to implement getFeatureFlagResult()

Updated callers:

  • PostHog and PostHogStateless clients now delegate to getFeatureFlagResult() internally
  • The public API behavior is currently unchanged

💚 How did you test it?

  • All existing feature flag tests pass without modification
  • New FeatureFlagResultTest covers serialization, deserialization, equals/hashCode/toString
  • New test validates getFeatureFlag sends defaultValue in event when flag doesn't exist
  • Updated mock implementations in PostHogStatelessTest

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.
  • If there are related changes in the core package, I've already released them, or I'll release them before this one.

This is a single call which returns information about the flag
evaluation:\
1. The feature flag key
2. Whether or not the flag is enabled for the user
3. The chosen variant (if applicable)
4. The associated payload (if applicable)

This is one step in exposing `getFeatureFlagResult` to the public API.
When `defaultValue` is selected over a null value,
`$feature_flag_called` reports the default value. I noticed there's a
gap in testing for this behavior, so here it is.
@dustinbyrne dustinbyrne marked this pull request as ready for review January 26, 2026 20:59
@dustinbyrne dustinbyrne requested a review from a team as a code owner January 26, 2026 20:59
@dustinbyrne dustinbyrne requested a review from a team January 26, 2026 20:59
if (payload == null) return null
return when (payload) {
is String -> payload
else -> gson.toJson(payload)
Copy link
Member

Choose a reason for hiding this comment

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

either we try catch here or the method should be marked as @throws

public fun serializedPayload(): String? {
if (payload == null) return null
return when (payload) {
is String -> payload
Copy link
Member

Choose a reason for hiding this comment

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

can it be else? eg Int, etc otherwise we will try to serialize a primitive type

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Primitives are also valid JSON so I'm relying on the serializer to do that. Though, I suppose it's my assumption that it'll handle those cases. I'll verify.

Copy link
Member

Choose a reason for hiding this comment

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

yep, i just think that we dont need to pay the price of gson.toJson if we can just add the other cases here since its just a few anyway, but your call since gson.toJson wont fail anyway

get() = variant ?: enabled

internal companion object {
internal val gson: Gson by lazy { Gson() }
Copy link
Member

Choose a reason for hiding this comment

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

not sure if an instance per class is ideal here, can we share an instance for all data classes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you talking about propagating the serializer's Gson instance here?

Copy link
Member

Choose a reason for hiding this comment

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

Nope because that one has our own adapters, just creating a gson instance outside somewhere and pass along all flag result instances maybe?

@@ -1,17 +1,22 @@
## Next

- feat: Add `getFeatureFlagResult` to `PostHogFeatureFlagsInterface`, drop `getFeatureFlag`, `getFeatureFlagPayload`. ([#398](https://github.com/PostHog/posthog-android/pull/398))
Copy link
Member

Choose a reason for hiding this comment

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

will that break public APIs on Android?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change only affects the internal API. I was planning on making a separate PR to introduce getFeatureFlagResult to the public API.

When we do this, we'll leave getFeatureFlag and getFeatureFlagPayload available to the public API.

@marandaneto
Copy link
Member

marandaneto commented Jan 27, 2026

Currently, getFeatureFlagPayload() doesn't raise a $feature_flag_called event, which causes confusion when customers only call this method

that was intentional afik since users should never call getFeatureFlagPayload directly before calling isFeatureEnabled or getFeatureFlag, I recall asking that once, customers as well, and that was the reasoning, all of our docs also point out that, that you should check if the flag is enabled once and so on, did we change that?
context PostHog/posthog-flutter#161 (comment)

@dustinbyrne
Copy link
Contributor Author

Thanks for the review! You're right that the behavior of getFeatureFlagPayload / isFeatureEnabled is intentional, though it's been a somewhat common support topic for feature flags. E.g., people calling getFeatureFlagPayload without getFeatureFlag, wondering where the flag usage analytics are.

The purpose of this method is to simplify the API such that that in either access pattern, the behavior remains the same.
The original issue has some more details: PostHog/posthog#43520

@marandaneto
Copy link
Member

Thanks for the review! You're right that the behavior of getFeatureFlagPayload / isFeatureEnabled is intentional, though it's been a somewhat common support topic for feature flags. E.g., people calling getFeatureFlagPayload without getFeatureFlag, wondering where the flag usage analytics are.

The purpose of this method is to simplify the API such that that in either access pattern, the behavior remains the same. The original issue has some more details: PostHog/posthog#43520

i also think it makes sense and i wanted to add it back then.
the only think we need to make sure is that we dont capture 2 times the feat flag usage event if people do eg isFeatureEnabled and getFeatureFlagPayload right away but this should be guarded already with the featureFlagsCalled map

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants