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 e7f614f2a..e32a39cb5 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -693,13 +693,15 @@ T getFeatureVariableValueForType(@Nonnull String featureKey, featureDecision.variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId()); if (featureVariableUsageInstance != null) { variableValue = featureVariableUsageInstance.getValue(); + logger.info("Got variable value \"{}\" for variable \"{}\" of feature flag \"{}\".", variableValue, variableKey, featureKey); } else { variableValue = variable.getDefaultValue(); + logger.info("Value is not defined for variable \"{}\". Returning default value \"{}\".", variableKey, variableValue); } } else { - logger.info("Feature \"{}\" for variation \"{}\" was not enabled. " + - "The default value is being returned.", - featureKey, featureDecision.variation.getKey(), variableValue, variableKey + logger.info("Feature \"{}\" is not enabled for user \"{}\". " + + "Returning the default variable value \"{}\".", + featureKey, userId, variableValue ); } featureEnabled = featureDecision.variation.getFeatureEnabled(); @@ -822,12 +824,12 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey, Variation variation = featureDecision.variation; if (variation != null) { - if (!variation.getFeatureEnabled()) { - logger.info("Feature \"{}\" for variation \"{}\" was not enabled. " + - "The default value is being returned.", featureKey, featureDecision.variation.getKey()); - } - featureEnabled = variation.getFeatureEnabled(); + if (featureEnabled) { + logger.info("Feature \"{}\" is enabled for user \"{}\".", featureKey, userId); + } else { + logger.info("Feature \"{}\" is not enabled for user \"{}\".", featureKey, userId); + } } else { logger.info("User \"{}\" was not bucketed into any variation for feature flag \"{}\". " + "The default values are being returned.", userId, featureKey); 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 c1115f4a4..13091472d 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 @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2017-2019, Optimizely, Inc. and contributors * + * Copyright 2017-2020, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -17,7 +17,6 @@ import com.optimizely.ab.OptimizelyRuntimeException; import com.optimizely.ab.config.*; -import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.internal.ExperimentUtils; import com.optimizely.ab.internal.ControlAttribute; @@ -32,6 +31,9 @@ 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; + /** * Optimizely's decision service that determines which variation of an experiment the user will be allocated to. * @@ -133,7 +135,7 @@ public Variation getVariation(@Nonnull Experiment experiment, userProfile = new UserProfile(userId, new HashMap()); } - if (ExperimentUtils.isUserInExperiment(projectConfig, experiment, filteredAttributes)) { + if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, experiment, filteredAttributes, EXPERIMENT, experiment.getKey())) { String bucketingId = getBucketingId(userId, filteredAttributes); variation = bucketer.bucket(experiment, bucketingId, projectConfig); @@ -221,8 +223,7 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag Variation variation; for (int i = 0; i < rolloutRulesLength - 1; i++) { Experiment rolloutRule = rollout.getExperiments().get(i); - Audience audience = projectConfig.getAudienceIdMapping().get(rolloutRule.getAudienceIds().get(0)); - if (ExperimentUtils.isUserInExperiment(projectConfig, rolloutRule, filteredAttributes)) { + if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, rolloutRule, filteredAttributes, RULE, Integer.toString(i + 1))) { variation = bucketer.bucket(rolloutRule, bucketingId, projectConfig); if (variation == null) { break; @@ -230,16 +231,16 @@ FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag return new FeatureDecision(rolloutRule, variation, FeatureDecision.DecisionSource.ROLLOUT); } else { - logger.debug("User \"{}\" did not meet the conditions to be in rollout rule for audience \"{}\".", - userId, audience.getName()); + logger.debug("User \"{}\" does not meet conditions for targeting rule \"{}\".", userId, i + 1); } } // get last rule which is the fall back rule Experiment finalRule = rollout.getExperiments().get(rolloutRulesLength - 1); - if (ExperimentUtils.isUserInExperiment(projectConfig, finalRule, filteredAttributes)) { + if (ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, finalRule, filteredAttributes, RULE, "Everyone Else")) { variation = bucketer.bucket(finalRule, bucketingId, projectConfig); if (variation != null) { + logger.debug("User \"{}\" meets conditions for targeting rule \"Everyone Else\".", userId); return new FeatureDecision(finalRule, variation, FeatureDecision.DecisionSource.ROLLOUT); } @@ -394,7 +395,6 @@ public boolean setForcedVariation(@Nonnull Experiment experiment, @Nullable String variationKey) { - Variation variation = null; // keep in mind that you can pass in a variationKey that is null if you want to 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 b5fb5fc96..57a4e5bec 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 @@ -74,9 +74,9 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { logger.error("Audience {} could not be found.", audienceId); return null; } - logger.debug("Starting to evaluate audience {} with conditions: \"{}\"", audience.getName(), audience.getConditions()); + logger.debug("Starting to evaluate audience \"{}\" with conditions: {}.", audience.getId(), audience.getConditions()); Boolean result = audience.getConditions().evaluate(config, attributes); - logger.debug("Audience {} evaluated to {}", audience.getName(), result); + logger.debug("Audience \"{}\" evaluated to {}.", audience.getId(), result); return result; } 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 eca5b08be..be1e11169 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019, Optimizely and contributors + * Copyright 2016-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. @@ -82,7 +82,7 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { Object userAttributeValue = attributes.get(name); if (!"custom_attribute".equals(type)) { - logger.warn("Audience condition \"{}\" has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK", this); + logger.warn("Audience condition \"{}\" uses an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.", this); return null; // unknown type } // check user attribute value is equal @@ -103,7 +103,7 @@ public Boolean evaluate(ProjectConfig config, Map attributes) { userAttributeValue.getClass().getCanonicalName(), name); } else { - logger.warn( + logger.debug( "Audience condition \"{}\" evaluated to UNKNOWN because a null value was passed for user attribute \"{}\"", this, name); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnexpectedValueTypeException.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnexpectedValueTypeException.java index 58a34f81f..cf513bc7d 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnexpectedValueTypeException.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnexpectedValueTypeException.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019, Optimizely and contributors + * Copyright 2019-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. @@ -18,7 +18,7 @@ package com.optimizely.ab.config.audience.match; public class UnexpectedValueTypeException extends Exception { - private static String message = "has an unexpected value type. You may need to upgrade to a newer release of the Optimizely SDK"; + private static String message = "has an unsupported condition value. You may need to upgrade to a newer release of the Optimizely SDK."; public UnexpectedValueTypeException() { super(message); diff --git a/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownMatchTypeException.java b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownMatchTypeException.java index b08a66fb6..0c5a972a7 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownMatchTypeException.java +++ b/core-api/src/main/java/com/optimizely/ab/config/audience/match/UnknownMatchTypeException.java @@ -1,6 +1,6 @@ /** * - * Copyright 2019, Optimizely and contributors + * Copyright 2019-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. @@ -18,7 +18,7 @@ package com.optimizely.ab.config.audience.match; public class UnknownMatchTypeException extends Exception { - private static String message = "uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK"; + private static String message = "uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK."; public UnknownMatchTypeException() { super(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 53662e9a6..f5109b624 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2019, Optimizely and contributors + * Copyright 2017-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. @@ -56,19 +56,24 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment) { /** * 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 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 isUserInExperiment(@Nonnull ProjectConfig projectConfig, - @Nonnull Experiment experiment, - @Nonnull Map attributes) { + public static boolean doesUserMeetAudienceConditions(@Nonnull ProjectConfig projectConfig, + @Nonnull Experiment experiment, + @Nonnull Map attributes, + @Nonnull String loggingEntityType, + @Nonnull String loggingKey) { if (experiment.getAudienceConditions() != null) { - Boolean resolveReturn = evaluateAudienceConditions(projectConfig, experiment, attributes); + logger.debug("Evaluating audiences for {} \"{}\": {}.", loggingEntityType, loggingKey, experiment.getAudienceConditions()); + Boolean resolveReturn = evaluateAudienceConditions(projectConfig, experiment, attributes, loggingEntityType, loggingKey); return resolveReturn == null ? false : resolveReturn; } else { - Boolean resolveReturn = evaluateAudience(projectConfig, experiment, attributes); + Boolean resolveReturn = evaluateAudience(projectConfig, experiment, attributes, loggingEntityType, loggingKey); return Boolean.TRUE.equals(resolveReturn); } } @@ -76,12 +81,13 @@ public static boolean isUserInExperiment(@Nonnull ProjectConfig projectConfig, @Nullable public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, @Nonnull Experiment experiment, - @Nonnull Map attributes) { + @Nonnull Map attributes, + @Nonnull String loggingEntityType, + @Nonnull String loggingKey) { List experimentAudienceIds = experiment.getAudienceIds(); // if there are no audiences, ALL users should be part of the experiment if (experimentAudienceIds.isEmpty()) { - logger.debug("There is no Audience associated with experiment {}", experiment.getKey()); return true; } @@ -93,11 +99,11 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, OrCondition implicitOr = new OrCondition(conditions); - logger.debug("Evaluating audiences for experiment \"{}\": \"{}\"", experiment.getKey(), conditions); + logger.debug("Evaluating audiences for {} \"{}\": {}.", loggingEntityType, loggingKey, conditions); Boolean result = implicitOr.evaluate(projectConfig, attributes); - logger.info("Audiences for experiment {} collectively evaluated to {}", experiment.getKey(), result); + logger.info("Audiences for {} \"{}\" collectively evaluated to {}.", loggingEntityType, loggingKey, result); return result; } @@ -105,14 +111,16 @@ public static Boolean evaluateAudience(@Nonnull ProjectConfig projectConfig, @Nullable public static Boolean evaluateAudienceConditions(@Nonnull ProjectConfig projectConfig, @Nonnull Experiment experiment, - @Nonnull Map attributes) { + @Nonnull Map attributes, + @Nonnull String loggingEntityType, + @Nonnull String loggingKey) { Condition conditions = experiment.getAudienceConditions(); if (conditions == null) return null; - logger.debug("Evaluating audiences for experiment \"{}\": \"{}\"", experiment.getKey(), conditions.toString()); + try { Boolean result = conditions.evaluate(projectConfig, attributes); - logger.info("Audiences for experiment {} collectively evaluated to {}", experiment.getKey(), result); + logger.info("Audiences for {} \"{}\" collectively evaluated to {}.", loggingEntityType, loggingKey, result); return result; } catch (Exception e) { logger.error("Condition invalid", e); diff --git a/core-api/src/main/java/com/optimizely/ab/internal/LoggingConstants.java b/core-api/src/main/java/com/optimizely/ab/internal/LoggingConstants.java new file mode 100644 index 000000000..66387f2e7 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/internal/LoggingConstants.java @@ -0,0 +1,24 @@ +/** + * + * 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.internal; + +public class LoggingConstants { + public static class LoggingEntityType { + public static final String EXPERIMENT = "experiment"; + public static final String RULE = "rule"; + } +} 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 f4c67df82..6c32975a8 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -2890,8 +2890,7 @@ public void getFeatureVariableValueReturnsDefaultValueWhenFeatureEnabledIsFalse( logbackVerifier.expectMessage( Level.INFO, - "Feature \"" + validFeatureKey + "\" for variation \"Gred\" was not enabled. " + - "The default value is being returned." + "Feature \"multi_variate_feature\" is not enabled for user \"genericUserId\". Returning the default variable value \"H\"." ); assertEquals(expectedValue, value); @@ -2918,6 +2917,11 @@ public void getFeatureVariableUserInExperimentFeatureOn() throws Exception { testUserId, Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)), expectedValue); + + logbackVerifier.expectMessage( + Level.INFO, + "Got variable value \"F\" for variable \"first_letter\" of feature flag \"multi_variate_feature\"." + ); } /** @@ -3061,6 +3065,11 @@ public void getFeatureVariableValueReturnsDefaultValueWhenNoVariationUsageIsPres ); assertEquals(expectedValue, value); + + logbackVerifier.expectMessage( + Level.INFO, + "Value is not defined for variable \"integer_variable\". Returning default value \"7\"." + ); } /** 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 5779be07f..2a3030314 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 @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2017-2019, Optimizely, Inc. and contributors * + * Copyright 2017-2020, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -625,6 +625,14 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsAllAudie Collections.emptyMap(), v4ProjectConfig ); + logbackVerifier.expectMessage(Level.DEBUG, "Evaluating audiences for rule \"1\": [3468206642]."); + logbackVerifier.expectMessage(Level.INFO, "Audiences for rule \"1\" collectively evaluated to null."); + logbackVerifier.expectMessage(Level.DEBUG, "Evaluating audiences for rule \"2\": [3988293898]."); + logbackVerifier.expectMessage(Level.INFO, "Audiences for rule \"2\" collectively evaluated to null."); + logbackVerifier.expectMessage(Level.DEBUG, "Evaluating audiences for rule \"3\": [4194404272]."); + logbackVerifier.expectMessage(Level.INFO, "Audiences for rule \"3\" collectively evaluated to null."); + logbackVerifier.expectMessage(Level.DEBUG, "User \"genericUserId\" meets conditions for targeting rule \"Everyone Else\"."); + assertEquals(expectedVariation, featureDecision.variation); assertEquals(FeatureDecision.DecisionSource.ROLLOUT, featureDecision.decisionSource); @@ -664,6 +672,8 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTrafficI assertEquals(expectedVariation, featureDecision.variation); assertEquals(FeatureDecision.DecisionSource.ROLLOUT, featureDecision.decisionSource); + 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)); } @@ -743,7 +753,11 @@ public void getVariationForFeatureInRolloutReturnsVariationWhenUserFailsTargetin ); assertEquals(englishCitizenVariation, featureDecision.variation); assertEquals(FeatureDecision.DecisionSource.ROLLOUT, featureDecision.decisionSource); - + logbackVerifier.expectMessage(Level.INFO, "Audiences for rule \"2\" collectively evaluated to null"); + logbackVerifier.expectMessage(Level.DEBUG, "Evaluating audiences for rule \"3\": [4194404272]."); + logbackVerifier.expectMessage(Level.DEBUG, "Starting to evaluate audience \"4194404272\" with conditions: [and, [or, [or, {name='nationality', type='custom_attribute', match='exact', value='English'}]]]."); + 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)); } 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 a167845b9..83c5e41df 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 @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2019, Optimizely and contributors + * Copyright 2016-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. @@ -136,7 +136,7 @@ public void unexpectedAttributeType() throws Exception { public void unexpectedAttributeTypeNull() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "custom_attribute", "gt", 20); assertNull(testInstance.evaluate(null, Collections.singletonMap("browser_type", null))); - logbackVerifier.expectMessage(Level.WARN, + 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\""); } @@ -171,7 +171,7 @@ public void unknownConditionType() throws Exception { UserAttribute testInstance = new UserAttribute("browser_type", "blah", "exists", "firefox"); assertNull(testInstance.evaluate(null, testUserAttributes)); logbackVerifier.expectMessage(Level.WARN, - "Audience condition \"{name='browser_type', type='blah', match='exists', value='firefox'}\" has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK"); + "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."); } /** diff --git a/core-api/src/test/java/com/optimizely/ab/internal/ExperimentUtilsTest.java b/core-api/src/test/java/com/optimizely/ab/internal/ExperimentUtilsTest.java index 216801388..841e5a504 100644 --- a/core-api/src/test/java/com/optimizely/ab/internal/ExperimentUtilsTest.java +++ b/core-api/src/test/java/com/optimizely/ab/internal/ExperimentUtilsTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017, 2019, Optimizely and contributors + * Copyright 2017, 2019-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. @@ -41,7 +41,9 @@ import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_WITH_MISSING_VALUE_VALUE; import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_WITH_MALFORMED_AUDIENCE_KEY; import static com.optimizely.ab.internal.ExperimentUtils.isExperimentActive; -import static com.optimizely.ab.internal.ExperimentUtils.isUserInExperiment; +import static com.optimizely.ab.internal.ExperimentUtils.doesUserMeetAudienceConditions; +import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.EXPERIMENT; +import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.RULE; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -121,115 +123,115 @@ public void isExperimentActiveReturnsFalseWhenTheExperimentIsNotStarted() { /** * If the {@link Experiment} does not have any {@link Audience}s, - * then {@link ExperimentUtils#isUserInExperiment(ProjectConfig, Experiment, Map)} should return true; + * then {@link ExperimentUtils#doesUserMeetAudienceConditions(ProjectConfig, Experiment, Map, String, String)} should return true; */ @Test - public void isUserInExperimentReturnsTrueIfExperimentHasNoAudiences() { + public void doesUserMeetAudienceConditionsReturnsTrueIfExperimentHasNoAudiences() { Experiment experiment = noAudienceProjectConfig.getExperiments().get(0); - assertTrue(isUserInExperiment(noAudienceProjectConfig, experiment, Collections.emptyMap())); + assertTrue(doesUserMeetAudienceConditions(noAudienceProjectConfig, experiment, Collections.emptyMap(), RULE, "Everyone Else")); } /** * If the {@link Experiment} contains at least one {@link Audience}, but attributes is empty, - * then {@link ExperimentUtils#isUserInExperiment(ProjectConfig, Experiment, Map)} should return false. + * then {@link ExperimentUtils#doesUserMeetAudienceConditions(ProjectConfig, Experiment, Map, String, String)} should return false. */ @Test - public void isUserInExperimentEvaluatesEvenIfExperimentHasAudiencesButUserHasNoAttributes() { + public void doesUserMeetAudienceConditionsEvaluatesEvenIfExperimentHasAudiencesButUserHasNoAttributes() { Experiment experiment = projectConfig.getExperiments().get(0); - Boolean result = isUserInExperiment(projectConfig, experiment, Collections.emptyMap()); + Boolean result = doesUserMeetAudienceConditions(projectConfig, experiment, Collections.emptyMap(), EXPERIMENT, experiment.getKey()); assertTrue(result); logbackVerifier.expectMessage(Level.DEBUG, - "Evaluating audiences for experiment \"etag1\": \"[100]\""); + "Evaluating audiences for experiment \"etag1\": [100]."); logbackVerifier.expectMessage(Level.DEBUG, - "Starting to evaluate audience not_firefox_users with conditions: \"[and, [or, [not, [or, {name='browser_type', type='custom_attribute', match='null', value='firefox'}]]]]\""); + "Starting to evaluate audience \"100\" with conditions: [and, [or, [not, [or, {name='browser_type', type='custom_attribute', match='null', value='firefox'}]]]]."); logbackVerifier.expectMessage(Level.DEBUG, - "Audience not_firefox_users evaluated to true"); + "Audience \"100\" evaluated to true."); logbackVerifier.expectMessage(Level.INFO, - "Audiences for experiment etag1 collectively evaluated to true"); + "Audiences for experiment \"etag1\" collectively evaluated to true."); } /** * If the {@link Experiment} contains at least one {@link Audience}, but attributes is empty, - * then {@link ExperimentUtils#isUserInExperiment(ProjectConfig, Experiment, Map)} should return false. + * then {@link ExperimentUtils#doesUserMeetAudienceConditions(ProjectConfig, Experiment, Map, String, String)} should return false. */ @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") @Test - public void isUserInExperimentEvaluatesEvenIfExperimentHasAudiencesButUserSendNullAttributes() throws Exception { + public void doesUserMeetAudienceConditionsEvaluatesEvenIfExperimentHasAudiencesButUserSendNullAttributes() throws Exception { Experiment experiment = projectConfig.getExperiments().get(0); - Boolean result = isUserInExperiment(projectConfig, experiment, null); + Boolean result = doesUserMeetAudienceConditions(projectConfig, experiment, null, EXPERIMENT, experiment.getKey()); assertTrue(result); logbackVerifier.expectMessage(Level.DEBUG, - "Evaluating audiences for experiment \"etag1\": \"[100]\""); + "Evaluating audiences for experiment \"etag1\": [100]."); logbackVerifier.expectMessage(Level.DEBUG, - "Starting to evaluate audience not_firefox_users with conditions: \"[and, [or, [not, [or, {name='browser_type', type='custom_attribute', match='null', value='firefox'}]]]]\""); + "Starting to evaluate audience \"100\" with conditions: [and, [or, [not, [or, {name='browser_type', type='custom_attribute', match='null', value='firefox'}]]]]."); logbackVerifier.expectMessage(Level.DEBUG, - "Audience not_firefox_users evaluated to true"); + "Audience \"100\" evaluated to true."); logbackVerifier.expectMessage(Level.INFO, - "Audiences for experiment etag1 collectively evaluated to true"); + "Audiences for experiment \"etag1\" collectively evaluated to true."); } /** * If the {@link Experiment} contains {@link TypedAudience}, and attributes is valid and true, - * then {@link ExperimentUtils#isUserInExperiment(ProjectConfig, Experiment, Map)} should return true. + * then {@link ExperimentUtils#doesUserMeetAudienceConditions(ProjectConfig, Experiment, Map, String, String)} should return true. */ @Test - public void isUserInExperimentEvaluatesExperimentHasTypedAudiences() { + public void doesUserMeetAudienceConditionsEvaluatesExperimentHasTypedAudiences() { Experiment experiment = v4ProjectConfig.getExperiments().get(1); Map attribute = Collections.singletonMap("booleanKey", true); - Boolean result = isUserInExperiment(v4ProjectConfig, experiment, attribute); + Boolean result = doesUserMeetAudienceConditions(v4ProjectConfig, experiment, attribute, EXPERIMENT, experiment.getKey()); assertTrue(result); logbackVerifier.expectMessage(Level.DEBUG, - "Evaluating audiences for experiment \"typed_audience_experiment\": \"[or, 3468206643, 3468206644, 3468206646, 3468206645]\""); + "Evaluating audiences for experiment \"typed_audience_experiment\": [or, 3468206643, 3468206644, 3468206646, 3468206645]."); logbackVerifier.expectMessage(Level.DEBUG, - "Starting to evaluate audience BOOL with conditions: \"[and, [or, [or, {name='booleanKey', type='custom_attribute', match='exact', value=true}]]]\""); + "Starting to evaluate audience \"3468206643\" with conditions: [and, [or, [or, {name='booleanKey', type='custom_attribute', match='exact', value=true}]]]."); logbackVerifier.expectMessage(Level.DEBUG, - "Audience BOOL evaluated to true"); + "Audience \"3468206643\" evaluated to true."); logbackVerifier.expectMessage(Level.INFO, - "Audiences for experiment typed_audience_experiment collectively evaluated to true"); + "Audiences for experiment \"typed_audience_experiment\" collectively evaluated to true."); } /** * If the attributes satisfies at least one {@link Condition} in an {@link Audience} of the {@link Experiment}, - * then {@link ExperimentUtils#isUserInExperiment(ProjectConfig, Experiment, Map)} should return true. + * then {@link ExperimentUtils#doesUserMeetAudienceConditions(ProjectConfig, Experiment, Map, String, String)} should return true. */ @Test - public void isUserInExperimentReturnsTrueIfUserSatisfiesAnAudience() { + public void doesUserMeetAudienceConditionsReturnsTrueIfUserSatisfiesAnAudience() { Experiment experiment = projectConfig.getExperiments().get(0); Map attributes = Collections.singletonMap("browser_type", "chrome"); - Boolean result = isUserInExperiment(projectConfig, experiment, attributes); + Boolean result = doesUserMeetAudienceConditions(projectConfig, experiment, attributes, EXPERIMENT, experiment.getKey()); assertTrue(result); logbackVerifier.expectMessage(Level.DEBUG, - "Evaluating audiences for experiment \"etag1\": \"[100]\""); + "Evaluating audiences for experiment \"etag1\": [100]."); logbackVerifier.expectMessage(Level.DEBUG, - "Starting to evaluate audience not_firefox_users with conditions: \"[and, [or, [not, [or, {name='browser_type', type='custom_attribute', match='null', value='firefox'}]]]]\""); + "Starting to evaluate audience \"100\" with conditions: [and, [or, [not, [or, {name='browser_type', type='custom_attribute', match='null', value='firefox'}]]]]."); logbackVerifier.expectMessage(Level.DEBUG, - "Audience not_firefox_users evaluated to true"); + "Audience \"100\" evaluated to true."); logbackVerifier.expectMessage(Level.INFO, - "Audiences for experiment etag1 collectively evaluated to true"); + "Audiences for experiment \"etag1\" collectively evaluated to true."); } /** * If the attributes satisfies no {@link Condition} of any {@link Audience} of the {@link Experiment}, - * then {@link ExperimentUtils#isUserInExperiment(ProjectConfig, Experiment, Map)} should return false. + * then {@link ExperimentUtils#doesUserMeetAudienceConditions(ProjectConfig, Experiment, Map, String, String)} should return false. */ @Test - public void isUserInExperimentReturnsTrueIfUserDoesNotSatisfyAnyAudiences() { + public void doesUserMeetAudienceConditionsReturnsTrueIfUserDoesNotSatisfyAnyAudiences() { Experiment experiment = projectConfig.getExperiments().get(0); Map attributes = Collections.singletonMap("browser_type", "firefox"); - Boolean result = isUserInExperiment(projectConfig, experiment, attributes); + Boolean result = doesUserMeetAudienceConditions(projectConfig, experiment, attributes, EXPERIMENT, experiment.getKey()); assertFalse(result); logbackVerifier.expectMessage(Level.DEBUG, - "Evaluating audiences for experiment \"etag1\": \"[100]\""); + "Evaluating audiences for experiment \"etag1\": [100]."); logbackVerifier.expectMessage(Level.DEBUG, - "Starting to evaluate audience not_firefox_users with conditions: \"[and, [or, [not, [or, {name='browser_type', type='custom_attribute', match='null', value='firefox'}]]]]\""); + "Starting to evaluate audience \"100\" with conditions: [and, [or, [not, [or, {name='browser_type', type='custom_attribute', match='null', value='firefox'}]]]]."); logbackVerifier.expectMessage(Level.DEBUG, - "Audience not_firefox_users evaluated to false"); + "Audience \"100\" evaluated to false."); logbackVerifier.expectMessage(Level.INFO, - "Audiences for experiment etag1 collectively evaluated to false"); + "Audiences for experiment \"etag1\" collectively evaluated to false."); } @@ -238,55 +240,55 @@ public void isUserInExperimentReturnsTrueIfUserDoesNotSatisfyAnyAudiences() { * they must explicitly pass in null in order for us to evaluate this. Otherwise we will say they do not match. */ @Test - public void isUserInExperimentHandlesNullValue() { + public void doesUserMeetAudienceConditionsHandlesNullValue() { Experiment experiment = v4ProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_WITH_MALFORMED_AUDIENCE_KEY); Map satisfiesFirstCondition = Collections.singletonMap(ATTRIBUTE_NATIONALITY_KEY, AUDIENCE_WITH_MISSING_VALUE_VALUE); Map nonMatchingMap = Collections.singletonMap(ATTRIBUTE_NATIONALITY_KEY, "American"); - assertTrue(isUserInExperiment(v4ProjectConfig, experiment, satisfiesFirstCondition)); - assertFalse(isUserInExperiment(v4ProjectConfig, experiment, nonMatchingMap)); + assertTrue(doesUserMeetAudienceConditions(v4ProjectConfig, experiment, satisfiesFirstCondition, EXPERIMENT, experiment.getKey())); + assertFalse(doesUserMeetAudienceConditions(v4ProjectConfig, experiment, nonMatchingMap, EXPERIMENT, experiment.getKey())); } /** * Audience will evaluate null when condition value is null and attribute value passed is also null */ @Test - public void isUserInExperimentHandlesNullValueAttributesWithNull() { + public void doesUserMeetAudienceConditionsHandlesNullValueAttributesWithNull() { Experiment experiment = v4ProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_WITH_MALFORMED_AUDIENCE_KEY); Map attributesWithNull = Collections.singletonMap(ATTRIBUTE_NATIONALITY_KEY, null); - assertFalse(isUserInExperiment(v4ProjectConfig, experiment, attributesWithNull)); + assertFalse(doesUserMeetAudienceConditions(v4ProjectConfig, experiment, attributesWithNull, EXPERIMENT, experiment.getKey())); logbackVerifier.expectMessage(Level.DEBUG, - "Starting to evaluate audience audience_with_missing_value with conditions: \"[and, [or, [or, {name='nationality', type='custom_attribute', match='null', value='English'}, {name='nationality', type='custom_attribute', match='null', value=null}]]]\""); + "Starting to evaluate audience \"2196265320\" with conditions: [and, [or, [or, {name='nationality', type='custom_attribute', match='null', value='English'}, {name='nationality', type='custom_attribute', match='null', value=null}]]]."); logbackVerifier.expectMessage(Level.WARN, - "Audience condition \"{name='nationality', type='custom_attribute', match='null', value=null}\" has an unexpected value type. You may need to upgrade to a newer release of the Optimizely SDK"); + "Audience condition \"{name='nationality', type='custom_attribute', match='null', value=null}\" has an unsupported condition value. You may need to upgrade to a newer release of the Optimizely SDK."); logbackVerifier.expectMessage(Level.DEBUG, - "Audience audience_with_missing_value evaluated to null"); + "Audience \"2196265320\" evaluated to null."); logbackVerifier.expectMessage(Level.INFO, - "Audiences for experiment experiment_with_malformed_audience collectively evaluated to null"); + "Audiences for experiment \"experiment_with_malformed_audience\" collectively evaluated to null."); } /** * Audience will evaluate null when condition value is null */ @Test - public void isUserInExperimentHandlesNullConditionValue() { + public void doesUserMeetAudienceConditionsHandlesNullConditionValue() { Experiment experiment = v4ProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_WITH_MALFORMED_AUDIENCE_KEY); Map attributesEmpty = Collections.emptyMap(); // It should explicitly be set to null otherwise we will return false on empty maps - assertFalse(isUserInExperiment(v4ProjectConfig, experiment, attributesEmpty)); + assertFalse(doesUserMeetAudienceConditions(v4ProjectConfig, experiment, attributesEmpty, EXPERIMENT, experiment.getKey())); logbackVerifier.expectMessage(Level.DEBUG, - "Starting to evaluate audience audience_with_missing_value with conditions: \"[and, [or, [or, {name='nationality', type='custom_attribute', match='null', value='English'}, {name='nationality', type='custom_attribute', match='null', value=null}]]]\""); + "Starting to evaluate audience \"2196265320\" with conditions: [and, [or, [or, {name='nationality', type='custom_attribute', match='null', value='English'}, {name='nationality', type='custom_attribute', match='null', value=null}]]]."); logbackVerifier.expectMessage(Level.WARN, - "Audience condition \"{name='nationality', type='custom_attribute', match='null', value=null}\" has an unexpected value type. You may need to upgrade to a newer release of the Optimizely SDK"); + "Audience condition \"{name='nationality', type='custom_attribute', match='null', value=null}\" has an unsupported condition value. You may need to upgrade to a newer release of the Optimizely SDK."); logbackVerifier.expectMessage(Level.DEBUG, - "Audience audience_with_missing_value evaluated to null"); + "Audience \"2196265320\" evaluated to null."); logbackVerifier.expectMessage(Level.INFO, - "Audiences for experiment experiment_with_malformed_audience collectively evaluated to null"); + "Audiences for experiment \"experiment_with_malformed_audience\" collectively evaluated to null."); } /**