Skip to content
Merged
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
28 changes: 23 additions & 5 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.optimizely.ab.event.internal.BuildVersionInfo;
import com.optimizely.ab.event.internal.EventFactory;
import com.optimizely.ab.event.internal.payload.EventBatch.ClientEngine;
import com.optimizely.ab.notification.DecisionNotification;
import com.optimizely.ab.notification.NotificationCenter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -217,7 +218,7 @@ private Variation activate(@Nonnull ProjectConfig projectConfig,
}
Map<String, ?> copiedAttributes = copyAttributes(attributes);
// bucket the user to the given experiment and dispatch an impression event
Variation variation = decisionService.getVariation(experiment, userId, copiedAttributes);
Variation variation = getVariation(experiment, userId, copiedAttributes);
if (variation == null) {
logger.info("Not activating user \"{}\" for experiment \"{}\".", userId, experiment.getKey());
return null;
Expand Down Expand Up @@ -389,7 +390,7 @@ public Boolean isFeatureEnabled(@Nonnull String featureKey,
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes);

if (featureDecision.variation != null) {
if (featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.EXPERIMENT)) {
if (featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.FEATURE_TEST)) {
sendImpression(
projectConfig,
featureDecision.experiment,
Expand Down Expand Up @@ -718,8 +719,25 @@ public Variation getVariation(@Nonnull Experiment experiment,
@Nonnull String userId,
@Nonnull Map<String, ?> attributes) throws UnknownExperimentException {
Map<String, ?> copiedAttributes = copyAttributes(attributes);
Variation variation = decisionService.getVariation(experiment, userId, copiedAttributes);

String notificationType = NotificationCenter.DecisionNotificationType.AB_TEST.toString();

if (getProjectConfig().getExperimentFeatureKeyMapping().get(experiment.getId()) != null) {
notificationType = NotificationCenter.DecisionNotificationType.FEATURE_TEST.toString();
}

return decisionService.getVariation(experiment, userId, copiedAttributes);
DecisionNotification decisionNotification = DecisionNotification.newExperimentDecisionNotificationBuilder()
.withUserId(userId)
.withAttributes(copiedAttributes)
.withExperimentKey(experiment.getKey())
.withVariation(variation)
.withType(notificationType)
.build();

notificationCenter.sendNotifications(decisionNotification);

return variation;
}

@Nullable
Expand Down Expand Up @@ -754,8 +772,8 @@ public Variation getVariation(@Nonnull String experimentKey,
// if we're unable to retrieve the associated experiment, return null
return null;
}
Map<String, ?> copiedAttributes = copyAttributes(attributes);
return decisionService.getVariation(experiment, userId, copiedAttributes);

return getVariation(experiment, userId, attributes);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag,
Variation variation = this.getVariation(experiment, userId, filteredAttributes);
if (variation != null) {
return new FeatureDecision(experiment, variation,
FeatureDecision.DecisionSource.EXPERIMENT);
FeatureDecision.DecisionSource.FEATURE_TEST);
}
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,19 @@ public class FeatureDecision {
public DecisionSource decisionSource;

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

private final String key;

DecisionSource(String key) {
this.key = key;
}

@Override
public String toString() {
return key;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public String toString() {
private final Map<String, EventType> eventNameMapping;
private final Map<String, Experiment> experimentKeyMapping;
private final Map<String, FeatureFlag> featureKeyMapping;
private final Map<String, List<String>> experimentFeatureKeyMapping;

// id to entity mappings
private final Map<String, Audience> audienceIdMapping;
Expand Down Expand Up @@ -216,6 +217,9 @@ public ProjectConfig(String accountId,
this.experimentIdMapping = ProjectConfigUtils.generateIdMapping(this.experiments);
this.groupIdMapping = ProjectConfigUtils.generateIdMapping(groups);
this.rolloutIdMapping = ProjectConfigUtils.generateIdMapping(this.rollouts);

// Generate experiment to featureFlag list mapping to identify if experiment is AB-Test experiment or Feature-Test Experiment.
this.experimentFeatureKeyMapping = ProjectConfigUtils.generateExperimentFeatureMapping(this.featureFlags);
}

/**
Expand Down Expand Up @@ -419,6 +423,10 @@ public Map<String, FeatureFlag> getFeatureKeyMapping() {
return featureKeyMapping;
}

public Map<String, List<String>> getExperimentFeatureKeyMapping() {
return experimentFeatureKeyMapping;
}

public ConcurrentHashMap<String, ConcurrentHashMap<String, String>> getForcedVariationMapping() {
return forcedVariationMapping;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,22 @@ public static <T extends IdMapped> Map<String, T> generateIdMapping(List<T> name
return Collections.unmodifiableMap(nameMapping);
}

/**
* Helper method for creating convenience mappings of ExperimentID to featureFlags it is included in.
*/
public static Map<String, List<String>> generateExperimentFeatureMapping(List<FeatureFlag> featureFlags) {
Map<String, List<String>> experimentFeatureMap = new HashMap<>();
for (FeatureFlag featureFlag : featureFlags) {
for (String experimentId : featureFlag.getExperimentIds()) {
if (experimentFeatureMap.containsKey(experimentId)) {
experimentFeatureMap.get(experimentId).add(featureFlag.getKey());
} else {
ArrayList<String> featureFlagKeysList = new ArrayList<>();
featureFlagKeysList.add(featureFlag.getKey());
experimentFeatureMap.put(experimentId, featureFlagKeysList);
}
}
}
return Collections.unmodifiableMap(experimentFeatureMap);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import javax.annotation.Nonnull;
import java.util.Map;


@Deprecated
public abstract class ActivateNotificationListener implements NotificationListener, ActivateNotificationListenerInterface {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import javax.annotation.Nonnull;
import java.util.Map;

@Deprecated
public interface ActivateNotificationListenerInterface {
/**
* onActivate called when an activate was triggered
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/****************************************************************************
* Copyright 2019, 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. *
* 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.notification;


import com.optimizely.ab.bucketing.FeatureDecision;
import com.optimizely.ab.config.FeatureVariable;
import com.optimizely.ab.config.Variation;

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

public class DecisionNotification {
protected String type;
protected String userId;
protected Map<String, ?> attributes;
protected Map<String, ?> decisionInfo;

protected DecisionNotification() {
}

protected DecisionNotification(@Nonnull String type,
@Nonnull String userId,
@Nullable Map<String, ?> attributes,
@Nonnull Map<String, ?> decisionInfo) {
this.type = type;
this.userId = userId;
if (attributes == null) {
attributes = new HashMap<>();
}
this.attributes = attributes;
this.decisionInfo = decisionInfo;
}

public String getType() {
return type;
}

public String getUserId() {
return userId;
}

public Map<String, ?> getAttributes() {
return attributes;
}

public Map<String, ?> getDecisionInfo() {
return decisionInfo;
}

public static ExperimentDecisionNotificationBuilder newExperimentDecisionNotificationBuilder() {
return new ExperimentDecisionNotificationBuilder();
}

public static class ExperimentDecisionNotificationBuilder {
public final static String EXPERIMENT_KEY = "experiment_key";
public final static String VARIATION_KEY = "variation_key";

private String type;
private String experimentKey;
private Variation variation;
private String userId;
private Map<String, ?> attributes;
private Map<String, Object> decisionInfo;

public ExperimentDecisionNotificationBuilder withUserId(String userId) {
this.userId = userId;
return this;
}

public ExperimentDecisionNotificationBuilder withAttributes(Map<String, ?> attributes) {
this.attributes = attributes;
return this;
}

public ExperimentDecisionNotificationBuilder withExperimentKey(String experimentKey) {
this.experimentKey = experimentKey;
return this;
}

public ExperimentDecisionNotificationBuilder withType(String type) {
this.type = type;
return this;
}

public ExperimentDecisionNotificationBuilder withVariation(Variation variation) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel this should be withVariationKey. Just seems inconsistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually I am passing variation here so that on build we can verify null check before getting key from its object

this.variation = variation;
return this;
}

public DecisionNotification build() {
decisionInfo = new HashMap<>();
decisionInfo.put(EXPERIMENT_KEY, experimentKey);
decisionInfo.put(VARIATION_KEY, variation != null ? variation.getKey() : null);

return new DecisionNotification(
type,
userId,
attributes,
decisionInfo);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/****************************************************************************
* Copyright 2019, 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. *
* 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.notification;

import javax.annotation.Nonnull;

public interface DecisionNotificationListener {

/**
* onDecision called when an activate was triggered
*
* @param decisionNotification - The decision notification object containing:
* type - The notification type.
* userId - The userId passed to the API.
* attributes - The attribute map passed to the API.
* decisionInfo - The decision information containing all parameters passed in API.
*/
void onDecision(@Nonnull DecisionNotification decisionNotification);
}
Loading