From 579ec4e2d991463acd038702b4788786acacee7e Mon Sep 17 00:00:00 2001 From: liran2000 Date: Thu, 10 Jul 2025 12:03:23 +0300 Subject: [PATCH 1/9] init draft Signed-off-by: liran2000 --- .github/component_owners.yml | 2 + providers/optimizely/CHANGELOG.md | 1 + providers/optimizely/lombok.config | 5 + providers/optimizely/pom.xml | 45 +++++ .../optimizely/ContextTransformer.java | 32 ++++ .../optimizely/OptimizelyProvider.java | 165 ++++++++++++++++++ .../optimizely/OptimizelyProviderConfig.java | 34 ++++ .../optimizely/OptimizelyProviderTest.java | 143 +++++++++++++++ .../src/test/resources/log4j2-test.xml | 13 ++ providers/optimizely/version.txt | 1 + release-please-config.json | 11 ++ 11 files changed, 452 insertions(+) create mode 100644 providers/optimizely/CHANGELOG.md create mode 100644 providers/optimizely/lombok.config create mode 100644 providers/optimizely/pom.xml create mode 100644 providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/ContextTransformer.java create mode 100644 providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProvider.java create mode 100644 providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java create mode 100644 providers/optimizely/src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java create mode 100644 providers/optimizely/src/test/resources/log4j2-test.xml create mode 100644 providers/optimizely/version.txt diff --git a/.github/component_owners.yml b/.github/component_owners.yml index c5641fde7..8c31c9c0c 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -35,6 +35,8 @@ components: - novalisdenahi providers/statsig: - liran2000 + providers/optimizely: + - liran2000 providers/multiprovider: - liran2000 tools/flagd-http-connector: diff --git a/providers/optimizely/CHANGELOG.md b/providers/optimizely/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/providers/optimizely/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/providers/optimizely/lombok.config b/providers/optimizely/lombok.config new file mode 100644 index 000000000..bcd1afdae --- /dev/null +++ b/providers/optimizely/lombok.config @@ -0,0 +1,5 @@ +# This file is needed to avoid errors throw by findbugs when working with lombok. +lombok.addSuppressWarnings = true +lombok.addLombokGeneratedAnnotation = true +config.stopBubbling = true +lombok.extern.findbugs.addSuppressFBWarnings = true diff --git a/providers/optimizely/pom.xml b/providers/optimizely/pom.xml new file mode 100644 index 000000000..69e2edd35 --- /dev/null +++ b/providers/optimizely/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + dev.openfeature.contrib + parent + [1.0,2.0) + ../../pom.xml + + dev.openfeature.contrib.providers + optimizely + 0.0.1 + + optimizely + optimizely provider for Java + https://optimizely.com/ + + + + com.optimizely.ab + core-api + 4.2.2 + + + com.optimizely.ab + core-httpclient-impl + 4.2.2 + + + + org.slf4j + slf4j-api + 2.0.17 + + + + org.apache.logging.log4j + log4j-slf4j2-impl + 2.25.0 + test + + + + diff --git a/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/ContextTransformer.java b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/ContextTransformer.java new file mode 100644 index 000000000..ee8e7d9af --- /dev/null +++ b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/ContextTransformer.java @@ -0,0 +1,32 @@ +package dev.openfeature.contrib.providers.optimizely; + +import com.optimizely.ab.Optimizely; +import com.optimizely.ab.OptimizelyUserContext; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.exceptions.TargetingKeyMissingError; +import java.util.HashMap; +import java.util.Map; +import lombok.Builder; + +/** Transformer from OpenFeature context to OptimizelyUserContext. */ +@Builder +class ContextTransformer { + public static final String CONTEXT_APP_VERSION = "appVersion"; + public static final String CONTEXT_COUNTRY = "country"; + public static final String CONTEXT_EMAIL = "email"; + public static final String CONTEXT_IP = "ip"; + public static final String CONTEXT_LOCALE = "locale"; + public static final String CONTEXT_USER_AGENT = "userAgent"; + public static final String CONTEXT_PRIVATE_ATTRIBUTES = "privateAttributes"; + + private Optimizely optimizely; + + public OptimizelyUserContext transform(EvaluationContext ctx) { + if (ctx.getTargetingKey() == null) { + throw new TargetingKeyMissingError("targeting key is required."); + } + Map attributes = new HashMap<>(); + attributes.putAll(ctx.asObjectMap()); + return optimizely.createUserContext(ctx.getTargetingKey(), attributes); + } +} 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 new file mode 100644 index 000000000..cc137ddab --- /dev/null +++ b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProvider.java @@ -0,0 +1,165 @@ +package dev.openfeature.contrib.providers.optimizely; + +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.MutableContext; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Structure; +import dev.openfeature.sdk.Value; +import java.util.List; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +/** Provider implementation for Optimizely. */ +@Slf4j +public class OptimizelyProvider extends EventProvider { + + @Getter + private static final String NAME = "Optimizely"; + + private OptimizelyProviderConfig optimizelyProviderConfig; + + private Optimizely optimizely; + + private ContextTransformer contextTransformer; + + /** + * Constructor. + * + * @param optimizelyProviderConfig configuration for the provider + */ + public OptimizelyProvider(OptimizelyProviderConfig optimizelyProviderConfig) { + this.optimizelyProviderConfig = optimizelyProviderConfig; + } + + /** + * Initialize the provider. + * + * @param evaluationContext evaluation context + * @throws Exception on error + */ + @Override + public void initialize(EvaluationContext evaluationContext) throws Exception { + optimizely = Optimizely.builder() + .withConfigManager(optimizelyProviderConfig.getProjectConfigManager()) + .withEventProcessor(optimizelyProviderConfig.getEventProcessor()) + .withDatafile(optimizelyProviderConfig.getDatafile()) + .withDefaultDecideOptions(optimizelyProviderConfig.getDefaultDecideOptions()) + .withErrorHandler(optimizelyProviderConfig.getErrorHandler()) + .withODPManager(optimizelyProviderConfig.getOdpManager()) + .withUserProfileService(optimizelyProviderConfig.getUserProfileService()) + .build(); + contextTransformer = ContextTransformer.builder() + .optimizely(optimizely) + .build(); + log.info("finished initializing provider"); + } + + @Override + public Metadata getMetadata() { + return () -> NAME; + } + + @SneakyThrows + @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 = reasons == null ? null : String.join(", ", reasons); + } + + boolean enabled = decision.getEnabled(); + + return ProviderEvaluation.builder() + .value(enabled) + .reason(reasonsString) + .build(); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String 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 = reasons == null ? null : String.join(", ", reasons); + } + + String evaluatedValue = defaultValue; + boolean enabled = decision.getEnabled(); + if (enabled) { + evaluatedValue = variationKey; + } + + return ProviderEvaluation.builder() + .value(evaluatedValue) + .reason(reasonsString) + .build(); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + throw new UnsupportedOperationException("Integer evaluation is not supported by Optimizely provider."); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + throw new UnsupportedOperationException("Double evaluation is not supported by Optimizely provider."); + } + + @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 = reasons == null ? null : String.join(", ", reasons); + } + + Value evaluatedValue = defaultValue; + boolean enabled = decision.getEnabled(); + if (enabled) { + OptimizelyJSON variables = decision.getVariables(); + evaluatedValue = toValue(variables); + } + + return ProviderEvaluation.builder() + .value(evaluatedValue) + .reason(reasonsString) + .build(); + } + + + private Value toValue(OptimizelyJSON optimizelyJSON) { + MutableContext mutableContext = new MutableContext(); + if (optimizelyJSON != null) { + mutableContext.add("variables", Structure.mapToStructure(optimizelyJSON.toMap())); + } + return new Value(mutableContext); + } + + @SneakyThrows + @Override + public void shutdown() { + log.info("shutdown"); + if (optimizely != null) { + optimizely.close(); + } + } + +} diff --git a/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java new file mode 100644 index 000000000..15e00178e --- /dev/null +++ b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java @@ -0,0 +1,34 @@ +package dev.openfeature.contrib.providers.optimizely; + +import com.optimizely.ab.bucketing.Bucketer; +import com.optimizely.ab.bucketing.DecisionService; +import com.optimizely.ab.bucketing.UserProfileService; +import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.ProjectConfigManager; +import com.optimizely.ab.error.ErrorHandler; +import com.optimizely.ab.event.EventHandler; +import com.optimizely.ab.event.EventProcessor; +import com.optimizely.ab.notification.NotificationCenter; +import com.optimizely.ab.odp.ODPManager; +import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; +import lombok.Builder; +import lombok.Getter; +import java.util.List; + +/** Configuration for initializing statsig provider. */ +@Getter +@Builder +public class OptimizelyProviderConfig { + + private ProjectConfigManager projectConfigManager; + private EventHandler eventHandler; + private EventProcessor eventProcessor; + private String datafile; + private ErrorHandler errorHandler; + private ProjectConfig projectConfig; + private UserProfileService userProfileService; + private List defaultDecideOptions; + private ODPManager odpManager; + +} 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 new file mode 100644 index 000000000..79f8e2ac9 --- /dev/null +++ b/providers/optimizely/src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java @@ -0,0 +1,143 @@ +package dev.openfeature.contrib.providers.optimizely; + +import static org.assertj.core.api.Assertions.assertThat; +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.optimizely.ab.Optimizely; +import com.optimizely.ab.OptimizelyUserContext; +import com.optimizely.ab.bucketing.UserProfileService; +import com.optimizely.ab.config.ProjectConfigManager; +import com.optimizely.ab.error.ErrorHandler; +import com.optimizely.ab.event.EventProcessor; +import com.optimizely.ab.odp.ODPManager; +import com.optimizely.ab.optimizelydecision.OptimizelyDecision; +import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.MutableContext; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Value; +import java.util.List; +import java.util.Map; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +public class OptimizelyProviderTest { + + @Test + public void test_constructor_initializes_provider_with_valid_config() { + OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() + .projectConfigManager(mock(ProjectConfigManager.class)) + .eventProcessor(mock(EventProcessor.class)) + .datafile("test-datafile") + .build(); + + OptimizelyProvider provider = new OptimizelyProvider(config); + + assertThat(provider).isNotNull(); + assertEquals("Optimizely", provider.getMetadata().getName()); + } + + @Test + public void test_initialize_handles_null_configuration_parameters() { + OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() + .projectConfigManager(null) + .eventProcessor(null) + .datafile(null) + .build(); + + OptimizelyProvider provider = new OptimizelyProvider(config); + EvaluationContext evaluationContext = mock(EvaluationContext.class); + + assertDoesNotThrow(() -> { + provider.initialize(evaluationContext); + }); + } + + @Test + public void test_initialize_builds_optimizely_and_context_transformer() throws Exception { + OptimizelyProviderConfig config = mock(OptimizelyProviderConfig.class); + when(config.getProjectConfigManager()).thenReturn(mock(ProjectConfigManager.class)); + OptimizelyProvider provider = new OptimizelyProvider(config); + provider.initialize(mock(EvaluationContext.class)); + } + + @SneakyThrows + @Test + public void test_get_string_evaluation_returns_correct_value() { + OptimizelyProviderConfig config = mock(OptimizelyProviderConfig.class); + ContextTransformer contextTransformer = mock(ContextTransformer.class); + OptimizelyUserContext userContext = mock(OptimizelyUserContext.class); + OptimizelyDecision decision = mock(OptimizelyDecision.class); + + when(contextTransformer.transform(any(EvaluationContext.class))).thenReturn(userContext); + when(userContext.decide(anyString())).thenReturn(decision); + when(decision.getVariationKey()).thenReturn("variationKey"); + when(decision.getEnabled()).thenReturn(true); + + OptimizelyProvider provider = new OptimizelyProvider(config); + provider.initialize(new MutableContext()); + + EvaluationContext ctx = new MutableContext("targetingKey"); + ProviderEvaluation result = provider.getStringEvaluation("featureKey", "defaultValue", ctx); + + assertEquals("variationKey", result.getValue()); + + when(decision.getEnabled()).thenReturn(false); + result = provider.getStringEvaluation("featureKey", "defaultValue", ctx); + + assertEquals("defaultValue", result.getValue()); + } + + @SneakyThrows + @Test + public void test_get_object_evaluation_returns_transformed_variables() { + OptimizelyProviderConfig config = mock(OptimizelyProviderConfig.class); + ContextTransformer contextTransformer = mock(ContextTransformer.class); + OptimizelyUserContext userContext = mock(OptimizelyUserContext.class); + OptimizelyDecision decision = mock(OptimizelyDecision.class); + OptimizelyJSON optimizelyJSON = mock(OptimizelyJSON.class); + + when(contextTransformer.transform(any(EvaluationContext.class))).thenReturn(userContext); + when(userContext.decide(anyString())).thenReturn(decision); + when(decision.getEnabled()).thenReturn(true); + when(decision.getVariables()).thenReturn(optimizelyJSON); + when(optimizelyJSON.toMap()).thenReturn(Map.of("key", "value")); + + OptimizelyProvider provider = new OptimizelyProvider(config); + provider.initialize(mock(EvaluationContext.class)); + + EvaluationContext ctx = new MutableContext("targetingKey"); + ProviderEvaluation result = provider.getObjectEvaluation("featureKey", new Value(), ctx); + + assertNotNull(result.getValue()); + assertNotNull(result.getValue().asStructure().getValue("variables")); + } + + @Test + public void test_get_boolean_evaluation_handles_null_variation_key() { + OptimizelyProviderConfig config = mock(OptimizelyProviderConfig.class); + ContextTransformer contextTransformerMock = mock(ContextTransformer.class); + OptimizelyUserContext userContextMock = mock(OptimizelyUserContext.class); + OptimizelyDecision decisionMock = mock(OptimizelyDecision.class); + + when(contextTransformerMock.transform(any())).thenReturn(userContextMock); + when(userContextMock.decide(anyString())).thenReturn(decisionMock); + when(decisionMock.getVariationKey()).thenReturn(null); + when(decisionMock.getReasons()).thenReturn(List.of("reason1", "reason2")); + when(decisionMock.getEnabled()).thenReturn(false); + + OptimizelyProvider provider = new OptimizelyProvider(config); + + ProviderEvaluation evaluation = provider.getBooleanEvaluation("key", false, mock(EvaluationContext.class)); + + assertFalse(evaluation.getValue()); + assertEquals("reason1, reason2", evaluation.getReason()); + } +} diff --git a/providers/optimizely/src/test/resources/log4j2-test.xml b/providers/optimizely/src/test/resources/log4j2-test.xml new file mode 100644 index 000000000..aced30f8a --- /dev/null +++ b/providers/optimizely/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/providers/optimizely/version.txt b/providers/optimizely/version.txt new file mode 100644 index 000000000..8acdd82b7 --- /dev/null +++ b/providers/optimizely/version.txt @@ -0,0 +1 @@ +0.0.1 diff --git a/release-please-config.json b/release-please-config.json index 11e8fffb0..7450f6c89 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -103,6 +103,17 @@ "README.md" ] }, + "providers/optimizely": { + "package-name": "dev.openfeature.contrib.providers.optimizely", + "release-type": "simple", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [ + "pom.xml", + "README.md" + ] + }, "providers/multiprovider": { "package-name": "dev.openfeature.contrib.providers.multiprovider", "release-type": "simple", From 37c41e53b534a068ace89c7171198c7fdc8c1f7f Mon Sep 17 00:00:00 2001 From: liran2000 Date: Fri, 11 Jul 2025 12:12:00 +0300 Subject: [PATCH 2/9] cont. Signed-off-by: liran2000 --- CHANGELOG.md | 2 +- providers/optimizely/README.md | 61 +++++++++ .../optimizely/OptimizelyProviderConfig.java | 6 +- .../optimizely/OptimizelyProviderTest.java | 118 +++++------------- .../optimizely/src/test/resources/data.json | 105 ++++++++++++++++ .../src/test/resources/log4j2-test.xml | 2 +- .../providers/statsig/ContextTransformer.java | 2 +- 7 files changed, 202 insertions(+), 94 deletions(-) create mode 100644 providers/optimizely/README.md create mode 100644 providers/optimizely/src/test/resources/data.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a81cd8d90..0585c2eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -279,7 +279,7 @@ * **main:** release dev.openfeature.contrib.providers.go-feature-flag 0.4.3 ([#1246](https://github.com/open-feature/java-sdk-contrib/issues/1246)) ([b3bef6a](https://github.com/open-feature/java-sdk-contrib/commit/b3bef6a912080d79733ebb76b3acd4b08c132045)) * **main:** release dev.openfeature.contrib.providers.jsonlogic 1.1.0 ([#616](https://github.com/open-feature/java-sdk-contrib/issues/616)) ([67e8572](https://github.com/open-feature/java-sdk-contrib/commit/67e85726c114c4c14f17a1fb4fe53808b820bed1)) * **main:** release dev.openfeature.contrib.providers.jsonlogic 1.1.1 ([#838](https://github.com/open-feature/java-sdk-contrib/issues/838)) ([143ecb1](https://github.com/open-feature/java-sdk-contrib/commit/143ecb1183c1430b017537a317f8b606d6c9e124)) -* **main:** release dev.openfeature.contrib.providers.statsig 0.1.0 ([#700](https://github.com/open-feature/java-sdk-contrib/issues/700)) ([ac5851e](https://github.com/open-feature/java-sdk-contrib/commit/ac5851e2f0c7257418811626d7cd0ed32857c083)) +* **main:** release dev.openfeature.contrib.providers.optimizely 0.1.0 ([#700](https://github.com/open-feature/java-sdk-contrib/issues/700)) ([ac5851e](https://github.com/open-feature/java-sdk-contrib/commit/ac5851e2f0c7257418811626d7cd0ed32857c083)) * **main:** release dev.openfeature.contrib.providers.unleash 0.1.0-alpha ([#620](https://github.com/open-feature/java-sdk-contrib/issues/620)) ([12d06d2](https://github.com/open-feature/java-sdk-contrib/commit/12d06d2c2b5ac0d82433752c8995b7b7d1b3462a)) * **main:** release dev.openfeature.contrib.tools.junitopenfeature 0.0.2 ([#892](https://github.com/open-feature/java-sdk-contrib/issues/892)) ([0efebc7](https://github.com/open-feature/java-sdk-contrib/commit/0efebc7630657a3f398caadaf652e79b525e5ef9)) * **main:** release dev.openfeature.contrib.tools.junitopenfeature 0.0.3 ([#896](https://github.com/open-feature/java-sdk-contrib/issues/896)) ([1c23b15](https://github.com/open-feature/java-sdk-contrib/commit/1c23b156d09011216cb57450c3ce23a309d5e657)) diff --git a/providers/optimizely/README.md b/providers/optimizely/README.md new file mode 100644 index 000000000..16c419e92 --- /dev/null +++ b/providers/optimizely/README.md @@ -0,0 +1,61 @@ +# Unofficial Optimizely OpenFeature Provider for Java + +[optimizely](https://www.optimizely.com/optimization-glossary/feature-flags/) OpenFeature Provider can provide usage for optimizely via OpenFeature Java SDK. + +## Installation + + + +```xml + + + dev.openfeature.contrib.providers + optimizely + 0.0.1 + +``` + + + +## 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. + +## Usage +Optimizely OpenFeature Provider is based on [Optimizely Java SDK documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/java-sdk). + +### Usage Example + +```java +OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() + .eventProcessor(mock(EventProcessor.class)) + .datafile(dataFileContent) + .build(); + +provider = new OptimizelyProvider(config); +provider.initialize(new MutableContext("test-targeting-key")); + +ProviderEvaluation evaluation = provider.getBooleanEvaluation("string-feature", false, ctx); +System.out.println("Feature enabled: " + evaluation.getValue()); + +ProviderEvaluation result = provider.getObjectEvaluation("string-feature", new Value(), ctx); +System.out.println("Feature variable: " + result.getValue().asStructure().getValue("string_variable_1").asString()); +``` + +See [OptimizelyProviderTest](./src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java) +for more information. + +## Notes +Some Optimizely custom operations are supported from the optimizely client via: + +```java +provider.getOptimizely()... +``` + +## Optimizely Provider Tests Strategies + +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. diff --git a/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java index 15e00178e..0461e0663 100644 --- a/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java +++ b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java @@ -1,20 +1,16 @@ package dev.openfeature.contrib.providers.optimizely; -import com.optimizely.ab.bucketing.Bucketer; -import com.optimizely.ab.bucketing.DecisionService; import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.ProjectConfigManager; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.EventProcessor; -import com.optimizely.ab.notification.NotificationCenter; import com.optimizely.ab.odp.ODPManager; -import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; +import java.util.List; import lombok.Builder; import lombok.Getter; -import java.util.List; /** Configuration for initializing statsig provider. */ @Getter 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 79f8e2ac9..1ffde2ef9 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 @@ -3,33 +3,41 @@ import static org.assertj.core.api.Assertions.assertThat; 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.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import com.optimizely.ab.Optimizely; -import com.optimizely.ab.OptimizelyUserContext; -import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.config.ProjectConfigManager; -import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.event.EventProcessor; -import com.optimizely.ab.odp.ODPManager; -import com.optimizely.ab.optimizelydecision.OptimizelyDecision; -import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.Value; -import java.util.List; -import java.util.Map; +import java.io.File; import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; public class OptimizelyProviderTest { + private static OptimizelyProvider provider; + + @SneakyThrows + @BeforeAll + static void setUp() { + File dataFile = new File( + OptimizelyProviderTest.class.getClassLoader().getResource("data.json").getFile()); + String dataFileContent = new String(java.nio.file.Files.readAllBytes(dataFile.toPath())); + + OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() + .eventProcessor(mock(EventProcessor.class)) + .datafile(dataFileContent) + .build(); + + provider = new OptimizelyProvider(config); + provider.initialize(new MutableContext("test-targeting-key")); + } + @Test public void test_constructor_initializes_provider_with_valid_config() { OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() @@ -38,10 +46,10 @@ public void test_constructor_initializes_provider_with_valid_config() { .datafile("test-datafile") .build(); - OptimizelyProvider provider = new OptimizelyProvider(config); + OptimizelyProvider localProvider = new OptimizelyProvider(config); - assertThat(provider).isNotNull(); - assertEquals("Optimizely", provider.getMetadata().getName()); + assertThat(localProvider).isNotNull(); + assertEquals("Optimizely", localProvider.getMetadata().getName()); } @Test @@ -52,92 +60,30 @@ public void test_initialize_handles_null_configuration_parameters() { .datafile(null) .build(); - OptimizelyProvider provider = new OptimizelyProvider(config); + OptimizelyProvider localProvider = new OptimizelyProvider(config); EvaluationContext evaluationContext = mock(EvaluationContext.class); assertDoesNotThrow(() -> { - provider.initialize(evaluationContext); + localProvider.initialize(evaluationContext); }); } - @Test - public void test_initialize_builds_optimizely_and_context_transformer() throws Exception { - OptimizelyProviderConfig config = mock(OptimizelyProviderConfig.class); - when(config.getProjectConfigManager()).thenReturn(mock(ProjectConfigManager.class)); - OptimizelyProvider provider = new OptimizelyProvider(config); - provider.initialize(mock(EvaluationContext.class)); - } - - @SneakyThrows - @Test - public void test_get_string_evaluation_returns_correct_value() { - OptimizelyProviderConfig config = mock(OptimizelyProviderConfig.class); - ContextTransformer contextTransformer = mock(ContextTransformer.class); - OptimizelyUserContext userContext = mock(OptimizelyUserContext.class); - OptimizelyDecision decision = mock(OptimizelyDecision.class); - - when(contextTransformer.transform(any(EvaluationContext.class))).thenReturn(userContext); - when(userContext.decide(anyString())).thenReturn(decision); - when(decision.getVariationKey()).thenReturn("variationKey"); - when(decision.getEnabled()).thenReturn(true); - - OptimizelyProvider provider = new OptimizelyProvider(config); - provider.initialize(new MutableContext()); - - EvaluationContext ctx = new MutableContext("targetingKey"); - ProviderEvaluation result = provider.getStringEvaluation("featureKey", "defaultValue", ctx); - - assertEquals("variationKey", result.getValue()); - - when(decision.getEnabled()).thenReturn(false); - result = provider.getStringEvaluation("featureKey", "defaultValue", ctx); - - assertEquals("defaultValue", result.getValue()); - } - @SneakyThrows @Test public void test_get_object_evaluation_returns_transformed_variables() { - OptimizelyProviderConfig config = mock(OptimizelyProviderConfig.class); - ContextTransformer contextTransformer = mock(ContextTransformer.class); - OptimizelyUserContext userContext = mock(OptimizelyUserContext.class); - OptimizelyDecision decision = mock(OptimizelyDecision.class); - OptimizelyJSON optimizelyJSON = mock(OptimizelyJSON.class); - - when(contextTransformer.transform(any(EvaluationContext.class))).thenReturn(userContext); - when(userContext.decide(anyString())).thenReturn(decision); - when(decision.getEnabled()).thenReturn(true); - when(decision.getVariables()).thenReturn(optimizelyJSON); - when(optimizelyJSON.toMap()).thenReturn(Map.of("key", "value")); - - OptimizelyProvider provider = new OptimizelyProvider(config); - provider.initialize(mock(EvaluationContext.class)); - EvaluationContext ctx = new MutableContext("targetingKey"); - ProviderEvaluation result = provider.getObjectEvaluation("featureKey", new Value(), ctx); + ProviderEvaluation result = provider.getObjectEvaluation("string-feature", new Value(), ctx); assertNotNull(result.getValue()); - assertNotNull(result.getValue().asStructure().getValue("variables")); + assertEquals("string_feature_variation", result.getVariant()); + assertEquals("str1", result.getValue().asStructure().getValue("string_variable_1").asString()); } @Test public void test_get_boolean_evaluation_handles_null_variation_key() { - OptimizelyProviderConfig config = mock(OptimizelyProviderConfig.class); - ContextTransformer contextTransformerMock = mock(ContextTransformer.class); - OptimizelyUserContext userContextMock = mock(OptimizelyUserContext.class); - OptimizelyDecision decisionMock = mock(OptimizelyDecision.class); - - when(contextTransformerMock.transform(any())).thenReturn(userContextMock); - when(userContextMock.decide(anyString())).thenReturn(decisionMock); - when(decisionMock.getVariationKey()).thenReturn(null); - when(decisionMock.getReasons()).thenReturn(List.of("reason1", "reason2")); - when(decisionMock.getEnabled()).thenReturn(false); - - OptimizelyProvider provider = new OptimizelyProvider(config); - - ProviderEvaluation evaluation = provider.getBooleanEvaluation("key", false, mock(EvaluationContext.class)); + EvaluationContext ctx = new MutableContext("targetingKey"); + ProviderEvaluation evaluation = provider.getBooleanEvaluation("string-feature", false, ctx); - assertFalse(evaluation.getValue()); - assertEquals("reason1, reason2", evaluation.getReason()); + assertTrue(evaluation.getValue()); } } diff --git a/providers/optimizely/src/test/resources/data.json b/providers/optimizely/src/test/resources/data.json new file mode 100644 index 000000000..ddc49f8d3 --- /dev/null +++ b/providers/optimizely/src/test/resources/data.json @@ -0,0 +1,105 @@ +{ + "version": "4", + "rollouts": [ + { + "experiments": [ + { + "status": "Running", + "key": "boolean-feature-rollout", + "layerId": "boolean_feature_layer", + "trafficAllocation": [ + { + "entityId": "boolean_feature_variation", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "boolean_feature_variation", + "key": "boolean_feature_variation", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "id": "boolean_feature_rollout_experiment" + } + ], + "id": "boolean_feature_rollout" + }, + { + "experiments": [ + { + "status": "Running", + "key": "string-feature-rollout", + "layerId": "string_feature_layer", + "trafficAllocation": [ + { + "entityId": "string_feature_variation", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "variations": [ + { + "variables": [ + { + "id": "string_variable_1", + "value": "str1" + } + ], + "id": "string_feature_variation", + "key": "string_feature_variation", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "id": "string_feature_rollout_experiment" + } + ], + "id": "string_feature_rollout" + } + ], + "typedAudiences": [], + "anonymizeIP": false, + "projectId": "12345678901", + "variables": [ + { + "defaultValue": "str1", + "type": "string", + "id": "string_variable_1", + "key": "string_variable_1" + } + ], + "featureFlags": [ + { + "experimentIds": [], + "rolloutId": "boolean_feature_rollout", + "variables": [], + "id": "boolean_feature_flag", + "key": "boolean-feature" + }, + { + "experimentIds": [], + "rolloutId": "string_feature_rollout", + "variables": [ + { + "defaultValue": "str1", + "type": "string", + "id": "string_variable_1", + "key": "string_variable_1" + } + ], + "id": "string_feature_flag", + "key": "string-feature" + } + ], + "experiments": [], + "audiences": [], + "groups": [], + "attributes": [], + "accountId": "12345678901", + "events": [], + "revision": "1" +} diff --git a/providers/optimizely/src/test/resources/log4j2-test.xml b/providers/optimizely/src/test/resources/log4j2-test.xml index aced30f8a..a13aeb0ea 100644 --- a/providers/optimizely/src/test/resources/log4j2-test.xml +++ b/providers/optimizely/src/test/resources/log4j2-test.xml @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java index bc5d75ce5..f29add5d4 100644 --- a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java +++ b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java @@ -1,4 +1,4 @@ -package dev.openfeature.contrib.providers.statsig; +package dev.openfeature.contrib.providers.optimizely; import com.statsig.sdk.StatsigUser; import dev.openfeature.sdk.EvaluationContext; From 44f815826a45c984a43749d5180401fe415d77f1 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Fri, 11 Jul 2025 12:18:54 +0300 Subject: [PATCH 3/9] cont. Signed-off-by: liran2000 --- .../contrib/providers/statsig/ContextTransformer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java index f29add5d4..bc5d75ce5 100644 --- a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java +++ b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java @@ -1,4 +1,4 @@ -package dev.openfeature.contrib.providers.optimizely; +package dev.openfeature.contrib.providers.statsig; import com.statsig.sdk.StatsigUser; import dev.openfeature.sdk.EvaluationContext; From fc9568e614cafeca495713f07a7a0acaad88f299 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Fri, 11 Jul 2025 12:19:05 +0300 Subject: [PATCH 4/9] cont. Signed-off-by: liran2000 --- .../optimizely/OptimizelyProvider.java | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) 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 cc137ddab..0f2027927 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 @@ -7,11 +7,11 @@ import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.Metadata; -import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; import java.util.List; +import java.util.Map; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -25,6 +25,7 @@ public class OptimizelyProvider extends EventProvider { private OptimizelyProviderConfig optimizelyProviderConfig; + @Getter private Optimizely optimizely; private ContextTransformer contextTransformer; @@ -75,48 +76,33 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa String reasonsString = null; if (variationKey == null) { List reasons = decision.getReasons(); - reasonsString = reasons == null ? null : String.join(", ", reasons); + reasonsString = String.join(", ", reasons); } boolean enabled = decision.getEnabled(); - return ProviderEvaluation.builder() .value(enabled) .reason(reasonsString) .build(); } + @SneakyThrows @Override public ProviderEvaluation getStringEvaluation(String key, String 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 = reasons == null ? null : String.join(", ", reasons); - } - - String evaluatedValue = defaultValue; - boolean enabled = decision.getEnabled(); - if (enabled) { - evaluatedValue = variationKey; - } - - return ProviderEvaluation.builder() - .value(evaluatedValue) - .reason(reasonsString) - .build(); + throw new UnsupportedOperationException("String evaluation is not directly supported by Optimizely provider," + + "use getObjectEvaluation instead."); } @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { - throw new UnsupportedOperationException("Integer evaluation is not supported by Optimizely provider."); + throw new UnsupportedOperationException("Integer evaluation is not directly supported by Optimizely provider," + + "use getObjectEvaluation instead."); } @Override public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { - throw new UnsupportedOperationException("Double evaluation is not supported by Optimizely provider."); + throw new UnsupportedOperationException("Double evaluation is not directly supported by Optimizely provider," + + "use getObjectEvaluation instead."); } @SneakyThrows @@ -128,7 +114,7 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa String reasonsString = null; if (variationKey == null) { List reasons = decision.getReasons(); - reasonsString = reasons == null ? null : String.join(", ", reasons); + reasonsString = String.join(", ", reasons); } Value evaluatedValue = defaultValue; @@ -141,16 +127,16 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa return ProviderEvaluation.builder() .value(evaluatedValue) .reason(reasonsString) + .variant(variationKey) .build(); } + @SneakyThrows private Value toValue(OptimizelyJSON optimizelyJSON) { - MutableContext mutableContext = new MutableContext(); - if (optimizelyJSON != null) { - mutableContext.add("variables", Structure.mapToStructure(optimizelyJSON.toMap())); - } - return new Value(mutableContext); + Map map = optimizelyJSON.toMap(); + Structure structure = Structure.mapToStructure(map); + return new Value(structure); } @SneakyThrows From 6a3b7b8ee6ba51623ddafaef19d6de4750763582 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Fri, 11 Jul 2025 12:19:27 +0300 Subject: [PATCH 5/9] cont. Signed-off-by: liran2000 --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 10108b21e..aac1a040e 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,7 @@ providers/flipt providers/configcat providers/statsig + providers/optimizely providers/multiprovider tools/flagd-http-connector From 2725577369638de951b753d0878fde0976b4f8f8 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Fri, 11 Jul 2025 12:28:57 +0300 Subject: [PATCH 6/9] cont. Signed-off-by: liran2000 --- .../contrib/providers/optimizely/OptimizelyProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 0f2027927..25ee229b3 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 @@ -90,7 +90,7 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa @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."); + + "use getObjectEvaluation instead."); } @Override @@ -133,8 +133,8 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa @SneakyThrows - private Value toValue(OptimizelyJSON optimizelyJSON) { - Map map = optimizelyJSON.toMap(); + private Value toValue(OptimizelyJSON optimizelyJson) { + Map map = optimizelyJson.toMap(); Structure structure = Structure.mapToStructure(map); return new Value(structure); } From ef11e63ef9f083ebbb7bc8ce7fcda50ef1772b5a Mon Sep 17 00:00:00 2001 From: liran2000 Date: Fri, 11 Jul 2025 12:36:35 +0300 Subject: [PATCH 7/9] cont. Signed-off-by: liran2000 --- .../optimizely/OptimizelyProvider.java | 32 ++++++++----------- .../optimizely/OptimizelyProviderConfig.java | 1 - .../optimizely/OptimizelyProviderTest.java | 32 +++++++++++-------- 3 files changed, 32 insertions(+), 33 deletions(-) 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 25ee229b3..dae35cbd3 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 @@ -48,17 +48,15 @@ public OptimizelyProvider(OptimizelyProviderConfig optimizelyProviderConfig) { @Override public void initialize(EvaluationContext evaluationContext) throws Exception { optimizely = Optimizely.builder() - .withConfigManager(optimizelyProviderConfig.getProjectConfigManager()) - .withEventProcessor(optimizelyProviderConfig.getEventProcessor()) - .withDatafile(optimizelyProviderConfig.getDatafile()) - .withDefaultDecideOptions(optimizelyProviderConfig.getDefaultDecideOptions()) - .withErrorHandler(optimizelyProviderConfig.getErrorHandler()) - .withODPManager(optimizelyProviderConfig.getOdpManager()) - .withUserProfileService(optimizelyProviderConfig.getUserProfileService()) - .build(); - contextTransformer = ContextTransformer.builder() - .optimizely(optimizely) - .build(); + .withConfigManager(optimizelyProviderConfig.getProjectConfigManager()) + .withEventProcessor(optimizelyProviderConfig.getEventProcessor()) + .withDatafile(optimizelyProviderConfig.getDatafile()) + .withDefaultDecideOptions(optimizelyProviderConfig.getDefaultDecideOptions()) + .withErrorHandler(optimizelyProviderConfig.getErrorHandler()) + .withODPManager(optimizelyProviderConfig.getOdpManager()) + .withUserProfileService(optimizelyProviderConfig.getUserProfileService()) + .build(); + contextTransformer = ContextTransformer.builder().optimizely(optimizely).build(); log.info("finished initializing provider"); } @@ -90,7 +88,7 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa @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."); + + "use getObjectEvaluation instead."); } @Override @@ -125,13 +123,12 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa } return ProviderEvaluation.builder() - .value(evaluatedValue) - .reason(reasonsString) - .variant(variationKey) - .build(); + .value(evaluatedValue) + .reason(reasonsString) + .variant(variationKey) + .build(); } - @SneakyThrows private Value toValue(OptimizelyJSON optimizelyJson) { Map map = optimizelyJson.toMap(); @@ -147,5 +144,4 @@ public void shutdown() { optimizely.close(); } } - } diff --git a/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java index 0461e0663..08148f3ad 100644 --- a/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java +++ b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java @@ -26,5 +26,4 @@ public class OptimizelyProviderConfig { private UserProfileService userProfileService; private List defaultDecideOptions; private ODPManager odpManager; - } 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 1ffde2ef9..6eac1e8d2 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 @@ -25,14 +25,16 @@ public class OptimizelyProviderTest { @SneakyThrows @BeforeAll static void setUp() { - File dataFile = new File( - OptimizelyProviderTest.class.getClassLoader().getResource("data.json").getFile()); + File dataFile = new File(OptimizelyProviderTest.class + .getClassLoader() + .getResource("data.json") + .getFile()); String dataFileContent = new String(java.nio.file.Files.readAllBytes(dataFile.toPath())); OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() - .eventProcessor(mock(EventProcessor.class)) - .datafile(dataFileContent) - .build(); + .eventProcessor(mock(EventProcessor.class)) + .datafile(dataFileContent) + .build(); provider = new OptimizelyProvider(config); provider.initialize(new MutableContext("test-targeting-key")); @@ -41,10 +43,10 @@ static void setUp() { @Test public void test_constructor_initializes_provider_with_valid_config() { OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() - .projectConfigManager(mock(ProjectConfigManager.class)) - .eventProcessor(mock(EventProcessor.class)) - .datafile("test-datafile") - .build(); + .projectConfigManager(mock(ProjectConfigManager.class)) + .eventProcessor(mock(EventProcessor.class)) + .datafile("test-datafile") + .build(); OptimizelyProvider localProvider = new OptimizelyProvider(config); @@ -55,10 +57,10 @@ public void test_constructor_initializes_provider_with_valid_config() { @Test public void test_initialize_handles_null_configuration_parameters() { OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() - .projectConfigManager(null) - .eventProcessor(null) - .datafile(null) - .build(); + .projectConfigManager(null) + .eventProcessor(null) + .datafile(null) + .build(); OptimizelyProvider localProvider = new OptimizelyProvider(config); EvaluationContext evaluationContext = mock(EvaluationContext.class); @@ -76,7 +78,9 @@ public void test_get_object_evaluation_returns_transformed_variables() { assertNotNull(result.getValue()); assertEquals("string_feature_variation", result.getVariant()); - assertEquals("str1", result.getValue().asStructure().getValue("string_variable_1").asString()); + assertEquals( + "str1", + result.getValue().asStructure().getValue("string_variable_1").asString()); } @Test From bb3de86c3ce067bad7a116d13f635cf47340d540 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 12 Jul 2025 17:47:47 +0300 Subject: [PATCH 8/9] cont. Signed-off-by: liran2000 --- .../optimizely/OptimizelyProviderTest.java | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) 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 6eac1e8d2..d92fba154 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 @@ -3,7 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; 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; @@ -13,8 +15,10 @@ import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.Value; +import dev.openfeature.sdk.exceptions.TargetingKeyMissingError; import java.io.File; import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -41,7 +45,7 @@ static void setUp() { } @Test - public void test_constructor_initializes_provider_with_valid_config() { + public void testConstructorInitializesProviderWithValidConfig() { OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() .projectConfigManager(mock(ProjectConfigManager.class)) .eventProcessor(mock(EventProcessor.class)) @@ -55,7 +59,7 @@ public void test_constructor_initializes_provider_with_valid_config() { } @Test - public void test_initialize_handles_null_configuration_parameters() { + public void testInitializeHandlesNullConfigurationParameters() { OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() .projectConfigManager(null) .eventProcessor(null) @@ -72,7 +76,7 @@ public void test_initialize_handles_null_configuration_parameters() { @SneakyThrows @Test - public void test_get_object_evaluation_returns_transformed_variables() { + public void testGetObjectEvaluation() { EvaluationContext ctx = new MutableContext("targetingKey"); ProviderEvaluation result = provider.getObjectEvaluation("string-feature", new Value(), ctx); @@ -81,13 +85,50 @@ public void test_get_object_evaluation_returns_transformed_variables() { assertEquals( "str1", result.getValue().asStructure().getValue("string_variable_1").asString()); + + result = provider.getObjectEvaluation("non-existing-object-feature", new Value(), ctx); + assertNotNull(result.getReason()); } @Test - public void test_get_boolean_evaluation_handles_null_variation_key() { + public void testGetBooleanEvaluation() { EvaluationContext ctx = new MutableContext("targetingKey"); ProviderEvaluation evaluation = provider.getBooleanEvaluation("string-feature", false, ctx); assertTrue(evaluation.getValue()); + + EvaluationContext emptyEvaluationContext = new MutableContext(); + assertThrows(TargetingKeyMissingError.class, () -> { + provider.getBooleanEvaluation("string-feature", false, emptyEvaluationContext); + }); + + evaluation = provider.getBooleanEvaluation("non-existing-feature", false, ctx); + assertFalse(evaluation.getValue()); + assertNotNull(evaluation.getReason()); + } + + @Test + public void testUnsupportedEvaluations() { + EvaluationContext ctx = new MutableContext("targetingKey"); + + assertThrows(UnsupportedOperationException.class, () -> { + provider.getDoubleEvaluation("string-feature", 0.0, ctx); + }); + + assertThrows(UnsupportedOperationException.class, () -> { + provider.getIntegerEvaluation("string-feature", 0, ctx); + }); + + assertThrows(UnsupportedOperationException.class, () -> { + provider.getStringEvaluation("string-feature", "default", ctx); + }); + } + + @SneakyThrows + @AfterAll + static void tearDown() { + if (provider != null) { + provider.shutdown(); + } } } From 34d685cd173e1b08bbcbe76cb721c9d9cf0ef188 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Mon, 14 Jul 2025 18:45:42 +0300 Subject: [PATCH 9/9] cont. Signed-off-by: liran2000 --- CHANGELOG.md | 2 +- providers/optimizely/README.md | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0585c2eb0..a81cd8d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -279,7 +279,7 @@ * **main:** release dev.openfeature.contrib.providers.go-feature-flag 0.4.3 ([#1246](https://github.com/open-feature/java-sdk-contrib/issues/1246)) ([b3bef6a](https://github.com/open-feature/java-sdk-contrib/commit/b3bef6a912080d79733ebb76b3acd4b08c132045)) * **main:** release dev.openfeature.contrib.providers.jsonlogic 1.1.0 ([#616](https://github.com/open-feature/java-sdk-contrib/issues/616)) ([67e8572](https://github.com/open-feature/java-sdk-contrib/commit/67e85726c114c4c14f17a1fb4fe53808b820bed1)) * **main:** release dev.openfeature.contrib.providers.jsonlogic 1.1.1 ([#838](https://github.com/open-feature/java-sdk-contrib/issues/838)) ([143ecb1](https://github.com/open-feature/java-sdk-contrib/commit/143ecb1183c1430b017537a317f8b606d6c9e124)) -* **main:** release dev.openfeature.contrib.providers.optimizely 0.1.0 ([#700](https://github.com/open-feature/java-sdk-contrib/issues/700)) ([ac5851e](https://github.com/open-feature/java-sdk-contrib/commit/ac5851e2f0c7257418811626d7cd0ed32857c083)) +* **main:** release dev.openfeature.contrib.providers.statsig 0.1.0 ([#700](https://github.com/open-feature/java-sdk-contrib/issues/700)) ([ac5851e](https://github.com/open-feature/java-sdk-contrib/commit/ac5851e2f0c7257418811626d7cd0ed32857c083)) * **main:** release dev.openfeature.contrib.providers.unleash 0.1.0-alpha ([#620](https://github.com/open-feature/java-sdk-contrib/issues/620)) ([12d06d2](https://github.com/open-feature/java-sdk-contrib/commit/12d06d2c2b5ac0d82433752c8995b7b7d1b3462a)) * **main:** release dev.openfeature.contrib.tools.junitopenfeature 0.0.2 ([#892](https://github.com/open-feature/java-sdk-contrib/issues/892)) ([0efebc7](https://github.com/open-feature/java-sdk-contrib/commit/0efebc7630657a3f398caadaf652e79b525e5ef9)) * **main:** release dev.openfeature.contrib.tools.junitopenfeature 0.0.3 ([#896](https://github.com/open-feature/java-sdk-contrib/issues/896)) ([1c23b15](https://github.com/open-feature/java-sdk-contrib/commit/1c23b156d09011216cb57450c3ce23a309d5e657)) diff --git a/providers/optimizely/README.md b/providers/optimizely/README.md index 16c419e92..f83faa122 100644 --- a/providers/optimizely/README.md +++ b/providers/optimizely/README.md @@ -30,8 +30,6 @@ Optimizely OpenFeature Provider is based on [Optimizely Java SDK documentation]( ```java OptimizelyProviderConfig config = OptimizelyProviderConfig.builder() - .eventProcessor(mock(EventProcessor.class)) - .datafile(dataFileContent) .build(); provider = new OptimizelyProvider(config);