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 347c27d64..0362c4be3 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -22,6 +22,9 @@ import com.optimizely.ab.config.Attribute; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.LiveVariable; +import com.optimizely.ab.config.LiveVariableUsageInstance; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.Variation; import com.optimizely.ab.config.parser.ConfigParseException; @@ -433,7 +436,57 @@ public void track(@Nonnull String eventName, @Nonnull String variableKey, @Nonnull String userId, @Nonnull Map attributes) { - return null; + return getFeatureVariableValueForType( + featureKey, + variableKey, + userId, + attributes, + LiveVariable.VariableType.STRING); + } + + @VisibleForTesting + String getFeatureVariableValueForType(@Nonnull String featureKey, + @Nonnull String variableKey, + @Nonnull String userId, + @Nonnull Map attributes, + @Nonnull LiveVariable.VariableType variableType) { + FeatureFlag featureFlag = projectConfig.getFeatureKeyMapping().get(featureKey); + if (featureFlag == null) { + logger.info("No feature flag was found for key \"" + featureKey + "\"."); + return null; + } + + LiveVariable variable = featureFlag.getVariableKeyToLiveVariableMap().get(variableKey); + if (variable == null) { + logger.info("No feature variable was found for key \"" + variableKey + "\" in feature flag \"" + + featureKey + "\"."); + return null; + } + else if (!variable.getType().equals(variableType)) { + logger.info("The feature variable \"" + variableKey + + "\" is actually of type \"" + variable.getType().toString() + + "\" type. You tried to access it as type \"" + variableType.toString() + + "\". Please use the appropriate feature variable accessor."); + return null; + } + + String variableValue = variable.getDefaultValue(); + + Variation variation = decisionService.getVariationForFeature(featureFlag, userId, attributes); + + if (variation != null) { + LiveVariableUsageInstance liveVariableUsageInstance = + variation.getVariableIdToLiveVariableUsageInstanceMap().get(variable.getId()); + variableValue = liveVariableUsageInstance.getValue(); + } + else { + logger.info("User \"" + userId + + "\" was not bucketed into any variation for feature flag \"" + featureKey + + "\". The default value is being returned." + ); + } + + return variableValue; } //======== getVariation calls ========// 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 cc754a609..8d6137321 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,6 +18,7 @@ import com.optimizely.ab.OptimizelyRuntimeException; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.Variation; import com.optimizely.ab.error.ErrorHandler; @@ -134,6 +135,33 @@ public DecisionService(@Nonnull Bucketer bucketer, return 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. + * @return null if the user is not bucketed into any variation + * {@link Variation} the user is bucketed into if the user is successfully bucketed. + */ + public @Nullable Variation getVariationForFeature(@Nonnull FeatureFlag featureFlag, + @Nonnull String userId, + @Nonnull Map filteredAttributes) { + if (!featureFlag.getExperimentIds().isEmpty()) { + for (String experimentId : featureFlag.getExperimentIds()) { + Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId); + Variation variation = this.getVariation(experiment, userId, filteredAttributes); + if (variation != null) { + return variation; + } + } + } + else { + logger.info("The feature flag \"" + featureFlag.getKey() + "\" is not used in any experiments."); + } + + return null; + } + /** * Get the variation the user has been whitelisted into. * @param experiment {@link Experiment} in which user is to be bucketed. diff --git a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java index 4e9ad423b..256472461 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java @@ -286,6 +286,10 @@ public Map> getVariationToLiveVar return variationToLiveVariableUsageInstanceMapping; } + public Map getFeatureKeyMapping() { + return featureKeyMapping; + } + @Override public String toString() { return "ProjectConfig{" + 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 4ad647457..e981704e4 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -23,10 +23,12 @@ import com.optimizely.ab.config.Attribute; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.LiveVariable; import com.optimizely.ab.config.LiveVariableUsageInstance; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.TrafficAllocation; import com.optimizely.ab.config.Variation; +import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.error.NoOpErrorHandler; import com.optimizely.ab.error.RaiseExceptionErrorHandler; @@ -74,8 +76,16 @@ import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_LAUNCHED_EXPERIMENT_KEY; import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY; import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_PAUSED_EXPERIMENT_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_MULTI_VARIATE_FEATURE; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_MULTI_VARIATE_FEATURE_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_SINGLE_VARIABLE_STRING_KEY; import static com.optimizely.ab.config.ValidProjectConfigV4.MULTIVARIATE_EXPERIMENT_FORCED_VARIATION_USER_ID_GRED; import static com.optimizely.ab.config.ValidProjectConfigV4.PAUSED_EXPERIMENT_FORCED_VARIATION_USER_ID_CONTROL; +import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_FIRST_LETTER_DEFAULT_VALUE; +import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_FIRST_LETTER_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_STRING_VARIABLE_DEFAULT_VALUE; +import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_STRING_VARIABLE_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_MULTIVARIATE_EXPERIMENT_GRED; import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_MULTIVARIATE_EXPERIMENT_GRED_KEY; import static com.optimizely.ab.event.LogEvent.RequestMethod; import static com.optimizely.ab.event.internal.EventBuilderV2Test.createExperimentVariationMap; @@ -84,17 +94,17 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.array; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasKey; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assume.assumeTrue; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -2143,6 +2153,224 @@ public void clearNotificationListeners() throws Exception { .onEventTracked(eventKey, genericUserId, attributes, null, logEventToDispatch); } + //======== Feature Accessor Tests ========// + + /** + * Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)} + * returns null and logs a message + * when it is called with a feature key that has no corresponding feature in the datafile. + * @throws ConfigParseException + */ + @Test + public void getFeatureVariableValueForTypeReturnsNullWhenFeatureNotFound() throws ConfigParseException { + + String invalidFeatureKey = "nonexistent feature key"; + String invalidVariableKey = "nonexistent variable key"; + Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) + .withConfig(validProjectConfig) + .withDecisionService(mockDecisionService) + .build(); + + String value = optimizely.getFeatureVariableValueForType( + invalidFeatureKey, + invalidVariableKey, + genericUserId, + Collections.emptyMap(), + LiveVariable.VariableType.STRING); + assertNull(value); + + value = optimizely.getFeatureVariableString(invalidFeatureKey, invalidVariableKey, genericUserId, attributes); + assertNull(value); + + logbackVerifier.expectMessage(Level.INFO, + "No feature flag was found for key \"" + invalidFeatureKey + "\".", + times(2)); + + verify(mockDecisionService, never()).getVariation( + any(Experiment.class), + anyString(), + anyMapOf(String.class, String.class)); + } + + /** + * Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)} + * returns null and logs a message + * when the feature key is valid, but no variable could be found for the variable key in the feature. + * @throws ConfigParseException + */ + @Test + public void getFeatureVariableValueForTypeReturnsNullWhenVariableNotFoundInValidFeature() throws ConfigParseException { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + + String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String invalidVariableKey = "nonexistent variable key"; + + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) + .withConfig(validProjectConfig) + .withDecisionService(mockDecisionService) + .build(); + + String value = optimizely.getFeatureVariableValueForType( + validFeatureKey, + invalidVariableKey, + genericUserId, + Collections.emptyMap(), + LiveVariable.VariableType.STRING); + assertNull(value); + + logbackVerifier.expectMessage(Level.INFO, + "No feature variable was found for key \"" + invalidVariableKey + "\" in feature flag \"" + + validFeatureKey + "\"."); + + verify(mockDecisionService, never()).getVariation( + any(Experiment.class), + anyString(), + anyMapOf(String.class, String.class) + ); + } + + /** + * Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)} + * returns null when the variable's type does not match the type with which it was attempted to be accessed. + * @throws ConfigParseException + */ + @Test + public void getFeatureVariableValueReturnsNullWhenVariableTypeDoesNotMatch() throws ConfigParseException { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + + String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String validVariableKey = VARIABLE_FIRST_LETTER_KEY; + + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) + .withConfig(validProjectConfig) + .withDecisionService(mockDecisionService) + .build(); + + String value = optimizely.getFeatureVariableValueForType( + validFeatureKey, + validVariableKey, + genericUserId, + Collections.emptyMap(), + LiveVariable.VariableType.INTEGER + ); + assertNull(value); + + logbackVerifier.expectMessage( + Level.INFO, + "The feature variable \"" + validVariableKey + + "\" is actually of type \"" + LiveVariable.VariableType.STRING.toString() + + "\" type. You tried to access it as type \"" + LiveVariable.VariableType.INTEGER.toString() + + "\". Please use the appropriate feature variable accessor." + ); + } + + /** + * Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)} + * returns the String default value of a live variable + * when the feature is not attached to an experiment. + * @throws ConfigParseException + */ + @Test + public void getFeatureVariableValueForTypeReturnsDefaultValueWhenFeatureIsNotAttached() throws ConfigParseException { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + + String validFeatureKey = FEATURE_SINGLE_VARIABLE_STRING_KEY; + String validVariableKey = VARIABLE_STRING_VARIABLE_KEY; + String defaultValue = VARIABLE_STRING_VARIABLE_DEFAULT_VALUE; + Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); + + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) + .withConfig(validProjectConfig) + .build(); + + String value = optimizely.getFeatureVariableValueForType( + validFeatureKey, + validVariableKey, + genericUserId, + attributes, + LiveVariable.VariableType.STRING); + assertEquals(defaultValue, value); + + logbackVerifier.expectMessage( + Level.INFO, + "The feature flag \"" + validFeatureKey + "\" is not used in any experiments." + ); + } + + /** + * Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)} + * returns the String default value for a live variable + * when the feature is attached to an experiment, but the user is excluded from the experiment. + * @throws ConfigParseException + */ + @Test + public void getFeatureVariableValueReturnsDefaultValueWhenFeatureIsAttachedToOneExperimentButFailsTargeting() throws ConfigParseException { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + + String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String validVariableKey = VARIABLE_FIRST_LETTER_KEY; + String expectedValue = VARIABLE_FIRST_LETTER_DEFAULT_VALUE; + + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) + .withConfig(validProjectConfig) + .build(); + + String valueWithImproperAttributes = optimizely.getFeatureVariableValueForType( + validFeatureKey, + validVariableKey, + genericUserId, + Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, "Slytherin"), + LiveVariable.VariableType.STRING + ); + assertEquals(expectedValue, valueWithImproperAttributes); + + logbackVerifier.expectMessage( + Level.INFO, + "User \"" + genericUserId + + "\" was not bucketed into any variation for feature flag \"" + validFeatureKey + + "\". The default value is being returned." + ); + } + + /** + * Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)} + * returns the variable value of the variation the user is bucketed into + * if the variation is not null and the variable has a usage within the variation. + * @throws ConfigParseException + */ + @Test + public void getFeatureVariableValueReturnsVariationValueWhenUserGetsBucketedToVariation() throws ConfigParseException { + assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())); + + String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY; + String validVariableKey = VARIABLE_FIRST_LETTER_KEY; + LiveVariable variable = FEATURE_FLAG_MULTI_VARIATE_FEATURE.getVariableKeyToLiveVariableMap().get(validVariableKey); + String expectedValue = VARIATION_MULTIVARIATE_EXPERIMENT_GRED.getVariableIdToLiveVariableUsageInstanceMap().get(variable.getId()).getValue(); + + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler) + .withConfig(validProjectConfig) + .withDecisionService(mockDecisionService) + .build(); + + doReturn(VARIATION_MULTIVARIATE_EXPERIMENT_GRED).when(mockDecisionService).getVariationForFeature( + FEATURE_FLAG_MULTI_VARIATE_FEATURE, + genericUserId, + Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE) + ); + + String value = optimizely.getFeatureVariableValueForType( + validFeatureKey, + validVariableKey, + genericUserId, + Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE), + LiveVariable.VariableType.STRING + ); + + assertEquals(expectedValue, value); + } + //======== Helper methods ========// private Experiment createUnknownExperiment() { 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 0a7ce8e81..d5f731877 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 @@ -18,8 +18,10 @@ import ch.qos.logback.classic.Level; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.TrafficAllocation; +import com.optimizely.ab.config.ValidProjectConfigV4; import com.optimizely.ab.config.Variation; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.internal.LogbackVerifier; @@ -38,17 +40,22 @@ import static com.optimizely.ab.config.ProjectConfigTestUtils.noAudienceProjectConfigV3; import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV3; +import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV4; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +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; @@ -136,6 +143,109 @@ public void getVariationEvaluatesUserProfileBeforeAudienceTargeting() throws Exc decisionService.getVariation(experiment, userProfileId, Collections.emptyMap())); } + //========== get Variation for Feature tests ==========// + + /** + * Verify that {@link DecisionService#getVariationForFeature(FeatureFlag, String, Map)} + * returns null when the {@link FeatureFlag} is not used in an experiments. + */ + @Test + @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT") + public void getVariationForFeatureReturnsNullWhenFeatureFlagExperimentIdsIsEmpty() { + FeatureFlag emptyFeatureFlag = mock(FeatureFlag.class); + when(emptyFeatureFlag.getExperimentIds()).thenReturn(Collections.emptyList()); + String featureKey = "testFeatureFlagKey"; + when(emptyFeatureFlag.getKey()).thenReturn(featureKey); + + DecisionService decisionService = new DecisionService( + mock(Bucketer.class), + mockErrorHandler, + validProjectConfig, + null); + + logbackVerifier.expectMessage(Level.INFO, + "The feature flag \"" + featureKey + "\" is not used in any experiments"); + + assertNull(decisionService.getVariationForFeature( + emptyFeatureFlag, + genericUserId, + Collections.emptyMap())); + + verify(emptyFeatureFlag, times(1)).getExperimentIds(); + verify(emptyFeatureFlag, times(1)).getKey(); + } + + /** + * Verify that {@link DecisionService#getVariationForFeature(FeatureFlag, String, Map)} + * returns null when the user is not bucketed into any experiments for the {@link FeatureFlag}. + */ + @Test + @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT") + public void getVariationForFeatureReturnsNullWhenItGetsNoVariationsForExperiments() { + FeatureFlag spyFeatureFlag = spy(ValidProjectConfigV4.FEATURE_FLAG_MULTI_VARIATE_FEATURE); + + DecisionService spyDecisionService = spy(new DecisionService( + mock(Bucketer.class), + mockErrorHandler, + validProjectConfig, + null) + ); + + doReturn(null).when(spyDecisionService).getVariation( + any(Experiment.class), + anyString(), + anyMapOf(String.class, String.class) + ); + + assertNull(spyDecisionService.getVariationForFeature( + spyFeatureFlag, + genericUserId, + Collections.emptyMap() + )); + + verify(spyFeatureFlag, times(2)).getExperimentIds(); + verify(spyFeatureFlag, never()).getKey(); + } + + /** + * Verify that {@link DecisionService#getVariationForFeature(FeatureFlag, String, Map)} + * returns the variation of the experiment a user gets bucketed into for an experiment. + */ + @Test + @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT") + public void getVariationForFeatureReturnsVariationReturnedFromGetVarition() { + FeatureFlag spyFeatureFlag = spy(ValidProjectConfigV4.FEATURE_FLAG_MUTEX_GROUP_FEATURE); + + DecisionService spyDecisionService = spy(new DecisionService( + mock(Bucketer.class), + mockErrorHandler, + validProjectConfigV4(), + null) + ); + + doReturn(null).when(spyDecisionService).getVariation( + eq(ValidProjectConfigV4.EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1), + anyString(), + anyMapOf(String.class, String.class) + ); + + doReturn(ValidProjectConfigV4.VARIATION_MUTEX_GROUP_EXP_2_VAR_1).when(spyDecisionService).getVariation( + eq(ValidProjectConfigV4.EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2), + anyString(), + anyMapOf(String.class, String.class) + ); + + assertEquals(ValidProjectConfigV4.VARIATION_MUTEX_GROUP_EXP_2_VAR_1, + spyDecisionService.getVariationForFeature( + spyFeatureFlag, + genericUserId, + Collections.emptyMap() + )); + + verify(spyFeatureFlag, times(2)).getExperimentIds(); + verify(spyFeatureFlag, never()).getKey(); + } + //========= white list tests ==========/ /** diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index e6693cdc3..e163abd52 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -129,10 +129,10 @@ public class ValidProjectConfigV4 { ) ); private static final String FEATURE_SINGLE_VARIABLE_STRING_ID = "2079378557"; - private static final String FEATURE_SINGLE_VARIABLE_STRING_KEY = "string_single_variable_feature"; + public static final String FEATURE_SINGLE_VARIABLE_STRING_KEY = "string_single_variable_feature"; private static final String VARIABLE_STRING_VARIABLE_ID = "2077511132"; - private static final String VARIABLE_STRING_VARIABLE_KEY = "string_variable"; - private static final String VARIABLE_STRING_VARIABLE_DEFAULT_VALUE = "wingardium leviosa"; + public static final String VARIABLE_STRING_VARIABLE_KEY = "string_variable"; + public static final String VARIABLE_STRING_VARIABLE_DEFAULT_VALUE = "wingardium leviosa"; private static final LiveVariable VARIABLE_STRING_VARIABLE = new LiveVariable( VARIABLE_STRING_VARIABLE_ID, VARIABLE_STRING_VARIABLE_KEY, @@ -152,8 +152,8 @@ public class ValidProjectConfigV4 { private static final String FEATURE_MULTI_VARIATE_FEATURE_ID = "3263342226"; public static final String FEATURE_MULTI_VARIATE_FEATURE_KEY = "multi_variate_feature"; private static final String VARIABLE_FIRST_LETTER_ID = "675244127"; - private static final String VARIABLE_FIRST_LETTER_KEY = "first_letter"; - private static final String VARIABLE_FIRST_LETTER_DEFAULT_VALUE = "H"; + public static final String VARIABLE_FIRST_LETTER_KEY = "first_letter"; + public static final String VARIABLE_FIRST_LETTER_DEFAULT_VALUE = "H"; private static final LiveVariable VARIABLE_FIRST_LETTER_VARIABLE = new LiveVariable( VARIABLE_FIRST_LETTER_ID, VARIABLE_FIRST_LETTER_KEY, @@ -171,19 +171,22 @@ public class ValidProjectConfigV4 { null, LiveVariable.VariableType.STRING ); - private static final FeatureFlag FEATURE_FLAG_MULTI_VARIATE_FEATURE = new FeatureFlag( - FEATURE_MULTI_VARIATE_FEATURE_ID, - FEATURE_MULTI_VARIATE_FEATURE_KEY, - "", - Collections.emptyList(), - ProjectConfigTestUtils.createListOfObjects( - VARIABLE_FIRST_LETTER_VARIABLE, - VARIABLE_REST_OF_NAME_VARIABLE - ) + private static final String FEATURE_MUTEX_GROUP_FEATURE_ID = "3263342226"; + public static final String FEATURE_MUTEX_GROUP_FEATURE_KEY = "mutex_group_feature"; + private static final String VARIABLE_CORRELATING_VARIATION_NAME_ID = "2059187672"; + private static final String VARIABLE_CORRELATING_VARIATION_NAME_KEY = "correlating_variation_name"; + private static final String VARIABLE_CORRELATING_VARIATION_NAME_DEFAULT_VALUE = "null"; + private static final LiveVariable VARIABLE_CORRELATING_VARIATION_NAME_VARIABLE = new LiveVariable( + VARIABLE_CORRELATING_VARIATION_NAME_ID, + VARIABLE_CORRELATING_VARIATION_NAME_KEY, + VARIABLE_CORRELATING_VARIATION_NAME_DEFAULT_VALUE, + null, + LiveVariable.VariableType.STRING ); // group IDs private static final String GROUP_1_ID = "1015968292"; + private static final String GROUP_2_ID = "2606208781"; // experiments private static final String LAYER_BASIC_EXPERIMENT_ID = "1630555626"; @@ -375,7 +378,7 @@ public class ValidProjectConfigV4 { ); private static final String VARIATION_MULTIVARIATE_EXPERIMENT_GRED_ID = "4204375027"; public static final String VARIATION_MULTIVARIATE_EXPERIMENT_GRED_KEY = "Gred"; - private static final Variation VARIATION_MULTIVARIATE_EXPERIMENT_GRED = new Variation( + public static final Variation VARIATION_MULTIVARIATE_EXPERIMENT_GRED = new Variation( VARIATION_MULTIVARIATE_EXPERIMENT_GRED_ID, VARIATION_MULTIVARIATE_EXPERIMENT_GRED_KEY, ProjectConfigTestUtils.createListOfObjects( @@ -506,6 +509,68 @@ public class ValidProjectConfigV4 { ) ) ); + private static final String LAYER_MUTEX_GROUP_EXPERIMENT_1_LAYER_ID = "3755588495"; + private static final String EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1_EXPERIMENT_ID = "4138322202"; + private static final String EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1_EXPERIMENT_KEY = "mutex_group_2_experiment_1"; + private static final String VARIATION_MUTEX_GROUP_EXP_1_VAR_1_ID = "1394671166"; + private static final String VARIATION_MUTEX_GROUP_EXP_1_VAR_1_KEY = "mutex_group_2_experiment_1_variation_1"; + private static final Variation VARIATION_MUTEX_GROUP_EXP_1_VAR_1 = new Variation( + VARIATION_MUTEX_GROUP_EXP_1_VAR_1_ID, + VARIATION_MUTEX_GROUP_EXP_1_VAR_1_KEY, + Collections.singletonList( + new LiveVariableUsageInstance( + VARIABLE_CORRELATING_VARIATION_NAME_ID, + VARIATION_MUTEX_GROUP_EXP_1_VAR_1_KEY + ) + ) + ); + public static final Experiment EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1 = new Experiment( + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1_EXPERIMENT_ID, + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1_EXPERIMENT_KEY, + Experiment.ExperimentStatus.RUNNING.toString(), + LAYER_MUTEX_GROUP_EXPERIMENT_1_LAYER_ID, + Collections.emptyList(), + Collections.singletonList(VARIATION_MUTEX_GROUP_EXP_1_VAR_1), + Collections.emptyMap(), + Collections.singletonList( + new TrafficAllocation( + VARIATION_MUTEX_GROUP_EXP_1_VAR_1_ID, + 10000 + ) + ), + GROUP_2_ID + ); + private static final String LAYER_MUTEX_GROUP_EXPERIMENT_2_LAYER_ID = "3818002538"; + private static final String EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2_EXPERIMENT_ID = "1786133852"; + private static final String EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2_EXPERIMENT_KEY = "mutex_group_2_experiment_2"; + private static final String VARIATION_MUTEX_GROUP_EXP_2_VAR_1_ID = "1619235542"; + private static final String VARIATION_MUTEX_GROUP_EXP_2_VAR_1_KEY = "mutex_group_2_experiment_2_variation_2"; + public static final Variation VARIATION_MUTEX_GROUP_EXP_2_VAR_1 = new Variation( + VARIATION_MUTEX_GROUP_EXP_2_VAR_1_ID, + VARIATION_MUTEX_GROUP_EXP_2_VAR_1_KEY, + Collections.singletonList( + new LiveVariableUsageInstance( + VARIABLE_CORRELATING_VARIATION_NAME_ID, + VARIATION_MUTEX_GROUP_EXP_2_VAR_1_KEY + ) + ) + ); + public static final Experiment EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2 = new Experiment( + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2_EXPERIMENT_ID, + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2_EXPERIMENT_KEY, + Experiment.ExperimentStatus.RUNNING.toString(), + LAYER_MUTEX_GROUP_EXPERIMENT_2_LAYER_ID, + Collections.emptyList(), + Collections.singletonList(VARIATION_MUTEX_GROUP_EXP_2_VAR_1), + Collections.emptyMap(), + Collections.singletonList( + new TrafficAllocation( + VARIATION_MUTEX_GROUP_EXP_2_VAR_1_ID, + 10000 + ) + ), + GROUP_2_ID + ); // generate groups private static final Group GROUP_1 = new Group( @@ -526,6 +591,24 @@ public class ValidProjectConfigV4 { ) ) ); + private static final Group GROUP_2 = new Group( + GROUP_2_ID, + Group.RANDOM_POLICY, + ProjectConfigTestUtils.createListOfObjects( + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1, + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2 + ), + ProjectConfigTestUtils.createListOfObjects( + new TrafficAllocation( + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1_EXPERIMENT_ID, + 5000 + ), + new TrafficAllocation( + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2_EXPERIMENT_ID, + 10000 + ) + ) + ); // events private static final String EVENT_BASIC_EVENT_ID = "3785620495"; @@ -560,6 +643,30 @@ public class ValidProjectConfigV4 { ) ); + // finish features + public static final FeatureFlag FEATURE_FLAG_MULTI_VARIATE_FEATURE = new FeatureFlag( + FEATURE_MULTI_VARIATE_FEATURE_ID, + FEATURE_MULTI_VARIATE_FEATURE_KEY, + "", + Collections.singletonList(EXPERIMENT_MULTIVARIATE_EXPERIMENT_ID), + ProjectConfigTestUtils.createListOfObjects( + VARIABLE_FIRST_LETTER_VARIABLE, + VARIABLE_REST_OF_NAME_VARIABLE + ) + ); + public static final FeatureFlag FEATURE_FLAG_MUTEX_GROUP_FEATURE = new FeatureFlag( + FEATURE_MUTEX_GROUP_FEATURE_ID, + FEATURE_MUTEX_GROUP_FEATURE_KEY, + "", + ProjectConfigTestUtils.createListOfObjects( + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1_EXPERIMENT_ID, + EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2_EXPERIMENT_ID + ), + Collections.singletonList( + VARIABLE_CORRELATING_VARIATION_NAME_VARIABLE + ) + ); + public static ProjectConfig generateValidProjectConfigV4() { @@ -592,9 +699,11 @@ public static ProjectConfig generateValidProjectConfigV4() { featureFlags.add(FEATURE_FLAG_SINGLE_VARIABLE_BOOLEAN); featureFlags.add(FEATURE_FLAG_SINGLE_VARIABLE_STRING); featureFlags.add(FEATURE_FLAG_MULTI_VARIATE_FEATURE); + featureFlags.add(FEATURE_FLAG_MUTEX_GROUP_FEATURE); List groups = new ArrayList(); groups.add(GROUP_1); + groups.add(GROUP_2); return new ProjectConfig( ACCOUNT_ID, diff --git a/core-api/src/test/resources/config/valid-project-config-v4.json b/core-api/src/test/resources/config/valid-project-config-v4.json index 624d8a538..75a91d422 100644 --- a/core-api/src/test/resources/config/valid-project-config-v4.json +++ b/core-api/src/test/resources/config/valid-project-config-v4.json @@ -300,6 +300,74 @@ "endOfRange": 8000 } ] + }, + { + "id": "2606208781", + "policy": "random", + "experiments": [ + { + "id": "4138322202", + "key": "mutex_group_2_experiment_1", + "layerId": "3755588495", + "status": "Running", + "variations": [ + { + "id": "1394671166", + "key": "mutex_group_2_experiment_1_variation_1", + "variables": [ + { + "id": "2059187672", + "value": "mutex_group_2_experiment_1_variation_1" + } + ] + } + ], + "audienceIds": [], + "forcedVariations": {}, + "trafficAllocation": [ + { + "entityId": "1394671166", + "endOfRange": 10000 + } + ] + }, + { + "id": "1786133852", + "key": "mutex_group_2_experiment_2", + "layerId": "3818002538", + "status": "Running", + "variations": [ + { + "id": "1619235542", + "key": "mutex_group_2_experiment_2_variation_2", + "variables": [ + { + "id": "2059187672", + "value": "mutex_group_2_experiment_2_variation_2" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "1619235542", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "forcedVariations": {} + } + ], + "trafficAllocation": [ + { + "entityId": "4138322202", + "endOfRange": 5000 + }, + { + "entityId": "1786133852", + "endOfRange": 10000 + } + ] } ], "featureFlags": [ @@ -370,7 +438,7 @@ "id": "3263342226", "key": "multi_variate_feature", "layerId": "", - "experimentIds": [], + "experimentIds": ["3262035800"], "variables": [ { "id": "675244127", @@ -385,6 +453,20 @@ "defaultValue": "arry" } ] + }, + { + "id": "3263342226", + "key": "mutex_group_feature", + "layerId": "", + "experimentIds": ["4138322202", "1786133852"], + "variables": [ + { + "id": "2059187672", + "key": "correlating_variation_name", + "type": "string", + "defaultValue": "null" + } + ] } ], "variables": []