Skip to content

[FSSDK-11522] feat: update decision service to apply ho #576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.optimizely.ab.config.DatafileProjectConfig;
import com.optimizely.ab.config.EventType;
import com.optimizely.ab.config.Experiment;
import com.optimizely.ab.config.ExperimentCore;
import com.optimizely.ab.config.FeatureFlag;
import com.optimizely.ab.config.FeatureVariable;
import com.optimizely.ab.config.FeatureVariableUsageInstance;
Expand Down Expand Up @@ -319,7 +320,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
* @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
*/
private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
@Nullable Experiment experiment,
@Nullable ExperimentCore experiment,
@Nonnull String userId,
@Nonnull Map<String, ?> filteredAttributes,
@Nullable Variation variation,
Expand Down
27 changes: 17 additions & 10 deletions core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,32 @@
*/
package com.optimizely.ab.bucketing;

import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.optimizely.ab.annotations.VisibleForTesting;
import com.optimizely.ab.bucketing.internal.MurmurHash3;
import com.optimizely.ab.config.*;
import com.optimizely.ab.config.Experiment;
import com.optimizely.ab.config.ExperimentCore;
import com.optimizely.ab.config.Group;
import com.optimizely.ab.config.ProjectConfig;
import com.optimizely.ab.config.TrafficAllocation;
import com.optimizely.ab.config.Variation;
import com.optimizely.ab.optimizelydecision.DecisionReasons;
import com.optimizely.ab.optimizelydecision.DecisionResponse;
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import java.util.List;

/**
* Default Optimizely bucketing algorithm that evenly distributes users using the Murmur3 hash of some provided
* identifier.
* <p>
* The user identifier <i>must</i> be provided in the first data argument passed to
* {@link #bucket(Experiment, String, ProjectConfig)} and <i>must</i> be non-null and non-empty.
* {@link #bucket(ExperimentCore, String, ProjectConfig)} and <i>must</i> be non-null and non-empty.
*
* @see <a href="https://en.wikipedia.org/wiki/MurmurHash">MurmurHash</a>
*/
Expand Down Expand Up @@ -89,7 +96,7 @@ private Experiment bucketToExperiment(@Nonnull Group group,
}

@Nonnull
private DecisionResponse<Variation> bucketToVariation(@Nonnull Experiment experiment,
private DecisionResponse<Variation> bucketToVariation(@Nonnull ExperimentCore experiment,
@Nonnull String bucketingId) {
DecisionReasons reasons = DefaultDecisionReasons.newInstance();

Expand Down Expand Up @@ -130,7 +137,7 @@ private DecisionResponse<Variation> bucketToVariation(@Nonnull Experiment experi
* @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons
*/
@Nonnull
public DecisionResponse<Variation> bucket(@Nonnull Experiment experiment,
public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
@Nonnull String bucketingId,
@Nonnull ProjectConfig projectConfig) {
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,39 @@
***************************************************************************/
package com.optimizely.ab.bucketing;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.optimizely.ab.OptimizelyDecisionContext;
import com.optimizely.ab.OptimizelyForcedDecision;
import com.optimizely.ab.OptimizelyRuntimeException;
import com.optimizely.ab.OptimizelyUserContext;
import com.optimizely.ab.config.*;
import com.optimizely.ab.config.Experiment;
import com.optimizely.ab.config.FeatureFlag;
import com.optimizely.ab.config.Holdout;
import com.optimizely.ab.config.ProjectConfig;
import com.optimizely.ab.config.Rollout;
import com.optimizely.ab.config.Variation;
import com.optimizely.ab.error.ErrorHandler;
import com.optimizely.ab.internal.ControlAttribute;
import com.optimizely.ab.internal.ExperimentUtils;
import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.EXPERIMENT;
import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.RULE;
import com.optimizely.ab.optimizelydecision.DecisionReasons;
import com.optimizely.ab.optimizelydecision.DecisionResponse;
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

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.
Expand Down Expand Up @@ -240,10 +252,22 @@ public List<DecisionResponse<FeatureDecision>> getVariationsForFeatureList(@Non

List<DecisionResponse<FeatureDecision>> decisions = new ArrayList<>();

for (FeatureFlag featureFlag: featureFlags) {
flagLoop: for (FeatureFlag featureFlag: featureFlags) {
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
reasons.merge(upsReasons);

List<Holdout> holdouts = projectConfig.getHoldoutForFlag(featureFlag.getId());
if (!holdouts.isEmpty()) {
for (Holdout holdout : holdouts) {
DecisionResponse<Variation> holdoutDecision = getVariationForHoldout(holdout, user, projectConfig);
reasons.merge(holdoutDecision.getReasons());
if (holdoutDecision.getResult() != null) {
decisions.add(new DecisionResponse<>(new FeatureDecision(holdout, holdoutDecision.getResult(), FeatureDecision.DecisionSource.HOLDOUT), reasons));
continue flagLoop;
}
}
}

DecisionResponse<FeatureDecision> decisionVariationResponse = getVariationFromExperiment(projectConfig, featureFlag, user, options, userProfileTracker);
reasons.merge(decisionVariationResponse.getReasons());

Expand Down Expand Up @@ -419,6 +443,54 @@ DecisionResponse<Variation> getWhitelistedVariation(@Nonnull Experiment experime
return new DecisionResponse(null, reasons);
}

/**
* Determines the variation for a holdout rule.
*
* @param holdout The holdout rule to evaluate.
* @param user The user context.
* @param projectConfig The current project configuration.
* @return A {@link DecisionResponse} with the variation (if any) and reasons.
*/
@Nonnull
DecisionResponse<Variation> getVariationForHoldout(@Nonnull Holdout holdout,
@Nonnull OptimizelyUserContext user,
@Nonnull ProjectConfig projectConfig) {
DecisionReasons reasons = DefaultDecisionReasons.newInstance();

if (!holdout.isActive()) {
String message = reasons.addInfo("Holdout (%s) is not running.", holdout.getKey());
logger.info(message);
return new DecisionResponse<>(null, reasons);
}

DecisionResponse<Boolean> decisionMeetAudience = ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, holdout, user, EXPERIMENT, holdout.getKey());
reasons.merge(decisionMeetAudience.getReasons());

if (decisionMeetAudience.getResult()) {
// User meets audience conditions for holdout
String audienceMatchMessage = reasons.addInfo("User (%s) meets audience conditions for holdout (%s).", user.getUserId(), holdout.getKey());
logger.info(audienceMatchMessage);

String bucketingId = getBucketingId(user.getUserId(), user.getAttributes());
DecisionResponse<Variation> decisionVariation = bucketer.bucket(holdout, bucketingId, projectConfig);
reasons.merge(decisionVariation.getReasons());
Variation variation = decisionVariation.getResult();

if (variation != null) {
String message = reasons.addInfo("User (%s) is in variation (%s) of holdout (%s).", user.getUserId(), variation.getKey(), holdout.getKey());
logger.info(message);
} else {
String message = reasons.addInfo("User (%s) is in no holdout variation.", user.getUserId());
logger.info(message);
}
return new DecisionResponse<>(variation, reasons);
}

String message = reasons.addInfo("User (%s) does not meet conditions for holdout (%s).", user.getUserId(), holdout.getKey());
logger.info(message);
return new DecisionResponse<>(null, reasons);
}


// TODO: Logically, it makes sense to move this method to UserProfileTracker. But some tests are also calling this
// method, requiring us to refactor those tests as well. We'll look to refactor this later.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
***************************************************************************/
package com.optimizely.ab.bucketing;

import com.optimizely.ab.config.Experiment;
import com.optimizely.ab.config.Variation;

import javax.annotation.Nullable;

import com.optimizely.ab.config.ExperimentCore;
import com.optimizely.ab.config.Variation;

public class FeatureDecision {
/**
* The {@link Experiment} the Feature is associated with.
* The {@link ExperimentCore} the Feature is associated with.
*/
@Nullable
public Experiment experiment;
public ExperimentCore experiment;

/**
* The {@link Variation} the user was bucketed into.
Expand All @@ -41,7 +41,8 @@ public class FeatureDecision {

public enum DecisionSource {
FEATURE_TEST("feature-test"),
ROLLOUT("rollout");
ROLLOUT("rollout"),
HOLDOUT("holdout");

private final String key;

Expand All @@ -58,11 +59,11 @@ public String toString() {
/**
* Initialize a FeatureDecision object.
*
* @param experiment The {@link Experiment} the Feature is associated with.
* @param experiment The {@link ExperimentCore} the Feature is associated with.
* @param variation The {@link Variation} the user was bucketed into.
* @param decisionSource The source of the variation.
*/
public FeatureDecision(@Nullable Experiment experiment, @Nullable Variation variation,
public FeatureDecision(@Nullable ExperimentCore experiment, @Nullable Variation variation,
@Nullable DecisionSource decisionSource) {
this.experiment = experiment;
this.variation = variation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,26 @@
*/
package com.optimizely.ab.event.internal;

import java.util.Map;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.optimizely.ab.bucketing.FeatureDecision;
import com.optimizely.ab.config.Experiment;
import com.optimizely.ab.config.ExperimentCore;
import com.optimizely.ab.config.ProjectConfig;
import com.optimizely.ab.config.Variation;
import com.optimizely.ab.event.internal.payload.DecisionMetadata;
import com.optimizely.ab.internal.EventTagUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Map;

public class UserEventFactory {
private static final Logger logger = LoggerFactory.getLogger(UserEventFactory.class);

public static ImpressionEvent createImpressionEvent(@Nonnull ProjectConfig projectConfig,
@Nullable Experiment activatedExperiment,
@Nullable ExperimentCore activatedExperiment,
@Nullable Variation variation,
@Nonnull String userId,
@Nonnull Map<String, ?> attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,24 @@
*/
package com.optimizely.ab.internal;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nonnull;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.optimizely.ab.OptimizelyUserContext;
import com.optimizely.ab.config.Experiment;
import com.optimizely.ab.config.ExperimentCore;
import com.optimizely.ab.config.ProjectConfig;
import com.optimizely.ab.config.audience.AudienceIdCondition;
import com.optimizely.ab.config.audience.Condition;
import com.optimizely.ab.config.audience.OrCondition;
import com.optimizely.ab.optimizelydecision.DecisionReasons;
import com.optimizely.ab.optimizelydecision.DecisionResponse;
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public final class ExperimentUtils {

Expand Down Expand Up @@ -62,7 +64,7 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment) {
*/
@Nonnull
public static DecisionResponse<Boolean> doesUserMeetAudienceConditions(@Nonnull ProjectConfig projectConfig,
@Nonnull Experiment experiment,
@Nonnull ExperimentCore experiment,
@Nonnull OptimizelyUserContext user,
@Nonnull String loggingEntityType,
@Nonnull String loggingKey) {
Expand All @@ -86,7 +88,7 @@ public static DecisionResponse<Boolean> doesUserMeetAudienceConditions(@Nonnull

@Nonnull
public static DecisionResponse<Boolean> evaluateAudience(@Nonnull ProjectConfig projectConfig,
@Nonnull Experiment experiment,
@Nonnull ExperimentCore experiment,
@Nonnull OptimizelyUserContext user,
@Nonnull String loggingEntityType,
@Nonnull String loggingKey) {
Expand Down Expand Up @@ -118,7 +120,7 @@ public static DecisionResponse<Boolean> evaluateAudience(@Nonnull ProjectConfig

@Nonnull
public static DecisionResponse<Boolean> evaluateAudienceConditions(@Nonnull ProjectConfig projectConfig,
@Nonnull Experiment experiment,
@Nonnull ExperimentCore experiment,
@Nonnull OptimizelyUserContext user,
@Nonnull String loggingEntityType,
@Nonnull String loggingKey) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
*/
package com.optimizely.ab.notification;

import java.util.Map;

import com.optimizely.ab.annotations.VisibleForTesting;
import com.optimizely.ab.config.Experiment;
import com.optimizely.ab.config.ExperimentCore;
import com.optimizely.ab.config.Variation;
import com.optimizely.ab.event.LogEvent;

import java.util.Map;

/**
* ActivateNotification supplies notification for AB activatation.
*
Expand All @@ -32,7 +32,7 @@
@Deprecated
public final class ActivateNotification {

private final Experiment experiment;
private final ExperimentCore experiment;
private final String userId;
private final Map<String, ?> attributes;
private final Variation variation;
Expand All @@ -50,15 +50,15 @@ public final class ActivateNotification {
* @param variation - The variation that was returned from activate.
* @param event - The impression event that was triggered.
*/
public ActivateNotification(Experiment experiment, String userId, Map<String, ?> attributes, Variation variation, LogEvent event) {
public ActivateNotification(ExperimentCore experiment, String userId, Map<String, ?> attributes, Variation variation, LogEvent event) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we keep Experiment? I'm concerned about breaking changes in this public APIs.
This legacy API is not related to HOs, so no need to support HO type.

this.experiment = experiment;
this.userId = userId;
this.attributes = attributes;
this.variation = variation;
this.event = event;
}

public Experiment getExperiment() {
public ExperimentCore getExperiment() {
return experiment;
}

Expand Down
Loading
Loading