Skip to content

Commit fbb8b56

Browse files
committed
Merge branch 'master' into 2.0.x
2 parents 3c08efc + 7a71f03 commit fbb8b56

27 files changed

+2500
-511
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Optimizely Java X SDK Changelog
22

3+
## 1.8.1
4+
December 12, 2017
5+
6+
This is a patch release for 1.8.0. It contains two bug fixes mentioned below.
7+
8+
### Bug Fixes
9+
SDK returns NullPointerException when activating with unknown attribute.
10+
11+
Pooled connection times out if it is idle for a long time (AsyncEventHandler's HttpClient uses PoolingHttpClientConnectionManager setting a validate interval).
12+
313
## 2.0.0 Beta 2
414
October 5, 2017
515

core-api/src/main/java/com/optimizely/ab/Optimizely.java

+66-44
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2016-2017, Optimizely, Inc. and contributors *
2+
* Copyright 2016-2018, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -18,6 +18,7 @@
1818
import com.optimizely.ab.annotations.VisibleForTesting;
1919
import com.optimizely.ab.bucketing.Bucketer;
2020
import com.optimizely.ab.bucketing.DecisionService;
21+
import com.optimizely.ab.bucketing.FeatureDecision;
2122
import com.optimizely.ab.bucketing.UserProfileService;
2223
import com.optimizely.ab.config.Attribute;
2324
import com.optimizely.ab.config.EventType;
@@ -40,6 +41,7 @@
4041
import com.optimizely.ab.event.internal.payload.Event.ClientEngine;
4142
import com.optimizely.ab.internal.EventTagUtils;
4243
import com.optimizely.ab.notification.NotificationBroadcaster;
44+
import com.optimizely.ab.notification.NotificationCenter;
4345
import com.optimizely.ab.notification.NotificationListener;
4446
import org.slf4j.Logger;
4547
import org.slf4j.LoggerFactory;
@@ -90,6 +92,8 @@ public class Optimizely {
9092
@VisibleForTesting final EventHandler eventHandler;
9193
@VisibleForTesting final ErrorHandler errorHandler;
9294
@VisibleForTesting final NotificationBroadcaster notificationBroadcaster = new NotificationBroadcaster();
95+
public final NotificationCenter notificationCenter = new NotificationCenter();
96+
9397
@Nullable private final UserProfileService userProfileService;
9498

9599
private Optimizely(@Nonnull ProjectConfig projectConfig,
@@ -203,6 +207,9 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
203207
}
204208

205209
notificationBroadcaster.broadcastExperimentActivated(experiment, userId, filteredAttributes, variation);
210+
211+
notificationCenter.sendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId,
212+
filteredAttributes, variation, impressionEvent);
206213
} else {
207214
logger.info("Experiment has \"Launched\" status so not dispatching event during activation.");
208215
}
@@ -289,6 +296,8 @@ public void track(@Nonnull String eventName,
289296

290297
notificationBroadcaster.broadcastEventTracked(eventName, userId, filteredAttributes, eventValue,
291298
conversionEvent);
299+
notificationCenter.sendNotifications(NotificationCenter.NotificationType.Track, eventName, userId,
300+
filteredAttributes, eventTags, conversionEvent);
292301
}
293302

294303
//======== FeatureFlag APIs ========//
@@ -322,36 +331,39 @@ public void track(@Nonnull String eventName,
322331
public @Nonnull Boolean isFeatureEnabled(@Nonnull String featureKey,
323332
@Nonnull String userId,
324333
@Nonnull Map<String, String> attributes) {
334+
if (featureKey == null) {
335+
logger.warn("The featureKey parameter must be nonnull.");
336+
return false;
337+
}
338+
else if (userId == null) {
339+
logger.warn("The userId parameter must be nonnull.");
340+
return false;
341+
}
325342
FeatureFlag featureFlag = projectConfig.getFeatureKeyMapping().get(featureKey);
326343
if (featureFlag == null) {
327-
logger.info("No feature flag was found for key \"" + featureKey + "\".");
344+
logger.info("No feature flag was found for key \"{}\".", featureKey);
328345
return false;
329346
}
330347

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

333-
Variation variation = decisionService.getVariationForFeature(featureFlag, userId, filteredAttributes);
334-
335-
if (variation != null) {
336-
Experiment experiment = projectConfig.getExperimentForVariationId(variation.getId());
337-
if (experiment != null) {
338-
// the user is in an experiment for the feature
350+
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, filteredAttributes);
351+
if (featureDecision.variation != null) {
352+
if (featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.EXPERIMENT)) {
339353
sendImpression(
340354
projectConfig,
341-
experiment,
355+
featureDecision.experiment,
342356
userId,
343357
filteredAttributes,
344-
variation);
345-
}
346-
else {
347-
logger.info("The user \"" + userId +
348-
"\" is not being experimented on in feature \"" + featureKey + "\".");
358+
featureDecision.variation);
359+
} else {
360+
logger.info("The user \"{}\" is not included in an experiment for feature \"{}\".",
361+
userId, featureKey);
349362
}
350-
logger.info("Feature \"" + featureKey + "\" is enabled for user \"" + userId + "\".");
363+
logger.info("Feature \"{}\" is enabled for user \"{}\".", featureKey, userId);
351364
return true;
352-
}
353-
else {
354-
logger.info("Feature \"" + featureKey + "\" is not enabled for user \"" + userId + "\".");
365+
} else {
366+
logger.info("Feature \"{}\" is not enabled for user \"{}\".", featureKey, userId);
355367
return false;
356368
}
357369
}
@@ -433,10 +445,9 @@ public void track(@Nonnull String eventName,
433445
if (variableValue != null) {
434446
try {
435447
return Double.parseDouble(variableValue);
436-
}
437-
catch (NumberFormatException exception) {
448+
} catch (NumberFormatException exception) {
438449
logger.error("NumberFormatException while trying to parse \"" + variableValue +
439-
"\" as Double. " + exception);
450+
"\" as Double. " + exception);
440451
}
441452
}
442453
return null;
@@ -479,10 +490,9 @@ public void track(@Nonnull String eventName,
479490
if (variableValue != null) {
480491
try {
481492
return Integer.parseInt(variableValue);
482-
}
483-
catch (NumberFormatException exception) {
493+
} catch (NumberFormatException exception) {
484494
logger.error("NumberFormatException while trying to parse \"" + variableValue +
485-
"\" as Integer. " + exception.toString());
495+
"\" as Integer. " + exception.toString());
486496
}
487497
}
488498
return null;
@@ -529,19 +539,30 @@ String getFeatureVariableValueForType(@Nonnull String featureKey,
529539
@Nonnull String userId,
530540
@Nonnull Map<String, String> attributes,
531541
@Nonnull LiveVariable.VariableType variableType) {
542+
if (featureKey == null) {
543+
logger.warn("The featureKey parameter must be nonnull.");
544+
return null;
545+
}
546+
else if (variableKey == null) {
547+
logger.warn("The variableKey parameter must be nonnull.");
548+
return null;
549+
}
550+
else if (userId == null) {
551+
logger.warn("The userId parameter must be nonnull.");
552+
return null;
553+
}
532554
FeatureFlag featureFlag = projectConfig.getFeatureKeyMapping().get(featureKey);
533555
if (featureFlag == null) {
534-
logger.info("No feature flag was found for key \"" + featureKey + "\".");
556+
logger.info("No feature flag was found for key \"{}\".", featureKey);
535557
return null;
536558
}
537559

538560
LiveVariable variable = featureFlag.getVariableKeyToLiveVariableMap().get(variableKey);
539-
if (variable == null) {
540-
logger.info("No feature variable was found for key \"" + variableKey + "\" in feature flag \"" +
541-
featureKey + "\".");
561+
if (variable == null) {
562+
logger.info("No feature variable was found for key \"{}\" in feature flag \"{}\".",
563+
variableKey, featureKey);
542564
return null;
543-
}
544-
else if (!variable.getType().equals(variableType)) {
565+
} else if (!variable.getType().equals(variableType)) {
545566
logger.info("The feature variable \"" + variableKey +
546567
"\" is actually of type \"" + variable.getType().toString() +
547568
"\" type. You tried to access it as type \"" + variableType.toString() +
@@ -551,23 +572,19 @@ else if (!variable.getType().equals(variableType)) {
551572

552573
String variableValue = variable.getDefaultValue();
553574

554-
Variation variation = decisionService.getVariationForFeature(featureFlag, userId, attributes);
555-
556-
if (variation != null) {
575+
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, attributes);
576+
if (featureDecision.variation != null) {
557577
LiveVariableUsageInstance liveVariableUsageInstance =
558-
variation.getVariableIdToLiveVariableUsageInstanceMap().get(variable.getId());
578+
featureDecision.variation.getVariableIdToLiveVariableUsageInstanceMap().get(variable.getId());
559579
if (liveVariableUsageInstance != null) {
560580
variableValue = liveVariableUsageInstance.getValue();
561-
}
562-
else {
581+
} else {
563582
variableValue = variable.getDefaultValue();
564583
}
565-
}
566-
else {
567-
logger.info("User \"" + userId +
568-
"\" was not bucketed into any variation for feature flag \"" + featureKey +
569-
"\". The default value \"" + variableValue +
570-
"\" for \"" + variableKey + "\" is being returned."
584+
} else {
585+
logger.info("User \"{}\" was not bucketed into any variation for feature flag \"{}\". " +
586+
"The default value \"{}\" for \"{}\" is being returned.",
587+
userId, featureKey, variableValue, variableKey
571588
);
572589
}
573590

@@ -697,6 +714,7 @@ public UserProfileService getUserProfileService() {
697714
*
698715
* @param listener listener to add
699716
*/
717+
@Deprecated
700718
public void addNotificationListener(@Nonnull NotificationListener listener) {
701719
notificationBroadcaster.addListener(listener);
702720
}
@@ -706,13 +724,15 @@ public void addNotificationListener(@Nonnull NotificationListener listener) {
706724
*
707725
* @param listener listener to remove
708726
*/
727+
@Deprecated
709728
public void removeNotificationListener(@Nonnull NotificationListener listener) {
710729
notificationBroadcaster.removeListener(listener);
711730
}
712731

713732
/**
714733
* Remove all {@link NotificationListener}.
715734
*/
735+
@Deprecated
716736
public void clearNotificationListeners() {
717737
notificationBroadcaster.clearListeners();
718738
}
@@ -782,7 +802,8 @@ private EventType getEventTypeOrThrow(ProjectConfig projectConfig, String eventN
782802
* {@link ProjectConfig}.
783803
*
784804
* @param projectConfig the current project config
785-
* @param attributes the attributes map to validate and potentially filter
805+
* @param attributes the attributes map to validate and potentially filter. The reserved key for bucketing id
806+
* {@link DecisionService#BUCKETING_ATTRIBUTE} is kept.
786807
* @return the filtered attributes map (containing only attributes that are present in the project config) or an
787808
* empty map if a null attributes object is passed in
788809
*/
@@ -797,7 +818,8 @@ private Map<String, String> filterAttributes(@Nonnull ProjectConfig projectConfi
797818

798819
Map<String, Attribute> attributeKeyMapping = projectConfig.getAttributeKeyMapping();
799820
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
800-
if (!attributeKeyMapping.containsKey(attribute.getKey())) {
821+
if (!attributeKeyMapping.containsKey(attribute.getKey()) &&
822+
attribute.getKey() != com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE) {
801823
if (unknownAttributes == null) {
802824
unknownAttributes = new ArrayList<String>();
803825
}

core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java

+20-17
Original file line numberDiff line numberDiff line change
@@ -76,87 +76,90 @@ private String bucketToEntity(int bucketValue, List<TrafficAllocation> trafficAl
7676
}
7777

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

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

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

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

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

9998
private Variation bucketToVariation(@Nonnull Experiment experiment,
100-
@Nonnull String userId) {
99+
@Nonnull String bucketingId) {
101100
// "salt" the bucket id using the experiment id
102101
String experimentId = experiment.getId();
103102
String experimentKey = experiment.getKey();
104-
String combinedBucketId = userId + experimentId;
103+
String combinedBucketId = bucketingId + experimentId;
105104

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

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

112111
String bucketedVariationId = bucketToEntity(bucketValue, trafficAllocations);
113112
if (bucketedVariationId != null) {
114113
Variation bucketedVariation = experiment.getVariationIdToVariationMap().get(bucketedVariationId);
115114
String variationKey = bucketedVariation.getKey();
116-
logger.info("User \"{}\" is in variation \"{}\" of experiment \"{}\".", userId, variationKey,
117-
experimentKey);
115+
logger.info("User with bucketingId \"{}\" is in variation \"{}\" of experiment \"{}\".", bucketingId, variationKey,
116+
experimentKey);
118117

119118
return bucketedVariation;
120119
}
121120

122121
// user was not bucketed to a variation
123-
logger.info("User \"{}\" is not in any variation of experiment \"{}\".", userId, experimentKey);
122+
logger.info("User with bucketingId \"{}\" is not in any variation of experiment \"{}\".", bucketingId, experimentKey);
124123
return null;
125124
}
126125

127126
/**
128127
* Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3.
129128
* @param experiment The Experiment in which the user is to be bucketed.
130-
* @param userId User Identifier
129+
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash.
131130
* @return Variation the user is bucketed into or null.
132131
*/
133132
public @Nullable Variation bucket(@Nonnull Experiment experiment,
134-
@Nonnull String userId) {
133+
@Nonnull String bucketingId) {
135134
// ---------- Bucket User ----------
136135
String groupId = experiment.getGroupId();
137136
// check whether the experiment belongs to a group
138137
if (!groupId.isEmpty()) {
139138
Group experimentGroup = projectConfig.getGroupIdMapping().get(groupId);
140139
// bucket to an experiment only if group entities are to be mutually exclusive
141140
if (experimentGroup.getPolicy().equals(Group.RANDOM_POLICY)) {
142-
Experiment bucketedExperiment = bucketToExperiment(experimentGroup, userId);
141+
Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId);
143142
if (bucketedExperiment == null) {
143+
logger.info("User with bucketingId \"{}\" is not in any experiment of group {}.", bucketingId, experimentGroup.getId());
144144
return null;
145+
}
146+
else {
147+
145148
}
146149
// if the experiment a user is bucketed in within a group isn't the same as the experiment provided,
147150
// don't perform further bucketing within the experiment
148151
if (!bucketedExperiment.getId().equals(experiment.getId())) {
149-
logger.info("User \"{}\" is not in experiment \"{}\" of group {}.", userId, experiment.getKey(),
152+
logger.info("User with bucketingId \"{}\" is not in experiment \"{}\" of group {}.", bucketingId, experiment.getKey(),
150153
experimentGroup.getId());
151154
return null;
152155
}
153156

154-
logger.info("User \"{}\" is in experiment \"{}\" of group {}.", userId, experiment.getKey(),
157+
logger.info("User with bucketingId \"{}\" is in experiment \"{}\" of group {}.", bucketingId, experiment.getKey(),
155158
experimentGroup.getId());
156159
}
157160
}
158161

159-
return bucketToVariation(experiment, userId);
162+
return bucketToVariation(experiment, bucketingId);
160163
}
161164

162165

0 commit comments

Comments
 (0)