@@ -122,6 +122,18 @@
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ src/test/resources/logging.properties
+
+
+
@@ -138,5 +150,20 @@
json
20231013
+
+
+ io.github.jamsesso
+ json-logic-java
+ 1.1.0
+
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.20.0
+ provided
+
diff --git a/src/main/java/com/mixpanel/mixpanelapi/featureflags/model/Rollout.java b/src/main/java/com/mixpanel/mixpanelapi/featureflags/model/Rollout.java
index ed4ba32..377810d 100644
--- a/src/main/java/com/mixpanel/mixpanelapi/featureflags/model/Rollout.java
+++ b/src/main/java/com/mixpanel/mixpanelapi/featureflags/model/Rollout.java
@@ -3,6 +3,8 @@
import java.util.Collections;
import java.util.Map;
+import org.json.JSONObject;
+
/**
* Represents a rollout rule within a feature flag experiment.
*
@@ -15,7 +17,8 @@
*/
public final class Rollout {
private final float rolloutPercentage;
- private final Map runtimeEvaluationDefinition;
+ private final JSONObject runtimeEvaluationRule;
+ private final Map legacyRuntimeEvaluationDefinition;
private final VariantOverride variantOverride;
private final Map variantSplits;
@@ -23,15 +26,37 @@ public final class Rollout {
* Creates a new Rollout with all parameters.
*
* @param rolloutPercentage the percentage of users to include (0.0-1.0)
- * @param runtimeEvaluationDefinition optional map of property name to expected value for targeting
+ * @param runtimeEvaluationRule optional JSONObject containing jsonLogic rule for targeting
+ * @param legacyRuntimeEvaluationDefinition optional map of property name to expected value for targeting
+ * @param variantOverride optional variant override to force selection
+ * @param variantSplits optional map of variant key to split percentage at assignment group level
+ */
+ public Rollout(float rolloutPercentage, JSONObject runtimeEvaluationRule, Map legacyRuntimeEvaluationDefinition, VariantOverride variantOverride, Map variantSplits) {
+ this.rolloutPercentage = rolloutPercentage;
+ this.legacyRuntimeEvaluationDefinition = legacyRuntimeEvaluationDefinition != null
+ ? Collections.unmodifiableMap(legacyRuntimeEvaluationDefinition)
+ : null;
+ this.runtimeEvaluationRule = runtimeEvaluationRule;
+ this.variantOverride = variantOverride;
+ this.variantSplits = variantSplits != null
+ ? Collections.unmodifiableMap(variantSplits)
+ : null;
+ }
+
+ /**
+ * Creates a new Rollout with all legacy parameters.
+ *
+ * @param rolloutPercentage the percentage of users to include (0.0-1.0)
+ * @param legacyRuntimeEvaluationDefinition optional map of property name to expected value for targeting
* @param variantOverride optional variant override to force selection
* @param variantSplits optional map of variant key to split percentage at assignment group level
*/
- public Rollout(float rolloutPercentage, Map runtimeEvaluationDefinition, VariantOverride variantOverride, Map variantSplits) {
+ public Rollout(float rolloutPercentage, Map legacyRuntimeEvaluationDefinition, VariantOverride variantOverride, Map variantSplits) {
this.rolloutPercentage = rolloutPercentage;
- this.runtimeEvaluationDefinition = runtimeEvaluationDefinition != null
- ? Collections.unmodifiableMap(runtimeEvaluationDefinition)
+ this.legacyRuntimeEvaluationDefinition = legacyRuntimeEvaluationDefinition != null
+ ? Collections.unmodifiableMap(legacyRuntimeEvaluationDefinition)
: null;
+ this.runtimeEvaluationRule = null;
this.variantOverride = variantOverride;
this.variantSplits = variantSplits != null
? Collections.unmodifiableMap(variantSplits)
@@ -57,8 +82,8 @@ public float getRolloutPercentage() {
/**
* @return optional map of property name to expected value for runtime evaluation, or null if not set
*/
- public Map getRuntimeEvaluationDefinition() {
- return runtimeEvaluationDefinition;
+ public Map getLegacyRuntimeEvaluationDefinition() {
+ return legacyRuntimeEvaluationDefinition;
}
/**
@@ -75,11 +100,18 @@ public Map getVariantSplits() {
return variantSplits;
}
+ /**
+ * @return true if this rollout has runtime evaluation criteria
+ */
+ public boolean hasLegacyRuntimeEvaluation() {
+ return legacyRuntimeEvaluationDefinition != null && !legacyRuntimeEvaluationDefinition.isEmpty();
+ }
+
/**
* @return true if this rollout has runtime evaluation criteria
*/
public boolean hasRuntimeEvaluation() {
- return runtimeEvaluationDefinition != null && !runtimeEvaluationDefinition.isEmpty();
+ return runtimeEvaluationRule != null && runtimeEvaluationRule.length() > 0;
}
/**
@@ -100,9 +132,17 @@ public boolean hasVariantSplits() {
public String toString() {
return "Rollout{" +
"rolloutPercentage=" + rolloutPercentage +
- ", runtimeEvaluationDefinition=" + runtimeEvaluationDefinition +
+ ", legacyRuntimeEvaluationDefinition=" + legacyRuntimeEvaluationDefinition +
+ ", runtimeEvaluationRule=" + runtimeEvaluationRule +
", variantOverride='" + variantOverride + '\'' +
", variantSplits=" + variantSplits +
'}';
}
+
+ /**
+ * @return optional JSONObject containing JsonLogic rule for runtime evaluation, or null if not set
+ */
+ public JSONObject getRuntimeEvaluationRule() {
+ return runtimeEvaluationRule;
+ }
}
diff --git a/src/main/java/com/mixpanel/mixpanelapi/featureflags/provider/LocalFlagsProvider.java b/src/main/java/com/mixpanel/mixpanelapi/featureflags/provider/LocalFlagsProvider.java
index 7e064a2..2b03b64 100644
--- a/src/main/java/com/mixpanel/mixpanelapi/featureflags/provider/LocalFlagsProvider.java
+++ b/src/main/java/com/mixpanel/mixpanelapi/featureflags/provider/LocalFlagsProvider.java
@@ -4,6 +4,7 @@
import com.mixpanel.mixpanelapi.featureflags.config.LocalFlagsConfig;
import com.mixpanel.mixpanelapi.featureflags.model.*;
import com.mixpanel.mixpanelapi.featureflags.util.HashUtils;
+import com.mixpanel.mixpanelapi.featureflags.util.JsonLogicEngine;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -19,6 +20,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
+
/**
* Local feature flags evaluation provider.
*
@@ -288,15 +290,18 @@ private Rollout parseRollout(JSONObject json) {
}
}
- Map runtimeEval = null;
- JSONObject runtimeEvalJson = json.optJSONObject("runtime_evaluation_definition");
- if (runtimeEvalJson != null) {
- runtimeEval = new HashMap<>();
- for (String key : runtimeEvalJson.keySet()) {
- runtimeEval.put(key, runtimeEvalJson.get(key));
+ // Parse legacy runtime evaluation (simple key-value format)
+ Map legacyRuntimeEval = null;
+ JSONObject legacyRuntimeEvalJson = json.optJSONObject("runtime_evaluation_definition");
+ if (legacyRuntimeEvalJson != null) {
+ legacyRuntimeEval = new HashMap<>();
+ for (String key : legacyRuntimeEvalJson.keySet()) {
+ legacyRuntimeEval.put(key, legacyRuntimeEvalJson.get(key));
}
}
+ JSONObject runtimeEvaluationRule = json.optJSONObject("runtime_evaluation_rule");
+
Map variantSplits = null;
JSONObject variantSplitsJson = json.optJSONObject("variant_splits");
if (variantSplitsJson != null) {
@@ -306,7 +311,7 @@ private Rollout parseRollout(JSONObject json) {
}
}
- return new Rollout(rolloutPercentage, runtimeEval, variantOverride, variantSplits);
+ return new Rollout(rolloutPercentage, runtimeEvaluationRule, legacyRuntimeEval, variantOverride, variantSplits);
}
// #endregion
@@ -386,6 +391,12 @@ public SelectedVariant getVariant(String flagKey, SelectedVariant fall
}
// Check runtime evaluation, continue if this rollout has runtime conditions and it doesn't match
+ if (rollout.hasLegacyRuntimeEvaluation()) {
+ if (!matchesLegacyRuntimeConditions(rollout, context)) {
+ continue;
+ }
+ }
+
if (rollout.hasRuntimeEvaluation()) {
if (!matchesRuntimeConditions(rollout, context)) {
continue;
@@ -433,18 +444,23 @@ public SelectedVariant getVariant(String flagKey, SelectedVariant fall
}
}
+ private boolean matchesRuntimeConditions(Rollout rollout, Map context) {
+ Map customProperties = getCustomProperties(context);
+ return JsonLogicEngine.evaluate(rollout.getRuntimeEvaluationRule(), customProperties);
+ }
+
/**
* Evaluates runtime conditions for a rollout.
*
* @return true if all runtime conditions match, false otherwise (or if custom_properties is missing)
*/
- private boolean matchesRuntimeConditions(Rollout rollout, Map context) {
+ private boolean matchesLegacyRuntimeConditions(Rollout rollout, Map context) {
Map customProperties = getCustomProperties(context);
if (customProperties == null) {
return false;
}
- Map runtimeEval = rollout.getRuntimeEvaluationDefinition();
+ Map runtimeEval = rollout.getLegacyRuntimeEvaluationDefinition();
for (Map.Entry entry : runtimeEval.entrySet()) {
String key = entry.getKey();
Object expectedValue = entry.getValue();
diff --git a/src/main/java/com/mixpanel/mixpanelapi/featureflags/util/JsonCaseDesensitizer.java b/src/main/java/com/mixpanel/mixpanelapi/featureflags/util/JsonCaseDesensitizer.java
new file mode 100644
index 0000000..1413dd5
--- /dev/null
+++ b/src/main/java/com/mixpanel/mixpanelapi/featureflags/util/JsonCaseDesensitizer.java
@@ -0,0 +1,60 @@
+package com.mixpanel.mixpanelapi.featureflags.util;
+
+import java.util.Map;
+
+/**
+ * Implements case-insensitive comparison for runtime evaluation rule definitions and runtime parameters.
+ */
+public class JsonCaseDesensitizer {
+ public static Object lowercaseLeafNodes(Object object) {
+ if (object == null) {
+ return null;
+ }
+ else if (object instanceof String){
+ return ((String) object).toLowerCase();
+ } else if (object instanceof org.json.JSONObject) {
+ org.json.JSONObject jsonObject = (org.json.JSONObject) object;
+ org.json.JSONObject result = new org.json.JSONObject();
+ for (String key : jsonObject.keySet()) {
+ result.put(key, lowercaseLeafNodes(jsonObject.get(key)));
+ }
+ return result;
+ } else if (object instanceof org.json.JSONArray) {
+ org.json.JSONArray jsonArray = (org.json.JSONArray) object;
+ org.json.JSONArray result = new org.json.JSONArray();
+ for (int i = 0; i < jsonArray.length(); i++) {
+ result.put(lowercaseLeafNodes(jsonArray.get(i)));
+ }
+ return result;
+ } else {
+ return object;
+ }
+ }
+ public static Object lowercaseAllNodes(Object object) {
+ if (object == null) {
+ return null;
+ }
+ else if (object instanceof String){
+ return ((String) object).toLowerCase();
+ } else if (object instanceof Map) {
+ Map, ?> map = (Map, ?>) object;
+ Map