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/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
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/README.md b/providers/optimizely/README.md
new file mode 100644
index 000000000..f83faa122
--- /dev/null
+++ b/providers/optimizely/README.md
@@ -0,0 +1,59 @@
+# 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()
+ .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/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..dae35cbd3
--- /dev/null
+++ b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProvider.java
@@ -0,0 +1,147 @@
+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.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;
+
+/** Provider implementation for Optimizely. */
+@Slf4j
+public class OptimizelyProvider extends EventProvider {
+
+ @Getter
+ private static final String NAME = "Optimizely";
+
+ private OptimizelyProviderConfig optimizelyProviderConfig;
+
+ @Getter
+ 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 = 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) {
+ 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 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 directly supported by Optimizely provider,"
+ + "use getObjectEvaluation instead.");
+ }
+
+ @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);
+ }
+
+ return ProviderEvaluation.builder()
+ .value(evaluatedValue)
+ .reason(reasonsString)
+ .variant(variationKey)
+ .build();
+ }
+
+ @SneakyThrows
+ private Value toValue(OptimizelyJSON optimizelyJson) {
+ Map map = optimizelyJson.toMap();
+ Structure structure = Structure.mapToStructure(map);
+ return new Value(structure);
+ }
+
+ @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..08148f3ad
--- /dev/null
+++ b/providers/optimizely/src/main/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderConfig.java
@@ -0,0 +1,29 @@
+package dev.openfeature.contrib.providers.optimizely;
+
+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.odp.ODPManager;
+import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
+import java.util.List;
+import lombok.Builder;
+import lombok.Getter;
+
+/** 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..d92fba154
--- /dev/null
+++ b/providers/optimizely/src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java
@@ -0,0 +1,134 @@
+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.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import com.optimizely.ab.config.ProjectConfigManager;
+import com.optimizely.ab.event.EventProcessor;
+import dev.openfeature.sdk.EvaluationContext;
+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;
+
+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 testConstructorInitializesProviderWithValidConfig() {
+ OptimizelyProviderConfig config = OptimizelyProviderConfig.builder()
+ .projectConfigManager(mock(ProjectConfigManager.class))
+ .eventProcessor(mock(EventProcessor.class))
+ .datafile("test-datafile")
+ .build();
+
+ OptimizelyProvider localProvider = new OptimizelyProvider(config);
+
+ assertThat(localProvider).isNotNull();
+ assertEquals("Optimizely", localProvider.getMetadata().getName());
+ }
+
+ @Test
+ public void testInitializeHandlesNullConfigurationParameters() {
+ OptimizelyProviderConfig config = OptimizelyProviderConfig.builder()
+ .projectConfigManager(null)
+ .eventProcessor(null)
+ .datafile(null)
+ .build();
+
+ OptimizelyProvider localProvider = new OptimizelyProvider(config);
+ EvaluationContext evaluationContext = mock(EvaluationContext.class);
+
+ assertDoesNotThrow(() -> {
+ localProvider.initialize(evaluationContext);
+ });
+ }
+
+ @SneakyThrows
+ @Test
+ public void testGetObjectEvaluation() {
+ EvaluationContext ctx = new MutableContext("targetingKey");
+ ProviderEvaluation result = provider.getObjectEvaluation("string-feature", new Value(), ctx);
+
+ assertNotNull(result.getValue());
+ assertEquals("string_feature_variation", result.getVariant());
+ 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 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();
+ }
+ }
+}
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
new file mode 100644
index 000000000..a13aeb0ea
--- /dev/null
+++ b/providers/optimizely/src/test/resources/log4j2-test.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
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",