Skip to content

Conversation

dustinbyrne
Copy link
Contributor

@dustinbyrne dustinbyrne commented Oct 3, 2025

💡 Motivation and Context

This PR adds local evaluation capabilities to feature flags. See the updated USAGE.md for details on how to use it.

If you're reviewing this PR, skimming FlagEvaluator.kt, LocalEvaluationPoller.kt, and changes to PostHogFeatureFlags.kt would be the best places to start.

This change is pretty massive. I'm sure there's still a couple of bugs lurking, but test coverage is pretty good. This PR doesn't really change the public API, so we can always follow up with smaller bug fixes.

Resolves #307

💚 How did you test it?

Lots of unit testing and updating the Java sample app to use local eval

📝 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.

@dustinbyrne dustinbyrne marked this pull request as ready for review October 6, 2025 22:14
@dustinbyrne dustinbyrne requested a review from a team as a code owner October 6, 2025 22:14
@dustinbyrne dustinbyrne requested a review from a team October 6, 2025 22:23
salt: String = "",
): Double {
val hashKey = "$key.$distinctId$salt"
val digest = MessageDigest.getInstance("SHA-1")
Copy link
Member

Choose a reason for hiding this comment

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

should this be a global var or do we need a fresh instance every time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

getInstance is a bit of a misnomer here - it's actually creating a new, stateful digest instance. It's possible we could use a single instance, but then would need to be synchronized. In this case, I don't think it's necessarily warranted.

val parsedDate =
try {
parseDateValue(value.toString())
} catch (e: Exception) {
Copy link
Member

Choose a reason for hiding this comment

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

I'd always use Throwable since it catches more than just Exception

Copy link
Contributor Author

@dustinbyrne dustinbyrne Oct 8, 2025

Choose a reason for hiding this comment

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

I typically try to avoid catching Throwable because it includes JVM exceptions that are generally unrecoverable like out of memory, thread death, JVM errors, etc.

Copy link
Member

Choose a reason for hiding this comment

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

you do catch a Throwable when executing scheduleAtFixedRate tho.
I usually prefer it because it's a lower level that captures many more errors, but of course, it depends on the throw signature of the methods you call.
I usually prefer to swallow SDK exceptions rather than let them bubble up, and if the caller does not handle the exception. It crashes the app
Sometimes you can get an OOM exception and its not even because the SDK is using a lot of memory, but it used the last bit, but people would still complain its the SDK doing the wrong thing :(
I will let you decide on this since its more server side and might be less of an issue

Copy link
Member

@marandaneto marandaneto left a comment

Choose a reason for hiding this comment

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

huge PR pheww... :D
i left a few comments, but i'd love to get eyes from the flags team as well, i just checked kotlin-ish code and if stuff made sense but i dont know the logic behind local eval

@dustinbyrne dustinbyrne marked this pull request as draft October 20, 2025 22:25
@dustinbyrne
Copy link
Contributor Author

Here's the current code coverage summary as of now

Metric Coverage Covered/Total
Class 100% 29/29
Method 90.1% 173/192
Branch 69.4% 369/532
Line 87.7% 1051/1198
Instruction 84.6% 4406/5209

@dustinbyrne dustinbyrne marked this pull request as ready for review October 21, 2025 20:33
@dustinbyrne dustinbyrne moved this to In Review in Feature Flags Oct 21, 2025
Deserialization is handled here, paired closely with the Api. This
simplifies deserialization in the server SDK.
This brings method coverage to 92% overall, line 85% overall.
It'll automatically clean up in case the developer forgets to call
posthog.close()
It's not necessary considering the first request to trigger local
evaluation will synchronously retrieve flag definitions if they're not
yet loaded.
Comment on lines +21 to +30
@IgnoreJRERequirement
@PostHogInternal
public data class FlagDefinition(
val id: Int,
val name: String,
val key: String,
val active: Boolean,
val filters: FlagFilters,
val version: Int,
)
Copy link
Contributor Author

@dustinbyrne dustinbyrne Oct 21, 2025

Choose a reason for hiding this comment

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

@marandaneto Is this okay to annotate with @IgnoreJRERequirement to suppress these Animal Sniffer errors?

[Undefined reference | android-api-level-21-5.0.1_r2] com.posthog.internal.(PostHogLocalEvaluationModels.kt:1)
  >> int Integer.hashCode(int)

[Undefined reference | android-api-level-21-5.0.1_r2] com.posthog.internal.(PostHogLocalEvaluationModels.kt:1)
  >> int Double.hashCode(double)

According to gummy-bears docs, this is incorrectly flagged as being added in API 24 despite working in API 19

Copy link
Member

Choose a reason for hiding this comment

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

nope not okay because of

// we don't use these methods, so ignore them, they are only available on Android >= 24
// [Undefined reference | android-api-level-21-5.0.1_r2] com.posthog.internal.(RREvent.kt:1)
// >> int Integer.hashCode(int)

also here
// not a data class to avoid (int Integer.hashCode(int))

desugaring would work but desugaring is an opt in feature and its not guaranteed that its enabled in the hosting app
https://developer.android.com/studio/write/java8-support-table

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

Labels

None yet

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

Add local evaluation

2 participants