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

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -240,10 +240,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 +431,50 @@ 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()) {
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,18 @@
***************************************************************************/
package com.optimizely.ab.bucketing;

import javax.annotation.Nullable;

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

import javax.annotation.Nullable;

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

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

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

private final String key;

Expand All @@ -62,7 +64,7 @@ public String toString() {
* @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) {
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@

package com.optimizely.ab.notification;

import java.util.Map;

import javax.annotation.Nonnull;

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 javax.annotation.Nonnull;
import java.util.Map;

/**
* ActivateNotificationListener handles the activate event notification.
*
Expand Down Expand Up @@ -80,7 +82,7 @@ public final void handle(ActivateNotification message) {
* @param variation - The variation that was returned from activate.
* @param event - The impression event that was triggered.
*/
public abstract void onActivate(@Nonnull Experiment experiment,
public abstract void onActivate(@Nonnull ExperimentCore experiment,
@Nonnull String userId,
@Nonnull Map<String, ?> attributes,
@Nonnull Variation variation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
*/
package com.optimizely.ab.notification;

import com.optimizely.ab.config.Experiment;
import com.optimizely.ab.config.Variation;
import com.optimizely.ab.event.LogEvent;
import java.util.Map;

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

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

/**
* ActivateNotificationListenerInterface provides and interface for activate event notification.
Expand All @@ -40,7 +41,7 @@ public interface ActivateNotificationListenerInterface {
* @param variation - The variation that was returned from activate.
* @param event - The impression event that was triggered.
*/
public void onActivate(@Nonnull Experiment experiment,
public void onActivate(@Nonnull ExperimentCore experiment,
@Nonnull String userId,
@Nonnull Map<String, ?> attributes,
@Nonnull Variation variation,
Expand Down
Loading
Loading