Skip to content

Commit

Permalink
Merge branch 'master' into 2.0.x
Browse files Browse the repository at this point in the history
  • Loading branch information
wangjoshuah committed Jan 5, 2018
2 parents 3c08efc + 7a71f03 commit fbb8b56
Show file tree
Hide file tree
Showing 27 changed files with 2,500 additions and 511 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Optimizely Java X SDK Changelog

## 1.8.1
December 12, 2017

This is a patch release for 1.8.0. It contains two bug fixes mentioned below.

### Bug Fixes
SDK returns NullPointerException when activating with unknown attribute.

Pooled connection times out if it is idle for a long time (AsyncEventHandler's HttpClient uses PoolingHttpClientConnectionManager setting a validate interval).

## 2.0.0 Beta 2
October 5, 2017

Expand Down
110 changes: 66 additions & 44 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2016-2017, Optimizely, Inc. and contributors *
* Copyright 2016-2018, 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. *
Expand All @@ -18,6 +18,7 @@
import com.optimizely.ab.annotations.VisibleForTesting;
import com.optimizely.ab.bucketing.Bucketer;
import com.optimizely.ab.bucketing.DecisionService;
import com.optimizely.ab.bucketing.FeatureDecision;
import com.optimizely.ab.bucketing.UserProfileService;
import com.optimizely.ab.config.Attribute;
import com.optimizely.ab.config.EventType;
Expand All @@ -40,6 +41,7 @@
import com.optimizely.ab.event.internal.payload.Event.ClientEngine;
import com.optimizely.ab.internal.EventTagUtils;
import com.optimizely.ab.notification.NotificationBroadcaster;
import com.optimizely.ab.notification.NotificationCenter;
import com.optimizely.ab.notification.NotificationListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -90,6 +92,8 @@ public class Optimizely {
@VisibleForTesting final EventHandler eventHandler;
@VisibleForTesting final ErrorHandler errorHandler;
@VisibleForTesting final NotificationBroadcaster notificationBroadcaster = new NotificationBroadcaster();
public final NotificationCenter notificationCenter = new NotificationCenter();

@Nullable private final UserProfileService userProfileService;

private Optimizely(@Nonnull ProjectConfig projectConfig,
Expand Down Expand Up @@ -203,6 +207,9 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
}

notificationBroadcaster.broadcastExperimentActivated(experiment, userId, filteredAttributes, variation);

notificationCenter.sendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId,
filteredAttributes, variation, impressionEvent);
} else {
logger.info("Experiment has \"Launched\" status so not dispatching event during activation.");
}
Expand Down Expand Up @@ -289,6 +296,8 @@ public void track(@Nonnull String eventName,

notificationBroadcaster.broadcastEventTracked(eventName, userId, filteredAttributes, eventValue,
conversionEvent);
notificationCenter.sendNotifications(NotificationCenter.NotificationType.Track, eventName, userId,
filteredAttributes, eventTags, conversionEvent);
}

//======== FeatureFlag APIs ========//
Expand Down Expand Up @@ -322,36 +331,39 @@ public void track(@Nonnull String eventName,
public @Nonnull Boolean isFeatureEnabled(@Nonnull String featureKey,
@Nonnull String userId,
@Nonnull Map<String, String> attributes) {
if (featureKey == null) {
logger.warn("The featureKey parameter must be nonnull.");
return false;
}
else if (userId == null) {
logger.warn("The userId parameter must be nonnull.");
return false;
}
FeatureFlag featureFlag = projectConfig.getFeatureKeyMapping().get(featureKey);
if (featureFlag == null) {
logger.info("No feature flag was found for key \"" + featureKey + "\".");
logger.info("No feature flag was found for key \"{}\".", featureKey);
return false;
}

Map<String, String> filteredAttributes = filterAttributes(projectConfig, attributes);

Variation variation = decisionService.getVariationForFeature(featureFlag, userId, filteredAttributes);

if (variation != null) {
Experiment experiment = projectConfig.getExperimentForVariationId(variation.getId());
if (experiment != null) {
// the user is in an experiment for the feature
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, filteredAttributes);
if (featureDecision.variation != null) {
if (featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.EXPERIMENT)) {
sendImpression(
projectConfig,
experiment,
featureDecision.experiment,
userId,
filteredAttributes,
variation);
}
else {
logger.info("The user \"" + userId +
"\" is not being experimented on in feature \"" + featureKey + "\".");
featureDecision.variation);
} else {
logger.info("The user \"{}\" is not included in an experiment for feature \"{}\".",
userId, featureKey);
}
logger.info("Feature \"" + featureKey + "\" is enabled for user \"" + userId + "\".");
logger.info("Feature \"{}\" is enabled for user \"{}\".", featureKey, userId);
return true;
}
else {
logger.info("Feature \"" + featureKey + "\" is not enabled for user \"" + userId + "\".");
} else {
logger.info("Feature \"{}\" is not enabled for user \"{}\".", featureKey, userId);
return false;
}
}
Expand Down Expand Up @@ -433,10 +445,9 @@ public void track(@Nonnull String eventName,
if (variableValue != null) {
try {
return Double.parseDouble(variableValue);
}
catch (NumberFormatException exception) {
} catch (NumberFormatException exception) {
logger.error("NumberFormatException while trying to parse \"" + variableValue +
"\" as Double. " + exception);
"\" as Double. " + exception);
}
}
return null;
Expand Down Expand Up @@ -479,10 +490,9 @@ public void track(@Nonnull String eventName,
if (variableValue != null) {
try {
return Integer.parseInt(variableValue);
}
catch (NumberFormatException exception) {
} catch (NumberFormatException exception) {
logger.error("NumberFormatException while trying to parse \"" + variableValue +
"\" as Integer. " + exception.toString());
"\" as Integer. " + exception.toString());
}
}
return null;
Expand Down Expand Up @@ -529,19 +539,30 @@ String getFeatureVariableValueForType(@Nonnull String featureKey,
@Nonnull String userId,
@Nonnull Map<String, String> attributes,
@Nonnull LiveVariable.VariableType variableType) {
if (featureKey == null) {
logger.warn("The featureKey parameter must be nonnull.");
return null;
}
else if (variableKey == null) {
logger.warn("The variableKey parameter must be nonnull.");
return null;
}
else if (userId == null) {
logger.warn("The userId parameter must be nonnull.");
return null;
}
FeatureFlag featureFlag = projectConfig.getFeatureKeyMapping().get(featureKey);
if (featureFlag == null) {
logger.info("No feature flag was found for key \"" + featureKey + "\".");
logger.info("No feature flag was found for key \"{}\".", featureKey);
return null;
}

LiveVariable variable = featureFlag.getVariableKeyToLiveVariableMap().get(variableKey);
if (variable == null) {
logger.info("No feature variable was found for key \"" + variableKey + "\" in feature flag \"" +
featureKey + "\".");
if (variable == null) {
logger.info("No feature variable was found for key \"{}\" in feature flag \"{}\".",
variableKey, featureKey);
return null;
}
else if (!variable.getType().equals(variableType)) {
} else if (!variable.getType().equals(variableType)) {
logger.info("The feature variable \"" + variableKey +
"\" is actually of type \"" + variable.getType().toString() +
"\" type. You tried to access it as type \"" + variableType.toString() +
Expand All @@ -551,23 +572,19 @@ else if (!variable.getType().equals(variableType)) {

String variableValue = variable.getDefaultValue();

Variation variation = decisionService.getVariationForFeature(featureFlag, userId, attributes);

if (variation != null) {
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, attributes);
if (featureDecision.variation != null) {
LiveVariableUsageInstance liveVariableUsageInstance =
variation.getVariableIdToLiveVariableUsageInstanceMap().get(variable.getId());
featureDecision.variation.getVariableIdToLiveVariableUsageInstanceMap().get(variable.getId());
if (liveVariableUsageInstance != null) {
variableValue = liveVariableUsageInstance.getValue();
}
else {
} else {
variableValue = variable.getDefaultValue();
}
}
else {
logger.info("User \"" + userId +
"\" was not bucketed into any variation for feature flag \"" + featureKey +
"\". The default value \"" + variableValue +
"\" for \"" + variableKey + "\" is being returned."
} else {
logger.info("User \"{}\" was not bucketed into any variation for feature flag \"{}\". " +
"The default value \"{}\" for \"{}\" is being returned.",
userId, featureKey, variableValue, variableKey
);
}

Expand Down Expand Up @@ -697,6 +714,7 @@ public UserProfileService getUserProfileService() {
*
* @param listener listener to add
*/
@Deprecated
public void addNotificationListener(@Nonnull NotificationListener listener) {
notificationBroadcaster.addListener(listener);
}
Expand All @@ -706,13 +724,15 @@ public void addNotificationListener(@Nonnull NotificationListener listener) {
*
* @param listener listener to remove
*/
@Deprecated
public void removeNotificationListener(@Nonnull NotificationListener listener) {
notificationBroadcaster.removeListener(listener);
}

/**
* Remove all {@link NotificationListener}.
*/
@Deprecated
public void clearNotificationListeners() {
notificationBroadcaster.clearListeners();
}
Expand Down Expand Up @@ -782,7 +802,8 @@ private EventType getEventTypeOrThrow(ProjectConfig projectConfig, String eventN
* {@link ProjectConfig}.
*
* @param projectConfig the current project config
* @param attributes the attributes map to validate and potentially filter
* @param attributes the attributes map to validate and potentially filter. The reserved key for bucketing id
* {@link DecisionService#BUCKETING_ATTRIBUTE} is kept.
* @return the filtered attributes map (containing only attributes that are present in the project config) or an
* empty map if a null attributes object is passed in
*/
Expand All @@ -797,7 +818,8 @@ private Map<String, String> filterAttributes(@Nonnull ProjectConfig projectConfi

Map<String, Attribute> attributeKeyMapping = projectConfig.getAttributeKeyMapping();
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
if (!attributeKeyMapping.containsKey(attribute.getKey())) {
if (!attributeKeyMapping.containsKey(attribute.getKey()) &&
attribute.getKey() != com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE) {
if (unknownAttributes == null) {
unknownAttributes = new ArrayList<String>();
}
Expand Down
37 changes: 20 additions & 17 deletions core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,87 +76,90 @@ private String bucketToEntity(int bucketValue, List<TrafficAllocation> trafficAl
}

private Experiment bucketToExperiment(@Nonnull Group group,
@Nonnull String userId) {
@Nonnull String bucketingId) {
// "salt" the bucket id using the group id
String bucketId = userId + group.getId();
String bucketKey = bucketingId + group.getId();

List<TrafficAllocation> trafficAllocations = group.getTrafficAllocation();

int hashCode = MurmurHash3.murmurhash3_x86_32(bucketId, 0, bucketId.length(), MURMUR_HASH_SEED);
int hashCode = MurmurHash3.murmurhash3_x86_32(bucketKey, 0, bucketKey.length(), MURMUR_HASH_SEED);
int bucketValue = generateBucketValue(hashCode);
logger.debug("Assigned bucket {} to user \"{}\" during experiment bucketing.", bucketValue, userId);
logger.debug("Assigned bucket {} to user with bucketingId \"{}\" during experiment bucketing.", bucketValue, bucketingId);

String bucketedExperimentId = bucketToEntity(bucketValue, trafficAllocations);
if (bucketedExperimentId != null) {
return projectConfig.getExperimentIdMapping().get(bucketedExperimentId);
}

// user was not bucketed to an experiment in the group
logger.info("User \"{}\" is not in any experiment of group {}.", userId, group.getId());
return null;
}

private Variation bucketToVariation(@Nonnull Experiment experiment,
@Nonnull String userId) {
@Nonnull String bucketingId) {
// "salt" the bucket id using the experiment id
String experimentId = experiment.getId();
String experimentKey = experiment.getKey();
String combinedBucketId = userId + experimentId;
String combinedBucketId = bucketingId + experimentId;

List<TrafficAllocation> trafficAllocations = experiment.getTrafficAllocation();

int hashCode = MurmurHash3.murmurhash3_x86_32(combinedBucketId, 0, combinedBucketId.length(), MURMUR_HASH_SEED);
int bucketValue = generateBucketValue(hashCode);
logger.debug("Assigned bucket {} to user \"{}\" during variation bucketing.", bucketValue, userId);
logger.debug("Assigned bucket {} to user with bucketingId \"{}\" when bucketing to a variation.", bucketValue, bucketingId);

String bucketedVariationId = bucketToEntity(bucketValue, trafficAllocations);
if (bucketedVariationId != null) {
Variation bucketedVariation = experiment.getVariationIdToVariationMap().get(bucketedVariationId);
String variationKey = bucketedVariation.getKey();
logger.info("User \"{}\" is in variation \"{}\" of experiment \"{}\".", userId, variationKey,
experimentKey);
logger.info("User with bucketingId \"{}\" is in variation \"{}\" of experiment \"{}\".", bucketingId, variationKey,
experimentKey);

return bucketedVariation;
}

// user was not bucketed to a variation
logger.info("User \"{}\" is not in any variation of experiment \"{}\".", userId, experimentKey);
logger.info("User with bucketingId \"{}\" is not in any variation of experiment \"{}\".", bucketingId, experimentKey);
return null;
}

/**
* Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3.
* @param experiment The Experiment in which the user is to be bucketed.
* @param userId User Identifier
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash.
* @return Variation the user is bucketed into or null.
*/
public @Nullable Variation bucket(@Nonnull Experiment experiment,
@Nonnull String userId) {
@Nonnull String bucketingId) {
// ---------- Bucket User ----------
String groupId = experiment.getGroupId();
// check whether the experiment belongs to a group
if (!groupId.isEmpty()) {
Group experimentGroup = projectConfig.getGroupIdMapping().get(groupId);
// bucket to an experiment only if group entities are to be mutually exclusive
if (experimentGroup.getPolicy().equals(Group.RANDOM_POLICY)) {
Experiment bucketedExperiment = bucketToExperiment(experimentGroup, userId);
Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId);
if (bucketedExperiment == null) {
logger.info("User with bucketingId \"{}\" is not in any experiment of group {}.", bucketingId, experimentGroup.getId());
return null;
}
else {

}
// if the experiment a user is bucketed in within a group isn't the same as the experiment provided,
// don't perform further bucketing within the experiment
if (!bucketedExperiment.getId().equals(experiment.getId())) {
logger.info("User \"{}\" is not in experiment \"{}\" of group {}.", userId, experiment.getKey(),
logger.info("User with bucketingId \"{}\" is not in experiment \"{}\" of group {}.", bucketingId, experiment.getKey(),
experimentGroup.getId());
return null;
}

logger.info("User \"{}\" is in experiment \"{}\" of group {}.", userId, experiment.getKey(),
logger.info("User with bucketingId \"{}\" is in experiment \"{}\" of group {}.", bucketingId, experiment.getKey(),
experimentGroup.getId());
}
}

return bucketToVariation(experiment, userId);
return bucketToVariation(experiment, bucketingId);
}


Expand Down
Loading

0 comments on commit fbb8b56

Please sign in to comment.