Skip to content

Commit 57b8a6a

Browse files
committed
add support for hash salts in feature flag definitions and evaluation
1 parent 21b8d8b commit 57b8a6a

File tree

3 files changed

+387
-7
lines changed

3 files changed

+387
-7
lines changed

src/main/java/com/mixpanel/mixpanelapi/featureflags/model/ExperimentationFlag.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public final class ExperimentationFlag {
2222
private final String context;
2323
private final UUID experimentId;
2424
private final Boolean isExperimentActive;
25+
private final String hashSalt;
2526

2627
/**
2728
* Creates a new ExperimentationFlag.
@@ -35,8 +36,9 @@ public final class ExperimentationFlag {
3536
* @param context the property name used for rollout hashing (e.g., "distinct_id")
3637
* @param experimentId the experiment ID (may be null)
3738
* @param isExperimentActive whether the experiment is active (may be null)
39+
* @param hashSalt the hash salt for this flag (may be null for legacy flags)
3840
*/
39-
public ExperimentationFlag(String id, String name, String key, String status, int projectId, RuleSet ruleset, String context, UUID experimentId, Boolean isExperimentActive) {
41+
public ExperimentationFlag(String id, String name, String key, String status, int projectId, RuleSet ruleset, String context, UUID experimentId, Boolean isExperimentActive, String hashSalt) {
4042
this.id = id;
4143
this.name = name;
4244
this.key = key;
@@ -46,6 +48,7 @@ public ExperimentationFlag(String id, String name, String key, String status, in
4648
this.context = context;
4749
this.experimentId = experimentId;
4850
this.isExperimentActive = isExperimentActive;
51+
this.hashSalt = hashSalt;
4952
}
5053

5154
/**
@@ -111,6 +114,13 @@ public Boolean getIsExperimentActive() {
111114
return isExperimentActive;
112115
}
113116

117+
/**
118+
* @return the hash salt for this flag, or null for legacy flags
119+
*/
120+
public String getHashSalt() {
121+
return hashSalt;
122+
}
123+
114124
@Override
115125
public String toString() {
116126
return "ExperimentationFlag{" +
@@ -123,6 +133,7 @@ public String toString() {
123133
", context='" + context + '\'' +
124134
", experimentId=" + experimentId +
125135
", isExperimentActive=" + isExperimentActive +
136+
", hashSalt='" + hashSalt + '\'' +
126137
'}';
127138
}
128139
}

src/main/java/com/mixpanel/mixpanelapi/featureflags/provider/LocalFlagsProvider.java

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,12 @@ private ExperimentationFlag parseFlag(JSONObject json) {
206206
isExperimentActive = json.optBoolean("is_experiment_active", false);
207207
}
208208

209+
// Parse hash_salt (may be null for legacy flags)
210+
String hashSalt = json.optString("hash_salt", null);
211+
209212
RuleSet ruleset = parseRuleSet(json.optJSONObject("ruleset"));
210213

211-
return new ExperimentationFlag(id, name, key, status, projectId, ruleset, context, experimentId, isExperimentActive);
214+
return new ExperimentationFlag(id, name, key, status, projectId, ruleset, context, experimentId, isExperimentActive, hashSalt);
212215
}
213216

214217
/**
@@ -371,9 +374,13 @@ public <T> SelectedVariant<T> getVariant(String flagKey, SelectedVariant<T> fall
371374
}
372375

373376
// Evaluate rollouts
374-
float rolloutHash = HashUtils.normalizedHash(contextValue + flagKey, "rollout");
377+
List<Rollout> rollouts = ruleset.getRollouts();
378+
for (int rolloutIndex = 0; rolloutIndex < rollouts.size(); rolloutIndex++) {
379+
Rollout rollout = rollouts.get(rolloutIndex);
380+
381+
// Calculate rollout hash
382+
float rolloutHash = calculateRolloutHash(contextValue, flagKey, flag.getHashSalt(), rolloutIndex);
375383

376-
for (Rollout rollout : ruleset.getRollouts()) {
377384
if (rolloutHash >= rollout.getRolloutPercentage()) {
378385
continue;
379386
}
@@ -391,8 +398,8 @@ public <T> SelectedVariant<T> getVariant(String flagKey, SelectedVariant<T> fall
391398
if (rollout.hasVariantOverride()) {
392399
selectedVariant = findVariantByKey(ruleset.getVariants(), rollout.getVariantOverride().getKey());
393400
} else {
394-
// Use variant hash to select from split
395-
float variantHash = HashUtils.normalizedHash(contextValue + flagKey, "variant");
401+
// Calculate variant hash
402+
float variantHash = calculateVariantHash(contextValue, flagKey, flag.getHashSalt());
396403
selectedVariant = selectVariantBySplit(ruleset.getVariants(), variantHash, rollout);
397404
}
398405

@@ -558,6 +565,46 @@ private Variant selectVariantBySplit(List<Variant> variants, float hash, Rollout
558565
return variantsToUse.isEmpty() ? null : variantsToUse.get(variantsToUse.size() - 1);
559566
}
560567

568+
/**
569+
* Calculates the rollout hash for a given context and rollout index.
570+
* <p>
571+
* This method can be overridden in tests to verify hash parameters.
572+
* </p>
573+
*
574+
* @param contextValue the context value (e.g., user ID)
575+
* @param flagKey the flag key
576+
* @param hashSalt the hash salt (null or empty for legacy behavior)
577+
* @param rolloutIndex the index of the rollout being evaluated
578+
* @return the normalized hash value (0.0 to 1.0)
579+
*/
580+
protected float calculateRolloutHash(String contextValue, String flagKey,
581+
String hashSalt, int rolloutIndex) {
582+
if (hashSalt != null && !hashSalt.isEmpty()) {
583+
return HashUtils.normalizedHash(contextValue + flagKey, hashSalt + rolloutIndex);
584+
} else {
585+
return HashUtils.normalizedHash(contextValue + flagKey, "rollout");
586+
}
587+
}
588+
589+
/**
590+
* Calculates the variant hash for a given context.
591+
* <p>
592+
* This method can be overridden in tests to verify hash parameters.
593+
* </p>
594+
*
595+
* @param contextValue the context value (e.g., user ID)
596+
* @param flagKey the flag key
597+
* @param hashSalt the hash salt (null or empty for legacy behavior)
598+
* @return the normalized hash value (0.0 to 1.0)
599+
*/
600+
protected float calculateVariantHash(String contextValue, String flagKey, String hashSalt) {
601+
if (hashSalt != null && !hashSalt.isEmpty()) {
602+
return HashUtils.normalizedHash(contextValue + flagKey, hashSalt + "variant");
603+
} else {
604+
return HashUtils.normalizedHash(contextValue + flagKey, "variant");
605+
}
606+
}
607+
561608
/**
562609
* Evaluates all flags and returns their selected variants.
563610
* <p>

0 commit comments

Comments
 (0)