Skip to content

feat: Add Optimizely provider #1510

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ components:
- novalisdenahi
providers/statsig:
- liran2000
providers/optimizely:
- liran2000
providers/multiprovider:
- liran2000
tools/flagd-http-connector:
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<module>providers/flipt</module>
<module>providers/configcat</module>
<module>providers/statsig</module>
<module>providers/optimizely</module>
<module>providers/multiprovider</module>
<module>tools/flagd-http-connector</module>
</modules>
Expand Down
1 change: 1 addition & 0 deletions providers/optimizely/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
59 changes: 59 additions & 0 deletions providers/optimizely/README.md
Original file line number Diff line number Diff line change
@@ -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

<!-- x-release-please-start-version -->

```xml

<dependency>
<groupId>dev.openfeature.contrib.providers</groupId>
<artifactId>optimizely</artifactId>
<version>0.0.1</version>
</dependency>
```

<!-- x-release-please-end-version -->

## 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<Boolean> evaluation = provider.getBooleanEvaluation("string-feature", false, ctx);
System.out.println("Feature enabled: " + evaluation.getValue());

ProviderEvaluation<Value> 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.
5 changes: 5 additions & 0 deletions providers/optimizely/lombok.config
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions providers/optimizely/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.openfeature.contrib</groupId>
<artifactId>parent</artifactId>
<version>[1.0,2.0)</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<groupId>dev.openfeature.contrib.providers</groupId>
<artifactId>optimizely</artifactId>
<version>0.0.1</version> <!--x-release-please-version -->

<name>optimizely</name>
<description>optimizely provider for Java</description>
<url>https://optimizely.com/</url>

<dependencies>
<dependency>
<groupId>com.optimizely.ab</groupId>
<artifactId>core-api</artifactId>
<version>4.2.2</version>
</dependency>
<dependency>
<groupId>com.optimizely.ab</groupId>
<artifactId>core-httpclient-impl</artifactId>
<version>4.2.2</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.25.0</version>
<scope>test</scope>
</dependency>

</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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<String, Object> attributes = new HashMap<>();
attributes.putAll(ctx.asObjectMap());
return optimizely.createUserContext(ctx.getTargetingKey(), attributes);
}
}
Original file line number Diff line number Diff line change
@@ -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<Boolean> 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<String> reasons = decision.getReasons();
reasonsString = String.join(", ", reasons);
}

boolean enabled = decision.getEnabled();
return ProviderEvaluation.<Boolean>builder()
.value(enabled)
.reason(reasonsString)
.build();
}

@SneakyThrows
@Override
public ProviderEvaluation<String> 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<Integer> 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<Double> 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<Value> 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<String> 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.<Value>builder()
.value(evaluatedValue)
.reason(reasonsString)
.variant(variationKey)
.build();
}

@SneakyThrows
private Value toValue(OptimizelyJSON optimizelyJson) {
Map<String, Object> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<OptimizelyDecideOption> defaultDecideOptions;
private ODPManager odpManager;
}
Loading