Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix NullPointerException when accessing optional variables #118

Merged
merged 4 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ The method returns a FeatureResult object, which contains the evaluated result o
public <ValueType> FeatureResult<ValueType> evalFeature(String key, Class<ValueType> valueTypeClass, UserContext userContext);
```

* `getFeatureValue()` the same purpose as in `evalFeature()` but have ability to provide default value)
* `getFeatureValue()` the same purpose as in `evalFeature()`, but have ability to provide default value
```java
public <ValueType> ValueType getFeatureValue(String featureKey, ValueType defaultValue, Class<ValueType> gsonDeserializableClass, UserContext userContext);
```
Expand Down Expand Up @@ -270,7 +270,7 @@ public class InMemoryStickyBucketServiceImpl implements StickyBucketService {

### Releasing a new version

For now we are manually managing the version number.
For now, we are manually managing the version number.

When making a new release, ensure the file `growthbook/sdk/java/Version.java` has the version matching the tag and release. For example, if you are releasing version `0.3.0`, the following criteria should be met:

Expand Down
17 changes: 14 additions & 3 deletions lib/src/main/java/growthbook/sdk/java/GrowthBook.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
import java.util.Objects;
import javax.annotation.Nullable;
import com.google.gson.JsonObject;
import growthbook.sdk.java.callback.ExperimentRunCallback;
import growthbook.sdk.java.evaluators.ConditionEvaluator;
import growthbook.sdk.java.evaluators.ExperimentEvaluator;
import growthbook.sdk.java.evaluators.FeatureEvaluator;
import growthbook.sdk.java.model.AssignedExperiment;
import growthbook.sdk.java.model.Experiment;
import growthbook.sdk.java.model.ExperimentResult;
import growthbook.sdk.java.model.FeatureResult;
import growthbook.sdk.java.model.GBContext;
import growthbook.sdk.java.util.GrowthBookJsonUtils;
import growthbook.sdk.java.util.GrowthBookUtils;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -149,7 +160,7 @@ private EvaluationContext getEvaluationContext() {
* <p>
* There are a few ordered steps to evaluate a feature
* <p>
* 1. If the key doesn't exist in context.features
* 1. If the key doesn't exist in context.getFeatures()
* 1.1 Return getFeatureResult(null, "unknownFeature")
* 2. Loop through the feature rules (if any)
* 2.1 If the rule has parentConditions (prerequisites) defined, loop through each one:
Expand Down Expand Up @@ -270,7 +281,7 @@ public void setInMemoryStickyBucketService() {
* 4) 0
* Everything else is considered "truthy", including empty arrays and objects.
* If the value is "truthy", then isOn() will return true and isOff() will return false.
* If the value is "falsy", then the opposite values will be returned.
* If the value is "false", then the opposite values will be returned.
*
* @param featureKey name of the feature
* @return true if the value is a truthy value
Expand All @@ -289,7 +300,7 @@ public Boolean isOn(String featureKey) {
* 4) 0
* Everything else is considered "truthy", including empty arrays and objects.
* If the value is "truthy", then isOn() will return true and isOff() will return false.
* If the value is "falsy", then the opposite values will be returned.
* If the value is "false", then the opposite values will be returned.
*
* @param featureKey name of the feature
* @return true if the value is a truthy value
Expand Down
4 changes: 4 additions & 0 deletions lib/src/main/java/growthbook/sdk/java/IGrowthBook.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package growthbook.sdk.java;

import com.google.gson.JsonObject;
import growthbook.sdk.java.callback.ExperimentRunCallback;
import growthbook.sdk.java.model.Experiment;
import growthbook.sdk.java.model.ExperimentResult;
import growthbook.sdk.java.model.FeatureResult;
import growthbook.sdk.java.stickyBucketing.StickyBucketService;
import javax.annotation.Nullable;

Expand Down
6 changes: 2 additions & 4 deletions lib/src/main/java/growthbook/sdk/java/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
/**
* Tag for the published GrowthBook SDK version
*/
class Version {
private Version() {}

static final String SDK_VERSION = "0.9.92";
public interface Version {
String SDK_VERSION = "0.9.92";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package growthbook.sdk.java;
package growthbook.sdk.java.callback;

import growthbook.sdk.java.model.Experiment;
import growthbook.sdk.java.model.ExperimentResult;

/**
* A callback to be executed with an {@link ExperimentResult} whenever an experiment is run.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package growthbook.sdk.java;
package growthbook.sdk.java.callback;

import growthbook.sdk.java.repository.GBFeaturesRepository;

/**
* See {@link GBFeaturesRepository#onFeaturesRefresh(FeatureRefreshCallback)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package growthbook.sdk.java;
package growthbook.sdk.java.callback;

import growthbook.sdk.java.model.FeatureResult;

/**
* Listen for feature usage events
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package growthbook.sdk.java;
package growthbook.sdk.java.callback;

import growthbook.sdk.java.model.Experiment;
import growthbook.sdk.java.model.ExperimentResult;

/**
* This callback is called with the {@link Experiment} and {@link ExperimentResult} when an experiment is evaluated.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package growthbook.sdk.java;
package growthbook.sdk.java.evaluators;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import growthbook.sdk.java.util.GrowthBookJsonUtils;
import growthbook.sdk.java.model.Operator;
import growthbook.sdk.java.util.StringUtils;
import growthbook.sdk.java.model.DataType;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import java.lang.reflect.Type;
Expand All @@ -15,6 +20,7 @@
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
* <b>INTERNAL</b>: Implementation of condition evaluation
Expand Down Expand Up @@ -89,10 +95,10 @@ public Boolean evaluateCondition(JsonObject attributes, JsonObject conditionJson
}
// If none of the entries failed their checks, `evalCondition` returns true
return true;
} catch (com.google.gson.JsonSyntaxException jsonSyntaxException) {
} catch (JsonSyntaxException jsonSyntaxException) {
log.error(jsonSyntaxException.getMessage(), jsonSyntaxException);
return false;
} catch (java.util.regex.PatternSyntaxException patternSyntaxException) {
} catch (PatternSyntaxException patternSyntaxException) {
log.error(patternSyntaxException.getMessage(), patternSyntaxException);
return false;
} catch (Exception exception) { // for the case if something was missed
Expand All @@ -107,7 +113,7 @@ public Boolean evaluateCondition(JsonObject attributes, JsonObject conditionJson
* @param object The object to evaluate
* @return if all keys start with $
*/
Boolean isOperatorObject(JsonElement object) {
public Boolean isOperatorObject(JsonElement object) {
if (!object.isJsonObject()) {
return false;
}
Expand All @@ -134,7 +140,7 @@ Boolean isOperatorObject(JsonElement object) {
* @return the value at that path (or null if the path doesn't exist)
*/
@Nullable
Object getPath(JsonElement attributes, String path) {
public Object getPath(JsonElement attributes, String path) {
if (Objects.equals(path, "")) return null;

ArrayList<String> paths = new ArrayList<>();
Expand Down Expand Up @@ -463,15 +469,15 @@ Boolean evalOperatorCondition(String operatorString, @Nullable JsonElement actua

case IN_GROUP:
if (actual != null && expected != null) {
JsonElement jsonElement = savedGroups.get(expected.getAsString());
JsonElement jsonElement = savedGroups != null ? savedGroups.get(expected.getAsString()) : null;
if (jsonElement != null) {
return isIn(actual, jsonElement.getAsJsonArray());
}
return isIn(actual, new JsonArray());
}
case NOT_IN_GROUP:
if (actual != null && expected != null) {
JsonElement jsonElement = savedGroups.get(expected.getAsString());
JsonElement jsonElement = savedGroups != null ? savedGroups.get(expected.getAsString()) : null;
if (jsonElement != null) {
return !isIn(actual, jsonElement.getAsJsonArray());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
package growthbook.sdk.java;
package growthbook.sdk.java.evaluators;

import com.google.gson.JsonObject;
import growthbook.sdk.java.model.GeneratedStickyBucketAssignmentDocModel;
import growthbook.sdk.java.util.GrowthBookJsonUtils;
import growthbook.sdk.java.util.GrowthBookUtils;
import growthbook.sdk.java.model.HashAttributeAndHashValue;
import growthbook.sdk.java.model.Namespace;
import growthbook.sdk.java.model.ParentCondition;
import growthbook.sdk.java.model.BucketRange;
import growthbook.sdk.java.model.Experiment;
import growthbook.sdk.java.model.ExperimentResult;
import growthbook.sdk.java.model.FeatureResult;
import growthbook.sdk.java.model.FeatureResultSource;
import growthbook.sdk.java.model.Filter;
import growthbook.sdk.java.model.StickyBucketVariation;
import growthbook.sdk.java.model.VariationMeta;
import growthbook.sdk.java.multiusermode.ExperimentTracker;
import growthbook.sdk.java.multiusermode.configurations.EvaluationContext;
import growthbook.sdk.java.multiusermode.usage.TrackingCallbackWithUser;
Expand Down Expand Up @@ -282,7 +296,9 @@ public <ValueType> ExperimentResult<ValueType> evaluateExperiment(Experiment<Val
);

// save doc
context.getOptions().getStickyBucketService().saveAssignments(docModel.getStickyAssignmentsDocument());
if (context.getOptions().getStickyBucketService() != null) {
context.getOptions().getStickyBucketService().saveAssignments(docModel.getStickyAssignmentsDocument());
}
}
}

Expand Down Expand Up @@ -336,8 +352,8 @@ private <ValueType> ExperimentResult<ValueType> getExperimentResult(

List<VariationMeta> experimentMeta = new ArrayList<>();

if (experiment.meta != null) {
experimentMeta = experiment.meta;
if (experiment.getMeta() != null) {
experimentMeta = experiment.getMeta();
}

VariationMeta meta = null;
Expand All @@ -350,8 +366,8 @@ private <ValueType> ExperimentResult<ValueType> getExperimentResult(
Boolean passThrough = meta != null ? meta.getPassThrough() : null;

ValueType targetValue = null;
if (experiment.variations.size() > targetVariationIndex) {
targetValue = experiment.variations.get(targetVariationIndex);
if (experiment.getVariations().size() > targetVariationIndex) {
targetValue = experiment.getVariations().get(targetVariationIndex);
}

return ExperimentResult
Expand All @@ -373,10 +389,10 @@ private <ValueType> ExperimentResult<ValueType> getExperimentResult(

// Track experiments to trigger callbacks.
private <ValueType> boolean isExperimentTracked(Experiment<ValueType> experiment, ExperimentResult<ValueType> result) {
String experimentKey = experiment.key;
String experimentKey = experiment.getKey();

String key = (
result.hashAttribute != null ? result.getHashAttribute() : "")
result.getHashAttribute() != null ? result.getHashAttribute() : "")
+ (result.getHashValue() != null ? result.getHashValue() : "")
+ (experimentKey + result.getVariationId());

Expand All @@ -391,7 +407,7 @@ private <ValueType> boolean isExperimentTracked(Experiment<ValueType> experiment
private <ValueType> boolean isStickyBucketingEnabledForExperiment(EvaluationContext context,
Experiment<ValueType> experiment) {
return context.getOptions().getStickyBucketService() != null
&& !Boolean.TRUE.equals(experiment.disableStickyBucketing);
&& !Boolean.TRUE.equals(experiment.getDisableStickyBucketing());
}

private Map<String, Integer> getForcedVariations(EvaluationContext evaluationContext) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
package growthbook.sdk.java;
package growthbook.sdk.java.evaluators;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import growthbook.sdk.java.util.GrowthBookJsonUtils;
import growthbook.sdk.java.util.GrowthBookUtils;
import growthbook.sdk.java.model.ParentCondition;
import growthbook.sdk.java.model.Experiment;
import growthbook.sdk.java.model.ExperimentResult;
import growthbook.sdk.java.model.Feature;
import growthbook.sdk.java.model.FeatureResult;
import growthbook.sdk.java.model.FeatureResultSource;
import growthbook.sdk.java.model.FeatureRule;
import growthbook.sdk.java.model.Filter;
import growthbook.sdk.java.model.TrackData;
import growthbook.sdk.java.multiusermode.configurations.EvaluationContext;
import growthbook.sdk.java.multiusermode.usage.FeatureUsageCallbackWithUser;
import growthbook.sdk.java.multiusermode.usage.TrackingCallbackWithUser;
Expand Down Expand Up @@ -152,8 +163,7 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(

// Loop through the feature rules (if any)
List<FeatureRule<ValueType>> featureRules = feature.getRules();
for (int i = 0; i < featureRules.size(); i++) {
FeatureRule<ValueType> rule = featureRules.get(i);
for (FeatureRule<ValueType> rule : featureRules) {
// If there are prerequisite flag(s), evaluate them
if (rule.getParentConditions() != null) {
for (ParentCondition parentCondition : rule.getParentConditions()) {
Expand Down Expand Up @@ -195,7 +205,7 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(
// blocking prerequisite eval failed: feature evaluation fails
if (!evalCondition) {
// blocking prerequisite eval failed: feature evaluation fails
if (parentCondition.getGate()) {
if (Boolean.TRUE.equals(parentCondition.getGate())) {
log.info("Feature blocked by prerequisite");

FeatureResult<ValueType> featureResultWhenBlockedByPrerequisite =
Expand Down Expand Up @@ -227,7 +237,7 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(
}

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

// If the rule has a condition, and it evaluates to false, skip this rule and continue to the next one
if (rule.getCondition() != null) {
Expand All @@ -240,7 +250,7 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(
}

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

// Pass fallback attribute if sticky bucketing is enabled.
Expand Down Expand Up @@ -288,7 +298,7 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(

String attributeValue = context.getUser().getAttributes().get(ruleKey) == null
? null : context.getUser().getAttributes().get(ruleKey).getAsString();

if (attributeValue == null || attributeValue.isEmpty()) {
continue;
}
Expand All @@ -315,7 +325,7 @@ public <ValueType> FeatureResult<ValueType> evaluateFeature(
if (featureUsageCallbackWithUser != null) {
featureUsageCallbackWithUser.onFeatureUsage(key, forcedRuleFeatureValue, context.getUser());
}

return forcedRuleFeatureValue;
} else {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package growthbook.sdk.java;
package growthbook.sdk.java.evaluators;

import com.google.gson.JsonObject;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package growthbook.sdk.java;
package growthbook.sdk.java.evaluators;

import com.google.gson.JsonObject;
import growthbook.sdk.java.model.Experiment;
import growthbook.sdk.java.model.ExperimentResult;
import growthbook.sdk.java.multiusermode.configurations.EvaluationContext;

import javax.annotation.Nullable;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package growthbook.sdk.java;
package growthbook.sdk.java.evaluators;

import com.google.gson.JsonObject;
import growthbook.sdk.java.model.FeatureResult;
import growthbook.sdk.java.multiusermode.configurations.EvaluationContext;

interface IFeatureEvaluator {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package growthbook.sdk.java;
package growthbook.sdk.java.exception;

import growthbook.sdk.java.repository.GBFeaturesRepository;
import lombok.Getter;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package growthbook.sdk.java;
package growthbook.sdk.java.model;

import lombok.AllArgsConstructor;
import lombok.Data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package growthbook.sdk.java;
package growthbook.sdk.java.model;

import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializer;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package growthbook.sdk.java;
package growthbook.sdk.java.model;

/**
* A data type class used internally to help evaluate conditions
Expand Down
Loading