From 0d02981f571ad5e1f3b5fbf04a6239a74b22b0c6 Mon Sep 17 00:00:00 2001
From: Zak Shearman <34372536+ZakShearman@users.noreply.github.com>
Date: Sun, 8 Feb 2026 14:26:39 +0000
Subject: [PATCH 1/4] feat: add method to block until local ffs loaded
---
.../java/net/hollowcube/posthog/PostHog.java | 61 ++++++++++-------
.../net/hollowcube/posthog/PostHogClient.java | 65 +++++++++++--------
.../hollowcube/posthog/PostHogClientImpl.java | 27 +++++++-
.../hollowcube/posthog/PostHogClientNoop.java | 5 ++
4 files changed, 107 insertions(+), 51 deletions(-)
diff --git a/src/main/java/net/hollowcube/posthog/PostHog.java b/src/main/java/net/hollowcube/posthog/PostHog.java
index dd1e5d7..3b1c09b 100644
--- a/src/main/java/net/hollowcube/posthog/PostHog.java
+++ b/src/main/java/net/hollowcube/posthog/PostHog.java
@@ -43,7 +43,7 @@ public static void shutdown(@NotNull Duration timeout) {
* Capture an event with the given name for the given distinct ID with no properties.
*
* @param distinctId Unique ID of the target in your database. May not be empty.
- * @param event Name of the event. May not be empty.
+ * @param event Name of the event. May not be empty.
*/
public static void capture(@NotNull String distinctId, @NotNull String event) {
getClient().capture(distinctId, event);
@@ -53,7 +53,7 @@ public static void capture(@NotNull String distinctId, @NotNull String event) {
* Capture an event with the given name for the given distinct ID with the provided properties.
*
* @param distinctId Unique ID of the target in your database. May not be empty.
- * @param event Name of the event. May not be empty.
+ * @param event Name of the event. May not be empty.
* @param properties Event properties
*/
public static void capture(@NotNull String distinctId, @NotNull String event, @NotNull Map The object must be serializable to a JSON object via Gson (not primitive or array)
The objects must be serializable to a JSON object via Gson (not primitive or array)
* - * @param distinctId Unique ID of the target in your database. May not be empty. - * @param properties Properties to set (including overwriting previous values) on the person profile + * @param distinctId Unique ID of the target in your database. May not be empty. + * @param properties Properties to set (including overwriting previous values) on the person profile * @param propertiesSetOnce Properties to set only if missing on the person profile */ public static void set(@NotNull String distinctId, @Nullable Object properties, @Nullable Object propertiesSetOnce) { @@ -169,7 +169,7 @@ public static void set(@NotNull String distinctId, @Nullable Object properties) * Alias the given distinct ID to the given alias. * * @param distinctId Unique ID of the target in your database. May not be empty. - * @param alias Alias to set for the distinct ID. May not be empty. + * @param alias Alias to set for the distinct ID. May not be empty. */ public static void alias(@NotNull String distinctId, @NotNull String alias) { getClient().alias(distinctId, alias); @@ -178,8 +178,8 @@ public static void alias(@NotNull String distinctId, @NotNull String alias) { /** * Assign the given properties to the given group (type & key). * - * @param type Group type. Must not be empty - * @param key Group key. Must not be empty + * @param type Group type. Must not be empty + * @param key Group key. Must not be empty * @param properties Properties to set (including overwriting previous values) on the group */ public static void groupIdentify(@NotNull String type, @NotNull String key, @NotNull MapThe object must be serializable to a JSON object via Gson (not primitive or array)
* - * @param type Group type. Must not be empty - * @param key Group key. Must not be empty + * @param type Group type. Must not be empty + * @param key Group key. Must not be empty * @param properties Properties to set (including overwriting previous values) on the group */ public static void groupIdentify(@NotNull String type, @NotNull String key, @NotNull Object properties) { @@ -212,7 +212,7 @@ public static void flush() { /** * Check if the given feature flag is enabled for the given distinct ID. * - * @param key Feature flag key + * @param key Feature flag key * @param distinctId Unique ID of the target in your database. May not be empty * @return True if the feature flag is enabled for the given distinct ID, false otherwise */ @@ -223,9 +223,9 @@ public static boolean isFeatureEnabled(@NotNull String key, @NotNull String dist /** * Check if the given feature flag is enabled for the given distinct ID with extra context. * - * @param key Feature flag key + * @param key Feature flag key * @param distinctId Unique ID of the target in your database. May not be empty - * @param context Extra context to pass to the feature flag evaluation + * @param context Extra context to pass to the feature flag evaluation * @return True if the feature flag is enabled for the given distinct ID, false otherwise */ public static boolean isFeatureEnabled(@NotNull String key, @NotNull String distinctId, @Nullable FeatureFlagContext context) { @@ -235,7 +235,7 @@ public static boolean isFeatureEnabled(@NotNull String key, @NotNull String dist /** * Get the feature flag state for the given distinct ID. * - * @param key Feature flag key + * @param key Feature flag key * @param distinctId Unique ID of the target in your database. May not be empty * @return Feature flag state */ @@ -246,9 +246,9 @@ public static boolean isFeatureEnabled(@NotNull String key, @NotNull String dist /** * Get the feature flag state for the given distinct ID with extra context. * - * @param key Feature flag key + * @param key Feature flag key * @param distinctId Unique ID of the target in your database. May not be empty - * @param context Extra context to pass to the feature flag evaluation + * @param context Extra context to pass to the feature flag evaluation * @return Feature flag state */ public static @NotNull FeatureFlagState getFeatureFlag(@NotNull String key, @NotNull String distinctId, @Nullable FeatureFlagContext context) { @@ -271,7 +271,7 @@ public static boolean isFeatureEnabled(@NotNull String key, @NotNull String dist * Get all feature flags for the given distinct ID with extra context. * * @param distinctId Unique ID of the target in your database. May not be empty - * @param context Extra context to pass to the feature flag evaluation + * @param context Extra context to pass to the feature flag evaluation * @return Feature flag states */ public static @NotNull FeatureFlagStates getAllFeatureFlags(@NotNull String distinctId, @Nullable FeatureFlagContext context) { @@ -288,6 +288,21 @@ public static void reloadFeatureFlags() { getClient().reloadFeatureFlags(); } + /** + * Blocks until local feature flags have been fetched, or the timeout is reached. + * + *This is useful during application startup to ensure feature flags are available
+ * before accepting requests. If it fails, local evals are disabled until it re-fetches (and it succeeds).
+ *
+ * @param timeout Maximum time to wait for the fetch to complete
+ * @return true if feature flags were successfully loaded, false if the fetch failed or timed out
+ * @throws UnsupportedOperationException if local feature flag evaluation is not enabled
+ */
+ @Blocking
+ public static boolean awaitFeatureFlags(@NotNull Duration timeout) {
+ return getClient().awaitFeatureFlags(timeout);
+ }
+
// Exceptions
public static void captureException(@NotNull Throwable throwable) {
diff --git a/src/main/java/net/hollowcube/posthog/PostHogClient.java b/src/main/java/net/hollowcube/posthog/PostHogClient.java
index 78017eb..ba83867 100644
--- a/src/main/java/net/hollowcube/posthog/PostHogClient.java
+++ b/src/main/java/net/hollowcube/posthog/PostHogClient.java
@@ -49,7 +49,7 @@ public sealed interface PostHogClient permits PostHogClientImpl, PostHogClientNo
* Capture an event with the given name for the given distinct ID with no properties.
*
* @param distinctId Unique ID of the target in your database. May not be empty.
- * @param event Name of the event. May not be empty.
+ * @param event Name of the event. May not be empty.
*/
default void capture(@NotNull String distinctId, @NotNull String event) {
capture(distinctId, event, Map.of());
@@ -59,7 +59,7 @@ default void capture(@NotNull String distinctId, @NotNull String event) {
* Capture an event with the given name for the given distinct ID with the provided properties.
*
* @param distinctId Unique ID of the target in your database. May not be empty.
- * @param event Name of the event. May not be empty.
+ * @param event Name of the event. May not be empty.
* @param properties Event properties
*/
default void capture(@NotNull String distinctId, @NotNull String event, @NotNull Map The object must be serializable to a JSON object via Gson (not primitive or array) The objects must be serializable to a JSON object via Gson (not primitive or array) The objects must be serializable to a JSON object via Gson (not primitive or array) The object must be serializable to a JSON object via Gson (not primitive or array) This is useful during application startup to ensure feature flags are available
+ * before accepting requests. If it fails, local evals are disabled until it re-fetches (and it succeeds).
+ *
+ * @param timeout Maximum time to wait for the fetch to complete
+ * @return true if feature flags were successfully loaded, false if the fetch failed or timed out
+ * @throws UnsupportedOperationException if local feature flag evaluation is not enabled
+ */
+ @Blocking
+ boolean awaitFeatureFlags(@NotNull Duration timeout);
+
// Exceptions
diff --git a/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java b/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java
index 736e61a..a7154dc 100644
--- a/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java
+++ b/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java
@@ -202,6 +202,12 @@ private void sendEventBatch(@NotNull JsonArray batch) {
}
}
+ // This occurs when local flags are not loaded and remote eval is disabled, so we return disabled
+ // If a client wants, they can block until local values are loaded with PostHogClient#awaitFeatureFlags
+ if (result == null) {
+ return FeatureFlagState.DISABLED;
+ }
+
// Send feature flag called event if configured to do so.
final boolean sendCalledEvent = featureFlagContext.sendFeatureFlagEvents() != null
? featureFlagContext.sendFeatureFlagEvents()
@@ -270,19 +276,32 @@ public void reloadFeatureFlags() {
this.featureFlagFetchTimer.wakeup();
}
+ @Override
+ public boolean awaitFeatureFlags(@NotNull Duration timeout) {
+ if (this.personalApiKey == null)
+ throw new UnsupportedOperationException("Local feature flag evaluation is not enabled (no personal API key)");
+ return loadRemoteFeatureFlags(timeout);
+ }
+
@Blocking
private void loadRemoteFeatureFlags() {
- if (this.personalApiKey == null) return; // Sanity check
+ loadRemoteFeatureFlags(featureFlagsRequestTimeout);
+ }
+
+ @Blocking
+ private boolean loadRemoteFeatureFlags(@NotNull Duration timeout) {
+ if (this.personalApiKey == null) return false; // Sanity check
final HttpRequest req = HttpRequest.newBuilder(URI.create(String.format("%s/api/feature_flag/local_evaluation", endpoint)))
.header("Authorization", String.format("Bearer %s", this.personalApiKey))
.header("User-Agent", USER_AGENT)
- .timeout(featureFlagsRequestTimeout)
+ .timeout(timeout)
.build();
try {
final HttpResponse This ensures feature flags are available immediately after the client is built.
+ * If the fetch fails or times out, construction still succeeds but feature flag
+ * evaluations will return {@link FeatureFlagState#DISABLED} until the next successful fetch. Requires a personal API key to be set via {@link #personalApiKey(String)}. This is useful during application startup to ensure feature flags are available
+ * before accepting requests. If it fails, local evals are disabled until it re-fetches (and it succeeds).
+ *
+ * Uses the timeout of {@link #loadRemoteFeatureFlags(Duration)}
+ *
+ * @return true if feature flags were successfully loaded, false if the fetch failed or timed out
+ * @throws UnsupportedOperationException if local feature flag evaluation is not enabled
+ */
+ @Blocking
+ boolean loadRemoteFeatureFlags();
+
/**
* Blocks until local feature flags have been fetched, or the timeout is reached.
*
@@ -345,7 +359,7 @@ default boolean isFeatureEnabled(@NotNull String key, @NotNull String distinctId
* @throws UnsupportedOperationException if local feature flag evaluation is not enabled
*/
@Blocking
- boolean awaitFeatureFlags(@NotNull Duration timeout);
+ boolean loadRemoteFeatureFlags(@NotNull Duration timeout);
// Exceptions
@@ -512,7 +526,7 @@ private Builder(@NotNull String projectApiKey) {
exceptionMiddleware // Exceptions
);
if (blockUntilLocalFlagsLoaded != null) {
- client.awaitFeatureFlags(blockUntilLocalFlagsLoaded);
+ client.loadRemoteFeatureFlags(blockUntilLocalFlagsLoaded);
}
return client;
}
diff --git a/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java b/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java
index d86d81c..ebd8f43 100644
--- a/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java
+++ b/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java
@@ -283,21 +283,16 @@ public void reloadFeatureFlags() {
this.featureFlagFetchTimer.wakeup();
}
- @Override
- public boolean awaitFeatureFlags(@NotNull Duration timeout) {
- if (this.personalApiKey == null)
- throw new UnsupportedOperationException("Local feature flag evaluation is not enabled (no personal API key)");
- return loadRemoteFeatureFlags(timeout);
- }
-
@Blocking
- private void loadRemoteFeatureFlags() {
- loadRemoteFeatureFlags(featureFlagsRequestTimeout);
+ public boolean loadRemoteFeatureFlags() {
+ return this.loadRemoteFeatureFlags(featureFlagsRequestTimeout);
}
@Blocking
- private boolean loadRemoteFeatureFlags(@NotNull Duration timeout) {
- if (this.personalApiKey == null) return false; // Sanity check
+ public boolean loadRemoteFeatureFlags(@NotNull Duration timeout) {
+ if (this.personalApiKey == null) {
+ throw new UnsupportedOperationException("Local feature flag evaluation is not enabled (no personal API key)");
+ }
final HttpRequest req = HttpRequest.newBuilder(URI.create(String.format("%s/api/feature_flag/local_evaluation", endpoint)))
.header("Authorization", String.format("Bearer %s", this.personalApiKey))
diff --git a/src/main/java/net/hollowcube/posthog/PostHogClientNoop.java b/src/main/java/net/hollowcube/posthog/PostHogClientNoop.java
index b647f58..da06a8c 100644
--- a/src/main/java/net/hollowcube/posthog/PostHogClientNoop.java
+++ b/src/main/java/net/hollowcube/posthog/PostHogClientNoop.java
@@ -38,7 +38,12 @@ public void reloadFeatureFlags() {
}
@Override
- public boolean awaitFeatureFlags(@NotNull Duration timeout) {
+ public boolean loadRemoteFeatureFlags() {
+ return true;
+ }
+
+ @Override
+ public boolean loadRemoteFeatureFlags(@NotNull Duration timeout) {
return true;
}
From ee172d01e3e369ae2615bbb848e49c39f43a0b55 Mon Sep 17 00:00:00 2001
From: Zak Shearman <34372536+ZakShearman@users.noreply.github.com>
Date: Mon, 9 Feb 2026 18:55:15 +0000
Subject: [PATCH 4/4] fix: rename missed methods
---
src/main/java/net/hollowcube/posthog/PostHog.java | 4 ++--
src/main/java/net/hollowcube/posthog/PostHogClientImpl.java | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/main/java/net/hollowcube/posthog/PostHog.java b/src/main/java/net/hollowcube/posthog/PostHog.java
index 3b1c09b..94d74ff 100644
--- a/src/main/java/net/hollowcube/posthog/PostHog.java
+++ b/src/main/java/net/hollowcube/posthog/PostHog.java
@@ -299,8 +299,8 @@ public static void reloadFeatureFlags() {
* @throws UnsupportedOperationException if local feature flag evaluation is not enabled
*/
@Blocking
- public static boolean awaitFeatureFlags(@NotNull Duration timeout) {
- return getClient().awaitFeatureFlags(timeout);
+ public static boolean loadRemoteFeatureFlags(@NotNull Duration timeout) {
+ return getClient().loadRemoteFeatureFlags(timeout);
}
// Exceptions
diff --git a/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java b/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java
index ebd8f43..111e60b 100644
--- a/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java
+++ b/src/main/java/net/hollowcube/posthog/PostHogClientImpl.java
@@ -204,13 +204,13 @@ private void sendEventBatch(@NotNull JsonArray batch) {
}
// This occurs when local flags are not loaded and remote eval is disabled, so we return disabled
- // If a client wants, they can block until local values are loaded with PostHogClient#awaitFeatureFlags
+ // If a client wants, they can block until local values are loaded with PostHogClient#loadRemoteFeatureFlags
if (result == null) {
if (!warnedAboutMissingFlags) {
warnedAboutMissingFlags = true;
log.warn("Local feature flags not yet loaded and remote evaluation is disabled. " +
"Returning DISABLED for all flags until loaded. " +
- "Use awaitFeatureFlags() or blockUntilLocalFlagsLoaded() to avoid this.");
+ "Use loadRemoteFeatureFlags() or blockUntilLocalFlagsLoaded() to avoid this.");
}
return FeatureFlagState.DISABLED;
}