Skip to content

Commit

Permalink
Changelog v0.5.3
Browse files Browse the repository at this point in the history
  • Loading branch information
Bohdan-Kim committed May 15, 2024
1 parent fff17ac commit c6d79a8
Show file tree
Hide file tree
Showing 32 changed files with 5,881 additions and 2,102 deletions.
12 changes: 12 additions & 0 deletions lib/src/main/java/growthbook/sdk/java/Experiment.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ public class Experiment<ValueType> {
@Nullable
String phase;

@Nullable
String fallbackAttribute;

@Nullable
Boolean disableStickyBucketing;

@Nullable
Integer bucketVersion;

@Nullable
Integer minBucketVersion;

/**
* Get a Gson JsonElement of the experiment
* @return JsonElement
Expand Down
322 changes: 211 additions & 111 deletions lib/src/main/java/growthbook/sdk/java/ExperimentEvaluator.java

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions lib/src/main/java/growthbook/sdk/java/ExperimentHelper.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package growthbook.sdk.java;

import lombok.val;

import java.util.HashSet;
import java.util.Set;

Expand Down
7 changes: 6 additions & 1 deletion lib/src/main/java/growthbook/sdk/java/ExperimentResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public class ExperimentResult<ValueType> {
@SerializedName("passthrough")
Boolean passThrough;

@Nullable
Boolean stickyBucketUsed;

/**
* The result of running an {@link Experiment} given a specific {@link GBContext}
*
Expand Down Expand Up @@ -72,7 +75,8 @@ public ExperimentResult(
@Nullable String key,
@Nullable String name,
@Nullable Float bucket,
@Nullable Boolean passThrough
@Nullable Boolean passThrough,
@Nullable Boolean stickyBucketUsed
) {
this.value = value;
this.variationId = variationId;
Expand All @@ -90,6 +94,7 @@ public ExperimentResult(
this.name = name;
this.bucket = bucket;
this.passThrough = passThrough;
this.stickyBucketUsed = stickyBucketUsed;
}

// region Serialization
Expand Down
193 changes: 132 additions & 61 deletions lib/src/main/java/growthbook/sdk/java/FeatureEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,35 @@
import javax.annotation.Nullable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
* <b>INTERNAL</b>: Implementation of feature evaluation
*/

/**
* Feature Evaluator Class
* Takes Context and Feature Key
* Returns Calculated Feature Result against that key
*/
class FeatureEvaluator implements IFeatureEvaluator {

private final GrowthBookJsonUtils jsonUtils = GrowthBookJsonUtils.getInstance();
private final ConditionEvaluator conditionEvaluator = new ConditionEvaluator();
private final ExperimentEvaluator experimentEvaluator = new ExperimentEvaluator();

// Takes Context and Feature Key
// Returns Calculated Feature Result against that key
@Override
public <ValueType> FeatureResult<ValueType> evaluateFeature(String key, GBContext context, Class<ValueType> valueTypeClass) throws ClassCastException {
public <ValueType> FeatureResult<ValueType> evaluateFeature(
String key,
GBContext context,
Class<ValueType> valueTypeClass,
JsonObject attributeOverrides
) throws ClassCastException {

// This callback serves for listening for feature usage events
FeatureUsageCallback featureUsageCallback = context.getFeatureUsageCallback();

FeatureResult<ValueType> emptyFeature = FeatureResult
Expand All @@ -33,10 +49,10 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(String key, GBContex
ValueType forcedValue = evaluateForcedFeatureValueFromUrl(key, context.getUrl(), valueTypeClass);
if (forcedValue != null) {
FeatureResult<ValueType> urlFeatureResult = FeatureResult
.<ValueType>builder()
.value(forcedValue)
.source(FeatureResultSource.URL_OVERRIDE)
.build();
.<ValueType>builder()
.value(forcedValue)
.source(FeatureResultSource.URL_OVERRIDE)
.build();

if (featureUsageCallback != null) {
featureUsageCallback.onFeatureUsage(key, urlFeatureResult);
Expand Down Expand Up @@ -108,23 +124,36 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(String key, GBContex
}
// System.out.printf("\n\nAttributes = %s", attributes);

// region Rules
// Loop through the feature rules (if any)

for (FeatureRule<ValueType> rule : feature.getRules()) {
// If the rule has a condition, and it evaluates to false, skip this rule and continue to the next one
if (rule.getCondition() != null) {
if (!conditionEvaluator.evaluateCondition(attributesJson, rule.getCondition().toString())) {
continue;
}
}

// If there are filters for who is included (e.g. namespaces)
List<Filter> filters = rule.getFilters();
if (GrowthBookUtils.isFilteredOut(filters, attributes)) {
if (GrowthBookUtils.isFilteredOut(filters, attributes, context)) {

// Skip rule because of filters
continue;
}

// Feature value is being forced
if (rule.getForce() != null) {

// If the rule has a condition, and it evaluates to false, skip this rule and continue to the next one
if (rule.getCondition() != null) {
if (!conditionEvaluator.evaluateCondition(attributesJson, rule.getCondition().toString())) {

// Skip rule because of condition
continue;
}
}

boolean gate1 = context.getStickyBucketService() != null;
boolean gate2 = !Boolean.TRUE.equals(rule.disableStickyBucketing);
boolean shouldFallbackAttributeBePassed = gate1 && gate2;

String fallback = shouldFallbackAttributeBePassed ? rule.getFallbackAttribute() : null;

String ruleKey = rule.getHashAttribute();
if (ruleKey == null) {
ruleKey = "id";
Expand All @@ -135,28 +164,51 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(String key, GBContex
seed = key;
}

// If this is a percentage rollout, skip if not included
if (
!GrowthBookUtils.isIncludedInRollout(
attributes,
seed,
ruleKey,
rule.getRange(),
rule.getCoverage(),
rule.getHashVersion()
)
!GrowthBookUtils.isIncludedInRollout(
attributes,
seed,
ruleKey,
fallback,
rule.getRange(),
rule.getCoverage(),
rule.getHashVersion(),
context
)
) {

// Skip rule because user not included in rollout
continue;
}

// Call the tracking callback with all the track data
List<TrackData<ValueType>> trackData = rule.getTracks();
TrackingCallback trackingCallback = context.getTrackingCallback();

// If this was a remotely evaluated experiment, fire the tracking callbacks
if (trackData != null && trackingCallback != null) {
trackData.forEach(t -> {
trackingCallback.onTrack(t.getExperiment(), t.getExperimentResult());
});
}

if (rule.getRange() == null) {
if (rule.getCoverage() != null) {
// String key = ruleKey;
String attributeValue = context.getAttributes().get(ruleKey) == null ? null : context.getAttributes().get(ruleKey).getAsString();
if (attributeValue == null || attributeValue.isEmpty()) {
Float hashFNV = GrowthBookUtils.hash(attributeValue, 1, key);
if (hashFNV == null) {
hashFNV = 0f;
}
if (hashFNV > rule.getCoverage()) {
continue;
}
}
}
}

ValueType value = (ValueType) GrowthBookJsonUtils.unwrap(rule.getForce());

// Apply the force rule
Expand All @@ -170,47 +222,61 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(String key, GBContex
featureUsageCallback.onFeatureUsage(key, forcedRuleFeatureValue);
}
return forcedRuleFeatureValue;
}

// Experiment rule
String experimentKey = rule.getKey();
if (experimentKey == null) {
experimentKey = key;
}

Experiment<ValueType> experiment = Experiment
.<ValueType>builder()
.key(experimentKey)
.coverage(rule.getCoverage())
.weights(rule.getWeights())
.hashAttribute(rule.getHashAttribute())
.namespace(rule.getNamespace())
.variations(rule.getVariations())
.meta(rule.getMeta())
.ranges(rule.getRanges())
.name(rule.getName())
.phase(rule.getPhase())
.seed(rule.getSeed())
.hashVersion(rule.getHashVersion())
.filters(rule.getFilters())
.build();

ExperimentResult<ValueType> result = experimentEvaluator.evaluateExperiment(experiment, context, key);
if (result.getInExperiment() && (result.getPassThrough() == null || !result.getPassThrough())) {
ValueType value = (ValueType) GrowthBookJsonUtils.unwrap(result.getValue());

FeatureResult<ValueType> experimentFeatureResult = FeatureResult
.<ValueType>builder()
.value(value)
.source(FeatureResultSource.EXPERIMENT)
.experiment(experiment)
.experimentResult(result)
.build();

if (featureUsageCallback != null) {
featureUsageCallback.onFeatureUsage(key, experimentFeatureResult);
} else {

ArrayList<ValueType> variations = rule.getVariations();
if (variations != null) {

// Experiment rule
String experimentKey = rule.getKey();
if (experimentKey == null) {
experimentKey = key;
}

// For experiment rules, run an experiment
Experiment<ValueType> experiment = Experiment
.<ValueType>builder()
.key(experimentKey)
.coverage(rule.getCoverage())
.weights(rule.getWeights())
.hashAttribute(rule.getHashAttribute())
.namespace(rule.getNamespace())
.variations(rule.getVariations())
.meta(rule.getMeta())
.ranges(rule.getRanges())
.name(rule.getName())
.phase(rule.getPhase())
.seed(rule.getSeed())
.hashVersion(rule.getHashVersion())
.filters(rule.getFilters())
.variations(variations)
.minBucketVersion(rule.getMinBucketVersion())
.bucketVersion(rule.getBucketVersion())
.disableStickyBucketing(rule.getDisableStickyBucketing())
.fallbackAttribute(rule.getFallbackAttribute())
.build();

// Only return a value if the user is part of the experiment
ExperimentResult<ValueType> result = experimentEvaluator.evaluateExperiment(experiment, context, key, attributeOverrides);
if (result.getInExperiment() && (result.getPassThrough() == null || !result.getPassThrough())) {
ValueType value = (ValueType) GrowthBookJsonUtils.unwrap(result.getValue());

FeatureResult<ValueType> experimentFeatureResult = FeatureResult
.<ValueType>builder()
.value(value)
.source(FeatureResultSource.EXPERIMENT)
.experiment(experiment)
.experimentResult(result)
.build();

if (featureUsageCallback != null) {
featureUsageCallback.onFeatureUsage(key, experimentFeatureResult);
}
return experimentFeatureResult;
}
} else {
continue;
}
return experimentFeatureResult;
}
}

Expand All @@ -227,9 +293,14 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(String key, GBContex
if (featureUsageCallback != null) {
featureUsageCallback.onFeatureUsage(key, defaultValueFeatureResult);
}

// Return (value = defaultValue or null, source = defaultValue)
return defaultValueFeatureResult;
} catch (Exception e) {
e.printStackTrace();

// If the key doesn't exist in context.features, return immediately
// (value = null, source = unknownFeature).
return emptyFeature;
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/main/java/growthbook/sdk/java/FeatureResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import java.lang.reflect.Type;

/**
* Results for a {@link FeatureEvaluator#evaluateFeature(String, GBContext, Class)}
* Results for a {@link FeatureEvaluator#evaluateFeature(String, GBContext, Class, JsonObject)}
*
* <ul>
* <li>value (any) - The assigned value of the feature</li>
Expand Down
14 changes: 12 additions & 2 deletions lib/src/main/java/growthbook/sdk/java/FeatureRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;

/**
* Overrides the defaultValue of a Feature based on a set of requirements. Has a number of optional properties
Expand Down Expand Up @@ -78,6 +76,18 @@ public class FeatureRule<ValueType> {
@Nullable
String phase;

@Nullable
String fallbackAttribute;

@Nullable
Boolean disableStickyBucketing;

@Nullable
Integer bucketVersion;

@Nullable
Integer minBucketVersion;

@Nullable
ArrayList<TrackData<ValueType>> tracks;
}
Loading

0 comments on commit c6d79a8

Please sign in to comment.