Skip to content
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
3 changes: 2 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@
**/DataStreams* @DataDog/data-streams-monitoring

# @DataDog/feature-flagging-and-experimentation-sdk
/products/feature-flagging/ @DataDog/feature-flagging-and-experimentation-sdk
/dd-smoke-tests/openfeature/ @DataDog/feature-flagging-and-experimentation-sdk
/products/feature-flagging/ @DataDog/feature-flagging-and-experimentation-sdk

# @DataDog/profiling-java
/dd-java-agent/agent-profiling/ @DataDog/profiling-java
Expand Down
38 changes: 38 additions & 0 deletions dd-smoke-tests/openfeature/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
id 'java'
id 'org.springframework.boot' version '2.7.15'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

ext {
minJavaVersionForTests = JavaVersion.VERSION_11
}

apply from: "$rootDir/gradle/java.gradle"
apply from: "$rootDir/gradle/spring-boot-plugin.gradle"
description = 'Open Feature provider Smoke Tests.'

tasks.named("compileJava", JavaCompile) {
configureCompiler(it, 11, JavaVersion.VERSION_11)
}

dependencies {
implementation project(':products:feature-flagging:api')
implementation 'org.springframework.boot:spring-boot-starter-web'

testImplementation project(':dd-smoke-tests')
testImplementation project(':products:feature-flagging:lib')
}

tasks.withType(Test).configureEach {
dependsOn "bootJar"
def bootJarTask = tasks.named('bootJar', BootJar)
jvmArgumentProviders.add(new CommandLineArgumentProvider() {
@Override
Iterable<String> asArguments() {
return bootJarTask.map { ["-Ddatadog.smoketest.springboot.shadowJar.path=${it.archiveFile.get()}"] }.get()
}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package datadog.smoketest.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootApplication {

public static void main(final String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package datadog.smoketest.springboot.openfeature;

import datadog.trace.api.openfeature.Provider;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.OpenFeatureAPI;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenFeatureConfiguration {

@Bean
public Client openFeatureClient() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProviderAndWait(new Provider());
return api.getClient();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package datadog.smoketest.springboot.openfeature;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.Structure;
import dev.openfeature.sdk.Value;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/openfeature")
public class OpenFeatureController {

private static final Logger LOGGER = LoggerFactory.getLogger(OpenFeatureController.class);

private final Client client;

public OpenFeatureController(final Client client) {
this.client = client;
}

@PostMapping(
value = "/evaluate",
consumes = APPLICATION_JSON_VALUE,
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> evaluate(@RequestBody final EvaluateRequest request) {
try {
final EvaluationContext context = context(request);
FlagEvaluationDetails<?> details;
switch (request.getVariationType()) {
case "BOOLEAN":
details =
client.getBooleanDetails(
request.getFlag(), (Boolean) request.getDefaultValue(), context);
break;
case "STRING":
details =
client.getStringDetails(
request.getFlag(), (String) request.getDefaultValue(), context);
break;
case "INTEGER":
final Number integerEval = (Number) request.getDefaultValue();
details = client.getIntegerDetails(request.getFlag(), integerEval.intValue(), context);
break;
case "NUMERIC":
final Number doubleEval = (Number) request.getDefaultValue();
details = client.getDoubleDetails(request.getFlag(), doubleEval.doubleValue(), context);
break;
case "JSON":
details =
client.getObjectDetails(
request.getFlag(), Value.objectToValue(request.getDefaultValue()), context);
break;
default:
throw new IllegalArgumentException(
"Unsupported variation type: " + request.getVariationType());
}

final Object value = details.getValue();
final Map<String, Object> result = new HashMap<>();
result.put("flagKey", details.getFlagKey());
result.put("variant", details.getVariant());
result.put("reason", details.getReason());
result.put("value", value instanceof Value ? context.convertValue((Value) value) : value);
result.put("errorCode", details.getErrorCode());
result.put("errorMessage", details.getErrorMessage());
result.put("flagMetadata", details.getFlagMetadata().asUnmodifiableMap());
return ResponseEntity.ok(result);
} catch (Throwable e) {
LOGGER.error("Error on resolution", e);
return ResponseEntity.internalServerError().body(e.getMessage());
}
}

private static EvaluationContext context(final EvaluateRequest request) {
final MutableContext context = new MutableContext();
context.setTargetingKey(request.getTargetingKey());
if (request.attributes != null) {
request.attributes.forEach(
(key, value) -> {
if (value instanceof Boolean) {
context.add(key, (Boolean) value);
} else if (value instanceof Integer) {
context.add(key, (Integer) value);
} else if (value instanceof Double) {
context.add(key, (Double) value);
} else if (value instanceof String) {
context.add(key, (String) value);
} else if (value instanceof Map) {
context.add(key, Value.objectToValue(value).asStructure());
} else if (value instanceof List) {
context.add(key, Value.objectToValue(value).asList());
} else {
context.add(key, (Structure) null);
}
});
}
return context;
}

public static class EvaluateRequest {
private String flag;
private String variationType;
private Object defaultValue;
private String targetingKey;
private Map<String, Object> attributes;

public Map<String, Object> getAttributes() {
return attributes;
}

public void setAttributes(Map<String, Object> attributes) {
this.attributes = attributes;
}

public Object getDefaultValue() {
return defaultValue;
}

public void setDefaultValue(Object defaultValue) {
this.defaultValue = defaultValue;
}

public String getFlag() {
return flag;
}

public void setFlag(String flag) {
this.flag = flag;
}

public String getTargetingKey() {
return targetingKey;
}

public void setTargetingKey(String targetingKey) {
this.targetingKey = targetingKey;
}

public String getVariationType() {
return variationType;
}

public void setVariationType(String variationType) {
this.variationType = variationType;
}
}
}
Loading