Skip to content

Commit 482daea

Browse files
liran2000chrfwow
andauthored
feat: Add Optimizely provider (#1510)
Signed-off-by: liran2000 <[email protected]> Co-authored-by: chrfwow <[email protected]>
1 parent e1d8e54 commit 482daea

File tree

14 files changed

+585
-0
lines changed

14 files changed

+585
-0
lines changed

.github/component_owners.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ components:
3535
- novalisdenahi
3636
providers/statsig:
3737
- liran2000
38+
providers/optimizely:
39+
- liran2000
3840
providers/multiprovider:
3941
- liran2000
4042
providers/ofrep-provider:

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<module>providers/flipt</module>
4040
<module>providers/configcat</module>
4141
<module>providers/statsig</module>
42+
<module>providers/optimizely</module>
4243
<module>providers/multiprovider</module>
4344
<module>providers/ofrep</module>
4445
<module>tools/flagd-http-connector</module>

providers/optimizely/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Changelog

providers/optimizely/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Unofficial Optimizely OpenFeature Provider for Java
2+
3+
[optimizely](https://www.optimizely.com/optimization-glossary/feature-flags/) OpenFeature Provider can provide usage for optimizely via OpenFeature Java SDK.
4+
5+
## Installation
6+
7+
<!-- x-release-please-start-version -->
8+
9+
```xml
10+
11+
<dependency>
12+
<groupId>dev.openfeature.contrib.providers</groupId>
13+
<artifactId>optimizely</artifactId>
14+
<version>0.0.1</version>
15+
</dependency>
16+
```
17+
18+
<!-- x-release-please-end-version -->
19+
20+
## Concepts
21+
22+
* Boolean evaluation gets feature [enabled](https://docs.developers.optimizely.com/feature-experimentation/docs/create-feature-flags) value.
23+
* Object evaluation gets a structure representing the evaluated variant variables.
24+
* String/Integer/Double evaluations evaluation are not directly supported by Optimizely provider, use getObjectEvaluation instead.
25+
26+
## Usage
27+
Optimizely OpenFeature Provider is based on [Optimizely Java SDK documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/java-sdk).
28+
29+
### Usage Example
30+
31+
```java
32+
OptimizelyProviderConfig config = OptimizelyProviderConfig.builder()
33+
.build();
34+
35+
provider = new OptimizelyProvider(config);
36+
provider.initialize(new MutableContext("test-targeting-key"));
37+
38+
ProviderEvaluation<Boolean> evaluation = provider.getBooleanEvaluation("string-feature", false, ctx);
39+
System.out.println("Feature enabled: " + evaluation.getValue());
40+
41+
ProviderEvaluation<Value> result = provider.getObjectEvaluation("string-feature", new Value(), ctx);
42+
System.out.println("Feature variable: " + result.getValue().asStructure().getValue("string_variable_1").asString());
43+
```
44+
45+
See [OptimizelyProviderTest](./src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java)
46+
for more information.
47+
48+
## Notes
49+
Some Optimizely custom operations are supported from the optimizely client via:
50+
51+
```java
52+
provider.getOptimizely()...
53+
```
54+
55+
## Optimizely Provider Tests Strategies
56+
57+
Unit test based on optimizely [Local Data File](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-java).
58+
See [OptimizelyProviderTest](./src/test/java/dev/openfeature/contrib/providers/optimizely/OptimizelyProviderTest.java)
59+
for more information.

providers/optimizely/lombok.config

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# This file is needed to avoid errors throw by findbugs when working with lombok.
2+
lombok.addSuppressWarnings = true
3+
lombok.addLombokGeneratedAnnotation = true
4+
config.stopBubbling = true
5+
lombok.extern.findbugs.addSuppressFBWarnings = true

providers/optimizely/pom.xml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
<parent>
6+
<groupId>dev.openfeature.contrib</groupId>
7+
<artifactId>parent</artifactId>
8+
<version>[1.0,2.0)</version>
9+
<relativePath>../../pom.xml</relativePath>
10+
</parent>
11+
<groupId>dev.openfeature.contrib.providers</groupId>
12+
<artifactId>optimizely</artifactId>
13+
<version>0.0.1</version> <!--x-release-please-version -->
14+
15+
<name>optimizely</name>
16+
<description>optimizely provider for Java</description>
17+
<url>https://optimizely.com/</url>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>com.optimizely.ab</groupId>
22+
<artifactId>core-api</artifactId>
23+
<version>4.2.2</version>
24+
</dependency>
25+
<dependency>
26+
<groupId>com.optimizely.ab</groupId>
27+
<artifactId>core-httpclient-impl</artifactId>
28+
<version>4.2.2</version>
29+
</dependency>
30+
31+
<dependency>
32+
<groupId>org.slf4j</groupId>
33+
<artifactId>slf4j-api</artifactId>
34+
<version>2.0.17</version>
35+
</dependency>
36+
37+
<dependency>
38+
<groupId>org.apache.logging.log4j</groupId>
39+
<artifactId>log4j-slf4j2-impl</artifactId>
40+
<version>2.25.0</version>
41+
<scope>test</scope>
42+
</dependency>
43+
44+
</dependencies>
45+
</project>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dev.openfeature.contrib.providers.optimizely;
2+
3+
import com.optimizely.ab.Optimizely;
4+
import com.optimizely.ab.OptimizelyUserContext;
5+
import dev.openfeature.sdk.EvaluationContext;
6+
import dev.openfeature.sdk.exceptions.TargetingKeyMissingError;
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
import lombok.Builder;
10+
11+
/** Transformer from OpenFeature context to OptimizelyUserContext. */
12+
@Builder
13+
class ContextTransformer {
14+
public static final String CONTEXT_APP_VERSION = "appVersion";
15+
public static final String CONTEXT_COUNTRY = "country";
16+
public static final String CONTEXT_EMAIL = "email";
17+
public static final String CONTEXT_IP = "ip";
18+
public static final String CONTEXT_LOCALE = "locale";
19+
public static final String CONTEXT_USER_AGENT = "userAgent";
20+
public static final String CONTEXT_PRIVATE_ATTRIBUTES = "privateAttributes";
21+
22+
private Optimizely optimizely;
23+
24+
public OptimizelyUserContext transform(EvaluationContext ctx) {
25+
if (ctx.getTargetingKey() == null) {
26+
throw new TargetingKeyMissingError("targeting key is required.");
27+
}
28+
Map<String, Object> attributes = new HashMap<>();
29+
attributes.putAll(ctx.asObjectMap());
30+
return optimizely.createUserContext(ctx.getTargetingKey(), attributes);
31+
}
32+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package dev.openfeature.contrib.providers.optimizely;
2+
3+
import com.optimizely.ab.Optimizely;
4+
import com.optimizely.ab.OptimizelyUserContext;
5+
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
6+
import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
7+
import dev.openfeature.sdk.EvaluationContext;
8+
import dev.openfeature.sdk.EventProvider;
9+
import dev.openfeature.sdk.Metadata;
10+
import dev.openfeature.sdk.ProviderEvaluation;
11+
import dev.openfeature.sdk.Structure;
12+
import dev.openfeature.sdk.Value;
13+
import java.util.List;
14+
import java.util.Map;
15+
import lombok.Getter;
16+
import lombok.SneakyThrows;
17+
import lombok.extern.slf4j.Slf4j;
18+
19+
/** Provider implementation for Optimizely. */
20+
@Slf4j
21+
public class OptimizelyProvider extends EventProvider {
22+
23+
@Getter
24+
private static final String NAME = "Optimizely";
25+
26+
private OptimizelyProviderConfig optimizelyProviderConfig;
27+
28+
@Getter
29+
private Optimizely optimizely;
30+
31+
private ContextTransformer contextTransformer;
32+
33+
/**
34+
* Constructor.
35+
*
36+
* @param optimizelyProviderConfig configuration for the provider
37+
*/
38+
public OptimizelyProvider(OptimizelyProviderConfig optimizelyProviderConfig) {
39+
this.optimizelyProviderConfig = optimizelyProviderConfig;
40+
}
41+
42+
/**
43+
* Initialize the provider.
44+
*
45+
* @param evaluationContext evaluation context
46+
* @throws Exception on error
47+
*/
48+
@Override
49+
public void initialize(EvaluationContext evaluationContext) throws Exception {
50+
optimizely = Optimizely.builder()
51+
.withConfigManager(optimizelyProviderConfig.getProjectConfigManager())
52+
.withEventProcessor(optimizelyProviderConfig.getEventProcessor())
53+
.withDatafile(optimizelyProviderConfig.getDatafile())
54+
.withDefaultDecideOptions(optimizelyProviderConfig.getDefaultDecideOptions())
55+
.withErrorHandler(optimizelyProviderConfig.getErrorHandler())
56+
.withODPManager(optimizelyProviderConfig.getOdpManager())
57+
.withUserProfileService(optimizelyProviderConfig.getUserProfileService())
58+
.build();
59+
contextTransformer = ContextTransformer.builder().optimizely(optimizely).build();
60+
log.info("finished initializing provider");
61+
}
62+
63+
@Override
64+
public Metadata getMetadata() {
65+
return () -> NAME;
66+
}
67+
68+
@SneakyThrows
69+
@Override
70+
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
71+
OptimizelyUserContext userContext = contextTransformer.transform(ctx);
72+
OptimizelyDecision decision = userContext.decide(key);
73+
String variationKey = decision.getVariationKey();
74+
String reasonsString = null;
75+
if (variationKey == null) {
76+
List<String> reasons = decision.getReasons();
77+
reasonsString = String.join(", ", reasons);
78+
}
79+
80+
boolean enabled = decision.getEnabled();
81+
return ProviderEvaluation.<Boolean>builder()
82+
.value(enabled)
83+
.reason(reasonsString)
84+
.build();
85+
}
86+
87+
@SneakyThrows
88+
@Override
89+
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
90+
throw new UnsupportedOperationException("String evaluation is not directly supported by Optimizely provider,"
91+
+ "use getObjectEvaluation instead.");
92+
}
93+
94+
@Override
95+
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
96+
throw new UnsupportedOperationException("Integer evaluation is not directly supported by Optimizely provider,"
97+
+ "use getObjectEvaluation instead.");
98+
}
99+
100+
@Override
101+
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
102+
throw new UnsupportedOperationException("Double evaluation is not directly supported by Optimizely provider,"
103+
+ "use getObjectEvaluation instead.");
104+
}
105+
106+
@SneakyThrows
107+
@Override
108+
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
109+
OptimizelyUserContext userContext = contextTransformer.transform(ctx);
110+
OptimizelyDecision decision = userContext.decide(key);
111+
String variationKey = decision.getVariationKey();
112+
String reasonsString = null;
113+
if (variationKey == null) {
114+
List<String> reasons = decision.getReasons();
115+
reasonsString = String.join(", ", reasons);
116+
}
117+
118+
Value evaluatedValue = defaultValue;
119+
boolean enabled = decision.getEnabled();
120+
if (enabled) {
121+
OptimizelyJSON variables = decision.getVariables();
122+
evaluatedValue = toValue(variables);
123+
}
124+
125+
return ProviderEvaluation.<Value>builder()
126+
.value(evaluatedValue)
127+
.reason(reasonsString)
128+
.variant(variationKey)
129+
.build();
130+
}
131+
132+
@SneakyThrows
133+
private Value toValue(OptimizelyJSON optimizelyJson) {
134+
Map<String, Object> map = optimizelyJson.toMap();
135+
Structure structure = Structure.mapToStructure(map);
136+
return new Value(structure);
137+
}
138+
139+
@SneakyThrows
140+
@Override
141+
public void shutdown() {
142+
log.info("shutdown");
143+
if (optimizely != null) {
144+
optimizely.close();
145+
}
146+
}
147+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.openfeature.contrib.providers.optimizely;
2+
3+
import com.optimizely.ab.bucketing.UserProfileService;
4+
import com.optimizely.ab.config.ProjectConfig;
5+
import com.optimizely.ab.config.ProjectConfigManager;
6+
import com.optimizely.ab.error.ErrorHandler;
7+
import com.optimizely.ab.event.EventHandler;
8+
import com.optimizely.ab.event.EventProcessor;
9+
import com.optimizely.ab.odp.ODPManager;
10+
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
11+
import java.util.List;
12+
import lombok.Builder;
13+
import lombok.Getter;
14+
15+
/** Configuration for initializing statsig provider. */
16+
@Getter
17+
@Builder
18+
public class OptimizelyProviderConfig {
19+
20+
private ProjectConfigManager projectConfigManager;
21+
private EventHandler eventHandler;
22+
private EventProcessor eventProcessor;
23+
private String datafile;
24+
private ErrorHandler errorHandler;
25+
private ProjectConfig projectConfig;
26+
private UserProfileService userProfileService;
27+
private List<OptimizelyDecideOption> defaultDecideOptions;
28+
private ODPManager odpManager;
29+
}

0 commit comments

Comments
 (0)