diff --git a/providers/statsig/README.md b/providers/statsig/README.md index a1eb57ce3..a7e804d75 100644 --- a/providers/statsig/README.md +++ b/providers/statsig/README.md @@ -74,4 +74,12 @@ 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. +## Release Notes +### 0.3.0 + - Migrated to Java Core according to [Migration guide](https://docs.statsig.com/server-core/migration-guides/java#java-migration-steps). + + The provider usage is basically unchanged, the underlying implementation is changed. + As the initialization code may change, can refer to the migration guide for details. + +- group and secondaryExposures usage removed. diff --git a/providers/statsig/pom.xml b/providers/statsig/pom.xml index e7f6d79cb..b8890c5fc 100644 --- a/providers/statsig/pom.xml +++ b/providers/statsig/pom.xml @@ -10,7 +10,7 @@ dev.openfeature.contrib.providers statsig - 0.2.1 + 0.3.0 statsig Statsig provider for Java @@ -19,8 +19,9 @@ com.statsig - serversdk - 1.18.1 + javacore + 0.12.1 + uber @@ -36,5 +37,12 @@ test + + org.mockito + mockito-core + 5.20.0 + test + + 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 bc5d75ce5..2b0faf3d0 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 @@ -1,6 +1,6 @@ package dev.openfeature.contrib.providers.statsig; -import com.statsig.sdk.StatsigUser; +import com.statsig.StatsigUser; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; @@ -23,7 +23,7 @@ static StatsigUser transform(EvaluationContext ctx) { if (ctx.getTargetingKey() == null) { throw new TargetingKeyMissingError("targeting key is required."); } - StatsigUser user = new StatsigUser(ctx.getTargetingKey()); + StatsigUser.Builder user = new StatsigUser.Builder().setUserID(ctx.getTargetingKey()); Map customMap = new HashMap<>(); ctx.asObjectMap().forEach((k, v) -> { switch (k) { @@ -64,6 +64,6 @@ static StatsigUser transform(EvaluationContext ctx) { privateAttributesStructure.asObjectMap().forEach((k, v) -> privateMap.put(k, String.valueOf(v))); user.setPrivateAttributes(privateMap); } - return user; + return user.build(); } } 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 8c1dd5a8f..70d1adef5 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,11 +1,10 @@ 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; +import com.statsig.DynamicConfig; +import com.statsig.FeatureGate; +import com.statsig.Layer; +import com.statsig.Statsig; +import com.statsig.StatsigUser; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; import dev.openfeature.sdk.Metadata; @@ -13,15 +12,11 @@ import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Future; +import java.util.concurrent.CompletableFuture; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; /** Provider implementation for Statsig. */ @Slf4j @@ -33,6 +28,9 @@ public class StatsigProvider extends EventProvider { private static final String FEATURE_CONFIG_KEY = "feature_config"; private final StatsigProviderConfig statsigProviderConfig; + @Getter + private Statsig statsig; + /** * Constructor. * @@ -50,8 +48,8 @@ public StatsigProvider(StatsigProviderConfig statsigProviderConfig) { */ @Override public void initialize(EvaluationContext evaluationContext) throws Exception { - Future initFuture = - Statsig.initializeAsync(statsigProviderConfig.getSdkKey(), statsigProviderConfig.getOptions()); + statsig = new Statsig(statsigProviderConfig.getSdkKey(), statsigProviderConfig.getOptions()); + CompletableFuture initFuture = statsig.initialize(); initFuture.get(); statsigProviderConfig.postInit(); @@ -65,22 +63,15 @@ 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) { StatsigUser user = ContextTransformer.transform(ctx); Boolean evaluatedValue = defaultValue; 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(); - } + FeatureGate featureGate = statsig.getFeatureGate(user, key); + reason = featureGate.getEvaluationDetails().getReason(); + evaluatedValue = featureGate.getValue(); } else { FeatureConfig featureConfig = parseFeatureConfig(ctx); switch (featureConfig.getType()) { @@ -103,18 +94,6 @@ 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) - || EvaluationReason.UNINITIALIZED.equals(reason) - || EvaluationReason.UNRECOGNIZED.equals(reason) - || EvaluationReason.UNSUPPORTED.equals(reason); - } - @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { StatsigUser user = ContextTransformer.transform(ctx); @@ -198,12 +177,12 @@ public ProviderEvaluation getObjectEvaluation(String key, Value defaultVa @SneakyThrows protected DynamicConfig fetchDynamicConfig(StatsigUser user, FeatureConfig featureConfig) { - return Statsig.getConfigAsync(user, featureConfig.getName()).get(); + return statsig.getDynamicConfig(user, featureConfig.getName()); } @SneakyThrows protected Layer fetchLayer(StatsigUser user, FeatureConfig featureConfig) { - return Statsig.getLayerAsync(user, featureConfig.getName()).get(); + return statsig.getLayer(user, featureConfig.getName()); } private Value toValue(DynamicConfig dynamicConfig) { @@ -211,13 +190,6 @@ private Value toValue(DynamicConfig dynamicConfig) { mutableContext.add("name", dynamicConfig.getName()); mutableContext.add("value", Structure.mapToStructure(dynamicConfig.getValue())); mutableContext.add("ruleID", dynamicConfig.getRuleID()); - mutableContext.add("groupName", dynamicConfig.getGroupName()); - List secondaryExposures = new ArrayList<>(); - dynamicConfig.getSecondaryExposures().forEach(secondaryExposure -> { - Value value = Value.objectToValue(secondaryExposure); - secondaryExposures.add(value); - }); - mutableContext.add("secondaryExposures", secondaryExposures); return new Value(mutableContext); } @@ -227,17 +199,11 @@ private Value toValue(Layer layer) { mutableContext.add("value", Structure.mapToStructure(layer.getValue())); mutableContext.add("ruleID", layer.getRuleID()); mutableContext.add("groupName", layer.getGroupName()); - List secondaryExposures = new ArrayList<>(); - layer.getSecondaryExposures().forEach(secondaryExposure -> { - Value value = Value.objectToValue(secondaryExposure); - secondaryExposures.add(value); - }); - mutableContext.add("secondaryExposures", secondaryExposures); - mutableContext.add("allocatedExperiment", layer.getAllocatedExperiment()); + mutableContext.add("allocatedExperiment", layer.getAllocatedExperimentName()); return new Value(mutableContext); } - @NotNull private static FeatureConfig parseFeatureConfig(EvaluationContext ctx) { + private static FeatureConfig parseFeatureConfig(EvaluationContext ctx) { Value featureConfigValue = ctx.getValue(FEATURE_CONFIG_KEY); if (featureConfigValue == null) { throw new IllegalArgumentException("feature config not found at evaluation context."); @@ -262,8 +228,12 @@ private Value toValue(Layer layer) { @SneakyThrows @Override public void shutdown() { - log.info("shutdown"); - Statsig.shutdown(); + log.info("shutdown begin"); + if (statsig != null) { + CompletableFuture shutdownFuture = statsig.shutdown(); + shutdownFuture.get(); + } + log.info("shutdown end"); } /** Feature config, as required for evaluation. */ diff --git a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProviderConfig.java b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProviderConfig.java index 0f43402f6..bf1e90c6a 100644 --- a/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProviderConfig.java +++ b/providers/statsig/src/main/java/dev/openfeature/contrib/providers/statsig/StatsigProviderConfig.java @@ -1,6 +1,6 @@ package dev.openfeature.contrib.providers.statsig; -import com.statsig.sdk.StatsigOptions; +import com.statsig.StatsigOptions; import lombok.Builder; import lombok.Getter; @@ -10,7 +10,7 @@ public class StatsigProviderConfig { @Builder.Default - private StatsigOptions options = new StatsigOptions(); + private StatsigOptions options = new StatsigOptions.Builder().build(); // Only holding temporary for initialization private String sdkKey; 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 f13155159..bdb42e54b 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 @@ -10,14 +10,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -import com.statsig.sdk.DynamicConfig; -import com.statsig.sdk.Layer; -import com.statsig.sdk.Statsig; -import com.statsig.sdk.StatsigOptions; -import com.statsig.sdk.StatsigUser; +import com.statsig.DynamicConfig; +import com.statsig.Layer; +import com.statsig.StatsigOptions; +import com.statsig.StatsigUser; import dev.openfeature.sdk.Client; import dev.openfeature.sdk.FlagEvaluationDetails; import dev.openfeature.sdk.ImmutableContext; @@ -60,12 +60,12 @@ class StatsigProviderTest { @BeforeAll static void setUp() { String sdkKey = "test"; - StatsigOptions statsigOptions = new StatsigOptions(); - statsigOptions.setLocalMode(true); + StatsigOptions statsigOptions = new StatsigOptions.Builder().build(); StatsigProviderConfig statsigProviderConfig = StatsigProviderConfig.builder() .sdkKey(sdkKey) .options(statsigOptions) .build(); + statsigProvider = spy(new StatsigProvider(statsigProviderConfig)); OpenFeatureAPI.getInstance().setProviderAndWait(statsigProvider); client = OpenFeatureAPI.getInstance().getClient(); @@ -74,23 +74,22 @@ static void setUp() { @SneakyThrows private static void buildFlags() { - Statsig.overrideGate(FLAG_NAME, true); + statsigProvider.getStatsig().overrideGate(FLAG_NAME, true); Map configMap = new HashMap<>(); configMap.put("boolean", true); configMap.put("alias", "test"); configMap.put("revision", INT_FLAG_VALUE); configMap.put("price", DOUBLE_FLAG_VALUE); - Statsig.overrideConfig("product", configMap); - Statsig.overrideLayer("product", configMap); + statsigProvider.getStatsig().overrideDynamicConfig("product", configMap); + statsigProvider.getStatsig().overrideLayer("product", configMap); ArrayList> secondaryExposures = new ArrayList<>(); secondaryExposures.add(Collections.singletonMap("test-exposure", "test-exposure-value")); - DynamicConfig dynamicConfig = new DynamicConfig( - "object-config-name", - Collections.singletonMap("value-key", "test-value"), - "test-rule-id", - "test-group-name", - secondaryExposures); + + DynamicConfig dynamicConfig = mock(DynamicConfig.class); + when(dynamicConfig.getName()).thenReturn("object-config-name"); + when(dynamicConfig.getValue()).thenReturn(Collections.singletonMap("value-key", "test-value")); + when(dynamicConfig.getRuleID()).thenReturn("test-rule-id"); doAnswer(invocation -> { if ("object-config-name" @@ -104,14 +103,12 @@ private static void buildFlags() { .when(statsigProvider) .fetchDynamicConfig(any(), any()); - Layer layer = new Layer( - "layer-name", - "test-rule-id", - "test-group-name", - Collections.singletonMap("value-key", "test-value"), - secondaryExposures, - "allocated", - null); + // Mock Layer + Layer layer = mock(Layer.class); + when(layer.getName()).thenReturn("layer-name"); + when(layer.getValue()).thenReturn(Collections.singletonMap("value-key", "test-value")); + when(layer.getRuleID()).thenReturn("test-rule-id"); + doAnswer(invocation -> { if ("layer-name" .equals(invocation @@ -137,6 +134,12 @@ void getBooleanEvaluation() { assertEquals(false, flagEvaluationDetails.getValue()); assertEquals("ERROR", flagEvaluationDetails.getReason()); + boolean res = statsigProvider + .getStatsig() + .checkGate(new StatsigUser.Builder().setUserID(TARGETING_KEY).build(), FLAG_NAME); + + System.out.println("Overridden flag evaluation: " + res); + MutableContext evaluationContext = new MutableContext(); evaluationContext.setTargetingKey(TARGETING_KEY); assertEquals( @@ -197,8 +200,8 @@ void getObjectConfigEvaluation() { .getObjectEvaluation("dummy", new Value("fallback"), evaluationContext) .getValue(); - String expectedObjectEvaluation = "{groupName=test-group-name, name=object-config-name, secondaryExposures=" - + "[{test-exposure=test-exposure-value}], ruleID=test-rule-id, value={value-key=test-value}}"; + String expectedObjectEvaluation = + "{name=object-config-name, ruleID=test-rule-id, value={value-key=test-value}}"; assertEquals( expectedObjectEvaluation, objectEvaluation.asStructure().asObjectMap().toString()); @@ -216,9 +219,9 @@ void getObjectLayerEvaluation() { .getObjectEvaluation("dummy", new Value("fallback"), evaluationContext) .getValue(); - String expectedObjectEvaluation = "{groupName=test-group-name, name=layer-name, secondaryExposures=" - + "[{test-exposure=test-exposure-value}], allocatedExperiment=allocated, ruleID=test-rule-id, " - + "value={value-key=test-value}}"; + String expectedObjectEvaluation = + "{groupName=null, name=layer-name, allocatedExperiment=null, ruleID=test-rule-id, " + + "value={value-key=test-value}}"; assertEquals( expectedObjectEvaluation, objectEvaluation.asStructure().asObjectMap().toString()); @@ -407,19 +410,25 @@ void contextTransformTest() { HashMap customMap = new HashMap<>(); customMap.put(customPropertyKey, customPropertyValue); - StatsigUser expectedUser = new StatsigUser(evaluationContext.getTargetingKey()); - expectedUser.setEmail(email); - expectedUser.setCountry(country); - expectedUser.setUserAgent(userAgent); - expectedUser.setIp(ip); - expectedUser.setAppVersion(appVersion); - Map privateAttributesMap = new HashMap<>(); - privateAttributesMap.put(CONTEXT_LOCALE, locale); - expectedUser.setPrivateAttributes(privateAttributesMap); - expectedUser.setCustomIDs(customMap); + StatsigUser expectedUser = new StatsigUser.Builder() + .setUserID(evaluationContext.getTargetingKey()) + .setEmail(email) + .setCountry(country) + .setUserAgent(userAgent) + .setIp(ip) + .setAppVersion(appVersion) + .setPrivateAttributes(Collections.singletonMap(CONTEXT_LOCALE, locale)) + .setCustomIDs(customMap) + .build(); StatsigUser transformedUser = ContextTransformer.transform(evaluationContext); - // equals not implemented for User, using toString - assertEquals(expectedUser.toString(), transformedUser.toString()); + assertEquals(expectedUser.getUserID(), transformedUser.getUserID()); + assertEquals(expectedUser.getEmail(), transformedUser.getEmail()); + assertEquals(expectedUser.getCountry(), transformedUser.getCountry()); + assertEquals(expectedUser.getUserAgent(), transformedUser.getUserAgent()); + assertEquals(expectedUser.getIp(), transformedUser.getIp()); + assertEquals(expectedUser.getAppVersion(), transformedUser.getAppVersion()); + assertEquals(expectedUser.getPrivateAttributes(), transformedUser.getPrivateAttributes()); + assertEquals(expectedUser.getCustomIDs(), transformedUser.getCustomIDs()); } }