From 9f6c40cdeb19ae4bb036a19cc3b382431ca9ed2d Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 5 Oct 2020 16:45:38 -0700 Subject: [PATCH 01/44] add OptimizelyUserContext --- .../java/com/optimizely/ab/Optimizely.java | 43 ++- .../OptimizelyDecideOption.java | 9 + .../OptimizelyDecision.java | 96 +++++ .../OptimizelyUserContext.java | 80 ++++ .../optimizely/ab/OptimizelyBuilderTest.java | 25 +- .../com/optimizely/ab/OptimizelyTest.java | 44 +++ .../OptimizelyDecisionTest.java | 58 +++ .../OptimizelyUserContextTest.java | 85 +++++ .../config/decide-project-config.json | 341 ++++++++++++++++++ 9 files changed, 778 insertions(+), 3 deletions(-) create mode 100644 core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java create mode 100644 core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java create mode 100644 core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java create mode 100644 core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java create mode 100644 core-api/src/test/resources/config/decide-project-config.json diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index e32a39cb5..1ebc6d420 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -35,6 +35,8 @@ import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; +import com.optimizely.ab.optimizelyusercontext.OptimizelyUserContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,6 +87,8 @@ public class Optimizely implements AutoCloseable { final EventProcessor eventProcessor; @VisibleForTesting final ErrorHandler errorHandler; + @VisibleForTesting + final OptimizelyDecideOption[] defaultDecideOptions; private final ProjectConfigManager projectConfigManager; @@ -104,7 +108,8 @@ private Optimizely(@Nonnull EventHandler eventHandler, @Nullable UserProfileService userProfileService, @Nonnull ProjectConfigManager projectConfigManager, @Nullable OptimizelyConfigManager optimizelyConfigManager, - @Nonnull NotificationCenter notificationCenter + @Nonnull NotificationCenter notificationCenter, + @Nonnull OptimizelyDecideOption[] defaultDecideOptions ) { this.eventHandler = eventHandler; this.eventProcessor = eventProcessor; @@ -114,6 +119,7 @@ private Optimizely(@Nonnull EventHandler eventHandler, this.projectConfigManager = projectConfigManager; this.optimizelyConfigManager = optimizelyConfigManager; this.notificationCenter = notificationCenter; + this.defaultDecideOptions = defaultDecideOptions; } /** @@ -1086,6 +1092,29 @@ public OptimizelyConfig getOptimizelyConfig() { return new OptimizelyConfigService(projectConfig).getConfig(); } + /** + * Set a context of the user for which decision APIs will be called. + * + * - This API can be called after SDK initialization is completed (otherwise the __sdkNotReady__ error will be returned). + * - Only one user outstanding. The user-context can be changed any time by calling the same method with a different user-context value. + * - The SDK will copy the parameter value to create an internal user-context data atomically, so any further change in its caller copy after the API call is not reflected into the SDK state. + * - Once this API is called, the following other API calls can be called without a user-context parameter to use the same user-context. + * - Each Decide API call can contain an optional user-context parameter when the call targets a different user-context. This optional user-context parameter value will be used once only, instead of replacing the saved user-context. This call-based context control can be used to support multiple users at the same time. + * - If a user-context has not been set yet and decide APIs are called without a user-context parameter, SDK will return an error decision (__userNotSet__). + * + * @param userId The user ID to be used for bucketing. + * @param attributes: A map of attribute names to current user attribute values. + * @return An OptimizelyUserContext associated with this OptimizelyClient. + */ + public OptimizelyUserContext createUserContext(@Nonnull String userId, + @Nonnull Map attributes) { + return new OptimizelyUserContext(this, userId, attributes); + } + + public OptimizelyUserContext createUserContext(@Nonnull String userId) { + return new OptimizelyUserContext(this, userId); + } + /** * Helper method which makes separate copy of attributesMap variable and returns it * @@ -1190,6 +1219,7 @@ public static class Builder { private OptimizelyConfigManager optimizelyConfigManager; private UserProfileService userProfileService; private NotificationCenter notificationCenter; + private OptimizelyDecideOption[] defaultDecideOptions; // For backwards compatibility private AtomicProjectConfigManager fallbackConfigManager = new AtomicProjectConfigManager(); @@ -1277,6 +1307,11 @@ protected Builder withDecisionService(DecisionService decisionService) { return this; } + protected Builder withDefaultDecideOptions(OptimizelyDecideOption[] options) { + this.defaultDecideOptions = options; + return this; + } + public Optimizely build() { if (errorHandler == null) { @@ -1329,7 +1364,11 @@ public Optimizely build() { eventProcessor = new ForwardingEventProcessor(eventHandler, notificationCenter); } - return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter); + if (defaultDecideOptions == null) { + defaultDecideOptions = new OptimizelyDecideOption[0]; + } + + return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions); } } } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java new file mode 100644 index 000000000..411a6ffa5 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java @@ -0,0 +1,9 @@ +package com.optimizely.ab.optimizelyusercontext; + +public enum OptimizelyDecideOption { + DISABLE_DECISION_EVENT, + ENABLED_FLAGS_ONLY, + IGNORE_USER_PROFILE_SERVICE, + INCLUDE_REASONS, + EXCLUDE_VARIABLES +} diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java new file mode 100644 index 000000000..a9a0f6c17 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java @@ -0,0 +1,96 @@ +package com.optimizely.ab.optimizelyusercontext; + +import com.optimizely.ab.optimizelyjson.OptimizelyJSON; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class OptimizelyDecision { + @Nullable + private final String variationKey; + + private final boolean enabled; + + @Nonnull + private final OptimizelyJSON variables; + + @Nullable + private final String ruleKey; + + @Nonnull + private final String flagKey; + + @Nullable + private final OptimizelyUserContext userContext; + + @Nonnull + private String[] reasons; + + + public OptimizelyDecision(@Nullable String variationKey, + boolean enabled, + @Nonnull OptimizelyJSON variables, + @Nullable String ruleKey, + @Nonnull String flagKey, + @Nullable OptimizelyUserContext userContext, + @Nonnull String[] reasons) { + this.variationKey = variationKey; + this.enabled = enabled; + this.variables = variables; + this.ruleKey = ruleKey; + this.flagKey = flagKey; + this.userContext = userContext; + this.reasons = reasons; + } + + @Nullable + public String getVariationKey() { + return variationKey; + } + + public boolean getEnabled() { + return enabled; + } + + @Nonnull + public OptimizelyJSON getVariables() { + return variables; + } + + @Nullable + public String getRuleKey() { + return ruleKey; + } + + @Nonnull + public String getFlagKey() { + return flagKey; + } + + @Nullable + public OptimizelyUserContext getUserContext() { + return userContext; + } + + @Nonnull + public String[] getReasons() { + return reasons; + } + + public static OptimizelyDecision createErrorDecision(@Nonnull String key, + @Nonnull OptimizelyUserContext user, + String error) { + return new OptimizelyDecision(null, + false, + null, + null, + key, + user, + new String[]{error}); + } + + public boolean hasFailed() { + return variationKey == null; + } + +} diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java new file mode 100644 index 000000000..8b62654f4 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -0,0 +1,80 @@ +package com.optimizely.ab.optimizelyusercontext; + +import com.optimizely.ab.Optimizely; +import com.optimizely.ab.UnknownEventTypeException; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class OptimizelyUserContext { + private final String userId; + private final Map attributes; + private final Optimizely optimizely; + + public OptimizelyUserContext(@Nonnull Optimizely optimizely, + @Nonnull String userId, + @Nonnull Map attributes) { + this.optimizely = optimizely; + this.userId = userId; + this.attributes = new HashMap<>(attributes); + } + + public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId) { + this(optimizely, userId, new HashMap<>()); + } + + public String getUserId() { + return userId; + } + + public Map getAttributes() { + return attributes; + } + + public Optimizely getOptimizely() { + return optimizely; + } + + public void setAttribute(@Nonnull String key, @Nonnull Object value) { + attributes.put(key, value); + } + + public OptimizelyDecision decide(@Nonnull String key, + @Nonnull OptimizelyDecideOption[] options) { + return OptimizelyDecision.createErrorDecision(key, this, "N/A"); + } + + public OptimizelyDecision decide(String key) { + return decide(key, new OptimizelyDecideOption[0]); + } + + public Map decideAll(@Nonnull String[] keys, + @Nonnull OptimizelyDecideOption[] options) { + return new HashMap<>(); + } + + public Map decideAll(@Nonnull String[] keys) { + return decideAll(keys, new OptimizelyDecideOption[0]); + } + + public Map decideAll(@Nonnull OptimizelyDecideOption[] options) { + String[] allFlagKeys = {}; + return decideAll(allFlagKeys, options); + } + + public Map decideAll() { + return decideAll(new OptimizelyDecideOption[0]); + } + + public void trackEvent(@Nonnull String eventName, + @Nonnull Map eventTags) throws UnknownEventTypeException { + optimizely.track(eventName, userId, attributes, eventTags); + } + + public void trackEvent(@Nonnull String eventName) throws UnknownEventTypeException { + trackEvent(eventName, Collections.emptyMap()); + } + +} diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java index 8bd613481..6779830da 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java @@ -21,6 +21,7 @@ import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.error.NoOpErrorHandler; import com.optimizely.ab.event.EventHandler; +import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Rule; import org.junit.Test; @@ -33,7 +34,8 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for {@link Optimizely#builder(String, EventHandler)}. @@ -169,4 +171,25 @@ public void withProjectConfigManagerAndFallbackDatafile() throws Exception { // Project Config manager takes precedence. assertFalse(optimizelyClient.isValid()); } + + @Test + public void withDefaultDecideOptions() throws Exception { + OptimizelyDecideOption[] options = { + OptimizelyDecideOption.DISABLE_DECISION_EVENT, + OptimizelyDecideOption.ENABLED_FLAGS_ONLY, + OptimizelyDecideOption.EXCLUDE_VARIABLES + }; + + Optimizely optimizelyClient = Optimizely.builder(validConfigJsonV4(), mockEventHandler) + .build(); + assertEquals(optimizelyClient.defaultDecideOptions.length, 0); + + optimizelyClient = Optimizely.builder(validConfigJsonV4(), mockEventHandler) + .withDefaultDecideOptions(options) + .build(); + assertEquals(optimizelyClient.defaultDecideOptions[0], OptimizelyDecideOption.DISABLE_DECISION_EVENT); + assertEquals(optimizelyClient.defaultDecideOptions[1], OptimizelyDecideOption.ENABLED_FLAGS_ONLY); + assertEquals(optimizelyClient.defaultDecideOptions[2], OptimizelyDecideOption.EXCLUDE_VARIABLES); + } + } diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index 21dcd017e..2aced2256 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -35,6 +35,7 @@ import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.notification.*; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import com.optimizely.ab.optimizelyusercontext.OptimizelyUserContext; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Before; import org.junit.Rule; @@ -4579,4 +4580,47 @@ public void getOptimizelyConfigValidDatafile() { assertEquals(optimizely.getOptimizelyConfig().getDatafile(), validDatafile); } + // OptimizelyUserContext + + @Test + public void createUserContext_withAttributes() { + String userId = "testUser1"; + Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + + Optimizely optimizely = optimizelyBuilder.build(); + OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); + + assertEquals(user.getOptimizely(), optimizely); + assertEquals(user.getUserId(), userId); + assertEquals(user.getAttributes(), attributes); + } + + @Test + public void createUserContext_noAttributes() { + String userId = "testUser1"; + + Optimizely optimizely = optimizelyBuilder.build(); + OptimizelyUserContext user = optimizely.createUserContext(userId); + + assertEquals(user.getOptimizely(), optimizely); + assertEquals(user.getUserId(), userId); + assertTrue(user.getAttributes().isEmpty()); + } + + @Test + public void createUserContext_multiple() { + String userId1 = "testUser1"; + String userId2 = "testUser1"; + Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + + Optimizely optimizely = optimizelyBuilder.build(); + OptimizelyUserContext user1 = optimizely.createUserContext(userId1, attributes); + OptimizelyUserContext user2 = optimizely.createUserContext(userId2); + + assertEquals(user1.getUserId(), userId1); + assertEquals(user1.getAttributes(), attributes); + assertEquals(user2.getUserId(), userId2); + assertTrue(user2.getAttributes().isEmpty()); + } + } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java new file mode 100644 index 000000000..3730736f3 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java @@ -0,0 +1,58 @@ +package com.optimizely.ab.optimizelyusercontext; + +import com.optimizely.ab.Optimizely; +import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import org.junit.Test; + +import static junit.framework.TestCase.assertEquals; + +public class OptimizelyDecisionTest { + + @Test + public void testOptimizelyDecision() { + String variationKey = "var1"; + boolean enabled = true; + OptimizelyJSON variables = new OptimizelyJSON("{\"k1\":\"v1\"}"); + String ruleKey = null; + String flagKey = "flag1"; + OptimizelyUserContext userContext = new OptimizelyUserContext(Optimizely.builder().build(), "tester"); + String[] reasons = new String[0]; + + OptimizelyDecision decision = new OptimizelyDecision( + variationKey, + enabled, + variables, + ruleKey, + flagKey, + userContext, + reasons + ); + + assertEquals(decision.getVariationKey(), variationKey); + assertEquals(decision.getEnabled(), enabled); + assertEquals(decision.getVariables(), variables); + assertEquals(decision.getRuleKey(), ruleKey); + assertEquals(decision.getFlagKey(), flagKey); + assertEquals(decision.getUserContext(), userContext); + assertEquals(decision.getReasons(), reasons); + } + + @Test + public void testCreateErrorDecision() { + String flagKey = "flag1"; + OptimizelyUserContext userContext = new OptimizelyUserContext(Optimizely.builder().build(), "tester"); + String error = "SDK has an error"; + + OptimizelyDecision decision = OptimizelyDecision.createErrorDecision(flagKey, userContext, error); + + assertEquals(decision.getVariationKey(), null); + assertEquals(decision.getEnabled(), false); + assertEquals(decision.getVariables(), null); + assertEquals(decision.getRuleKey(), null); + assertEquals(decision.getFlagKey(), flagKey); + assertEquals(decision.getUserContext(), userContext); + assertEquals(decision.getReasons().length, 1); + assertEquals(decision.getReasons()[0], error); + } + +} diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java new file mode 100644 index 000000000..7631ff756 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -0,0 +1,85 @@ +package com.optimizely.ab.optimizelyusercontext; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import com.optimizely.ab.Optimizely; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; + +public class OptimizelyUserContextTest { + + public Optimizely optimizely; + public String userId = "tester"; + + @Before + public void setUp() throws Exception { + String datafile = Resources.toString(Resources.getResource("config/decide-project-config.json"), Charsets.UTF_8); + + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .build(); + } + + @Test + public void testOptimizelyUserContext_withAttributes() { + Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId, attributes); + + assertEquals(user.getOptimizely(), optimizely); + assertEquals(user.getUserId(), userId); + assertEquals(user.getAttributes(), attributes); + } + + @Test + public void testOptimizelyUserContext_noAttributes() { + OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId); + + assertEquals(user.getOptimizely(), optimizely); + assertEquals(user.getUserId(), userId); + assertTrue(user.getAttributes().isEmpty()); + } + + @Test + public void testSetAttribute() { + Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId, attributes); + + user.setAttribute("k1", "v1"); + user.setAttribute("k2", true); + user.setAttribute("k3", 100); + user.setAttribute("k4", 3.5); + + assertEquals(user.getOptimizely(), optimizely); + assertEquals(user.getUserId(), userId); + Map newAttributes = user.getAttributes(); + assertEquals(newAttributes.get(ATTRIBUTE_HOUSE_KEY), AUDIENCE_GRYFFINDOR_VALUE); + assertEquals(newAttributes.get("k1"), "v1"); + assertEquals(newAttributes.get("k2"), true); + assertEquals(newAttributes.get("k3"), 100); + assertEquals(newAttributes.get("k4"), 3.5); + } + + @Test + public void testSetAttribute_override() { + Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId, attributes); + + user.setAttribute("k1", "v1"); + user.setAttribute(ATTRIBUTE_HOUSE_KEY, "v2"); + + assertEquals(user.getOptimizely(), optimizely); + assertEquals(user.getUserId(), userId); + Map newAttributes = user.getAttributes(); + assertEquals(newAttributes.get("k1"), "v1"); + assertEquals(newAttributes.get(ATTRIBUTE_HOUSE_KEY), "v2"); + } + +} diff --git a/core-api/src/test/resources/config/decide-project-config.json b/core-api/src/test/resources/config/decide-project-config.json new file mode 100644 index 000000000..28f45db01 --- /dev/null +++ b/core-api/src/test/resources/config/decide-project-config.json @@ -0,0 +1,341 @@ +{ + "version": "4", + "rollouts": [ + { + "experiments": [ + { + "audienceIds": ["13389130056"], + "forcedVariations": {}, + "id": "3332020515", + "key": "3332020515", + "layerId": "3319450668", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 10000, + "entityId": "3324490633" + } + ], + "variations": [ + { + "featureEnabled": true, + "id": "3324490633", + "key": "3324490633", + "variables": [] + } + ] + }, + { + "audienceIds": ["12208130097"], + "forcedVariations": {}, + "id": "3332020494", + "key": "3332020494", + "layerId": "3319450668", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 0, + "entityId": "3324490562" + } + ], + "variations": [ + { + "featureEnabled": true, + "id": "3324490562", + "key": "3324490562", + "variables": [] + } + ] + }, + { + "status": "Running", + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "18257766532", + "key": "18257766532", + "featureEnabled": true + } + ], + "id": "18322080788", + "key": "18322080788", + "layerId": "18263344648", + "trafficAllocation": [ + { + "entityId": "18257766532", + "endOfRange": 10000 + } + ], + "forcedVariations": { } + } + ], + "id": "3319450668" + } + ], + "anonymizeIP": true, + "botFiltering": true, + "projectId": "10431130345", + "variables": [], + "featureFlags": [ + { + "experimentIds": ["10390977673"], + "id": "4482920077", + "key": "feature_1", + "rolloutId": "3319450668", + "variables": [ + { + "defaultValue": "42", + "id": "2687470095", + "key": "i_42", + "type": "integer" + }, + { + "defaultValue": "4.2", + "id": "2689280165", + "key": "d_4_2", + "type": "double" + }, + { + "defaultValue": "true", + "id": "2689660112", + "key": "b_true", + "type": "boolean" + }, + { + "defaultValue": "foo", + "id": "2696150066", + "key": "s_foo", + "type": "string" + }, + { + "defaultValue": "{\"value\":1}", + "id": "2696150067", + "key": "j_1", + "type": "string", + "subType": "json" + }, + { + "defaultValue": "invalid", + "id": "2696150068", + "key": "i_1", + "type": "invalid", + "subType": "" + } + ] + }, + { + "experimentIds": ["10420810910"], + "id": "4482920078", + "key": "feature_2", + "rolloutId": "", + "variables": [ + { + "defaultValue": "42", + "id": "2687470095", + "key": "i_42", + "type": "integer" + } + ] + }, + { + "experimentIds": [], + "id": "44829230000", + "key": "feature_3", + "rolloutId": "", + "variables": [] + } + ], + "experiments": [ + { + "status": "Running", + "key": "exp_with_audience", + "layerId": "10420273888", + "trafficAllocation": [ + { + "entityId": "10389729780", + "endOfRange": 10000 + } + ], + "audienceIds": ["13389141123"], + "variations": [ + { + "variables": [], + "featureEnabled": true, + "id": "10389729780", + "key": "a" + }, + { + "variables": [], + "id": "10416523121", + "key": "b" + } + ], + "forcedVariations": {}, + "id": "10390977673" + }, + { + "status": "Running", + "key": "exp_no_audience", + "layerId": "10417730432", + "trafficAllocation": [ + { + "entityId": "10418551353", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "variations": [ + { + "variables": [], + "featureEnabled": true, + "id": "10418551353", + "key": "variation_with_traffic" + }, + { + "variables": [], + "featureEnabled": false, + "id": "10418510624", + "key": "variation_no_traffic" + } + ], + "forcedVariations": {}, + "id": "10420810910" + } + ], + "audiences": [ + { + "id": "13389141123", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"exact\", \"name\": \"gender\", \"type\": \"custom_attribute\", \"value\": \"f\"}]]]", + "name": "gender" + }, + { + "id": "13389130056", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"exact\", \"name\": \"country\", \"type\": \"custom_attribute\", \"value\": \"US\"}]]]", + "name": "US" + }, + { + "id": "12208130097", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"exact\", \"name\": \"browser\", \"type\": \"custom_attribute\", \"value\": \"safari\"}]]]", + "name": "safari" + }, + { + "id": "age_18", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"name\": \"age\", \"type\": \"custom_attribute\", \"value\": 18}]]]", + "name": "age_18" + }, + { + "id": "invalid_format", + "conditions": "[]", + "name": "invalid_format" + }, + { + "id": "invalid_condition", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"name\": \"age\", \"type\": \"custom_attribute\", \"value\": \"US\"}]]]", + "name": "invalid_condition" + }, + { + "id": "invalid_type", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"name\": \"age\", \"type\": \"invalid\", \"value\": 18}]]]", + "name": "invalid_type" + }, + { + "id": "invalid_match", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"invalid\", \"name\": \"age\", \"type\": \"custom_attribute\", \"value\": 18}]]]", + "name": "invalid_match" + }, + { + "id": "nil_value", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"name\": \"age\", \"type\": \"custom_attribute\"}]]]", + "name": "nil_value" + }, + { + "id": "invalid_name", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"type\": \"custom_attribute\", \"value\": 18}]]]", + "name": "invalid_name" + } + ], + "groups": [ + { + "policy": "random", + "trafficAllocation": [ + { + "entityId": "10390965532", + "endOfRange": 10000 + } + ], + "experiments": [ + { + "status": "Running", + "key": "group_exp_1", + "layerId": "10420222423", + "trafficAllocation": [ + { + "entityId": "10389752311", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "variations": [ + { + "variables": [], + "featureEnabled": false, + "id": "10389752311", + "key": "a" + } + ], + "forcedVariations": {}, + "id": "10390965532" + }, + { + "status": "Running", + "key": "group_exp_2", + "layerId": "10417730432", + "trafficAllocation": [ + { + "entityId": "10418524243", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "variations": [ + { + "variables": [], + "featureEnabled": false, + "id": "10418524243", + "key": "a" + } + ], + "forcedVariations": {}, + "id": "10420843432" + } + ], + "id": "13142870430" + } + ], + "attributes": [ + { + "id": "10401066170", + "key": "testvar" + } + ], + "accountId": "10367498574", + "events": [ + { + "experimentIds": [ + "10420810910" + ], + "id": "10404198134", + "key": "event1" + }, + { + "experimentIds": [ + "10420810910", + "10390977673" + ], + "id": "10404198135", + "key": "event_multiple_running_exp_attached" + } + ], + "revision": "241" +} From d476539bfce64aff5394a02c7cf436901cca88f2 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 7 Oct 2020 09:29:30 -0700 Subject: [PATCH 02/44] add decide api --- .../java/com/optimizely/ab/Optimizely.java | 12 +- .../ab/notification/DecisionNotification.java | 98 ++++++++ .../ab/notification/NotificationCenter.java | 3 +- .../DecisionReasons.java | 33 +++ .../OptimizelyDecision.java | 10 +- .../OptimizelyUserContext.java | 227 +++++++++++++++++- 6 files changed, 367 insertions(+), 16 deletions(-) create mode 100644 core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 1ebc6d420..1f874b7ac 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -78,8 +78,7 @@ public class Optimizely implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(Optimizely.class); - @VisibleForTesting - final DecisionService decisionService; + public final DecisionService decisionService; @VisibleForTesting @Deprecated final EventHandler eventHandler; @@ -87,8 +86,8 @@ public class Optimizely implements AutoCloseable { final EventProcessor eventProcessor; @VisibleForTesting final ErrorHandler errorHandler; - @VisibleForTesting - final OptimizelyDecideOption[] defaultDecideOptions; + + public final OptimizelyDecideOption[] defaultDecideOptions; private final ProjectConfigManager projectConfigManager; @@ -227,7 +226,7 @@ private Variation activate(@Nullable ProjectConfig projectConfig, return variation; } - private void sendImpression(@Nonnull ProjectConfig projectConfig, + public void sendImpression(@Nonnull ProjectConfig projectConfig, @Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map filteredAttributes, @@ -742,8 +741,7 @@ T getFeatureVariableValueForType(@Nonnull String featureKey, } // Helper method which takes type and variable value and convert it to object to use in Listener DecisionInfo object variable value - @VisibleForTesting - Object convertStringToType(String variableValue, String type) { + public Object convertStringToType(String variableValue, String type) { if (variableValue != null) { switch (type) { case FeatureVariable.DOUBLE_TYPE: diff --git a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java index 0a7ea6e3c..ad95a8ded 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java @@ -24,6 +24,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -350,4 +351,101 @@ public DecisionNotification build() { decisionInfo); } } + + public static FlagDecisionNotificationBuilder newFlagDecisionNotificationBuilder() { + return new FlagDecisionNotificationBuilder(); + } + + public static class FlagDecisionNotificationBuilder { + public final static String FLAG_KEY = "flagKey"; + public final static String ENABLED = "enabled"; + public final static String VARIABLES = "variables"; + public final static String VARIATION_KEY = "variationKey"; + public final static String RULE_KEY = "ruleKey"; + public final static String REASONS = "reasons"; + public final static String DECISION_EVENT_DISPATCHED = "decisionEventDispatched"; + + private String flagKey; + private Boolean enabled; + private Object variables; + private String userId; + private Map attributes; + private String variationKey; + private String ruleKey; + private List reasons; + private Boolean decisionEventDispatched; + + private Map decisionInfo; + + public FlagDecisionNotificationBuilder withUserId(String userId) { + this.userId = userId; + return this; + } + + public FlagDecisionNotificationBuilder withAttributes(Map attributes) { + this.attributes = attributes; + return this; + } + + public FlagDecisionNotificationBuilder withFlagKey(String flagKey) { + this.flagKey = flagKey; + return this; + } + + public FlagDecisionNotificationBuilder withEnabled(Boolean enabled) { + this.enabled = enabled; + return this; + } + + public FlagDecisionNotificationBuilder withVariables(Object variables) { + this.enabled = enabled; + return this; + } + + public FlagDecisionNotificationBuilder withVariationKey(String key) { + this.variationKey = key; + return this; + } + + public FlagDecisionNotificationBuilder withRuleKey(String key) { + this.ruleKey = key; + return this; + } + + public FlagDecisionNotificationBuilder withReasons(List reasons) { + this.reasons = reasons; + return this; + } + + public FlagDecisionNotificationBuilder withDecisionEventDispatched(Boolean dispatched) { + this.decisionEventDispatched = dispatched; + return this; + } + + public DecisionNotification build() { + if (flagKey == null) { + throw new OptimizelyRuntimeException("flagKey not set"); + } + + if (enabled == null) { + throw new OptimizelyRuntimeException("enabled not set"); + } + + decisionInfo = new HashMap<>(); + decisionInfo.put(FLAG_KEY, flagKey); + decisionInfo.put(ENABLED, enabled); + decisionInfo.put(VARIABLES, variables); + decisionInfo.put(VARIATION_KEY, variationKey); + decisionInfo.put(RULE_KEY, ruleKey); + decisionInfo.put(REASONS, reasons); + decisionInfo.put(DECISION_EVENT_DISPATCHED, decisionEventDispatched); + + return new DecisionNotification( + NotificationCenter.DecisionNotificationType.FLAG.toString(), + userId, + attributes, + decisionInfo); + } + } + } diff --git a/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java b/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java index 4b0b3e406..ff13c8d09 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java @@ -55,7 +55,8 @@ public enum DecisionNotificationType { FEATURE("feature"), FEATURE_TEST("feature-test"), FEATURE_VARIABLE("feature-variable"), - ALL_FEATURE_VARIABLES("all-feature-variables"); + ALL_FEATURE_VARIABLES("all-feature-variables"), + FLAG("flag"); private final String key; diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java new file mode 100644 index 000000000..6cb8c6acb --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java @@ -0,0 +1,33 @@ +package com.optimizely.ab.optimizelyusercontext; + +import java.util.ArrayList; +import java.util.List; + +public class DecisionReasons { + + List errors; + List logs; + + public DecisionReasons() { + this.errors = new ArrayList(); + this.logs = new ArrayList(); + } + + public void addError(String message) { + errors.add(message); + } + + public void addInfo(String message) { + logs.add(message); + } + + + public List toReport(List options) { + List reasons = new ArrayList<>(errors); + if(options.contains(OptimizelyDecideOption.INCLUDE_REASONS)) { + reasons.addAll(logs); + } + return reasons; + } + +} diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java index a9a0f6c17..e4a8b9a8b 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java @@ -4,6 +4,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.List; public class OptimizelyDecision { @Nullable @@ -24,7 +26,7 @@ public class OptimizelyDecision { private final OptimizelyUserContext userContext; @Nonnull - private String[] reasons; + private List reasons; public OptimizelyDecision(@Nullable String variationKey, @@ -33,7 +35,7 @@ public OptimizelyDecision(@Nullable String variationKey, @Nullable String ruleKey, @Nonnull String flagKey, @Nullable OptimizelyUserContext userContext, - @Nonnull String[] reasons) { + @Nonnull List reasons) { this.variationKey = variationKey; this.enabled = enabled; this.variables = variables; @@ -73,7 +75,7 @@ public OptimizelyUserContext getUserContext() { } @Nonnull - public String[] getReasons() { + public List getReasons() { return reasons; } @@ -86,7 +88,7 @@ public static OptimizelyDecision createErrorDecision(@Nonnull String key, null, key, user, - new String[]{error}); + Arrays.asList(error)); } public boolean hasFailed() { diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index 8b62654f4..c17184f10 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -2,17 +2,37 @@ import com.optimizely.ab.Optimizely; import com.optimizely.ab.UnknownEventTypeException; +import com.optimizely.ab.bucketing.FeatureDecision; +import com.optimizely.ab.config.*; +import com.optimizely.ab.notification.DecisionNotification; +import com.optimizely.ab.notification.FeatureTestSourceInfo; +import com.optimizely.ab.notification.RolloutSourceInfo; +import com.optimizely.ab.notification.SourceInfo; +import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.*; public class OptimizelyUserContext { + @Nonnull private final String userId; + + @Nonnull private final Map attributes; + + @Nonnull private final Optimizely optimizely; + private static final Logger logger = LoggerFactory.getLogger(OptimizelyUserContext.class); + + public final static String SDK_NOT_READY = "Optimizely SDK not configured properly yet"; + public final static String FLAG_KEY_INVALID = "Flag key \"%s\" is not in datafile."; + public final static String VARIABLE_VALUE_INVALID = "Variable value for key \"%s\" is invalid or wrong type."; + public final static String OPTIMIZELY_JSON_ERROR = "Invalid variables for OptimizelyJSON."; + + public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId, @Nonnull Map attributes) { @@ -37,44 +57,243 @@ public Optimizely getOptimizely() { return optimizely; } + /** + * Set an attribute for a given key. + * + * @param key An attribute key + * @param value An attribute value + */ public void setAttribute(@Nonnull String key, @Nonnull Object value) { attributes.put(key, value); } + /** + * Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + *
    + *
  • If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons. + *
+ * @param key A flag key for which a decision will be made. + * @param options An array of options for decision-making. + * @return A decision result. + */ public OptimizelyDecision decide(@Nonnull String key, @Nonnull OptimizelyDecideOption[] options) { - return OptimizelyDecision.createErrorDecision(key, this, "N/A"); + + ProjectConfig projectConfig = optimizely.getProjectConfig(); + if (projectConfig == null) { + return OptimizelyDecision.createErrorDecision(key, this, SDK_NOT_READY); + } + + FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key); + if (flag == null) { + return OptimizelyDecision.createErrorDecision(key, this, getFlagKeyInvalidMessage(key)); + } + + List allOptions = getAllOptions(options); + DecisionReasons decisionReasons = new DecisionReasons(); + Boolean sentEvent = false; + Boolean flagEnabled = false; + + Map copiedAttributes = copyAttributes(); + FeatureDecision flagDecision = optimizely.decisionService.getVariationForFeature( + flag, + userId, + copiedAttributes, + projectConfig); + + FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT; + SourceInfo sourceInfo = new RolloutSourceInfo(); + + if (flagDecision.variation != null) { + if (flagDecision.decisionSource.equals(FeatureDecision.DecisionSource.FEATURE_TEST)) { + if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) { + optimizely.sendImpression( + projectConfig, + flagDecision.experiment, + userId, + copiedAttributes, + flagDecision.variation); + sentEvent = true; + } + + decisionSource = flagDecision.decisionSource; + sourceInfo = new FeatureTestSourceInfo(flagDecision.experiment.getKey(), flagDecision.variation.getKey()); + } else { + String message = String.format("The user \"%s\" is not included in an experiment for flag \"%s\".", userId, key); + logger.info(message); + decisionReasons.addInfo(message); + } + if (flagDecision.variation.getFeatureEnabled()) { + flagEnabled = true; + } + } + + Map variableMap = new HashMap<>(); + if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) { + variableMap = getDecisionVariableMap( + flag, + flagDecision.variation, + flagEnabled, + decisionReasons); + } + + OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap); + if (optimizelyJSON == null) { + decisionReasons.addError(OPTIMIZELY_JSON_ERROR); + } + + List reasonsToReport = decisionReasons.toReport(allOptions); + String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null; + // TODO: add ruleKey values when available later. + String ruleKey = null; + + DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder() + .withUserId(userId) + .withAttributes(copiedAttributes) + .withFlagKey(key) + .withEnabled(flagEnabled) + .withVariables(variableMap) + .withVariationKey(variationKey) + .withRuleKey(ruleKey) + .withReasons(reasonsToReport) + .withDecisionEventDispatched(sentEvent) + .build(); + optimizely.notificationCenter.send(decisionNotification); + + logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled); + + return new OptimizelyDecision( + variationKey, + flagEnabled, + optimizelyJSON, + ruleKey, + key, + this, + reasonsToReport); } + /** + * Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. + * + * @param key A flag key for which a decision will be made. + * @return A decision result. + */ public OptimizelyDecision decide(String key) { return decide(key, new OptimizelyDecideOption[0]); } + /** + * Returns a key-map of decision results ({@link OptimizelyDecision}) for multiple flag keys and a user context. + *
    + *
  • If the SDK finds an error for a key, the response will include a decision for the key showing reasons for the error. + *
  • The SDK will always return key-mapped decisions. When it can not process requests, it’ll return an empty map after logging the errors. + *
      + * @param keys An array of flag keys for which decisions will be made. + * @param options An array of options for decision-making. + * @return All decision results mapped by flag keys. + */ public Map decideAll(@Nonnull String[] keys, @Nonnull OptimizelyDecideOption[] options) { return new HashMap<>(); } + /** + * Returns a key-map of decision results for multiple flag keys and a user context. + * + * @param keys An array of flag keys for which decisions will be made. + * @return All decision results mapped by flag keys. + */ public Map decideAll(@Nonnull String[] keys) { return decideAll(keys, new OptimizelyDecideOption[0]); } + /** + * Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. + * + * @param options An array of options for decision-making. + * @return All decision results mapped by flag keys. + */ public Map decideAll(@Nonnull OptimizelyDecideOption[] options) { String[] allFlagKeys = {}; return decideAll(allFlagKeys, options); } + /** + * Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. + * + * @return A dictionary of all decision results, mapped by flag keys. + */ public Map decideAll() { return decideAll(new OptimizelyDecideOption[0]); } + /** + * Track an event. + * + * @param eventName The event name. + * @param eventTags A map of event tag names to event tag values. + * @throws UnknownEventTypeException + */ public void trackEvent(@Nonnull String eventName, @Nonnull Map eventTags) throws UnknownEventTypeException { optimizely.track(eventName, userId, attributes, eventTags); } + /** + * Track an event. + * + * @param eventName The event name. + * @throws UnknownEventTypeException + */ public void trackEvent(@Nonnull String eventName) throws UnknownEventTypeException { trackEvent(eventName, Collections.emptyMap()); } + // Utils + + private Map copyAttributes() { + return new HashMap<>(attributes); + } + + private List getAllOptions(OptimizelyDecideOption[] options) { + List allOptions = new ArrayList(Arrays.asList(optimizely.defaultDecideOptions)); + allOptions.addAll(Arrays.asList(options)); + return allOptions; + } + + public static String getFlagKeyInvalidMessage(String flagKey) { + return String.format(FLAG_KEY_INVALID, flagKey); + } + + public static String getVariableValueInvalidMessage(String variableKey) { + return String.format(VARIABLE_VALUE_INVALID, variableKey); + } + + private Map getDecisionVariableMap(FeatureFlag flag, + Variation variation, + Boolean featureEnabled, + DecisionReasons decisionReasons) { + Map valuesMap = new HashMap(); + for (FeatureVariable variable : flag.getVariables()) { + String value = variable.getDefaultValue(); + if (featureEnabled) { + FeatureVariableUsageInstance instance = variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId()); + if (instance != null) { + value = instance.getValue(); + } + } + + Object convertedValue = optimizely.convertStringToType(value, variable.getType()); + if (convertedValue == null) { + decisionReasons.addError(getVariableValueInvalidMessage(variable.getKey())); + } else if (convertedValue instanceof OptimizelyJSON) { + convertedValue = ((OptimizelyJSON) convertedValue).toMap(); + } + + valuesMap.put(variable.getKey(), convertedValue); + } + + return valuesMap; + } + } From 8bea7388ae2a5824161eb9cb37e7105b9220f06a Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 7 Oct 2020 15:14:26 -0700 Subject: [PATCH 03/44] add options and reasons --- .../com/optimizely/ab/bucketing/Bucketer.java | 45 +++- .../ab/bucketing/DecisionService.java | 217 +++++++++++++----- .../ab/internal/ExperimentUtils.java | 72 +++++- .../OptimizelyUserContext.java | 4 +- 4 files changed, 266 insertions(+), 72 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index c9295c6bb..f423472a7 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -23,6 +23,8 @@ import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.TrafficAllocation; import com.optimizely.ab.config.Variation; +import com.optimizely.ab.optimizelyusercontext.DecisionReasons; +import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,7 +73,9 @@ private String bucketToEntity(int bucketValue, List trafficAl private Experiment bucketToExperiment(@Nonnull Group group, @Nonnull String bucketingId, - @Nonnull ProjectConfig projectConfig) { + @Nonnull ProjectConfig projectConfig, + @Nullable List options, + @Nullable DecisionReasons reasons) { // "salt" the bucket id using the group id String bucketKey = bucketingId + group.getId(); @@ -91,7 +95,9 @@ private Experiment bucketToExperiment(@Nonnull Group group, } private Variation bucketToVariation(@Nonnull Experiment experiment, - @Nonnull String bucketingId) { + @Nonnull String bucketingId, + @Nullable List options, + @Nullable DecisionReasons reasons) { // "salt" the bucket id using the experiment id String experimentId = experiment.getId(); String experimentKey = experiment.getKey(); @@ -107,14 +113,14 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, if (bucketedVariationId != null) { Variation bucketedVariation = experiment.getVariationIdToVariationMap().get(bucketedVariationId); String variationKey = bucketedVariation.getKey(); - logger.info("User with bucketingId \"{}\" is in variation \"{}\" of experiment \"{}\".", bucketingId, variationKey, + DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", bucketingId, variationKey, experimentKey); return bucketedVariation; } // user was not bucketed to a variation - logger.info("User with bucketingId \"{}\" is not in any variation of experiment \"{}\".", bucketingId, experimentKey); + DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", bucketingId, experimentKey); return null; } @@ -123,12 +129,17 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, * * @param experiment The Experiment in which the user is to be bucketed. * @param bucketingId string A customer-assigned value used to create the key for the murmur hash. + * @param projectConfig The current projectConfig + * @param options An array of decision options + * @param reasons Decision log messages * @return Variation the user is bucketed into or null. */ @Nullable public Variation bucket(@Nonnull Experiment experiment, @Nonnull String bucketingId, - @Nonnull ProjectConfig projectConfig) { + @Nonnull ProjectConfig projectConfig, + @Nullable List options, + @Nullable DecisionReasons reasons) { // ---------- Bucket User ---------- String groupId = experiment.getGroupId(); // check whether the experiment belongs to a group @@ -136,9 +147,9 @@ public Variation bucket(@Nonnull Experiment experiment, Group experimentGroup = projectConfig.getGroupIdMapping().get(groupId); // bucket to an experiment only if group entities are to be mutually exclusive if (experimentGroup.getPolicy().equals(Group.RANDOM_POLICY)) { - Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig); + Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig, options, reasons); if (bucketedExperiment == null) { - logger.info("User with bucketingId \"{}\" is not in any experiment of group {}.", bucketingId, experimentGroup.getId()); + DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId()); return null; } else { @@ -146,19 +157,33 @@ public Variation bucket(@Nonnull Experiment experiment, // if the experiment a user is bucketed in within a group isn't the same as the experiment provided, // don't perform further bucketing within the experiment if (!bucketedExperiment.getId().equals(experiment.getId())) { - logger.info("User with bucketingId \"{}\" is not in experiment \"{}\" of group {}.", bucketingId, experiment.getKey(), + DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is not in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), experimentGroup.getId()); return null; } - logger.info("User with bucketingId \"{}\" is in experiment \"{}\" of group {}.", bucketingId, experiment.getKey(), + DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), experimentGroup.getId()); } } - return bucketToVariation(experiment, bucketingId); + return bucketToVariation(experiment, bucketingId, options, reasons); } + /** + * Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3. + * + * @param experiment The Experiment in which the user is to be bucketed. + * @param bucketingId string A customer-assigned value used to create the key for the murmur hash. + * @param projectConfig The current projectConfig + * @return Variation the user is bucketed into or null. + */ + @Nullable + public Variation bucket(@Nonnull Experiment experiment, + @Nonnull String bucketingId, + @Nonnull ProjectConfig projectConfig) { + return bucket(experiment, bucketingId, projectConfig, null, null); + } //======== Helper methods ========// diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index 13091472d..d1cfd3b77 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -18,19 +18,20 @@ import com.optimizely.ab.OptimizelyRuntimeException; import com.optimizely.ab.config.*; import com.optimizely.ab.error.ErrorHandler; -import com.optimizely.ab.internal.ExperimentUtils; import com.optimizely.ab.internal.ControlAttribute; - +import com.optimizely.ab.internal.ExperimentUtils; +import com.optimizely.ab.optimizelyusercontext.DecisionReasons; +import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.EXPERIMENT; import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.RULE; @@ -81,24 +82,28 @@ public DecisionService(@Nonnull Bucketer bucketer, * @param experiment The Experiment the user will be bucketed into. * @param userId The userId of the user. * @param filteredAttributes The user's attributes. This should be filtered to just attributes in the Datafile. + * @param projectConfig The current projectConfig + * @param options An array of decision options + * @param reasons Decision log messages * @return The {@link Variation} the user is allocated into. */ @Nullable public Variation getVariation(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map filteredAttributes, - @Nonnull ProjectConfig projectConfig) { - - if (!ExperimentUtils.isExperimentActive(experiment)) { + @Nonnull ProjectConfig projectConfig, + @Nullable List options, + @Nullable DecisionReasons reasons) { + if (!ExperimentUtils.isExperimentActive(experiment, options, reasons)) { return null; } // look for forced bucketing first. - Variation variation = getForcedVariation(experiment, userId); + Variation variation = getForcedVariation(experiment, userId, options, reasons); // check for whitelisting if (variation == null) { - variation = getWhitelistedVariation(experiment, userId); + variation = getWhitelistedVariation(experiment, userId, options, reasons); } if (variation != null) { @@ -112,21 +117,21 @@ public Variation getVariation(@Nonnull Experiment experiment, try { Map userProfileMap = userProfileService.lookup(userId); if (userProfileMap == null) { - logger.info("We were unable to get a user profile map from the UserProfileService."); + logInfo(logger, reasons, "We were unable to get a user profile map from the UserProfileService."); } else if (UserProfileUtils.isValidUserProfileMap(userProfileMap)) { userProfile = UserProfileUtils.convertMapToUserProfile(userProfileMap); } else { - logger.warn("The UserProfileService returned an invalid map."); + logWarn(logger, reasons, "The UserProfileService returned an invalid map."); } } catch (Exception exception) { - logger.error(exception.getMessage()); + logError(logger, reasons, exception.getMessage()); errorHandler.handleError(new OptimizelyRuntimeException(exception)); } } // check if user exists in user profile if (userProfile != null) { - variation = getStoredVariation(experiment, userProfile, projectConfig); + variation = getStoredVariation(experiment, userProfile, projectConfig, options, reasons); // return the stored variation if it exists if (variation != null) { return variation; @@ -135,13 +140,13 @@ public Variation getVariation(@Nonnull Experiment experiment, userProfile = new UserProfile(userId, new HashMap()); } - if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, experiment, filteredAttributes, EXPERIMENT, experiment.getKey())) { - String bucketingId = getBucketingId(userId, filteredAttributes); - variation = bucketer.bucket(experiment, bucketingId, projectConfig); + if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, experiment, filteredAttributes, EXPERIMENT, experiment.getKey(), options, reasons)) { + String bucketingId = getBucketingId(userId, filteredAttributes, options, reasons); + variation = bucketer.bucket(experiment, bucketingId, projectConfig, options, reasons); if (variation != null) { if (userProfileService != null) { - saveVariation(experiment, variation, userProfile); + saveVariation(experiment, variation, userProfile, options, reasons); } else { logger.debug("This decision will not be saved since the UserProfileService is null."); } @@ -150,46 +155,87 @@ public Variation getVariation(@Nonnull Experiment experiment, return variation; } - logger.info("User \"{}\" does not meet conditions to be in experiment \"{}\".", userId, experiment.getKey()); + logInfo(logger, reasons, "User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experiment.getKey()); return null; } + /** + * Get a {@link Variation} of an {@link Experiment} for a user to be allocated into. + * + * @param experiment The Experiment the user will be bucketed into. + * @param userId The userId of the user. + * @param filteredAttributes The user's attributes. This should be filtered to just attributes in the Datafile. + * @param projectConfig The current projectConfig + * @return The {@link Variation} the user is allocated into. + */ + @Nullable + public Variation getVariation(@Nonnull Experiment experiment, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull ProjectConfig projectConfig) { + return getVariation(experiment, userId, filteredAttributes, projectConfig, null, null); + } + /** * Get the variation the user is bucketed into for the FeatureFlag * * @param featureFlag The feature flag the user wants to access. * @param userId User Identifier * @param filteredAttributes A map of filtered attributes. + * @param projectConfig The current projectConfig + * @param options An array of decision options + * @param reasons Decision log messages * @return {@link FeatureDecision} */ @Nonnull public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, @Nonnull String userId, @Nonnull Map filteredAttributes, - @Nonnull ProjectConfig projectConfig) { + @Nonnull ProjectConfig projectConfig, + @Nullable List options, + @Nullable DecisionReasons reasons) { if (!featureFlag.getExperimentIds().isEmpty()) { for (String experimentId : featureFlag.getExperimentIds()) { Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId); - Variation variation = getVariation(experiment, userId, filteredAttributes, projectConfig); + Variation variation = getVariation(experiment, userId, filteredAttributes, projectConfig, options, reasons); if (variation != null) { return new FeatureDecision(experiment, variation, FeatureDecision.DecisionSource.FEATURE_TEST); } } } else { - logger.info("The feature flag \"{}\" is not used in any experiments.", featureFlag.getKey()); + logInfo(logger, reasons, "The feature flag \"%s\" is not used in any experiments.", featureFlag.getKey()); } - FeatureDecision featureDecision = getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig); + FeatureDecision featureDecision = getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, options, reasons); if (featureDecision.variation == null) { - logger.info("The user \"{}\" was not bucketed into a rollout for feature flag \"{}\".", + logInfo(logger, reasons, "The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".", userId, featureFlag.getKey()); } else { - logger.info("The user \"{}\" was bucketed into a rollout for feature flag \"{}\".", + logInfo(logger, reasons, "The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, featureFlag.getKey()); } return featureDecision; } + /** + * Get the variation the user is bucketed into for the FeatureFlag + * + * @param featureFlag The feature flag the user wants to access. + * @param userId User Identifier + * @param filteredAttributes A map of filtered attributes. + * @param projectConfig The current projectConfig + * @return {@link FeatureDecision} + */ + @Nonnull + public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull ProjectConfig projectConfig) { + + return getVariationForFeature(featureFlag, userId, filteredAttributes, projectConfig,null, null); + } + + /** * Try to bucket the user into a rollout rule. * Evaluate the user for rules in priority order by seeing if the user satisfies the audience. @@ -198,49 +244,54 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, * @param featureFlag The feature flag the user wants to access. * @param userId User Identifier * @param filteredAttributes A map of filtered attributes. + * @param projectConfig The current projectConfig + * @param options An array of decision options + * @param reasons Decision log messages * @return {@link FeatureDecision} */ @Nonnull FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag, @Nonnull String userId, @Nonnull Map filteredAttributes, - @Nonnull ProjectConfig projectConfig) { + @Nonnull ProjectConfig projectConfig, + @Nullable List options, + @Nullable DecisionReasons reasons) { // use rollout to get variation for feature if (featureFlag.getRolloutId().isEmpty()) { - logger.info("The feature flag \"{}\" is not used in a rollout.", featureFlag.getKey()); + logInfo(logger, reasons, "The feature flag \"%s\" is not used in a rollout.", featureFlag.getKey()); return new FeatureDecision(null, null, null); } Rollout rollout = projectConfig.getRolloutIdMapping().get(featureFlag.getRolloutId()); if (rollout == null) { - logger.error("The rollout with id \"{}\" was not found in the datafile for feature flag \"{}\".", + logError(logger, reasons, "The rollout with id \"%s\" was not found in the datafile for feature flag \"%s\".", featureFlag.getRolloutId(), featureFlag.getKey()); return new FeatureDecision(null, null, null); } // for all rules before the everyone else rule int rolloutRulesLength = rollout.getExperiments().size(); - String bucketingId = getBucketingId(userId, filteredAttributes); + String bucketingId = getBucketingId(userId, filteredAttributes, options, reasons); Variation variation; for (int i = 0; i < rolloutRulesLength - 1; i++) { Experiment rolloutRule = rollout.getExperiments().get(i); - if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, rolloutRule, filteredAttributes, RULE, Integer.toString(i + 1))) { - variation = bucketer.bucket(rolloutRule, bucketingId, projectConfig); + if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, rolloutRule, filteredAttributes, RULE, Integer.toString(i + 1), options, reasons)) { + variation = bucketer.bucket(rolloutRule, bucketingId, projectConfig, options, reasons); if (variation == null) { break; } return new FeatureDecision(rolloutRule, variation, FeatureDecision.DecisionSource.ROLLOUT); } else { - logger.debug("User \"{}\" does not meet conditions for targeting rule \"{}\".", userId, i + 1); + logDebug(logger, reasons, "User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, i + 1); } } // get last rule which is the fall back rule Experiment finalRule = rollout.getExperiments().get(rolloutRulesLength - 1); - if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, finalRule, filteredAttributes, RULE, "Everyone Else")) { - variation = bucketer.bucket(finalRule, bucketingId, projectConfig); + if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, finalRule, filteredAttributes, RULE, "Everyone Else", options, reasons)) { + variation = bucketer.bucket(finalRule, bucketingId, projectConfig, options, reasons); if (variation != null) { - logger.debug("User \"{}\" meets conditions for targeting rule \"Everyone Else\".", userId); + logDebug(logger, reasons, "User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId); return new FeatureDecision(finalRule, variation, FeatureDecision.DecisionSource.ROLLOUT); } @@ -253,20 +304,25 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag * * @param experiment {@link Experiment} in which user is to be bucketed. * @param userId User Identifier + * @param options An array of decision options + * @param reasons Decision log messages * @return null if the user is not whitelisted into any variation * {@link Variation} the user is bucketed into if the user has a specified whitelisted variation. */ @Nullable - Variation getWhitelistedVariation(@Nonnull Experiment experiment, @Nonnull String userId) { + Variation getWhitelistedVariation(@Nonnull Experiment experiment, + @Nonnull String userId, + @Nullable List options, + @Nullable DecisionReasons reasons) { // if a user has a forced variation mapping, return the respective variation Map userIdToVariationKeyMap = experiment.getUserIdToVariationKeyMap(); if (userIdToVariationKeyMap.containsKey(userId)) { String forcedVariationKey = userIdToVariationKeyMap.get(userId); Variation forcedVariation = experiment.getVariationKeyToVariationMap().get(forcedVariationKey); if (forcedVariation != null) { - logger.info("User \"{}\" is forced in variation \"{}\".", userId, forcedVariationKey); + logInfo(logger, reasons, "User \"%s\" is forced in variation \"%s\".", userId, forcedVariationKey); } else { - logger.error("Variation \"{}\" is not in the datafile. Not activating user \"{}\".", + logError(logger, reasons, "Variation \"%s\" is not in the datafile. Not activating user \"%s\".", forcedVariationKey, userId); } return forcedVariation; @@ -279,13 +335,21 @@ Variation getWhitelistedVariation(@Nonnull Experiment experiment, @Nonnull Strin * * @param experiment {@link Experiment} in which the user was bucketed. * @param userProfile {@link UserProfile} of the user. + * @param projectConfig The current projectConfig + * @param options An array of decision options + * @param reasons Decision log messages * @return null if the {@link UserProfileService} implementation is null or the user was not previously bucketed. * else return the {@link Variation} the user was previously bucketed into. */ @Nullable Variation getStoredVariation(@Nonnull Experiment experiment, @Nonnull UserProfile userProfile, - @Nonnull ProjectConfig projectConfig) { + @Nonnull ProjectConfig projectConfig, + @Nullable List options, + @Nullable DecisionReasons reasons) { + + if (options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return null; + // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation String experimentId = experiment.getId(); @@ -299,20 +363,17 @@ Variation getStoredVariation(@Nonnull Experiment experiment, .getVariationIdToVariationMap() .get(variationId); if (savedVariation != null) { - logger.info("Returning previously activated variation \"{}\" of experiment \"{}\" " + - "for user \"{}\" from user profile.", + logInfo(logger, reasons,"Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", savedVariation.getKey(), experimentKey, userProfile.userId); // A variation is stored for this combined bucket id return savedVariation; } else { - logger.info("User \"{}\" was previously bucketed into variation with ID \"{}\" for experiment \"{}\", " + - "but no matching variation was found for that user. We will re-bucket the user.", + logInfo(logger, reasons,"User \"%s\" was previously bucketed into variation with ID \"%s\" for experiment \"%s\", but no matching variation was found for that user. We will re-bucket the user.", userProfile.userId, variationId, experimentKey); return null; } } else { - logger.info("No previously activated variation of experiment \"{}\" " + - "for user \"{}\" found in user profile.", + logInfo(logger, reasons, "No previously activated variation of experiment \"%s\" for user \"%s\" found in user profile.", experimentKey, userProfile.userId); return null; } @@ -324,10 +385,17 @@ Variation getStoredVariation(@Nonnull Experiment experiment, * @param experiment The experiment the user was buck * @param variation The Variation to save. * @param userProfile A {@link UserProfile} instance of the user information. + * @param options An array of decision options + * @param reasons Decision log messages */ void saveVariation(@Nonnull Experiment experiment, @Nonnull Variation variation, - @Nonnull UserProfile userProfile) { + @Nonnull UserProfile userProfile, + @Nullable List options, + @Nullable DecisionReasons reasons) { + + if (options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return; + // only save if the user has implemented a user profile service if (userProfileService != null) { String experimentId = experiment.getId(); @@ -358,18 +426,22 @@ void saveVariation(@Nonnull Experiment experiment, * * @param userId The userId of the user. * @param filteredAttributes The user's attributes. This should be filtered to just attributes in the Datafile. + * @param options An array of decision options + * @param reasons Decision log messages * @return bucketingId if it is a String type in attributes. * else return userId */ String getBucketingId(@Nonnull String userId, - @Nonnull Map filteredAttributes) { + @Nonnull Map filteredAttributes, + @Nullable List options, + @Nullable DecisionReasons reasons) { String bucketingId = userId; if (filteredAttributes != null && filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) { if (String.class.isInstance(filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()))) { bucketingId = (String) filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()); logger.debug("BucketingId is valid: \"{}\"", bucketingId); } else { - logger.warn("BucketingID attribute is not a string. Defaulted to userId"); + logWarn(logger, reasons, "BucketingID attribute is not a string. Defaulted to userId"); } } return bucketingId; @@ -393,8 +465,6 @@ public ConcurrentHashMap> getForcedVar public boolean setForcedVariation(@Nonnull Experiment experiment, @Nonnull String userId, @Nullable String variationKey) { - - Variation variation = null; // keep in mind that you can pass in a variationKey that is null if you want to @@ -455,12 +525,16 @@ public boolean setForcedVariation(@Nonnull Experiment experiment, * * @param experiment The experiment forced. * @param userId The user ID to be used for bucketing. + * @param options An array of decision options + * @param reasons Decision log messages * @return The variation the user was bucketed into. This value can be null if the * forced variation fails. */ @Nullable public Variation getForcedVariation(@Nonnull Experiment experiment, - @Nonnull String userId) { + @Nonnull String userId, + @Nullable List options, + @Nullable DecisionReasons reasons) { // if the user id is invalid, return false. if (!validateUserId(userId)) { @@ -473,7 +547,7 @@ public Variation getForcedVariation(@Nonnull Experiment experiment, if (variationId != null) { Variation variation = experiment.getVariationIdToVariationMap().get(variationId); if (variation != null) { - logger.debug("Variation \"{}\" is mapped to experiment \"{}\" and user \"{}\" in the forced variation map", + logDebug(logger, reasons, "Variation \"%s\" is mapped to experiment \"%s\" and user \"%s\" in the forced variation map", variation.getKey(), experiment.getKey(), userId); return variation; } @@ -484,6 +558,20 @@ public Variation getForcedVariation(@Nonnull Experiment experiment, return null; } + /** + * Gets the forced variation for a given user and experiment. + * + * @param experiment The experiment forced. + * @param userId The user ID to be used for bucketing. + * @return The variation the user was bucketed into. This value can be null if the + * forced variation fails. + */ + @Nullable + public Variation getForcedVariation(@Nonnull Experiment experiment, + @Nonnull String userId) { + return getForcedVariation(experiment, userId, null, null); + } + /** * Helper function to check that the provided userId is valid * @@ -498,4 +586,29 @@ private boolean validateUserId(String userId) { return true; } + + public static void logError(Logger logger, DecisionReasons reasons, String format, Object... args) { + String message = String.format(format, args); + logger.error(message); + if (reasons != null) reasons.addInfo(message); + } + + public static void logWarn(Logger logger, DecisionReasons reasons, String format, Object... args) { + String message = String.format(format, args); + logger.warn(message); + if (reasons != null) reasons.addInfo(message); + } + + public static void logInfo(Logger logger, DecisionReasons reasons, String format, Object... args) { + String message = String.format(format, args); + logger.info(message); + if (reasons != null) reasons.addInfo(message); + } + + public static void logDebug(Logger logger, DecisionReasons reasons, String format, Object... args) { + String message = String.format(format, args); + logger.debug(message); + if (reasons != null) reasons.addInfo(message); + } + } diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java index f5109b624..c1d40b8a3 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java @@ -16,11 +16,14 @@ */ package com.optimizely.ab.internal; +import com.optimizely.ab.bucketing.DecisionService; import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.AudienceIdCondition; import com.optimizely.ab.config.audience.Condition; import com.optimizely.ab.config.audience.OrCondition; +import com.optimizely.ab.optimizelyusercontext.DecisionReasons; +import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,18 +44,26 @@ private ExperimentUtils() { * Helper method to validate all pre-conditions before bucketing a user. * * @param experiment the experiment we are validating pre-conditions for + * @param options An array of decision options + * @param reasons Decision log messages * @return whether the pre-conditions are satisfied */ - public static boolean isExperimentActive(@Nonnull Experiment experiment) { + public static boolean isExperimentActive(@Nonnull Experiment experiment, + @Nullable List options, + @Nullable DecisionReasons reasons) { if (!experiment.isActive()) { - logger.info("Experiment \"{}\" is not running.", experiment.getKey()); + DecisionService.logInfo(logger, reasons, "Experiment \"%s\" is not running.", experiment.getKey()); return false; } return true; } + public static boolean isExperimentActive(@Nonnull Experiment experiment) { + return isExperimentActive(experiment, null, null); + } + /** * Determines whether a user satisfies audience conditions for the experiment. * @@ -61,13 +72,17 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment) { * @param attributes the attributes of the user * @param loggingEntityType It can be either experiment or rule. * @param loggingKey In case of loggingEntityType is experiment it will be experiment key or else it will be rule number. + * @param options An array of decision options + * @param reasons Decision log messages * @return whether the user meets the criteria for the experiment */ public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig projectConfig, @Nonnull Experiment experiment, @Nonnull Map attributes, @Nonnull String loggingEntityType, - @Nonnull String loggingKey) { + @Nonnull String loggingKey, + @Nullable List options, + @Nullable DecisionReasons reasons) { if (experiment.getAudienceConditions() != null) { logger.debug("Evaluating audiences for {} \"{}\": {}.", loggingEntityType, loggingKey, experiment.getAudienceConditions()); Boolean resolveReturn = evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey); @@ -78,12 +93,32 @@ public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig proj } } + /** + * Determines whether a user satisfies audience conditions for the experiment. + * + * @param projectConfig the current projectConfig + * @param experiment the experiment we are evaluating audiences for + * @param attributes the attributes of the user + * @param loggingEntityType It can be either experiment or rule. + * @param loggingKey In case of loggingEntityType is experiment it will be experiment key or else it will be rule number. + * @return whether the user meets the criteria for the experiment + */ + public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig projectConfig, + @Nonnull Experiment experiment, + @Nonnull Map attributes, + @Nonnull String loggingEntityType, + @Nonnull String loggingKey) { + return doesUserMeetAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, null, null); + } + @Nullable public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, @Nonnull Experiment experiment, @Nonnull Map attributes, @Nonnull String loggingEntityType, - @Nonnull String loggingKey) { + @Nonnull String loggingKey, + @Nullable List options, + @Nullable DecisionReasons reasons) { List experimentAudienceIds = experiment.getAudienceIds(); // if there are no audiences, ALL users should be part of the experiment @@ -103,30 +138,49 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, Boolean result = implicitOr.evaluate(projectConfig, attributes); - logger.info("Audiences for {} \"{}\" collectively evaluated to {}.", loggingEntityType, loggingKey, result); + DecisionService.logInfo(logger, reasons, "Audiences for %s \"%s\" collectively evaluated to %b.", loggingEntityType, loggingKey, result); return result; } + @Nullable + public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, + @Nonnull Experiment experiment, + @Nonnull Map attributes, + @Nonnull String loggingEntityType, + @Nonnull String loggingKey) { + return evaluateAudience(projectConfig, experiment, attributes, loggingEntityType, loggingKey, null, null); + } + @Nullable public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectConfig, @Nonnull Experiment experiment, @Nonnull Map attributes, @Nonnull String loggingEntityType, - @Nonnull String loggingKey) { + @Nonnull String loggingKey, + @Nullable List options, + @Nullable DecisionReasons reasons) { Condition conditions = experiment.getAudienceConditions(); if (conditions == null) return null; try { Boolean result = conditions.evaluate(projectConfig, attributes); - logger.info("Audiences for {} \"{}\" collectively evaluated to {}.", loggingEntityType, loggingKey, result); + DecisionService.logInfo(logger, reasons,"Audiences for %s \"%s\" collectively evaluated to %b.", loggingEntityType, loggingKey, result); return result; } catch (Exception e) { - logger.error("Condition invalid", e); + DecisionService.logError(logger, reasons,"Condition invalid: %s", e.getMessage()); return null; } } + @Nullable + public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectConfig, + @Nonnull Experiment experiment, + @Nonnull Map attributes, + @Nonnull String loggingEntityType, + @Nonnull String loggingKey) { + return evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, null, null); + } -} +} \ No newline at end of file diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index c17184f10..a93d249f9 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -99,7 +99,9 @@ public OptimizelyDecision decide(@Nonnull String key, flag, userId, copiedAttributes, - projectConfig); + projectConfig, + allOptions, + decisionReasons); FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT; SourceInfo sourceInfo = new RolloutSourceInfo(); From 6800e66b1257779f8e594b0cfe890a6c7b597a4f Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 7 Oct 2020 17:18:38 -0700 Subject: [PATCH 04/44] add decide-all api --- .../OptimizelyUserContext.java | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index a93d249f9..06a67cc68 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -196,7 +196,26 @@ public OptimizelyDecision decide(String key) { */ public Map decideAll(@Nonnull String[] keys, @Nonnull OptimizelyDecideOption[] options) { - return new HashMap<>(); + Map decisionMap = new HashMap<>(); + + ProjectConfig projectConfig = optimizely.getProjectConfig(); + if (projectConfig == null) { + logger.error("Optimizely instance is not valid, failing isFeatureEnabled call."); + return decisionMap; + } + + if (keys.length == 0) return decisionMap; + + List allOptions = getAllOptions(options); + + for (String key : keys) { + OptimizelyDecision decision = decide(key, options); + if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.getEnabled()) { + decisionMap.put(key, decision); + } + } + + return decisionMap; } /** @@ -216,7 +235,18 @@ public Map decideAll(@Nonnull String[] keys) { * @return All decision results mapped by flag keys. */ public Map decideAll(@Nonnull OptimizelyDecideOption[] options) { - String[] allFlagKeys = {}; + Map decisionMap = new HashMap<>(); + + ProjectConfig projectConfig = optimizely.getProjectConfig(); + if (projectConfig == null) { + logger.error("Optimizely instance is not valid, failing isFeatureEnabled call."); + return decisionMap; + } + + List allFlags = projectConfig.getFeatureFlags(); + String[] allFlagKeys = new String[allFlags.size()]; + for (int i = 0; i < allFlags.size(); i++) allFlagKeys[i] = allFlags.get(i).getKey(); + return decideAll(allFlagKeys, options); } From 642f957534c11a74c68a37112b38bcdde9ba9c98 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 7 Oct 2020 17:47:59 -0700 Subject: [PATCH 05/44] fix errors --- .../OptimizelyDecideOption.java | 16 ++++++++ .../OptimizelyDecision.java | 37 ++++++++++++++----- .../OptimizelyUserContext.java | 16 ++++++++ .../OptimizelyDecisionTest.java | 25 +++++++++++-- .../OptimizelyUserContextTest.java | 16 ++++++++ 5 files changed, 98 insertions(+), 12 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java index 411a6ffa5..e7f6d6874 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.optimizely.ab.optimizelyusercontext; public enum OptimizelyDecideOption { diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java index a9a0f6c17..33d95f1ab 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java @@ -1,9 +1,27 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.optimizely.ab.optimizelyusercontext; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.List; public class OptimizelyDecision { @Nullable @@ -11,7 +29,7 @@ public class OptimizelyDecision { private final boolean enabled; - @Nonnull + @Nullable private final OptimizelyJSON variables; @Nullable @@ -24,16 +42,16 @@ public class OptimizelyDecision { private final OptimizelyUserContext userContext; @Nonnull - private String[] reasons; + private List reasons; public OptimizelyDecision(@Nullable String variationKey, boolean enabled, - @Nonnull OptimizelyJSON variables, + @Nullable OptimizelyJSON variables, @Nullable String ruleKey, @Nonnull String flagKey, - @Nullable OptimizelyUserContext userContext, - @Nonnull String[] reasons) { + @Nonnull OptimizelyUserContext userContext, + @Nonnull List reasons) { this.variationKey = variationKey; this.enabled = enabled; this.variables = variables; @@ -73,20 +91,21 @@ public OptimizelyUserContext getUserContext() { } @Nonnull - public String[] getReasons() { + public List getReasons() { return reasons; } public static OptimizelyDecision createErrorDecision(@Nonnull String key, @Nonnull OptimizelyUserContext user, - String error) { - return new OptimizelyDecision(null, + @Nonnull String error) { + return new OptimizelyDecision( + null, false, null, null, key, user, - new String[]{error}); + Arrays.asList(error)); } public boolean hasFailed() { diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index 8b62654f4..47bcdb03b 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.optimizely.ab.optimizelyusercontext; import com.optimizely.ab.Optimizely; diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java index 3730736f3..dd010731b 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java @@ -1,9 +1,28 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.optimizely.ab.optimizelyusercontext; import com.optimizely.ab.Optimizely; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.junit.Test; +import java.util.ArrayList; +import java.util.List; + import static junit.framework.TestCase.assertEquals; public class OptimizelyDecisionTest { @@ -16,7 +35,7 @@ public void testOptimizelyDecision() { String ruleKey = null; String flagKey = "flag1"; OptimizelyUserContext userContext = new OptimizelyUserContext(Optimizely.builder().build(), "tester"); - String[] reasons = new String[0]; + List reasons = new ArrayList<>(); OptimizelyDecision decision = new OptimizelyDecision( variationKey, @@ -51,8 +70,8 @@ public void testCreateErrorDecision() { assertEquals(decision.getRuleKey(), null); assertEquals(decision.getFlagKey(), flagKey); assertEquals(decision.getUserContext(), userContext); - assertEquals(decision.getReasons().length, 1); - assertEquals(decision.getReasons()[0], error); + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), error); } } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java index 7631ff756..3f08873f4 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.optimizely.ab.optimizelyusercontext; import com.google.common.base.Charsets; From d4e2b1c0ca2f55bcec50f552edb7fe3cc480abb7 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 7 Oct 2020 17:52:43 -0700 Subject: [PATCH 06/44] change user-context nonnull --- .../optimizely/ab/optimizelyusercontext/OptimizelyDecision.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java index 33d95f1ab..dce7f763e 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java @@ -38,7 +38,7 @@ public class OptimizelyDecision { @Nonnull private final String flagKey; - @Nullable + @Nonnull private final OptimizelyUserContext userContext; @Nonnull From b8a19a7e4c1ec0643394d74ed4eb2bfbd0dc121a Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 8 Oct 2020 14:36:26 -0700 Subject: [PATCH 07/44] fix existing tests for decision service --- .../com/optimizely/ab/bucketing/Bucketer.java | 58 ++++--- .../ab/bucketing/DecisionService.java | 163 ++++++++++++------ .../ab/internal/ExperimentUtils.java | 4 +- .../ab/notification/DecisionNotification.java | 2 +- .../DecisionReasons.java | 16 ++ .../OptimizelyDecision.java | 2 +- .../OptimizelyUserContext.java | 16 +- .../com/optimizely/ab/OptimizelyTest.java | 26 +-- .../ab/bucketing/DecisionServiceTest.java | 153 ++++++++-------- 9 files changed, 254 insertions(+), 186 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index f423472a7..b45abc203 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -18,11 +18,7 @@ import com.optimizely.ab.annotations.VisibleForTesting; import com.optimizely.ab.bucketing.internal.MurmurHash3; -import com.optimizely.ab.config.Experiment; -import com.optimizely.ab.config.Group; -import com.optimizely.ab.config.ProjectConfig; -import com.optimizely.ab.config.TrafficAllocation; -import com.optimizely.ab.config.Variation; +import com.optimizely.ab.config.*; import com.optimizely.ab.optimizelyusercontext.DecisionReasons; import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; import org.slf4j.Logger; @@ -140,6 +136,36 @@ public Variation bucket(@Nonnull Experiment experiment, @Nonnull ProjectConfig projectConfig, @Nullable List options, @Nullable DecisionReasons reasons) { + + // support existing custom Bucketers + if (isMethodOverriden("bucket", Experiment.class, String.class, ProjectConfig.class)) { + return bucket(experiment, bucketingId, projectConfig); + } + + return bucketCore(experiment, bucketingId, projectConfig, options, reasons); + } + + /** + * Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3. + * + * @param experiment The Experiment in which the user is to be bucketed. + * @param bucketingId string A customer-assigned value used to create the key for the murmur hash. + * @param projectConfig The current projectConfig + * @return Variation the user is bucketed into or null. + */ + @Nullable + public Variation bucket(@Nonnull Experiment experiment, + @Nonnull String bucketingId, + @Nonnull ProjectConfig projectConfig) { + return bucketCore(experiment, bucketingId, projectConfig, null, null); + } + + @Nullable + public Variation bucketCore(@Nonnull Experiment experiment, + @Nonnull String bucketingId, + @Nonnull ProjectConfig projectConfig, + @Nullable List options, + @Nullable DecisionReasons reasons) { // ---------- Bucket User ---------- String groupId = experiment.getGroupId(); // check whether the experiment belongs to a group @@ -170,21 +196,6 @@ public Variation bucket(@Nonnull Experiment experiment, return bucketToVariation(experiment, bucketingId, options, reasons); } - /** - * Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3. - * - * @param experiment The Experiment in which the user is to be bucketed. - * @param bucketingId string A customer-assigned value used to create the key for the murmur hash. - * @param projectConfig The current projectConfig - * @return Variation the user is bucketed into or null. - */ - @Nullable - public Variation bucket(@Nonnull Experiment experiment, - @Nonnull String bucketingId, - @Nonnull ProjectConfig projectConfig) { - return bucket(experiment, bucketingId, projectConfig, null, null); - } - //======== Helper methods ========// /** @@ -200,5 +211,12 @@ int generateBucketValue(int hashCode) { return (int) Math.floor(MAX_TRAFFIC_VALUE * ratio); } + Boolean isMethodOverriden(String methodName, Class... params) { + try { + return getClass() != Bucketer.class && getClass().getDeclaredMethod(methodName, params) != null; + } catch (NoSuchMethodException e) { + return false; + } + } } diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index d1cfd3b77..588df9cf3 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -94,6 +94,30 @@ public Variation getVariation(@Nonnull Experiment experiment, @Nonnull ProjectConfig projectConfig, @Nullable List options, @Nullable DecisionReasons reasons) { + + // support existing custom DecisionServices + if (isMethodOverriden("getVariation", Experiment.class, String.class, Map.class, ProjectConfig.class)) { + return getVariation(experiment, userId, filteredAttributes, projectConfig); + } + + return getVariationCore(experiment, userId, filteredAttributes, projectConfig, options, reasons); + } + + @Nullable + public Variation getVariation(@Nonnull Experiment experiment, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull ProjectConfig projectConfig) { + return getVariationCore(experiment, userId, filteredAttributes, projectConfig, null, null); + } + + @Nullable + public Variation getVariationCore(@Nonnull Experiment experiment, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull ProjectConfig projectConfig, + @Nullable List options, + @Nullable DecisionReasons reasons) { if (!ExperimentUtils.isExperimentActive(experiment, options, reasons)) { return null; } @@ -159,23 +183,6 @@ public Variation getVariation(@Nonnull Experiment experiment, return null; } - /** - * Get a {@link Variation} of an {@link Experiment} for a user to be allocated into. - * - * @param experiment The Experiment the user will be bucketed into. - * @param userId The userId of the user. - * @param filteredAttributes The user's attributes. This should be filtered to just attributes in the Datafile. - * @param projectConfig The current projectConfig - * @return The {@link Variation} the user is allocated into. - */ - @Nullable - public Variation getVariation(@Nonnull Experiment experiment, - @Nonnull String userId, - @Nonnull Map filteredAttributes, - @Nonnull ProjectConfig projectConfig) { - return getVariation(experiment, userId, filteredAttributes, projectConfig, null, null); - } - /** * Get the variation the user is bucketed into for the FeatureFlag * @@ -194,6 +201,30 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, @Nonnull ProjectConfig projectConfig, @Nullable List options, @Nullable DecisionReasons reasons) { + // support existing custom DecisionServices + if (isMethodOverriden("getVariationForFeature", FeatureFlag.class, String.class, Map.class, ProjectConfig.class)) { + return getVariationForFeature(featureFlag, userId, filteredAttributes, projectConfig); + } + + return getVariationForFeatureCore(featureFlag, userId, filteredAttributes, projectConfig, options, reasons); + } + + @Nonnull + public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull ProjectConfig projectConfig) { + + return getVariationForFeatureCore(featureFlag, userId, filteredAttributes, projectConfig,null, null); + } + + @Nonnull + public FeatureDecision getVariationForFeatureCore(@Nonnull FeatureFlag featureFlag, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull ProjectConfig projectConfig, + @Nullable List options, + @Nullable DecisionReasons reasons) { if (!featureFlag.getExperimentIds().isEmpty()) { for (String experimentId : featureFlag.getExperimentIds()) { Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId); @@ -217,25 +248,6 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, return featureDecision; } - /** - * Get the variation the user is bucketed into for the FeatureFlag - * - * @param featureFlag The feature flag the user wants to access. - * @param userId User Identifier - * @param filteredAttributes A map of filtered attributes. - * @param projectConfig The current projectConfig - * @return {@link FeatureDecision} - */ - @Nonnull - public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, - @Nonnull String userId, - @Nonnull Map filteredAttributes, - @Nonnull ProjectConfig projectConfig) { - - return getVariationForFeature(featureFlag, userId, filteredAttributes, projectConfig,null, null); - } - - /** * Try to bucket the user into a rollout rule. * Evaluate the user for rules in priority order by seeing if the user satisfies the audience. @@ -299,6 +311,14 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag return new FeatureDecision(null, null, null); } + @Nonnull + FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull ProjectConfig projectConfig) { + return getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, null, null); + } + /** * Get the variation the user has been whitelisted into. * @@ -330,6 +350,13 @@ Variation getWhitelistedVariation(@Nonnull Experiment experiment, return null; } + @Nullable + Variation getWhitelistedVariation(@Nonnull Experiment experiment, + @Nonnull String userId) { + + return getWhitelistedVariation(experiment, userId, null, null); + } + /** * Get the {@link Variation} that has been stored for the user in the {@link UserProfileService} implementation. * @@ -348,7 +375,7 @@ Variation getStoredVariation(@Nonnull Experiment experiment, @Nullable List options, @Nullable DecisionReasons reasons) { - if (options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return null; + if (options != null && options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return null; // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation @@ -379,6 +406,13 @@ Variation getStoredVariation(@Nonnull Experiment experiment, } } + @Nullable + Variation getStoredVariation(@Nonnull Experiment experiment, + @Nonnull UserProfile userProfile, + @Nonnull ProjectConfig projectConfig) { + return getStoredVariation(experiment, userProfile, projectConfig, null, null); + } + /** * Save a {@link Variation} of an {@link Experiment} for a user in the {@link UserProfileService}. * @@ -394,7 +428,7 @@ void saveVariation(@Nonnull Experiment experiment, @Nullable List options, @Nullable DecisionReasons reasons) { - if (options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return; + if (options != null && options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return; // only save if the user has implemented a user profile service if (userProfileService != null) { @@ -421,6 +455,12 @@ void saveVariation(@Nonnull Experiment experiment, } } + void saveVariation(@Nonnull Experiment experiment, + @Nonnull Variation variation, + @Nonnull UserProfile userProfile) { + saveVariation(experiment, variation, userProfile, null, null); + } + /** * Get the bucketingId of a user if a bucketingId exists in attributes, or else default to userId. * @@ -447,6 +487,11 @@ String getBucketingId(@Nonnull String userId, return bucketingId; } + String getBucketingId(@Nonnull String userId, + @Nonnull Map filteredAttributes) { + return getBucketingId(userId, filteredAttributes, null, null); + } + public ConcurrentHashMap> getForcedVariationMapping() { return forcedVariationMapping; } @@ -536,6 +581,26 @@ public Variation getForcedVariation(@Nonnull Experiment experiment, @Nullable List options, @Nullable DecisionReasons reasons) { + // support existing custom DecisionServices + if (isMethodOverriden("getForcedVariation", Experiment.class, String.class)) { + return getForcedVariation(experiment, userId); + } + + return getForcedVariationCore(experiment, userId, options, reasons); + } + + @Nullable + public Variation getForcedVariation(@Nonnull Experiment experiment, + @Nonnull String userId) { + return getForcedVariationCore(experiment, userId, null, null); + } + + @Nullable + public Variation getForcedVariationCore(@Nonnull Experiment experiment, + @Nonnull String userId, + @Nullable List options, + @Nullable DecisionReasons reasons) { + // if the user id is invalid, return false. if (!validateUserId(userId)) { return null; @@ -558,20 +623,6 @@ public Variation getForcedVariation(@Nonnull Experiment experiment, return null; } - /** - * Gets the forced variation for a given user and experiment. - * - * @param experiment The experiment forced. - * @param userId The user ID to be used for bucketing. - * @return The variation the user was bucketed into. This value can be null if the - * forced variation fails. - */ - @Nullable - public Variation getForcedVariation(@Nonnull Experiment experiment, - @Nonnull String userId) { - return getForcedVariation(experiment, userId, null, null); - } - /** * Helper function to check that the provided userId is valid * @@ -611,4 +662,12 @@ public static void logDebug(Logger logger, DecisionReasons reasons, String forma if (reasons != null) reasons.addInfo(message); } + Boolean isMethodOverriden(String methodName, Class... params) { + try { + return getClass() != DecisionService.class && getClass().getDeclaredMethod(methodName, params) != null; + } catch (NoSuchMethodException e) { + return false; + } + } + } diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java index c1d40b8a3..68628516d 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java @@ -138,7 +138,7 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, Boolean result = implicitOr.evaluate(projectConfig, attributes); - DecisionService.logInfo(logger, reasons, "Audiences for %s \"%s\" collectively evaluated to %b.", loggingEntityType, loggingKey, result); + DecisionService.logInfo(logger, reasons, "Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); return result; } @@ -166,7 +166,7 @@ public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectC try { Boolean result = conditions.evaluate(projectConfig, attributes); - DecisionService.logInfo(logger, reasons,"Audiences for %s \"%s\" collectively evaluated to %b.", loggingEntityType, loggingKey, result); + DecisionService.logInfo(logger, reasons,"Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); return result; } catch (Exception e) { DecisionService.logError(logger, reasons,"Condition invalid: %s", e.getMessage()); diff --git a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java index ad95a8ded..d98b12e41 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java @@ -398,7 +398,7 @@ public FlagDecisionNotificationBuilder withEnabled(Boolean enabled) { } public FlagDecisionNotificationBuilder withVariables(Object variables) { - this.enabled = enabled; + this.variables = variables; return this; } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java index 6cb8c6acb..7230b7291 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.optimizely.ab.optimizelyusercontext; import java.util.ArrayList; diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java index 1fd7ccabd..dce7f763e 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java @@ -50,7 +50,7 @@ public OptimizelyDecision(@Nullable String variationKey, @Nullable OptimizelyJSON variables, @Nullable String ruleKey, @Nonnull String flagKey, - @Nullable OptimizelyUserContext userContext, + @Nonnull OptimizelyUserContext userContext, @Nonnull List reasons) { this.variationKey = variationKey; this.enabled = enabled; diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index 9e8782bbe..b0c682db2 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -21,9 +21,6 @@ import com.optimizely.ab.bucketing.FeatureDecision; import com.optimizely.ab.config.*; import com.optimizely.ab.notification.DecisionNotification; -import com.optimizely.ab.notification.FeatureTestSourceInfo; -import com.optimizely.ab.notification.RolloutSourceInfo; -import com.optimizely.ab.notification.SourceInfo; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,8 +43,6 @@ public class OptimizelyUserContext { public final static String SDK_NOT_READY = "Optimizely SDK not configured properly yet"; public final static String FLAG_KEY_INVALID = "Flag key \"%s\" is not in datafile."; public final static String VARIABLE_VALUE_INVALID = "Variable value for key \"%s\" is invalid or wrong type."; - public final static String OPTIMIZELY_JSON_ERROR = "Invalid variables for OptimizelyJSON."; - public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId, @@ -119,9 +114,6 @@ public OptimizelyDecision decide(@Nonnull String key, allOptions, decisionReasons); - FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT; - SourceInfo sourceInfo = new RolloutSourceInfo(); - if (flagDecision.variation != null) { if (flagDecision.decisionSource.equals(FeatureDecision.DecisionSource.FEATURE_TEST)) { if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) { @@ -133,9 +125,6 @@ public OptimizelyDecision decide(@Nonnull String key, flagDecision.variation); sentEvent = true; } - - decisionSource = flagDecision.decisionSource; - sourceInfo = new FeatureTestSourceInfo(flagDecision.experiment.getKey(), flagDecision.variation.getKey()); } else { String message = String.format("The user \"%s\" is not included in an experiment for flag \"%s\".", userId, key); logger.info(message); @@ -156,9 +145,6 @@ public OptimizelyDecision decide(@Nonnull String key, } OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap); - if (optimizelyJSON == null) { - decisionReasons.addError(OPTIMIZELY_JSON_ERROR); - } List reasonsToReport = decisionReasons.toReport(allOptions); String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null; @@ -205,7 +191,7 @@ public OptimizelyDecision decide(String key) { *
        *
      • If the SDK finds an error for a key, the response will include a decision for the key showing reasons for the error. *
      • The SDK will always return key-mapped decisions. When it can not process requests, it’ll return an empty map after logging the errors. - *
          + *
        * @param keys An array of flag keys for which decisions will be made. * @param options An array of options for decision-making. * @return All decision results mapped by flag keys. diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index 2aced2256..d77d54875 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -378,7 +378,7 @@ public void activateForNullVariation() throws Exception { Experiment activatedExperiment = validProjectConfig.getExperiments().get(0); Map testUserAttributes = Collections.singletonMap("browser_type", "chromey"); - when(mockBucketer.bucket(activatedExperiment, testBucketingId, validProjectConfig)).thenReturn(null); + when(mockBucketer.bucket(activatedExperiment, testBucketingId, validProjectConfig, null, null)).thenReturn(null); logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"" + activatedExperiment.getKey() + "\"."); @@ -937,7 +937,7 @@ public void activateWithInvalidDatafile() throws Exception { assertNull(expectedVariation); // make sure we didn't even attempt to bucket the user - verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)); + verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); } //======== track tests ========// @@ -1238,7 +1238,7 @@ public void trackWithInvalidDatafile() throws Exception { optimizely.track("event_with_launched_and_running_experiments", genericUserId); // make sure we didn't even attempt to bucket the user or fire any conversion events - verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)); + verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class)); } @@ -1255,7 +1255,7 @@ public void getVariation() throws Exception { Optimizely optimizely = optimizelyBuilder.withBucketing(mockBucketer).build(); - when(mockBucketer.bucket(activatedExperiment, testUserId, validProjectConfig)).thenReturn(bucketedVariation); + when(mockBucketer.bucket(activatedExperiment, testUserId, validProjectConfig, null, null)).thenReturn(bucketedVariation); Map testUserAttributes = new HashMap<>(); testUserAttributes.put("browser_type", "chrome"); @@ -1265,7 +1265,7 @@ public void getVariation() throws Exception { testUserAttributes); // verify that the bucketing algorithm was called correctly - verify(mockBucketer).bucket(activatedExperiment, testUserId, validProjectConfig); + verify(mockBucketer).bucket(activatedExperiment, testUserId, validProjectConfig, null, null); assertThat(actualVariation, is(bucketedVariation)); // verify that we didn't attempt to dispatch an event @@ -1286,13 +1286,13 @@ public void getVariationWithExperimentKey() throws Exception { .withConfig(noAudienceProjectConfig) .build(); - when(mockBucketer.bucket(activatedExperiment, testUserId, noAudienceProjectConfig)).thenReturn(bucketedVariation); + when(mockBucketer.bucket(activatedExperiment, testUserId, noAudienceProjectConfig, null, null)).thenReturn(bucketedVariation); // activate the experiment Variation actualVariation = optimizely.getVariation(activatedExperiment.getKey(), testUserId); // verify that the bucketing algorithm was called correctly - verify(mockBucketer).bucket(activatedExperiment, testUserId, noAudienceProjectConfig); + verify(mockBucketer).bucket(activatedExperiment, testUserId, noAudienceProjectConfig, null, null); assertThat(actualVariation, is(bucketedVariation)); // verify that we didn't attempt to dispatch an event @@ -1347,7 +1347,7 @@ public void getVariationWithAudiences() throws Exception { Experiment experiment = validProjectConfig.getExperiments().get(0); Variation bucketedVariation = experiment.getVariations().get(0); - when(mockBucketer.bucket(experiment, testUserId, validProjectConfig)).thenReturn(bucketedVariation); + when(mockBucketer.bucket(experiment, testUserId, validProjectConfig, null, null)).thenReturn(bucketedVariation); Optimizely optimizely = optimizelyBuilder.withBucketing(mockBucketer).build(); @@ -1356,7 +1356,7 @@ public void getVariationWithAudiences() throws Exception { Variation actualVariation = optimizely.getVariation(experiment.getKey(), testUserId, testUserAttributes); - verify(mockBucketer).bucket(experiment, testUserId, validProjectConfig); + verify(mockBucketer).bucket(experiment, testUserId, validProjectConfig, null, null); assertThat(actualVariation, is(bucketedVariation)); } @@ -1397,7 +1397,7 @@ public void getVariationNoAudiences() throws Exception { Experiment experiment = noAudienceProjectConfig.getExperiments().get(0); Variation bucketedVariation = experiment.getVariations().get(0); - when(mockBucketer.bucket(experiment, testUserId, noAudienceProjectConfig)).thenReturn(bucketedVariation); + when(mockBucketer.bucket(experiment, testUserId, noAudienceProjectConfig, null, null)).thenReturn(bucketedVariation); Optimizely optimizely = optimizelyBuilder .withConfig(noAudienceProjectConfig) @@ -1406,7 +1406,7 @@ public void getVariationNoAudiences() throws Exception { Variation actualVariation = optimizely.getVariation(experiment.getKey(), testUserId); - verify(mockBucketer).bucket(experiment, testUserId, noAudienceProjectConfig); + verify(mockBucketer).bucket(experiment, testUserId, noAudienceProjectConfig, null, null); assertThat(actualVariation, is(bucketedVariation)); } @@ -1464,7 +1464,7 @@ public void getVariationForGroupExperimentWithMatchingAttributes() throws Except attributes.put("browser_type", "chrome"); } - when(mockBucketer.bucket(experiment, "user", validProjectConfig)).thenReturn(variation); + when(mockBucketer.bucket(experiment,"user", validProjectConfig, null, null)).thenReturn(variation); Optimizely optimizely = optimizelyBuilder.withBucketing(mockBucketer).build(); @@ -1522,7 +1522,7 @@ public void getVariationWithInvalidDatafile() throws Exception { assertNull(variation); // make sure we didn't even attempt to bucket the user - verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)); + verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); } //======== Notification listeners ========// diff --git a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java index 2a3030314..c6d2b4a1c 100644 --- a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java @@ -15,18 +15,12 @@ ***************************************************************************/ package com.optimizely.ab.bucketing; -import com.optimizely.ab.config.Experiment; -import com.optimizely.ab.config.FeatureFlag; -import com.optimizely.ab.config.ProjectConfig; -import com.optimizely.ab.config.DatafileProjectConfigTestUtils; -import com.optimizely.ab.config.Rollout; -import com.optimizely.ab.config.TrafficAllocation; -import com.optimizely.ab.config.ValidProjectConfigV4; -import com.optimizely.ab.config.Variation; +import ch.qos.logback.classic.Level; +import com.optimizely.ab.config.*; import com.optimizely.ab.error.ErrorHandler; -import com.optimizely.ab.internal.LogbackVerifier; - import com.optimizely.ab.internal.ControlAttribute; +import com.optimizely.ab.internal.LogbackVerifier; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -39,42 +33,13 @@ import java.util.List; import java.util.Map; -import ch.qos.logback.classic.Level; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.noAudienceProjectConfigV3; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; -import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; -import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_NATIONALITY_KEY; -import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_ENGLISH_CITIZENS_VALUE; -import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; -import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_MULTI_VARIATE_FEATURE; -import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_SINGLE_VARIABLE_INTEGER; -import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_MULTI_VARIATE_FEATURE_KEY; -import static com.optimizely.ab.config.ValidProjectConfigV4.ROLLOUT_2; -import static com.optimizely.ab.config.ValidProjectConfigV4.ROLLOUT_3_EVERYONE_ELSE_RULE; -import static com.optimizely.ab.config.ValidProjectConfigV4.ROLLOUT_3_EVERYONE_ELSE_RULE_ENABLED_VARIATION; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.*; +import static com.optimizely.ab.config.ValidProjectConfigV4.*; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyMapOf; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.atMost; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; public class DecisionServiceTest { @@ -130,8 +95,8 @@ public void getVariationWhitelistedPrecedesAudienceEval() throws Exception { // no attributes provided for a experiment that has an audience assertThat(decisionService.getVariation(experiment, whitelistedUserId, Collections.emptyMap(), validProjectConfig), is(expectedVariation)); - verify(decisionService).getWhitelistedVariation(experiment, whitelistedUserId); - verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), any(ProjectConfig.class)); + verify(decisionService).getWhitelistedVariation(experiment, whitelistedUserId, null, null); + verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), any(ProjectConfig.class), anyObject(), anyObject()); } /** @@ -153,7 +118,7 @@ public void getForcedVariationBeforeWhitelisting() throws Exception { assertThat(decisionService.getVariation(experiment, whitelistedUserId, Collections.emptyMap(), validProjectConfig), is(expectedVariation)); //verify(decisionService).getForcedVariation(experiment.getKey(), whitelistedUserId); - verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), any(ProjectConfig.class)); + verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), any(ProjectConfig.class), anyObject(), anyObject()); assertEquals(decisionService.getWhitelistedVariation(experiment, whitelistedUserId), whitelistVariation); assertTrue(decisionService.setForcedVariation(experiment, whitelistedUserId, null)); assertNull(decisionService.getForcedVariation(experiment, whitelistedUserId)); @@ -177,7 +142,7 @@ public void getVariationForcedPrecedesAudienceEval() throws Exception { // no attributes provided for a experiment that has an audience assertThat(decisionService.getVariation(experiment, genericUserId, Collections.emptyMap(), validProjectConfig), is(expectedVariation)); - verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), eq(validProjectConfig)); + verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), eq(validProjectConfig), anyObject(), anyObject()); assertEquals(decisionService.setForcedVariation(experiment, genericUserId, null), true); assertNull(decisionService.getForcedVariation(experiment, genericUserId)); } @@ -322,14 +287,18 @@ public void getVariationForFeatureReturnsNullWhenItGetsNoVariationsForExperiment any(Experiment.class), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); // do not bucket to any rollouts doReturn(new FeatureDecision(null, null, null)).when(decisionService).getVariationForFeatureInRollout( any(FeatureFlag.class), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); // try to get a variation back from the decision service for the feature flag @@ -363,14 +332,18 @@ public void getVariationForFeatureReturnsVariationReturnedFromGetVariation() { eq(ValidProjectConfigV4.EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); doReturn(ValidProjectConfigV4.VARIATION_MUTEX_GROUP_EXP_2_VAR_1).when(decisionService).getVariation( eq(ValidProjectConfigV4.EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); FeatureDecision featureDecision = decisionService.getVariationForFeature( @@ -409,7 +382,9 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout() eq(featureExperiment), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); // return variation for rollout @@ -418,7 +393,9 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout() eq(featureFlag), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); // make sure we get the right variation back @@ -436,7 +413,9 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout() any(FeatureFlag.class), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); // make sure we ask for experiment bucketing once @@ -444,7 +423,9 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout() any(Experiment.class), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); } @@ -469,7 +450,9 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails eq(featureExperiment), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); // return variation for rollout @@ -478,7 +461,9 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails eq(featureFlag), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); // make sure we get the right variation back @@ -496,7 +481,9 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails any(FeatureFlag.class), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); // make sure we ask for experiment bucketing once @@ -504,7 +491,9 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails any(Experiment.class), anyString(), anyMapOf(String.class, String.class), - any(ProjectConfig.class) + any(ProjectConfig.class), + anyObject(), + anyObject() ); logbackVerifier.expectMessage( @@ -550,7 +539,7 @@ public void getVariationForFeatureInRolloutReturnsNullWhenFeatureIsNotAttachedTo @Test public void getVariationForFeatureInRolloutReturnsNullWhenUserIsExcludedFromAllTraffic() { Bucketer mockBucketer = mock(Bucketer.class); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class))).thenReturn(null); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); DecisionService decisionService = new DecisionService( mockBucketer, @@ -572,7 +561,7 @@ public void getVariationForFeatureInRolloutReturnsNullWhenUserIsExcludedFromAllT // with fall back bucketing, the user has at most 2 chances to get bucketed with traffic allocation // one chance with the audience rollout rule // one chance with the everyone else rule - verify(mockBucketer, atMost(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)); + verify(mockBucketer, atMost(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); } /** @@ -583,7 +572,7 @@ public void getVariationForFeatureInRolloutReturnsNullWhenUserIsExcludedFromAllT @Test public void getVariationForFeatureInRolloutReturnsNullWhenUserFailsAllAudiencesAndTraffic() { Bucketer mockBucketer = mock(Bucketer.class); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class))).thenReturn(null); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, null); @@ -597,7 +586,7 @@ public void getVariationForFeatureInRolloutReturnsNullWhenUserFailsAllAudiencesA assertNull(featureDecision.decisionSource); // user is only bucketed once for the everyone else rule - verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)); + verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); } /** @@ -611,7 +600,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsAllAudie Rollout rollout = ROLLOUT_2; Experiment everyoneElseRule = rollout.getExperiments().get(rollout.getExperiments().size() - 1); Variation expectedVariation = everyoneElseRule.getVariations().get(0); - when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class))).thenReturn(expectedVariation); + when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(expectedVariation); DecisionService decisionService = new DecisionService( mockBucketer, @@ -637,7 +626,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsAllAudie assertEquals(FeatureDecision.DecisionSource.ROLLOUT, featureDecision.decisionSource); // verify user is only bucketed once for everyone else rule - verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)); + verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); } /** @@ -652,8 +641,8 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTrafficI Rollout rollout = ROLLOUT_2; Experiment everyoneElseRule = rollout.getExperiments().get(rollout.getExperiments().size() - 1); Variation expectedVariation = everyoneElseRule.getVariations().get(0); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class))).thenReturn(null); - when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class))).thenReturn(expectedVariation); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); + when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(expectedVariation); DecisionService decisionService = new DecisionService( mockBucketer, @@ -675,7 +664,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTrafficI logbackVerifier.expectMessage(Level.DEBUG, "User \"genericUserId\" meets conditions for targeting rule \"Everyone Else\"."); // verify user is only bucketed once for everyone else rule - verify(mockBucketer, times(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)); + verify(mockBucketer, times(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); } /** @@ -694,9 +683,9 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTrafficI Variation englishCitizenVariation = englishCitizensRule.getVariations().get(0); Experiment everyoneElseRule = rollout.getExperiments().get(rollout.getExperiments().size() - 1); Variation expectedVariation = everyoneElseRule.getVariations().get(0); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class))).thenReturn(null); - when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class))).thenReturn(expectedVariation); - when(mockBucketer.bucket(eq(englishCitizensRule), anyString(), any(ProjectConfig.class))).thenReturn(englishCitizenVariation); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); + when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(expectedVariation); + when(mockBucketer.bucket(eq(englishCitizensRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(englishCitizenVariation); DecisionService decisionService = new DecisionService( mockBucketer, @@ -721,7 +710,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTrafficI assertEquals(FeatureDecision.DecisionSource.ROLLOUT, featureDecision.decisionSource); // verify user is only bucketed once for everyone else rule - verify(mockBucketer, times(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)); + verify(mockBucketer, times(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); } /** @@ -737,9 +726,9 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTargetin Variation englishCitizenVariation = englishCitizensRule.getVariations().get(0); Experiment everyoneElseRule = rollout.getExperiments().get(rollout.getExperiments().size() - 1); Variation everyoneElseVariation = everyoneElseRule.getVariations().get(0); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class))).thenReturn(null); - when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class))).thenReturn(everyoneElseVariation); - when(mockBucketer.bucket(eq(englishCitizensRule), anyString(), any(ProjectConfig.class))).thenReturn(englishCitizenVariation); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); + when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(everyoneElseVariation); + when(mockBucketer.bucket(eq(englishCitizensRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(englishCitizenVariation); DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, null); @@ -759,7 +748,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTargetin logbackVerifier.expectMessage(Level.DEBUG, "Audience \"4194404272\" evaluated to true."); logbackVerifier.expectMessage(Level.INFO, "Audiences for rule \"3\" collectively evaluated to true"); // verify user is only bucketed once for everyone else rule - verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class)); + verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); } //========= white list tests ==========/ @@ -912,7 +901,7 @@ public void getVariationSavesBucketedVariationIntoUserProfile() throws Exception Collections.singletonMap(experiment.getId(), decision)); Bucketer mockBucketer = mock(Bucketer.class); - when(mockBucketer.bucket(experiment, userProfileId, noAudienceProjectConfig)).thenReturn(variation); + when(mockBucketer.bucket(experiment, userProfileId, noAudienceProjectConfig, null, null)).thenReturn(variation); DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, userProfileService); @@ -974,7 +963,7 @@ public void getVariationSavesANewUserProfile() throws Exception { UserProfileService userProfileService = mock(UserProfileService.class); DecisionService decisionService = new DecisionService(bucketer, mockErrorHandler, userProfileService); - when(bucketer.bucket(experiment, userProfileId, noAudienceProjectConfig)).thenReturn(variation); + when(bucketer.bucket(experiment, userProfileId, noAudienceProjectConfig, null, null)).thenReturn(variation); when(userProfileService.lookup(userProfileId)).thenReturn(null); assertEquals(variation, decisionService.getVariation(experiment, userProfileId, Collections.emptyMap(), noAudienceProjectConfig)); @@ -988,7 +977,7 @@ public void getVariationBucketingId() throws Exception { Experiment experiment = validProjectConfig.getExperiments().get(0); Variation expectedVariation = experiment.getVariations().get(0); - when(bucketer.bucket(experiment, "bucketId", validProjectConfig)).thenReturn(expectedVariation); + when(bucketer.bucket(experiment, "bucketId", validProjectConfig, null, null)).thenReturn(expectedVariation); Map attr = new HashMap(); attr.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), "bucketId"); @@ -1012,8 +1001,8 @@ public void getVariationForRolloutWithBucketingId() { attributes.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), bucketingId); Bucketer bucketer = mock(Bucketer.class); - when(bucketer.bucket(rolloutRuleExperiment, userId, v4ProjectConfig)).thenReturn(null); - when(bucketer.bucket(rolloutRuleExperiment, bucketingId, v4ProjectConfig)).thenReturn(rolloutVariation); + when(bucketer.bucket(rolloutRuleExperiment, userId, v4ProjectConfig, null, null)).thenReturn(null); + when(bucketer.bucket(rolloutRuleExperiment, bucketingId, v4ProjectConfig, null, null)).thenReturn(rolloutVariation); DecisionService decisionService = spy(new DecisionService( bucketer, From e3cedab51f6af0f3704fa700a2a2a73a5bb2b693 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 8 Oct 2020 17:13:29 -0700 Subject: [PATCH 08/44] add more decide tests --- .../OptimizelyDecision.java | 18 ++ .../OptimizelyUserContext.java | 17 +- .../OptimizelyUserContextTest.java | 306 +++++++++++++++++- 3 files changed, 334 insertions(+), 7 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java index dce7f763e..9d7d67ff1 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java @@ -112,4 +112,22 @@ public boolean hasFailed() { return variationKey == null; } + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) return false; + if (obj == this) return true; + OptimizelyDecision d = (OptimizelyDecision) obj; + return equals(variationKey, d.getVariationKey()) && + equals(enabled, d.getEnabled()) && + equals(variables.toMap(), d.getVariables().toMap()) && + equals(ruleKey, d.getRuleKey()) && + equals(flagKey, d.getFlagKey()) && + equals(userContext, d.getUserContext()) && + equals(reasons, d.getReasons()); + } + + private static boolean equals(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index b0c682db2..3119913f0 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -101,9 +101,10 @@ public OptimizelyDecision decide(@Nonnull String key, } List allOptions = getAllOptions(options); - DecisionReasons decisionReasons = new DecisionReasons(); Boolean sentEvent = false; Boolean flagEnabled = false; + DecisionReasons decisionReasons = new DecisionReasons(); + Boolean includeReasons = allOptions.contains(OptimizelyDecideOption.INCLUDE_REASONS); Map copiedAttributes = copyAttributes(); FeatureDecision flagDecision = optimizely.decisionService.getVariationForFeature( @@ -112,7 +113,7 @@ public OptimizelyDecision decide(@Nonnull String key, copiedAttributes, projectConfig, allOptions, - decisionReasons); + includeReasons ? decisionReasons : null); if (flagDecision.variation != null) { if (flagDecision.decisionSource.equals(FeatureDecision.DecisionSource.FEATURE_TEST)) { @@ -141,7 +142,7 @@ public OptimizelyDecision decide(@Nonnull String key, flag, flagDecision.variation, flagEnabled, - decisionReasons); + includeReasons ? decisionReasons : null); } OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap); @@ -330,4 +331,14 @@ private Map getDecisionVariableMap(FeatureFlag flag, return valuesMap; } + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) return false; + if (obj == this) return true; + OptimizelyUserContext userContext = (OptimizelyUserContext) obj; + return userId.equals(userContext.getUserId()) && + attributes.equals(userContext.getAttributes()) && + optimizely.equals(userContext.getOptimizely()); + } + } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java index 3f08873f4..0418eacf7 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -19,6 +19,7 @@ import com.google.common.base.Charsets; import com.google.common.io.Resources; import com.optimizely.ab.Optimizely; +import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.junit.Before; import org.junit.Test; @@ -29,6 +30,8 @@ import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; public class OptimizelyUserContextTest { @@ -45,7 +48,7 @@ public void setUp() throws Exception { } @Test - public void testOptimizelyUserContext_withAttributes() { + public void optimizelyUserContext_withAttributes() { Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId, attributes); @@ -55,7 +58,7 @@ public void testOptimizelyUserContext_withAttributes() { } @Test - public void testOptimizelyUserContext_noAttributes() { + public void optimizelyUserContext_noAttributes() { OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId); assertEquals(user.getOptimizely(), optimizely); @@ -64,7 +67,7 @@ public void testOptimizelyUserContext_noAttributes() { } @Test - public void testSetAttribute() { + public void setAttribute() { Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId, attributes); @@ -84,7 +87,7 @@ public void testSetAttribute() { } @Test - public void testSetAttribute_override() { + public void setAttribute_override() { Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId, attributes); @@ -98,4 +101,299 @@ public void testSetAttribute_override() { assertEquals(newAttributes.get(ATTRIBUTE_HOUSE_KEY), "v2"); } + // decide + + @Test + public void decide() { + String flagKey = "feature_2"; + OptimizelyJSON variablesExpected = optimizely.getAllFeatureVariables(flagKey, userId); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getVariationKey(), "variation_with_traffic"); + assertTrue(decision.getEnabled()); + assertEquals(decision.getVariables().toMap(), variablesExpected.toMap()); + assertEquals(decision.getFlagKey(), flagKey); + assertEquals(decision.getUserContext(), user); + assertTrue(decision.getReasons().isEmpty()); + } + + // decideAll + + @Test + public void decideAll_oneFeature() { + String flagKey = "feature_2"; + String[] flagKeys = {flagKey}; + OptimizelyJSON variablesExpected = optimizely.getAllFeatureVariables(flagKey, userId); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + Map decisions = user.decideAll(flagKeys); + + assertTrue(decisions.size() == 1); + OptimizelyDecision decision = decisions.get(flagKey); + + OptimizelyDecision expDecision = new OptimizelyDecision( + "variation_with_traffic", + true, + variablesExpected, + null, + flagKey, + user, + Collections.emptyList()); + assertEquals(decision, expDecision); + } + + @Test + public void decideAll_twoFeatures() { + String flagKey1 = "feature_1"; + String flagKey2 = "feature_2"; + + String[] flagKeys = {flagKey1, flagKey2}; + OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); + OptimizelyJSON variablesExpected2 = optimizely.getAllFeatureVariables(flagKey2, userId); + + OptimizelyUserContext user = optimizely.createUserContext(userId, Collections.singletonMap("gender", "f")); + Map decisions = user.decideAll(flagKeys); + + assertTrue(decisions.size() == 2); + + assertEquals( + decisions.get(flagKey1), + new OptimizelyDecision("a", + true, + variablesExpected1, + null, + flagKey1, + user, + Collections.emptyList())); + assertEquals( + decisions.get(flagKey2), + new OptimizelyDecision("variation_with_traffic", + true, + variablesExpected2, + null, + flagKey2, + user, + Collections.emptyList())); + } + + @Test + public void decideAll_allFeatures() { + String flagKey1 = "feature_1"; + String flagKey2 = "feature_2"; + String flagKey3 = "feature_3"; + + OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); + OptimizelyJSON variablesExpected2 = optimizely.getAllFeatureVariables(flagKey2, userId); + OptimizelyJSON variablesExpected3 = new OptimizelyJSON(Collections.emptyMap()); + + OptimizelyUserContext user = optimizely.createUserContext(userId, Collections.singletonMap("gender", "f")); + Map decisions = user.decideAll(); + + assertTrue(decisions.size() == 3); + + assertEquals( + decisions.get(flagKey1), + new OptimizelyDecision( + "a", + true, + variablesExpected1, + null, + flagKey1, + user, + Collections.emptyList())); + assertEquals( + decisions.get(flagKey2), + new OptimizelyDecision( + "variation_with_traffic", + true, + variablesExpected2, + null, + flagKey2, + user, + Collections.emptyList())); + assertEquals( + decisions.get(flagKey3), + new OptimizelyDecision( + null, + false, + variablesExpected3, + null, + flagKey3, + user, + Collections.emptyList())); + } + + @Test + public void decideAll_allFeatures_enabledOnly() { + String flagKey1 = "feature_1"; + OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); + + OptimizelyUserContext user = optimizely.createUserContext(userId, Collections.singletonMap("gender", "f")); + OptimizelyDecideOption[] decideOptions = {OptimizelyDecideOption.ENABLED_FLAGS_ONLY}; + Map decisions = user.decideAll(decideOptions); + + assertTrue(decisions.size() == 2); + + assertEquals( + decisions.get(flagKey1), + new OptimizelyDecision( + "a", + true, + variablesExpected1, + null, + flagKey1, + user, + Collections.emptyList())); + } + + // send events + + @Test + public void decide_sendEvent() { + + } + + @Test + public void decide_doNotSendEvent() { + + } + + // options + + @Test + public void decideOptions_disbleTracking() { + } + + @Test + public void decideOptions_useUPSbyDefault() { + } + + @Test + public void decideOptions_bypassUPS_doNotUpdateUPS() { + } + + @Test + public void decideOptions_bypassUPS_doNotReadUPS() { + } + + @Test + public void decideOptions_excludeVariables() { + } + + @Test + public void decideOptions_defaultDecideOption() { + } + + // errors + + @Test + public void decide_sdkNotReady() { + String flagKey = "feature_1"; + + Optimizely optimizely = new Optimizely.Builder().build(); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertNull(decision.getVariationKey()); + assertFalse(decision.getEnabled()); + assertNull(decision.getVariables()); + assertEquals(decision.getFlagKey(), flagKey); + assertEquals(decision.getUserContext(), user); + + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(1), OptimizelyUserContext.SDK_NOT_READY); + } + + @Test + public void decide_invalidFeatureKey() { + + } + + @Test + public void decideAll_sdkNotReady() { + + } + + @Test + public void decideAll_errorDecisionIncluded() { + + } + + // reasons (errors) + + @Test + public void decideReasons_sdkNotReady() { + + } + + @Test + public void decideReasons_featureKeyInvalid() { + + } + + @Test + public void decideReasons_variableValueInvalid() { + + } + + // reasons (logs with includeReasons) + + @Test + public void decideReasons_conditionNoMatchingAudience() {} + @Test + public void decideReasons_conditionInvalidFormat() {} + @Test + public void decideReasons_evaluateAttributeInvalidCondition() {} + @Test + public void decideReasons_evaluateAttributeInvalidType() {} + @Test + public void decideReasons_evaluateAttributeValueOutOfRange() {} + @Test + public void decideReasons_userAttributeInvalidType() {} + @Test + public void decideReasons_userAttributeInvalidMatch() {} + @Test + public void decideReasons_userAttributeNilValue() {} + @Test + public void decideReasons_userAttributeInvalidName() {} + @Test + public void decideReasons_missingAttributeValue() {} + + @Test + public void decideReasons_experimentNotRunning() {} + @Test + public void decideReasons_gotVariationFromUserProfile() {} + @Test + public void decideReasons_forcedVariationFound() {} + @Test + public void decideReasons_forcedVariationFoundButInvalid() {} + @Test + public void decideReasons_userMeetsConditionsForTargetingRule() {} + @Test + public void decideReasons_userDoesntMeetConditionsForTargetingRule() {} + @Test + public void decideReasons_userBucketedIntoTargetingRule() {} + @Test + public void decideReasons_userBucketedIntoEveryoneTargetingRule() {} + @Test + public void decideReasons_userNotBucketedIntoTargetingRule() {} + @Test + public void decideReasons_userBucketedIntoVariationInExperiment() {} + @Test + public void decideReasons_userNotBucketedIntoVariation() {} + @Test + public void decideReasons_userBucketedIntoInvalidVariation() {} + @Test + public void decideReasons_userBucketedIntoExperimentInGroup() {} + @Test + public void decideReasons_userNotBucketedIntoExperimentInGroup() {} + @Test + public void decideReasons_userNotBucketedIntoAnyExperimentInGroup() {} + @Test + public void decideReasons_userBucketedIntoInvalidExperiment() {} + @Test + public void decideReasons_userNotInExperiment() {} } From 2c83fa27229f70dd08ecb66dfd237b9735bc0b05 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 9 Oct 2020 09:54:49 -0700 Subject: [PATCH 09/44] add more tests --- .../ab/optimizelyjson/OptimizelyJSON.java | 15 +++++ .../OptimizelyDecision.java | 15 ++++- .../OptimizelyUserContext.java | 7 +++ .../OptimizelyUserContextTest.java | 57 ++++++++++++++++++- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index 811999e24..2815dea6d 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -157,5 +157,20 @@ private T getValueInternal(@Nullable Object object, Class clazz) { return null; } + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) return false; + if (obj == this) return true; + if (toMap() == null) return false; + + return toMap().equals(((OptimizelyJSON) obj).toMap()); + } + + @Override + public int hashCode() { + int hash = toMap() != null ? toMap().hashCode() : 0; + return hash; + } + } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java index 9d7d67ff1..2eafd4001 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java @@ -119,7 +119,7 @@ public boolean equals(Object obj) { OptimizelyDecision d = (OptimizelyDecision) obj; return equals(variationKey, d.getVariationKey()) && equals(enabled, d.getEnabled()) && - equals(variables.toMap(), d.getVariables().toMap()) && + equals(variables, d.getVariables()) && equals(ruleKey, d.getRuleKey()) && equals(flagKey, d.getFlagKey()) && equals(userContext, d.getUserContext()) && @@ -130,4 +130,17 @@ private static boolean equals(Object a, Object b) { return a == b || (a != null && a.equals(b)); } + @Override + public int hashCode() { + int hash = variationKey != null ? variationKey.hashCode() : 0; + hash = 31 * hash + (enabled ? 1 : 0); + hash = 31 * hash + (variables != null ? variables.hashCode() : 0); + hash = 31 * hash + (ruleKey != null ? ruleKey.hashCode() : 0); + hash = 31 * hash + flagKey.hashCode(); + hash = 31 * hash + userContext.hashCode(); + hash = 31 * hash + reasons.hashCode(); + return hash; + } + + } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index 3119913f0..e90041d49 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -341,4 +341,11 @@ public boolean equals(Object obj) { optimizely.equals(userContext.getOptimizely()); } + @Override + public int hashCode() { + int hash = userId.hashCode(); + hash = 31 * hash + attributes.hashCode(); + hash = 31 * hash + optimizely.hashCode(); + return hash; + } } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java index 0418eacf7..c9945822d 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -293,7 +293,6 @@ public void decide_sdkNotReady() { String flagKey = "feature_1"; Optimizely optimizely = new Optimizely.Builder().build(); - OptimizelyUserContext user = optimizely.createUserContext(userId); OptimizelyDecision decision = user.decide(flagKey); @@ -304,39 +303,91 @@ public void decide_sdkNotReady() { assertEquals(decision.getUserContext(), user); assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(1), OptimizelyUserContext.SDK_NOT_READY); + assertEquals(decision.getReasons().get(0), OptimizelyUserContext.SDK_NOT_READY); } @Test public void decide_invalidFeatureKey() { + String flagKey = "invalid_key"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + assertNull(decision.getVariationKey()); + assertFalse(decision.getEnabled()); + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); } @Test public void decideAll_sdkNotReady() { + String[] flagKeys = {"feature_1"}; + + Optimizely optimizely = new Optimizely.Builder().build(); + OptimizelyUserContext user = optimizely.createUserContext(userId); + Map decisions = user.decideAll(flagKeys); + assertEquals(decisions.size(), 0); } @Test public void decideAll_errorDecisionIncluded() { + String flagKey1 = "feature_2"; + String flagKey2 = "invalid_key"; + String[] flagKeys = {flagKey1, flagKey2}; + OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + Map decisions = user.decideAll(flagKeys); + + assertEquals(decisions.size(), 2); + + assertEquals( + decisions.get(flagKey1), + new OptimizelyDecision( + "variation_with_traffic", + true, + variablesExpected1, + null, + flagKey1, + user, + Collections.emptyList())); + assertEquals( + decisions.get(flagKey2), + OptimizelyDecision.createErrorDecision( + flagKey2, + user, + OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey2))); } // reasons (errors) @Test public void decideReasons_sdkNotReady() { + String flagKey = "feature_1"; + Optimizely optimizely = new Optimizely.Builder().build(); + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), OptimizelyUserContext.SDK_NOT_READY); } @Test public void decideReasons_featureKeyInvalid() { + String flagKey = "invalid_key"; + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); } @Test public void decideReasons_variableValueInvalid() { - } // reasons (logs with includeReasons) From bb9f4dc2e93c32a5d4044cd0ba97eee5d5f70fe6 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 12 Oct 2020 15:10:32 -0700 Subject: [PATCH 10/44] change ruleKey to have a copy of experimentKey --- .../OptimizelyUserContext.java | 4 ++-- .../OptimizelyUserContextTest.java | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index e90041d49..39d99d8a9 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -149,8 +149,8 @@ public OptimizelyDecision decide(@Nonnull String key, List reasonsToReport = decisionReasons.toReport(allOptions); String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null; - // TODO: add ruleKey values when available later. - String ruleKey = null; + // TODO: add ruleKey values when available later. use a copy of experimentKey until then. + String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null; DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder() .withUserId(userId) diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java index c9945822d..625529696 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -137,7 +137,7 @@ public void decideAll_oneFeature() { "variation_with_traffic", true, variablesExpected, - null, + "exp_no_audience", flagKey, user, Collections.emptyList()); @@ -163,7 +163,7 @@ public void decideAll_twoFeatures() { new OptimizelyDecision("a", true, variablesExpected1, - null, + "exp_with_audience", flagKey1, user, Collections.emptyList())); @@ -172,7 +172,7 @@ public void decideAll_twoFeatures() { new OptimizelyDecision("variation_with_traffic", true, variablesExpected2, - null, + "exp_no_audience", flagKey2, user, Collections.emptyList())); @@ -199,7 +199,7 @@ public void decideAll_allFeatures() { "a", true, variablesExpected1, - null, + "exp_with_audience", flagKey1, user, Collections.emptyList())); @@ -209,7 +209,7 @@ public void decideAll_allFeatures() { "variation_with_traffic", true, variablesExpected2, - null, + "exp_no_audience", flagKey2, user, Collections.emptyList())); @@ -242,7 +242,7 @@ public void decideAll_allFeatures_enabledOnly() { "a", true, variablesExpected1, - null, + "exp_with_audience", flagKey1, user, Collections.emptyList())); @@ -349,7 +349,7 @@ public void decideAll_errorDecisionIncluded() { "variation_with_traffic", true, variablesExpected1, - null, + "exp_no_audience", flagKey1, user, Collections.emptyList())); From 3223a640e4d599bdbdad7dd4b54fec54d4ad1575 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 13 Oct 2020 16:56:27 -0700 Subject: [PATCH 11/44] add all tests (except for reasons) --- .../java/com/optimizely/ab/Optimizely.java | 18 +- .../OptimizelyUserContext.java | 10 +- .../optimizely/ab/OptimizelyBuilderTest.java | 8 +- .../OptimizelyUserContextTest.java | 244 +++++++++++++++++- .../config/decide-project-config.json | 4 + 5 files changed, 253 insertions(+), 31 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 1f874b7ac..1bb448212 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -87,7 +87,7 @@ public class Optimizely implements AutoCloseable { @VisibleForTesting final ErrorHandler errorHandler; - public final OptimizelyDecideOption[] defaultDecideOptions; + public final List defaultDecideOptions; private final ProjectConfigManager projectConfigManager; @@ -108,7 +108,7 @@ private Optimizely(@Nonnull EventHandler eventHandler, @Nonnull ProjectConfigManager projectConfigManager, @Nullable OptimizelyConfigManager optimizelyConfigManager, @Nonnull NotificationCenter notificationCenter, - @Nonnull OptimizelyDecideOption[] defaultDecideOptions + @Nonnull List defaultDecideOptions ) { this.eventHandler = eventHandler; this.eventProcessor = eventProcessor; @@ -1217,7 +1217,7 @@ public static class Builder { private OptimizelyConfigManager optimizelyConfigManager; private UserProfileService userProfileService; private NotificationCenter notificationCenter; - private OptimizelyDecideOption[] defaultDecideOptions; + private List defaultDecideOptions; // For backwards compatibility private AtomicProjectConfigManager fallbackConfigManager = new AtomicProjectConfigManager(); @@ -1289,6 +1289,11 @@ public Builder withDatafile(String datafile) { return this; } + public Builder withDefaultDecideOptions(OptimizelyDecideOption[] options) { + this.defaultDecideOptions = new ArrayList<>(Arrays.asList(options)); + return this; + } + // Helper functions for making testing easier protected Builder withBucketing(Bucketer bucketer) { this.bucketer = bucketer; @@ -1305,11 +1310,6 @@ protected Builder withDecisionService(DecisionService decisionService) { return this; } - protected Builder withDefaultDecideOptions(OptimizelyDecideOption[] options) { - this.defaultDecideOptions = options; - return this; - } - public Optimizely build() { if (errorHandler == null) { @@ -1363,7 +1363,7 @@ public Optimizely build() { } if (defaultDecideOptions == null) { - defaultDecideOptions = new OptimizelyDecideOption[0]; + defaultDecideOptions = new ArrayList<>(); } return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions); diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index 39d99d8a9..67cfa8557 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -40,8 +40,8 @@ public class OptimizelyUserContext { private static final Logger logger = LoggerFactory.getLogger(OptimizelyUserContext.class); - public final static String SDK_NOT_READY = "Optimizely SDK not configured properly yet"; - public final static String FLAG_KEY_INVALID = "Flag key \"%s\" is not in datafile."; + public final static String SDK_NOT_READY = "Optimizely SDK not configured properly yet."; + public final static String FLAG_KEY_INVALID = "No flag was found for key \"%s\"."; public final static String VARIABLE_VALUE_INVALID = "Variable value for key \"%s\" is invalid or wrong type."; public OptimizelyUserContext(@Nonnull Optimizely optimizely, @@ -291,9 +291,9 @@ private Map copyAttributes() { } private List getAllOptions(OptimizelyDecideOption[] options) { - List allOptions = new ArrayList(Arrays.asList(optimizely.defaultDecideOptions)); - allOptions.addAll(Arrays.asList(options)); - return allOptions; + List copiedOptions = new ArrayList(optimizely.defaultDecideOptions); + copiedOptions.addAll(Arrays.asList(options)); + return copiedOptions; } public static String getFlagKeyInvalidMessage(String flagKey) { diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java index 6779830da..45c9511f9 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java @@ -182,14 +182,14 @@ public void withDefaultDecideOptions() throws Exception { Optimizely optimizelyClient = Optimizely.builder(validConfigJsonV4(), mockEventHandler) .build(); - assertEquals(optimizelyClient.defaultDecideOptions.length, 0); + assertEquals(optimizelyClient.defaultDecideOptions.size(), 0); optimizelyClient = Optimizely.builder(validConfigJsonV4(), mockEventHandler) .withDefaultDecideOptions(options) .build(); - assertEquals(optimizelyClient.defaultDecideOptions[0], OptimizelyDecideOption.DISABLE_DECISION_EVENT); - assertEquals(optimizelyClient.defaultDecideOptions[1], OptimizelyDecideOption.ENABLED_FLAGS_ONLY); - assertEquals(optimizelyClient.defaultDecideOptions[2], OptimizelyDecideOption.EXCLUDE_VARIABLES); + assertEquals(optimizelyClient.defaultDecideOptions.get(0), OptimizelyDecideOption.DISABLE_DECISION_EVENT); + assertEquals(optimizelyClient.defaultDecideOptions.get(1), OptimizelyDecideOption.ENABLED_FLAGS_ONLY); + assertEquals(optimizelyClient.defaultDecideOptions.get(2), OptimizelyDecideOption.EXCLUDE_VARIABLES); } } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java index 625529696..18d3730c3 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -18,29 +18,44 @@ import com.google.common.base.Charsets; import com.google.common.io.Resources; +import com.optimizely.ab.EventHandlerRule; import com.optimizely.ab.Optimizely; +import com.optimizely.ab.bucketing.UserProfileService; +import com.optimizely.ab.event.ForwardingEventProcessor; +import com.optimizely.ab.notification.NotificationCenter; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; +import static com.optimizely.ab.notification.DecisionNotification.ExperimentDecisionNotificationBuilder.VARIATION_KEY; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.*; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.*; public class OptimizelyUserContextTest { + @Rule + public EventHandlerRule eventHandler = new EventHandlerRule(); public Optimizely optimizely; + public String datafile; public String userId = "tester"; + boolean isListenerCalled = false; @Before public void setUp() throws Exception { - String datafile = Resources.toString(Resources.getResource("config/decide-project-config.json"), Charsets.UTF_8); + datafile = Resources.toString(Resources.getResource("config/decide-project-config.json"), Charsets.UTF_8); optimizely = new Optimizely.Builder() .withDatafile(datafile) @@ -114,6 +129,7 @@ public void decide() { assertEquals(decision.getVariationKey(), "variation_with_traffic"); assertTrue(decision.getEnabled()); assertEquals(decision.getVariables().toMap(), variablesExpected.toMap()); + assertEquals(decision.getRuleKey(), "exp_no_audience"); assertEquals(decision.getFlagKey(), flagKey); assertEquals(decision.getUserContext(), user); assertTrue(decision.getReasons().isEmpty()); @@ -122,7 +138,7 @@ public void decide() { // decideAll @Test - public void decideAll_oneFeature() { + public void decideAll_oneFlag() { String flagKey = "feature_2"; String[] flagKeys = {flagKey}; OptimizelyJSON variablesExpected = optimizely.getAllFeatureVariables(flagKey, userId); @@ -145,7 +161,7 @@ public void decideAll_oneFeature() { } @Test - public void decideAll_twoFeatures() { + public void decideAll_twoFlags() { String flagKey1 = "feature_1"; String flagKey2 = "feature_2"; @@ -179,7 +195,7 @@ public void decideAll_twoFeatures() { } @Test - public void decideAll_allFeatures() { + public void decideAll_allFlags() { String flagKey1 = "feature_1"; String flagKey2 = "feature_2"; String flagKey3 = "feature_3"; @@ -226,7 +242,7 @@ public void decideAll_allFeatures() { } @Test - public void decideAll_allFeatures_enabledOnly() { + public void decideAll_allFlags_enabledFlagsOnly() { String flagKey1 = "feature_1"; OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); @@ -248,42 +264,227 @@ public void decideAll_allFeatures_enabledOnly() { Collections.emptyList())); } + // trackEvent + + @Test + public void trackEvent() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + Map attributes = Collections.singletonMap("gender", "f"); + String eventKey = "event1"; + Map eventTags = Collections.singletonMap("name", "carrot"); + OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); + user.trackEvent(eventKey, eventTags); + + eventHandler.expectConversion(eventKey, userId, attributes, eventTags); + } + + @Test + public void trackEvent_noEventTags() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + Map attributes = Collections.singletonMap("gender", "f"); + String eventKey = "event1"; + OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); + user.trackEvent(eventKey); + + eventHandler.expectConversion(eventKey, userId, attributes); + } + + @Test + public void trackEvent_emptyAttributes() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + String eventKey = "event1"; + Map eventTags = Collections.singletonMap("name", "carrot"); + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.trackEvent(eventKey, eventTags); + + eventHandler.expectConversion(eventKey, userId, Collections.emptyMap(), eventTags); + } + // send events @Test public void decide_sendEvent() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + String flagKey = "feature_2"; + String experimentId = "10420810910"; + String variationId = "10418551353"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getVariationKey(), "variation_with_traffic"); + eventHandler.expectImpression(experimentId, variationId, userId); } @Test public void decide_doNotSendEvent() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + String flagKey = "feature_2"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.DISABLE_DECISION_EVENT}); + + assertEquals(decision.getVariationKey(), "variation_with_traffic"); } - // options + // notifications @Test - public void decideOptions_disbleTracking() { + public void decisionNotification() { + String flagKey = "feature_2"; + String variationKey = "variation_with_traffic"; + boolean enabled = true; + OptimizelyJSON variables = optimizely.getAllFeatureVariables(flagKey, userId); + String ruleKey = "exp_no_audience"; + List reasons = Collections.emptyList(); + + final Map testDecisionInfoMap = new HashMap<>(); + testDecisionInfoMap.put(FLAG_KEY, flagKey); + testDecisionInfoMap.put(VARIATION_KEY, variationKey); + testDecisionInfoMap.put(ENABLED, enabled); + testDecisionInfoMap.put(VARIABLES, variables.toMap()); + testDecisionInfoMap.put(RULE_KEY, ruleKey); + testDecisionInfoMap.put(REASONS, reasons); + + Map attributes = Collections.singletonMap("gender", "f"); + OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); + + optimizely.addDecisionNotificationHandler( + decisionNotification -> { + Assert.assertEquals(decisionNotification.getType(), NotificationCenter.DecisionNotificationType.FLAG.toString()); + Assert.assertEquals(decisionNotification.getUserId(), userId); + Assert.assertEquals(decisionNotification.getAttributes(), attributes); + Assert.assertEquals(decisionNotification.getDecisionInfo(), testDecisionInfoMap); + isListenerCalled = true; + }); + + isListenerCalled = false; + testDecisionInfoMap.put(DECISION_EVENT_DISPATCHED, true); + user.decide(flagKey); + assertTrue(isListenerCalled); + + isListenerCalled = false; + testDecisionInfoMap.put(DECISION_EVENT_DISPATCHED, false); + user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.DISABLE_DECISION_EVENT}); + assertTrue(isListenerCalled); } + // options + @Test - public void decideOptions_useUPSbyDefault() { + public void decideOptions_bypassUPS() throws Exception { + String flagKey = "feature_2"; // embedding experiment: "exp_no_audience" + String experimentId = "10420810910"; // "exp_no_audience" + String variationId1 = "10418551353"; + String variationId2 = "10418510624"; + String variationKey1 = "variation_with_traffic"; + String variationKey2 = "variation_no_traffic"; + + UserProfileService ups = mock(UserProfileService.class); + when(ups.lookup(userId)).thenReturn(createUserProfileMap(experimentId, variationId2)); + + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withUserProfileService(ups) + .build(); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + // should return variationId2 set by UPS + assertEquals(decision.getVariationKey(), variationKey2); + + decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE}); + // should ignore variationId2 set by UPS and return variationId1 + assertEquals(decision.getVariationKey(), variationKey1); + // also should not save either + verify(ups, never()).save(anyObject()); } @Test - public void decideOptions_bypassUPS_doNotUpdateUPS() { + public void decideOptions_excludeVariables() { + String flagKey = "feature_1"; + OptimizelyUserContext user = optimizely.createUserContext(userId); + + OptimizelyDecision decision = user.decide(flagKey); + assertTrue(decision.getVariables().toMap().size() > 0); + + decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.EXCLUDE_VARIABLES}); + assertTrue(decision.getVariables().toMap().size() == 0); } @Test - public void decideOptions_bypassUPS_doNotReadUPS() { + public void decideOptions_includeReasons() { + OptimizelyUserContext user = optimizely.createUserContext(userId); + + String flagKey = "invalid_key"; + OptimizelyDecision decision = user.decide(flagKey); + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); + + decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); + + flagKey = "feature_1"; + decision = user.decide(flagKey); + assertEquals(decision.getReasons().size(), 0); + + decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + assertTrue(decision.getReasons().size() > 0); } - @Test - public void decideOptions_excludeVariables() { + public void decideOptions_disableDispatchEvent() { + // tested already with decide_doNotSendEvent() above + } + + public void decideOptions_enabledFlagsOnly() { + // tested already with decideAll_allFlags_enabledFlagsOnly() above } @Test - public void decideOptions_defaultDecideOption() { + public void decideOptions_defaultDecideOptions() { + OptimizelyDecideOption[] options = { + OptimizelyDecideOption.EXCLUDE_VARIABLES + }; + + optimizely = Optimizely.builder() + .withDatafile(datafile) + .withDefaultDecideOptions(options) + .build(); + + String flagKey = "feature_1"; + OptimizelyUserContext user = optimizely.createUserContext(userId); + + // should be excluded by DefaultDecideOption + OptimizelyDecision decision = user.decide(flagKey); + assertTrue(decision.getVariables().toMap().size() == 0); + + decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS, OptimizelyDecideOption.EXCLUDE_VARIABLES}); + // other options should work as well + assertTrue(decision.getReasons().size() > 0); + // redundant setting ignored + assertTrue(decision.getVariables().toMap().size() == 0); } // errors @@ -447,4 +648,21 @@ public void decideReasons_userNotBucketedIntoAnyExperimentInGroup() {} public void decideReasons_userBucketedIntoInvalidExperiment() {} @Test public void decideReasons_userNotInExperiment() {} + + // utils + + Map createUserProfileMap(String experimentId, String variationId) { + Map userProfileMap = new HashMap(); + userProfileMap.put(UserProfileService.userIdKey, userId); + + Map decisionMap = new HashMap(1); + decisionMap.put(UserProfileService.variationIdKey, variationId); + + Map> decisionsMap = new HashMap>(); + decisionsMap.put(experimentId, decisionMap); + userProfileMap.put(UserProfileService.experimentBucketMapKey, decisionsMap); + + return userProfileMap; + } + } diff --git a/core-api/src/test/resources/config/decide-project-config.json b/core-api/src/test/resources/config/decide-project-config.json index 28f45db01..d6b53bdc0 100644 --- a/core-api/src/test/resources/config/decide-project-config.json +++ b/core-api/src/test/resources/config/decide-project-config.json @@ -314,6 +314,10 @@ } ], "attributes": [ + { + "id": "10401066117", + "key": "gender" + }, { "id": "10401066170", "key": "testvar" From 77929269fc4f886ec447d7a24b1516f7e8fcd02e Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 14 Oct 2020 09:42:40 -0700 Subject: [PATCH 12/44] add more tests --- .../OptimizelyUserContextTest.java | 172 +++++++++++++++--- 1 file changed, 151 insertions(+), 21 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java index 18d3730c3..41cdd3f70 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -21,6 +21,8 @@ import com.optimizely.ab.EventHandlerRule; import com.optimizely.ab.Optimizely; import com.optimizely.ab.bucketing.UserProfileService; +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.event.ForwardingEventProcessor; import com.optimizely.ab.notification.NotificationCenter; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; @@ -29,10 +31,7 @@ import org.junit.Rule; import org.junit.Test; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; @@ -594,7 +593,9 @@ public void decideReasons_variableValueInvalid() { // reasons (logs with includeReasons) @Test - public void decideReasons_conditionNoMatchingAudience() {} + public void decideReasons_conditionNoMatchingAudience() throws ConfigParseException { + } + @Test public void decideReasons_conditionInvalidFormat() {} @Test @@ -616,38 +617,159 @@ public void decideReasons_missingAttributeValue() {} @Test public void decideReasons_experimentNotRunning() {} + @Test - public void decideReasons_gotVariationFromUserProfile() {} + public void decideReasons_gotVariationFromUserProfile() throws Exception { + String flagKey = "feature_2"; // embedding experiment: "exp_no_audience" + String experimentId = "10420810910"; // "exp_no_audience" + String experimentKey = "exp_no_audience"; + String variationId2 = "10418510624"; + String variationKey2 = "variation_no_traffic"; + + UserProfileService ups = mock(UserProfileService.class); + when(ups.lookup(userId)).thenReturn(createUserProfileMap(experimentId, variationId2)); + + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withUserProfileService(ups) + .build(); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + + assertTrue(decision.getReasons().contains( + String.format( + "Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", variationKey2, experimentKey, userId) + )); + } + @Test - public void decideReasons_forcedVariationFound() {} + public void decideReasons_forcedVariationFound() { + + } + @Test - public void decideReasons_forcedVariationFoundButInvalid() {} + public void decideReasons_forcedVariationFoundButInvalid() { + + } + @Test - public void decideReasons_userMeetsConditionsForTargetingRule() {} + public void decideReasons_userMeetsConditionsForTargetingRule() { + String flagKey = "feature_1"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("country", "US"); + OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + + assertTrue(decision.getReasons().contains( + String.format( + "The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) + )); + } + @Test - public void decideReasons_userDoesntMeetConditionsForTargetingRule() {} + public void decideReasons_userDoesntMeetConditionsForTargetingRule() { + String flagKey = "feature_1"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("country", "CA"); + OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + + assertTrue(decision.getReasons().contains( + String.format("User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, 1) + )); + } + @Test - public void decideReasons_userBucketedIntoTargetingRule() {} + public void decideReasons_userBucketedIntoTargetingRule() { + String flagKey = "feature_1"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("country", "US"); + OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + + assertTrue(decision.getReasons().contains( + String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) + )); + } + @Test - public void decideReasons_userBucketedIntoEveryoneTargetingRule() {} + public void decideReasons_userBucketedIntoEveryoneTargetingRule() { + String flagKey = "feature_1"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("country", "KO"); + OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + + assertTrue(decision.getReasons().contains( + String.format("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId) + )); + } + @Test - public void decideReasons_userNotBucketedIntoTargetingRule() {} + public void decideReasons_userNotBucketedIntoTargetingRule() { + String flagKey = "feature_1"; + String experimentKey = "3332020494"; // experimentKey of rollout[2] + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("browser", "safari"); + OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", userId, experimentKey) + )); + } + @Test - public void decideReasons_userBucketedIntoVariationInExperiment() {} + public void decideReasons_userBucketedIntoVariationInExperiment() { + String flagKey = "feature_2"; + String experimentKey = "exp_no_audience"; + String variationKey = "variation_with_traffic"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", userId, variationKey, experimentKey) + )); + } + @Test - public void decideReasons_userNotBucketedIntoVariation() {} + public void decideReasons_userNotBucketedIntoVariation() { + } + @Test - public void decideReasons_userBucketedIntoInvalidVariation() {} + public void decideReasons_userBucketedIntoInvalidVariation() { + } + @Test - public void decideReasons_userBucketedIntoExperimentInGroup() {} + public void decideReasons_userBucketedIntoExperimentInGroup() { + + } @Test - public void decideReasons_userNotBucketedIntoExperimentInGroup() {} + public void decideReasons_userNotBucketedIntoExperimentInGroup() { + + } @Test - public void decideReasons_userNotBucketedIntoAnyExperimentInGroup() {} + public void decideReasons_userNotBucketedIntoAnyExperimentInGroup() { + + } @Test - public void decideReasons_userBucketedIntoInvalidExperiment() {} + public void decideReasons_userBucketedIntoInvalidExperiment() { + + } @Test - public void decideReasons_userNotInExperiment() {} + public void decideReasons_userNotInExperiment() { + String flagKey = "feature_1"; + String experimentKey = "exp_with_audience"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + + assertTrue(decision.getReasons().contains( + String.format("User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experimentKey) + )); + } // utils @@ -665,4 +787,12 @@ Map createUserProfileMap(String experimentId, String variationId return userProfileMap; } + void setAudienceForFeatureTest(String featureKey, String audienceId) throws ConfigParseException { + String experimentId = optimizely.getProjectConfig().getFeatureKeyMapping().get(featureKey).getExperimentIds().get(0); + Experiment experimentReal = optimizely.getProjectConfig().getExperimentIdMapping().get(experimentId); + + Experiment experiment = spy(experimentReal); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + } + } From 31cce4fdfbf23e1837acb9204f52bed76007fa1a Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 14 Oct 2020 14:33:16 -0700 Subject: [PATCH 13/44] fix per reviews --- .../java/com/optimizely/ab/Optimizely.java | 37 ++++++++----------- .../OptimizelyUserContext.java | 16 ++++---- .../optimizely/ab/OptimizelyBuilderTest.java | 15 +++++--- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 1ebc6d420..78762377c 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -78,8 +78,7 @@ public class Optimizely implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(Optimizely.class); - @VisibleForTesting - final DecisionService decisionService; + public final DecisionService decisionService; @VisibleForTesting @Deprecated final EventHandler eventHandler; @@ -87,8 +86,8 @@ public class Optimizely implements AutoCloseable { final EventProcessor eventProcessor; @VisibleForTesting final ErrorHandler errorHandler; - @VisibleForTesting - final OptimizelyDecideOption[] defaultDecideOptions; + + public final List defaultDecideOptions; private final ProjectConfigManager projectConfigManager; @@ -109,7 +108,7 @@ private Optimizely(@Nonnull EventHandler eventHandler, @Nonnull ProjectConfigManager projectConfigManager, @Nullable OptimizelyConfigManager optimizelyConfigManager, @Nonnull NotificationCenter notificationCenter, - @Nonnull OptimizelyDecideOption[] defaultDecideOptions + @Nonnull List defaultDecideOptions ) { this.eventHandler = eventHandler; this.eventProcessor = eventProcessor; @@ -227,7 +226,7 @@ private Variation activate(@Nullable ProjectConfig projectConfig, return variation; } - private void sendImpression(@Nonnull ProjectConfig projectConfig, + public void sendImpression(@Nonnull ProjectConfig projectConfig, @Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map filteredAttributes, @@ -742,8 +741,7 @@ T getFeatureVariableValueForType(@Nonnull String featureKey, } // Helper method which takes type and variable value and convert it to object to use in Listener DecisionInfo object variable value - @VisibleForTesting - Object convertStringToType(String variableValue, String type) { + public Object convertStringToType(String variableValue, String type) { if (variableValue != null) { switch (type) { case FeatureVariable.DOUBLE_TYPE: @@ -1093,14 +1091,9 @@ public OptimizelyConfig getOptimizelyConfig() { } /** - * Set a context of the user for which decision APIs will be called. + * Create a context of the user for which decision APIs will be called. * - * - This API can be called after SDK initialization is completed (otherwise the __sdkNotReady__ error will be returned). - * - Only one user outstanding. The user-context can be changed any time by calling the same method with a different user-context value. - * - The SDK will copy the parameter value to create an internal user-context data atomically, so any further change in its caller copy after the API call is not reflected into the SDK state. - * - Once this API is called, the following other API calls can be called without a user-context parameter to use the same user-context. - * - Each Decide API call can contain an optional user-context parameter when the call targets a different user-context. This optional user-context parameter value will be used once only, instead of replacing the saved user-context. This call-based context control can be used to support multiple users at the same time. - * - If a user-context has not been set yet and decide APIs are called without a user-context parameter, SDK will return an error decision (__userNotSet__). + * A user context will be created successfully even when the SDK is not fully configured yet. * * @param userId The user ID to be used for bucketing. * @param attributes: A map of attribute names to current user attribute values. @@ -1219,7 +1212,7 @@ public static class Builder { private OptimizelyConfigManager optimizelyConfigManager; private UserProfileService userProfileService; private NotificationCenter notificationCenter; - private OptimizelyDecideOption[] defaultDecideOptions; + private List defaultDecideOptions; // For backwards compatibility private AtomicProjectConfigManager fallbackConfigManager = new AtomicProjectConfigManager(); @@ -1291,6 +1284,11 @@ public Builder withDatafile(String datafile) { return this; } + public Builder withDefaultDecideOptions(List options) { + this.defaultDecideOptions = Collections.unmodifiableList(options); + return this; + } + // Helper functions for making testing easier protected Builder withBucketing(Bucketer bucketer) { this.bucketer = bucketer; @@ -1307,11 +1305,6 @@ protected Builder withDecisionService(DecisionService decisionService) { return this; } - protected Builder withDefaultDecideOptions(OptimizelyDecideOption[] options) { - this.defaultDecideOptions = options; - return this; - } - public Optimizely build() { if (errorHandler == null) { @@ -1365,7 +1358,7 @@ public Optimizely build() { } if (defaultDecideOptions == null) { - defaultDecideOptions = new OptimizelyDecideOption[0]; + defaultDecideOptions = Collections.emptyList(); } return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions); diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index 47bcdb03b..71d877fe0 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -22,6 +22,7 @@ import javax.annotation.Nonnull; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; public class OptimizelyUserContext { @@ -66,22 +67,21 @@ public OptimizelyDecision decide(String key) { return decide(key, new OptimizelyDecideOption[0]); } - public Map decideAll(@Nonnull String[] keys, - @Nonnull OptimizelyDecideOption[] options) { + public Map decideForKeys(@Nonnull List keys, + @Nonnull List options) { return new HashMap<>(); } - public Map decideAll(@Nonnull String[] keys) { - return decideAll(keys, new OptimizelyDecideOption[0]); + public Map decideForKeys(@Nonnull List keys) { + return decideForKeys(keys, Collections.emptyList()); } - public Map decideAll(@Nonnull OptimizelyDecideOption[] options) { - String[] allFlagKeys = {}; - return decideAll(allFlagKeys, options); + public Map decideAll(@Nonnull List options) { + return decideForKeys(Collections.emptyList(), options); } public Map decideAll() { - return decideAll(new OptimizelyDecideOption[0]); + return decideAll(Collections.emptyList()); } public void trackEvent(@Nonnull String eventName, diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java index 6779830da..7e6579658 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java @@ -30,6 +30,9 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Arrays; +import java.util.List; + import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.*; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; @@ -174,22 +177,22 @@ public void withProjectConfigManagerAndFallbackDatafile() throws Exception { @Test public void withDefaultDecideOptions() throws Exception { - OptimizelyDecideOption[] options = { + List options = Arrays.asList( OptimizelyDecideOption.DISABLE_DECISION_EVENT, OptimizelyDecideOption.ENABLED_FLAGS_ONLY, OptimizelyDecideOption.EXCLUDE_VARIABLES - }; + ); Optimizely optimizelyClient = Optimizely.builder(validConfigJsonV4(), mockEventHandler) .build(); - assertEquals(optimizelyClient.defaultDecideOptions.length, 0); + assertEquals(optimizelyClient.defaultDecideOptions.size(), 0); optimizelyClient = Optimizely.builder(validConfigJsonV4(), mockEventHandler) .withDefaultDecideOptions(options) .build(); - assertEquals(optimizelyClient.defaultDecideOptions[0], OptimizelyDecideOption.DISABLE_DECISION_EVENT); - assertEquals(optimizelyClient.defaultDecideOptions[1], OptimizelyDecideOption.ENABLED_FLAGS_ONLY); - assertEquals(optimizelyClient.defaultDecideOptions[2], OptimizelyDecideOption.EXCLUDE_VARIABLES); + assertEquals(optimizelyClient.defaultDecideOptions.get(0), OptimizelyDecideOption.DISABLE_DECISION_EVENT); + assertEquals(optimizelyClient.defaultDecideOptions.get(1), OptimizelyDecideOption.ENABLED_FLAGS_ONLY); + assertEquals(optimizelyClient.defaultDecideOptions.get(2), OptimizelyDecideOption.EXCLUDE_VARIABLES); } } From 4bfa1f7711136ce696c8379a55d41e5d1c166edb Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 14 Oct 2020 15:17:41 -0700 Subject: [PATCH 14/44] fix conflicts --- .../OptimizelyUserContext.java | 10 ++-- .../OptimizelyUserContextTest.java | 55 +++++++++---------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java index 957f4d962..ef7cc57ae 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java @@ -84,7 +84,7 @@ public void setAttribute(@Nonnull String key, @Nonnull Object value) { *
      • If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons. *
      * @param key A flag key for which a decision will be made. - * @param options An array of options for decision-making. + * @param options A list of options for decision-making. * @return A decision result. */ public OptimizelyDecision decide(@Nonnull String key, @@ -193,8 +193,8 @@ public OptimizelyDecision decide(String key) { *
    • If the SDK finds an error for a key, the response will include a decision for the key showing reasons for the error. *
    • The SDK will always return key-mapped decisions. When it can not process requests, it’ll return an empty map after logging the errors. *
    - * @param keys An array of flag keys for which decisions will be made. - * @param options An array of options for decision-making. + * @param keys A list of flag keys for which decisions will be made. + * @param options A list of options for decision-making. * @return All decision results mapped by flag keys. */ public Map decideForKeys(@Nonnull List keys, @@ -224,7 +224,7 @@ public Map decideForKeys(@Nonnull List keys, /** * Returns a key-map of decision results for multiple flag keys and a user context. * - * @param keys An array of flag keys for which decisions will be made. + * @param keys A list of flag keys for which decisions will be made. * @return All decision results mapped by flag keys. */ public Map decideForKeys(@Nonnull List keys) { @@ -234,7 +234,7 @@ public Map decideForKeys(@Nonnull List keys) /** * Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. * - * @param options An array of options for decision-making. + * @param options A list of options for decision-making. * @return All decision results mapped by flag keys. */ public Map decideAll(@Nonnull List options) { diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java index 41cdd3f70..c207c5fe9 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -139,11 +139,11 @@ public void decide() { @Test public void decideAll_oneFlag() { String flagKey = "feature_2"; - String[] flagKeys = {flagKey}; + List flagKeys = Arrays.asList(flagKey); OptimizelyJSON variablesExpected = optimizely.getAllFeatureVariables(flagKey, userId); OptimizelyUserContext user = optimizely.createUserContext(userId); - Map decisions = user.decideAll(flagKeys); + Map decisions = user.decideForKeys(flagKeys); assertTrue(decisions.size() == 1); OptimizelyDecision decision = decisions.get(flagKey); @@ -164,12 +164,12 @@ public void decideAll_twoFlags() { String flagKey1 = "feature_1"; String flagKey2 = "feature_2"; - String[] flagKeys = {flagKey1, flagKey2}; + List flagKeys = Arrays.asList(flagKey1, flagKey2); OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); OptimizelyJSON variablesExpected2 = optimizely.getAllFeatureVariables(flagKey2, userId); OptimizelyUserContext user = optimizely.createUserContext(userId, Collections.singletonMap("gender", "f")); - Map decisions = user.decideAll(flagKeys); + Map decisions = user.decideForKeys(flagKeys); assertTrue(decisions.size() == 2); @@ -246,8 +246,7 @@ public void decideAll_allFlags_enabledFlagsOnly() { OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); OptimizelyUserContext user = optimizely.createUserContext(userId, Collections.singletonMap("gender", "f")); - OptimizelyDecideOption[] decideOptions = {OptimizelyDecideOption.ENABLED_FLAGS_ONLY}; - Map decisions = user.decideAll(decideOptions); + Map decisions = user.decideAll(Arrays.asList(OptimizelyDecideOption.ENABLED_FLAGS_ONLY)); assertTrue(decisions.size() == 2); @@ -342,7 +341,7 @@ public void decide_doNotSendEvent() { String flagKey = "feature_2"; OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.DISABLE_DECISION_EVENT}); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.DISABLE_DECISION_EVENT)); assertEquals(decision.getVariationKey(), "variation_with_traffic"); } @@ -385,7 +384,7 @@ public void decisionNotification() { isListenerCalled = false; testDecisionInfoMap.put(DECISION_EVENT_DISPATCHED, false); - user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.DISABLE_DECISION_EVENT}); + user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.DISABLE_DECISION_EVENT)); assertTrue(isListenerCalled); } @@ -413,7 +412,7 @@ public void decideOptions_bypassUPS() throws Exception { // should return variationId2 set by UPS assertEquals(decision.getVariationKey(), variationKey2); - decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE}); + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)); // should ignore variationId2 set by UPS and return variationId1 assertEquals(decision.getVariationKey(), variationKey1); // also should not save either @@ -428,7 +427,7 @@ public void decideOptions_excludeVariables() { OptimizelyDecision decision = user.decide(flagKey); assertTrue(decision.getVariables().toMap().size() > 0); - decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.EXCLUDE_VARIABLES}); + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.EXCLUDE_VARIABLES)); assertTrue(decision.getVariables().toMap().size() == 0); } @@ -441,7 +440,7 @@ public void decideOptions_includeReasons() { assertEquals(decision.getReasons().size(), 1); assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); - decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertEquals(decision.getReasons().size(), 1); assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); @@ -449,7 +448,7 @@ public void decideOptions_includeReasons() { decision = user.decide(flagKey); assertEquals(decision.getReasons().size(), 0); - decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().size() > 0); } @@ -463,9 +462,9 @@ public void decideOptions_enabledFlagsOnly() { @Test public void decideOptions_defaultDecideOptions() { - OptimizelyDecideOption[] options = { + List options = Arrays.asList( OptimizelyDecideOption.EXCLUDE_VARIABLES - }; + ); optimizely = Optimizely.builder() .withDatafile(datafile) @@ -479,7 +478,7 @@ public void decideOptions_defaultDecideOptions() { OptimizelyDecision decision = user.decide(flagKey); assertTrue(decision.getVariables().toMap().size() == 0); - decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS, OptimizelyDecideOption.EXCLUDE_VARIABLES}); + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS, OptimizelyDecideOption.EXCLUDE_VARIABLES)); // other options should work as well assertTrue(decision.getReasons().size() > 0); // redundant setting ignored @@ -521,11 +520,11 @@ public void decide_invalidFeatureKey() { @Test public void decideAll_sdkNotReady() { - String[] flagKeys = {"feature_1"}; + List flagKeys = Arrays.asList("feature_1"); Optimizely optimizely = new Optimizely.Builder().build(); OptimizelyUserContext user = optimizely.createUserContext(userId); - Map decisions = user.decideAll(flagKeys); + Map decisions = user.decideForKeys(flagKeys); assertEquals(decisions.size(), 0); } @@ -535,11 +534,11 @@ public void decideAll_errorDecisionIncluded() { String flagKey1 = "feature_2"; String flagKey2 = "invalid_key"; - String[] flagKeys = {flagKey1, flagKey2}; + List flagKeys = Arrays.asList(flagKey1, flagKey2); OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); OptimizelyUserContext user = optimizely.createUserContext(userId); - Map decisions = user.decideAll(flagKeys); + Map decisions = user.decideForKeys(flagKeys); assertEquals(decisions.size(), 2); @@ -617,7 +616,7 @@ public void decideReasons_missingAttributeValue() {} @Test public void decideReasons_experimentNotRunning() {} - + @Test public void decideReasons_gotVariationFromUserProfile() throws Exception { String flagKey = "feature_2"; // embedding experiment: "exp_no_audience" @@ -635,7 +634,7 @@ public void decideReasons_gotVariationFromUserProfile() throws Exception { .build(); OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( String.format( @@ -659,7 +658,7 @@ public void decideReasons_userMeetsConditionsForTargetingRule() { OptimizelyUserContext user = optimizely.createUserContext(userId); user.setAttribute("country", "US"); - OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( String.format( @@ -673,7 +672,7 @@ public void decideReasons_userDoesntMeetConditionsForTargetingRule() { OptimizelyUserContext user = optimizely.createUserContext(userId); user.setAttribute("country", "CA"); - OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( String.format("User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, 1) @@ -686,7 +685,7 @@ public void decideReasons_userBucketedIntoTargetingRule() { OptimizelyUserContext user = optimizely.createUserContext(userId); user.setAttribute("country", "US"); - OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) @@ -699,7 +698,7 @@ public void decideReasons_userBucketedIntoEveryoneTargetingRule() { OptimizelyUserContext user = optimizely.createUserContext(userId); user.setAttribute("country", "KO"); - OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( String.format("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId) @@ -713,7 +712,7 @@ public void decideReasons_userNotBucketedIntoTargetingRule() { OptimizelyUserContext user = optimizely.createUserContext(userId); user.setAttribute("browser", "safari"); - OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( String.format("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", userId, experimentKey) @@ -727,7 +726,7 @@ public void decideReasons_userBucketedIntoVariationInExperiment() { String variationKey = "variation_with_traffic"; OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( String.format("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", userId, variationKey, experimentKey) @@ -764,7 +763,7 @@ public void decideReasons_userNotInExperiment() { String experimentKey = "exp_with_audience"; OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey, new OptimizelyDecideOption[]{OptimizelyDecideOption.INCLUDE_REASONS}); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( String.format("User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experimentKey) From fdbf75a9a4d102399a0f1e369e1a7d5488fb2dbb Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 15 Oct 2020 09:03:54 -0700 Subject: [PATCH 15/44] fix to nonnull options and reasons --- .../java/com/optimizely/ab/Optimizely.java | 17 +- .../OptimizelyUserContext.java | 25 +-- .../com/optimizely/ab/bucketing/Bucketer.java | 38 +++-- .../ab/bucketing/DecisionService.java | 147 +++++++++--------- .../ab/internal/ExperimentUtils.java | 42 ++--- .../DecisionReasons.java | 19 ++- .../OptimizelyDecideOption.java | 2 +- .../OptimizelyDecision.java | 3 +- .../optimizely/ab/OptimizelyBuilderTest.java | 2 +- .../com/optimizely/ab/OptimizelyTest.java | 21 ++- .../ab/bucketing/DecisionServiceTest.java | 12 +- .../OptimizelyDecisionTest.java | 2 + .../OptimizelyUserContextTest.java | 3 + 13 files changed, 177 insertions(+), 156 deletions(-) rename core-api/src/main/java/com/optimizely/ab/{optimizelyusercontext => }/OptimizelyUserContext.java (94%) rename core-api/src/main/java/com/optimizely/ab/{optimizelyusercontext => optimizelydecision}/DecisionReasons.java (72%) rename core-api/src/main/java/com/optimizely/ab/{optimizelyusercontext => optimizelydecision}/OptimizelyDecideOption.java (94%) rename core-api/src/main/java/com/optimizely/ab/{optimizelyusercontext => optimizelydecision}/OptimizelyDecision.java (97%) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 78762377c..26fe4a6b9 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -35,8 +35,7 @@ import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; -import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; -import com.optimizely.ab.optimizelyusercontext.OptimizelyUserContext; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,7 +77,7 @@ public class Optimizely implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(Optimizely.class); - public final DecisionService decisionService; + final DecisionService decisionService; @VisibleForTesting @Deprecated final EventHandler eventHandler; @@ -226,11 +225,11 @@ private Variation activate(@Nullable ProjectConfig projectConfig, return variation; } - public void sendImpression(@Nonnull ProjectConfig projectConfig, - @Nonnull Experiment experiment, - @Nonnull String userId, - @Nonnull Map filteredAttributes, - @Nonnull Variation variation) { + void sendImpression(@Nonnull ProjectConfig projectConfig, + @Nonnull Experiment experiment, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull Variation variation) { if (!experiment.isRunning()) { logger.info("Experiment has \"Launched\" status so not dispatching event during activation."); return; @@ -741,7 +740,7 @@ T getFeatureVariableValueForType(@Nonnull String featureKey, } // Helper method which takes type and variable value and convert it to object to use in Listener DecisionInfo object variable value - public Object convertStringToType(String variableValue, String type) { + Object convertStringToType(String variableValue, String type) { if (variableValue != null) { switch (type) { case FeatureVariable.DOUBLE_TYPE: diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java similarity index 94% rename from core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java rename to core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java index ef7cc57ae..36c87512f 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java @@ -14,14 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.optimizely.ab.optimizelyusercontext; +package com.optimizely.ab; -import com.optimizely.ab.Optimizely; -import com.optimizely.ab.UnknownEventTypeException; import com.optimizely.ab.bucketing.FeatureDecision; import com.optimizely.ab.config.*; import com.optimizely.ab.notification.DecisionNotification; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; +import com.optimizely.ab.optimizelydecision.OptimizelyDecision; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,11 +101,11 @@ public OptimizelyDecision decide(@Nonnull String key, return OptimizelyDecision.createErrorDecision(key, this, getFlagKeyInvalidMessage(key)); } - List allOptions = getAllOptions(options); Boolean sentEvent = false; Boolean flagEnabled = false; - DecisionReasons decisionReasons = new DecisionReasons(); + List allOptions = getAllOptions(options); Boolean includeReasons = allOptions.contains(OptimizelyDecideOption.INCLUDE_REASONS); + DecisionReasons decisionReasons = new DecisionReasons(includeReasons); Map copiedAttributes = copyAttributes(); FeatureDecision flagDecision = optimizely.decisionService.getVariationForFeature( @@ -113,7 +114,7 @@ public OptimizelyDecision decide(@Nonnull String key, copiedAttributes, projectConfig, allOptions, - includeReasons ? decisionReasons : null); + decisionReasons); if (flagDecision.variation != null) { if (flagDecision.decisionSource.equals(FeatureDecision.DecisionSource.FEATURE_TEST)) { @@ -142,12 +143,12 @@ public OptimizelyDecision decide(@Nonnull String key, flag, flagDecision.variation, flagEnabled, - includeReasons ? decisionReasons : null); + decisionReasons); } OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap); - List reasonsToReport = decisionReasons.toReport(allOptions); + List reasonsToReport = decisionReasons.toReport(); String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null; // TODO: add ruleKey values when available later. use a copy of experimentKey until then. String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null; @@ -304,10 +305,10 @@ public static String getVariableValueInvalidMessage(String variableKey) { return String.format(VARIABLE_VALUE_INVALID, variableKey); } - private Map getDecisionVariableMap(FeatureFlag flag, - Variation variation, - Boolean featureEnabled, - DecisionReasons decisionReasons) { + private Map getDecisionVariableMap(@Nonnull FeatureFlag flag, + @Nonnull Variation variation, + @Nonnull Boolean featureEnabled, + @Nonnull DecisionReasons decisionReasons) { Map valuesMap = new HashMap(); for (FeatureVariable variable : flag.getVariables()) { String value = variable.getDefaultValue(); diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index b45abc203..7b0aa66eb 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -19,14 +19,15 @@ import com.optimizely.ab.annotations.VisibleForTesting; import com.optimizely.ab.bucketing.internal.MurmurHash3; import com.optimizely.ab.config.*; -import com.optimizely.ab.optimizelyusercontext.DecisionReasons; -import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import java.util.Collections; import java.util.List; /** @@ -70,8 +71,8 @@ private String bucketToEntity(int bucketValue, List trafficAl private Experiment bucketToExperiment(@Nonnull Group group, @Nonnull String bucketingId, @Nonnull ProjectConfig projectConfig, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // "salt" the bucket id using the group id String bucketKey = bucketingId + group.getId(); @@ -92,8 +93,8 @@ private Experiment bucketToExperiment(@Nonnull Group group, private Variation bucketToVariation(@Nonnull Experiment experiment, @Nonnull String bucketingId, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // "salt" the bucket id using the experiment id String experimentId = experiment.getId(); String experimentKey = experiment.getKey(); @@ -109,14 +110,16 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, if (bucketedVariationId != null) { Variation bucketedVariation = experiment.getVariationIdToVariationMap().get(bucketedVariationId); String variationKey = bucketedVariation.getKey(); - DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", bucketingId, variationKey, + String message = reasons.addInfoF("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", bucketingId, variationKey, experimentKey); + logger.info(message); return bucketedVariation; } // user was not bucketed to a variation - DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", bucketingId, experimentKey); + String message = reasons.addInfoF("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", bucketingId, experimentKey); + logger.info(message); return null; } @@ -134,8 +137,8 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, public Variation bucket(@Nonnull Experiment experiment, @Nonnull String bucketingId, @Nonnull ProjectConfig projectConfig, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // support existing custom Bucketers if (isMethodOverriden("bucket", Experiment.class, String.class, ProjectConfig.class)) { @@ -157,15 +160,15 @@ public Variation bucket(@Nonnull Experiment experiment, public Variation bucket(@Nonnull Experiment experiment, @Nonnull String bucketingId, @Nonnull ProjectConfig projectConfig) { - return bucketCore(experiment, bucketingId, projectConfig, null, null); + return bucketCore(experiment, bucketingId, projectConfig, Collections.emptyList(), new DecisionReasons()); } @Nullable public Variation bucketCore(@Nonnull Experiment experiment, @Nonnull String bucketingId, @Nonnull ProjectConfig projectConfig, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // ---------- Bucket User ---------- String groupId = experiment.getGroupId(); // check whether the experiment belongs to a group @@ -175,7 +178,8 @@ public Variation bucketCore(@Nonnull Experiment experiment, if (experimentGroup.getPolicy().equals(Group.RANDOM_POLICY)) { Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig, options, reasons); if (bucketedExperiment == null) { - DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId()); + String message = reasons.addInfoF("User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId()); + logger.info(message); return null; } else { @@ -183,13 +187,15 @@ public Variation bucketCore(@Nonnull Experiment experiment, // if the experiment a user is bucketed in within a group isn't the same as the experiment provided, // don't perform further bucketing within the experiment if (!bucketedExperiment.getId().equals(experiment.getId())) { - DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is not in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), + String message = reasons.addInfoF("User with bucketingId \"%s\" is not in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), experimentGroup.getId()); + logger.info(message); return null; } - DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), + String message = reasons.addInfoF("User with bucketingId \"%s\" is in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), experimentGroup.getId()); + logger.info(message); } } diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index 588df9cf3..bc701326b 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -20,13 +20,14 @@ import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.internal.ControlAttribute; import com.optimizely.ab.internal.ExperimentUtils; -import com.optimizely.ab.optimizelyusercontext.DecisionReasons; -import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -92,8 +93,8 @@ public Variation getVariation(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // support existing custom DecisionServices if (isMethodOverriden("getVariation", Experiment.class, String.class, Map.class, ProjectConfig.class)) { @@ -108,7 +109,7 @@ public Variation getVariation(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig) { - return getVariationCore(experiment, userId, filteredAttributes, projectConfig, null, null); + return getVariationCore(experiment, userId, filteredAttributes, projectConfig, Collections.emptyList(), new DecisionReasons()); } @Nullable @@ -116,8 +117,8 @@ public Variation getVariationCore(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { if (!ExperimentUtils.isExperimentActive(experiment, options, reasons)) { return null; } @@ -141,14 +142,17 @@ public Variation getVariationCore(@Nonnull Experiment experiment, try { Map userProfileMap = userProfileService.lookup(userId); if (userProfileMap == null) { - logInfo(logger, reasons, "We were unable to get a user profile map from the UserProfileService."); + String message = reasons.addInfoF("We were unable to get a user profile map from the UserProfileService."); + logger.info(message); } else if (UserProfileUtils.isValidUserProfileMap(userProfileMap)) { userProfile = UserProfileUtils.convertMapToUserProfile(userProfileMap); } else { - logWarn(logger, reasons, "The UserProfileService returned an invalid map."); + String message = reasons.addInfoF("The UserProfileService returned an invalid map."); + logger.warn(message); } } catch (Exception exception) { - logError(logger, reasons, exception.getMessage()); + String message = reasons.addInfoF(exception.getMessage()); + logger.error(message); errorHandler.handleError(new OptimizelyRuntimeException(exception)); } } @@ -179,7 +183,8 @@ public Variation getVariationCore(@Nonnull Experiment experiment, return variation; } - logInfo(logger, reasons, "User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experiment.getKey()); + String message = reasons.addInfoF("User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experiment.getKey()); + logger.info(message); return null; } @@ -199,8 +204,8 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // support existing custom DecisionServices if (isMethodOverriden("getVariationForFeature", FeatureFlag.class, String.class, Map.class, ProjectConfig.class)) { return getVariationForFeature(featureFlag, userId, filteredAttributes, projectConfig); @@ -215,7 +220,7 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig) { - return getVariationForFeatureCore(featureFlag, userId, filteredAttributes, projectConfig,null, null); + return getVariationForFeatureCore(featureFlag, userId, filteredAttributes, projectConfig,Collections.emptyList(), new DecisionReasons()); } @Nonnull @@ -223,8 +228,8 @@ public FeatureDecision getVariationForFeatureCore(@Nonnull FeatureFlag featureFl @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { if (!featureFlag.getExperimentIds().isEmpty()) { for (String experimentId : featureFlag.getExperimentIds()) { Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId); @@ -234,16 +239,19 @@ public FeatureDecision getVariationForFeatureCore(@Nonnull FeatureFlag featureFl } } } else { - logInfo(logger, reasons, "The feature flag \"%s\" is not used in any experiments.", featureFlag.getKey()); + String message = reasons.addInfoF("The feature flag \"%s\" is not used in any experiments.", featureFlag.getKey()); + logger.info(message); } FeatureDecision featureDecision = getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, options, reasons); if (featureDecision.variation == null) { - logInfo(logger, reasons, "The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".", + String message = reasons.addInfoF("The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".", userId, featureFlag.getKey()); + logger.info(message); } else { - logInfo(logger, reasons, "The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", + String message = reasons.addInfoF("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, featureFlag.getKey()); + logger.info(message); } return featureDecision; } @@ -266,17 +274,19 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // use rollout to get variation for feature if (featureFlag.getRolloutId().isEmpty()) { - logInfo(logger, reasons, "The feature flag \"%s\" is not used in a rollout.", featureFlag.getKey()); + String message = reasons.addInfoF("The feature flag \"%s\" is not used in a rollout.", featureFlag.getKey()); + logger.info(message); return new FeatureDecision(null, null, null); } Rollout rollout = projectConfig.getRolloutIdMapping().get(featureFlag.getRolloutId()); if (rollout == null) { - logError(logger, reasons, "The rollout with id \"%s\" was not found in the datafile for feature flag \"%s\".", + String message = reasons.addInfoF("The rollout with id \"%s\" was not found in the datafile for feature flag \"%s\".", featureFlag.getRolloutId(), featureFlag.getKey()); + logger.error(message); return new FeatureDecision(null, null, null); } @@ -294,7 +304,8 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag return new FeatureDecision(rolloutRule, variation, FeatureDecision.DecisionSource.ROLLOUT); } else { - logDebug(logger, reasons, "User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, i + 1); + String message = reasons.addInfoF("User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, i + 1); + logger.debug(message); } } @@ -303,7 +314,8 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, finalRule, filteredAttributes, RULE, "Everyone Else", options, reasons)) { variation = bucketer.bucket(finalRule, bucketingId, projectConfig, options, reasons); if (variation != null) { - logDebug(logger, reasons, "User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId); + String message = reasons.addInfoF("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId); + logger.debug(message); return new FeatureDecision(finalRule, variation, FeatureDecision.DecisionSource.ROLLOUT); } @@ -316,7 +328,7 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig) { - return getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, null, null); + return getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, Collections.emptyList(), new DecisionReasons()); } /** @@ -332,18 +344,20 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag @Nullable Variation getWhitelistedVariation(@Nonnull Experiment experiment, @Nonnull String userId, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // if a user has a forced variation mapping, return the respective variation Map userIdToVariationKeyMap = experiment.getUserIdToVariationKeyMap(); if (userIdToVariationKeyMap.containsKey(userId)) { String forcedVariationKey = userIdToVariationKeyMap.get(userId); Variation forcedVariation = experiment.getVariationKeyToVariationMap().get(forcedVariationKey); if (forcedVariation != null) { - logInfo(logger, reasons, "User \"%s\" is forced in variation \"%s\".", userId, forcedVariationKey); + String message = reasons.addInfoF("User \"%s\" is forced in variation \"%s\".", userId, forcedVariationKey); + logger.info(message); } else { - logError(logger, reasons, "Variation \"%s\" is not in the datafile. Not activating user \"%s\".", + String message = reasons.addInfoF("Variation \"%s\" is not in the datafile. Not activating user \"%s\".", forcedVariationKey, userId); + logger.error(message); } return forcedVariation; } @@ -354,7 +368,7 @@ Variation getWhitelistedVariation(@Nonnull Experiment experiment, Variation getWhitelistedVariation(@Nonnull Experiment experiment, @Nonnull String userId) { - return getWhitelistedVariation(experiment, userId, null, null); + return getWhitelistedVariation(experiment, userId, Collections.emptyList(), new DecisionReasons()); } /** @@ -372,10 +386,10 @@ Variation getWhitelistedVariation(@Nonnull Experiment experiment, Variation getStoredVariation(@Nonnull Experiment experiment, @Nonnull UserProfile userProfile, @Nonnull ProjectConfig projectConfig, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { - if (options != null && options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return null; + if (options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return null; // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation @@ -390,18 +404,21 @@ Variation getStoredVariation(@Nonnull Experiment experiment, .getVariationIdToVariationMap() .get(variationId); if (savedVariation != null) { - logInfo(logger, reasons,"Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", + String message = reasons.addInfoF("Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", savedVariation.getKey(), experimentKey, userProfile.userId); + logger.info(message); // A variation is stored for this combined bucket id return savedVariation; } else { - logInfo(logger, reasons,"User \"%s\" was previously bucketed into variation with ID \"%s\" for experiment \"%s\", but no matching variation was found for that user. We will re-bucket the user.", + String message = reasons.addInfoF("User \"%s\" was previously bucketed into variation with ID \"%s\" for experiment \"%s\", but no matching variation was found for that user. We will re-bucket the user.", userProfile.userId, variationId, experimentKey); + logger.info(message); return null; } } else { - logInfo(logger, reasons, "No previously activated variation of experiment \"%s\" for user \"%s\" found in user profile.", + String message = reasons.addInfoF("No previously activated variation of experiment \"%s\" for user \"%s\" found in user profile.", experimentKey, userProfile.userId); + logger.info(message); return null; } } @@ -410,7 +427,7 @@ Variation getStoredVariation(@Nonnull Experiment experiment, Variation getStoredVariation(@Nonnull Experiment experiment, @Nonnull UserProfile userProfile, @Nonnull ProjectConfig projectConfig) { - return getStoredVariation(experiment, userProfile, projectConfig, null, null); + return getStoredVariation(experiment, userProfile, projectConfig, Collections.emptyList(), new DecisionReasons()); } /** @@ -425,10 +442,10 @@ Variation getStoredVariation(@Nonnull Experiment experiment, void saveVariation(@Nonnull Experiment experiment, @Nonnull Variation variation, @Nonnull UserProfile userProfile, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { - if (options != null && options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return; + if (options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return; // only save if the user has implemented a user profile service if (userProfileService != null) { @@ -458,7 +475,7 @@ void saveVariation(@Nonnull Experiment experiment, void saveVariation(@Nonnull Experiment experiment, @Nonnull Variation variation, @Nonnull UserProfile userProfile) { - saveVariation(experiment, variation, userProfile, null, null); + saveVariation(experiment, variation, userProfile, Collections.emptyList(), new DecisionReasons()); } /** @@ -473,15 +490,16 @@ void saveVariation(@Nonnull Experiment experiment, */ String getBucketingId(@Nonnull String userId, @Nonnull Map filteredAttributes, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { String bucketingId = userId; if (filteredAttributes != null && filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) { if (String.class.isInstance(filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()))) { bucketingId = (String) filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()); logger.debug("BucketingId is valid: \"{}\"", bucketingId); } else { - logWarn(logger, reasons, "BucketingID attribute is not a string. Defaulted to userId"); + String message = reasons.addInfoF("BucketingID attribute is not a string. Defaulted to userId"); + logger.warn(message); } } return bucketingId; @@ -489,7 +507,7 @@ String getBucketingId(@Nonnull String userId, String getBucketingId(@Nonnull String userId, @Nonnull Map filteredAttributes) { - return getBucketingId(userId, filteredAttributes, null, null); + return getBucketingId(userId, filteredAttributes, Collections.emptyList(), new DecisionReasons()); } public ConcurrentHashMap> getForcedVariationMapping() { @@ -578,8 +596,8 @@ public boolean setForcedVariation(@Nonnull Experiment experiment, @Nullable public Variation getForcedVariation(@Nonnull Experiment experiment, @Nonnull String userId, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // support existing custom DecisionServices if (isMethodOverriden("getForcedVariation", Experiment.class, String.class)) { @@ -592,14 +610,14 @@ public Variation getForcedVariation(@Nonnull Experiment experiment, @Nullable public Variation getForcedVariation(@Nonnull Experiment experiment, @Nonnull String userId) { - return getForcedVariationCore(experiment, userId, null, null); + return getForcedVariationCore(experiment, userId, Collections.emptyList(), new DecisionReasons()); } @Nullable public Variation getForcedVariationCore(@Nonnull Experiment experiment, @Nonnull String userId, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { // if the user id is invalid, return false. if (!validateUserId(userId)) { @@ -612,8 +630,9 @@ public Variation getForcedVariationCore(@Nonnull Experiment experiment, if (variationId != null) { Variation variation = experiment.getVariationIdToVariationMap().get(variationId); if (variation != null) { - logDebug(logger, reasons, "Variation \"%s\" is mapped to experiment \"%s\" and user \"%s\" in the forced variation map", + String message = reasons.addInfoF("Variation \"%s\" is mapped to experiment \"%s\" and user \"%s\" in the forced variation map", variation.getKey(), experiment.getKey(), userId); + logger.debug(message); return variation; } } else { @@ -638,30 +657,6 @@ private boolean validateUserId(String userId) { return true; } - public static void logError(Logger logger, DecisionReasons reasons, String format, Object... args) { - String message = String.format(format, args); - logger.error(message); - if (reasons != null) reasons.addInfo(message); - } - - public static void logWarn(Logger logger, DecisionReasons reasons, String format, Object... args) { - String message = String.format(format, args); - logger.warn(message); - if (reasons != null) reasons.addInfo(message); - } - - public static void logInfo(Logger logger, DecisionReasons reasons, String format, Object... args) { - String message = String.format(format, args); - logger.info(message); - if (reasons != null) reasons.addInfo(message); - } - - public static void logDebug(Logger logger, DecisionReasons reasons, String format, Object... args) { - String message = String.format(format, args); - logger.debug(message); - if (reasons != null) reasons.addInfo(message); - } - Boolean isMethodOverriden(String methodName, Class... params) { try { return getClass() != DecisionService.class && getClass().getDeclaredMethod(methodName, params) != null; diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java index 68628516d..e703cc527 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java @@ -16,20 +16,20 @@ */ package com.optimizely.ab.internal; -import com.optimizely.ab.bucketing.DecisionService; import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.AudienceIdCondition; import com.optimizely.ab.config.audience.Condition; import com.optimizely.ab.config.audience.OrCondition; -import com.optimizely.ab.optimizelyusercontext.DecisionReasons; -import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -49,11 +49,12 @@ private ExperimentUtils() { * @return whether the pre-conditions are satisfied */ public static boolean isExperimentActive(@Nonnull Experiment experiment, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { if (!experiment.isActive()) { - DecisionService.logInfo(logger, reasons, "Experiment \"%s\" is not running.", experiment.getKey()); + String message = reasons.addInfoF("Experiment \"%s\" is not running.", experiment.getKey()); + logger.info(message); return false; } @@ -61,7 +62,7 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment, } public static boolean isExperimentActive(@Nonnull Experiment experiment) { - return isExperimentActive(experiment, null, null); + return isExperimentActive(experiment, Collections.emptyList(), new DecisionReasons()); } /** @@ -81,8 +82,8 @@ public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig proj @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { if (experiment.getAudienceConditions() != null) { logger.debug("Evaluating audiences for {} \"{}\": {}.", loggingEntityType, loggingKey, experiment.getAudienceConditions()); Boolean resolveReturn = evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey); @@ -108,7 +109,7 @@ public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig proj @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey) { - return doesUserMeetAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, null, null); + return doesUserMeetAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, Collections.emptyList(), new DecisionReasons()); } @Nullable @@ -117,8 +118,8 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { List experimentAudienceIds = experiment.getAudienceIds(); // if there are no audiences, ALL users should be part of the experiment @@ -138,7 +139,8 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, Boolean result = implicitOr.evaluate(projectConfig, attributes); - DecisionService.logInfo(logger, reasons, "Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); + String message = reasons.addInfoF("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); + logger.info(message); return result; } @@ -149,7 +151,7 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey) { - return evaluateAudience(projectConfig, experiment, attributes, loggingEntityType, loggingKey, null, null); + return evaluateAudience(projectConfig, experiment, attributes, loggingEntityType, loggingKey, Collections.emptyList(), new DecisionReasons()); } @Nullable @@ -158,18 +160,20 @@ public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectC @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey, - @Nullable List options, - @Nullable DecisionReasons reasons) { + @Nonnull List options, + @Nonnull DecisionReasons reasons) { Condition conditions = experiment.getAudienceConditions(); if (conditions == null) return null; try { Boolean result = conditions.evaluate(projectConfig, attributes); - DecisionService.logInfo(logger, reasons,"Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); + String message = reasons.addInfoF("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); + logger.info(message); return result; } catch (Exception e) { - DecisionService.logError(logger, reasons,"Condition invalid: %s", e.getMessage()); + String message = reasons.addInfoF("Condition invalid: %s", e.getMessage()); + logger.error(message); return null; } } @@ -180,7 +184,7 @@ public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectC @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey) { - return evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, null, null); + return evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, Collections.emptyList(), new DecisionReasons()); } } \ No newline at end of file diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java similarity index 72% rename from core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java rename to core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java index 7230b7291..c7c7d91b6 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/DecisionReasons.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java @@ -14,21 +14,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.optimizely.ab.optimizelyusercontext; +package com.optimizely.ab.optimizelydecision; import java.util.ArrayList; import java.util.List; public class DecisionReasons { + boolean includeReasons; List errors; List logs; - public DecisionReasons() { + public DecisionReasons(boolean includeReasons) { + this.includeReasons = includeReasons; this.errors = new ArrayList(); this.logs = new ArrayList(); } + public DecisionReasons() { + this(false); + } + public void addError(String message) { errors.add(message); } @@ -37,10 +43,15 @@ public void addInfo(String message) { logs.add(message); } + public String addInfoF(String format, Object... args) { + String message = String.format(format, args); + if(includeReasons) addInfo(message); + return message; + } - public List toReport(List options) { + public List toReport() { List reasons = new ArrayList<>(errors); - if(options.contains(OptimizelyDecideOption.INCLUDE_REASONS)) { + if(includeReasons) { reasons.addAll(logs); } return reasons; diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecideOption.java similarity index 94% rename from core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java rename to core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecideOption.java index e7f6d6874..ccd08bb63 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecideOption.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecideOption.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.optimizely.ab.optimizelyusercontext; +package com.optimizely.ab.optimizelydecision; public enum OptimizelyDecideOption { DISABLE_DECISION_EVENT, diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java similarity index 97% rename from core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java rename to core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java index 2eafd4001..77c05642f 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java @@ -14,8 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.optimizely.ab.optimizelyusercontext; +package com.optimizely.ab.optimizelydecision; +import com.optimizely.ab.OptimizelyUserContext; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import javax.annotation.Nonnull; diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java index 7e6579658..932150337 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java @@ -21,7 +21,7 @@ import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.error.NoOpErrorHandler; import com.optimizely.ab.event.EventHandler; -import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Rule; import org.junit.Test; diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index d77d54875..2e3cedc88 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -35,7 +35,6 @@ import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.notification.*; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; -import com.optimizely.ab.optimizelyusercontext.OptimizelyUserContext; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Before; import org.junit.Rule; @@ -378,7 +377,7 @@ public void activateForNullVariation() throws Exception { Experiment activatedExperiment = validProjectConfig.getExperiments().get(0); Map testUserAttributes = Collections.singletonMap("browser_type", "chromey"); - when(mockBucketer.bucket(activatedExperiment, testBucketingId, validProjectConfig, null, null)).thenReturn(null); + when(mockBucketer.bucket(eq(activatedExperiment), eq(testBucketingId), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(null); logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"" + activatedExperiment.getKey() + "\"."); @@ -1255,7 +1254,7 @@ public void getVariation() throws Exception { Optimizely optimizely = optimizelyBuilder.withBucketing(mockBucketer).build(); - when(mockBucketer.bucket(activatedExperiment, testUserId, validProjectConfig, null, null)).thenReturn(bucketedVariation); + when(mockBucketer.bucket(eq(activatedExperiment), eq(testUserId), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(bucketedVariation); Map testUserAttributes = new HashMap<>(); testUserAttributes.put("browser_type", "chrome"); @@ -1265,7 +1264,7 @@ public void getVariation() throws Exception { testUserAttributes); // verify that the bucketing algorithm was called correctly - verify(mockBucketer).bucket(activatedExperiment, testUserId, validProjectConfig, null, null); + verify(mockBucketer).bucket(eq(activatedExperiment), eq(testUserId), eq(validProjectConfig), anyObject(), anyObject()); assertThat(actualVariation, is(bucketedVariation)); // verify that we didn't attempt to dispatch an event @@ -1286,13 +1285,13 @@ public void getVariationWithExperimentKey() throws Exception { .withConfig(noAudienceProjectConfig) .build(); - when(mockBucketer.bucket(activatedExperiment, testUserId, noAudienceProjectConfig, null, null)).thenReturn(bucketedVariation); + when(mockBucketer.bucket(eq(activatedExperiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject(), anyObject())).thenReturn(bucketedVariation); // activate the experiment Variation actualVariation = optimizely.getVariation(activatedExperiment.getKey(), testUserId); // verify that the bucketing algorithm was called correctly - verify(mockBucketer).bucket(activatedExperiment, testUserId, noAudienceProjectConfig, null, null); + verify(mockBucketer).bucket(eq(activatedExperiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject(), anyObject()); assertThat(actualVariation, is(bucketedVariation)); // verify that we didn't attempt to dispatch an event @@ -1347,7 +1346,7 @@ public void getVariationWithAudiences() throws Exception { Experiment experiment = validProjectConfig.getExperiments().get(0); Variation bucketedVariation = experiment.getVariations().get(0); - when(mockBucketer.bucket(experiment, testUserId, validProjectConfig, null, null)).thenReturn(bucketedVariation); + when(mockBucketer.bucket(eq(experiment), eq(testUserId), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(bucketedVariation); Optimizely optimizely = optimizelyBuilder.withBucketing(mockBucketer).build(); @@ -1356,7 +1355,7 @@ public void getVariationWithAudiences() throws Exception { Variation actualVariation = optimizely.getVariation(experiment.getKey(), testUserId, testUserAttributes); - verify(mockBucketer).bucket(experiment, testUserId, validProjectConfig, null, null); + verify(mockBucketer).bucket(eq(experiment), eq(testUserId), eq(validProjectConfig), anyObject(), anyObject()); assertThat(actualVariation, is(bucketedVariation)); } @@ -1397,7 +1396,7 @@ public void getVariationNoAudiences() throws Exception { Experiment experiment = noAudienceProjectConfig.getExperiments().get(0); Variation bucketedVariation = experiment.getVariations().get(0); - when(mockBucketer.bucket(experiment, testUserId, noAudienceProjectConfig, null, null)).thenReturn(bucketedVariation); + when(mockBucketer.bucket(eq(experiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject(), anyObject())).thenReturn(bucketedVariation); Optimizely optimizely = optimizelyBuilder .withConfig(noAudienceProjectConfig) @@ -1406,7 +1405,7 @@ public void getVariationNoAudiences() throws Exception { Variation actualVariation = optimizely.getVariation(experiment.getKey(), testUserId); - verify(mockBucketer).bucket(experiment, testUserId, noAudienceProjectConfig, null, null); + verify(mockBucketer).bucket(eq(experiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject(), anyObject()); assertThat(actualVariation, is(bucketedVariation)); } @@ -1464,7 +1463,7 @@ public void getVariationForGroupExperimentWithMatchingAttributes() throws Except attributes.put("browser_type", "chrome"); } - when(mockBucketer.bucket(experiment,"user", validProjectConfig, null, null)).thenReturn(variation); + when(mockBucketer.bucket(eq(experiment), eq("user"), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(variation); Optimizely optimizely = optimizelyBuilder.withBucketing(mockBucketer).build(); diff --git a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java index c6d2b4a1c..9c7eae98a 100644 --- a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java @@ -95,7 +95,7 @@ public void getVariationWhitelistedPrecedesAudienceEval() throws Exception { // no attributes provided for a experiment that has an audience assertThat(decisionService.getVariation(experiment, whitelistedUserId, Collections.emptyMap(), validProjectConfig), is(expectedVariation)); - verify(decisionService).getWhitelistedVariation(experiment, whitelistedUserId, null, null); + verify(decisionService).getWhitelistedVariation(eq(experiment), eq(whitelistedUserId), anyObject(), anyObject()); verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), any(ProjectConfig.class), anyObject(), anyObject()); } @@ -901,7 +901,7 @@ public void getVariationSavesBucketedVariationIntoUserProfile() throws Exception Collections.singletonMap(experiment.getId(), decision)); Bucketer mockBucketer = mock(Bucketer.class); - when(mockBucketer.bucket(experiment, userProfileId, noAudienceProjectConfig, null, null)).thenReturn(variation); + when(mockBucketer.bucket(eq(experiment), eq(userProfileId), eq(noAudienceProjectConfig), anyObject(), anyObject())).thenReturn(variation); DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, userProfileService); @@ -963,7 +963,7 @@ public void getVariationSavesANewUserProfile() throws Exception { UserProfileService userProfileService = mock(UserProfileService.class); DecisionService decisionService = new DecisionService(bucketer, mockErrorHandler, userProfileService); - when(bucketer.bucket(experiment, userProfileId, noAudienceProjectConfig, null, null)).thenReturn(variation); + when(bucketer.bucket(eq(experiment), eq(userProfileId), eq(noAudienceProjectConfig), anyObject(), anyObject())).thenReturn(variation); when(userProfileService.lookup(userProfileId)).thenReturn(null); assertEquals(variation, decisionService.getVariation(experiment, userProfileId, Collections.emptyMap(), noAudienceProjectConfig)); @@ -977,7 +977,7 @@ public void getVariationBucketingId() throws Exception { Experiment experiment = validProjectConfig.getExperiments().get(0); Variation expectedVariation = experiment.getVariations().get(0); - when(bucketer.bucket(experiment, "bucketId", validProjectConfig, null, null)).thenReturn(expectedVariation); + when(bucketer.bucket(eq(experiment), eq("bucketId"), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(expectedVariation); Map attr = new HashMap(); attr.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), "bucketId"); @@ -1001,8 +1001,8 @@ public void getVariationForRolloutWithBucketingId() { attributes.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), bucketingId); Bucketer bucketer = mock(Bucketer.class); - when(bucketer.bucket(rolloutRuleExperiment, userId, v4ProjectConfig, null, null)).thenReturn(null); - when(bucketer.bucket(rolloutRuleExperiment, bucketingId, v4ProjectConfig, null, null)).thenReturn(rolloutVariation); + when(bucketer.bucket(eq(rolloutRuleExperiment), eq(userId), eq(v4ProjectConfig), anyObject(), anyObject())).thenReturn(null); + when(bucketer.bucket(eq(rolloutRuleExperiment), eq(bucketingId), eq(v4ProjectConfig), anyObject(), anyObject())).thenReturn(rolloutVariation); DecisionService decisionService = spy(new DecisionService( bucketer, diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java index dd010731b..32cb17050 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java @@ -17,6 +17,8 @@ package com.optimizely.ab.optimizelyusercontext; import com.optimizely.ab.Optimizely; +import com.optimizely.ab.OptimizelyUserContext; +import com.optimizely.ab.optimizelydecision.OptimizelyDecision; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.junit.Test; diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java index c207c5fe9..a3ddd6255 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -20,11 +20,14 @@ import com.google.common.io.Resources; import com.optimizely.ab.EventHandlerRule; import com.optimizely.ab.Optimizely; +import com.optimizely.ab.OptimizelyUserContext; import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.event.ForwardingEventProcessor; import com.optimizely.ab.notification.NotificationCenter; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; +import com.optimizely.ab.optimizelydecision.OptimizelyDecision; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.junit.Assert; import org.junit.Before; From c0b6c85b30dc5d9caa68de2b97e0d5f06e4dbe81 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 15 Oct 2020 10:44:46 -0700 Subject: [PATCH 16/44] remove support for custom decisionservice --- .../optimizely/ab/OptimizelyUserContext.java | 27 ++--- .../com/optimizely/ab/bucketing/Bucketer.java | 45 ++------ .../ab/bucketing/DecisionService.java | 100 ++++-------------- .../optimizelydecision/DecisionMessage.java | 34 ++++++ .../optimizelydecision/DecisionReasons.java | 8 +- .../OptimizelyUserContextTest.java | 15 +-- 6 files changed, 81 insertions(+), 148 deletions(-) create mode 100644 core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionMessage.java diff --git a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java index 36c87512f..d372cfc9b 100644 --- a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java @@ -19,10 +19,11 @@ import com.optimizely.ab.bucketing.FeatureDecision; import com.optimizely.ab.config.*; import com.optimizely.ab.notification.DecisionNotification; -import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import com.optimizely.ab.optimizelydecision.DecisionMessage; import com.optimizely.ab.optimizelydecision.DecisionReasons; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecision; +import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,10 +42,6 @@ public class OptimizelyUserContext { private static final Logger logger = LoggerFactory.getLogger(OptimizelyUserContext.class); - public final static String SDK_NOT_READY = "Optimizely SDK not configured properly yet."; - public final static String FLAG_KEY_INVALID = "No flag was found for key \"%s\"."; - public final static String VARIABLE_VALUE_INVALID = "Variable value for key \"%s\" is invalid or wrong type."; - public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId, @Nonnull Map attributes) { @@ -93,12 +90,12 @@ public OptimizelyDecision decide(@Nonnull String key, ProjectConfig projectConfig = optimizely.getProjectConfig(); if (projectConfig == null) { - return OptimizelyDecision.createErrorDecision(key, this, SDK_NOT_READY); + return OptimizelyDecision.createErrorDecision(key, this, DecisionMessage.SDK_NOT_READY.reason()); } FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key); if (flag == null) { - return OptimizelyDecision.createErrorDecision(key, this, getFlagKeyInvalidMessage(key)); + return OptimizelyDecision.createErrorDecision(key, this, DecisionMessage.FLAG_KEY_INVALID.reason(key)); } Boolean sentEvent = false; @@ -107,7 +104,7 @@ public OptimizelyDecision decide(@Nonnull String key, Boolean includeReasons = allOptions.contains(OptimizelyDecideOption.INCLUDE_REASONS); DecisionReasons decisionReasons = new DecisionReasons(includeReasons); - Map copiedAttributes = copyAttributes(); + Map copiedAttributes = new HashMap<>(attributes); FeatureDecision flagDecision = optimizely.decisionService.getVariationForFeature( flag, userId, @@ -287,24 +284,12 @@ public void trackEvent(@Nonnull String eventName) throws UnknownEventTypeExcepti // Utils - private Map copyAttributes() { - return new HashMap<>(attributes); - } - private List getAllOptions(List options) { List copiedOptions = new ArrayList(optimizely.defaultDecideOptions); copiedOptions.addAll(options); return copiedOptions; } - public static String getFlagKeyInvalidMessage(String flagKey) { - return String.format(FLAG_KEY_INVALID, flagKey); - } - - public static String getVariableValueInvalidMessage(String variableKey) { - return String.format(VARIABLE_VALUE_INVALID, variableKey); - } - private Map getDecisionVariableMap(@Nonnull FeatureFlag flag, @Nonnull Variation variation, @Nonnull Boolean featureEnabled, @@ -321,7 +306,7 @@ private Map getDecisionVariableMap(@Nonnull FeatureFlag flag, Object convertedValue = optimizely.convertStringToType(value, variable.getType()); if (convertedValue == null) { - decisionReasons.addError(getVariableValueInvalidMessage(variable.getKey())); + decisionReasons.addError(DecisionMessage.VARIABLE_VALUE_INVALID.reason(variable.getKey())); } else if (convertedValue instanceof OptimizelyJSON) { convertedValue = ((OptimizelyJSON) convertedValue).toMap(); } diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index 7b0aa66eb..576227408 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -139,36 +139,6 @@ public Variation bucket(@Nonnull Experiment experiment, @Nonnull ProjectConfig projectConfig, @Nonnull List options, @Nonnull DecisionReasons reasons) { - - // support existing custom Bucketers - if (isMethodOverriden("bucket", Experiment.class, String.class, ProjectConfig.class)) { - return bucket(experiment, bucketingId, projectConfig); - } - - return bucketCore(experiment, bucketingId, projectConfig, options, reasons); - } - - /** - * Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3. - * - * @param experiment The Experiment in which the user is to be bucketed. - * @param bucketingId string A customer-assigned value used to create the key for the murmur hash. - * @param projectConfig The current projectConfig - * @return Variation the user is bucketed into or null. - */ - @Nullable - public Variation bucket(@Nonnull Experiment experiment, - @Nonnull String bucketingId, - @Nonnull ProjectConfig projectConfig) { - return bucketCore(experiment, bucketingId, projectConfig, Collections.emptyList(), new DecisionReasons()); - } - - @Nullable - public Variation bucketCore(@Nonnull Experiment experiment, - @Nonnull String bucketingId, - @Nonnull ProjectConfig projectConfig, - @Nonnull List options, - @Nonnull DecisionReasons reasons) { // ---------- Bucket User ---------- String groupId = experiment.getGroupId(); // check whether the experiment belongs to a group @@ -202,6 +172,13 @@ public Variation bucketCore(@Nonnull Experiment experiment, return bucketToVariation(experiment, bucketingId, options, reasons); } + @Nullable + public Variation bucket(@Nonnull Experiment experiment, + @Nonnull String bucketingId, + @Nonnull ProjectConfig projectConfig) { + return bucket(experiment, bucketingId, projectConfig, Collections.emptyList(), new DecisionReasons()); + } + //======== Helper methods ========// /** @@ -217,12 +194,4 @@ int generateBucketValue(int hashCode) { return (int) Math.floor(MAX_TRAFFIC_VALUE * ratio); } - Boolean isMethodOverriden(String methodName, Class... params) { - try { - return getClass() != Bucketer.class && getClass().getDeclaredMethod(methodName, params) != null; - } catch (NoSuchMethodException e) { - return false; - } - } - } diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index bc701326b..f80075c7d 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -95,30 +95,6 @@ public Variation getVariation(@Nonnull Experiment experiment, @Nonnull ProjectConfig projectConfig, @Nonnull List options, @Nonnull DecisionReasons reasons) { - - // support existing custom DecisionServices - if (isMethodOverriden("getVariation", Experiment.class, String.class, Map.class, ProjectConfig.class)) { - return getVariation(experiment, userId, filteredAttributes, projectConfig); - } - - return getVariationCore(experiment, userId, filteredAttributes, projectConfig, options, reasons); - } - - @Nullable - public Variation getVariation(@Nonnull Experiment experiment, - @Nonnull String userId, - @Nonnull Map filteredAttributes, - @Nonnull ProjectConfig projectConfig) { - return getVariationCore(experiment, userId, filteredAttributes, projectConfig, Collections.emptyList(), new DecisionReasons()); - } - - @Nullable - public Variation getVariationCore(@Nonnull Experiment experiment, - @Nonnull String userId, - @Nonnull Map filteredAttributes, - @Nonnull ProjectConfig projectConfig, - @Nonnull List options, - @Nonnull DecisionReasons reasons) { if (!ExperimentUtils.isExperimentActive(experiment, options, reasons)) { return null; } @@ -188,6 +164,14 @@ public Variation getVariationCore(@Nonnull Experiment experiment, return null; } + @Nullable + public Variation getVariation(@Nonnull Experiment experiment, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull ProjectConfig projectConfig) { + return getVariation(experiment, userId, filteredAttributes, projectConfig, Collections.emptyList(), new DecisionReasons()); + } + /** * Get the variation the user is bucketed into for the FeatureFlag * @@ -206,30 +190,6 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, @Nonnull ProjectConfig projectConfig, @Nonnull List options, @Nonnull DecisionReasons reasons) { - // support existing custom DecisionServices - if (isMethodOverriden("getVariationForFeature", FeatureFlag.class, String.class, Map.class, ProjectConfig.class)) { - return getVariationForFeature(featureFlag, userId, filteredAttributes, projectConfig); - } - - return getVariationForFeatureCore(featureFlag, userId, filteredAttributes, projectConfig, options, reasons); - } - - @Nonnull - public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, - @Nonnull String userId, - @Nonnull Map filteredAttributes, - @Nonnull ProjectConfig projectConfig) { - - return getVariationForFeatureCore(featureFlag, userId, filteredAttributes, projectConfig,Collections.emptyList(), new DecisionReasons()); - } - - @Nonnull - public FeatureDecision getVariationForFeatureCore(@Nonnull FeatureFlag featureFlag, - @Nonnull String userId, - @Nonnull Map filteredAttributes, - @Nonnull ProjectConfig projectConfig, - @Nonnull List options, - @Nonnull DecisionReasons reasons) { if (!featureFlag.getExperimentIds().isEmpty()) { for (String experimentId : featureFlag.getExperimentIds()) { Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId); @@ -256,6 +216,15 @@ public FeatureDecision getVariationForFeatureCore(@Nonnull FeatureFlag featureFl return featureDecision; } + @Nonnull + public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, + @Nonnull String userId, + @Nonnull Map filteredAttributes, + @Nonnull ProjectConfig projectConfig) { + + return getVariationForFeature(featureFlag, userId, filteredAttributes, projectConfig,Collections.emptyList(), new DecisionReasons()); + } + /** * Try to bucket the user into a rollout rule. * Evaluate the user for rules in priority order by seeing if the user satisfies the audience. @@ -598,27 +567,6 @@ public Variation getForcedVariation(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull List options, @Nonnull DecisionReasons reasons) { - - // support existing custom DecisionServices - if (isMethodOverriden("getForcedVariation", Experiment.class, String.class)) { - return getForcedVariation(experiment, userId); - } - - return getForcedVariationCore(experiment, userId, options, reasons); - } - - @Nullable - public Variation getForcedVariation(@Nonnull Experiment experiment, - @Nonnull String userId) { - return getForcedVariationCore(experiment, userId, Collections.emptyList(), new DecisionReasons()); - } - - @Nullable - public Variation getForcedVariationCore(@Nonnull Experiment experiment, - @Nonnull String userId, - @Nonnull List options, - @Nonnull DecisionReasons reasons) { - // if the user id is invalid, return false. if (!validateUserId(userId)) { return null; @@ -642,6 +590,12 @@ public Variation getForcedVariationCore(@Nonnull Experiment experiment, return null; } + @Nullable + public Variation getForcedVariation(@Nonnull Experiment experiment, + @Nonnull String userId) { + return getForcedVariation(experiment, userId, Collections.emptyList(), new DecisionReasons()); + } + /** * Helper function to check that the provided userId is valid * @@ -657,12 +611,4 @@ private boolean validateUserId(String userId) { return true; } - Boolean isMethodOverriden(String methodName, Class... params) { - try { - return getClass() != DecisionService.class && getClass().getDeclaredMethod(methodName, params) != null; - } catch (NoSuchMethodException e) { - return false; - } - } - } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionMessage.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionMessage.java new file mode 100644 index 000000000..c66be6bee --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionMessage.java @@ -0,0 +1,34 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.optimizely.ab.optimizelydecision; + +public enum DecisionMessage { + SDK_NOT_READY("Optimizely SDK not configured properly yet."), + FLAG_KEY_INVALID("No flag was found for key \"%s\"."), + VARIABLE_VALUE_INVALID("Variable value for key \"%s\" is invalid or wrong type."); + + private String format; + + DecisionMessage(String format) { + this.format = format; + } + + public String reason(Object... args){ + return String.format(format, args); + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java index c7c7d91b6..ca8bf7c12 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java @@ -21,14 +21,12 @@ public class DecisionReasons { - boolean includeReasons; - List errors; - List logs; + private boolean includeReasons; + private final List errors = new ArrayList<>(); + private final List logs = new ArrayList<>(); public DecisionReasons(boolean includeReasons) { this.includeReasons = includeReasons; - this.errors = new ArrayList(); - this.logs = new ArrayList(); } public DecisionReasons() { diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java index a3ddd6255..9cd978852 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java @@ -26,6 +26,7 @@ import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.event.ForwardingEventProcessor; import com.optimizely.ab.notification.NotificationCenter; +import com.optimizely.ab.optimizelydecision.DecisionMessage; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecision; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; @@ -441,11 +442,11 @@ public void decideOptions_includeReasons() { String flagKey = "invalid_key"; OptimizelyDecision decision = user.decide(flagKey); assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); + assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); + assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); flagKey = "feature_1"; decision = user.decide(flagKey); @@ -505,7 +506,7 @@ public void decide_sdkNotReady() { assertEquals(decision.getUserContext(), user); assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), OptimizelyUserContext.SDK_NOT_READY); + assertEquals(decision.getReasons().get(0), DecisionMessage.SDK_NOT_READY.reason()); } @Test @@ -518,7 +519,7 @@ public void decide_invalidFeatureKey() { assertNull(decision.getVariationKey()); assertFalse(decision.getEnabled()); assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); + assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); } @Test @@ -560,7 +561,7 @@ public void decideAll_errorDecisionIncluded() { OptimizelyDecision.createErrorDecision( flagKey2, user, - OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey2))); + DecisionMessage.FLAG_KEY_INVALID.reason(flagKey2))); } // reasons (errors) @@ -574,7 +575,7 @@ public void decideReasons_sdkNotReady() { OptimizelyDecision decision = user.decide(flagKey); assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), OptimizelyUserContext.SDK_NOT_READY); + assertEquals(decision.getReasons().get(0), DecisionMessage.SDK_NOT_READY.reason()); } @Test @@ -585,7 +586,7 @@ public void decideReasons_featureKeyInvalid() { OptimizelyDecision decision = user.decide(flagKey); assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), OptimizelyUserContext.getFlagKeyInvalidMessage(flagKey)); + assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); } @Test From a93fc23f2f82fb06e367f342a156253a0fe1a3cd Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 15 Oct 2020 11:24:03 -0700 Subject: [PATCH 17/44] thread-safe setAttribute --- .../main/java/com/optimizely/ab/OptimizelyUserContext.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java index d372cfc9b..ac2028da4 100644 --- a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java @@ -29,6 +29,7 @@ import javax.annotation.Nonnull; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class OptimizelyUserContext { @Nonnull @@ -47,7 +48,7 @@ public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull Map attributes) { this.optimizely = optimizely; this.userId = userId; - this.attributes = new HashMap<>(attributes); + this.attributes = new ConcurrentHashMap<>(attributes); } public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId) { @@ -59,7 +60,7 @@ public String getUserId() { } public Map getAttributes() { - return attributes; + return new HashMap(attributes); } public Optimizely getOptimizely() { From f803cd7d40e4f19c0995173271ad39207a5b553f Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 15 Oct 2020 14:29:51 -0700 Subject: [PATCH 18/44] add option and reasons to condition evaluation --- .../ab/config/audience/AndCondition.java | 16 ++- .../config/audience/AudienceIdCondition.java | 22 +++-- .../ab/config/audience/Condition.java | 10 ++ .../ab/config/audience/EmptyCondition.java | 14 ++- .../ab/config/audience/NotCondition.java | 16 ++- .../ab/config/audience/NullCondition.java | 14 ++- .../ab/config/audience/OrCondition.java | 15 ++- .../ab/config/audience/UserAttribute.java | 13 ++- .../ab/internal/ExperimentUtils.java | 26 +---- .../OptimizelyUserContextTest.java | 66 ++++++++++--- .../AudienceConditionEvaluationTest.java | 99 +++++++++---------- .../OptimizelyDecisionTest.java | 3 +- 12 files changed, 204 insertions(+), 110 deletions(-) rename core-api/src/test/java/com/optimizely/ab/{optimizelyusercontext => }/OptimizelyUserContextTest.java (91%) rename core-api/src/test/java/com/optimizely/ab/{optimizelyusercontext => optimizelydecision}/OptimizelyDecisionTest.java (96%) diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java index 8b458d059..a8a5f3e65 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java @@ -17,10 +17,12 @@ package com.optimizely.ab.config.audience; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -40,7 +42,10 @@ public List getConditions() { } @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { + public Boolean evaluate(ProjectConfig config, + Map attributes, + List options, + DecisionReasons reasons) { if (conditions == null) return null; boolean foundNull = false; // According to the matrix where: @@ -51,7 +56,7 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { // true and true is true // null and null is null for (Condition condition : conditions) { - Boolean conditionEval = condition.evaluate(config, attributes); + Boolean conditionEval = condition.evaluate(config, attributes, options, reasons); if (conditionEval == null) { foundNull = true; } else if (!conditionEval) { // false with nulls or trues is false. @@ -67,6 +72,11 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { return true; // otherwise, return true } + @Nullable + public Boolean evaluate(ProjectConfig config, Map attributes) { + return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); + } + @Override public String toString() { StringBuilder s = new StringBuilder(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java index 57a4e5bec..ee58fe4d9 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java @@ -18,14 +18,15 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; import com.optimizely.ab.config.ProjectConfig; -import com.optimizely.ab.internal.InvalidAudienceCondition; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -66,20 +67,29 @@ public String getAudienceId() { @Nullable @Override - public Boolean evaluate(ProjectConfig config, Map attributes) { + public Boolean evaluate(ProjectConfig config, + Map attributes, + List options, + DecisionReasons reasons) { if (config != null) { audience = config.getAudienceIdMapping().get(audienceId); } if (audience == null) { - logger.error("Audience {} could not be found.", audienceId); + String message = reasons.addInfoF("Audience %s could not be found.", audienceId); + logger.error(message); return null; } logger.debug("Starting to evaluate audience \"{}\" with conditions: {}.", audience.getId(), audience.getConditions()); - Boolean result = audience.getConditions().evaluate(config, attributes); + Boolean result = audience.getConditions().evaluate(config, attributes, options, reasons); logger.debug("Audience \"{}\" evaluated to {}.", audience.getId(), result); return result; } + @Nullable + public Boolean evaluate(ProjectConfig config, Map attributes) { + return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java index 772d2b03e..dff29a0dc 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java @@ -17,8 +17,11 @@ package com.optimizely.ab.config.audience; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nullable; +import java.util.List; import java.util.Map; /** @@ -26,6 +29,13 @@ */ public interface Condition { + @Nullable + Boolean evaluate(ProjectConfig config, + Map attributes, + List options, + DecisionReasons reasons); + @Nullable Boolean evaluate(ProjectConfig config, Map attributes); + } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java index 8f8aedeae..ad9d79b41 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java @@ -16,15 +16,27 @@ package com.optimizely.ab.config.audience; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; import java.util.Map; public class EmptyCondition implements Condition { @Nullable @Override - public Boolean evaluate(ProjectConfig config, Map attributes) { + public Boolean evaluate(ProjectConfig config, + Map attributes, + List options, + DecisionReasons reasons) { return true; } + @Nullable + public Boolean evaluate(ProjectConfig config, Map attributes) { + return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); + } + } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java index b7f45f2ac..96a192d98 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java @@ -17,11 +17,15 @@ package com.optimizely.ab.config.audience; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -41,12 +45,20 @@ public Condition getCondition() { } @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { + public Boolean evaluate(ProjectConfig config, + Map attributes, + List options, + DecisionReasons reasons) { - Boolean conditionEval = condition == null ? null : condition.evaluate(config, attributes); + Boolean conditionEval = condition == null ? null : condition.evaluate(config, attributes, options, reasons); return (conditionEval == null ? null : !conditionEval); } + @Nullable + public Boolean evaluate(ProjectConfig config, Map attributes) { + return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); + } + @Override public String toString() { StringBuilder s = new StringBuilder(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java index fcf5100db..bd49229ae 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java @@ -16,14 +16,26 @@ package com.optimizely.ab.config.audience; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; import java.util.Map; public class NullCondition implements Condition { @Nullable @Override - public Boolean evaluate(ProjectConfig config, Map attributes) { + public Boolean evaluate(ProjectConfig config, + Map attributes, + List options, + DecisionReasons reasons) { return null; } + + @Nullable + public Boolean evaluate(ProjectConfig config, Map attributes) { + return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); + } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java index 70572a9a9..c3f890b98 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java @@ -17,10 +17,13 @@ package com.optimizely.ab.config.audience; import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -45,11 +48,14 @@ public List getConditions() { // false or false is false // null or null is null @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { + public Boolean evaluate(ProjectConfig config, + Map attributes, + List options, + DecisionReasons reasons) { if (conditions == null) return null; boolean foundNull = false; for (Condition condition : conditions) { - Boolean conditionEval = condition.evaluate(config, attributes); + Boolean conditionEval = condition.evaluate(config, attributes, options, reasons); if (conditionEval == null) { // true with falses and nulls is still true foundNull = true; } else if (conditionEval) { @@ -65,6 +71,11 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { return false; } + @Nullable + public Boolean evaluate(ProjectConfig config, Map attributes) { + return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); + } + @Override public String toString() { StringBuilder s = new StringBuilder(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java index 277f2f184..9141300aa 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.match.*; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +30,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -71,7 +74,10 @@ public Object getValue() { } @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { + public Boolean evaluate(ProjectConfig config, + Map attributes, + List options, + DecisionReasons reasons) { if (attributes == null) { attributes = Collections.emptyMap(); } @@ -118,6 +124,11 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { return null; } + @Nullable + public Boolean evaluate(ProjectConfig config, Map attributes) { + return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); + } + @Override public String toString() { final String valueStr; diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java index e703cc527..479e1c237 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java @@ -86,10 +86,10 @@ public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig proj @Nonnull DecisionReasons reasons) { if (experiment.getAudienceConditions() != null) { logger.debug("Evaluating audiences for {} \"{}\": {}.", loggingEntityType, loggingKey, experiment.getAudienceConditions()); - Boolean resolveReturn = evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey); + Boolean resolveReturn = evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, options, reasons); return resolveReturn == null ? false : resolveReturn; } else { - Boolean resolveReturn = evaluateAudience(projectConfig, experiment, attributes, loggingEntityType, loggingKey); + Boolean resolveReturn = evaluateAudience(projectConfig, experiment, attributes, loggingEntityType, loggingKey, options, reasons); return Boolean.TRUE.equals(resolveReturn); } } @@ -137,7 +137,7 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, logger.debug("Evaluating audiences for {} \"{}\": {}.", loggingEntityType, loggingKey, conditions); - Boolean result = implicitOr.evaluate(projectConfig, attributes); + Boolean result = implicitOr.evaluate(projectConfig, attributes, options, reasons); String message = reasons.addInfoF("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); logger.info(message); @@ -145,15 +145,6 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, return result; } - @Nullable - public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, - @Nonnull Experiment experiment, - @Nonnull Map attributes, - @Nonnull String loggingEntityType, - @Nonnull String loggingKey) { - return evaluateAudience(projectConfig, experiment, attributes, loggingEntityType, loggingKey, Collections.emptyList(), new DecisionReasons()); - } - @Nullable public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectConfig, @Nonnull Experiment experiment, @@ -167,7 +158,7 @@ public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectC if (conditions == null) return null; try { - Boolean result = conditions.evaluate(projectConfig, attributes); + Boolean result = conditions.evaluate(projectConfig, attributes, options, reasons); String message = reasons.addInfoF("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); logger.info(message); return result; @@ -178,13 +169,4 @@ public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectC } } - @Nullable - public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectConfig, - @Nonnull Experiment experiment, - @Nonnull Map attributes, - @Nonnull String loggingEntityType, - @Nonnull String loggingKey) { - return evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, Collections.emptyList(), new DecisionReasons()); - } - } \ No newline at end of file diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java similarity index 91% rename from core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java rename to core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 9cd978852..5be10890a 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -14,15 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.optimizely.ab.optimizelyusercontext; +package com.optimizely.ab; import com.google.common.base.Charsets; import com.google.common.io.Resources; -import com.optimizely.ab.EventHandlerRule; -import com.optimizely.ab.Optimizely; -import com.optimizely.ab.OptimizelyUserContext; import com.optimizely.ab.bucketing.UserProfileService; +import com.optimizely.ab.config.DatafileProjectConfig; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.Rollout; import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.event.ForwardingEventProcessor; import com.optimizely.ab.notification.NotificationCenter; @@ -30,6 +30,7 @@ import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecision; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import junit.framework.TestCase; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -104,6 +105,20 @@ public void setAttribute() { assertEquals(newAttributes.get("k4"), 3.5); } + @Test + public void setAttribute_noAttribute() { + OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId); + + user.setAttribute("k1", "v1"); + user.setAttribute("k2", true); + + assertEquals(user.getOptimizely(), optimizely); + assertEquals(user.getUserId(), userId); + Map newAttributes = user.getAttributes(); + assertEquals(newAttributes.get("k1"), "v1"); + assertEquals(newAttributes.get("k2"), true); + } + @Test public void setAttribute_override() { Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); @@ -112,8 +127,6 @@ public void setAttribute_override() { user.setAttribute("k1", "v1"); user.setAttribute(ATTRIBUTE_HOUSE_KEY, "v2"); - assertEquals(user.getOptimizely(), optimizely); - assertEquals(user.getUserId(), userId); Map newAttributes = user.getAttributes(); assertEquals(newAttributes.get("k1"), "v1"); assertEquals(newAttributes.get(ATTRIBUTE_HOUSE_KEY), "v2"); @@ -442,7 +455,7 @@ public void decideOptions_includeReasons() { String flagKey = "invalid_key"; OptimizelyDecision decision = user.decide(flagKey); assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); + TestCase.assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertEquals(decision.getReasons().size(), 1); @@ -597,6 +610,16 @@ public void decideReasons_variableValueInvalid() { @Test public void decideReasons_conditionNoMatchingAudience() throws ConfigParseException { + String flagKey = "feature_1"; + String audienceId = "invalid_id"; + setAudienceForFeatureTest(flagKey, audienceId); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("Audience %s could not be found.", audienceId) + )); } @Test @@ -641,8 +664,7 @@ public void decideReasons_gotVariationFromUserProfile() throws Exception { OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format( - "Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", variationKey2, experimentKey, userId) + String.format("Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", variationKey2, experimentKey, userId) )); } @@ -665,9 +687,8 @@ public void decideReasons_userMeetsConditionsForTargetingRule() { OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format( - "The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) - )); + String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) + )); } @Test @@ -790,12 +811,27 @@ Map createUserProfileMap(String experimentId, String variationId return userProfileMap; } - void setAudienceForFeatureTest(String featureKey, String audienceId) throws ConfigParseException { - String experimentId = optimizely.getProjectConfig().getFeatureKeyMapping().get(featureKey).getExperimentIds().get(0); - Experiment experimentReal = optimizely.getProjectConfig().getExperimentIdMapping().get(experimentId); + void setAudienceForFeatureTest(String flagKey, String audienceId) throws ConfigParseException { + ProjectConfig configReal = new DatafileProjectConfig.Builder().withDatafile(datafile).build(); + ProjectConfig config = spy(configReal); + optimizely = Optimizely.builder().withConfig(config).build(); + + String experimentId = config.getFeatureKeyMapping().get(flagKey).getExperimentIds().get(0); + String rolloutId = config.getFeatureKeyMapping().get(flagKey).getRolloutId(); + Map experimentIdMapping = new HashMap<>(config.getExperimentIdMapping()); + Map rolloutIdMapping = new HashMap<>(config.getRolloutIdMapping()); + Experiment experimentReal = experimentIdMapping.get(experimentId); + Rollout rolloutReal = rolloutIdMapping.get(rolloutId); Experiment experiment = spy(experimentReal); + Rollout rollout = spy(rolloutReal); when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + + experimentIdMapping.put(experimentId, experiment); + rolloutIdMapping.put(rolloutId, rollout); + + when(config.getExperimentIdMapping()).thenReturn(experimentIdMapping); + when(config.getRolloutIdMapping()).thenReturn(rolloutIdMapping); } } diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java index 772d22ef7..dbcdda88e 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java @@ -18,27 +18,16 @@ import ch.qos.logback.classic.Level; import com.optimizely.ab.internal.LogbackVerifier; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.*; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; /** * Tests for the evaluation of different audience condition types (And, Or, Not, and UserAttribute) @@ -1183,11 +1172,11 @@ public void notConditionEvaluateNull() { @Test public void notConditionEvaluateTrue() { UserAttribute userAttribute = mock(UserAttribute.class); - when(userAttribute.evaluate(null, testUserAttributes)).thenReturn(false); + when(userAttribute.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); NotCondition notCondition = new NotCondition(userAttribute); assertTrue(notCondition.evaluate(null, testUserAttributes)); - verify(userAttribute, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1196,11 +1185,11 @@ public void notConditionEvaluateTrue() { @Test public void notConditionEvaluateFalse() { UserAttribute userAttribute = mock(UserAttribute.class); - when(userAttribute.evaluate(null, testUserAttributes)).thenReturn(true); + when(userAttribute.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); NotCondition notCondition = new NotCondition(userAttribute); assertFalse(notCondition.evaluate(null, testUserAttributes)); - verify(userAttribute, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1209,10 +1198,10 @@ public void notConditionEvaluateFalse() { @Test public void orConditionEvaluateTrue() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(null, testUserAttributes)).thenReturn(true); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(null, testUserAttributes)).thenReturn(false); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(userAttribute1); @@ -1220,9 +1209,9 @@ public void orConditionEvaluateTrue() { OrCondition orCondition = new OrCondition(conditions); assertTrue(orCondition.evaluate(null, testUserAttributes)); - verify(userAttribute1, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation - verify(userAttribute2, times(0)).evaluate(null, testUserAttributes); + verify(userAttribute2, times(0)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1231,10 +1220,10 @@ public void orConditionEvaluateTrue() { @Test public void orConditionEvaluateTrueWithNullAndTrue() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(null, testUserAttributes)).thenReturn(null); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(null); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(null, testUserAttributes)).thenReturn(true); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); List conditions = new ArrayList(); conditions.add(userAttribute1); @@ -1242,9 +1231,9 @@ public void orConditionEvaluateTrueWithNullAndTrue() { OrCondition orCondition = new OrCondition(conditions); assertTrue(orCondition.evaluate(null, testUserAttributes)); - verify(userAttribute1, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation - verify(userAttribute2, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1253,10 +1242,10 @@ public void orConditionEvaluateTrueWithNullAndTrue() { @Test public void orConditionEvaluateNullWithNullAndFalse() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(null, testUserAttributes)).thenReturn(null); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(null); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(null, testUserAttributes)).thenReturn(false); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(userAttribute1); @@ -1264,9 +1253,9 @@ public void orConditionEvaluateNullWithNullAndFalse() { OrCondition orCondition = new OrCondition(conditions); assertNull(orCondition.evaluate(null, testUserAttributes)); - verify(userAttribute1, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation - verify(userAttribute2, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1275,10 +1264,10 @@ public void orConditionEvaluateNullWithNullAndFalse() { @Test public void orConditionEvaluateFalseWithFalseAndFalse() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(null, testUserAttributes)).thenReturn(false); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(null, testUserAttributes)).thenReturn(false); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(userAttribute1); @@ -1286,9 +1275,9 @@ public void orConditionEvaluateFalseWithFalseAndFalse() { OrCondition orCondition = new OrCondition(conditions); assertFalse(orCondition.evaluate(null, testUserAttributes)); - verify(userAttribute1, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation - verify(userAttribute2, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1297,10 +1286,10 @@ public void orConditionEvaluateFalseWithFalseAndFalse() { @Test public void orConditionEvaluateFalse() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(null, testUserAttributes)).thenReturn(false); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(null, testUserAttributes)).thenReturn(false); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(userAttribute1); @@ -1308,8 +1297,8 @@ public void orConditionEvaluateFalse() { OrCondition orCondition = new OrCondition(conditions); assertFalse(orCondition.evaluate(null, testUserAttributes)); - verify(userAttribute1, times(1)).evaluate(null, testUserAttributes); - verify(userAttribute2, times(1)).evaluate(null, testUserAttributes); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1318,10 +1307,10 @@ public void orConditionEvaluateFalse() { @Test public void andConditionEvaluateTrue() { OrCondition orCondition1 = mock(OrCondition.class); - when(orCondition1.evaluate(null, testUserAttributes)).thenReturn(true); + when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); OrCondition orCondition2 = mock(OrCondition.class); - when(orCondition2.evaluate(null, testUserAttributes)).thenReturn(true); + when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); List conditions = new ArrayList(); conditions.add(orCondition1); @@ -1329,8 +1318,8 @@ public void andConditionEvaluateTrue() { AndCondition andCondition = new AndCondition(conditions); assertTrue(andCondition.evaluate(null, testUserAttributes)); - verify(orCondition1, times(1)).evaluate(null, testUserAttributes); - verify(orCondition2, times(1)).evaluate(null, testUserAttributes); + verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1339,10 +1328,10 @@ public void andConditionEvaluateTrue() { @Test public void andConditionEvaluateFalseWithNullAndFalse() { OrCondition orCondition1 = mock(OrCondition.class); - when(orCondition1.evaluate(null, testUserAttributes)).thenReturn(null); + when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(null); OrCondition orCondition2 = mock(OrCondition.class); - when(orCondition2.evaluate(null, testUserAttributes)).thenReturn(false); + when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(orCondition1); @@ -1350,8 +1339,8 @@ public void andConditionEvaluateFalseWithNullAndFalse() { AndCondition andCondition = new AndCondition(conditions); assertFalse(andCondition.evaluate(null, testUserAttributes)); - verify(orCondition1, times(1)).evaluate(null, testUserAttributes); - verify(orCondition2, times(1)).evaluate(null, testUserAttributes); + verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1360,10 +1349,10 @@ public void andConditionEvaluateFalseWithNullAndFalse() { @Test public void andConditionEvaluateNullWithNullAndTrue() { OrCondition orCondition1 = mock(OrCondition.class); - when(orCondition1.evaluate(null, testUserAttributes)).thenReturn(null); + when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(null); OrCondition orCondition2 = mock(OrCondition.class); - when(orCondition2.evaluate(null, testUserAttributes)).thenReturn(true); + when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); List conditions = new ArrayList(); conditions.add(orCondition1); @@ -1371,8 +1360,8 @@ public void andConditionEvaluateNullWithNullAndTrue() { AndCondition andCondition = new AndCondition(conditions); assertNull(andCondition.evaluate(null, testUserAttributes)); - verify(orCondition1, times(1)).evaluate(null, testUserAttributes); - verify(orCondition2, times(1)).evaluate(null, testUserAttributes); + verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } /** @@ -1381,10 +1370,10 @@ public void andConditionEvaluateNullWithNullAndTrue() { @Test public void andConditionEvaluateFalse() { OrCondition orCondition1 = mock(OrCondition.class); - when(orCondition1.evaluate(null, testUserAttributes)).thenReturn(false); + when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); OrCondition orCondition2 = mock(OrCondition.class); - when(orCondition2.evaluate(null, testUserAttributes)).thenReturn(true); + when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); // and[false, true] List conditions = new ArrayList(); @@ -1393,9 +1382,9 @@ public void andConditionEvaluateFalse() { AndCondition andCondition = new AndCondition(conditions); assertFalse(andCondition.evaluate(null, testUserAttributes)); - verify(orCondition1, times(1)).evaluate(null, testUserAttributes); + verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'And' evaluation - verify(orCondition2, times(0)).evaluate(null, testUserAttributes); + verify(orCondition2, times(0)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); OrCondition orCondition3 = mock(OrCondition.class); when(orCondition3.evaluate(null, testUserAttributes)).thenReturn(null); diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelydecision/OptimizelyDecisionTest.java similarity index 96% rename from core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java rename to core-api/src/test/java/com/optimizely/ab/optimizelydecision/OptimizelyDecisionTest.java index 32cb17050..92fdbde4a 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyusercontext/OptimizelyDecisionTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelydecision/OptimizelyDecisionTest.java @@ -14,11 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.optimizely.ab.optimizelyusercontext; +package com.optimizely.ab.optimizelydecision; import com.optimizely.ab.Optimizely; import com.optimizely.ab.OptimizelyUserContext; -import com.optimizely.ab.optimizelydecision.OptimizelyDecision; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.junit.Test; From 43f7b017f8b03477553812e86d6e0714121c7d4e Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 15 Oct 2020 18:03:56 -0700 Subject: [PATCH 19/44] add more tests for reasons --- .../ab/config/audience/UserAttribute.java | 22 +- .../ab/OptimizelyUserContextTest.java | 286 +++++++++++++++--- 2 files changed, 252 insertions(+), 56 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java index 9141300aa..5044e3dac 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java @@ -85,7 +85,8 @@ public Boolean evaluate(ProjectConfig config, Object userAttributeValue = attributes.get(name); if (!"custom_attribute".equals(type)) { - logger.warn("Audience condition \"{}\" uses an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.", this); + String message = reasons.addInfoF("Audience condition \"%s\" uses an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.", this); + logger.warn(message); return null; // unknown type } // check user attribute value is equal @@ -100,26 +101,27 @@ public Boolean evaluate(ProjectConfig config, } catch(UnknownValueTypeException e) { if (!attributes.containsKey(name)) { //Missing attribute value - logger.debug("Audience condition \"{}\" evaluated to UNKNOWN because no value was passed for user attribute \"{}\"", this, name); + String message = reasons.addInfoF("Audience condition \"%s\" evaluated to UNKNOWN because no value was passed for user attribute \"%s\"", this, name); + logger.debug(message); } else { //if attribute value is not valid if (userAttributeValue != null) { - logger.warn( - "Audience condition \"{}\" evaluated to UNKNOWN because a value of type \"{}\" was passed for user attribute \"{}\"", + String message = reasons.addInfoF("Audience condition \"%s\" evaluated to UNKNOWN because a value of type \"%s\" was passed for user attribute \"%s\"", this, userAttributeValue.getClass().getCanonicalName(), name); + logger.warn(message); } else { - logger.debug( - "Audience condition \"{}\" evaluated to UNKNOWN because a null value was passed for user attribute \"{}\"", - this, - name); + String message = reasons.addInfoF("Audience condition \"%s\" evaluated to UNKNOWN because a null value was passed for user attribute \"%s\"", this, name); + logger.debug(message); } } } catch (UnknownMatchTypeException | UnexpectedValueTypeException e) { - logger.warn("Audience condition \"{}\" " + e.getMessage(), this); + String message = reasons.addInfoF("Audience condition \"%s\" " + e.getMessage(), this); + logger.warn(message); } catch (NullPointerException e) { - logger.error("attribute or value null for match {}", match != null ? match : "legacy condition", e); + String message = reasons.addInfoF("attribute or value null for match %s", match != null ? match : "legacy condition"); + logger.error(message, e); } return null; } diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 5be10890a..ce8131049 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -19,10 +19,7 @@ import com.google.common.base.Charsets; import com.google.common.io.Resources; import com.optimizely.ab.bucketing.UserProfileService; -import com.optimizely.ab.config.DatafileProjectConfig; -import com.optimizely.ab.config.Experiment; -import com.optimizely.ab.config.ProjectConfig; -import com.optimizely.ab.config.Rollout; +import com.optimizely.ab.config.*; import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.event.ForwardingEventProcessor; import com.optimizely.ab.notification.NotificationCenter; @@ -44,19 +41,23 @@ import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.*; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class OptimizelyUserContextTest { @Rule public EventHandlerRule eventHandler = new EventHandlerRule(); - public Optimizely optimizely; - public String datafile; - public String userId = "tester"; + String userId = "tester"; boolean isListenerCalled = false; + Optimizely optimizely; + String datafile; + ProjectConfig config; + Map experimentIdMapping; + Map featureKeyMapping; + Map groupIdMapping; + @Before public void setUp() throws Exception { datafile = Resources.toString(Resources.getResource("config/decide-project-config.json"), Charsets.UTF_8); @@ -604,6 +605,17 @@ public void decideReasons_featureKeyInvalid() { @Test public void decideReasons_variableValueInvalid() { + String flagKey = "feature_1"; + + FeatureFlag flag = getSpyFeatureFlag(flagKey); + List variables = Arrays.asList(new FeatureVariable("any-id", "any-key", "invalid", null, "integer", null)); + when(flag.getVariables()).thenReturn(variables); + addSpyFeatureFlag(flag); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getReasons().get(0), DecisionMessage.VARIABLE_VALUE_INVALID.reason("any-key")); } // reasons (logs with includeReasons) @@ -612,10 +624,11 @@ public void decideReasons_variableValueInvalid() { public void decideReasons_conditionNoMatchingAudience() throws ConfigParseException { String flagKey = "feature_1"; String audienceId = "invalid_id"; - setAudienceForFeatureTest(flagKey, audienceId); - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); assertTrue(decision.getReasons().contains( String.format("Audience %s could not be found.", audienceId) @@ -623,26 +636,108 @@ public void decideReasons_conditionNoMatchingAudience() throws ConfigParseExcept } @Test - public void decideReasons_conditionInvalidFormat() {} - @Test - public void decideReasons_evaluateAttributeInvalidCondition() {} - @Test - public void decideReasons_evaluateAttributeInvalidType() {} - @Test - public void decideReasons_evaluateAttributeValueOutOfRange() {} + public void decideReasons_evaluateAttributeInvalidType() { + String flagKey = "feature_1"; + String audienceId = "13389130056"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("country", 25)); + + assertTrue(decision.getReasons().contains( + String.format("Audience condition \"{name='country', type='custom_attribute', match='exact', value='US'}\" evaluated to UNKNOWN because a value of type \"java.lang.Integer\" was passed for user attribute \"country\"") + )); + } + @Test - public void decideReasons_userAttributeInvalidType() {} + public void decideReasons_evaluateAttributeValueOutOfRange() { + String flagKey = "feature_1"; + String audienceId = "age_18"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", (float)Math.pow(2, 54))); + + assertTrue(decision.getReasons().contains( + String.format("Audience condition \"{name='age', type='custom_attribute', match='gt', value=18.0}\" evaluated to UNKNOWN because a value of type \"java.lang.Float\" was passed for user attribute \"age\"") + )); + } + @Test - public void decideReasons_userAttributeInvalidMatch() {} + public void decideReasons_userAttributeInvalidType() { + String flagKey = "feature_1"; + String audienceId = "invalid_type"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); + + assertTrue(decision.getReasons().contains( + String.format("Audience condition \"{name='age', type='invalid', match='gt', value=18.0}\" uses an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.") + )); + } + @Test - public void decideReasons_userAttributeNilValue() {} + public void decideReasons_userAttributeInvalidMatch() { + String flagKey = "feature_1"; + String audienceId = "invalid_match"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); + + assertTrue(decision.getReasons().contains( + String.format("Audience condition \"{name='age', type='custom_attribute', match='invalid', value=18.0}\" uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK.") + )); + } + @Test - public void decideReasons_userAttributeInvalidName() {} + public void decideReasons_userAttributeNilValue() { + String flagKey = "feature_1"; + String audienceId = "nil_value"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); + + assertTrue(decision.getReasons().contains( + String.format("Audience condition \"{name='age', type='custom_attribute', match='gt', value=null}\" evaluated to UNKNOWN because a value of type \"java.lang.Integer\" was passed for user attribute \"age\"") + )); + } + @Test - public void decideReasons_missingAttributeValue() {} + public void decideReasons_missingAttributeValue() { + String flagKey = "feature_1"; + String audienceId = "age_18"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("Audience condition \"{name='age', type='custom_attribute', match='gt', value=18.0}\" evaluated to UNKNOWN because no value was passed for user attribute \"age\"") + )); + } @Test - public void decideReasons_experimentNotRunning() {} + public void decideReasons_experimentNotRunning() { + String flagKey = "feature_1"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.isActive()).thenReturn(false); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("Experiment \"exp_with_audience\" is not running.") + )); + } @Test public void decideReasons_gotVariationFromUserProfile() throws Exception { @@ -670,12 +765,32 @@ public void decideReasons_gotVariationFromUserProfile() throws Exception { @Test public void decideReasons_forcedVariationFound() { + String flagKey = "feature_1"; + String variationKey = "b"; + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getUserIdToVariationKeyMap()).thenReturn(Collections.singletonMap(userId, variationKey)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("User \"%s\" is forced in variation \"%s\".", userId, variationKey) + )); } @Test public void decideReasons_forcedVariationFoundButInvalid() { + String flagKey = "feature_1"; + String variationKey = "invalid-key"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getUserIdToVariationKeyMap()).thenReturn(Collections.singletonMap(userId, variationKey)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + assertTrue(decision.getReasons().contains( + String.format("Variation \"%s\" is not in the datafile. Not activating user \"%s\".", variationKey, userId) + )); } @Test @@ -760,28 +875,68 @@ public void decideReasons_userBucketedIntoVariationInExperiment() { @Test public void decideReasons_userNotBucketedIntoVariation() { - } + String flagKey = "feature_2"; - @Test - public void decideReasons_userBucketedIntoInvalidVariation() { + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getTrafficAllocation()).thenReturn(Arrays.asList(new TrafficAllocation("any-id", 0))); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"%s\" is not in any variation of experiment \"exp_no_audience\".", userId) + )); } @Test public void decideReasons_userBucketedIntoExperimentInGroup() { + String flagKey = "feature_3"; + String experimentId = "10390965532"; // "group_exp_1" + + FeatureFlag flag = getSpyFeatureFlag(flagKey); + when(flag.getExperimentIds()).thenReturn(Arrays.asList(experimentId)); + addSpyFeatureFlag(flag); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"tester\" is in experiment \"group_exp_1\" of group 13142870430.") + )); } + @Test public void decideReasons_userNotBucketedIntoExperimentInGroup() { + String flagKey = "feature_3"; + String experimentId = "10420843432"; // "group_exp_2" + FeatureFlag flag = getSpyFeatureFlag(flagKey); + when(flag.getExperimentIds()).thenReturn(Arrays.asList(experimentId)); + addSpyFeatureFlag(flag); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"tester\" is not in experiment \"group_exp_2\" of group 13142870430.") + )); } + @Test public void decideReasons_userNotBucketedIntoAnyExperimentInGroup() { + String flagKey = "feature_3"; + String experimentId = "10390965532"; // "group_exp_1" + String groupId = "13142870430"; - } - @Test - public void decideReasons_userBucketedIntoInvalidExperiment() { + FeatureFlag flag = getSpyFeatureFlag(flagKey); + when(flag.getExperimentIds()).thenReturn(Arrays.asList(experimentId)); + addSpyFeatureFlag(flag); + + Group group = getSpyGroup(groupId); + when(group.getTrafficAllocation()).thenReturn(Collections.emptyList()); + addSpyGroup(group); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"tester\" is not in any experiment of group 13142870430.") + )); } + @Test public void decideReasons_userNotInExperiment() { String flagKey = "feature_1"; @@ -812,26 +967,65 @@ Map createUserProfileMap(String experimentId, String variationId } void setAudienceForFeatureTest(String flagKey, String audienceId) throws ConfigParseException { - ProjectConfig configReal = new DatafileProjectConfig.Builder().withDatafile(datafile).build(); - ProjectConfig config = spy(configReal); - optimizely = Optimizely.builder().withConfig(config).build(); + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + } + Experiment getSpyExperiment(String flagKey) { + setMockConfig(); String experimentId = config.getFeatureKeyMapping().get(flagKey).getExperimentIds().get(0); - String rolloutId = config.getFeatureKeyMapping().get(flagKey).getRolloutId(); - Map experimentIdMapping = new HashMap<>(config.getExperimentIdMapping()); - Map rolloutIdMapping = new HashMap<>(config.getRolloutIdMapping()); - Experiment experimentReal = experimentIdMapping.get(experimentId); - Rollout rolloutReal = rolloutIdMapping.get(rolloutId); - - Experiment experiment = spy(experimentReal); - Rollout rollout = spy(rolloutReal); - when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + return spy(experimentIdMapping.get(experimentId)); + } - experimentIdMapping.put(experimentId, experiment); - rolloutIdMapping.put(rolloutId, rollout); + FeatureFlag getSpyFeatureFlag(String flagKey) { + setMockConfig(); + return spy(config.getFeatureKeyMapping().get(flagKey)); + } + + Group getSpyGroup(String groupId) { + setMockConfig(); + return spy(groupIdMapping.get(groupId)); + } + void addSpyExperiment(Experiment experiment) { + experimentIdMapping.put(experiment.getId(), experiment); when(config.getExperimentIdMapping()).thenReturn(experimentIdMapping); - when(config.getRolloutIdMapping()).thenReturn(rolloutIdMapping); + } + + void addSpyFeatureFlag(FeatureFlag flag) { + featureKeyMapping.put(flag.getKey(), flag); + when(config.getFeatureKeyMapping()).thenReturn(featureKeyMapping); + } + + void addSpyGroup(Group group) { + groupIdMapping.put(group.getId(), group); + when(config.getGroupIdMapping()).thenReturn(groupIdMapping); + } + + void setMockConfig() { + if (config != null) return; + + ProjectConfig configReal = null; + try { + configReal = new DatafileProjectConfig.Builder().withDatafile(datafile).build(); + config = spy(configReal); + optimizely = Optimizely.builder().withConfig(config).build(); + experimentIdMapping = new HashMap<>(config.getExperimentIdMapping()); + groupIdMapping = new HashMap<>(config.getGroupIdMapping()); + featureKeyMapping = new HashMap<>(config.getFeatureKeyMapping()); + } catch (ConfigParseException e) { + fail("ProjectConfig build failed"); + } + } + + OptimizelyDecision callDecideWithIncludeReasons(String flagKey, Map attributes) { + OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); + return user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + } + + OptimizelyDecision callDecideWithIncludeReasons(String flagKey) { + return callDecideWithIncludeReasons(flagKey, Collections.emptyMap()); } } From 7ce768d61befa0ce35d370126cc498e8b6b5856a Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 16 Oct 2020 09:46:42 -0700 Subject: [PATCH 20/44] pass options to DecisonReasons --- .../java/com/optimizely/ab/OptimizelyUserContext.java | 3 +-- .../ab/optimizelydecision/DecisionReasons.java | 10 ++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java index ac2028da4..e760a0c25 100644 --- a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java @@ -102,8 +102,7 @@ public OptimizelyDecision decide(@Nonnull String key, Boolean sentEvent = false; Boolean flagEnabled = false; List allOptions = getAllOptions(options); - Boolean includeReasons = allOptions.contains(OptimizelyDecideOption.INCLUDE_REASONS); - DecisionReasons decisionReasons = new DecisionReasons(includeReasons); + DecisionReasons decisionReasons = new DecisionReasons(allOptions); Map copiedAttributes = new HashMap<>(attributes); FeatureDecision flagDecision = optimizely.decisionService.getVariationForFeature( diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java index ca8bf7c12..0a25b29eb 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java @@ -16,21 +16,23 @@ */ package com.optimizely.ab.optimizelydecision; +import javax.annotation.Nonnull; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class DecisionReasons { - private boolean includeReasons; private final List errors = new ArrayList<>(); private final List logs = new ArrayList<>(); + private boolean includeReasons; - public DecisionReasons(boolean includeReasons) { - this.includeReasons = includeReasons; + public DecisionReasons(@Nonnull List options) { + this.includeReasons = options.contains(OptimizelyDecideOption.INCLUDE_REASONS); } public DecisionReasons() { - this(false); + this(Collections.emptyList()); } public void addError(String message) { From 5b6e79869e5a32110cbf1b28f3315a44581072ab Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 16 Oct 2020 10:31:31 -0700 Subject: [PATCH 21/44] move decide core to optimizely --- .../java/com/optimizely/ab/Optimizely.java | 174 +++++++++++++++++- .../optimizely/ab/OptimizelyUserContext.java | 171 +---------------- 2 files changed, 183 insertions(+), 162 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 26fe4a6b9..aa3214dd6 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -34,8 +34,11 @@ import com.optimizely.ab.optimizelyconfig.OptimizelyConfig; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService; -import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import com.optimizely.ab.optimizelydecision.DecisionMessage; +import com.optimizely.ab.optimizelydecision.DecisionReasons; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; +import com.optimizely.ab.optimizelydecision.OptimizelyDecision; +import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -1089,6 +1092,8 @@ public OptimizelyConfig getOptimizelyConfig() { return new OptimizelyConfigService(projectConfig).getConfig(); } + //============ decide ============// + /** * Create a context of the user for which decision APIs will be called. * @@ -1107,6 +1112,173 @@ public OptimizelyUserContext createUserContext(@Nonnull String userId) { return new OptimizelyUserContext(this, userId); } + OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, + @Nonnull String key, + @Nonnull List options) { + + ProjectConfig projectConfig = getProjectConfig(); + if (projectConfig == null) { + return OptimizelyDecision.createErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason()); + } + + FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key); + if (flag == null) { + return OptimizelyDecision.createErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key)); + } + + String userId = user.getUserId(); + Map attributes = user.getAttributes(); + Boolean sentEvent = false; + Boolean flagEnabled = false; + List allOptions = getAllOptions(options); + DecisionReasons decisionReasons = new DecisionReasons(allOptions); + + Map copiedAttributes = new HashMap<>(attributes); + FeatureDecision flagDecision = decisionService.getVariationForFeature( + flag, + userId, + copiedAttributes, + projectConfig, + allOptions, + decisionReasons); + + if (flagDecision.variation != null) { + if (flagDecision.decisionSource.equals(FeatureDecision.DecisionSource.FEATURE_TEST)) { + if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) { + sendImpression( + projectConfig, + flagDecision.experiment, + userId, + copiedAttributes, + flagDecision.variation); + sentEvent = true; + } + } else { + String message = String.format("The user \"%s\" is not included in an experiment for flag \"%s\".", userId, key); + logger.info(message); + decisionReasons.addInfo(message); + } + if (flagDecision.variation.getFeatureEnabled()) { + flagEnabled = true; + } + } + + Map variableMap = new HashMap<>(); + if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) { + variableMap = getDecisionVariableMap( + flag, + flagDecision.variation, + flagEnabled, + decisionReasons); + } + + OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap); + + List reasonsToReport = decisionReasons.toReport(); + String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null; + // TODO: add ruleKey values when available later. use a copy of experimentKey until then. + String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null; + + DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder() + .withUserId(userId) + .withAttributes(copiedAttributes) + .withFlagKey(key) + .withEnabled(flagEnabled) + .withVariables(variableMap) + .withVariationKey(variationKey) + .withRuleKey(ruleKey) + .withReasons(reasonsToReport) + .withDecisionEventDispatched(sentEvent) + .build(); + notificationCenter.send(decisionNotification); + + logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled); + + return new OptimizelyDecision( + variationKey, + flagEnabled, + optimizelyJSON, + ruleKey, + key, + user, + reasonsToReport); + } + + Map decideForKeys(@Nonnull OptimizelyUserContext user, + @Nonnull List keys, + @Nonnull List options) { + Map decisionMap = new HashMap<>(); + + ProjectConfig projectConfig = getProjectConfig(); + if (projectConfig == null) { + logger.error("Optimizely instance is not valid, failing isFeatureEnabled call."); + return decisionMap; + } + + if (keys.isEmpty()) return decisionMap; + + List allOptions = getAllOptions(options); + + for (String key : keys) { + OptimizelyDecision decision = decide(user, key, options); + if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.getEnabled()) { + decisionMap.put(key, decision); + } + } + + return decisionMap; + } + + Map decideAll(@Nonnull OptimizelyUserContext user, + @Nonnull List options) { + Map decisionMap = new HashMap<>(); + + ProjectConfig projectConfig = getProjectConfig(); + if (projectConfig == null) { + logger.error("Optimizely instance is not valid, failing isFeatureEnabled call."); + return decisionMap; + } + + List allFlags = projectConfig.getFeatureFlags(); + List allFlagKeys = new ArrayList<>(); + for (int i = 0; i < allFlags.size(); i++) allFlagKeys.add(allFlags.get(i).getKey()); + + return decideForKeys(user, allFlagKeys, options); + } + + private List getAllOptions(List options) { + List copiedOptions = new ArrayList(defaultDecideOptions); + copiedOptions.addAll(options); + return copiedOptions; + } + + private Map getDecisionVariableMap(@Nonnull FeatureFlag flag, + @Nonnull Variation variation, + @Nonnull Boolean featureEnabled, + @Nonnull DecisionReasons decisionReasons) { + Map valuesMap = new HashMap(); + for (FeatureVariable variable : flag.getVariables()) { + String value = variable.getDefaultValue(); + if (featureEnabled) { + FeatureVariableUsageInstance instance = variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId()); + if (instance != null) { + value = instance.getValue(); + } + } + + Object convertedValue = convertStringToType(value, variable.getType()); + if (convertedValue == null) { + decisionReasons.addError(DecisionMessage.VARIABLE_VALUE_INVALID.reason(variable.getKey())); + } else if (convertedValue instanceof OptimizelyJSON) { + convertedValue = ((OptimizelyJSON) convertedValue).toMap(); + } + + valuesMap.put(variable.getKey(), convertedValue); + } + + return valuesMap; + } + /** * Helper method which makes separate copy of attributesMap variable and returns it * diff --git a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java index e760a0c25..5f65bca36 100644 --- a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java @@ -16,19 +16,16 @@ */ package com.optimizely.ab; -import com.optimizely.ab.bucketing.FeatureDecision; -import com.optimizely.ab.config.*; -import com.optimizely.ab.notification.DecisionNotification; -import com.optimizely.ab.optimizelydecision.DecisionMessage; -import com.optimizely.ab.optimizelydecision.DecisionReasons; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecision; -import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class OptimizelyUserContext { @@ -88,91 +85,7 @@ public void setAttribute(@Nonnull String key, @Nonnull Object value) { */ public OptimizelyDecision decide(@Nonnull String key, @Nonnull List options) { - - ProjectConfig projectConfig = optimizely.getProjectConfig(); - if (projectConfig == null) { - return OptimizelyDecision.createErrorDecision(key, this, DecisionMessage.SDK_NOT_READY.reason()); - } - - FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key); - if (flag == null) { - return OptimizelyDecision.createErrorDecision(key, this, DecisionMessage.FLAG_KEY_INVALID.reason(key)); - } - - Boolean sentEvent = false; - Boolean flagEnabled = false; - List allOptions = getAllOptions(options); - DecisionReasons decisionReasons = new DecisionReasons(allOptions); - - Map copiedAttributes = new HashMap<>(attributes); - FeatureDecision flagDecision = optimizely.decisionService.getVariationForFeature( - flag, - userId, - copiedAttributes, - projectConfig, - allOptions, - decisionReasons); - - if (flagDecision.variation != null) { - if (flagDecision.decisionSource.equals(FeatureDecision.DecisionSource.FEATURE_TEST)) { - if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) { - optimizely.sendImpression( - projectConfig, - flagDecision.experiment, - userId, - copiedAttributes, - flagDecision.variation); - sentEvent = true; - } - } else { - String message = String.format("The user \"%s\" is not included in an experiment for flag \"%s\".", userId, key); - logger.info(message); - decisionReasons.addInfo(message); - } - if (flagDecision.variation.getFeatureEnabled()) { - flagEnabled = true; - } - } - - Map variableMap = new HashMap<>(); - if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) { - variableMap = getDecisionVariableMap( - flag, - flagDecision.variation, - flagEnabled, - decisionReasons); - } - - OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap); - - List reasonsToReport = decisionReasons.toReport(); - String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null; - // TODO: add ruleKey values when available later. use a copy of experimentKey until then. - String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null; - - DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder() - .withUserId(userId) - .withAttributes(copiedAttributes) - .withFlagKey(key) - .withEnabled(flagEnabled) - .withVariables(variableMap) - .withVariationKey(variationKey) - .withRuleKey(ruleKey) - .withReasons(reasonsToReport) - .withDecisionEventDispatched(sentEvent) - .build(); - optimizely.notificationCenter.send(decisionNotification); - - logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled); - - return new OptimizelyDecision( - variationKey, - flagEnabled, - optimizelyJSON, - ruleKey, - key, - this, - reasonsToReport); + return optimizely.decide(this, key, options); } /** @@ -182,7 +95,7 @@ public OptimizelyDecision decide(@Nonnull String key, * @return A decision result. */ public OptimizelyDecision decide(String key) { - return decide(key, Collections.emptyList()); + return optimizely.decide(this, key, Collections.emptyList()); } /** @@ -197,26 +110,7 @@ public OptimizelyDecision decide(String key) { */ public Map decideForKeys(@Nonnull List keys, @Nonnull List options) { - Map decisionMap = new HashMap<>(); - - ProjectConfig projectConfig = optimizely.getProjectConfig(); - if (projectConfig == null) { - logger.error("Optimizely instance is not valid, failing isFeatureEnabled call."); - return decisionMap; - } - - if (keys.isEmpty()) return decisionMap; - - List allOptions = getAllOptions(options); - - for (String key : keys) { - OptimizelyDecision decision = decide(key, options); - if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.getEnabled()) { - decisionMap.put(key, decision); - } - } - - return decisionMap; + return optimizely.decideForKeys(this, keys, options); } /** @@ -226,7 +120,7 @@ public Map decideForKeys(@Nonnull List keys, * @return All decision results mapped by flag keys. */ public Map decideForKeys(@Nonnull List keys) { - return decideForKeys(keys, Collections.emptyList()); + return optimizely.decideForKeys(this, keys, Collections.emptyList()); } /** @@ -236,19 +130,7 @@ public Map decideForKeys(@Nonnull List keys) * @return All decision results mapped by flag keys. */ public Map decideAll(@Nonnull List options) { - Map decisionMap = new HashMap<>(); - - ProjectConfig projectConfig = optimizely.getProjectConfig(); - if (projectConfig == null) { - logger.error("Optimizely instance is not valid, failing isFeatureEnabled call."); - return decisionMap; - } - - List allFlags = projectConfig.getFeatureFlags(); - List allFlagKeys = new ArrayList<>(); - for (int i = 0; i < allFlags.size(); i++) allFlagKeys.add(allFlags.get(i).getKey()); - - return decideForKeys(allFlagKeys, options); + return optimizely.decideAll(this, options); } /** @@ -257,7 +139,7 @@ public Map decideAll(@Nonnull List decideAll() { - return decideAll(Collections.emptyList()); + return optimizely.decideAll(this, Collections.emptyList()); } /** @@ -284,39 +166,6 @@ public void trackEvent(@Nonnull String eventName) throws UnknownEventTypeExcepti // Utils - private List getAllOptions(List options) { - List copiedOptions = new ArrayList(optimizely.defaultDecideOptions); - copiedOptions.addAll(options); - return copiedOptions; - } - - private Map getDecisionVariableMap(@Nonnull FeatureFlag flag, - @Nonnull Variation variation, - @Nonnull Boolean featureEnabled, - @Nonnull DecisionReasons decisionReasons) { - Map valuesMap = new HashMap(); - for (FeatureVariable variable : flag.getVariables()) { - String value = variable.getDefaultValue(); - if (featureEnabled) { - FeatureVariableUsageInstance instance = variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId()); - if (instance != null) { - value = instance.getValue(); - } - } - - Object convertedValue = optimizely.convertStringToType(value, variable.getType()); - if (convertedValue == null) { - decisionReasons.addError(DecisionMessage.VARIABLE_VALUE_INVALID.reason(variable.getKey())); - } else if (convertedValue instanceof OptimizelyJSON) { - convertedValue = ((OptimizelyJSON) convertedValue).toMap(); - } - - valuesMap.put(variable.getKey(), convertedValue); - } - - return valuesMap; - } - @Override public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) return false; From 29d3de4cce11fe2ec61c61440814263401b6f796 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 16 Oct 2020 14:45:32 -0700 Subject: [PATCH 22/44] change variables in decision nonnull --- .../ab/optimizelydecision/OptimizelyDecision.java | 9 +++++---- .../com/optimizely/ab/optimizelyjson/OptimizelyJSON.java | 4 ++++ .../com/optimizely/ab/OptimizelyUserContextTest.java | 3 ++- .../ab/optimizelydecision/OptimizelyDecisionTest.java | 3 ++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java index 77c05642f..713e7331f 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java @@ -22,6 +22,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Arrays; +import java.util.Collections; import java.util.List; public class OptimizelyDecision { @@ -30,7 +31,7 @@ public class OptimizelyDecision { private final boolean enabled; - @Nullable + @Nonnull private final OptimizelyJSON variables; @Nullable @@ -48,7 +49,7 @@ public class OptimizelyDecision { public OptimizelyDecision(@Nullable String variationKey, boolean enabled, - @Nullable OptimizelyJSON variables, + @Nonnull OptimizelyJSON variables, @Nullable String ruleKey, @Nonnull String flagKey, @Nonnull OptimizelyUserContext userContext, @@ -102,7 +103,7 @@ public static OptimizelyDecision createErrorDecision(@Nonnull String key, return new OptimizelyDecision( null, false, - null, + new OptimizelyJSON(Collections.emptyMap()), null, key, user, @@ -135,7 +136,7 @@ private static boolean equals(Object a, Object b) { public int hashCode() { int hash = variationKey != null ? variationKey.hashCode() : 0; hash = 31 * hash + (enabled ? 1 : 0); - hash = 31 * hash + (variables != null ? variables.hashCode() : 0); + hash = 31 * hash + variables.hashCode(); hash = 31 * hash + (ruleKey != null ? ruleKey.hashCode() : 0); hash = 31 * hash + flagKey.hashCode(); hash = 31 * hash + userContext.hashCode(); diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java index 2815dea6d..97bff838c 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelyjson/OptimizelyJSON.java @@ -157,6 +157,10 @@ private T getValueInternal(@Nullable Object object, Class clazz) { return null; } + public boolean isEmpty() { + return map == null || map.isEmpty(); + } + @Override public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) return false; diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index ce8131049..5f0c2d0b0 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -515,7 +515,7 @@ public void decide_sdkNotReady() { assertNull(decision.getVariationKey()); assertFalse(decision.getEnabled()); - assertNull(decision.getVariables()); + assertTrue(decision.getVariables().isEmpty()); assertEquals(decision.getFlagKey(), flagKey); assertEquals(decision.getUserContext(), user); @@ -532,6 +532,7 @@ public void decide_invalidFeatureKey() { assertNull(decision.getVariationKey()); assertFalse(decision.getEnabled()); + assertTrue(decision.getVariables().isEmpty()); assertEquals(decision.getReasons().size(), 1); assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); } diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelydecision/OptimizelyDecisionTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelydecision/OptimizelyDecisionTest.java index 92fdbde4a..7492297df 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelydecision/OptimizelyDecisionTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelydecision/OptimizelyDecisionTest.java @@ -25,6 +25,7 @@ import java.util.List; import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertTrue; public class OptimizelyDecisionTest { @@ -67,7 +68,7 @@ public void testCreateErrorDecision() { assertEquals(decision.getVariationKey(), null); assertEquals(decision.getEnabled(), false); - assertEquals(decision.getVariables(), null); + assertTrue(decision.getVariables().isEmpty()); assertEquals(decision.getRuleKey(), null); assertEquals(decision.getFlagKey(), flagKey); assertEquals(decision.getUserContext(), userContext); From a5954e47d8eb23f70b72f7a6d1bc62cae6b4e606 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 19 Oct 2020 13:32:14 -0700 Subject: [PATCH 23/44] change withDefaultDecideOptions per reviews --- core-api/src/main/java/com/optimizely/ab/Optimizely.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index aa3214dd6..9cbfc82b3 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1455,8 +1455,8 @@ public Builder withDatafile(String datafile) { return this; } - public Builder withDefaultDecideOptions(List options) { - this.defaultDecideOptions = Collections.unmodifiableList(options); + public Builder withDefaultDecideOptions(List defaultDecideOtions) { + this.defaultDecideOptions = defaultDecideOtions; return this; } @@ -1528,7 +1528,9 @@ public Optimizely build() { eventProcessor = new ForwardingEventProcessor(eventHandler, notificationCenter); } - if (defaultDecideOptions == null) { + if (defaultDecideOptions != null) { + defaultDecideOptions = Collections.unmodifiableList(defaultDecideOptions); + } else { defaultDecideOptions = Collections.emptyList(); } From 6f40cb017d8f7d1d45d74bd702eebac618b40959 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 22 Oct 2020 11:03:18 -0700 Subject: [PATCH 24/44] clean up addInfo --- .../java/com/optimizely/ab/Optimizely.java | 3 +- .../com/optimizely/ab/bucketing/Bucketer.java | 10 +++--- .../ab/bucketing/DecisionService.java | 36 +++++++++---------- .../config/audience/AudienceIdCondition.java | 2 +- .../ab/config/audience/UserAttribute.java | 12 +++---- .../ab/internal/ExperimentUtils.java | 8 ++--- .../optimizelydecision/DecisionReasons.java | 15 ++++---- 7 files changed, 42 insertions(+), 44 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 9cbfc82b3..cbf693690 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1154,9 +1154,8 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, sentEvent = true; } } else { - String message = String.format("The user \"%s\" is not included in an experiment for flag \"%s\".", userId, key); + String message = decisionReasons.addInfo("The user \"%s\" is not included in an experiment for flag \"%s\".", userId, key); logger.info(message); - decisionReasons.addInfo(message); } if (flagDecision.variation.getFeatureEnabled()) { flagEnabled = true; diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index 576227408..702c5cd51 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -110,7 +110,7 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, if (bucketedVariationId != null) { Variation bucketedVariation = experiment.getVariationIdToVariationMap().get(bucketedVariationId); String variationKey = bucketedVariation.getKey(); - String message = reasons.addInfoF("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", bucketingId, variationKey, + String message = reasons.addInfo("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", bucketingId, variationKey, experimentKey); logger.info(message); @@ -118,7 +118,7 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, } // user was not bucketed to a variation - String message = reasons.addInfoF("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", bucketingId, experimentKey); + String message = reasons.addInfo("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", bucketingId, experimentKey); logger.info(message); return null; } @@ -148,7 +148,7 @@ public Variation bucket(@Nonnull Experiment experiment, if (experimentGroup.getPolicy().equals(Group.RANDOM_POLICY)) { Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig, options, reasons); if (bucketedExperiment == null) { - String message = reasons.addInfoF("User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId()); + String message = reasons.addInfo("User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId()); logger.info(message); return null; } else { @@ -157,13 +157,13 @@ public Variation bucket(@Nonnull Experiment experiment, // if the experiment a user is bucketed in within a group isn't the same as the experiment provided, // don't perform further bucketing within the experiment if (!bucketedExperiment.getId().equals(experiment.getId())) { - String message = reasons.addInfoF("User with bucketingId \"%s\" is not in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), + String message = reasons.addInfo("User with bucketingId \"%s\" is not in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), experimentGroup.getId()); logger.info(message); return null; } - String message = reasons.addInfoF("User with bucketingId \"%s\" is in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), + String message = reasons.addInfo("User with bucketingId \"%s\" is in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), experimentGroup.getId()); logger.info(message); } diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index f80075c7d..9fe89b25a 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -118,16 +118,16 @@ public Variation getVariation(@Nonnull Experiment experiment, try { Map userProfileMap = userProfileService.lookup(userId); if (userProfileMap == null) { - String message = reasons.addInfoF("We were unable to get a user profile map from the UserProfileService."); + String message = reasons.addInfo("We were unable to get a user profile map from the UserProfileService."); logger.info(message); } else if (UserProfileUtils.isValidUserProfileMap(userProfileMap)) { userProfile = UserProfileUtils.convertMapToUserProfile(userProfileMap); } else { - String message = reasons.addInfoF("The UserProfileService returned an invalid map."); + String message = reasons.addInfo("The UserProfileService returned an invalid map."); logger.warn(message); } } catch (Exception exception) { - String message = reasons.addInfoF(exception.getMessage()); + String message = reasons.addInfo(exception.getMessage()); logger.error(message); errorHandler.handleError(new OptimizelyRuntimeException(exception)); } @@ -159,7 +159,7 @@ public Variation getVariation(@Nonnull Experiment experiment, return variation; } - String message = reasons.addInfoF("User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experiment.getKey()); + String message = reasons.addInfo("User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experiment.getKey()); logger.info(message); return null; } @@ -199,17 +199,17 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, } } } else { - String message = reasons.addInfoF("The feature flag \"%s\" is not used in any experiments.", featureFlag.getKey()); + String message = reasons.addInfo("The feature flag \"%s\" is not used in any experiments.", featureFlag.getKey()); logger.info(message); } FeatureDecision featureDecision = getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, options, reasons); if (featureDecision.variation == null) { - String message = reasons.addInfoF("The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".", + String message = reasons.addInfo("The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".", userId, featureFlag.getKey()); logger.info(message); } else { - String message = reasons.addInfoF("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", + String message = reasons.addInfo("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, featureFlag.getKey()); logger.info(message); } @@ -247,13 +247,13 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag @Nonnull DecisionReasons reasons) { // use rollout to get variation for feature if (featureFlag.getRolloutId().isEmpty()) { - String message = reasons.addInfoF("The feature flag \"%s\" is not used in a rollout.", featureFlag.getKey()); + String message = reasons.addInfo("The feature flag \"%s\" is not used in a rollout.", featureFlag.getKey()); logger.info(message); return new FeatureDecision(null, null, null); } Rollout rollout = projectConfig.getRolloutIdMapping().get(featureFlag.getRolloutId()); if (rollout == null) { - String message = reasons.addInfoF("The rollout with id \"%s\" was not found in the datafile for feature flag \"%s\".", + String message = reasons.addInfo("The rollout with id \"%s\" was not found in the datafile for feature flag \"%s\".", featureFlag.getRolloutId(), featureFlag.getKey()); logger.error(message); return new FeatureDecision(null, null, null); @@ -273,7 +273,7 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag return new FeatureDecision(rolloutRule, variation, FeatureDecision.DecisionSource.ROLLOUT); } else { - String message = reasons.addInfoF("User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, i + 1); + String message = reasons.addInfo("User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, i + 1); logger.debug(message); } } @@ -283,7 +283,7 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, finalRule, filteredAttributes, RULE, "Everyone Else", options, reasons)) { variation = bucketer.bucket(finalRule, bucketingId, projectConfig, options, reasons); if (variation != null) { - String message = reasons.addInfoF("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId); + String message = reasons.addInfo("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId); logger.debug(message); return new FeatureDecision(finalRule, variation, FeatureDecision.DecisionSource.ROLLOUT); @@ -321,10 +321,10 @@ Variation getWhitelistedVariation(@Nonnull Experiment experiment, String forcedVariationKey = userIdToVariationKeyMap.get(userId); Variation forcedVariation = experiment.getVariationKeyToVariationMap().get(forcedVariationKey); if (forcedVariation != null) { - String message = reasons.addInfoF("User \"%s\" is forced in variation \"%s\".", userId, forcedVariationKey); + String message = reasons.addInfo("User \"%s\" is forced in variation \"%s\".", userId, forcedVariationKey); logger.info(message); } else { - String message = reasons.addInfoF("Variation \"%s\" is not in the datafile. Not activating user \"%s\".", + String message = reasons.addInfo("Variation \"%s\" is not in the datafile. Not activating user \"%s\".", forcedVariationKey, userId); logger.error(message); } @@ -373,19 +373,19 @@ Variation getStoredVariation(@Nonnull Experiment experiment, .getVariationIdToVariationMap() .get(variationId); if (savedVariation != null) { - String message = reasons.addInfoF("Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", + String message = reasons.addInfo("Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", savedVariation.getKey(), experimentKey, userProfile.userId); logger.info(message); // A variation is stored for this combined bucket id return savedVariation; } else { - String message = reasons.addInfoF("User \"%s\" was previously bucketed into variation with ID \"%s\" for experiment \"%s\", but no matching variation was found for that user. We will re-bucket the user.", + String message = reasons.addInfo("User \"%s\" was previously bucketed into variation with ID \"%s\" for experiment \"%s\", but no matching variation was found for that user. We will re-bucket the user.", userProfile.userId, variationId, experimentKey); logger.info(message); return null; } } else { - String message = reasons.addInfoF("No previously activated variation of experiment \"%s\" for user \"%s\" found in user profile.", + String message = reasons.addInfo("No previously activated variation of experiment \"%s\" for user \"%s\" found in user profile.", experimentKey, userProfile.userId); logger.info(message); return null; @@ -467,7 +467,7 @@ String getBucketingId(@Nonnull String userId, bucketingId = (String) filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()); logger.debug("BucketingId is valid: \"{}\"", bucketingId); } else { - String message = reasons.addInfoF("BucketingID attribute is not a string. Defaulted to userId"); + String message = reasons.addInfo("BucketingID attribute is not a string. Defaulted to userId"); logger.warn(message); } } @@ -578,7 +578,7 @@ public Variation getForcedVariation(@Nonnull Experiment experiment, if (variationId != null) { Variation variation = experiment.getVariationIdToVariationMap().get(variationId); if (variation != null) { - String message = reasons.addInfoF("Variation \"%s\" is mapped to experiment \"%s\" and user \"%s\" in the forced variation map", + String message = reasons.addInfo("Variation \"%s\" is mapped to experiment \"%s\" and user \"%s\" in the forced variation map", variation.getKey(), experiment.getKey(), userId); logger.debug(message); return variation; diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java index ee58fe4d9..2ff489bf6 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java @@ -75,7 +75,7 @@ public Boolean evaluate(ProjectConfig config, audience = config.getAudienceIdMapping().get(audienceId); } if (audience == null) { - String message = reasons.addInfoF("Audience %s could not be found.", audienceId); + String message = reasons.addInfo("Audience %s could not be found.", audienceId); logger.error(message); return null; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java index 5044e3dac..b67753fa8 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java @@ -85,7 +85,7 @@ public Boolean evaluate(ProjectConfig config, Object userAttributeValue = attributes.get(name); if (!"custom_attribute".equals(type)) { - String message = reasons.addInfoF("Audience condition \"%s\" uses an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.", this); + String message = reasons.addInfo("Audience condition \"%s\" uses an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.", this); logger.warn(message); return null; // unknown type } @@ -101,26 +101,26 @@ public Boolean evaluate(ProjectConfig config, } catch(UnknownValueTypeException e) { if (!attributes.containsKey(name)) { //Missing attribute value - String message = reasons.addInfoF("Audience condition \"%s\" evaluated to UNKNOWN because no value was passed for user attribute \"%s\"", this, name); + String message = reasons.addInfo("Audience condition \"%s\" evaluated to UNKNOWN because no value was passed for user attribute \"%s\"", this, name); logger.debug(message); } else { //if attribute value is not valid if (userAttributeValue != null) { - String message = reasons.addInfoF("Audience condition \"%s\" evaluated to UNKNOWN because a value of type \"%s\" was passed for user attribute \"%s\"", + String message = reasons.addInfo("Audience condition \"%s\" evaluated to UNKNOWN because a value of type \"%s\" was passed for user attribute \"%s\"", this, userAttributeValue.getClass().getCanonicalName(), name); logger.warn(message); } else { - String message = reasons.addInfoF("Audience condition \"%s\" evaluated to UNKNOWN because a null value was passed for user attribute \"%s\"", this, name); + String message = reasons.addInfo("Audience condition \"%s\" evaluated to UNKNOWN because a null value was passed for user attribute \"%s\"", this, name); logger.debug(message); } } } catch (UnknownMatchTypeException | UnexpectedValueTypeException e) { - String message = reasons.addInfoF("Audience condition \"%s\" " + e.getMessage(), this); + String message = reasons.addInfo("Audience condition \"%s\" " + e.getMessage(), this); logger.warn(message); } catch (NullPointerException e) { - String message = reasons.addInfoF("attribute or value null for match %s", match != null ? match : "legacy condition"); + String message = reasons.addInfo("attribute or value null for match %s", match != null ? match : "legacy condition"); logger.error(message, e); } return null; diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java index 479e1c237..d2c686012 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java @@ -53,7 +53,7 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment, @Nonnull DecisionReasons reasons) { if (!experiment.isActive()) { - String message = reasons.addInfoF("Experiment \"%s\" is not running.", experiment.getKey()); + String message = reasons.addInfo("Experiment \"%s\" is not running.", experiment.getKey()); logger.info(message); return false; } @@ -139,7 +139,7 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, Boolean result = implicitOr.evaluate(projectConfig, attributes, options, reasons); - String message = reasons.addInfoF("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); + String message = reasons.addInfo("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); logger.info(message); return result; @@ -159,11 +159,11 @@ public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectC try { Boolean result = conditions.evaluate(projectConfig, attributes, options, reasons); - String message = reasons.addInfoF("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); + String message = reasons.addInfo("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); logger.info(message); return result; } catch (Exception e) { - String message = reasons.addInfoF("Condition invalid: %s", e.getMessage()); + String message = reasons.addInfo("Condition invalid: %s", e.getMessage()); logger.error(message); return null; } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java index 0a25b29eb..fd2e74c7d 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java @@ -35,23 +35,22 @@ public DecisionReasons() { this(Collections.emptyList()); } - public void addError(String message) { + public void addError(String format, Object... args) { + String message = String.format(format, args); errors.add(message); } - public void addInfo(String message) { - logs.add(message); - } - - public String addInfoF(String format, Object... args) { + public String addInfo(String format, Object... args) { String message = String.format(format, args); - if(includeReasons) addInfo(message); + if (includeReasons) { + logs.add(message); + } return message; } public List toReport() { List reasons = new ArrayList<>(errors); - if(includeReasons) { + if (includeReasons) { reasons.addAll(logs); } return reasons; From 0ecc4b64296f4438a306e08790f095822b965956 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 22 Oct 2020 11:30:43 -0700 Subject: [PATCH 25/44] remove unused evaluate method --- .../ab/config/audience/AndCondition.java | 6 - .../config/audience/AudienceIdCondition.java | 6 - .../ab/config/audience/Condition.java | 3 - .../ab/config/audience/EmptyCondition.java | 6 - .../ab/config/audience/NotCondition.java | 9 +- .../ab/config/audience/NullCondition.java | 5 - .../ab/config/audience/OrCondition.java | 6 - .../ab/config/audience/UserAttribute.java | 5 - .../AudienceConditionEvaluationTest.java | 365 +++++++++--------- 9 files changed, 186 insertions(+), 225 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java index a8a5f3e65..0465bf5b7 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java @@ -22,7 +22,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -72,11 +71,6 @@ public Boolean evaluate(ProjectConfig config, return true; // otherwise, return true } - @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { - return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); - } - @Override public String toString() { StringBuilder s = new StringBuilder(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java index 2ff489bf6..121c81f0c 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java @@ -25,7 +25,6 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nullable; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -85,11 +84,6 @@ public Boolean evaluate(ProjectConfig config, return result; } - @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { - return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java index dff29a0dc..c490fb1e0 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java @@ -35,7 +35,4 @@ Boolean evaluate(ProjectConfig config, List options, DecisionReasons reasons); - @Nullable - Boolean evaluate(ProjectConfig config, Map attributes); - } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java index ad9d79b41..53a8ff400 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java @@ -20,7 +20,6 @@ import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nullable; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -34,9 +33,4 @@ public Boolean evaluate(ProjectConfig config, return true; } - @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { - return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); - } - } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java index 96a192d98..44b715a52 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java @@ -20,11 +20,9 @@ import com.optimizely.ab.optimizelydecision.DecisionReasons; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import javax.annotation.Nonnull; - -import java.util.Collections; import java.util.List; import java.util.Map; @@ -54,11 +52,6 @@ public Boolean evaluate(ProjectConfig config, return (conditionEval == null ? null : !conditionEval); } - @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { - return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); - } - @Override public String toString() { StringBuilder s = new StringBuilder(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java index bd49229ae..adfa89554 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java @@ -20,7 +20,6 @@ import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nullable; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -34,8 +33,4 @@ public Boolean evaluate(ProjectConfig config, return null; } - @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { - return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); - } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java index c3f890b98..ac22914df 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java @@ -23,7 +23,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -71,11 +70,6 @@ public Boolean evaluate(ProjectConfig config, return false; } - @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { - return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); - } - @Override public String toString() { StringBuilder s = new StringBuilder(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java index b67753fa8..65df43535 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java @@ -126,11 +126,6 @@ public Boolean evaluate(ProjectConfig config, return null; } - @Nullable - public Boolean evaluate(ProjectConfig config, Map attributes) { - return evaluate(config, attributes, Collections.emptyList(), new DecisionReasons()); - } - @Override public String toString() { final String valueStr; diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java index dbcdda88e..ce955d627 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java @@ -18,6 +18,8 @@ import ch.qos.logback.classic.Level; import com.optimizely.ab.internal.LogbackVerifier; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Before; import org.junit.Rule; @@ -42,6 +44,9 @@ public class AudienceConditionEvaluationTest { Map testUserAttributes; Map testTypedUserAttributes; + List options = Collections.EMPTY_LIST; + DecisionReasons reasons = new DecisionReasons(); + @Before public void initialize() { testUserAttributes = new HashMap<>(); @@ -66,7 +71,7 @@ public void userAttributeEvaluateTrue() throws Exception { assertNull(testInstance.getMatch()); assertEquals(testInstance.getName(), "browser_type"); assertEquals(testInstance.getType(), "custom_attribute"); - assertTrue(testInstance.evaluate(null, testUserAttributes)); + assertTrue(testInstance.evaluate(null, testUserAttributes, options, reasons)); } /** @@ -75,7 +80,7 @@ public void userAttributeEvaluateTrue() throws Exception { @Test public void userAttributeEvaluateFalse() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", null, "firefox"); - assertFalse(testInstance.evaluate(null, testUserAttributes)); + assertFalse(testInstance.evaluate(null, testUserAttributes, options, reasons)); } /** @@ -84,7 +89,7 @@ public void userAttributeEvaluateFalse() throws Exception { @Test public void userAttributeUnknownAttribute() throws Exception { UserAttribute testInstance = new UserAttribute("unknown_dim", "custom_attribute", null, "unknown"); - assertFalse(testInstance.evaluate(null, testUserAttributes)); + assertFalse(testInstance.evaluate(null, testUserAttributes, options, reasons)); } /** @@ -93,7 +98,7 @@ public void userAttributeUnknownAttribute() throws Exception { @Test public void invalidMatchCondition() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "unknown_dimension", null, "chrome"); - assertNull(testInstance.evaluate(null, testUserAttributes)); + assertNull(testInstance.evaluate(null, testUserAttributes, options, reasons)); } /** @@ -102,7 +107,7 @@ public void invalidMatchCondition() throws Exception { @Test public void invalidMatch() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "blah", "chrome"); - assertNull(testInstance.evaluate(null, testUserAttributes)); + assertNull(testInstance.evaluate(null, testUserAttributes, options, reasons)); logbackVerifier.expectMessage(Level.WARN, "Audience condition \"{name='browser_type', type='custom_attribute', match='blah', value='chrome'}\" uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK"); } @@ -113,7 +118,7 @@ public void invalidMatch() throws Exception { @Test public void unexpectedAttributeType() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "gt", 20); - assertNull(testInstance.evaluate(null, testUserAttributes)); + assertNull(testInstance.evaluate(null, testUserAttributes, options, reasons)); logbackVerifier.expectMessage(Level.WARN, "Audience condition \"{name='browser_type', type='custom_attribute', match='gt', value=20}\" evaluated to UNKNOWN because a value of type \"java.lang.String\" was passed for user attribute \"browser_type\""); } @@ -124,7 +129,7 @@ public void unexpectedAttributeType() throws Exception { @Test public void unexpectedAttributeTypeNull() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "gt", 20); - assertNull(testInstance.evaluate(null, Collections.singletonMap("browser_type", null))); + assertNull(testInstance.evaluate(null, Collections.singletonMap("browser_type", null), options, reasons)); logbackVerifier.expectMessage(Level.DEBUG, "Audience condition \"{name='browser_type', type='custom_attribute', match='gt', value=20}\" evaluated to UNKNOWN because a null value was passed for user attribute \"browser_type\""); } @@ -136,7 +141,7 @@ public void unexpectedAttributeTypeNull() throws Exception { @Test public void missingAttribute() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "gt", 20); - assertNull(testInstance.evaluate(null, Collections.EMPTY_MAP)); + assertNull(testInstance.evaluate(null, Collections.EMPTY_MAP, options, reasons)); logbackVerifier.expectMessage(Level.DEBUG, "Audience condition \"{name='browser_type', type='custom_attribute', match='gt', value=20}\" evaluated to UNKNOWN because no value was passed for user attribute \"browser_type\""); } @@ -147,7 +152,7 @@ public void missingAttribute() throws Exception { @Test public void nullAttribute() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "gt", 20); - assertNull(testInstance.evaluate(null, null)); + assertNull(testInstance.evaluate(null, null, options, reasons)); logbackVerifier.expectMessage(Level.DEBUG, "Audience condition \"{name='browser_type', type='custom_attribute', match='gt', value=20}\" evaluated to UNKNOWN because no value was passed for user attribute \"browser_type\""); } @@ -158,7 +163,7 @@ public void nullAttribute() throws Exception { @Test public void unknownConditionType() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "blah", "exists", "firefox"); - assertNull(testInstance.evaluate(null, testUserAttributes)); + assertNull(testInstance.evaluate(null, testUserAttributes, options, reasons)); logbackVerifier.expectMessage(Level.WARN, "Audience condition \"{name='browser_type', type='blah', match='exists', value='firefox'}\" uses an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK."); } @@ -172,9 +177,9 @@ public void existsMatchConditionEmptyStringEvaluatesTrue() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "exists", "firefox"); Map attributes = new HashMap<>(); attributes.put("browser_type", ""); - assertTrue(testInstance.evaluate(null, attributes)); + assertTrue(testInstance.evaluate(null, attributes, options, reasons)); attributes.put("browser_type", null); - assertFalse(testInstance.evaluate(null, attributes)); + assertFalse(testInstance.evaluate(null, attributes, options, reasons)); } /** @@ -184,16 +189,16 @@ public void existsMatchConditionEmptyStringEvaluatesTrue() throws Exception { @Test public void existsMatchConditionEvaluatesTrue() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "exists", "firefox"); - assertTrue(testInstance.evaluate(null, testUserAttributes)); + assertTrue(testInstance.evaluate(null, testUserAttributes, options, reasons)); UserAttribute testInstanceBoolean = new UserAttribute("is_firefox", "custom_attribute", "exists", false); UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "exists", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "exists", 4.55); UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "exists", testUserAttributes); - assertTrue(testInstanceBoolean.evaluate(null, testTypedUserAttributes)); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes)); - assertTrue(testInstanceObject.evaluate(null, testTypedUserAttributes)); + assertTrue(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -204,8 +209,8 @@ public void existsMatchConditionEvaluatesTrue() throws Exception { public void existsMatchConditionEvaluatesFalse() throws Exception { UserAttribute testInstance = new UserAttribute("bad_var", "custom_attribute", "exists", "chrome"); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "exists", "chrome"); - assertFalse(testInstance.evaluate(null, testTypedUserAttributes)); - assertFalse(testInstanceNull.evaluate(null, testTypedUserAttributes)); + assertFalse(testInstance.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -220,11 +225,11 @@ public void exactMatchConditionEvaluatesTrue() { UserAttribute testInstanceFloat = new UserAttribute("num_size", "custom_attribute", "exact", (float) 3); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "exact", 3.55); - assertTrue(testInstanceString.evaluate(null, testUserAttributes)); - assertTrue(testInstanceBoolean.evaluate(null, testTypedUserAttributes)); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 3))); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes)); + assertTrue(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertTrue(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 3), options, reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -257,22 +262,22 @@ public void exactMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger))); + Collections.singletonMap("num_size", bigInteger), options, reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue))); + Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble))); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble))); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)))); + Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)))); + Collections.singletonMap("num_counts", largeDouble)), options, reasons)); } /** @@ -290,10 +295,10 @@ public void invalidExactMatchConditionEvaluatesNull() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "exact", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "exact", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -307,10 +312,10 @@ public void exactMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "exact", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "exact", 5.55); - assertFalse(testInstanceString.evaluate(null, testUserAttributes)); - assertFalse(testInstanceBoolean.evaluate(null, testTypedUserAttributes)); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes)); + assertFalse(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertFalse(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -326,15 +331,15 @@ public void exactMatchConditionEvaluatesNull() { UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "exact", "3.55"); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "exact", "null_val"); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceString.evaluate(null, testUserAttributes)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceDouble.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); Map attr = new HashMap<>(); attr.put("browser_type", "true"); - assertNull(testInstanceString.evaluate(null, attr)); + assertNull(testInstanceString.evaluate(null, attr, options, reasons)); } /** @@ -348,13 +353,13 @@ public void gtMatchConditionEvaluatesTrue() { UserAttribute testInstanceFloat = new UserAttribute("num_size", "custom_attribute", "gt", (float) 2); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "gt", 2.55); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 3))); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 3), options, reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); Map badAttributes = new HashMap<>(); badAttributes.put("num_size", "bobs burgers"); - assertNull(testInstanceInteger.evaluate(null, badAttributes)); + assertNull(testInstanceInteger.evaluate(null, badAttributes, options, reasons)); } /** @@ -388,22 +393,22 @@ public void gtMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger))); + Collections.singletonMap("num_size", bigInteger), options, reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue))); + Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble))); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble))); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)))); + Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)))); + Collections.singletonMap("num_counts", largeDouble)), options, reasons)); } /** @@ -421,10 +426,10 @@ public void gtMatchConditionEvaluatesNullWithInvalidAttr() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "gt", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "gt", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -437,8 +442,8 @@ public void gtMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "gt", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "gt", 5.55); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -452,10 +457,10 @@ public void gtMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "gt", 3.5); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "gt", 3.5); - assertNull(testInstanceString.evaluate(null, testUserAttributes)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); } @@ -470,13 +475,13 @@ public void geMatchConditionEvaluatesTrue() { UserAttribute testInstanceFloat = new UserAttribute("num_size", "custom_attribute", "ge", (float) 2); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "ge", 2.55); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 2))); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 2), options, reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); Map badAttributes = new HashMap<>(); badAttributes.put("num_size", "bobs burgers"); - assertNull(testInstanceInteger.evaluate(null, badAttributes)); + assertNull(testInstanceInteger.evaluate(null, badAttributes, options, reasons)); } /** @@ -510,22 +515,22 @@ public void geMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger))); + Collections.singletonMap("num_size", bigInteger), options, reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue))); + Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble))); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble))); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)))); + Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)))); + Collections.singletonMap("num_counts", largeDouble)), options, reasons)); } /** @@ -543,10 +548,10 @@ public void geMatchConditionEvaluatesNullWithInvalidAttr() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "ge", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "ge", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -559,8 +564,8 @@ public void geMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "ge", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "ge", 5.55); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -574,10 +579,10 @@ public void geMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "ge", 3.5); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "ge", 3.5); - assertNull(testInstanceString.evaluate(null, testUserAttributes)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); } @@ -591,8 +596,8 @@ public void ltMatchConditionEvaluatesTrue() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "lt", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "lt", 5.55); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -605,8 +610,8 @@ public void ltMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "lt", 2); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "lt", 2.55); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -620,10 +625,10 @@ public void ltMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "lt", 3.5); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "lt", 3.5); - assertNull(testInstanceString.evaluate(null, testUserAttributes)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -657,22 +662,22 @@ public void ltMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger))); + Collections.singletonMap("num_size", bigInteger), options, reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue))); + Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble))); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble))); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)))); + Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)))); + Collections.singletonMap("num_counts", largeDouble)), options, reasons)); } /** @@ -690,10 +695,10 @@ public void ltMatchConditionEvaluatesNullWithInvalidAttributes() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "lt", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "lt", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } @@ -707,8 +712,8 @@ public void leMatchConditionEvaluatesTrue() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "le", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "le", 5.55); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertTrue(testInstanceDouble.evaluate(null, Collections.singletonMap("num_counts", 5.55))); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceDouble.evaluate(null, Collections.singletonMap("num_counts", 5.55), options, reasons)); } /** @@ -721,8 +726,8 @@ public void leMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "le", 2); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "le", 2.55); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -736,10 +741,10 @@ public void leMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "le", 3.5); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "le", 3.5); - assertNull(testInstanceString.evaluate(null, testUserAttributes)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -773,22 +778,22 @@ public void leMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger))); + Collections.singletonMap("num_size", bigInteger), options, reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue))); + Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble))); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble))); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)))); + Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)))); + Collections.singletonMap("num_counts", largeDouble)), options, reasons)); } /** @@ -806,10 +811,10 @@ public void leMatchConditionEvaluatesNullWithInvalidAttributes() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "le", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "le", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); } /** @@ -819,7 +824,7 @@ public void leMatchConditionEvaluatesNullWithInvalidAttributes() { @Test public void substringMatchConditionEvaluatesTrue() { UserAttribute testInstanceString = new UserAttribute("browser_type", "custom_attribute", "substring", "chrome"); - assertTrue(testInstanceString.evaluate(null, testUserAttributes)); + assertTrue(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); } /** @@ -829,7 +834,7 @@ public void substringMatchConditionEvaluatesTrue() { @Test public void substringMatchConditionPartialMatchEvaluatesTrue() { UserAttribute testInstanceString = new UserAttribute("browser_type", "custom_attribute", "substring", "chro"); - assertTrue(testInstanceString.evaluate(null, testUserAttributes)); + assertTrue(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); } /** @@ -839,7 +844,7 @@ public void substringMatchConditionPartialMatchEvaluatesTrue() { @Test public void substringMatchConditionEvaluatesFalse() { UserAttribute testInstanceString = new UserAttribute("browser_type", "custom_attribute", "substring", "chr0me"); - assertFalse(testInstanceString.evaluate(null, testUserAttributes)); + assertFalse(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); } /** @@ -854,11 +859,11 @@ public void substringMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "substring", "chrome1"); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "substring", "chrome1"); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceDouble.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); } //======== Semantic version evaluation tests ========// @@ -869,7 +874,7 @@ public void testSemanticVersionEqualsMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", 2.0); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes)); + assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); } @Test @@ -877,7 +882,7 @@ public void semanticVersionInvalidMajorShouldBeNumberOnly() { Map testAttributes = new HashMap(); testAttributes.put("version", "a.1.2"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes)); + assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); } @Test @@ -885,7 +890,7 @@ public void semanticVersionInvalidMinorShouldBeNumberOnly() { Map testAttributes = new HashMap(); testAttributes.put("version", "1.b.2"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes)); + assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); } @Test @@ -893,7 +898,7 @@ public void semanticVersionInvalidPatchShouldBeNumberOnly() { Map testAttributes = new HashMap(); testAttributes.put("version", "1.2.c"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes)); + assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test SemanticVersionEqualsMatch returns null if given invalid UserCondition Variable type @@ -902,7 +907,7 @@ public void testSemanticVersionEqualsMatchInvalidUserConditionVariable() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.0"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", 2.0); - assertNull(testInstanceString.evaluate(null, testAttributes)); + assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test SemanticVersionGTMatch returns null if given invalid value type @@ -911,7 +916,7 @@ public void testSemanticVersionGTMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", false); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes)); + assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test SemanticVersionGEMatch returns null if given invalid value type @@ -920,7 +925,7 @@ public void testSemanticVersionGEMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", 2); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_ge", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes)); + assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test SemanticVersionLTMatch returns null if given invalid value type @@ -929,7 +934,7 @@ public void testSemanticVersionLTMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", 2); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_lt", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes)); + assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test SemanticVersionLEMatch returns null if given invalid value type @@ -938,7 +943,7 @@ public void testSemanticVersionLEMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", 2); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_le", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes)); + assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test if not same when targetVersion is only major.minor.patch and version is major.minor @@ -947,7 +952,7 @@ public void testIsSemanticNotSameConditionValueMajorMinorPatch() { Map testAttributes = new HashMap(); testAttributes.put("version", "1.2"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "1.2.0"); - assertFalse(testInstanceString.evaluate(null, testAttributes)); + assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test if same when target is only major but user condition checks only major.minor,patch @@ -956,7 +961,7 @@ public void testIsSemanticSameSingleDigit() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.0.0"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "3"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test if greater when User value patch is greater even when its beta @@ -965,7 +970,7 @@ public void testIsSemanticGreaterWhenUserConditionComparesMajorMinorAndPatchVers Map testAttributes = new HashMap(); testAttributes.put("version", "3.1.1-beta"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "3.1.0"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test if greater when preRelease is greater alphabetically @@ -974,7 +979,7 @@ public void testIsSemanticGreaterWhenMajorMinorPatchReleaseVersionCharacter() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.1.1-beta.y.1+1.1"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "3.1.1-beta.x.1+1.1"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test if greater when preRelease version number is greater @@ -983,7 +988,7 @@ public void testIsSemanticGreaterWhenMajorMinorPatchPreReleaseVersionNum() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.1.1-beta.x.2+1.1"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "3.1.1-beta.x.1+1.1"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test if equals semantic version even when only same preRelease is passed in user attribute and no build meta @@ -992,7 +997,7 @@ public void testIsSemanticEqualWhenMajorMinorPatchPreReleaseVersionNum() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.1.1-beta.x.1"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "3.1.1-beta.x.1"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test if not same @@ -1001,7 +1006,7 @@ public void testIsSemanticNotSameReturnsFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.2"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.1.1"); - assertFalse(testInstanceString.evaluate(null, testAttributes)); + assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test when target is full semantic version major.minor.patch @@ -1010,7 +1015,7 @@ public void testIsSemanticSameFull() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.0.1"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "3.0.1"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare less when user condition checks only major.minor @@ -1019,7 +1024,7 @@ public void testIsSemanticLess() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.6"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_lt", "2.2"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // When user condition checks major.minor but target is major.minor.patch then its equals @@ -1028,7 +1033,7 @@ public void testIsSemanticLessFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.0"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_lt", "2.1"); - assertFalse(testInstanceString.evaluate(null, testAttributes)); + assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare less when target is full major.minor.patch @@ -1037,7 +1042,7 @@ public void testIsSemanticFullLess() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.6"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_lt", "2.1.9"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare greater when user condition checks only major.minor @@ -1046,7 +1051,7 @@ public void testIsSemanticMore() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.3.6"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.2"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare greater when both are major.minor.patch-beta but target is greater than user condition @@ -1055,7 +1060,7 @@ public void testIsSemanticMoreWhenBeta() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.3.6-beta"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.3.5-beta"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare greater when target is major.minor.patch @@ -1064,7 +1069,7 @@ public void testIsSemanticFullMore() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.7"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.1.6"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare greater when target is major.minor.patch is smaller then it returns false @@ -1073,7 +1078,7 @@ public void testSemanticVersionGTFullMoreReturnsFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.1.10"); - assertFalse(testInstanceString.evaluate(null, testAttributes)); + assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare equal when both are exactly same - major.minor.patch-beta @@ -1082,7 +1087,7 @@ public void testIsSemanticFullEqual() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9-beta"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.1.9-beta"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare equal when both major.minor.patch is same, but due to beta user condition is smaller @@ -1091,7 +1096,7 @@ public void testIsSemanticLessWhenBeta() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.1.9-beta"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare greater when target is major.minor.patch-beta and user condition only compares major.minor.patch @@ -1100,7 +1105,7 @@ public void testIsSemanticGreaterBeta() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.1.9-beta"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare equal when target is major.minor.patch @@ -1109,7 +1114,7 @@ public void testIsSemanticLessEqualsWhenEqualsReturnsTrue() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_le", "2.1.9"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare less when target is major.minor.patch @@ -1118,7 +1123,7 @@ public void testIsSemanticLessEqualsWhenLessReturnsTrue() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.132.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_le", "2.233.91"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare less when target is major.minor.patch @@ -1127,7 +1132,7 @@ public void testIsSemanticLessEqualsWhenGreaterReturnsFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.233.91"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_le", "2.132.009"); - assertFalse(testInstanceString.evaluate(null, testAttributes)); + assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare equal when target is major.minor.patch @@ -1136,7 +1141,7 @@ public void testIsSemanticGreaterEqualsWhenEqualsReturnsTrue() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_ge", "2.1.9"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare less when target is major.minor.patch @@ -1145,7 +1150,7 @@ public void testIsSemanticGreaterEqualsWhenLessReturnsTrue() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.233.91"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_ge", "2.132.9"); - assertTrue(testInstanceString.evaluate(null, testAttributes)); + assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); } // Test compare less when target is major.minor.patch @@ -1154,7 +1159,7 @@ public void testIsSemanticGreaterEqualsWhenLessReturnsFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.132.009"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_ge", "2.233.91"); - assertFalse(testInstanceString.evaluate(null, testAttributes)); + assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); } /** @@ -1163,7 +1168,7 @@ public void testIsSemanticGreaterEqualsWhenLessReturnsFalse() { @Test public void notConditionEvaluateNull() { NotCondition notCondition = new NotCondition(new NullCondition()); - assertNull(notCondition.evaluate(null, testUserAttributes)); + assertNull(notCondition.evaluate(null, testUserAttributes, options, reasons)); } /** @@ -1175,7 +1180,7 @@ public void notConditionEvaluateTrue() { when(userAttribute.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); NotCondition notCondition = new NotCondition(userAttribute); - assertTrue(notCondition.evaluate(null, testUserAttributes)); + assertTrue(notCondition.evaluate(null, testUserAttributes, options, reasons)); verify(userAttribute, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } @@ -1188,7 +1193,7 @@ public void notConditionEvaluateFalse() { when(userAttribute.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); NotCondition notCondition = new NotCondition(userAttribute); - assertFalse(notCondition.evaluate(null, testUserAttributes)); + assertFalse(notCondition.evaluate(null, testUserAttributes, options, reasons)); verify(userAttribute, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } @@ -1208,7 +1213,7 @@ public void orConditionEvaluateTrue() { conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertTrue(orCondition.evaluate(null, testUserAttributes)); + assertTrue(orCondition.evaluate(null, testUserAttributes, options, reasons)); verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation verify(userAttribute2, times(0)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); @@ -1230,7 +1235,7 @@ public void orConditionEvaluateTrueWithNullAndTrue() { conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertTrue(orCondition.evaluate(null, testUserAttributes)); + assertTrue(orCondition.evaluate(null, testUserAttributes, options, reasons)); verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); @@ -1252,7 +1257,7 @@ public void orConditionEvaluateNullWithNullAndFalse() { conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertNull(orCondition.evaluate(null, testUserAttributes)); + assertNull(orCondition.evaluate(null, testUserAttributes, options, reasons)); verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); @@ -1274,7 +1279,7 @@ public void orConditionEvaluateFalseWithFalseAndFalse() { conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertFalse(orCondition.evaluate(null, testUserAttributes)); + assertFalse(orCondition.evaluate(null, testUserAttributes, options, reasons)); verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); @@ -1296,7 +1301,7 @@ public void orConditionEvaluateFalse() { conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertFalse(orCondition.evaluate(null, testUserAttributes)); + assertFalse(orCondition.evaluate(null, testUserAttributes, options, reasons)); verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } @@ -1317,7 +1322,7 @@ public void andConditionEvaluateTrue() { conditions.add(orCondition2); AndCondition andCondition = new AndCondition(conditions); - assertTrue(andCondition.evaluate(null, testUserAttributes)); + assertTrue(andCondition.evaluate(null, testUserAttributes, options, reasons)); verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } @@ -1338,7 +1343,7 @@ public void andConditionEvaluateFalseWithNullAndFalse() { conditions.add(orCondition2); AndCondition andCondition = new AndCondition(conditions); - assertFalse(andCondition.evaluate(null, testUserAttributes)); + assertFalse(andCondition.evaluate(null, testUserAttributes, options, reasons)); verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } @@ -1359,7 +1364,7 @@ public void andConditionEvaluateNullWithNullAndTrue() { conditions.add(orCondition2); AndCondition andCondition = new AndCondition(conditions); - assertNull(andCondition.evaluate(null, testUserAttributes)); + assertNull(andCondition.evaluate(null, testUserAttributes, options, reasons)); verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); } @@ -1381,13 +1386,13 @@ public void andConditionEvaluateFalse() { conditions.add(orCondition2); AndCondition andCondition = new AndCondition(conditions); - assertFalse(andCondition.evaluate(null, testUserAttributes)); + assertFalse(andCondition.evaluate(null, testUserAttributes, options, reasons)); verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); // shouldn't be called due to short-circuiting in 'And' evaluation verify(orCondition2, times(0)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); OrCondition orCondition3 = mock(OrCondition.class); - when(orCondition3.evaluate(null, testUserAttributes)).thenReturn(null); + when(orCondition3.evaluate(null, testUserAttributes, options, reasons)).thenReturn(null); // and[null, false] List conditions2 = new ArrayList(); @@ -1395,7 +1400,7 @@ public void andConditionEvaluateFalse() { conditions2.add(orCondition1); AndCondition andCondition2 = new AndCondition(conditions2); - assertFalse(andCondition2.evaluate(null, testUserAttributes)); + assertFalse(andCondition2.evaluate(null, testUserAttributes, options, reasons)); // and[true, false, null] List conditions3 = new ArrayList(); @@ -1404,7 +1409,7 @@ public void andConditionEvaluateFalse() { conditions3.add(orCondition1); AndCondition andCondition3 = new AndCondition(conditions3); - assertFalse(andCondition3.evaluate(null, testUserAttributes)); + assertFalse(andCondition3.evaluate(null, testUserAttributes, options, reasons)); } /** @@ -1436,8 +1441,8 @@ public void nullValueEvaluate() { attributeValue ); - assertNull(nullValueAttribute.evaluate(null, Collections.emptyMap())); - assertNull(nullValueAttribute.evaluate(null, Collections.singletonMap(attributeName, attributeValue))); - assertNull(nullValueAttribute.evaluate(null, (Collections.singletonMap(attributeName, "")))); + assertNull(nullValueAttribute.evaluate(null, Collections.emptyMap(), options, reasons)); + assertNull(nullValueAttribute.evaluate(null, Collections.singletonMap(attributeName, attributeValue), options, reasons)); + assertNull(nullValueAttribute.evaluate(null, (Collections.singletonMap(attributeName, "")), options, reasons)); } } From 28493df396f711a49b23cff4dd6f32524f89e4dd Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 23 Oct 2020 11:25:19 -0700 Subject: [PATCH 26/44] add toString to OptimizelyUserContext and OptimizelyDecision --- .../com/optimizely/ab/OptimizelyUserContext.java | 9 +++++++++ .../ab/optimizelydecision/OptimizelyDecision.java | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java index 5f65bca36..85ecd4a25 100644 --- a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java @@ -183,4 +183,13 @@ public int hashCode() { hash = 31 * hash + optimizely.hashCode(); return hash; } + + @Override + public String toString() { + return "OptimizelyUserContext {" + + "userId='" + userId + '\'' + + ", attributes='" + attributes + '\'' + + '}'; + } + } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java index 713e7331f..4c92c1300 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/OptimizelyDecision.java @@ -144,5 +144,18 @@ public int hashCode() { return hash; } + @Override + public String toString() { + return "OptimizelyDecision {" + + "variationKey='" + variationKey + '\'' + + ", enabled='" + enabled + '\'' + + ", variables='" + variables + '\'' + + ", ruleKey='" + ruleKey + '\'' + + ", flagKey='" + flagKey + '\'' + + ", userContext='" + userContext + '\'' + + ", enabled='" + enabled + '\'' + + ", reasons='" + reasons + '\'' + + '}'; + } } From fe6cde5aa9f877a857a72ffc28155c3a9094b566 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 29 Oct 2020 14:25:16 -0700 Subject: [PATCH 27/44] check null userId for createUserContext --- .../java/com/optimizely/ab/Optimizely.java | 7 +++++- .../optimizely/ab/OptimizelyUserContext.java | 10 ++++---- .../ab/OptimizelyUserContextTest.java | 23 ++++++++++++++++--- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index cbf693690..8d6f5ead5 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1104,7 +1104,12 @@ public OptimizelyConfig getOptimizelyConfig() { * @return An OptimizelyUserContext associated with this OptimizelyClient. */ public OptimizelyUserContext createUserContext(@Nonnull String userId, - @Nonnull Map attributes) { + @Nonnull Map attributes) { + if (userId == null) { + logger.warn("The userId parameter must be nonnull."); + return null; + } + return new OptimizelyUserContext(this, userId, attributes); } diff --git a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java index 85ecd4a25..f986348a7 100644 --- a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java @@ -22,11 +22,11 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; public class OptimizelyUserContext { @Nonnull @@ -42,14 +42,14 @@ public class OptimizelyUserContext { public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId, - @Nonnull Map attributes) { + @Nonnull Map attributes) { this.optimizely = optimizely; this.userId = userId; - this.attributes = new ConcurrentHashMap<>(attributes); + this.attributes = Collections.synchronizedMap(new HashMap<>(attributes)); } public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId) { - this(optimizely, userId, new HashMap<>()); + this(optimizely, userId, Collections.EMPTY_MAP); } public String getUserId() { @@ -70,7 +70,7 @@ public Optimizely getOptimizely() { * @param key An attribute key * @param value An attribute value */ - public void setAttribute(@Nonnull String key, @Nonnull Object value) { + public void setAttribute(@Nonnull String key, @Nullable Object value) { attributes.put(key, value); } diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 5f0c2d0b0..4caf1d587 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -133,6 +133,23 @@ public void setAttribute_override() { assertEquals(newAttributes.get(ATTRIBUTE_HOUSE_KEY), "v2"); } + @Test + public void setAttribute_nullValue() { + Map attributes = Collections.singletonMap("k1", null); + OptimizelyUserContext user = new OptimizelyUserContext(optimizely, userId, attributes); + + Map newAttributes = user.getAttributes(); + assertEquals(newAttributes.get("k1"), null); + + user.setAttribute("k1", true); + newAttributes = user.getAttributes(); + assertEquals(newAttributes.get("k1"), true); + + user.setAttribute("k1", null); + newAttributes = user.getAttributes(); + assertEquals(newAttributes.get("k1"), null); + } + // decide @Test @@ -289,9 +306,9 @@ public void trackEvent() { .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) .build(); - Map attributes = Collections.singletonMap("gender", "f"); + Map attributes = Collections.singletonMap("gender", "f"); String eventKey = "event1"; - Map eventTags = Collections.singletonMap("name", "carrot"); + Map eventTags = Collections.singletonMap("name", "carrot"); OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); user.trackEvent(eventKey, eventTags); @@ -305,7 +322,7 @@ public void trackEvent_noEventTags() { .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) .build(); - Map attributes = Collections.singletonMap("gender", "f"); + Map attributes = Collections.singletonMap("gender", "f"); String eventKey = "event1"; OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); user.trackEvent(eventKey); From 82e90367752ab46ba45daadbb0b57bbbfd116450 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 30 Oct 2020 12:50:35 -0700 Subject: [PATCH 28/44] clean up decision options --- .../java/com/optimizely/ab/Optimizely.java | 5 +- .../com/optimizely/ab/bucketing/Bucketer.java | 12 +-- .../ab/bucketing/DecisionService.java | 77 ++++++++----------- .../ab/config/audience/AndCondition.java | 4 +- .../config/audience/AudienceIdCondition.java | 5 +- .../ab/config/audience/Condition.java | 3 - .../ab/config/audience/EmptyCondition.java | 3 - .../ab/config/audience/NotCondition.java | 5 +- .../ab/config/audience/NullCondition.java | 1 - .../ab/config/audience/OrCondition.java | 4 +- .../ab/config/audience/UserAttribute.java | 3 - .../ab/internal/ExperimentUtils.java | 20 ++--- 12 files changed, 45 insertions(+), 97 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 9ab00b7af..e81c6de1e 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -34,10 +34,7 @@ import com.optimizely.ab.optimizelyconfig.OptimizelyConfig; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService; -import com.optimizely.ab.optimizelydecision.DecisionMessage; -import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; -import com.optimizely.ab.optimizelydecision.OptimizelyDecision; +import com.optimizely.ab.optimizelydecision.*; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index 702c5cd51..6d4ac9433 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -20,14 +20,12 @@ import com.optimizely.ab.bucketing.internal.MurmurHash3; import com.optimizely.ab.config.*; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import java.util.Collections; import java.util.List; /** @@ -71,7 +69,6 @@ private String bucketToEntity(int bucketValue, List trafficAl private Experiment bucketToExperiment(@Nonnull Group group, @Nonnull String bucketingId, @Nonnull ProjectConfig projectConfig, - @Nonnull List options, @Nonnull DecisionReasons reasons) { // "salt" the bucket id using the group id String bucketKey = bucketingId + group.getId(); @@ -93,7 +90,6 @@ private Experiment bucketToExperiment(@Nonnull Group group, private Variation bucketToVariation(@Nonnull Experiment experiment, @Nonnull String bucketingId, - @Nonnull List options, @Nonnull DecisionReasons reasons) { // "salt" the bucket id using the experiment id String experimentId = experiment.getId(); @@ -129,7 +125,6 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, * @param experiment The Experiment in which the user is to be bucketed. * @param bucketingId string A customer-assigned value used to create the key for the murmur hash. * @param projectConfig The current projectConfig - * @param options An array of decision options * @param reasons Decision log messages * @return Variation the user is bucketed into or null. */ @@ -137,7 +132,6 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, public Variation bucket(@Nonnull Experiment experiment, @Nonnull String bucketingId, @Nonnull ProjectConfig projectConfig, - @Nonnull List options, @Nonnull DecisionReasons reasons) { // ---------- Bucket User ---------- String groupId = experiment.getGroupId(); @@ -146,7 +140,7 @@ public Variation bucket(@Nonnull Experiment experiment, Group experimentGroup = projectConfig.getGroupIdMapping().get(groupId); // bucket to an experiment only if group entities are to be mutually exclusive if (experimentGroup.getPolicy().equals(Group.RANDOM_POLICY)) { - Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig, options, reasons); + Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig, reasons); if (bucketedExperiment == null) { String message = reasons.addInfo("User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId()); logger.info(message); @@ -169,14 +163,14 @@ public Variation bucket(@Nonnull Experiment experiment, } } - return bucketToVariation(experiment, bucketingId, options, reasons); + return bucketToVariation(experiment, bucketingId, reasons); } @Nullable public Variation bucket(@Nonnull Experiment experiment, @Nonnull String bucketingId, @Nonnull ProjectConfig projectConfig) { - return bucket(experiment, bucketingId, projectConfig, Collections.emptyList(), new DecisionReasons()); + return bucket(experiment, bucketingId, projectConfig, new DecisionReasons()); } //======== Helper methods ========// diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index 9fe89b25a..696bd7418 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -95,16 +95,16 @@ public Variation getVariation(@Nonnull Experiment experiment, @Nonnull ProjectConfig projectConfig, @Nonnull List options, @Nonnull DecisionReasons reasons) { - if (!ExperimentUtils.isExperimentActive(experiment, options, reasons)) { + if (!ExperimentUtils.isExperimentActive(experiment, reasons)) { return null; } // look for forced bucketing first. - Variation variation = getForcedVariation(experiment, userId, options, reasons); + Variation variation = getForcedVariation(experiment, userId, reasons); // check for whitelisting if (variation == null) { - variation = getWhitelistedVariation(experiment, userId, options, reasons); + variation = getWhitelistedVariation(experiment, userId, reasons); } if (variation != null) { @@ -112,9 +112,10 @@ public Variation getVariation(@Nonnull Experiment experiment, } // fetch the user profile map from the user profile service + boolean ignoreUPS = options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); UserProfile userProfile = null; - if (userProfileService != null) { + if (userProfileService != null && !ignoreUPS) { try { Map userProfileMap = userProfileService.lookup(userId); if (userProfileMap == null) { @@ -131,26 +132,26 @@ public Variation getVariation(@Nonnull Experiment experiment, logger.error(message); errorHandler.handleError(new OptimizelyRuntimeException(exception)); } - } - // check if user exists in user profile - if (userProfile != null) { - variation = getStoredVariation(experiment, userProfile, projectConfig, options, reasons); - // return the stored variation if it exists - if (variation != null) { - return variation; + // check if user exists in user profile + if (userProfile != null) { + variation = getStoredVariation(experiment, userProfile, projectConfig, reasons); + // return the stored variation if it exists + if (variation != null) { + return variation; + } + } else { // if we could not find a user profile, make a new one + userProfile = new UserProfile(userId, new HashMap()); } - } else { // if we could not find a user profile, make a new one - userProfile = new UserProfile(userId, new HashMap()); } - if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, experiment, filteredAttributes, EXPERIMENT, experiment.getKey(), options, reasons)) { - String bucketingId = getBucketingId(userId, filteredAttributes, options, reasons); - variation = bucketer.bucket(experiment, bucketingId, projectConfig, options, reasons); + if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, experiment, filteredAttributes, EXPERIMENT, experiment.getKey(), reasons)) { + String bucketingId = getBucketingId(userId, filteredAttributes, reasons); + variation = bucketer.bucket(experiment, bucketingId, projectConfig, reasons); if (variation != null) { - if (userProfileService != null) { - saveVariation(experiment, variation, userProfile, options, reasons); + if (userProfileService != null && !ignoreUPS) { + saveVariation(experiment, variation, userProfile, reasons); } else { logger.debug("This decision will not be saved since the UserProfileService is null."); } @@ -203,7 +204,7 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, logger.info(message); } - FeatureDecision featureDecision = getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, options, reasons); + FeatureDecision featureDecision = getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, reasons); if (featureDecision.variation == null) { String message = reasons.addInfo("The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".", userId, featureFlag.getKey()); @@ -234,7 +235,6 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, * @param userId User Identifier * @param filteredAttributes A map of filtered attributes. * @param projectConfig The current projectConfig - * @param options An array of decision options * @param reasons Decision log messages * @return {@link FeatureDecision} */ @@ -243,7 +243,6 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig, - @Nonnull List options, @Nonnull DecisionReasons reasons) { // use rollout to get variation for feature if (featureFlag.getRolloutId().isEmpty()) { @@ -261,12 +260,12 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag // for all rules before the everyone else rule int rolloutRulesLength = rollout.getExperiments().size(); - String bucketingId = getBucketingId(userId, filteredAttributes, options, reasons); + String bucketingId = getBucketingId(userId, filteredAttributes, reasons); Variation variation; for (int i = 0; i < rolloutRulesLength - 1; i++) { Experiment rolloutRule = rollout.getExperiments().get(i); - if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, rolloutRule, filteredAttributes, RULE, Integer.toString(i + 1), options, reasons)) { - variation = bucketer.bucket(rolloutRule, bucketingId, projectConfig, options, reasons); + if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, rolloutRule, filteredAttributes, RULE, Integer.toString(i + 1), reasons)) { + variation = bucketer.bucket(rolloutRule, bucketingId, projectConfig, reasons); if (variation == null) { break; } @@ -280,8 +279,8 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag // get last rule which is the fall back rule Experiment finalRule = rollout.getExperiments().get(rolloutRulesLength - 1); - if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, finalRule, filteredAttributes, RULE, "Everyone Else", options, reasons)) { - variation = bucketer.bucket(finalRule, bucketingId, projectConfig, options, reasons); + if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, finalRule, filteredAttributes, RULE, "Everyone Else", reasons)) { + variation = bucketer.bucket(finalRule, bucketingId, projectConfig, reasons); if (variation != null) { String message = reasons.addInfo("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId); logger.debug(message); @@ -297,7 +296,7 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig) { - return getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, Collections.emptyList(), new DecisionReasons()); + return getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, new DecisionReasons()); } /** @@ -305,7 +304,6 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag * * @param experiment {@link Experiment} in which user is to be bucketed. * @param userId User Identifier - * @param options An array of decision options * @param reasons Decision log messages * @return null if the user is not whitelisted into any variation * {@link Variation} the user is bucketed into if the user has a specified whitelisted variation. @@ -313,7 +311,6 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag @Nullable Variation getWhitelistedVariation(@Nonnull Experiment experiment, @Nonnull String userId, - @Nonnull List options, @Nonnull DecisionReasons reasons) { // if a user has a forced variation mapping, return the respective variation Map userIdToVariationKeyMap = experiment.getUserIdToVariationKeyMap(); @@ -337,7 +334,7 @@ Variation getWhitelistedVariation(@Nonnull Experiment experiment, Variation getWhitelistedVariation(@Nonnull Experiment experiment, @Nonnull String userId) { - return getWhitelistedVariation(experiment, userId, Collections.emptyList(), new DecisionReasons()); + return getWhitelistedVariation(experiment, userId, new DecisionReasons()); } /** @@ -346,7 +343,6 @@ Variation getWhitelistedVariation(@Nonnull Experiment experiment, * @param experiment {@link Experiment} in which the user was bucketed. * @param userProfile {@link UserProfile} of the user. * @param projectConfig The current projectConfig - * @param options An array of decision options * @param reasons Decision log messages * @return null if the {@link UserProfileService} implementation is null or the user was not previously bucketed. * else return the {@link Variation} the user was previously bucketed into. @@ -355,11 +351,8 @@ Variation getWhitelistedVariation(@Nonnull Experiment experiment, Variation getStoredVariation(@Nonnull Experiment experiment, @Nonnull UserProfile userProfile, @Nonnull ProjectConfig projectConfig, - @Nonnull List options, @Nonnull DecisionReasons reasons) { - if (options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return null; - // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation String experimentId = experiment.getId(); @@ -396,7 +389,7 @@ Variation getStoredVariation(@Nonnull Experiment experiment, Variation getStoredVariation(@Nonnull Experiment experiment, @Nonnull UserProfile userProfile, @Nonnull ProjectConfig projectConfig) { - return getStoredVariation(experiment, userProfile, projectConfig, Collections.emptyList(), new DecisionReasons()); + return getStoredVariation(experiment, userProfile, projectConfig, new DecisionReasons()); } /** @@ -405,17 +398,13 @@ Variation getStoredVariation(@Nonnull Experiment experiment, * @param experiment The experiment the user was buck * @param variation The Variation to save. * @param userProfile A {@link UserProfile} instance of the user information. - * @param options An array of decision options * @param reasons Decision log messages */ void saveVariation(@Nonnull Experiment experiment, @Nonnull Variation variation, @Nonnull UserProfile userProfile, - @Nonnull List options, @Nonnull DecisionReasons reasons) { - if (options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)) return; - // only save if the user has implemented a user profile service if (userProfileService != null) { String experimentId = experiment.getId(); @@ -444,7 +433,7 @@ void saveVariation(@Nonnull Experiment experiment, void saveVariation(@Nonnull Experiment experiment, @Nonnull Variation variation, @Nonnull UserProfile userProfile) { - saveVariation(experiment, variation, userProfile, Collections.emptyList(), new DecisionReasons()); + saveVariation(experiment, variation, userProfile, new DecisionReasons()); } /** @@ -452,14 +441,12 @@ void saveVariation(@Nonnull Experiment experiment, * * @param userId The userId of the user. * @param filteredAttributes The user's attributes. This should be filtered to just attributes in the Datafile. - * @param options An array of decision options * @param reasons Decision log messages * @return bucketingId if it is a String type in attributes. * else return userId */ String getBucketingId(@Nonnull String userId, @Nonnull Map filteredAttributes, - @Nonnull List options, @Nonnull DecisionReasons reasons) { String bucketingId = userId; if (filteredAttributes != null && filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) { @@ -476,7 +463,7 @@ String getBucketingId(@Nonnull String userId, String getBucketingId(@Nonnull String userId, @Nonnull Map filteredAttributes) { - return getBucketingId(userId, filteredAttributes, Collections.emptyList(), new DecisionReasons()); + return getBucketingId(userId, filteredAttributes, new DecisionReasons()); } public ConcurrentHashMap> getForcedVariationMapping() { @@ -557,7 +544,6 @@ public boolean setForcedVariation(@Nonnull Experiment experiment, * * @param experiment The experiment forced. * @param userId The user ID to be used for bucketing. - * @param options An array of decision options * @param reasons Decision log messages * @return The variation the user was bucketed into. This value can be null if the * forced variation fails. @@ -565,7 +551,6 @@ public boolean setForcedVariation(@Nonnull Experiment experiment, @Nullable public Variation getForcedVariation(@Nonnull Experiment experiment, @Nonnull String userId, - @Nonnull List options, @Nonnull DecisionReasons reasons) { // if the user id is invalid, return false. if (!validateUserId(userId)) { @@ -593,7 +578,7 @@ public Variation getForcedVariation(@Nonnull Experiment experiment, @Nullable public Variation getForcedVariation(@Nonnull Experiment experiment, @Nonnull String userId) { - return getForcedVariation(experiment, userId, Collections.emptyList(), new DecisionReasons()); + return getForcedVariation(experiment, userId, new DecisionReasons()); } /** diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java index 0465bf5b7..20c15e95d 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java @@ -18,7 +18,6 @@ import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -43,7 +42,6 @@ public List getConditions() { @Nullable public Boolean evaluate(ProjectConfig config, Map attributes, - List options, DecisionReasons reasons) { if (conditions == null) return null; boolean foundNull = false; @@ -55,7 +53,7 @@ public Boolean evaluate(ProjectConfig config, // true and true is true // null and null is null for (Condition condition : conditions) { - Boolean conditionEval = condition.evaluate(config, attributes, options, reasons); + Boolean conditionEval = condition.evaluate(config, attributes, reasons); if (conditionEval == null) { foundNull = true; } else if (!conditionEval) { // false with nulls or trues is false. diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java index 121c81f0c..fb076a90d 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java @@ -20,12 +20,10 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; -import java.util.List; import java.util.Map; import java.util.Objects; @@ -68,7 +66,6 @@ public String getAudienceId() { @Override public Boolean evaluate(ProjectConfig config, Map attributes, - List options, DecisionReasons reasons) { if (config != null) { audience = config.getAudienceIdMapping().get(audienceId); @@ -79,7 +76,7 @@ public Boolean evaluate(ProjectConfig config, return null; } logger.debug("Starting to evaluate audience \"{}\" with conditions: {}.", audience.getId(), audience.getConditions()); - Boolean result = audience.getConditions().evaluate(config, attributes, options, reasons); + Boolean result = audience.getConditions().evaluate(config, attributes, reasons); logger.debug("Audience \"{}\" evaluated to {}.", audience.getId(), result); return result; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java index c490fb1e0..4d108214c 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java @@ -18,10 +18,8 @@ import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nullable; -import java.util.List; import java.util.Map; /** @@ -32,7 +30,6 @@ public interface Condition { @Nullable Boolean evaluate(ProjectConfig config, Map attributes, - List options, DecisionReasons reasons); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java index 53a8ff400..b5978d200 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/EmptyCondition.java @@ -17,10 +17,8 @@ import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nullable; -import java.util.List; import java.util.Map; public class EmptyCondition implements Condition { @@ -28,7 +26,6 @@ public class EmptyCondition implements Condition { @Override public Boolean evaluate(ProjectConfig config, Map attributes, - List options, DecisionReasons reasons) { return true; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java index 44b715a52..8a523bb8d 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java @@ -18,12 +18,10 @@ import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; -import java.util.List; import java.util.Map; /** @@ -45,10 +43,9 @@ public Condition getCondition() { @Nullable public Boolean evaluate(ProjectConfig config, Map attributes, - List options, DecisionReasons reasons) { - Boolean conditionEval = condition == null ? null : condition.evaluate(config, attributes, options, reasons); + Boolean conditionEval = condition == null ? null : condition.evaluate(config, attributes, reasons); return (conditionEval == null ? null : !conditionEval); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java index adfa89554..102828ce8 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java @@ -28,7 +28,6 @@ public class NullCondition implements Condition { @Override public Boolean evaluate(ProjectConfig config, Map attributes, - List options, DecisionReasons reasons) { return null; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java index ac22914df..b2c2f0afe 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java @@ -18,7 +18,6 @@ import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -49,12 +48,11 @@ public List getConditions() { @Nullable public Boolean evaluate(ProjectConfig config, Map attributes, - List options, DecisionReasons reasons) { if (conditions == null) return null; boolean foundNull = false; for (Condition condition : conditions) { - Boolean conditionEval = condition.evaluate(config, attributes, options, reasons); + Boolean conditionEval = condition.evaluate(config, attributes, reasons); if (conditionEval == null) { // true with falses and nulls is still true foundNull = true; } else if (conditionEval) { diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java index 65df43535..152bb7048 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java @@ -22,7 +22,6 @@ import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.match.*; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,7 +29,6 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import java.util.Collections; -import java.util.List; import java.util.Map; /** @@ -76,7 +74,6 @@ public Object getValue() { @Nullable public Boolean evaluate(ProjectConfig config, Map attributes, - List options, DecisionReasons reasons) { if (attributes == null) { attributes = Collections.emptyMap(); diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java index 692b287d8..04e281ccb 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java @@ -22,14 +22,12 @@ import com.optimizely.ab.config.audience.Condition; import com.optimizely.ab.config.audience.OrCondition; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -44,12 +42,10 @@ private ExperimentUtils() { * Helper method to validate all pre-conditions before bucketing a user. * * @param experiment the experiment we are validating pre-conditions for - * @param options An array of decision options * @param reasons Decision log messages * @return whether the pre-conditions are satisfied */ public static boolean isExperimentActive(@Nonnull Experiment experiment, - @Nonnull List options, @Nonnull DecisionReasons reasons) { if (!experiment.isActive()) { @@ -62,7 +58,7 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment, } public static boolean isExperimentActive(@Nonnull Experiment experiment) { - return isExperimentActive(experiment, Collections.emptyList(), new DecisionReasons()); + return isExperimentActive(experiment, new DecisionReasons()); } /** @@ -73,7 +69,6 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment) { * @param attributes the attributes of the user * @param loggingEntityType It can be either experiment or rule. * @param loggingKey In case of loggingEntityType is experiment it will be experiment key or else it will be rule number. - * @param options An array of decision options * @param reasons Decision log messages * @return whether the user meets the criteria for the experiment */ @@ -82,14 +77,13 @@ public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig proj @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey, - @Nonnull List options, @Nonnull DecisionReasons reasons) { if (experiment.getAudienceConditions() != null) { logger.debug("Evaluating audiences for {} \"{}\": {}.", loggingEntityType, loggingKey, experiment.getAudienceConditions()); - Boolean resolveReturn = evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, options, reasons); + Boolean resolveReturn = evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, reasons); return resolveReturn == null ? false : resolveReturn; } else { - Boolean resolveReturn = evaluateAudience(projectConfig, experiment, attributes, loggingEntityType, loggingKey, options, reasons); + Boolean resolveReturn = evaluateAudience(projectConfig, experiment, attributes, loggingEntityType, loggingKey, reasons); return Boolean.TRUE.equals(resolveReturn); } } @@ -109,7 +103,7 @@ public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig proj @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey) { - return doesUserMeetAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, Collections.emptyList(), new DecisionReasons()); + return doesUserMeetAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, new DecisionReasons()); } @Nullable @@ -118,7 +112,6 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey, - @Nonnull List options, @Nonnull DecisionReasons reasons) { List experimentAudienceIds = experiment.getAudienceIds(); @@ -137,7 +130,7 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, logger.debug("Evaluating audiences for {} \"{}\": {}.", loggingEntityType, loggingKey, conditions); - Boolean result = implicitOr.evaluate(projectConfig, attributes, options, reasons); + Boolean result = implicitOr.evaluate(projectConfig, attributes, reasons); String message = reasons.addInfo("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); logger.info(message); @@ -151,14 +144,13 @@ public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectC @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey, - @Nonnull List options, @Nonnull DecisionReasons reasons) { Condition conditions = experiment.getAudienceConditions(); if (conditions == null) return null; try { - Boolean result = conditions.evaluate(projectConfig, attributes, options, reasons); + Boolean result = conditions.evaluate(projectConfig, attributes, reasons); String message = reasons.addInfo("Audiences for %s \"%s\" collectively evaluated to %s.", loggingEntityType, loggingKey, result); logger.info(message); return result; From 73668a9ec41a9efe93be8766584e8a8d4c368141 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 30 Oct 2020 15:34:21 -0700 Subject: [PATCH 29/44] clean up decide --- .../java/com/optimizely/ab/Optimizely.java | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index e81c6de1e..836906891 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1189,12 +1189,28 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, flagEnabled = true; } } + logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled); + + Map variableMap = new HashMap<>(); + if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) { + variableMap = getDecisionVariableMap( + flag, + flagDecision.variation, + flagEnabled, + decisionReasons); + } + OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap); FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT; if (flagDecision.decisionSource != null) { decisionSource = flagDecision.decisionSource; } + List reasonsToReport = decisionReasons.toReport(); + String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null; + // TODO: add ruleKey values when available later. use a copy of experimentKey until then. + String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null; + if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) { sendImpression( projectConfig, @@ -1207,22 +1223,6 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, decisionEventDispatched = true; } - Map variableMap = new HashMap<>(); - if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) { - variableMap = getDecisionVariableMap( - flag, - flagDecision.variation, - flagEnabled, - decisionReasons); - } - - OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap); - - List reasonsToReport = decisionReasons.toReport(); - String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null; - // TODO: add ruleKey values when available later. use a copy of experimentKey until then. - String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null; - DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder() .withUserId(userId) .withAttributes(copiedAttributes) @@ -1236,8 +1236,6 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, .build(); notificationCenter.send(decisionNotification); - logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled); - return new OptimizelyDecision( variationKey, flagEnabled, From e0cb8fb2dea2db7aa0133bee442d72c979e625a0 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 30 Oct 2020 16:12:49 -0700 Subject: [PATCH 30/44] remove options propagation from tests --- .../com/optimizely/ab/OptimizelyTest.java | 26 +- .../ab/bucketing/DecisionServiceTest.java | 57 ++- .../AudienceConditionEvaluationTest.java | 442 +++++++++--------- 3 files changed, 259 insertions(+), 266 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index 00e5b949d..6cb7eb360 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -377,7 +377,7 @@ public void activateForNullVariation() throws Exception { Experiment activatedExperiment = validProjectConfig.getExperiments().get(0); Map testUserAttributes = Collections.singletonMap("browser_type", "chromey"); - when(mockBucketer.bucket(eq(activatedExperiment), eq(testBucketingId), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(null); + when(mockBucketer.bucket(eq(activatedExperiment), eq(testBucketingId), eq(validProjectConfig), anyObject())).thenReturn(null); logbackVerifier.expectMessage(Level.INFO, "Not activating user \"userId\" for experiment \"" + activatedExperiment.getKey() + "\"."); @@ -936,7 +936,7 @@ public void activateWithInvalidDatafile() throws Exception { assertNull(expectedVariation); // make sure we didn't even attempt to bucket the user - verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); + verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject()); } //======== track tests ========// @@ -1237,7 +1237,7 @@ public void trackWithInvalidDatafile() throws Exception { optimizely.track("event_with_launched_and_running_experiments", genericUserId); // make sure we didn't even attempt to bucket the user or fire any conversion events - verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); + verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject()); verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class)); } @@ -1254,7 +1254,7 @@ public void getVariation() throws Exception { Optimizely optimizely = optimizelyBuilder.withBucketing(mockBucketer).build(); - when(mockBucketer.bucket(eq(activatedExperiment), eq(testUserId), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(bucketedVariation); + when(mockBucketer.bucket(eq(activatedExperiment), eq(testUserId), eq(validProjectConfig), anyObject())).thenReturn(bucketedVariation); Map testUserAttributes = new HashMap<>(); testUserAttributes.put("browser_type", "chrome"); @@ -1264,7 +1264,7 @@ public void getVariation() throws Exception { testUserAttributes); // verify that the bucketing algorithm was called correctly - verify(mockBucketer).bucket(eq(activatedExperiment), eq(testUserId), eq(validProjectConfig), anyObject(), anyObject()); + verify(mockBucketer).bucket(eq(activatedExperiment), eq(testUserId), eq(validProjectConfig), anyObject()); assertThat(actualVariation, is(bucketedVariation)); // verify that we didn't attempt to dispatch an event @@ -1285,13 +1285,13 @@ public void getVariationWithExperimentKey() throws Exception { .withConfig(noAudienceProjectConfig) .build(); - when(mockBucketer.bucket(eq(activatedExperiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject(), anyObject())).thenReturn(bucketedVariation); + when(mockBucketer.bucket(eq(activatedExperiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject())).thenReturn(bucketedVariation); // activate the experiment Variation actualVariation = optimizely.getVariation(activatedExperiment.getKey(), testUserId); // verify that the bucketing algorithm was called correctly - verify(mockBucketer).bucket(eq(activatedExperiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject(), anyObject()); + verify(mockBucketer).bucket(eq(activatedExperiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject()); assertThat(actualVariation, is(bucketedVariation)); // verify that we didn't attempt to dispatch an event @@ -1346,7 +1346,7 @@ public void getVariationWithAudiences() throws Exception { Experiment experiment = validProjectConfig.getExperiments().get(0); Variation bucketedVariation = experiment.getVariations().get(0); - when(mockBucketer.bucket(eq(experiment), eq(testUserId), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(bucketedVariation); + when(mockBucketer.bucket(eq(experiment), eq(testUserId), eq(validProjectConfig), anyObject())).thenReturn(bucketedVariation); Optimizely optimizely = optimizelyBuilder.withBucketing(mockBucketer).build(); @@ -1355,7 +1355,7 @@ public void getVariationWithAudiences() throws Exception { Variation actualVariation = optimizely.getVariation(experiment.getKey(), testUserId, testUserAttributes); - verify(mockBucketer).bucket(eq(experiment), eq(testUserId), eq(validProjectConfig), anyObject(), anyObject()); + verify(mockBucketer).bucket(eq(experiment), eq(testUserId), eq(validProjectConfig), anyObject()); assertThat(actualVariation, is(bucketedVariation)); } @@ -1396,7 +1396,7 @@ public void getVariationNoAudiences() throws Exception { Experiment experiment = noAudienceProjectConfig.getExperiments().get(0); Variation bucketedVariation = experiment.getVariations().get(0); - when(mockBucketer.bucket(eq(experiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject(), anyObject())).thenReturn(bucketedVariation); + when(mockBucketer.bucket(eq(experiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject())).thenReturn(bucketedVariation); Optimizely optimizely = optimizelyBuilder .withConfig(noAudienceProjectConfig) @@ -1405,7 +1405,7 @@ public void getVariationNoAudiences() throws Exception { Variation actualVariation = optimizely.getVariation(experiment.getKey(), testUserId); - verify(mockBucketer).bucket(eq(experiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject(), anyObject()); + verify(mockBucketer).bucket(eq(experiment), eq(testUserId), eq(noAudienceProjectConfig), anyObject()); assertThat(actualVariation, is(bucketedVariation)); } @@ -1463,7 +1463,7 @@ public void getVariationForGroupExperimentWithMatchingAttributes() throws Except attributes.put("browser_type", "chrome"); } - when(mockBucketer.bucket(eq(experiment), eq("user"), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(variation); + when(mockBucketer.bucket(eq(experiment), eq("user"), eq(validProjectConfig), anyObject())).thenReturn(variation); Optimizely optimizely = optimizelyBuilder.withBucketing(mockBucketer).build(); @@ -1521,7 +1521,7 @@ public void getVariationWithInvalidDatafile() throws Exception { assertNull(variation); // make sure we didn't even attempt to bucket the user - verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); + verify(mockBucketer, never()).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject()); } //======== Notification listeners ========// diff --git a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java index 9c7eae98a..d12cd9a56 100644 --- a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java @@ -95,8 +95,8 @@ public void getVariationWhitelistedPrecedesAudienceEval() throws Exception { // no attributes provided for a experiment that has an audience assertThat(decisionService.getVariation(experiment, whitelistedUserId, Collections.emptyMap(), validProjectConfig), is(expectedVariation)); - verify(decisionService).getWhitelistedVariation(eq(experiment), eq(whitelistedUserId), anyObject(), anyObject()); - verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), any(ProjectConfig.class), anyObject(), anyObject()); + verify(decisionService).getWhitelistedVariation(eq(experiment), eq(whitelistedUserId), anyObject()); + verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), any(ProjectConfig.class), anyObject()); } /** @@ -118,7 +118,7 @@ public void getForcedVariationBeforeWhitelisting() throws Exception { assertThat(decisionService.getVariation(experiment, whitelistedUserId, Collections.emptyMap(), validProjectConfig), is(expectedVariation)); //verify(decisionService).getForcedVariation(experiment.getKey(), whitelistedUserId); - verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), any(ProjectConfig.class), anyObject(), anyObject()); + verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), any(ProjectConfig.class), anyObject()); assertEquals(decisionService.getWhitelistedVariation(experiment, whitelistedUserId), whitelistVariation); assertTrue(decisionService.setForcedVariation(experiment, whitelistedUserId, null)); assertNull(decisionService.getForcedVariation(experiment, whitelistedUserId)); @@ -142,7 +142,7 @@ public void getVariationForcedPrecedesAudienceEval() throws Exception { // no attributes provided for a experiment that has an audience assertThat(decisionService.getVariation(experiment, genericUserId, Collections.emptyMap(), validProjectConfig), is(expectedVariation)); - verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), eq(validProjectConfig), anyObject(), anyObject()); + verify(decisionService, never()).getStoredVariation(eq(experiment), any(UserProfile.class), eq(validProjectConfig), anyObject()); assertEquals(decisionService.setForcedVariation(experiment, genericUserId, null), true); assertNull(decisionService.getForcedVariation(experiment, genericUserId)); } @@ -297,7 +297,6 @@ public void getVariationForFeatureReturnsNullWhenItGetsNoVariationsForExperiment anyString(), anyMapOf(String.class, String.class), any(ProjectConfig.class), - anyObject(), anyObject() ); @@ -394,7 +393,6 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout() anyString(), anyMapOf(String.class, String.class), any(ProjectConfig.class), - anyObject(), anyObject() ); @@ -414,7 +412,6 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout() anyString(), anyMapOf(String.class, String.class), any(ProjectConfig.class), - anyObject(), anyObject() ); @@ -462,7 +459,6 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails anyString(), anyMapOf(String.class, String.class), any(ProjectConfig.class), - anyObject(), anyObject() ); @@ -482,7 +478,6 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails anyString(), anyMapOf(String.class, String.class), any(ProjectConfig.class), - anyObject(), anyObject() ); @@ -539,7 +534,7 @@ public void getVariationForFeatureInRolloutReturnsNullWhenFeatureIsNotAttachedTo @Test public void getVariationForFeatureInRolloutReturnsNullWhenUserIsExcludedFromAllTraffic() { Bucketer mockBucketer = mock(Bucketer.class); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(null); DecisionService decisionService = new DecisionService( mockBucketer, @@ -561,7 +556,7 @@ public void getVariationForFeatureInRolloutReturnsNullWhenUserIsExcludedFromAllT // with fall back bucketing, the user has at most 2 chances to get bucketed with traffic allocation // one chance with the audience rollout rule // one chance with the everyone else rule - verify(mockBucketer, atMost(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); + verify(mockBucketer, atMost(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject()); } /** @@ -572,7 +567,7 @@ public void getVariationForFeatureInRolloutReturnsNullWhenUserIsExcludedFromAllT @Test public void getVariationForFeatureInRolloutReturnsNullWhenUserFailsAllAudiencesAndTraffic() { Bucketer mockBucketer = mock(Bucketer.class); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(null); DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, null); @@ -586,7 +581,7 @@ public void getVariationForFeatureInRolloutReturnsNullWhenUserFailsAllAudiencesA assertNull(featureDecision.decisionSource); // user is only bucketed once for the everyone else rule - verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); + verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject()); } /** @@ -600,7 +595,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsAllAudie Rollout rollout = ROLLOUT_2; Experiment everyoneElseRule = rollout.getExperiments().get(rollout.getExperiments().size() - 1); Variation expectedVariation = everyoneElseRule.getVariations().get(0); - when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(expectedVariation); + when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(expectedVariation); DecisionService decisionService = new DecisionService( mockBucketer, @@ -626,7 +621,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsAllAudie assertEquals(FeatureDecision.DecisionSource.ROLLOUT, featureDecision.decisionSource); // verify user is only bucketed once for everyone else rule - verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); + verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject()); } /** @@ -641,8 +636,8 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTrafficI Rollout rollout = ROLLOUT_2; Experiment everyoneElseRule = rollout.getExperiments().get(rollout.getExperiments().size() - 1); Variation expectedVariation = everyoneElseRule.getVariations().get(0); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); - when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(expectedVariation); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(null); + when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(expectedVariation); DecisionService decisionService = new DecisionService( mockBucketer, @@ -664,7 +659,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTrafficI logbackVerifier.expectMessage(Level.DEBUG, "User \"genericUserId\" meets conditions for targeting rule \"Everyone Else\"."); // verify user is only bucketed once for everyone else rule - verify(mockBucketer, times(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); + verify(mockBucketer, times(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject()); } /** @@ -683,9 +678,9 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTrafficI Variation englishCitizenVariation = englishCitizensRule.getVariations().get(0); Experiment everyoneElseRule = rollout.getExperiments().get(rollout.getExperiments().size() - 1); Variation expectedVariation = everyoneElseRule.getVariations().get(0); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); - when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(expectedVariation); - when(mockBucketer.bucket(eq(englishCitizensRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(englishCitizenVariation); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(null); + when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(expectedVariation); + when(mockBucketer.bucket(eq(englishCitizensRule), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(englishCitizenVariation); DecisionService decisionService = new DecisionService( mockBucketer, @@ -710,7 +705,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTrafficI assertEquals(FeatureDecision.DecisionSource.ROLLOUT, featureDecision.decisionSource); // verify user is only bucketed once for everyone else rule - verify(mockBucketer, times(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); + verify(mockBucketer, times(2)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject()); } /** @@ -726,9 +721,9 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTargetin Variation englishCitizenVariation = englishCitizensRule.getVariations().get(0); Experiment everyoneElseRule = rollout.getExperiments().get(rollout.getExperiments().size() - 1); Variation everyoneElseVariation = everyoneElseRule.getVariations().get(0); - when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(null); - when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(everyoneElseVariation); - when(mockBucketer.bucket(eq(englishCitizensRule), anyString(), any(ProjectConfig.class), anyObject(), anyObject())).thenReturn(englishCitizenVariation); + when(mockBucketer.bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(null); + when(mockBucketer.bucket(eq(everyoneElseRule), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(everyoneElseVariation); + when(mockBucketer.bucket(eq(englishCitizensRule), anyString(), any(ProjectConfig.class), anyObject())).thenReturn(englishCitizenVariation); DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, null); @@ -748,7 +743,7 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTargetin logbackVerifier.expectMessage(Level.DEBUG, "Audience \"4194404272\" evaluated to true."); logbackVerifier.expectMessage(Level.INFO, "Audiences for rule \"3\" collectively evaluated to true"); // verify user is only bucketed once for everyone else rule - verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject(), anyObject()); + verify(mockBucketer, times(1)).bucket(any(Experiment.class), anyString(), any(ProjectConfig.class), anyObject()); } //========= white list tests ==========/ @@ -901,7 +896,7 @@ public void getVariationSavesBucketedVariationIntoUserProfile() throws Exception Collections.singletonMap(experiment.getId(), decision)); Bucketer mockBucketer = mock(Bucketer.class); - when(mockBucketer.bucket(eq(experiment), eq(userProfileId), eq(noAudienceProjectConfig), anyObject(), anyObject())).thenReturn(variation); + when(mockBucketer.bucket(eq(experiment), eq(userProfileId), eq(noAudienceProjectConfig), anyObject())).thenReturn(variation); DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, userProfileService); @@ -963,7 +958,7 @@ public void getVariationSavesANewUserProfile() throws Exception { UserProfileService userProfileService = mock(UserProfileService.class); DecisionService decisionService = new DecisionService(bucketer, mockErrorHandler, userProfileService); - when(bucketer.bucket(eq(experiment), eq(userProfileId), eq(noAudienceProjectConfig), anyObject(), anyObject())).thenReturn(variation); + when(bucketer.bucket(eq(experiment), eq(userProfileId), eq(noAudienceProjectConfig), anyObject())).thenReturn(variation); when(userProfileService.lookup(userProfileId)).thenReturn(null); assertEquals(variation, decisionService.getVariation(experiment, userProfileId, Collections.emptyMap(), noAudienceProjectConfig)); @@ -977,7 +972,7 @@ public void getVariationBucketingId() throws Exception { Experiment experiment = validProjectConfig.getExperiments().get(0); Variation expectedVariation = experiment.getVariations().get(0); - when(bucketer.bucket(eq(experiment), eq("bucketId"), eq(validProjectConfig), anyObject(), anyObject())).thenReturn(expectedVariation); + when(bucketer.bucket(eq(experiment), eq("bucketId"), eq(validProjectConfig), anyObject())).thenReturn(expectedVariation); Map attr = new HashMap(); attr.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), "bucketId"); @@ -1001,8 +996,8 @@ public void getVariationForRolloutWithBucketingId() { attributes.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), bucketingId); Bucketer bucketer = mock(Bucketer.class); - when(bucketer.bucket(eq(rolloutRuleExperiment), eq(userId), eq(v4ProjectConfig), anyObject(), anyObject())).thenReturn(null); - when(bucketer.bucket(eq(rolloutRuleExperiment), eq(bucketingId), eq(v4ProjectConfig), anyObject(), anyObject())).thenReturn(rolloutVariation); + when(bucketer.bucket(eq(rolloutRuleExperiment), eq(userId), eq(v4ProjectConfig), anyObject())).thenReturn(null); + when(bucketer.bucket(eq(rolloutRuleExperiment), eq(bucketingId), eq(v4ProjectConfig), anyObject())).thenReturn(rolloutVariation); DecisionService decisionService = spy(new DecisionService( bucketer, diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java index ce955d627..ea5b3edd9 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java @@ -19,7 +19,6 @@ import ch.qos.logback.classic.Level; import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Before; import org.junit.Rule; @@ -44,7 +43,6 @@ public class AudienceConditionEvaluationTest { Map testUserAttributes; Map testTypedUserAttributes; - List options = Collections.EMPTY_LIST; DecisionReasons reasons = new DecisionReasons(); @Before @@ -71,7 +69,7 @@ public void userAttributeEvaluateTrue() throws Exception { assertNull(testInstance.getMatch()); assertEquals(testInstance.getName(), "browser_type"); assertEquals(testInstance.getType(), "custom_attribute"); - assertTrue(testInstance.evaluate(null, testUserAttributes, options, reasons)); + assertTrue(testInstance.evaluate(null, testUserAttributes, reasons)); } /** @@ -80,7 +78,7 @@ public void userAttributeEvaluateTrue() throws Exception { @Test public void userAttributeEvaluateFalse() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", null, "firefox"); - assertFalse(testInstance.evaluate(null, testUserAttributes, options, reasons)); + assertFalse(testInstance.evaluate(null, testUserAttributes, reasons)); } /** @@ -89,7 +87,7 @@ public void userAttributeEvaluateFalse() throws Exception { @Test public void userAttributeUnknownAttribute() throws Exception { UserAttribute testInstance = new UserAttribute("unknown_dim", "custom_attribute", null, "unknown"); - assertFalse(testInstance.evaluate(null, testUserAttributes, options, reasons)); + assertFalse(testInstance.evaluate(null, testUserAttributes, reasons)); } /** @@ -98,7 +96,7 @@ public void userAttributeUnknownAttribute() throws Exception { @Test public void invalidMatchCondition() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "unknown_dimension", null, "chrome"); - assertNull(testInstance.evaluate(null, testUserAttributes, options, reasons)); + assertNull(testInstance.evaluate(null, testUserAttributes, reasons)); } /** @@ -107,7 +105,7 @@ public void invalidMatchCondition() throws Exception { @Test public void invalidMatch() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "blah", "chrome"); - assertNull(testInstance.evaluate(null, testUserAttributes, options, reasons)); + assertNull(testInstance.evaluate(null, testUserAttributes, reasons)); logbackVerifier.expectMessage(Level.WARN, "Audience condition \"{name='browser_type', type='custom_attribute', match='blah', value='chrome'}\" uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK"); } @@ -118,7 +116,7 @@ public void invalidMatch() throws Exception { @Test public void unexpectedAttributeType() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "gt", 20); - assertNull(testInstance.evaluate(null, testUserAttributes, options, reasons)); + assertNull(testInstance.evaluate(null, testUserAttributes, reasons)); logbackVerifier.expectMessage(Level.WARN, "Audience condition \"{name='browser_type', type='custom_attribute', match='gt', value=20}\" evaluated to UNKNOWN because a value of type \"java.lang.String\" was passed for user attribute \"browser_type\""); } @@ -129,7 +127,7 @@ public void unexpectedAttributeType() throws Exception { @Test public void unexpectedAttributeTypeNull() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "gt", 20); - assertNull(testInstance.evaluate(null, Collections.singletonMap("browser_type", null), options, reasons)); + assertNull(testInstance.evaluate(null, Collections.singletonMap("browser_type", null), reasons)); logbackVerifier.expectMessage(Level.DEBUG, "Audience condition \"{name='browser_type', type='custom_attribute', match='gt', value=20}\" evaluated to UNKNOWN because a null value was passed for user attribute \"browser_type\""); } @@ -141,7 +139,7 @@ public void unexpectedAttributeTypeNull() throws Exception { @Test public void missingAttribute() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "gt", 20); - assertNull(testInstance.evaluate(null, Collections.EMPTY_MAP, options, reasons)); + assertNull(testInstance.evaluate(null, Collections.EMPTY_MAP, reasons)); logbackVerifier.expectMessage(Level.DEBUG, "Audience condition \"{name='browser_type', type='custom_attribute', match='gt', value=20}\" evaluated to UNKNOWN because no value was passed for user attribute \"browser_type\""); } @@ -152,7 +150,7 @@ public void missingAttribute() throws Exception { @Test public void nullAttribute() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "gt", 20); - assertNull(testInstance.evaluate(null, null, options, reasons)); + assertNull(testInstance.evaluate(null, null, reasons)); logbackVerifier.expectMessage(Level.DEBUG, "Audience condition \"{name='browser_type', type='custom_attribute', match='gt', value=20}\" evaluated to UNKNOWN because no value was passed for user attribute \"browser_type\""); } @@ -163,7 +161,7 @@ public void nullAttribute() throws Exception { @Test public void unknownConditionType() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "blah", "exists", "firefox"); - assertNull(testInstance.evaluate(null, testUserAttributes, options, reasons)); + assertNull(testInstance.evaluate(null, testUserAttributes, reasons)); logbackVerifier.expectMessage(Level.WARN, "Audience condition \"{name='browser_type', type='blah', match='exists', value='firefox'}\" uses an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK."); } @@ -177,9 +175,9 @@ public void existsMatchConditionEmptyStringEvaluatesTrue() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "exists", "firefox"); Map attributes = new HashMap<>(); attributes.put("browser_type", ""); - assertTrue(testInstance.evaluate(null, attributes, options, reasons)); + assertTrue(testInstance.evaluate(null, attributes, reasons)); attributes.put("browser_type", null); - assertFalse(testInstance.evaluate(null, attributes, options, reasons)); + assertFalse(testInstance.evaluate(null, attributes, reasons)); } /** @@ -189,16 +187,16 @@ public void existsMatchConditionEmptyStringEvaluatesTrue() throws Exception { @Test public void existsMatchConditionEvaluatesTrue() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "exists", "firefox"); - assertTrue(testInstance.evaluate(null, testUserAttributes, options, reasons)); + assertTrue(testInstance.evaluate(null, testUserAttributes, reasons)); UserAttribute testInstanceBoolean = new UserAttribute("is_firefox", "custom_attribute", "exists", false); UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "exists", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "exists", 4.55); UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "exists", testUserAttributes); - assertTrue(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); - assertTrue(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceBoolean.evaluate(null, testTypedUserAttributes, reasons)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); + assertTrue(testInstanceObject.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -209,8 +207,8 @@ public void existsMatchConditionEvaluatesTrue() throws Exception { public void existsMatchConditionEvaluatesFalse() throws Exception { UserAttribute testInstance = new UserAttribute("bad_var", "custom_attribute", "exists", "chrome"); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "exists", "chrome"); - assertFalse(testInstance.evaluate(null, testTypedUserAttributes, options, reasons)); - assertFalse(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstance.evaluate(null, testTypedUserAttributes, reasons)); + assertFalse(testInstanceNull.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -225,11 +223,11 @@ public void exactMatchConditionEvaluatesTrue() { UserAttribute testInstanceFloat = new UserAttribute("num_size", "custom_attribute", "exact", (float) 3); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "exact", 3.55); - assertTrue(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); - assertTrue(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 3), options, reasons)); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testUserAttributes, reasons)); + assertTrue(testInstanceBoolean.evaluate(null, testTypedUserAttributes, reasons)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 3), reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -262,22 +260,22 @@ public void exactMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger), options, reasons)); + Collections.singletonMap("num_size", bigInteger), reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); + Collections.singletonMap("num_size", invalidFloatValue), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); + Collections.singletonMap("num_counts", infiniteNANDouble)), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)), options, reasons)); + Collections.singletonMap("num_counts", largeDouble)), reasons)); } /** @@ -295,10 +293,10 @@ public void invalidExactMatchConditionEvaluatesNull() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "exact", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "exact", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -312,10 +310,10 @@ public void exactMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "exact", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "exact", 5.55); - assertFalse(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); - assertFalse(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceString.evaluate(null, testUserAttributes, reasons)); + assertFalse(testInstanceBoolean.evaluate(null, testTypedUserAttributes, reasons)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -331,15 +329,15 @@ public void exactMatchConditionEvaluatesNull() { UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "exact", "3.55"); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "exact", "null_val"); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, reasons)); Map attr = new HashMap<>(); attr.put("browser_type", "true"); - assertNull(testInstanceString.evaluate(null, attr, options, reasons)); + assertNull(testInstanceString.evaluate(null, attr, reasons)); } /** @@ -353,13 +351,13 @@ public void gtMatchConditionEvaluatesTrue() { UserAttribute testInstanceFloat = new UserAttribute("num_size", "custom_attribute", "gt", (float) 2); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "gt", 2.55); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 3), options, reasons)); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 3), reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); Map badAttributes = new HashMap<>(); badAttributes.put("num_size", "bobs burgers"); - assertNull(testInstanceInteger.evaluate(null, badAttributes, options, reasons)); + assertNull(testInstanceInteger.evaluate(null, badAttributes, reasons)); } /** @@ -393,22 +391,22 @@ public void gtMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger), options, reasons)); + Collections.singletonMap("num_size", bigInteger), reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); + Collections.singletonMap("num_size", invalidFloatValue), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); + Collections.singletonMap("num_counts", infiniteNANDouble)), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)), options, reasons)); + Collections.singletonMap("num_counts", largeDouble)), reasons)); } /** @@ -426,10 +424,10 @@ public void gtMatchConditionEvaluatesNullWithInvalidAttr() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "gt", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "gt", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -442,8 +440,8 @@ public void gtMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "gt", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "gt", 5.55); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -457,10 +455,10 @@ public void gtMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "gt", 3.5); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "gt", 3.5); - assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, reasons)); } @@ -475,13 +473,13 @@ public void geMatchConditionEvaluatesTrue() { UserAttribute testInstanceFloat = new UserAttribute("num_size", "custom_attribute", "ge", (float) 2); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "ge", 2.55); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 2), options, reasons)); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertTrue(testInstanceFloat.evaluate(null, Collections.singletonMap("num_size", (float) 2), reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); Map badAttributes = new HashMap<>(); badAttributes.put("num_size", "bobs burgers"); - assertNull(testInstanceInteger.evaluate(null, badAttributes, options, reasons)); + assertNull(testInstanceInteger.evaluate(null, badAttributes, reasons)); } /** @@ -515,22 +513,22 @@ public void geMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger), options, reasons)); + Collections.singletonMap("num_size", bigInteger), reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); + Collections.singletonMap("num_size", invalidFloatValue), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); + Collections.singletonMap("num_counts", infiniteNANDouble)), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)), options, reasons)); + Collections.singletonMap("num_counts", largeDouble)), reasons)); } /** @@ -548,10 +546,10 @@ public void geMatchConditionEvaluatesNullWithInvalidAttr() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "ge", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "ge", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -564,8 +562,8 @@ public void geMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "ge", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "ge", 5.55); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -579,10 +577,10 @@ public void geMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "ge", 3.5); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "ge", 3.5); - assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, reasons)); } @@ -596,8 +594,8 @@ public void ltMatchConditionEvaluatesTrue() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "lt", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "lt", 5.55); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertTrue(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -610,8 +608,8 @@ public void ltMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "lt", 2); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "lt", 2.55); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -625,10 +623,10 @@ public void ltMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "lt", 3.5); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "lt", 3.5); - assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -662,22 +660,22 @@ public void ltMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger), options, reasons)); + Collections.singletonMap("num_size", bigInteger), reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); + Collections.singletonMap("num_size", invalidFloatValue), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); + Collections.singletonMap("num_counts", infiniteNANDouble)), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)), options, reasons)); + Collections.singletonMap("num_counts", largeDouble)), reasons)); } /** @@ -695,10 +693,10 @@ public void ltMatchConditionEvaluatesNullWithInvalidAttributes() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "lt", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "lt", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, reasons)); } @@ -712,8 +710,8 @@ public void leMatchConditionEvaluatesTrue() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "le", 5); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "le", 5.55); - assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertTrue(testInstanceDouble.evaluate(null, Collections.singletonMap("num_counts", 5.55), options, reasons)); + assertTrue(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertTrue(testInstanceDouble.evaluate(null, Collections.singletonMap("num_counts", 5.55), reasons)); } /** @@ -726,8 +724,8 @@ public void leMatchConditionEvaluatesFalse() { UserAttribute testInstanceInteger = new UserAttribute("num_size", "custom_attribute", "le", 2); UserAttribute testInstanceDouble = new UserAttribute("num_counts", "custom_attribute", "le", 2.55); - assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertFalse(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertFalse(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -741,10 +739,10 @@ public void leMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "le", 3.5); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "le", 3.5); - assertNull(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testUserAttributes, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -778,22 +776,22 @@ public void leMatchConditionEvaluatesNullWithInvalidUserAttr() { assertNull(testInstanceInteger.evaluate( null, - Collections.singletonMap("num_size", bigInteger), options, reasons)); + Collections.singletonMap("num_size", bigInteger), reasons)); assertNull(testInstanceFloat.evaluate( null, - Collections.singletonMap("num_size", invalidFloatValue), options, reasons)); + Collections.singletonMap("num_size", invalidFloatValue), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infinitePositiveInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, - Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), options, reasons)); + Collections.singletonMap("num_counts", infiniteNegativeInfiniteDouble), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", infiniteNANDouble)), options, reasons)); + Collections.singletonMap("num_counts", infiniteNANDouble)), reasons)); assertNull(testInstanceDouble.evaluate( null, Collections.singletonMap("num_counts", - Collections.singletonMap("num_counts", largeDouble)), options, reasons)); + Collections.singletonMap("num_counts", largeDouble)), reasons)); } /** @@ -811,10 +809,10 @@ public void leMatchConditionEvaluatesNullWithInvalidAttributes() { UserAttribute testInstanceNegativeInfiniteDouble = new UserAttribute("num_counts", "custom_attribute", "le", infiniteNegativeInfiniteDouble); UserAttribute testInstanceNANDouble = new UserAttribute("num_counts", "custom_attribute", "le", infiniteNANDouble); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstancePositiveInfinite.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNegativeInfiniteDouble.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNANDouble.evaluate(null, testTypedUserAttributes, reasons)); } /** @@ -824,7 +822,7 @@ public void leMatchConditionEvaluatesNullWithInvalidAttributes() { @Test public void substringMatchConditionEvaluatesTrue() { UserAttribute testInstanceString = new UserAttribute("browser_type", "custom_attribute", "substring", "chrome"); - assertTrue(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testUserAttributes, reasons)); } /** @@ -834,7 +832,7 @@ public void substringMatchConditionEvaluatesTrue() { @Test public void substringMatchConditionPartialMatchEvaluatesTrue() { UserAttribute testInstanceString = new UserAttribute("browser_type", "custom_attribute", "substring", "chro"); - assertTrue(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testUserAttributes, reasons)); } /** @@ -844,7 +842,7 @@ public void substringMatchConditionPartialMatchEvaluatesTrue() { @Test public void substringMatchConditionEvaluatesFalse() { UserAttribute testInstanceString = new UserAttribute("browser_type", "custom_attribute", "substring", "chr0me"); - assertFalse(testInstanceString.evaluate(null, testUserAttributes, options, reasons)); + assertFalse(testInstanceString.evaluate(null, testUserAttributes, reasons)); } /** @@ -859,11 +857,11 @@ public void substringMatchConditionEvaluatesNull() { UserAttribute testInstanceObject = new UserAttribute("meta_data", "custom_attribute", "substring", "chrome1"); UserAttribute testInstanceNull = new UserAttribute("null_val", "custom_attribute", "substring", "chrome1"); - assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceDouble.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, options, reasons)); - assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, options, reasons)); + assertNull(testInstanceBoolean.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceInteger.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceDouble.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceObject.evaluate(null, testTypedUserAttributes, reasons)); + assertNull(testInstanceNull.evaluate(null, testTypedUserAttributes, reasons)); } //======== Semantic version evaluation tests ========// @@ -874,7 +872,7 @@ public void testSemanticVersionEqualsMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", 2.0); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testAttributes, reasons)); } @Test @@ -882,7 +880,7 @@ public void semanticVersionInvalidMajorShouldBeNumberOnly() { Map testAttributes = new HashMap(); testAttributes.put("version", "a.1.2"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testAttributes, reasons)); } @Test @@ -890,7 +888,7 @@ public void semanticVersionInvalidMinorShouldBeNumberOnly() { Map testAttributes = new HashMap(); testAttributes.put("version", "1.b.2"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testAttributes, reasons)); } @Test @@ -898,7 +896,7 @@ public void semanticVersionInvalidPatchShouldBeNumberOnly() { Map testAttributes = new HashMap(); testAttributes.put("version", "1.2.c"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test SemanticVersionEqualsMatch returns null if given invalid UserCondition Variable type @@ -907,7 +905,7 @@ public void testSemanticVersionEqualsMatchInvalidUserConditionVariable() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.0"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", 2.0); - assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test SemanticVersionGTMatch returns null if given invalid value type @@ -916,7 +914,7 @@ public void testSemanticVersionGTMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", false); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test SemanticVersionGEMatch returns null if given invalid value type @@ -925,7 +923,7 @@ public void testSemanticVersionGEMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", 2); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_ge", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test SemanticVersionLTMatch returns null if given invalid value type @@ -934,7 +932,7 @@ public void testSemanticVersionLTMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", 2); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_lt", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test SemanticVersionLEMatch returns null if given invalid value type @@ -943,7 +941,7 @@ public void testSemanticVersionLEMatchInvalidInput() { Map testAttributes = new HashMap(); testAttributes.put("version", 2); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_le", "2.0.0"); - assertNull(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertNull(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test if not same when targetVersion is only major.minor.patch and version is major.minor @@ -952,7 +950,7 @@ public void testIsSemanticNotSameConditionValueMajorMinorPatch() { Map testAttributes = new HashMap(); testAttributes.put("version", "1.2"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "1.2.0"); - assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertFalse(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test if same when target is only major but user condition checks only major.minor,patch @@ -961,7 +959,7 @@ public void testIsSemanticSameSingleDigit() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.0.0"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "3"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test if greater when User value patch is greater even when its beta @@ -970,7 +968,7 @@ public void testIsSemanticGreaterWhenUserConditionComparesMajorMinorAndPatchVers Map testAttributes = new HashMap(); testAttributes.put("version", "3.1.1-beta"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "3.1.0"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test if greater when preRelease is greater alphabetically @@ -979,7 +977,7 @@ public void testIsSemanticGreaterWhenMajorMinorPatchReleaseVersionCharacter() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.1.1-beta.y.1+1.1"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "3.1.1-beta.x.1+1.1"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test if greater when preRelease version number is greater @@ -988,7 +986,7 @@ public void testIsSemanticGreaterWhenMajorMinorPatchPreReleaseVersionNum() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.1.1-beta.x.2+1.1"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "3.1.1-beta.x.1+1.1"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test if equals semantic version even when only same preRelease is passed in user attribute and no build meta @@ -997,7 +995,7 @@ public void testIsSemanticEqualWhenMajorMinorPatchPreReleaseVersionNum() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.1.1-beta.x.1"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "3.1.1-beta.x.1"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test if not same @@ -1006,7 +1004,7 @@ public void testIsSemanticNotSameReturnsFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.2"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.1.1"); - assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertFalse(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test when target is full semantic version major.minor.patch @@ -1015,7 +1013,7 @@ public void testIsSemanticSameFull() { Map testAttributes = new HashMap(); testAttributes.put("version", "3.0.1"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "3.0.1"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare less when user condition checks only major.minor @@ -1024,7 +1022,7 @@ public void testIsSemanticLess() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.6"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_lt", "2.2"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // When user condition checks major.minor but target is major.minor.patch then its equals @@ -1033,7 +1031,7 @@ public void testIsSemanticLessFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.0"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_lt", "2.1"); - assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertFalse(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare less when target is full major.minor.patch @@ -1042,7 +1040,7 @@ public void testIsSemanticFullLess() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.6"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_lt", "2.1.9"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare greater when user condition checks only major.minor @@ -1051,7 +1049,7 @@ public void testIsSemanticMore() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.3.6"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.2"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare greater when both are major.minor.patch-beta but target is greater than user condition @@ -1060,7 +1058,7 @@ public void testIsSemanticMoreWhenBeta() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.3.6-beta"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.3.5-beta"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare greater when target is major.minor.patch @@ -1069,7 +1067,7 @@ public void testIsSemanticFullMore() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.7"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.1.6"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare greater when target is major.minor.patch is smaller then it returns false @@ -1078,7 +1076,7 @@ public void testSemanticVersionGTFullMoreReturnsFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.1.10"); - assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertFalse(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare equal when both are exactly same - major.minor.patch-beta @@ -1087,7 +1085,7 @@ public void testIsSemanticFullEqual() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9-beta"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_eq", "2.1.9-beta"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare equal when both major.minor.patch is same, but due to beta user condition is smaller @@ -1096,7 +1094,7 @@ public void testIsSemanticLessWhenBeta() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.1.9-beta"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare greater when target is major.minor.patch-beta and user condition only compares major.minor.patch @@ -1105,7 +1103,7 @@ public void testIsSemanticGreaterBeta() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_gt", "2.1.9-beta"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare equal when target is major.minor.patch @@ -1114,7 +1112,7 @@ public void testIsSemanticLessEqualsWhenEqualsReturnsTrue() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_le", "2.1.9"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare less when target is major.minor.patch @@ -1123,7 +1121,7 @@ public void testIsSemanticLessEqualsWhenLessReturnsTrue() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.132.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_le", "2.233.91"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare less when target is major.minor.patch @@ -1132,7 +1130,7 @@ public void testIsSemanticLessEqualsWhenGreaterReturnsFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.233.91"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_le", "2.132.009"); - assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertFalse(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare equal when target is major.minor.patch @@ -1141,7 +1139,7 @@ public void testIsSemanticGreaterEqualsWhenEqualsReturnsTrue() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.1.9"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_ge", "2.1.9"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare less when target is major.minor.patch @@ -1150,7 +1148,7 @@ public void testIsSemanticGreaterEqualsWhenLessReturnsTrue() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.233.91"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_ge", "2.132.9"); - assertTrue(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertTrue(testInstanceString.evaluate(null, testAttributes, reasons)); } // Test compare less when target is major.minor.patch @@ -1159,7 +1157,7 @@ public void testIsSemanticGreaterEqualsWhenLessReturnsFalse() { Map testAttributes = new HashMap(); testAttributes.put("version", "2.132.009"); UserAttribute testInstanceString = new UserAttribute("version", "custom_attribute", "semver_ge", "2.233.91"); - assertFalse(testInstanceString.evaluate(null, testAttributes, options, reasons)); + assertFalse(testInstanceString.evaluate(null, testAttributes, reasons)); } /** @@ -1168,7 +1166,7 @@ public void testIsSemanticGreaterEqualsWhenLessReturnsFalse() { @Test public void notConditionEvaluateNull() { NotCondition notCondition = new NotCondition(new NullCondition()); - assertNull(notCondition.evaluate(null, testUserAttributes, options, reasons)); + assertNull(notCondition.evaluate(null, testUserAttributes, reasons)); } /** @@ -1177,11 +1175,11 @@ public void notConditionEvaluateNull() { @Test public void notConditionEvaluateTrue() { UserAttribute userAttribute = mock(UserAttribute.class); - when(userAttribute.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); + when(userAttribute.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(false); NotCondition notCondition = new NotCondition(userAttribute); - assertTrue(notCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(userAttribute, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertTrue(notCondition.evaluate(null, testUserAttributes, reasons)); + verify(userAttribute, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1190,11 +1188,11 @@ public void notConditionEvaluateTrue() { @Test public void notConditionEvaluateFalse() { UserAttribute userAttribute = mock(UserAttribute.class); - when(userAttribute.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); + when(userAttribute.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(true); NotCondition notCondition = new NotCondition(userAttribute); - assertFalse(notCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(userAttribute, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertFalse(notCondition.evaluate(null, testUserAttributes, reasons)); + verify(userAttribute, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1203,20 +1201,20 @@ public void notConditionEvaluateFalse() { @Test public void orConditionEvaluateTrue() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(true); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(userAttribute1); conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertTrue(orCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertTrue(orCondition.evaluate(null, testUserAttributes, reasons)); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation - verify(userAttribute2, times(0)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + verify(userAttribute2, times(0)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1225,20 +1223,20 @@ public void orConditionEvaluateTrue() { @Test public void orConditionEvaluateTrueWithNullAndTrue() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(null); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(null); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(true); List conditions = new ArrayList(); conditions.add(userAttribute1); conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertTrue(orCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertTrue(orCondition.evaluate(null, testUserAttributes, reasons)); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation - verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1247,20 +1245,20 @@ public void orConditionEvaluateTrueWithNullAndTrue() { @Test public void orConditionEvaluateNullWithNullAndFalse() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(null); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(null); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(userAttribute1); conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertNull(orCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertNull(orCondition.evaluate(null, testUserAttributes, reasons)); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation - verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1269,20 +1267,20 @@ public void orConditionEvaluateNullWithNullAndFalse() { @Test public void orConditionEvaluateFalseWithFalseAndFalse() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(false); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(userAttribute1); conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertFalse(orCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertFalse(orCondition.evaluate(null, testUserAttributes, reasons)); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); // shouldn't be called due to short-circuiting in 'Or' evaluation - verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1291,19 +1289,19 @@ public void orConditionEvaluateFalseWithFalseAndFalse() { @Test public void orConditionEvaluateFalse() { UserAttribute userAttribute1 = mock(UserAttribute.class); - when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); + when(userAttribute1.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(false); UserAttribute userAttribute2 = mock(UserAttribute.class); - when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); + when(userAttribute2.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(userAttribute1); conditions.add(userAttribute2); OrCondition orCondition = new OrCondition(conditions); - assertFalse(orCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); - verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertFalse(orCondition.evaluate(null, testUserAttributes, reasons)); + verify(userAttribute1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); + verify(userAttribute2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1312,19 +1310,19 @@ public void orConditionEvaluateFalse() { @Test public void andConditionEvaluateTrue() { OrCondition orCondition1 = mock(OrCondition.class); - when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); + when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(true); OrCondition orCondition2 = mock(OrCondition.class); - when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); + when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(true); List conditions = new ArrayList(); conditions.add(orCondition1); conditions.add(orCondition2); AndCondition andCondition = new AndCondition(conditions); - assertTrue(andCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); - verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertTrue(andCondition.evaluate(null, testUserAttributes, reasons)); + verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); + verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1333,19 +1331,19 @@ public void andConditionEvaluateTrue() { @Test public void andConditionEvaluateFalseWithNullAndFalse() { OrCondition orCondition1 = mock(OrCondition.class); - when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(null); + when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(null); OrCondition orCondition2 = mock(OrCondition.class); - when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); + when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(false); List conditions = new ArrayList(); conditions.add(orCondition1); conditions.add(orCondition2); AndCondition andCondition = new AndCondition(conditions); - assertFalse(andCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); - verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertFalse(andCondition.evaluate(null, testUserAttributes, reasons)); + verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); + verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1354,19 +1352,19 @@ public void andConditionEvaluateFalseWithNullAndFalse() { @Test public void andConditionEvaluateNullWithNullAndTrue() { OrCondition orCondition1 = mock(OrCondition.class); - when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(null); + when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(null); OrCondition orCondition2 = mock(OrCondition.class); - when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); + when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(true); List conditions = new ArrayList(); conditions.add(orCondition1); conditions.add(orCondition2); AndCondition andCondition = new AndCondition(conditions); - assertNull(andCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); - verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertNull(andCondition.evaluate(null, testUserAttributes, reasons)); + verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); + verify(orCondition2, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); } /** @@ -1375,10 +1373,10 @@ public void andConditionEvaluateNullWithNullAndTrue() { @Test public void andConditionEvaluateFalse() { OrCondition orCondition1 = mock(OrCondition.class); - when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(false); + when(orCondition1.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(false); OrCondition orCondition2 = mock(OrCondition.class); - when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject())).thenReturn(true); + when(orCondition2.evaluate(eq(null), eq(testUserAttributes), anyObject())).thenReturn(true); // and[false, true] List conditions = new ArrayList(); @@ -1386,13 +1384,13 @@ public void andConditionEvaluateFalse() { conditions.add(orCondition2); AndCondition andCondition = new AndCondition(conditions); - assertFalse(andCondition.evaluate(null, testUserAttributes, options, reasons)); - verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + assertFalse(andCondition.evaluate(null, testUserAttributes, reasons)); + verify(orCondition1, times(1)).evaluate(eq(null), eq(testUserAttributes), anyObject()); // shouldn't be called due to short-circuiting in 'And' evaluation - verify(orCondition2, times(0)).evaluate(eq(null), eq(testUserAttributes), anyObject(), anyObject()); + verify(orCondition2, times(0)).evaluate(eq(null), eq(testUserAttributes), anyObject()); OrCondition orCondition3 = mock(OrCondition.class); - when(orCondition3.evaluate(null, testUserAttributes, options, reasons)).thenReturn(null); + when(orCondition3.evaluate(null, testUserAttributes, reasons)).thenReturn(null); // and[null, false] List conditions2 = new ArrayList(); @@ -1400,7 +1398,7 @@ public void andConditionEvaluateFalse() { conditions2.add(orCondition1); AndCondition andCondition2 = new AndCondition(conditions2); - assertFalse(andCondition2.evaluate(null, testUserAttributes, options, reasons)); + assertFalse(andCondition2.evaluate(null, testUserAttributes, reasons)); // and[true, false, null] List conditions3 = new ArrayList(); @@ -1409,7 +1407,7 @@ public void andConditionEvaluateFalse() { conditions3.add(orCondition1); AndCondition andCondition3 = new AndCondition(conditions3); - assertFalse(andCondition3.evaluate(null, testUserAttributes, options, reasons)); + assertFalse(andCondition3.evaluate(null, testUserAttributes, reasons)); } /** @@ -1441,8 +1439,8 @@ public void nullValueEvaluate() { attributeValue ); - assertNull(nullValueAttribute.evaluate(null, Collections.emptyMap(), options, reasons)); - assertNull(nullValueAttribute.evaluate(null, Collections.singletonMap(attributeName, attributeValue), options, reasons)); - assertNull(nullValueAttribute.evaluate(null, (Collections.singletonMap(attributeName, "")), options, reasons)); + assertNull(nullValueAttribute.evaluate(null, Collections.emptyMap(), reasons)); + assertNull(nullValueAttribute.evaluate(null, Collections.singletonMap(attributeName, attributeValue), reasons)); + assertNull(nullValueAttribute.evaluate(null, (Collections.singletonMap(attributeName, "")), reasons)); } } From 4c68f745b216af5ca0c2f3c473f82a0cfa605431 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 30 Oct 2020 17:05:23 -0700 Subject: [PATCH 31/44] add DecisionReasons interface --- .../java/com/optimizely/ab/Optimizely.java | 2 +- .../com/optimizely/ab/bucketing/Bucketer.java | 3 +- .../ab/bucketing/DecisionService.java | 17 +++--- .../ab/config/audience/NullCondition.java | 2 - .../ab/internal/ExperimentUtils.java | 5 +- .../optimizelydecision/DecisionReasons.java | 38 ++----------- .../DefaultDecisionReasons.java | 54 ++++++++++++++++++ .../ErrorsDecisionReasons.java | 56 +++++++++++++++++++ .../AudienceConditionEvaluationTest.java | 3 +- 9 files changed, 131 insertions(+), 49 deletions(-) create mode 100644 core-api/src/main/java/com/optimizely/ab/optimizelydecision/DefaultDecisionReasons.java create mode 100644 core-api/src/main/java/com/optimizely/ab/optimizelydecision/ErrorsDecisionReasons.java diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 836906891..f1208ad36 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1172,7 +1172,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, Map attributes = user.getAttributes(); Boolean decisionEventDispatched = false; List allOptions = getAllOptions(options); - DecisionReasons decisionReasons = new DecisionReasons(allOptions); + DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions); Map copiedAttributes = new HashMap<>(attributes); FeatureDecision flagDecision = decisionService.getVariationForFeature( diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index 6d4ac9433..0429dd585 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -20,6 +20,7 @@ import com.optimizely.ab.bucketing.internal.MurmurHash3; import com.optimizely.ab.config.*; import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -170,7 +171,7 @@ public Variation bucket(@Nonnull Experiment experiment, public Variation bucket(@Nonnull Experiment experiment, @Nonnull String bucketingId, @Nonnull ProjectConfig projectConfig) { - return bucket(experiment, bucketingId, projectConfig, new DecisionReasons()); + return bucket(experiment, bucketingId, projectConfig, DefaultDecisionReasons.newInstance()); } //======== Helper methods ========// diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index 696bd7418..588f6eefb 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -21,6 +21,7 @@ import com.optimizely.ab.internal.ControlAttribute; import com.optimizely.ab.internal.ExperimentUtils; import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -170,7 +171,7 @@ public Variation getVariation(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig) { - return getVariation(experiment, userId, filteredAttributes, projectConfig, Collections.emptyList(), new DecisionReasons()); + return getVariation(experiment, userId, filteredAttributes, projectConfig, Collections.emptyList(), DefaultDecisionReasons.newInstance()); } /** @@ -223,7 +224,7 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig) { - return getVariationForFeature(featureFlag, userId, filteredAttributes, projectConfig,Collections.emptyList(), new DecisionReasons()); + return getVariationForFeature(featureFlag, userId, filteredAttributes, projectConfig,Collections.emptyList(), DefaultDecisionReasons.newInstance()); } /** @@ -296,7 +297,7 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag @Nonnull String userId, @Nonnull Map filteredAttributes, @Nonnull ProjectConfig projectConfig) { - return getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, new DecisionReasons()); + return getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes, projectConfig, DefaultDecisionReasons.newInstance()); } /** @@ -334,7 +335,7 @@ Variation getWhitelistedVariation(@Nonnull Experiment experiment, Variation getWhitelistedVariation(@Nonnull Experiment experiment, @Nonnull String userId) { - return getWhitelistedVariation(experiment, userId, new DecisionReasons()); + return getWhitelistedVariation(experiment, userId, DefaultDecisionReasons.newInstance()); } /** @@ -389,7 +390,7 @@ Variation getStoredVariation(@Nonnull Experiment experiment, Variation getStoredVariation(@Nonnull Experiment experiment, @Nonnull UserProfile userProfile, @Nonnull ProjectConfig projectConfig) { - return getStoredVariation(experiment, userProfile, projectConfig, new DecisionReasons()); + return getStoredVariation(experiment, userProfile, projectConfig, DefaultDecisionReasons.newInstance()); } /** @@ -433,7 +434,7 @@ void saveVariation(@Nonnull Experiment experiment, void saveVariation(@Nonnull Experiment experiment, @Nonnull Variation variation, @Nonnull UserProfile userProfile) { - saveVariation(experiment, variation, userProfile, new DecisionReasons()); + saveVariation(experiment, variation, userProfile, DefaultDecisionReasons.newInstance()); } /** @@ -463,7 +464,7 @@ String getBucketingId(@Nonnull String userId, String getBucketingId(@Nonnull String userId, @Nonnull Map filteredAttributes) { - return getBucketingId(userId, filteredAttributes, new DecisionReasons()); + return getBucketingId(userId, filteredAttributes, DefaultDecisionReasons.newInstance()); } public ConcurrentHashMap> getForcedVariationMapping() { @@ -578,7 +579,7 @@ public Variation getForcedVariation(@Nonnull Experiment experiment, @Nullable public Variation getForcedVariation(@Nonnull Experiment experiment, @Nonnull String userId) { - return getForcedVariation(experiment, userId, new DecisionReasons()); + return getForcedVariation(experiment, userId, DefaultDecisionReasons.newInstance()); } /** diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java index 102828ce8..ef76d92ad 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/NullCondition.java @@ -17,10 +17,8 @@ import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.optimizelydecision.DecisionReasons; -import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import javax.annotation.Nullable; -import java.util.List; import java.util.Map; public class NullCondition implements Condition { diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java index 04e281ccb..e24cc1b6a 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java @@ -22,6 +22,7 @@ import com.optimizely.ab.config.audience.Condition; import com.optimizely.ab.config.audience.OrCondition; import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,7 +59,7 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment, } public static boolean isExperimentActive(@Nonnull Experiment experiment) { - return isExperimentActive(experiment, new DecisionReasons()); + return isExperimentActive(experiment, DefaultDecisionReasons.newInstance()); } /** @@ -103,7 +104,7 @@ public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig proj @Nonnull Map attributes, @Nonnull String loggingEntityType, @Nonnull String loggingKey) { - return doesUserMeetAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, new DecisionReasons()); + return doesUserMeetAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey, DefaultDecisionReasons.newInstance()); } @Nullable diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java index fd2e74c7d..0983ee4d2 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DecisionReasons.java @@ -16,44 +16,14 @@ */ package com.optimizely.ab.optimizelydecision; -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -public class DecisionReasons { +public interface DecisionReasons { - private final List errors = new ArrayList<>(); - private final List logs = new ArrayList<>(); - private boolean includeReasons; + public void addError(String format, Object... args); - public DecisionReasons(@Nonnull List options) { - this.includeReasons = options.contains(OptimizelyDecideOption.INCLUDE_REASONS); - } + public String addInfo(String format, Object... args); - public DecisionReasons() { - this(Collections.emptyList()); - } - - public void addError(String format, Object... args) { - String message = String.format(format, args); - errors.add(message); - } - - public String addInfo(String format, Object... args) { - String message = String.format(format, args); - if (includeReasons) { - logs.add(message); - } - return message; - } - - public List toReport() { - List reasons = new ArrayList<>(errors); - if (includeReasons) { - reasons.addAll(logs); - } - return reasons; - } + public List toReport(); } diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DefaultDecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DefaultDecisionReasons.java new file mode 100644 index 000000000..18dc841d7 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DefaultDecisionReasons.java @@ -0,0 +1,54 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.optimizelydecision; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +public class DefaultDecisionReasons implements DecisionReasons { + + private final List errors = new ArrayList<>(); + private final List logs = new ArrayList<>(); + + public static DecisionReasons newInstance(@Nullable List options) { + if (options != null && options.contains(OptimizelyDecideOption.INCLUDE_REASONS)) return new DefaultDecisionReasons(); + else return new ErrorsDecisionReasons(); + } + + public static DecisionReasons newInstance() { + return newInstance(null); + } + + public void addError(String format, Object... args) { + String message = String.format(format, args); + errors.add(message); + } + + public String addInfo(String format, Object... args) { + String message = String.format(format, args); + logs.add(message); + return message; + } + + public List toReport() { + List reasons = new ArrayList<>(errors); + reasons.addAll(logs); + return reasons; + } + +} diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/ErrorsDecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/ErrorsDecisionReasons.java new file mode 100644 index 000000000..91875eece --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/ErrorsDecisionReasons.java @@ -0,0 +1,56 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.optimizelydecision; + +import java.util.ArrayList; +import java.util.List; + +public class ErrorsDecisionReasons implements DecisionReasons { + + private final List errors = new ArrayList<>(); + + public void addError(String format, Object... args) { + String message = String.format(format, args); + errors.add(message); + } + + public String addInfo(String format, Object... args) { + // skip tracking and pass-through reasons other than critical errors. + return String.format(format, args); + } + + public List toReport() { + return new ArrayList<>(errors); + } + +} diff --git a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java index ea5b3edd9..216099298 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/audience/AudienceConditionEvaluationTest.java @@ -19,6 +19,7 @@ import ch.qos.logback.classic.Level; import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Before; import org.junit.Rule; @@ -43,7 +44,7 @@ public class AudienceConditionEvaluationTest { Map testUserAttributes; Map testTypedUserAttributes; - DecisionReasons reasons = new DecisionReasons(); + DecisionReasons reasons = DefaultDecisionReasons.newInstance(); @Before public void initialize() { From 0fbe851bca073627d2985e12018490503f4b7ff8 Mon Sep 17 00:00:00 2001 From: Jae Kim <45045038+jaeopt@users.noreply.github.com> Date: Mon, 2 Nov 2020 09:42:18 -0800 Subject: [PATCH 32/44] Update core-api/src/main/java/com/optimizely/ab/Optimizely.java Co-authored-by: mnoman09 --- core-api/src/main/java/com/optimizely/ab/Optimizely.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 9ab00b7af..3390d7bb7 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1295,7 +1295,9 @@ Map decideAll(@Nonnull OptimizelyUserContext user, private List getAllOptions(List options) { List copiedOptions = new ArrayList(defaultDecideOptions); - copiedOptions.addAll(options); + if (options != null) { + copiedOptions.addAll(options); + } return copiedOptions; } From b3165f6283763d74a51ee7dff12f21f6d929c095 Mon Sep 17 00:00:00 2001 From: Jae Kim <45045038+jaeopt@users.noreply.github.com> Date: Mon, 2 Nov 2020 09:43:01 -0800 Subject: [PATCH 33/44] Update core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java Co-authored-by: mnoman09 --- .../main/java/com/optimizely/ab/OptimizelyUserContext.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java index 69dac0fb9..b71bdacab 100644 --- a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java @@ -45,7 +45,11 @@ public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull Map attributes) { this.optimizely = optimizely; this.userId = userId; - this.attributes = Collections.synchronizedMap(new HashMap<>(attributes)); + if (attributes != null) { + this.attributes = Collections.synchronizedMap(new HashMap<>(attributes)); + } else { + this.attributes = Collections.synchronizedMap(new HashMap<>()); + } } public OptimizelyUserContext(@Nonnull Optimizely optimizely, @Nonnull String userId) { From de508fb0d74528095b32ec78423f0df55ae4a704 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 4 Nov 2020 17:06:09 -0800 Subject: [PATCH 34/44] fix convenience methods in user context --- .../main/java/com/optimizely/ab/OptimizelyUserContext.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java index b71bdacab..300d50d6f 100644 --- a/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java +++ b/core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java @@ -99,7 +99,7 @@ public OptimizelyDecision decide(@Nonnull String key, * @return A decision result. */ public OptimizelyDecision decide(@Nonnull String key) { - return optimizely.decide(this, key, Collections.emptyList()); + return decide(key, Collections.emptyList()); } /** @@ -124,7 +124,7 @@ public Map decideForKeys(@Nonnull List keys, * @return All decision results mapped by flag keys. */ public Map decideForKeys(@Nonnull List keys) { - return optimizely.decideForKeys(this, keys, Collections.emptyList()); + return decideForKeys(keys, Collections.emptyList()); } /** @@ -143,7 +143,7 @@ public Map decideAll(@Nonnull List decideAll() { - return optimizely.decideAll(this, Collections.emptyList()); + return decideAll(Collections.emptyList()); } /** From dd47439b18342d1336e75d755689346fceb34878 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 12 Nov 2020 16:57:52 -0800 Subject: [PATCH 35/44] add featureEnabled to metadata for decide-api --- core-api/src/main/java/com/optimizely/ab/Optimizely.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 3d2ce8845..983428121 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1222,7 +1222,8 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, copiedAttributes, flagDecision.variation, key, - decisionSource.toString()); + decisionSource.toString(), + flagEnabled); decisionEventDispatched = true; } From af0f1f9e7a1a9fd404ba7eaa120cb71d787fc851 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 16 Nov 2020 17:46:53 -0800 Subject: [PATCH 36/44] add metadata validation for decide-api --- .../internal/payload/DecisionMetadata.java | 14 +++++++ .../com/optimizely/ab/EventHandlerRule.java | 41 +++++++++++++++---- .../ab/OptimizelyUserContextTest.java | 13 +++++- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/DecisionMetadata.java b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/DecisionMetadata.java index 8189dae72..aec6cdce2 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/DecisionMetadata.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/DecisionMetadata.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.optimizely.ab.annotations.VisibleForTesting; +import java.util.StringJoiner; + public class DecisionMetadata { @JsonProperty("flag_key") @@ -87,6 +89,18 @@ public int hashCode() { return result; } + @Override + public String toString() { + return new StringJoiner(", ", DecisionMetadata.class.getSimpleName() + "[", "]") + .add("flagKey='" + flagKey + "'") + .add("ruleKey='" + ruleKey + "'") + .add("ruleType='" + ruleType + "'") + .add("variationKey='" + variationKey + "'") + .add("enabled=" + enabled) + .toString(); + } + + public static class Builder { private String ruleType; diff --git a/core-api/src/test/java/com/optimizely/ab/EventHandlerRule.java b/core-api/src/test/java/com/optimizely/ab/EventHandlerRule.java index 4245a8f8a..577a1891d 100644 --- a/core-api/src/test/java/com/optimizely/ab/EventHandlerRule.java +++ b/core-api/src/test/java/com/optimizely/ab/EventHandlerRule.java @@ -28,9 +28,7 @@ import java.util.stream.Collectors; import static com.optimizely.ab.config.ProjectConfig.RESERVED_ATTRIBUTE_PREFIX; -import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; /** * EventHandlerRule is a JUnit rule that implements an Optimizely {@link EventHandler}. @@ -108,9 +106,14 @@ public void expectImpression(String experientId, String variationId, String user } public void expectImpression(String experientId, String variationId, String userId, Map attributes) { - expect(experientId, variationId, IMPRESSION_EVENT_NAME, userId, attributes, null); + expectImpression(experientId, variationId, userId, attributes, null); } + public void expectImpression(String experientId, String variationId, String userId, Map attributes, DecisionMetadata metadata) { + expect(experientId, variationId, IMPRESSION_EVENT_NAME, userId, attributes, null, metadata); + } + + public void expectConversion(String eventName, String userId) { expectConversion(eventName, userId, Collections.emptyMap()); } @@ -124,11 +127,17 @@ public void expectConversion(String eventName, String userId, Map att } public void expect(String experientId, String variationId, String eventName, String userId, - Map attributes, Map tags) { - CanonicalEvent expectedEvent = new CanonicalEvent(experientId, variationId, eventName, userId, attributes, tags); + Map attributes, Map tags, DecisionMetadata metadata) { + CanonicalEvent expectedEvent = new CanonicalEvent(experientId, variationId, eventName, userId, attributes, tags, metadata); expectedEvents.add(expectedEvent); } + public void expect(String experientId, String variationId, String eventName, String userId, + Map attributes, Map tags) { + expect(experientId, variationId, eventName, userId, attributes, tags, null); + } + + @Override public void dispatchEvent(LogEvent logEvent) { logger.info("Receiving event: {}", logEvent); @@ -161,7 +170,8 @@ public void dispatchEvent(LogEvent logEvent) { visitor.getAttributes().stream() .filter(attribute -> !attribute.getKey().startsWith(RESERVED_ATTRIBUTE_PREFIX)) .collect(Collectors.toMap(Attribute::getKey, Attribute::getValue)), - event.getTags() + event.getTags(), + decision.getMetadata() ); logger.info("Adding dispatched, event: {}", actual); @@ -179,33 +189,45 @@ private static class CanonicalEvent { private String visitorId; private Map attributes; private Map tags; + private DecisionMetadata metadata; public CanonicalEvent(String experimentId, String variationId, String eventName, - String visitorId, Map attributes, Map tags) { + String visitorId, Map attributes, Map tags, + DecisionMetadata metadata) { this.experimentId = experimentId; this.variationId = variationId; this.eventName = eventName; this.visitorId = visitorId; this.attributes = attributes; this.tags = tags; + this.metadata = metadata; + } + + public CanonicalEvent(String experimentId, String variationId, String eventName, + String visitorId, Map attributes, Map tags) { + this(experimentId, variationId, eventName, visitorId, attributes, tags, null); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + CanonicalEvent that = (CanonicalEvent) o; + + boolean isMetaDataEqual = (metadata == null) || Objects.equals(metadata, that.metadata); return Objects.equals(experimentId, that.experimentId) && Objects.equals(variationId, that.variationId) && Objects.equals(eventName, that.eventName) && Objects.equals(visitorId, that.visitorId) && Objects.equals(attributes, that.attributes) && - Objects.equals(tags, that.tags); + Objects.equals(tags, that.tags) && + isMetaDataEqual; } @Override public int hashCode() { - return Objects.hash(experimentId, variationId, eventName, visitorId, attributes, tags); + return Objects.hash(experimentId, variationId, eventName, visitorId, attributes, tags, metadata); } @Override @@ -217,6 +239,7 @@ public String toString() { .add("visitorId='" + visitorId + "'") .add("attributes=" + attributes) .add("tags=" + tags) + .add("metadata=" + metadata) .toString(); } } diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index d44a59396..10e31bbac 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -18,10 +18,12 @@ import com.google.common.base.Charsets; import com.google.common.io.Resources; +import com.optimizely.ab.bucketing.FeatureDecision; import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.config.*; import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.event.ForwardingEventProcessor; +import com.optimizely.ab.event.internal.payload.DecisionMetadata; import com.optimizely.ab.notification.NotificationCenter; import com.optimizely.ab.optimizelydecision.DecisionMessage; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; @@ -363,11 +365,18 @@ public void decide_sendEvent() { assertEquals(decision.getVariationKey(), "variation_with_traffic"); - eventHandler.expectImpression(experimentId, variationId, userId); + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey(decision.getRuleKey()) + .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) + .setVariationKey(decision.getVariationKey()) + .setEnabled(decision.getEnabled()) + .build(); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } @Test - public void decide_doNotSendEvent() { + public void decide_doNotSendEvent_withOption() { optimizely = new Optimizely.Builder() .withDatafile(datafile) .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) From 4a35f337afc3b7dbfccba81898ab230db607a773 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 17 Nov 2020 13:22:35 -0800 Subject: [PATCH 37/44] add more tests for decide-api event validations --- .../ab/OptimizelyUserContextTest.java | 100 +++++++++++++++--- .../config/decide-project-config.json | 1 + 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 10e31bbac..725186e88 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -156,26 +156,85 @@ public void setAttribute_nullValue() { @Test public void decide() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + String flagKey = "feature_2"; + String experimentKey = "exp_no_audience"; + String variationKey = "variation_with_traffic"; + String experimentId = "10420810910"; + String variationId = "10418551353"; OptimizelyJSON variablesExpected = optimizely.getAllFeatureVariables(flagKey, userId); OptimizelyUserContext user = optimizely.createUserContext(userId); OptimizelyDecision decision = user.decide(flagKey); - assertEquals(decision.getVariationKey(), "variation_with_traffic"); + assertEquals(decision.getVariationKey(), variationKey); assertTrue(decision.getEnabled()); assertEquals(decision.getVariables().toMap(), variablesExpected.toMap()); - assertEquals(decision.getRuleKey(), "exp_no_audience"); + assertEquals(decision.getRuleKey(), experimentKey); assertEquals(decision.getFlagKey(), flagKey); assertEquals(decision.getUserContext(), user); assertTrue(decision.getReasons().isEmpty()); + + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey(experimentKey) + .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) + .setVariationKey(variationKey) + .setEnabled(true) + .build(); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); + } + + @Test + public void decide_nullVariation() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + String flagKey = "feature_3"; + OptimizelyJSON variablesExpected = new OptimizelyJSON(Collections.emptyMap()); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getVariationKey(), null); + assertFalse(decision.getEnabled()); + assertEquals(decision.getVariables().toMap(), variablesExpected.toMap()); + assertEquals(decision.getRuleKey(), null); + assertEquals(decision.getFlagKey(), flagKey); + assertEquals(decision.getUserContext(), user); + assertTrue(decision.getReasons().isEmpty()); + + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("") + .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) + .setVariationKey("") + .setEnabled(false) + .build(); + eventHandler.expectImpression(null, "", userId, Collections.emptyMap(), metadata); } // decideAll @Test public void decideAll_oneFlag() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + String flagKey = "feature_2"; + String experimentKey = "exp_no_audience"; + String variationKey = "variation_with_traffic"; + String experimentId = "10420810910"; + String variationId = "10418551353"; + List flagKeys = Arrays.asList(flagKey); OptimizelyJSON variablesExpected = optimizely.getAllFeatureVariables(flagKey, userId); @@ -186,14 +245,23 @@ public void decideAll_oneFlag() { OptimizelyDecision decision = decisions.get(flagKey); OptimizelyDecision expDecision = new OptimizelyDecision( - "variation_with_traffic", + variationKey, true, variablesExpected, - "exp_no_audience", + experimentKey, flagKey, user, Collections.emptyList()); assertEquals(decision, expDecision); + + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey(experimentKey) + .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) + .setVariationKey(variationKey) + .setEnabled(true) + .build(); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } @Test @@ -232,15 +300,21 @@ public void decideAll_twoFlags() { @Test public void decideAll_allFlags() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + String flagKey1 = "feature_1"; String flagKey2 = "feature_2"; String flagKey3 = "feature_3"; + Map attributes = Collections.singletonMap("gender", "f"); OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); OptimizelyJSON variablesExpected2 = optimizely.getAllFeatureVariables(flagKey2, userId); OptimizelyJSON variablesExpected3 = new OptimizelyJSON(Collections.emptyMap()); - OptimizelyUserContext user = optimizely.createUserContext(userId, Collections.singletonMap("gender", "f")); + OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); Map decisions = user.decideAll(); assertTrue(decisions.size() == 3); @@ -275,6 +349,10 @@ public void decideAll_allFlags() { flagKey3, user, Collections.emptyList())); + + eventHandler.expectImpression("10390977673", "10389729780", userId, attributes); + eventHandler.expectImpression("10420810910", "10418551353", userId, attributes); + eventHandler.expectImpression(null, "", userId, attributes); } @Test @@ -357,22 +435,16 @@ public void decide_sendEvent() { .build(); String flagKey = "feature_2"; + String variationKey = "variation_with_traffic"; String experimentId = "10420810910"; String variationId = "10418551353"; OptimizelyUserContext user = optimizely.createUserContext(userId); OptimizelyDecision decision = user.decide(flagKey); - assertEquals(decision.getVariationKey(), "variation_with_traffic"); + assertEquals(decision.getVariationKey(), variationKey); - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey(decision.getRuleKey()) - .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) - .setVariationKey(decision.getVariationKey()) - .setEnabled(decision.getEnabled()) - .build(); - eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap()); } @Test diff --git a/core-api/src/test/resources/config/decide-project-config.json b/core-api/src/test/resources/config/decide-project-config.json index d6b53bdc0..eb7b0f802 100644 --- a/core-api/src/test/resources/config/decide-project-config.json +++ b/core-api/src/test/resources/config/decide-project-config.json @@ -1,5 +1,6 @@ { "version": "4", + "sendFlagDecisions": true, "rollouts": [ { "experiments": [ From 21c2a9d208587ca0adbe34eb47ae85a9681db236 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 17 Nov 2020 13:38:27 -0800 Subject: [PATCH 38/44] add more tests for decide-api event validations --- .../java/com/optimizely/ab/Optimizely.java | 1 + .../ab/OptimizelyUserContextTest.java | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 983428121..695dcd28f 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1212,6 +1212,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, List reasonsToReport = decisionReasons.toReport(); String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null; // TODO: add ruleKey values when available later. use a copy of experimentKey until then. + // add to event metadata as well (currently set to experimentKey) String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null; if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) { diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 725186e88..50c62141d 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -155,7 +155,7 @@ public void setAttribute_nullValue() { // decide @Test - public void decide() { + public void decide_featureTest() { optimizely = new Optimizely.Builder() .withDatafile(datafile) .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) @@ -189,6 +189,41 @@ public void decide() { eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } + @Test + public void decide_rollout() { + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + String flagKey = "feature_1"; + String experimentKey = "18322080788"; + String variationKey = "18257766532"; + String experimentId = "18322080788"; + String variationId = "18257766532"; + OptimizelyJSON variablesExpected = optimizely.getAllFeatureVariables(flagKey, userId); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getVariationKey(), variationKey); + assertTrue(decision.getEnabled()); + assertEquals(decision.getVariables().toMap(), variablesExpected.toMap()); + assertEquals(decision.getRuleKey(), experimentKey); + assertEquals(decision.getFlagKey(), flagKey); + assertEquals(decision.getUserContext(), user); + assertTrue(decision.getReasons().isEmpty()); + + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey(experimentKey) + .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) + .setVariationKey(variationKey) + .setEnabled(true) + .build(); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); + } + @Test public void decide_nullVariation() { optimizely = new Optimizely.Builder() From ff67de039905b2aa7074aedadfe3ed12d8f644f7 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 18 Nov 2020 16:43:45 -0800 Subject: [PATCH 39/44] fix import --- core-api/src/main/java/com/optimizely/ab/Optimizely.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 90e71b475..75979e335 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -34,7 +34,11 @@ import com.optimizely.ab.optimizelyconfig.OptimizelyConfig; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager; import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService; -import com.optimizely.ab.optimizelydecision.*; +import com.optimizely.ab.optimizelydecision.DecisionMessage; +import com.optimizely.ab.optimizelydecision.DecisionReasons; +import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons; +import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; +import com.optimizely.ab.optimizelydecision.OptimizelyDecision; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From 70373c1489dad0054d0e1bfab131db55044ddc3a Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 23 Nov 2020 16:29:27 -0800 Subject: [PATCH 40/44] change decision logs to infos --- .../ab/optimizelydecision/DefaultDecisionReasons.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DefaultDecisionReasons.java b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DefaultDecisionReasons.java index 18dc841d7..dd84d04fe 100644 --- a/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DefaultDecisionReasons.java +++ b/core-api/src/main/java/com/optimizely/ab/optimizelydecision/DefaultDecisionReasons.java @@ -23,7 +23,7 @@ public class DefaultDecisionReasons implements DecisionReasons { private final List errors = new ArrayList<>(); - private final List logs = new ArrayList<>(); + private final List infos = new ArrayList<>(); public static DecisionReasons newInstance(@Nullable List options) { if (options != null && options.contains(OptimizelyDecideOption.INCLUDE_REASONS)) return new DefaultDecisionReasons(); @@ -41,13 +41,13 @@ public void addError(String format, Object... args) { public String addInfo(String format, Object... args) { String message = String.format(format, args); - logs.add(message); + infos.add(message); return message; } public List toReport() { List reasons = new ArrayList<>(errors); - reasons.addAll(logs); + reasons.addAll(infos); return reasons; } From 342e8cae8825c3f07c87dcb3a033eb9bac093797 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 11 Dec 2020 10:19:28 -0800 Subject: [PATCH 41/44] insert errors for testing --- .../src/main/java/com/optimizely/ab/Optimizely.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 75979e335..a79c7d2dc 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1228,7 +1228,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, flagDecision.variation, key, decisionSource.toString(), - flagEnabled); + false); decisionEventDispatched = true; } @@ -1236,18 +1236,18 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, .withUserId(userId) .withAttributes(copiedAttributes) .withFlagKey(key) - .withEnabled(flagEnabled) + // .withEnabled(flagEnabled) .withVariables(variableMap) .withVariationKey(variationKey) .withRuleKey(ruleKey) - .withReasons(reasonsToReport) - .withDecisionEventDispatched(decisionEventDispatched) + // .withReasons(reasonsToReport) + // .withDecisionEventDispatched(decisionEventDispatched) .build(); notificationCenter.send(decisionNotification); return new OptimizelyDecision( - variationKey, - flagEnabled, + null, + false, optimizelyJSON, ruleKey, key, From 18d01c8c7ad342e303ac79edb461ef1f6d85032a Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 11 Dec 2020 11:26:55 -0800 Subject: [PATCH 42/44] Revert "insert errors for testing" This reverts commit 342e8cae8825c3f07c87dcb3a033eb9bac093797. --- .../src/main/java/com/optimizely/ab/Optimizely.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index a79c7d2dc..75979e335 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1228,7 +1228,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, flagDecision.variation, key, decisionSource.toString(), - false); + flagEnabled); decisionEventDispatched = true; } @@ -1236,18 +1236,18 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, .withUserId(userId) .withAttributes(copiedAttributes) .withFlagKey(key) - // .withEnabled(flagEnabled) + .withEnabled(flagEnabled) .withVariables(variableMap) .withVariationKey(variationKey) .withRuleKey(ruleKey) - // .withReasons(reasonsToReport) - // .withDecisionEventDispatched(decisionEventDispatched) + .withReasons(reasonsToReport) + .withDecisionEventDispatched(decisionEventDispatched) .build(); notificationCenter.send(decisionNotification); return new OptimizelyDecision( - null, - false, + variationKey, + flagEnabled, optimizelyJSON, ruleKey, key, From c13dd5b5cb3deb01ce4299687526e37dcde91648 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 11 Dec 2020 11:27:39 -0800 Subject: [PATCH 43/44] inject error false --- core-api/src/main/java/com/optimizely/ab/Optimizely.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 75979e335..75999f896 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1247,7 +1247,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, return new OptimizelyDecision( variationKey, - flagEnabled, + false, optimizelyJSON, ruleKey, key, From a489d341b0e7e9397ac8ca1667d273c6d81a8c77 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 11 Dec 2020 13:07:27 -0800 Subject: [PATCH 44/44] more error --- core-api/src/main/java/com/optimizely/ab/Optimizely.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 75999f896..a63f4be43 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1246,7 +1246,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, notificationCenter.send(decisionNotification); return new OptimizelyDecision( - variationKey, + null, false, optimizelyJSON, ruleKey,