Skip to content
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

feat: Add OpenFeature provider #111

Merged
merged 29 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions .github/workflows/test-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@ jobs:
DEVCYCLE_SERVER_SDK_KEY: "${{ secrets.DEVCYCLE_SERVER_SDK_KEY }}"
- name: Run cloud bucketing example
run: ./gradlew runCloudExample
env:
DEVCYCLE_SERVER_SDK_KEY: "${{ secrets.DEVCYCLE_SERVER_SDK_KEY }}"
- name: Run OpenFeature example
run: ./gradlew runOpenFeatureExample
env:
DEVCYCLE_SERVER_SDK_KEY: "${{ secrets.DEVCYCLE_SERVER_SDK_KEY }}"
88 changes: 88 additions & 0 deletions OpenFeature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# DevCycle Java SDK OpenFeature Provider

This SDK provides a Java implementation of the [OpenFeature](https://openfeature.dev/) Provider interface.

## Example App

See the [example app](src/examples/java/com/devcycle/examples/OpenFeatureExample.java) for a working example of the DevCycle Java SDK OpenFeature Provider.

## Usage

Start by creating the appropriate DevCycle SDK client (`DevCycleLocalClient` or `DevCycleCloudClient`).

See our [Java Cloud Bucketing SDK](https://docs.devcycle.com/sdk/server-side-sdks/java-cloud) and [Java Local Bucketing SDK](https://docs.devcycle.com/sdk/server-side-sdks/java-local) documentation for more information on how to configure the SDK.

```java
// Initialize DevCycle Client
DevCycleLocalOptions options = DevCycleLocalOptions.builder().build();
DevCycleLocalClient devCycleClient = new DevCycleLocalClient("DEVCYCLE_SERVER_SDK_KEY", options);

// Set the initialzed DevCycle client as the provider for OpenFeature
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(devCycleClient.getOpenFeatureProvider());

// Get the OpenFeature client
Client openFeatureClient = api.getClient();

// Create the evaluation context to use for fetching variable values
EvaluationContext context = new MutableContext("user-1234");

// Retrieve a boolean flag from the OpenFeature client
Boolean variableValue = openFeatureClient.getBooleanValue(VARIABLE_KEY, false, context);
```

### Required Targeting Key

For DevCycle SDK to work we require either a `targeting key` or `user_id` attribute to be set on the OpenFeature context.
This value is used to identify the user as the `user_id` property for a `DevCycleUser` in DevCycle.

### Mapping Context Properties to DevCycleUser

The provider will automatically translate known `DevCycleUser` properties from the OpenFeature context to the `DevCycleUser` object.
[DevCycleUser Java Interface](https://github.com/DevCycleHQ/java-server-sdk/blob/main/src/main/java/com/devcycle/sdk/server/common/model/DevCycleUser.java)

For example all these properties will be set on the `DevCycleUser`:
```java
MutableContext context = new MutableContext("test-1234");
context.add("email", "email@devcycle.com");
context.add("name", "name");
context.add("country", "CA");
context.add("language", "en");
context.add("appVersion", "1.0.11");
context.add("appBuild", 1000);

Map<String,Object> customData = new LinkedHashMap<>();
customData.put("custom", "value");
context.add("customData", Structure.mapToStructure(customData));

Map<String,Object> privateCustomData = new LinkedHashMap<>();
privateCustomData.put("private", "data");
context.add("privateCustomData", Structure.mapToStructure(privateCustomData));
```

Context properties that are not known `DevCycleUser` properties will be automatically
added to the `customData` property of the `DevCycleUser`.

DevCycle allows the following data types for custom data values: **boolean**, **integer**, **double**, **float**, and **String**. Other data types will be ignored

### JSON Flag Limitations

The OpenFeature spec for JSON flags allows for any type of valid JSON value to be set as the flag value.

For example the following are all valid default value types to use with OpenFeature:
```java
// Invalid JSON values for the DevCycle SDK, will return defaults
openFeatureClient.getObjectValue("json-flag", new Value(new ArrayList<String>(Arrays.asList("value1", "value2"))));
openFeatureClient.getObjectValue("json-flag", new Value(610));
openFeatureClient.getObjectValue("json-flag", new Value(false));
openFeatureClient.getObjectValue("json-flag", new Value("string"));
openFeatureClient.getObjectValue("json-flag", new Value());
```

However, these are not valid types for the DevCycle SDK, the DevCycle SDK only supports JSON Objects:
```java

Map<String,Object> defaultJsonData = new LinkedHashMap<>();
defaultJsonData.put("default", "value");
openFeatureClient.getObjectValue("json-flag", new Value(Structure.mapToStructure(defaultJsonData)));
```
chris-hoefgen marked this conversation as resolved.
Show resolved Hide resolved
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ ldd --version

## Installation

### Gradle
You can use the SDK in your Gradle project by adding the following to *build.gradle*:

```yaml
implementation("com.devcycle:java-server-sdk:2.0.1")
```

### Maven

You can use the SDK in your Maven project by adding the following to your *pom.xml*:
Expand All @@ -40,13 +47,6 @@ You can use the SDK in your Maven project by adding the following to your *pom.x
</dependency>
```

### Gradle
Alternatively you can use the SDK in your Gradle project by adding the following to *build.gradle*:

```yaml
implementation("com.devcycle:java-server-sdk:2.0.1")
```

## DNS Caching
The JVM, by default, caches DNS for infinity. DevCycle servers are load balanced and dynamic. To address this concern,
setting the DNS cache TTL to a short duration is recommended. The TTL is controlled by this security setting `networkaddress.cache.ttl`.
Expand Down Expand Up @@ -84,9 +84,22 @@ public class MyClass {
}
```

## OpenFeature Support

This SDK provides an implementation of the [OpenFeature](https://openfeature.dev/) Provider interface. Use the `getOpenFeatureProvider()` method on the DevCycle SDK client to obtain a provider for OpenFeature.

```java
DevCycleLocalClient devCycleClient = new DevCycleLocalClient("DEVCYCLE_SERVER_SDK_KEY", options);
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(devCycleClient.getOpenFeatureProvider());
```

You can find instructions on how to use it here: [DevCycle Java SDK OpenFeature Provider](OpenFeature.md)


## Usage

To find usage documentation, visit our docs for [Local Bucketing](https://docs.devcycle.com/docs/sdk/server-side-sdks/java-local).
To find usage documentation, visit our docs for [Local Bucketing](https://docs.devcycle.com/docs/sdk/server-side-sdks/java-local) and [Cloud Bucketing](https://docs.devcycle.com/docs/sdk/server-side-sdks/java-cloud)

## Logging

Expand Down
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ ext {
junit_version = "4.13.2"
mockito_core_version = "5.6.0"
protobuf_version = "3.24.4"
openfeature_version = "1.6.1"
}

dependencies {
Expand All @@ -161,6 +162,8 @@ dependencies {

implementation("com.google.protobuf:protobuf-java:$protobuf_version")

implementation("dev.openfeature:sdk:$openfeature_version")

compileOnly("org.projectlombok:lombok:$lombok_version")

testAnnotationProcessor("org.projectlombok:lombok:$lombok_version")
Expand Down Expand Up @@ -196,3 +199,9 @@ task runCloudExample(type: JavaExec) {
classpath = sourceSets.examples.runtimeClasspath
main = 'com.devcycle.examples.CloudExample'
}

task runOpenFeatureExample(type: JavaExec) {
description = "Run the OpenFeature example"
classpath = sourceSets.examples.runtimeClasspath
main = 'com.devcycle.examples.OpenFeatureExample'
}
7 changes: 3 additions & 4 deletions src/examples/java/com/devcycle/examples/CloudExample.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.devcycle.sdk.server.cloud.api.DevCycleCloudClient;
import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions;
import com.devcycle.sdk.server.common.exception.DevCycleException;
import com.devcycle.sdk.server.common.model.DevCycleUser;

public class CloudExample {
Expand Down Expand Up @@ -34,16 +33,16 @@ public static void main(String[] args) throws InterruptedException {
Boolean variableValue = false;
try {
variableValue = client.variableValue(user, VARIABLE_KEY, defaultValue);
} catch(DevCycleException e) {
} catch (IllegalArgumentException e) {
chris-hoefgen marked this conversation as resolved.
Show resolved Hide resolved
System.err.println("Error fetching variable value: " + e.getMessage());
System.exit(1);
}

// Use variable value
if (variableValue) {
System.err.println("feature is enabled");
System.out.println("feature is enabled");
} else {
System.err.println("feature is NOT enabled");
System.out.println("feature is NOT enabled");
}
}
}
8 changes: 4 additions & 4 deletions src/examples/java/com/devcycle/examples/LocalExample.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.devcycle.examples;

import com.devcycle.sdk.server.common.model.DevCycleUser;
import com.devcycle.sdk.server.local.api.DevCycleLocalClient;
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
import com.devcycle.sdk.server.common.model.DevCycleUser;

public class LocalExample {
public static String VARIABLE_KEY = "test-boolean-variable";
Expand All @@ -29,7 +29,7 @@ public static void main(String[] args) throws InterruptedException {
DevCycleLocalClient client = new DevCycleLocalClient(server_sdk_key, options);

for (int i = 0; i < 10; i++) {
if(client.isInitialized()) {
if (client.isInitialized()) {
break;
}
Thread.sleep(500);
Expand All @@ -42,9 +42,9 @@ public static void main(String[] args) throws InterruptedException {

// Use variable value
if (variableValue) {
System.err.println("feature is enabled");
System.out.println("feature is enabled");
} else {
System.err.println("feature is NOT enabled");
System.out.println("feature is NOT enabled");
}
}
}
85 changes: 85 additions & 0 deletions src/examples/java/com/devcycle/examples/OpenFeatureExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.devcycle.examples;

import com.devcycle.sdk.server.local.api.DevCycleLocalClient;
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
import dev.openfeature.sdk.*;

import java.util.LinkedHashMap;
import java.util.Map;

public class OpenFeatureExample {
public static void main(String[] args) throws InterruptedException {
String server_sdk_key = System.getenv("DEVCYCLE_SERVER_SDK_KEY");
if (server_sdk_key == null) {
System.err.println("Please set the DEVCYCLE_SERVER_SDK_KEY environment variable");
System.exit(1);
}

DevCycleLocalOptions options = DevCycleLocalOptions.builder().configPollingIntervalMS(60000)
.disableAutomaticEventLogging(false).disableCustomEventLogging(false).build();

// Initialize DevCycle Client
DevCycleLocalClient devCycleClient = new DevCycleLocalClient(server_sdk_key, options);

for (int i = 0; i < 10; i++) {
if (devCycleClient.isInitialized()) {
break;
}
Thread.sleep(500);
}
Comment on lines +22 to +29
Copy link

@toddbaert toddbaert Nov 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on how DevCycle users already use the product, but if you want to avoid something like this in OpenFeature use-cases, you could implement the getState method, and return NOT_READY, then do something like this in the initialize method of the provider, and setting READY and returning when you have initialized.


// Setup OpenFeature with the DevCycle Provider
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(devCycleClient.getOpenFeatureProvider());

Client openFeatureClient = api.getClient();

// Create the evaluation context to use for fetching variable values
MutableContext context = new MutableContext("test-1234");
context.add("email", "test-user@domain.com");
context.add("name", "Test User");
context.add("language", "en");
context.add("country", "CA");
context.add("appVersion", "1.0.0");
context.add("appBuild", "1");
context.add("deviceModel", "Macbook");

// Add Devcycle Custom Data values
Map<String,Object> customData = new LinkedHashMap<>();
customData.put("custom", "value");
context.add("customData", Structure.mapToStructure(customData));

// Add Devcycle Private Custom Data values
Map<String,Object> privateCustomData = new LinkedHashMap<>();
privateCustomData.put("private", "data");
context.add("privateCustomData", Structure.mapToStructure(privateCustomData));

// The default value can be of type string, boolean, number, or JSON
Boolean defaultValue = false;

// Fetch variable values using the identifier key, with a default value and user
// object. The default value can be of type string, boolean, number, or JSON
Boolean variableValue = openFeatureClient.getBooleanValue("test-boolean-variable", defaultValue, context);

// Use variable value
if (variableValue) {
System.out.println("feature is enabled");
} else {
System.out.println("feature is NOT enabled");
}

// Default JSON objects must be a map of string to primitive values
Map<String, Object> defaultJsonData = new LinkedHashMap<>();
defaultJsonData.put("default", "value");

// Fetch a JSON object variable
Value jsonObject = openFeatureClient.getObjectValue("test-json-variable", new Value(Structure.mapToStructure(defaultJsonData)), context);
System.out.println(jsonObject.toString());

// Retrieving a string variable along with the resolution details
FlagEvaluationDetails<String> details = openFeatureClient.getStringDetails("doesnt-exist", "default", context);
System.out.println("Value: " + details.getValue());
System.out.println("Reason: " + details.getReason());

}
}
Loading
Loading