diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java index bb18ff3a5..fba29b9bf 100644 --- a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java +++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProvider.java @@ -10,6 +10,7 @@ import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidTypeInCache; import dev.openfeature.contrib.providers.gofeatureflag.hook.DataCollectorHook; import dev.openfeature.contrib.providers.gofeatureflag.hook.DataCollectorHookOptions; +import dev.openfeature.contrib.providers.gofeatureflag.hook.EnrichEvaluationContextHook; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.Hook; @@ -104,6 +105,7 @@ public void initialize(EvaluationContext evaluationContext) throws Exception { super.initialize(evaluationContext); this.gofeatureflagController = GoFeatureFlagController.builder().options(options).build(); + this.hooks.add(new EnrichEvaluationContextHook(options.getExporterMetadata())); if (options.getEnableCache() == null || options.getEnableCache()) { this.cacheCtrl = CacheController.builder().options(options).build(); diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/GoFeatureFlagController.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/GoFeatureFlagController.java index 1cd44a30f..c39bfee4b 100644 --- a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/GoFeatureFlagController.java +++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/GoFeatureFlagController.java @@ -90,7 +90,9 @@ public class GoFeatureFlagController { @Builder private GoFeatureFlagController(final GoFeatureFlagProviderOptions options) throws InvalidOptions { this.apiKey = options.getApiKey(); - this.exporterMetadata = options.getExporterMetadata(); + this.exporterMetadata = options.getExporterMetadata() == null ? new HashMap<>() : options.getExporterMetadata(); + this.exporterMetadata.put("provider", "java"); + this.exporterMetadata.put("openfeature", true); this.parsedEndpoint = HttpUrl.parse(options.getEndpoint()); if (this.parsedEndpoint == null) { diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/hook/EnrichEvaluationContextHook.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/hook/EnrichEvaluationContextHook.java new file mode 100644 index 000000000..d3c7d912d --- /dev/null +++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/hook/EnrichEvaluationContextHook.java @@ -0,0 +1,57 @@ +package dev.openfeature.contrib.providers.gofeatureflag.hook; + +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.HookContext; +import dev.openfeature.sdk.MutableContext; +import dev.openfeature.sdk.MutableStructure; +import dev.openfeature.sdk.Value; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * EnrichEvaluationContextHook is an OpenFeature Hook in charge of enriching the evaluation context. + */ +public class EnrichEvaluationContextHook implements Hook { + private final Map exporterMetadata; + + public EnrichEvaluationContextHook(Map exporterMetadata) { + this.exporterMetadata = Optional.ofNullable(exporterMetadata).orElseGet(HashMap::new); + } + + @Override + public Optional before(HookContext ctx, Map hints) { + if (ctx == null) { + return Optional.empty(); + } + + MutableContext mutableContext = + new MutableContext(ctx.getCtx().getTargetingKey(), ctx.getCtx().asMap()); + + MutableStructure metadata = new MutableStructure(); + for (Map.Entry entry : exporterMetadata.entrySet()) { + switch (entry.getValue().getClass().getSimpleName()) { + case "String": + metadata.add(entry.getKey(), (String) entry.getValue()); + break; + case "Boolean": + metadata.add(entry.getKey(), (Boolean) entry.getValue()); + break; + case "Integer": + metadata.add(entry.getKey(), (Integer) entry.getValue()); + break; + case "Double": + metadata.add(entry.getKey(), (Double) entry.getValue()); + break; + default: + throw new IllegalArgumentException( + "Unsupported type: " + entry.getValue().getClass().getSimpleName()); + } + } + Map expMetadata = new HashMap<>(); + expMetadata.put("exporterMetadata", new Value(metadata)); + mutableContext.add("gofeatureflag", new MutableStructure(expMetadata)); + return Optional.of(mutableContext); + } +} diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/hook/events/Events.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/hook/events/Events.java index ba4900923..c6742d08a 100644 --- a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/hook/events/Events.java +++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/hook/events/Events.java @@ -27,8 +27,6 @@ public class Events { */ public Events(List events, Map exporterMetadata) { this.events = new ArrayList<>(events); - this.meta.put("provider", "java"); - this.meta.put("openfeature", true); if (exporterMetadata != null) { this.meta.putAll(exporterMetadata); } diff --git a/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java b/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java index 5d529db79..38a81c60b 100644 --- a/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java +++ b/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.net.HttpHeaders; import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidEndpoint; @@ -21,6 +22,7 @@ import dev.openfeature.sdk.Value; import java.io.IOException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; @@ -48,12 +50,14 @@ class GoFeatureFlagProviderTest { private Map exporterMetadata; private int flagChangeCallCounter = 0; private boolean flagChanged404 = false; + private List requests = new ArrayList<>(); // Dispatcher is the configuration of the mock server to test the provider. final Dispatcher dispatcher = new Dispatcher() { @NotNull @SneakyThrows @Override public MockResponse dispatch(RecordedRequest request) { + requests.add(request); assert request.getPath() != null; if (request.getPath().contains("fail_500")) { return new MockResponse().setResponseCode(500); @@ -987,6 +991,42 @@ void should_send_exporter_metadata() { "we should have the exporter metadata in the last event sent to the data collector"); } + @SneakyThrows + @Test + void should_add_exporter_metadata_into_evaluation_call() { + Map customExporterMetadata = new HashMap<>(); + customExporterMetadata.put("version", "1.0.0"); + customExporterMetadata.put("intTest", 1234567890); + customExporterMetadata.put("doubleTest", 12345.67890); + GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder() + .endpoint(this.baseUrl.toString()) + .timeout(1000) + .enableCache(true) + .flushIntervalMs(150L) + .exporterMetadata(customExporterMetadata) + .build()); + String providerName = this.testName; + OpenFeatureAPI.getInstance().setProviderAndWait(providerName, g); + Client client = OpenFeatureAPI.getInstance().getClient(providerName); + client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext); + ObjectMapper objectMapper = new ObjectMapper(); + String want = objectMapper + .readValue( + "{ \"user\" : { \"key\" : \"d45e303a-38c2-11ed-a261-0242ac120002\", " + + "\"anonymous\" : false, \"custom\" : { \"firstname\" : \"john\", \"gofeatureflag\" : { " + + "\"exporterMetadata\" : { \"openfeature\" : true, \"provider\" : \"java\", \"doubleTest\" : 12345.6789, " + + "\"intTest\" : 1234567890, \"version\" : \"1.0.0\" } }, \"rate\" : 3.14, \"targetingKey\" : " + + "\"d45e303a-38c2-11ed-a261-0242ac120002\", \"company_info\" : { \"size\" : 120, \"name\" : \"my_company\" }, " + + "\"email\" : \"john.doe@gofeatureflag.org\", \"age\" : 30, \"lastname\" : \"doe\", \"professional\" : true, " + + "\"labels\" : [ \"pro\", \"beta\" ] } }, \"defaultValue\" : false }", + Object.class) + .toString(); + String got = objectMapper + .readValue(this.requests.get(0).getBody().readString(Charset.defaultCharset()), Object.class) + .toString(); + assertEquals(want, got, "we should have the exporter metadata in the last event sent to the data collector"); + } + private String readMockResponse(String filename) throws Exception { URL url = getClass().getClassLoader().getResource("mock_responses/" + filename); assert url != null;