diff --git a/providers/optimizely/README.md b/providers/optimizely/README.md
index f83faa122..9e16bacf7 100644
--- a/providers/optimizely/README.md
+++ b/providers/optimizely/README.md
@@ -19,9 +19,13 @@
## Concepts
-* Boolean evaluation gets feature [enabled](https://docs.developers.optimizely.com/feature-experimentation/docs/create-feature-flags) value.
-* Object evaluation gets a structure representing the evaluated variant variables.
-* String/Integer/Double evaluations evaluation are not directly supported by Optimizely provider, use getObjectEvaluation instead.
+### Evaluation Context
+
+The `targetingKey` is required and maps to the Optimizely user ID. Additional attributes are passed to Optimizely for audience targeting.
+
+### Variable Key Selection
+
+Optimizely flags can have multiple variables. By default, the provider looks for a variable named `"value"`. Specify a different variable using the `variableKey` attribute:
## Usage
Optimizely OpenFeature Provider is based on [Optimizely Java SDK documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/java-sdk).
@@ -57,3 +61,9 @@ provider.getOptimizely()...
Unit test based on optimizely [Local Data File](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-java).
See [OptimizelyProviderTest](./src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java)
for more information.
+
+## Release Notes
+
+### 0.1.0
+
+Concepts updated, evaluation acts accordingly.
diff --git a/providers/optimizely/pom.xml b/providers/optimizely/pom.xml
index 6b4594ebe..a7312fe54 100644
--- a/providers/optimizely/pom.xml
+++ b/providers/optimizely/pom.xml
@@ -10,7 +10,7 @@
dev.openfeature.contrib.providers
optimizely
- 0.0.1
+ 0.1.0
optimizely
optimizely provider for Java
diff --git a/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProvider.java b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProvider.java
index dae35cbd3..d238dea69 100644
--- a/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProvider.java
+++ b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProvider.java
@@ -2,15 +2,14 @@
import com.optimizely.ab.Optimizely;
import com.optimizely.ab.OptimizelyUserContext;
-import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.EventProvider;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
-import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.SneakyThrows;
@@ -69,68 +68,135 @@ public Metadata getMetadata() {
@Override
public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
OptimizelyUserContext userContext = contextTransformer.transform(ctx);
- OptimizelyDecision decision = userContext.decide(key);
- String variationKey = decision.getVariationKey();
- String reasonsString = null;
- if (variationKey == null) {
- List reasons = decision.getReasons();
- reasonsString = String.join(", ", reasons);
+
+ String variableKey = getVariableKey(ctx);
+ Boolean enabled = optimizely.getFeatureVariableBoolean(
+ key, variableKey, userContext.getUserId(), userContext.getAttributes());
+
+ String variant = variableKey;
+ String reason = Reason.TARGETING_MATCH.name();
+ if (enabled == null) {
+ enabled = false;
+ variant = null;
+ reason = Reason.DEFAULT.name();
}
- boolean enabled = decision.getEnabled();
return ProviderEvaluation.builder()
.value(enabled)
- .reason(reasonsString)
+ .reason(reason)
+ .variant(variant)
.build();
}
+ private static String getVariableKey(EvaluationContext ctx) {
+ String variableKey = "value";
+ Value varKey = ctx.getValue("variableKey");
+ if (varKey != null && varKey.isString() && !varKey.asString().isBlank()) {
+ variableKey = varKey.asString();
+ }
+ return variableKey;
+ }
+
@SneakyThrows
@Override
public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
- throw new UnsupportedOperationException("String evaluation is not directly supported by Optimizely provider,"
- + "use getObjectEvaluation instead.");
+ OptimizelyUserContext userContext = contextTransformer.transform(ctx);
+
+ String variableKey = getVariableKey(ctx);
+ String value = optimizely.getFeatureVariableString(
+ key, variableKey, userContext.getUserId(), userContext.getAttributes());
+
+ String variant = variableKey;
+ String reason = Reason.TARGETING_MATCH.name();
+ if (value == null) {
+ value = defaultValue;
+ variant = null;
+ reason = Reason.DEFAULT.name();
+ }
+
+ return ProviderEvaluation.builder()
+ .value(value)
+ .reason(reason)
+ .variant(variant)
+ .build();
}
@Override
public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
- throw new UnsupportedOperationException("Integer evaluation is not directly supported by Optimizely provider,"
- + "use getObjectEvaluation instead.");
+ OptimizelyUserContext userContext = contextTransformer.transform(ctx);
+
+ String variableKey = getVariableKey(ctx);
+ Integer value = optimizely.getFeatureVariableInteger(
+ key, variableKey, userContext.getUserId(), userContext.getAttributes());
+
+ String variant = variableKey;
+ String reason = Reason.TARGETING_MATCH.name();
+ if (value == null) {
+ value = defaultValue;
+ variant = null;
+ reason = Reason.DEFAULT.name();
+ }
+
+ return ProviderEvaluation.builder()
+ .value(value)
+ .reason(reason)
+ .variant(variant)
+ .build();
}
@Override
public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
- throw new UnsupportedOperationException("Double evaluation is not directly supported by Optimizely provider,"
- + "use getObjectEvaluation instead.");
+ OptimizelyUserContext userContext = contextTransformer.transform(ctx);
+
+ String variableKey = getVariableKey(ctx);
+ Double value = optimizely.getFeatureVariableDouble(
+ key, variableKey, userContext.getUserId(), userContext.getAttributes());
+
+ String variant = variableKey;
+ String reason = Reason.TARGETING_MATCH.name();
+ if (value == null) {
+ value = defaultValue;
+ variant = null;
+ reason = Reason.DEFAULT.name();
+ }
+
+ return ProviderEvaluation.builder()
+ .value(value)
+ .reason(reason)
+ .variant(variant)
+ .build();
}
@SneakyThrows
@Override
public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
OptimizelyUserContext userContext = contextTransformer.transform(ctx);
- OptimizelyDecision decision = userContext.decide(key);
- String variationKey = decision.getVariationKey();
- String reasonsString = null;
- if (variationKey == null) {
- List reasons = decision.getReasons();
- reasonsString = String.join(", ", reasons);
- }
- Value evaluatedValue = defaultValue;
- boolean enabled = decision.getEnabled();
- if (enabled) {
- OptimizelyJSON variables = decision.getVariables();
- evaluatedValue = toValue(variables);
+ String variableKey = getVariableKey(ctx);
+ OptimizelyJSON value = optimizely.getFeatureVariableJSON(
+ key, variableKey, userContext.getUserId(), userContext.getAttributes());
+ Value evaluatedValue = toValue(value);
+
+ String variant = variableKey;
+ String reason = Reason.TARGETING_MATCH.name();
+ if (value == null) {
+ evaluatedValue = defaultValue;
+ variant = null;
+ reason = Reason.DEFAULT.name();
}
return ProviderEvaluation.builder()
.value(evaluatedValue)
- .reason(reasonsString)
- .variant(variationKey)
+ .reason(reason)
+ .variant(variant)
.build();
}
@SneakyThrows
private Value toValue(OptimizelyJSON optimizelyJson) {
+ if (optimizelyJson == null) {
+ return new Value();
+ }
Map map = optimizelyJson.toMap();
Structure structure = Structure.mapToStructure(map);
return new Value(structure);
diff --git a/providers/optimizely/src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java b/providers/optimizely/src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java
index d92fba154..50ec83206 100644
--- a/providers/optimizely/src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java
+++ b/providers/optimizely/src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java
@@ -4,7 +4,6 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
@@ -14,6 +13,7 @@
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
import java.io.File;
@@ -78,24 +78,69 @@ public void testInitializeHandlesNullConfigurationParameters() {
@Test
public void testGetObjectEvaluation() {
EvaluationContext ctx = new MutableContext("targetingKey");
- ProviderEvaluation result = provider.getObjectEvaluation("string-feature", new Value(), ctx);
+ ProviderEvaluation evaluation = provider.getObjectEvaluation("json_feature_flag", new Value(), ctx);
- assertNotNull(result.getValue());
- assertEquals("string_feature_variation", result.getVariant());
assertEquals(
- "str1",
- result.getValue().asStructure().getValue("string_variable_1").asString());
+ "{key1=value1, key2=2.0}",
+ evaluation.getValue().asStructure().asObjectMap().toString());
- result = provider.getObjectEvaluation("non-existing-object-feature", new Value(), ctx);
- assertNotNull(result.getReason());
+ MutableContext contextWithVarKey = new MutableContext("targetingKey");
+ contextWithVarKey.add("variableKey", "var_json");
+
+ evaluation = provider.getObjectEvaluation("json_feature_flag", new Value(), contextWithVarKey);
+
+ assertEquals(
+ "{key1=value1a, key2=3.0}",
+ evaluation.getValue().asStructure().asObjectMap().toString());
+ assertEquals("var_json", evaluation.getVariant());
+ assertEquals(Reason.TARGETING_MATCH.name(), evaluation.getReason());
+
+ MutableContext contextWithNonExistingVarKey = new MutableContext("targetingKey");
+ contextWithNonExistingVarKey.add("variableKey", "non-existing-var");
+
+ evaluation = provider.getObjectEvaluation("json_feature_flag", new Value(), contextWithNonExistingVarKey);
+
+ assertTrue(evaluation.getValue().isNull());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
+
+ EvaluationContext emptyEvaluationContext = new MutableContext();
+ assertThrows(TargetingKeyMissingError.class, () -> {
+ provider.getObjectEvaluation("string-feature", new Value(), emptyEvaluationContext);
+ });
+
+ evaluation = provider.getObjectEvaluation("non-existing-feature", new Value(), ctx);
+ assertTrue(evaluation.getValue().isNull());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
}
@Test
public void testGetBooleanEvaluation() {
EvaluationContext ctx = new MutableContext("targetingKey");
- ProviderEvaluation evaluation = provider.getBooleanEvaluation("string-feature", false, ctx);
+ ProviderEvaluation evaluation = provider.getBooleanEvaluation("boolean_feature_flag", false, ctx);
+
+ assertTrue(evaluation.getValue());
+ assertEquals("value", evaluation.getVariant());
+ assertEquals(Reason.TARGETING_MATCH.name(), evaluation.getReason());
+
+ MutableContext contextWithVarKey = new MutableContext("targetingKey");
+ contextWithVarKey.add("variableKey", "var_bool");
+
+ evaluation = provider.getBooleanEvaluation("boolean_feature_flag", false, contextWithVarKey);
assertTrue(evaluation.getValue());
+ assertEquals("var_bool", evaluation.getVariant());
+ assertEquals(Reason.TARGETING_MATCH.name(), evaluation.getReason());
+
+ MutableContext contextWithNonExistingVarKey = new MutableContext("targetingKey");
+ contextWithNonExistingVarKey.add("variableKey", "non-existing-var");
+
+ evaluation = provider.getBooleanEvaluation("boolean_feature_flag", false, contextWithNonExistingVarKey);
+
+ assertFalse(evaluation.getValue());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
EvaluationContext emptyEvaluationContext = new MutableContext();
assertThrows(TargetingKeyMissingError.class, () -> {
@@ -104,24 +149,116 @@ public void testGetBooleanEvaluation() {
evaluation = provider.getBooleanEvaluation("non-existing-feature", false, ctx);
assertFalse(evaluation.getValue());
- assertNotNull(evaluation.getReason());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
}
@Test
- public void testUnsupportedEvaluations() {
+ public void testGetStringEvaluation() {
EvaluationContext ctx = new MutableContext("targetingKey");
+ ProviderEvaluation evaluation = provider.getStringEvaluation("string_feature_flag", "", ctx);
+
+ assertEquals("str1", evaluation.getValue());
+
+ MutableContext contextWithVarKey = new MutableContext("targetingKey");
+ contextWithVarKey.add("variableKey", "var_str");
+
+ evaluation = provider.getStringEvaluation("string_feature_flag", "", contextWithVarKey);
- assertThrows(UnsupportedOperationException.class, () -> {
- provider.getDoubleEvaluation("string-feature", 0.0, ctx);
+ assertEquals("str2", evaluation.getValue());
+ assertEquals("var_str", evaluation.getVariant());
+ assertEquals(Reason.TARGETING_MATCH.name(), evaluation.getReason());
+
+ MutableContext contextWithNonExistingVarKey = new MutableContext("targetingKey");
+ contextWithNonExistingVarKey.add("variableKey", "non-existing-var");
+
+ evaluation = provider.getStringEvaluation("string_feature_flag", "", contextWithNonExistingVarKey);
+
+ assertEquals("", evaluation.getValue());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
+
+ EvaluationContext emptyEvaluationContext = new MutableContext();
+ assertThrows(TargetingKeyMissingError.class, () -> {
+ provider.getStringEvaluation("string-feature", "", emptyEvaluationContext);
});
- assertThrows(UnsupportedOperationException.class, () -> {
- provider.getIntegerEvaluation("string-feature", 0, ctx);
+ evaluation = provider.getStringEvaluation("non-existing-feature", "", ctx);
+ assertEquals("", evaluation.getValue());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
+ }
+
+ @Test
+ public void testGetIntegerEvaluation() {
+ EvaluationContext ctx = new MutableContext("targetingKey");
+ ProviderEvaluation evaluation = provider.getIntegerEvaluation("int_feature_flag", 0, ctx);
+
+ assertEquals(1, evaluation.getValue());
+
+ MutableContext contextWithVarKey = new MutableContext("targetingKey");
+ contextWithVarKey.add("variableKey", "var_int");
+
+ evaluation = provider.getIntegerEvaluation("int_feature_flag", 0, contextWithVarKey);
+
+ assertEquals(2, evaluation.getValue());
+ assertEquals("var_int", evaluation.getVariant());
+ assertEquals(Reason.TARGETING_MATCH.name(), evaluation.getReason());
+
+ MutableContext contextWithNonExistingVarKey = new MutableContext("targetingKey");
+ contextWithNonExistingVarKey.add("variableKey", "non-existing-var");
+
+ evaluation = provider.getIntegerEvaluation("int_feature_flag", 0, contextWithNonExistingVarKey);
+
+ assertEquals(0, evaluation.getValue());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
+
+ EvaluationContext emptyEvaluationContext = new MutableContext();
+ assertThrows(TargetingKeyMissingError.class, () -> {
+ provider.getIntegerEvaluation("string-feature", 0, emptyEvaluationContext);
});
- assertThrows(UnsupportedOperationException.class, () -> {
- provider.getStringEvaluation("string-feature", "default", ctx);
+ evaluation = provider.getIntegerEvaluation("non-existing-feature", 0, ctx);
+ assertEquals(0, evaluation.getValue());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
+ }
+
+ @Test
+ public void testGetDoubleEvaluation() {
+ EvaluationContext ctx = new MutableContext("targetingKey");
+ ProviderEvaluation evaluation = provider.getDoubleEvaluation("double_feature_flag", 0.0, ctx);
+
+ assertEquals(1.5, evaluation.getValue());
+
+ MutableContext contextWithVarKey = new MutableContext("targetingKey");
+ contextWithVarKey.add("variableKey", "var_double");
+
+ evaluation = provider.getDoubleEvaluation("double_feature_flag", 0.0, contextWithVarKey);
+
+ assertEquals(2.5, evaluation.getValue());
+ assertEquals("var_double", evaluation.getVariant());
+ assertEquals(Reason.TARGETING_MATCH.name(), evaluation.getReason());
+
+ MutableContext contextWithNonExistingVarKey = new MutableContext("targetingKey");
+ contextWithNonExistingVarKey.add("variableKey", "non-existing-var");
+
+ evaluation = provider.getDoubleEvaluation("double_feature_flag", 0.0, contextWithNonExistingVarKey);
+
+ assertEquals(0.0, evaluation.getValue());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
+
+ EvaluationContext emptyEvaluationContext = new MutableContext();
+ assertThrows(TargetingKeyMissingError.class, () -> {
+ provider.getDoubleEvaluation("string-feature", 0.0, emptyEvaluationContext);
});
+
+ evaluation = provider.getDoubleEvaluation("non-existing-feature", 0.0, ctx);
+ assertEquals(0.0, evaluation.getValue());
+ assertEquals(null, evaluation.getVariant());
+ assertEquals(Reason.DEFAULT.name(), evaluation.getReason());
}
@SneakyThrows
diff --git a/providers/optimizely/src/test/resources/data.json b/providers/optimizely/src/test/resources/data.json
index ddc49f8d3..713a12cdc 100644
--- a/providers/optimizely/src/test/resources/data.json
+++ b/providers/optimizely/src/test/resources/data.json
@@ -16,7 +16,12 @@
"audienceIds": [],
"variations": [
{
- "variables": [],
+ "variables": [
+ {
+ "id": "boolean_variable_1",
+ "value": "true"
+ }
+ ],
"id": "boolean_feature_variation",
"key": "boolean_feature_variation",
"featureEnabled": true
@@ -81,6 +86,8 @@
"key": "boolean-feature"
},
{
+ "id": "boolean_feature_flag",
+ "key": "boolean_feature_flag",
"experimentIds": [],
"rolloutId": "string_feature_rollout",
"variables": [
@@ -89,10 +96,100 @@
"type": "string",
"id": "string_variable_1",
"key": "string_variable_1"
+ },
+ {
+ "id": "value",
+ "key": "value",
+ "defaultValue": "true",
+ "type": "boolean"
+ },
+ {
+ "id": "var_bool",
+ "key": "var_bool",
+ "defaultValue": "true",
+ "type": "boolean"
}
- ],
+ ]
+ },
+ {
"id": "string_feature_flag",
- "key": "string-feature"
+ "key": "string_feature_flag",
+ "experimentIds": [],
+ "rolloutId": "string_feature_rollout",
+ "variables": [
+ {
+ "id": "value",
+ "key": "value",
+ "defaultValue": "str1",
+ "type": "string"
+ },
+ {
+ "id": "var_str",
+ "key": "var_str",
+ "defaultValue": "str2",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "int_feature_flag",
+ "key": "int_feature_flag",
+ "experimentIds": [],
+ "rolloutId": "int_feature_rollout",
+ "variables": [
+ {
+ "id": "value",
+ "key": "value",
+ "defaultValue": "1",
+ "type": "integer"
+ },
+ {
+ "id": "var_int",
+ "key": "var_int",
+ "defaultValue": "2",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "double_feature_flag",
+ "key": "double_feature_flag",
+ "experimentIds": [],
+ "rolloutId": "double_feature_rollout",
+ "variables": [
+ {
+ "id": "value",
+ "key": "value",
+ "defaultValue": "1.5",
+ "type": "double"
+ },
+ {
+ "id": "var_double",
+ "key": "var_double",
+ "defaultValue": "2.5",
+ "type": "double"
+ }
+ ]
+ },
+ {
+ "id": "json_feature_flag",
+ "key": "json_feature_flag",
+ "experimentIds": [],
+ "rolloutId": "json_feature_rollout",
+ "variables": [
+ {
+ "id": "value",
+ "key": "value",
+ "defaultValue": "{\"key1\":\"value1\",\"key2\":2}",
+ "type": "json"
+ },
+ {
+ "id": "var_json",
+ "key": "var_json",
+ "defaultValue": "{\"key1\":\"value1a\",\"key2\":3}",
+ "type": "json"
+ }
+ ]
}
],
"experiments": [],