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 fede007d5..0b3cb8d8c 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2016-2023, Optimizely, Inc. and contributors * + * Copyright 2016-2024, 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. * @@ -42,6 +42,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; + import java.io.Closeable; import java.util.*; @@ -1191,45 +1192,40 @@ private OptimizelyUserContext createUserContextCopy(@Nonnull String userId, @Non return new OptimizelyUserContext(this, userId, attributes, Collections.EMPTY_MAP, null, false); } + Optional getForcedDecision(@Nonnull String flagKey, + @Nonnull DecisionReasons decisionReasons, + @Nonnull ProjectConfig projectConfig, + @Nonnull OptimizelyUserContext user) { + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); + DecisionResponse forcedDecisionVariation = decisionService.validatedForcedDecision(optimizelyDecisionContext, projectConfig, user); + decisionReasons.merge(forcedDecisionVariation.getReasons()); + if (forcedDecisionVariation.getResult() != null) { + return Optional.of(new FeatureDecision(null, forcedDecisionVariation.getResult(), FeatureDecision.DecisionSource.FEATURE_TEST)); + } + + return Optional.empty(); + } + OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, @Nonnull String key, @Nonnull List options) { - ProjectConfig projectConfig = getProjectConfig(); if (projectConfig == null) { return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason()); } + return decideForKeys(user, Arrays.asList(key), options).get(key); + } - FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key); - if (flag == null) { - return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key)); - } - + private OptimizelyDecision createOptimizelyDecision( + OptimizelyUserContext user, + String flagKey, + FeatureDecision flagDecision, + DecisionReasons decisionReasons, + List allOptions, + ProjectConfig projectConfig + ) { String userId = user.getUserId(); - Map attributes = user.getAttributes(); - Boolean decisionEventDispatched = false; - List allOptions = getAllOptions(options); - DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions); - - Map copiedAttributes = new HashMap<>(attributes); - FeatureDecision flagDecision; - - // Check Forced Decision - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flag.getKey(), null); - DecisionResponse forcedDecisionVariation = decisionService.validatedForcedDecision(optimizelyDecisionContext, projectConfig, user); - decisionReasons.merge(forcedDecisionVariation.getReasons()); - if (forcedDecisionVariation.getResult() != null) { - flagDecision = new FeatureDecision(null, forcedDecisionVariation.getResult(), FeatureDecision.DecisionSource.FEATURE_TEST); - } else { - // Regular decision - DecisionResponse decisionVariation = decisionService.getVariationForFeature( - flag, - user, - projectConfig, - allOptions); - flagDecision = decisionVariation.getResult(); - decisionReasons.merge(decisionVariation.getReasons()); - } Boolean flagEnabled = false; if (flagDecision.variation != null) { @@ -1237,12 +1233,12 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, flagEnabled = true; } } - logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled); + logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", flagKey, userId, flagEnabled); Map variableMap = new HashMap<>(); if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) { DecisionResponse> decisionVariables = getDecisionVariableMap( - flag, + projectConfig.getFeatureKeyMapping().get(flagKey), flagDecision.variation, flagEnabled); variableMap = decisionVariables.getResult(); @@ -1261,6 +1257,12 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, // add to event metadata as well (currently set to experimentKey) String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null; + + Boolean decisionEventDispatched = false; + + Map attributes = user.getAttributes(); + Map copiedAttributes = new HashMap<>(attributes); + if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) { decisionEventDispatched = sendImpression( projectConfig, @@ -1268,7 +1270,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, userId, copiedAttributes, flagDecision.variation, - key, + flagKey, decisionSource.toString(), flagEnabled); } @@ -1276,7 +1278,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder() .withUserId(userId) .withAttributes(copiedAttributes) - .withFlagKey(key) + .withFlagKey(flagKey) .withEnabled(flagEnabled) .withVariables(variableMap) .withVariationKey(variationKey) @@ -1291,7 +1293,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user, flagEnabled, optimizelyJSON, ruleKey, - key, + flagKey, user, reasonsToReport); } @@ -1303,7 +1305,7 @@ Map decideForKeys(@Nonnull OptimizelyUserContext use ProjectConfig projectConfig = getProjectConfig(); if (projectConfig == null) { - logger.error("Optimizely instance is not valid, failing isFeatureEnabled call."); + logger.error("Optimizely instance is not valid, failing decideForKeys call."); return decisionMap; } @@ -1311,10 +1313,53 @@ Map decideForKeys(@Nonnull OptimizelyUserContext use List allOptions = getAllOptions(options); + Map flagDecisions = new HashMap<>(); + Map decisionReasonsMap = new HashMap<>(); + + List flagsWithoutForcedDecision = new ArrayList<>(); + + List validKeys = new ArrayList<>(); + for (String key : keys) { - OptimizelyDecision decision = decide(user, key, options); - if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.getEnabled()) { - decisionMap.put(key, decision); + FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key); + if (flag == null) { + decisionMap.put(key, OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key))); + continue; + } + + validKeys.add(key); + + DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions); + Optional forcedDecision = getForcedDecision(key, decisionReasons, projectConfig, user); + decisionReasonsMap.put(key, decisionReasons); + + if (forcedDecision.isPresent()) { + flagDecisions.put(key, forcedDecision.get()); + } else { + flagsWithoutForcedDecision.add(flag); + } + } + + List> decisionList = + decisionService.getVariationsForFeatureList(flagsWithoutForcedDecision, user, projectConfig, allOptions); + + for (int i = 0; i < flagsWithoutForcedDecision.size(); i++) { + DecisionResponse decision = decisionList.get(i); + String flagKey = flagsWithoutForcedDecision.get(i).getKey(); + flagDecisions.put(flagKey, decision.getResult()); + decisionReasonsMap.get(flagKey).merge(decision.getReasons()); + } + + for (String key: validKeys) { + FeatureDecision flagDecision = flagDecisions.get(key); + DecisionReasons decisionReasons = decisionReasonsMap.get((key)); + + OptimizelyDecision optimizelyDecision = createOptimizelyDecision( + user, key, flagDecision, decisionReasons, allOptions, projectConfig + ); + + if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || optimizelyDecision.getEnabled()) { + decisionMap.put(key, optimizelyDecision); } } 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 84d47d03f..9a288130c 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-2022, Optimizely, Inc. and contributors * + * Copyright 2017-2022, 2024, 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. * @@ -78,21 +78,16 @@ public DecisionService(@Nonnull Bucketer bucketer, this.userProfileService = userProfileService; } - /** - * 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 user The current OptimizelyUserContext - * @param projectConfig The current projectConfig - * @param options An array of decision options - * @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons - */ @Nonnull public DecisionResponse getVariation(@Nonnull Experiment experiment, @Nonnull OptimizelyUserContext user, @Nonnull ProjectConfig projectConfig, - @Nonnull List options) { - DecisionReasons reasons = DefaultDecisionReasons.newInstance(); + @Nonnull List options, + @Nullable UserProfileTracker userProfileTracker, + @Nullable DecisionReasons reasons) { + if (reasons == null) { + reasons = DefaultDecisionReasons.newInstance(); + } if (!ExperimentUtils.isExperimentActive(experiment)) { String message = reasons.addInfo("Experiment \"%s\" is not running.", experiment.getKey()); @@ -116,39 +111,13 @@ public DecisionResponse getVariation(@Nonnull Experiment experiment, return new DecisionResponse(variation, reasons); } - // 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 && !ignoreUPS) { - try { - Map userProfileMap = userProfileService.lookup(user.getUserId()); - if (userProfileMap == null) { - 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.addInfo("The UserProfileService returned an invalid map."); - logger.warn(message); - } - } catch (Exception exception) { - String message = reasons.addInfo(exception.getMessage()); - logger.error(message); - errorHandler.handleError(new OptimizelyRuntimeException(exception)); - } - - // check if user exists in user profile - if (userProfile != null) { - decisionVariation = getStoredVariation(experiment, userProfile, projectConfig); - reasons.merge(decisionVariation.getReasons()); - variation = decisionVariation.getResult(); - // return the stored variation if it exists - if (variation != null) { - return new DecisionResponse(variation, reasons); - } - } else { // if we could not find a user profile, make a new one - userProfile = new UserProfile(user.getUserId(), new HashMap()); + if (userProfileTracker != null) { + decisionVariation = getStoredVariation(experiment, userProfileTracker.userProfile, projectConfig); + reasons.merge(decisionVariation.getReasons()); + variation = decisionVariation.getResult(); + // return the stored variation if it exists + if (variation != null) { + return new DecisionResponse(variation, reasons); } } @@ -162,8 +131,9 @@ public DecisionResponse getVariation(@Nonnull Experiment experiment, variation = decisionVariation.getResult(); if (variation != null) { - if (userProfileService != null && !ignoreUPS) { - saveVariation(experiment, variation, userProfile); + if (userProfileTracker != null) { + updateUserProfile(experiment, variation, userProfileTracker.userProfile); + userProfileTracker.profileUpdated = true; } else { logger.debug("This decision will not be saved since the UserProfileService is null."); } @@ -177,6 +147,41 @@ public DecisionResponse getVariation(@Nonnull Experiment experiment, return new DecisionResponse(null, reasons); } + /** + * 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 user The current OptimizelyUserContext + * @param projectConfig The current projectConfig + * @param options An array of decision options + * @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons + */ + @Nonnull + public DecisionResponse getVariation(@Nonnull Experiment experiment, + @Nonnull OptimizelyUserContext user, + @Nonnull ProjectConfig projectConfig, + @Nonnull List options) { + DecisionReasons reasons = DefaultDecisionReasons.newInstance(); + + // fetch the user profile map from the user profile service + boolean ignoreUPS = options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); +// UserProfile userProfile = null; + UserProfileTracker userProfileTracker = null; + + if (userProfileService != null && !ignoreUPS) { + UserProfile userProfile = getUserProfile(user.getUserId(), reasons); + userProfileTracker = new UserProfileTracker(userProfile, false); + } + + + DecisionResponse response = getVariation(experiment, user, projectConfig, options, userProfileTracker, reasons); + + if(userProfileService != null && !ignoreUPS && userProfileTracker.profileUpdated) { + saveUserProfile(userProfileTracker.userProfile); + } + return response; + } + @Nonnull public DecisionResponse getVariation(@Nonnull Experiment experiment, @Nonnull OptimizelyUserContext user, @@ -198,31 +203,107 @@ public DecisionResponse getVariationForFeature(@Nonnull Feature @Nonnull OptimizelyUserContext user, @Nonnull ProjectConfig projectConfig, @Nonnull List options) { - DecisionReasons reasons = DefaultDecisionReasons.newInstance(); + return getVariationsForFeatureList(Arrays.asList(featureFlag), user, projectConfig, options).get(0); + } - DecisionResponse decisionVariationResponse = getVariationFromExperiment(projectConfig, featureFlag, user, options); - reasons.merge(decisionVariationResponse.getReasons()); + private UserProfile getUserProfile(String userId, DecisionReasons reasons) { + UserProfile userProfile = null; - FeatureDecision decision = decisionVariationResponse.getResult(); - if (decision != null) { - return new DecisionResponse(decision, reasons); + try { + Map userProfileMap = userProfileService.lookup(userId); + if (userProfileMap == null) { + 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.addInfo("The UserProfileService returned an invalid map."); + logger.warn(message); + } + } catch (Exception exception) { + String message = reasons.addInfo(exception.getMessage()); + logger.error(message); + errorHandler.handleError(new OptimizelyRuntimeException(exception)); + } + + if (userProfile == null) { + userProfile = new UserProfile(userId, new HashMap()); } - DecisionResponse decisionFeatureResponse = getVariationForFeatureInRollout(featureFlag, user, projectConfig); - reasons.merge(decisionFeatureResponse.getReasons()); - decision = decisionFeatureResponse.getResult(); + return userProfile; + } - String message; - if (decision.variation == null) { - message = reasons.addInfo("The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".", - user.getUserId(), featureFlag.getKey()); - } else { - message = reasons.addInfo("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", - user.getUserId(), featureFlag.getKey()); + static class UserProfileTracker { + public UserProfile userProfile; + public boolean profileUpdated; + + UserProfileTracker(UserProfile userProfile, boolean profileUpdated) { + this.userProfile = userProfile; + this.profileUpdated = profileUpdated; } - logger.info(message); + } + + /** + * Get the variation the user is bucketed into for the FeatureFlag + * + * @param featureFlags The feature flag list the user wants to access. + * @param user The current OptimizelyuserContext + * @param projectConfig The current projectConfig + * @param options An array of decision options + * @return A {@link DecisionResponse} including a {@link FeatureDecision} and the decision reasons + */ + @Nonnull + public List> getVariationsForFeatureList(@Nonnull List featureFlags, + @Nonnull OptimizelyUserContext user, + @Nonnull ProjectConfig projectConfig, + @Nonnull List options) { + DecisionReasons upsReasons = DefaultDecisionReasons.newInstance(); + + boolean ignoreUPS = options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); + UserProfileTracker userProfileTracker = null; + + if (userProfileService != null && !ignoreUPS) { + UserProfile userProfile = getUserProfile(user.getUserId(), upsReasons); + userProfileTracker = new UserProfileTracker(userProfile, false); + } + + List> decisions = new ArrayList<>(); + + for (FeatureFlag featureFlag: featureFlags) { + DecisionReasons reasons = DefaultDecisionReasons.newInstance(); + reasons.merge(upsReasons); + + DecisionResponse decisionVariationResponse = getVariationFromExperiment(projectConfig, featureFlag, user, options, userProfileTracker); + reasons.merge(decisionVariationResponse.getReasons()); + + FeatureDecision decision = decisionVariationResponse.getResult(); + if (decision != null) { + decisions.add(new DecisionResponse(decision, reasons)); + continue; + } + + DecisionResponse decisionFeatureResponse = getVariationForFeatureInRollout(featureFlag, user, projectConfig); + reasons.merge(decisionFeatureResponse.getReasons()); + decision = decisionFeatureResponse.getResult(); + + String message; + if (decision.variation == null) { + message = reasons.addInfo("The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".", + user.getUserId(), featureFlag.getKey()); + } else { + message = reasons.addInfo("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", + user.getUserId(), featureFlag.getKey()); + } + logger.info(message); - return new DecisionResponse(decision, reasons); + decisions.add(new DecisionResponse(decision, reasons)); + } + + if (userProfileService != null && !ignoreUPS && userProfileTracker.profileUpdated) { + saveUserProfile(userProfileTracker.userProfile); + } + + return decisions; } @Nonnull @@ -244,13 +325,15 @@ public DecisionResponse getVariationForFeature(@Nonnull Feature DecisionResponse getVariationFromExperiment(@Nonnull ProjectConfig projectConfig, @Nonnull FeatureFlag featureFlag, @Nonnull OptimizelyUserContext user, - @Nonnull List options) { + @Nonnull List options, + @Nullable UserProfileTracker userProfileTracker) { DecisionReasons reasons = DefaultDecisionReasons.newInstance(); if (!featureFlag.getExperimentIds().isEmpty()) { for (String experimentId : featureFlag.getExperimentIds()) { Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId); - DecisionResponse decisionVariation = getVariationFromExperimentRule(projectConfig, featureFlag.getKey(), experiment, user, options); + DecisionResponse decisionVariation = + getVariationFromExperimentRule(projectConfig, featureFlag.getKey(), experiment, user, options, userProfileTracker); reasons.merge(decisionVariation.getReasons()); Variation variation = decisionVariation.getResult(); @@ -412,6 +495,31 @@ DecisionResponse getStoredVariation(@Nonnull Experiment experiment, } } + /** + * Save a {@link Variation} of an {@link Experiment} for a user in the {@link UserProfileService}. + * + * @param experiment The experiment the user was buck + * @param variation The Variation to save. + * @param userProfile A {@link UserProfile} instance of the user information. + */ + void updateUserProfile(@Nonnull Experiment experiment, + @Nonnull Variation variation, + @Nonnull UserProfile userProfile) { + + String experimentId = experiment.getId(); + String variationId = variation.getId(); + Decision decision; + if (userProfile.experimentBucketMap.containsKey(experimentId)) { + decision = userProfile.experimentBucketMap.get(experimentId); + decision.variationId = variationId; + } else { + decision = new Decision(variationId); + } + userProfile.experimentBucketMap.put(experimentId, decision); + logger.info("Updated variation \"{}\" of experiment \"{}\" for user \"{}\".", + variationId, experimentId, userProfile.userId); + } + /** * Save a {@link Variation} of an {@link Experiment} for a user in the {@link UserProfileService}. * @@ -448,6 +556,22 @@ void saveVariation(@Nonnull Experiment experiment, } } + void saveUserProfile(@Nonnull UserProfile userProfile) { + if (userProfileService == null) { + return; + } + + try { + userProfileService.save(userProfile.toMap()); + logger.info("Saved user profile of user \"{}\".", + userProfile.userId); + } catch (Exception exception) { + logger.warn("Failed to save user profile of user \"{}\".", + userProfile.userId); + errorHandler.handleError(new OptimizelyRuntimeException(exception)); + } + } + /** * Get the bucketingId of a user if a bucketingId exists in attributes, or else default to userId. * @@ -615,11 +739,12 @@ public DecisionResponse getForcedVariation(@Nonnull Experiment experi } - public DecisionResponse getVariationFromExperimentRule(@Nonnull ProjectConfig projectConfig, + private DecisionResponse getVariationFromExperimentRule(@Nonnull ProjectConfig projectConfig, @Nonnull String flagKey, @Nonnull Experiment rule, @Nonnull OptimizelyUserContext user, - @Nonnull List options) { + @Nonnull List options, + @Nullable UserProfileTracker userProfileTracker) { DecisionReasons reasons = DefaultDecisionReasons.newInstance(); String ruleKey = rule != null ? rule.getKey() : null; @@ -634,7 +759,7 @@ public DecisionResponse getVariationFromExperimentRule(@Nonnull Proje return new DecisionResponse(variation, reasons); } //regular decision - DecisionResponse decisionResponse = getVariation(rule, user, projectConfig, options); + DecisionResponse decisionResponse = getVariation(rule, user, projectConfig, options, userProfileTracker, null); reasons.merge(decisionResponse.getReasons()); variation = decisionResponse.getResult(); 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 0c07ef56a..54213dc6a 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2021-2023, Optimizely and contributors + * Copyright 2021-2024, 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. @@ -23,7 +23,10 @@ import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.config.*; import com.optimizely.ab.config.parser.ConfigParseException; +import com.optimizely.ab.event.EventHandler; +import com.optimizely.ab.event.EventProcessor; import com.optimizely.ab.event.ForwardingEventProcessor; +import com.optimizely.ab.event.internal.ImpressionEvent; import com.optimizely.ab.event.internal.payload.DecisionMetadata; import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.notification.NotificationCenter; @@ -37,7 +40,10 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.CountDownLatch; @@ -345,9 +351,11 @@ public void decideAll_twoFlags() { @Test public void decideAll_allFlags() { + EventProcessor mockEventProcessor = mock(EventProcessor.class); + optimizely = new Optimizely.Builder() .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withEventProcessor(mockEventProcessor) .build(); String flagKey1 = "feature_1"; @@ -361,7 +369,6 @@ public void decideAll_allFlags() { OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); Map decisions = user.decideAll(); - assertTrue(decisions.size() == 3); assertEquals( @@ -395,9 +402,23 @@ public void decideAll_allFlags() { user, Collections.emptyList())); - eventHandler.expectImpression("10390977673", "10389729780", userId, attributes); - eventHandler.expectImpression("10420810910", "10418551353", userId, attributes); - eventHandler.expectImpression(null, "", userId, attributes); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ImpressionEvent.class); + verify(mockEventProcessor, times(3)).process(argumentCaptor.capture()); + + List sentEvents = argumentCaptor.getAllValues(); + assertEquals(sentEvents.size(), 3); + + assertEquals(sentEvents.get(0).getExperimentKey(), "exp_with_audience"); + assertEquals(sentEvents.get(0).getVariationKey(), "a"); + assertEquals(sentEvents.get(0).getUserContext().getUserId(), userId); + + + assertEquals(sentEvents.get(1).getExperimentKey(), "exp_no_audience"); + assertEquals(sentEvents.get(1).getVariationKey(), "variation_with_traffic"); + assertEquals(sentEvents.get(1).getUserContext().getUserId(), userId); + + assertEquals(sentEvents.get(2).getExperimentKey(), ""); + assertEquals(sentEvents.get(2).getUserContext().getUserId(), userId); } @Test 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 6057b43cf..350e05b28 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-2022, Optimizely, Inc. and contributors * + * Copyright 2017-2022, 2024, 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. * @@ -24,6 +24,7 @@ import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.internal.ControlAttribute; import com.optimizely.ab.internal.LogbackVerifier; +import com.optimizely.ab.optimizelydecision.DecisionReasons; import com.optimizely.ab.optimizelydecision.DecisionResponse; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.Before; @@ -297,7 +298,9 @@ public void getVariationForFeatureReturnsNullWhenItGetsNoVariationsForExperiment any(Experiment.class), any(OptimizelyUserContext.class), any(ProjectConfig.class), - anyObject() + anyObject(), + anyObject(), + any(DecisionReasons.class) ); // do not bucket to any rollouts doReturn(DecisionResponse.responseNoReasons(new FeatureDecision(null, null, null))).when(decisionService).getVariationForFeatureInRollout( @@ -381,7 +384,9 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout() eq(featureExperiment), any(OptimizelyUserContext.class), any(ProjectConfig.class), - anyObject() + anyObject(), + anyObject(), + any(DecisionReasons.class) ); // return variation for rollout @@ -413,7 +418,9 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout() any(Experiment.class), any(OptimizelyUserContext.class), any(ProjectConfig.class), - anyObject() + anyObject(), + anyObject(), + any(DecisionReasons.class) ); } @@ -438,7 +445,9 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails eq(featureExperiment), any(OptimizelyUserContext.class), any(ProjectConfig.class), - anyObject() + anyObject(), + anyObject(), + any(DecisionReasons.class) ); // return variation for rollout @@ -470,7 +479,9 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails any(Experiment.class), any(OptimizelyUserContext.class), any(ProjectConfig.class), - anyObject() + anyObject(), + anyObject(), + any(DecisionReasons.class) ); logbackVerifier.expectMessage( @@ -743,27 +754,6 @@ public void getVariationFromDeliveryRuleTest() { assertFalse(skipToEveryoneElse); } - @Test - public void getVariationFromExperimentRuleTest() { - int index = 3; - Experiment experiment = ROLLOUT_2.getExperiments().get(index); - Variation expectedVariation = null; - for (Variation variation : experiment.getVariations()) { - if (variation.getKey().equals("3137445031")) { - expectedVariation = variation; - } - } - DecisionResponse decisionResponse = decisionService.getVariationFromExperimentRule( - v4ProjectConfig, - FEATURE_FLAG_MULTI_VARIATE_FEATURE.getKey(), - experiment, - optimizely.createUserContext(genericUserId, Collections.singletonMap(ATTRIBUTE_NATIONALITY_KEY, AUDIENCE_ENGLISH_CITIZENS_VALUE)), - Collections.emptyList() - ); - - assertEquals(expectedVariation, decisionResponse.getResult()); - } - @Test public void validatedForcedDecisionWithRuleKey() { String userId = "testUser1"; @@ -961,9 +951,12 @@ public void getVariationSavesBucketedVariationIntoUserProfile() throws Exception experiment, optimizely.createUserContext(userProfileId, Collections.emptyMap()), noAudienceProjectConfig).getResult() ); logbackVerifier.expectMessage(Level.INFO, - String.format("Saved variation \"%s\" of experiment \"%s\" for user \"" + userProfileId + "\".", variation.getId(), + String.format("Updated variation \"%s\" of experiment \"%s\" for user \"" + userProfileId + "\".", variation.getId(), experiment.getId())); + logbackVerifier.expectMessage(Level.INFO, + String.format("Saved user profile of user \"%s\".", userProfileId)); + verify(userProfileService).save(eq(expectedUserProfile.toMap())); }