From 0c87b3bb8628715c780834a278288d44f9763aec Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 17 Mar 2024 09:32:28 +0200 Subject: [PATCH 1/5] evaluate boolean updates Signed-off-by: liran2000 --- providers/statsig/README.md | 6 ++-- .../providers/statsig/ContextTransformer.java | 18 +++++++----- .../providers/statsig/StatsigProvider.java | 26 +++++++++++++---- .../statsig/StatsigProviderTest.java | 29 +++++++++++++++---- 4 files changed, 57 insertions(+), 22 deletions(-) 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..8be88f5c7 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; @@ -86,7 +88,16 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa 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); + if (assumeFailure(featureGate)) { + reason = featureGate.getReason().getReason(); + } else { + evaluatedValue = featureGate.getValue(); + } + } else { FeatureConfig featureConfig = parseFeatureConfig(ctx); switch (featureConfig.getType()) { case CONFIG: @@ -100,17 +111,22 @@ 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(); } + 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..f48584794 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,24 @@ 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)); + + // TODO issue + FlagEvaluationDetails flagEvaluationDetails = client.getBooleanDetails(FLAG_NAME, false, new ImmutableContext()); + assertEquals(false, flagEvaluationDetails.getValue()); + // TODO add reason + 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)); + + // 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)); + MutableContext featureConfig = new MutableContext(); featureConfig.add("type", "CONFIG"); featureConfig.add("name", "product"); @@ -135,6 +146,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 +161,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 +177,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 +194,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 +212,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 +230,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"); From 7403aad01c1aae7e9ade521b496fa79aebb88f08 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 17 Mar 2024 10:24:59 +0200 Subject: [PATCH 2/5] updates Signed-off-by: liran2000 --- .../contrib/providers/statsig/StatsigProvider.java | 12 +++++++----- .../providers/statsig/StatsigProviderTest.java | 3 +-- 2 files changed, 8 insertions(+), 7 deletions(-) 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 8be88f5c7..e130260ab 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 @@ -16,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; @@ -84,6 +85,7 @@ 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); @@ -92,7 +94,7 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa String reason = null; if (featureConfigValue == null) { APIFeatureGate featureGate = Statsig.getFeatureGate(user, key); - if (assumeFailure(featureGate)) { + if (featureGate.getReason() != null && assumeFailure(featureGate)) { reason = featureGate.getReason().getReason(); } else { evaluatedValue = featureGate.getValue(); @@ -121,10 +123,10 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa 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); + return EvaluationReason.DEFAULT.equals(reason) + || EvaluationReason.UNINITIALIZED.equals(reason) + || EvaluationReason.UNRECOGNIZED.equals(reason) + || EvaluationReason.UNSUPPORTED.equals(reason); } @Override 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 f48584794..3679d6e25 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 @@ -119,10 +119,9 @@ static void shutdown() { void getBooleanEvaluation() { - // TODO issue FlagEvaluationDetails flagEvaluationDetails = client.getBooleanDetails(FLAG_NAME, false, new ImmutableContext()); assertEquals(false, flagEvaluationDetails.getValue()); - // TODO add reason + // TODO add reason after https://github.com/open-feature/java-sdk/pull/849 MutableContext evaluationContext = new MutableContext(); From 93deaad622aaa1903ac97e38856daa20aafcfba3 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sat, 23 Mar 2024 19:29:27 +0200 Subject: [PATCH 3/5] update for java-sdk/releases/tag/v1.7.6 Signed-off-by: liran2000 --- .../contrib/providers/statsig/StatsigProviderTest.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 3679d6e25..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 @@ -117,12 +117,9 @@ static void shutdown() { @Test void getBooleanEvaluation() { - - FlagEvaluationDetails flagEvaluationDetails = client.getBooleanDetails(FLAG_NAME, false, new ImmutableContext()); assertEquals(false, flagEvaluationDetails.getValue()); - // TODO add reason after https://github.com/open-feature/java-sdk/pull/849 - + assertEquals("ERROR", flagEvaluationDetails.getReason()); MutableContext evaluationContext = new MutableContext(); evaluationContext.setTargetingKey(TARGETING_KEY); @@ -130,8 +127,6 @@ void getBooleanEvaluation() { 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)); - - // 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)); MutableContext featureConfig = new MutableContext(); From 6116a2f3d9ffdfca64384ae0d65a58d4db0c6db9 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 31 Mar 2024 09:13:59 +0300 Subject: [PATCH 4/5] minor updates Signed-off-by: liran2000 --- .../contrib/providers/statsig/StatsigProvider.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 e130260ab..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 @@ -94,9 +94,10 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa String reason = null; if (featureConfigValue == null) { APIFeatureGate featureGate = Statsig.getFeatureGate(user, key); - if (featureGate.getReason() != null && assumeFailure(featureGate)) { - reason = featureGate.getReason().getReason(); - } else { + reason = featureGate.getReason().getReason(); + + // in case of evaluation failure, remain with default value. + if (!assumeFailure(featureGate)) { evaluatedValue = featureGate.getValue(); } } else { @@ -121,6 +122,10 @@ public ProviderEvaluation getBooleanEvaluation(String key, Boolean defa .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) From 28b7eaf02944e3a6a1f8403cec560a81a57ec4d0 Mon Sep 17 00:00:00 2001 From: liran2000 Date: Sun, 31 Mar 2024 11:04:00 +0300 Subject: [PATCH 5/5] adjust ConfigCatProviderTest Signed-off-by: liran2000 --- .../contrib/providers/configcat/ConfigCatProviderTest.java | 1 + 1 file changed, 1 insertion(+) 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);