Skip to content

Commit 4ecd765

Browse files
authored
feat(api): Accepting all types for attributes values (#207)
* attributes parameter from <String, String> to <String, ?>
1 parent 1275c9c commit 4ecd765

19 files changed

+215
-80
lines changed

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

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Variation activate(@Nonnull String experimentKey,
120120
public @Nullable
121121
Variation activate(@Nonnull String experimentKey,
122122
@Nonnull String userId,
123-
@Nonnull Map<String, String> attributes) throws UnknownExperimentException {
123+
@Nonnull Map<String, ?> attributes) throws UnknownExperimentException {
124124

125125
if (experimentKey == null) {
126126
logger.error("The experimentKey parameter must be nonnull.");
@@ -153,7 +153,7 @@ Variation activate(@Nonnull Experiment experiment,
153153
public @Nullable
154154
Variation activate(@Nonnull Experiment experiment,
155155
@Nonnull String userId,
156-
@Nonnull Map<String, String> attributes) {
156+
@Nonnull Map<String, ?> attributes) {
157157

158158
ProjectConfig currentConfig = getProjectConfig();
159159

@@ -164,15 +164,15 @@ Variation activate(@Nonnull Experiment experiment,
164164
Variation activate(@Nonnull ProjectConfig projectConfig,
165165
@Nonnull Experiment experiment,
166166
@Nonnull String userId,
167-
@Nonnull Map<String, String> attributes) {
167+
@Nonnull Map<String, ?> attributes) {
168168

169169
if (!validateUserId(userId)){
170170
logger.info("Not activating user \"{}\" for experiment \"{}\".", userId, experiment.getKey());
171171
return null;
172172
}
173173
// determine whether all the given attributes are present in the project config. If not, filter out the unknown
174174
// attributes.
175-
Map<String, String> filteredAttributes = filterAttributes(projectConfig, attributes);
175+
Map<String, ?> filteredAttributes = filterAttributes(projectConfig, attributes);
176176

177177
// bucket the user to the given experiment and dispatch an impression event
178178
Variation variation = decisionService.getVariation(experiment, userId, filteredAttributes);
@@ -189,7 +189,7 @@ Variation activate(@Nonnull ProjectConfig projectConfig,
189189
private void sendImpression(@Nonnull ProjectConfig projectConfig,
190190
@Nonnull Experiment experiment,
191191
@Nonnull String userId,
192-
@Nonnull Map<String, String> filteredAttributes,
192+
@Nonnull Map<String, ?> filteredAttributes,
193193
@Nonnull Variation variation) {
194194
if (experiment.isRunning()) {
195195
LogEvent impressionEvent = eventFactory.createImpressionEvent(
@@ -228,13 +228,13 @@ public void track(@Nonnull String eventName,
228228

229229
public void track(@Nonnull String eventName,
230230
@Nonnull String userId,
231-
@Nonnull Map<String, String> attributes) throws UnknownEventTypeException {
231+
@Nonnull Map<String, ?> attributes) throws UnknownEventTypeException {
232232
track(eventName, userId, attributes, Collections.<String, String>emptyMap());
233233
}
234234

235235
public void track(@Nonnull String eventName,
236236
@Nonnull String userId,
237-
@Nonnull Map<String, String> attributes,
237+
@Nonnull Map<String, ?> attributes,
238238
@Nonnull Map<String, ?> eventTags) throws UnknownEventTypeException {
239239

240240
if (!validateUserId(userId)) {
@@ -259,7 +259,7 @@ public void track(@Nonnull String eventName,
259259

260260
// determine whether all the given attributes are present in the project config. If not, filter out the unknown
261261
// attributes.
262-
Map<String, String> filteredAttributes = filterAttributes(currentConfig, attributes);
262+
Map<String, ?> filteredAttributes = filterAttributes(currentConfig, attributes);
263263

264264
if (eventTags == null) {
265265
logger.warn("Event tags is null when non-null was expected. Defaulting to an empty event tags map.");
@@ -344,7 +344,7 @@ public void track(@Nonnull String eventName,
344344
*/
345345
public @Nonnull Boolean isFeatureEnabled(@Nonnull String featureKey,
346346
@Nonnull String userId,
347-
@Nonnull Map<String, String> attributes) {
347+
@Nonnull Map<String, ?> attributes) {
348348
if (featureKey == null) {
349349
logger.warn("The featureKey parameter must be nonnull.");
350350
return false;
@@ -359,7 +359,7 @@ else if (userId == null) {
359359
return false;
360360
}
361361

362-
Map<String, String> filteredAttributes = filterAttributes(projectConfig, attributes);
362+
Map<String, ?> filteredAttributes = filterAttributes(projectConfig, attributes);
363363

364364
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, filteredAttributes);
365365
if (featureDecision.variation != null) {
@@ -410,7 +410,7 @@ else if (userId == null) {
410410
public @Nullable Boolean getFeatureVariableBoolean(@Nonnull String featureKey,
411411
@Nonnull String variableKey,
412412
@Nonnull String userId,
413-
@Nonnull Map<String, String> attributes) {
413+
@Nonnull Map<String, ?> attributes) {
414414
String variableValue = getFeatureVariableValueForType(
415415
featureKey,
416416
variableKey,
@@ -450,7 +450,7 @@ else if (userId == null) {
450450
public @Nullable Double getFeatureVariableDouble(@Nonnull String featureKey,
451451
@Nonnull String variableKey,
452452
@Nonnull String userId,
453-
@Nonnull Map<String, String> attributes) {
453+
@Nonnull Map<String, ?> attributes) {
454454
String variableValue = getFeatureVariableValueForType(
455455
featureKey,
456456
variableKey,
@@ -495,7 +495,7 @@ else if (userId == null) {
495495
public @Nullable Integer getFeatureVariableInteger(@Nonnull String featureKey,
496496
@Nonnull String variableKey,
497497
@Nonnull String userId,
498-
@Nonnull Map<String, String> attributes) {
498+
@Nonnull Map<String, ?> attributes) {
499499
String variableValue = getFeatureVariableValueForType(
500500
featureKey,
501501
variableKey,
@@ -540,7 +540,7 @@ else if (userId == null) {
540540
public @Nullable String getFeatureVariableString(@Nonnull String featureKey,
541541
@Nonnull String variableKey,
542542
@Nonnull String userId,
543-
@Nonnull Map<String, String> attributes) {
543+
@Nonnull Map<String, ?> attributes) {
544544
return getFeatureVariableValueForType(
545545
featureKey,
546546
variableKey,
@@ -553,7 +553,7 @@ else if (userId == null) {
553553
String getFeatureVariableValueForType(@Nonnull String featureKey,
554554
@Nonnull String variableKey,
555555
@Nonnull String userId,
556-
@Nonnull Map<String, String> attributes,
556+
@Nonnull Map<String, ?> attributes,
557557
@Nonnull LiveVariable.VariableType variableType) {
558558
if (featureKey == null) {
559559
logger.warn("The featureKey parameter must be nonnull.");
@@ -614,7 +614,7 @@ else if (userId == null) {
614614
* @return List of the feature keys that are enabled for the user if the userId is empty it will
615615
* return Empty List.
616616
*/
617-
public List<String> getEnabledFeatures(@Nonnull String userId, @Nonnull Map<String, String> attributes) {
617+
public List<String> getEnabledFeatures(@Nonnull String userId, @Nonnull Map<String, ?> attributes) {
618618
List<String> enabledFeaturesList = new ArrayList<String>();
619619

620620
if (!validateUserId(userId)){
@@ -642,9 +642,9 @@ Variation getVariation(@Nonnull Experiment experiment,
642642
public @Nullable
643643
Variation getVariation(@Nonnull Experiment experiment,
644644
@Nonnull String userId,
645-
@Nonnull Map<String, String> attributes) throws UnknownExperimentException {
645+
@Nonnull Map<String, ?> attributes) throws UnknownExperimentException {
646646

647-
Map<String, String> filteredAttributes = filterAttributes(projectConfig, attributes);
647+
Map<String, ?> filteredAttributes = filterAttributes(projectConfig, attributes);
648648

649649
return decisionService.getVariation(experiment, userId, filteredAttributes);
650650
}
@@ -659,7 +659,7 @@ Variation getVariation(@Nonnull String experimentKey,
659659
public @Nullable
660660
Variation getVariation(@Nonnull String experimentKey,
661661
@Nonnull String userId,
662-
@Nonnull Map<String, String> attributes) {
662+
@Nonnull Map<String, ?> attributes) {
663663
if (!validateUserId(userId)) {
664664
return null;
665665
}
@@ -677,7 +677,7 @@ Variation getVariation(@Nonnull String experimentKey,
677677
return null;
678678
}
679679

680-
Map<String, String> filteredAttributes = filterAttributes(projectConfig, attributes);
680+
Map<String, ?> filteredAttributes = filterAttributes(projectConfig, attributes);
681681

682682
return decisionService.getVariation(experiment,userId,filteredAttributes);
683683
}
@@ -742,17 +742,18 @@ public UserProfileService getUserProfileService() {
742742
* @return the filtered attributes map (containing only attributes that are present in the project config) or an
743743
* empty map if a null attributes object is passed in
744744
*/
745-
private Map<String, String> filterAttributes(@Nonnull ProjectConfig projectConfig,
746-
@Nonnull Map<String, String> attributes) {
745+
private Map<String, ?> filterAttributes(@Nonnull ProjectConfig projectConfig,
746+
@Nonnull Map<String, ?> attributes) {
747747
if (attributes == null) {
748748
logger.warn("Attributes is null when non-null was expected. Defaulting to an empty attributes map.");
749749
return Collections.<String, String>emptyMap();
750750
}
751751

752+
// List of attribute keys
752753
List<String> unknownAttributes = null;
753754

754755
Map<String, Attribute> attributeKeyMapping = projectConfig.getAttributeKeyMapping();
755-
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
756+
for (Map.Entry<String, ?> attribute : attributes.entrySet()) {
756757
if (!attributeKeyMapping.containsKey(attribute.getKey()) &&
757758
!attribute.getKey().startsWith(ProjectConfig.RESERVED_ATTRIBUTE_PREFIX)) {
758759
if (unknownAttributes == null) {
@@ -765,7 +766,7 @@ private Map<String, String> filterAttributes(@Nonnull ProjectConfig projectConfi
765766
if (unknownAttributes != null) {
766767
logger.warn("Attribute(s) {} not in the datafile.", unknownAttributes);
767768
// make a copy of the passed through attributes, then remove the unknown list
768-
attributes = new HashMap<String, String>(attributes);
769+
attributes = new HashMap<>(attributes);
769770
for (String unknownAttribute : unknownAttributes) {
770771
attributes.remove(unknownAttribute);
771772
}

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

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public DecisionService(@Nonnull Bucketer bucketer,
8080
*/
8181
public @Nullable Variation getVariation(@Nonnull Experiment experiment,
8282
@Nonnull String userId,
83-
@Nonnull Map<String, String> filteredAttributes) {
83+
@Nonnull Map<String, ?> filteredAttributes) {
8484

8585
if (!ExperimentUtils.isExperimentActive(experiment)) {
8686
return null;
@@ -129,10 +129,7 @@ public DecisionService(@Nonnull Bucketer bucketer,
129129
}
130130

131131
if (ExperimentUtils.isUserInExperiment(projectConfig, experiment, filteredAttributes)) {
132-
String bucketingId = userId;
133-
if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) {
134-
bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
135-
}
132+
String bucketingId = getBucketingId(userId, filteredAttributes);
136133
variation = bucketer.bucket(experiment, bucketingId);
137134

138135
if (variation != null) {
@@ -159,7 +156,7 @@ public DecisionService(@Nonnull Bucketer bucketer,
159156
*/
160157
public @Nonnull FeatureDecision getVariationForFeature(@Nonnull FeatureFlag featureFlag,
161158
@Nonnull String userId,
162-
@Nonnull Map<String, String> filteredAttributes) {
159+
@Nonnull Map<String, ?> filteredAttributes) {
163160
if (!featureFlag.getExperimentIds().isEmpty()) {
164161
for (String experimentId : featureFlag.getExperimentIds()) {
165162
Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId);
@@ -195,7 +192,7 @@ public DecisionService(@Nonnull Bucketer bucketer,
195192
*/
196193
@Nonnull FeatureDecision getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag,
197194
@Nonnull String userId,
198-
@Nonnull Map<String, String> filteredAttributes) {
195+
@Nonnull Map<String, ?> filteredAttributes) {
199196
// use rollout to get variation for feature
200197
if (featureFlag.getRolloutId().isEmpty()) {
201198
logger.info("The feature flag \"{}\" is not used in a rollout.", featureFlag.getKey());
@@ -210,10 +207,7 @@ public DecisionService(@Nonnull Bucketer bucketer,
210207

211208
// for all rules before the everyone else rule
212209
int rolloutRulesLength = rollout.getExperiments().size();
213-
String bucketingId = userId;
214-
if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) {
215-
bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
216-
}
210+
String bucketingId = getBucketingId(userId, filteredAttributes);
217211
Variation variation;
218212
for (int i = 0; i < rolloutRulesLength - 1; i++) {
219213
Experiment rolloutRule = rollout.getExperiments().get(i);
@@ -343,4 +337,26 @@ void saveVariation(@Nonnull Experiment experiment,
343337
}
344338
}
345339
}
340+
341+
/**
342+
* Get the bucketingId of a user if a bucketingId exists in attributes, or else default to userId.
343+
* @param userId The userId of the user.
344+
* @param filteredAttributes The user's attributes. This should be filtered to just attributes in the Datafile.
345+
* @return bucketingId if it is a String type in attributes.
346+
* else return userId
347+
*/
348+
String getBucketingId(@Nonnull String userId,
349+
@Nonnull Map<String, ?> filteredAttributes) {
350+
String bucketingId = userId;
351+
if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) {
352+
if (String.class.isInstance(filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()))) {
353+
bucketingId = (String) filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
354+
logger.debug("BucketingId is valid: \"{}\"", bucketingId);
355+
}
356+
else {
357+
logger.warn("BucketingID attribute is not a string. Defaulted to userId");
358+
}
359+
}
360+
return bucketingId;
361+
}
346362
}

core-api/src/main/java/com/optimizely/ab/config/audience/AndCondition.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2017, Optimizely and contributors
3+
* Copyright 2016-2018, Optimizely and contributors
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -36,7 +36,7 @@ public List<Condition> getConditions() {
3636
return conditions;
3737
}
3838

39-
public boolean evaluate(Map<String, String> attributes) {
39+
public boolean evaluate(Map<String, ?> attributes) {
4040
for (Condition condition : conditions) {
4141
if (!condition.evaluate(attributes))
4242
return false;

core-api/src/main/java/com/optimizely/ab/config/audience/Condition.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2017, Optimizely and contributors
3+
* Copyright 2016-2018, Optimizely and contributors
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -23,5 +23,5 @@
2323
*/
2424
public interface Condition {
2525

26-
boolean evaluate(Map<String, String> attributes);
26+
boolean evaluate(Map<String, ?> attributes);
2727
}

core-api/src/main/java/com/optimizely/ab/config/audience/NotCondition.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2017, Optimizely and contributors
3+
* Copyright 2016-2018, Optimizely and contributors
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -37,7 +37,7 @@ public Condition getCondition() {
3737
return condition;
3838
}
3939

40-
public boolean evaluate(Map<String, String> attributes) {
40+
public boolean evaluate(Map<String, ?> attributes) {
4141
return !condition.evaluate(attributes);
4242
}
4343

core-api/src/main/java/com/optimizely/ab/config/audience/OrCondition.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2017, Optimizely and contributors
3+
* Copyright 2016-2018, Optimizely and contributors
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -36,7 +36,7 @@ public List<Condition> getConditions() {
3636
return conditions;
3737
}
3838

39-
public boolean evaluate(Map<String, String> attributes) {
39+
public boolean evaluate(Map<String, ?> attributes) {
4040
for (Condition condition : conditions) {
4141
if (condition.evaluate(attributes))
4242
return true;

core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2017, Optimizely and contributors
3+
* Copyright 2016-2018, Optimizely and contributors
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -29,9 +29,9 @@ public class UserAttribute implements Condition {
2929

3030
private final String name;
3131
private final String type;
32-
private final String value;
32+
private final Object value;
3333

34-
public UserAttribute(@Nonnull String name, @Nonnull String type, @Nullable String value) {
34+
public UserAttribute(@Nonnull String name, @Nonnull String type, @Nullable Object value) {
3535
this.name = name;
3636
this.type = type;
3737
this.value = value;
@@ -45,12 +45,13 @@ public String getType() {
4545
return type;
4646
}
4747

48-
public String getValue() {
48+
public Object getValue() {
4949
return value;
5050
}
5151

52-
public boolean evaluate(Map<String, String> attributes) {
53-
String userAttributeValue = attributes.get(name);
52+
public boolean evaluate(Map<String, ?> attributes) {
53+
// Valid for primative types, but needs to change when a value is an object or an array
54+
Object userAttributeValue = attributes.get(name);
5455

5556
if (value != null) { // if there is a value in the condition
5657
// check user attribute value is equal
@@ -69,7 +70,7 @@ else if (userAttributeValue != null) { // if the datafile value is null but user
6970
public String toString() {
7071
return "{name='" + name + "\'" +
7172
", type='" + type + "\'" +
72-
", value='" + value + "\'" +
73+
", value='" + value.toString() + "\'" +
7374
"}";
7475
}
7576

0 commit comments

Comments
 (0)