diff --git a/providers/configcat/src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java b/providers/configcat/src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java index 91ac4adfe..01ce867ed 100644 --- a/providers/configcat/src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java +++ b/providers/configcat/src/test/java/dev/openfeature/contrib/providers/configcat/ConfigCatProviderTest.java @@ -227,6 +227,7 @@ void contextTransformTest() { HashMap customMap = new HashMap<>(); customMap.put(customPropertyKey, customPropertyValue); + customMap.put("targetingKey", userId); User expectedUser = User.newBuilder().email(email).country(country).custom(customMap).build(userId); User transformedUser = ContextTransformer.transform(evaluationContext); diff --git a/providers/statsig/README.md b/providers/statsig/README.md index 96dfbdbb3..2cc653f0c 100644 --- a/providers/statsig/README.md +++ b/providers/statsig/README.md @@ -38,9 +38,10 @@ StatsigProviderConfig statsigProviderConfig = StatsigProviderConfig.builder().sd statsigProvider = new StatsigProvider(statsigProviderConfig); OpenFeatureAPI.getInstance().setProviderAndWait(statsigProvider); +MutableContext evaluationContext = new MutableContext(); +evaluationContext.setTargetingKey(TARGETING_KEY); boolean featureEnabled = client.getBooleanValue(FLAG_NAME, false); -MutableContext evaluationContext = new MutableContext(); MutableContext featureConfig = new MutableContext(); featureConfig.add("type", "CONFIG"); featureConfig.add("name", "product"); @@ -73,7 +74,4 @@ As it is limited, evaluation context based tests are limited. See [statsigProviderTest](./src/test/java/dev/openfeature/contrib/providers/statsig/StatsigProviderTest.java) for more information. -## Known issues -- Gate BooleanEvaluation with default value true cannot fallback to true. - https://github.com/statsig-io/java-server-sdk/issues/22 diff --git a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java index 1ae16bbb8..fc45b4168 100644 --- a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java +++ b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/ContextTransformer.java @@ -8,6 +8,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Transformer from OpenFeature context to statsig User. @@ -29,27 +30,30 @@ static StatsigUser transform(EvaluationContext ctx) { Map customMap = new HashMap<>(); ctx.asObjectMap().forEach((k, v) -> { switch (k) { + case "targetingKey": + user.setUserID(Objects.toString(v, null)); + break; case CONTEXT_APP_VERSION: - user.setAppVersion(String.valueOf(v)); + user.setAppVersion(Objects.toString(v, null)); break; case CONTEXT_COUNTRY: - user.setCountry(String.valueOf(v)); + user.setCountry(Objects.toString(v, null)); break; case CONTEXT_EMAIL: - user.setEmail(String.valueOf(v)); + user.setEmail(Objects.toString(v, null)); break; case CONTEXT_IP: - user.setIp(String.valueOf(v)); + user.setIp(Objects.toString(v, null)); break; case CONTEXT_USER_AGENT: - user.setUserAgent(String.valueOf(v)); + user.setUserAgent(Objects.toString(v, null)); break; case CONTEXT_LOCALE: - user.setLocale(String.valueOf(v)); + user.setLocale(Objects.toString(v, null)); break; default: if (!CONTEXT_PRIVATE_ATTRIBUTES.equals(k)) { - customMap.put(k, String.valueOf(v)); + customMap.put(k, Objects.toString(v, null)); } break; } diff --git a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProvider.java b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProvider.java index f70097678..e1baa4099 100644 --- a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProvider.java +++ b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProvider.java @@ -1,6 +1,8 @@ package dev.openfeature.contrib.providers.statsig; +import com.statsig.sdk.APIFeatureGate; import com.statsig.sdk.DynamicConfig; +import com.statsig.sdk.EvaluationReason; import com.statsig.sdk.Layer; import com.statsig.sdk.Statsig; import com.statsig.sdk.StatsigUser; @@ -14,6 +16,7 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.SneakyThrows; @@ -82,11 +85,22 @@ public Metadata getMetadata() { @SneakyThrows @Override + @SuppressFBWarnings(value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}, justification = "reason can be null") public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { verifyEvaluation(); StatsigUser user = ContextTransformer.transform(ctx); Boolean evaluatedValue = defaultValue; - try { + Value featureConfigValue = ctx.getValue(FEATURE_CONFIG_KEY); + String reason = null; + if (featureConfigValue == null) { + APIFeatureGate featureGate = Statsig.getFeatureGate(user, key); + reason = featureGate.getReason().getReason(); + + // in case of evaluation failure, remain with default value. + if (!assumeFailure(featureGate)) { + evaluatedValue = featureGate.getValue(); + } + } else { FeatureConfig featureConfig = parseFeatureConfig(ctx); switch (featureConfig.getType()) { case CONFIG: @@ -100,17 +114,26 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa default: break; } - } catch (Exception e) { - log.debug("could not fetch feature config. checking gate {}.", key); - Future featureOn = Statsig.checkGateAsync(user, key); - evaluatedValue = featureOn.get(); } return ProviderEvaluation.builder() .value(evaluatedValue) + .reason(reason) .build(); } + /* + https://github.com/statsig-io/java-server-sdk/issues/22#issuecomment-2002346349 + failure is assumed by reason, since success status is not returned. + */ + private boolean assumeFailure(APIFeatureGate featureGate) { + EvaluationReason reason = featureGate.getReason(); + return EvaluationReason.DEFAULT.equals(reason) + || EvaluationReason.UNINITIALIZED.equals(reason) + || EvaluationReason.UNRECOGNIZED.equals(reason) + || EvaluationReason.UNSUPPORTED.equals(reason); + } + @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { verifyEvaluation(); diff --git a/providers/statsig/src/test/java/dev/openfeature/contrib/providers/statsig/StatsigProviderTest.java b/providers/statsig/src/test/java/dev/openfeature/contrib/providers/statsig/StatsigProviderTest.java index b36c3a747..83efffc3a 100644 --- a/providers/statsig/src/test/java/dev/openfeature/contrib/providers/statsig/StatsigProviderTest.java +++ b/providers/statsig/src/test/java/dev/openfeature/contrib/providers/statsig/StatsigProviderTest.java @@ -6,6 +6,7 @@ import com.statsig.sdk.StatsigOptions; import com.statsig.sdk.StatsigUser; import dev.openfeature.sdk.Client; +import dev.openfeature.sdk.FlagEvaluationDetails; import dev.openfeature.sdk.ImmutableContext; import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.OpenFeatureAPI; @@ -54,6 +55,7 @@ class StatsigProviderTest { public static final Double DOUBLE_FLAG_VALUE = 3.14; public static final String USERS_FLAG_NAME = "userIdMatching"; public static final String PROPERTIES_FLAG_NAME = "emailMatching"; + public static final String TARGETING_KEY = "user1"; private static StatsigProvider statsigProvider; private static Client client; @@ -115,15 +117,18 @@ static void shutdown() { @Test void getBooleanEvaluation() { - assertEquals(true, statsigProvider.getBooleanEvaluation(FLAG_NAME, false, new ImmutableContext()).getValue()); - assertEquals(true, client.getBooleanValue(FLAG_NAME, false)); - assertEquals(false, statsigProvider.getBooleanEvaluation("non-existing", false, new ImmutableContext()).getValue()); - assertEquals(false, client.getBooleanValue("non-existing", false)); - - // expected to succeed when https://github.com/statsig-io/java-server-sdk/issues/22 is resolved and adopted -// assertEquals(true, client.getBooleanValue("non-existing", true)); + FlagEvaluationDetails flagEvaluationDetails = client.getBooleanDetails(FLAG_NAME, false, new ImmutableContext()); + assertEquals(false, flagEvaluationDetails.getValue()); + assertEquals("ERROR", flagEvaluationDetails.getReason()); MutableContext evaluationContext = new MutableContext(); + evaluationContext.setTargetingKey(TARGETING_KEY); + assertEquals(true, statsigProvider.getBooleanEvaluation(FLAG_NAME, false, evaluationContext).getValue()); + assertEquals(true, client.getBooleanValue(FLAG_NAME, false, evaluationContext)); + assertEquals(false, statsigProvider.getBooleanEvaluation("non-existing", false, evaluationContext).getValue()); + assertEquals(false, client.getBooleanValue("non-existing", false, evaluationContext)); + assertEquals(true, client.getBooleanValue("non-existing", true)); + MutableContext featureConfig = new MutableContext(); featureConfig.add("type", "CONFIG"); featureConfig.add("name", "product"); @@ -135,6 +140,7 @@ void getBooleanEvaluation() { @Test void getStringEvaluation() { MutableContext evaluationContext = new MutableContext(); + evaluationContext.setTargetingKey(TARGETING_KEY); MutableContext featureConfig = new MutableContext(); featureConfig.add("type", "CONFIG"); featureConfig.add("name", "product"); @@ -149,6 +155,7 @@ void getStringEvaluation() { @Test void getObjectConfigEvaluation() { MutableContext evaluationContext = new MutableContext(); + evaluationContext.setTargetingKey(TARGETING_KEY); MutableContext featureConfig = new MutableContext(); featureConfig.add("type", "CONFIG"); featureConfig.add("name", "object-config-name"); @@ -164,6 +171,7 @@ void getObjectConfigEvaluation() { @Test void getObjectLayerEvaluation() { MutableContext evaluationContext = new MutableContext(); + evaluationContext.setTargetingKey(TARGETING_KEY); MutableContext featureConfig = new MutableContext(); featureConfig.add("type", "LAYER"); featureConfig.add("name", "layer-name"); @@ -180,6 +188,7 @@ void getObjectLayerEvaluation() { @Test void getIntegerEvaluation() { MutableContext evaluationContext = new MutableContext(); + evaluationContext.setTargetingKey(TARGETING_KEY); MutableContext featureConfig = new MutableContext(); featureConfig.add("type", "CONFIG"); featureConfig.add("name", "product"); @@ -197,6 +206,7 @@ void getIntegerEvaluation() { @Test void getDoubleEvaluation() { MutableContext evaluationContext = new MutableContext(); + evaluationContext.setTargetingKey(TARGETING_KEY); MutableContext featureConfig = new MutableContext(); featureConfig.add("type", "CONFIG"); featureConfig.add("name", "product"); @@ -214,6 +224,7 @@ void getDoubleEvaluation() { @Test void getBooleanEvaluationByUser() { MutableContext evaluationContext = new MutableContext(); + evaluationContext.setTargetingKey(TARGETING_KEY); MutableContext featureConfig = new MutableContext(); featureConfig.add("type", "CONFIG"); featureConfig.add("name", "product");