Skip to content

Commit 3485072

Browse files
mnoman09aliabbasrizvi
authored andcommitted
Added BotFiltering (#184)
1 parent 9803b3c commit 3485072

File tree

17 files changed

+447
-75
lines changed

17 files changed

+447
-75
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.optimizely.ab.event.internal.BuildVersionInfo;
3838
import com.optimizely.ab.event.internal.EventBuilder;
3939
import com.optimizely.ab.event.internal.payload.EventBatch.ClientEngine;
40+
import com.optimizely.ab.internal.ControlAttribute;
4041
import com.optimizely.ab.notification.NotificationCenter;
4142
import org.slf4j.Logger;
4243
import org.slf4j.LoggerFactory;
@@ -748,8 +749,8 @@ public UserProfileService getUserProfileService() {
748749
* {@link ProjectConfig}.
749750
*
750751
* @param projectConfig the current project config
751-
* @param attributes the attributes map to validate and potentially filter. The reserved key for bucketing id
752-
* {@link DecisionService#BUCKETING_ATTRIBUTE} is kept.
752+
* @param attributes the attributes map to validate and potentially filter. Attributes which starts with reserved key
753+
* {@link ProjectConfig#RESERVED_ATTRIBUTE_PREFIX} are kept.
753754
* @return the filtered attributes map (containing only attributes that are present in the project config) or an
754755
* empty map if a null attributes object is passed in
755756
*/
@@ -765,7 +766,7 @@ private Map<String, String> filterAttributes(@Nonnull ProjectConfig projectConfi
765766
Map<String, Attribute> attributeKeyMapping = projectConfig.getAttributeKeyMapping();
766767
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
767768
if (!attributeKeyMapping.containsKey(attribute.getKey()) &&
768-
attribute.getKey() != com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE) {
769+
!attribute.getKey().startsWith(ProjectConfig.RESERVED_ATTRIBUTE_PREFIX)) {
769770
if (unknownAttributes == null) {
770771
unknownAttributes = new ArrayList<String>();
771772
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2017, Optimizely, Inc. and contributors *
2+
* Copyright 2017-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. *
@@ -24,6 +24,7 @@
2424
import com.optimizely.ab.config.audience.Audience;
2525
import com.optimizely.ab.error.ErrorHandler;
2626
import com.optimizely.ab.internal.ExperimentUtils;
27+
import com.optimizely.ab.internal.ControlAttribute;
2728

2829
import org.slf4j.Logger;
2930
import org.slf4j.LoggerFactory;
@@ -46,7 +47,6 @@
4647
*/
4748
public class DecisionService {
4849

49-
public static final String BUCKETING_ATTRIBUTE = "$opt_bucketing_id";
5050
private final Bucketer bucketer;
5151
private final ErrorHandler errorHandler;
5252
private final ProjectConfig projectConfig;
@@ -130,8 +130,8 @@ public DecisionService(@Nonnull Bucketer bucketer,
130130

131131
if (ExperimentUtils.isUserInExperiment(projectConfig, experiment, filteredAttributes)) {
132132
String bucketingId = userId;
133-
if (filteredAttributes.containsKey(BUCKETING_ATTRIBUTE)) {
134-
bucketingId = filteredAttributes.get(BUCKETING_ATTRIBUTE);
133+
if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) {
134+
bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
135135
}
136136
variation = bucketer.bucket(experiment, bucketingId);
137137

@@ -211,8 +211,8 @@ public DecisionService(@Nonnull Bucketer bucketer,
211211
// for all rules before the everyone else rule
212212
int rolloutRulesLength = rollout.getExperiments().size();
213213
String bucketingId = userId;
214-
if (filteredAttributes.containsKey(BUCKETING_ATTRIBUTE)) {
215-
bucketingId = filteredAttributes.get(BUCKETING_ATTRIBUTE);
214+
if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) {
215+
bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
216216
}
217217
Variation variation;
218218
for (int i = 0; i < rolloutRulesLength - 1; i++) {

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

Lines changed: 37 additions & 1 deletion
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.
@@ -24,6 +24,7 @@
2424
import com.optimizely.ab.error.ErrorHandler;
2525
import com.optimizely.ab.error.NoOpErrorHandler;
2626
import com.optimizely.ab.error.RaiseExceptionErrorHandler;
27+
import com.optimizely.ab.internal.ControlAttribute;
2728
import org.slf4j.Logger;
2829
import org.slf4j.LoggerFactory;
2930

@@ -73,6 +74,7 @@ public String toString() {
7374
private final String revision;
7475
private final String version;
7576
private final boolean anonymizeIP;
77+
private final Boolean botFiltering;
7678
private final List<Attribute> attributes;
7779
private final List<Audience> audiences;
7880
private final List<EventType> events;
@@ -100,6 +102,8 @@ public String toString() {
100102
private final Map<String, Map<String, LiveVariableUsageInstance>> variationToLiveVariableUsageInstanceMapping;
101103
private final Map<String, Experiment> variationIdToExperimentMapping;
102104

105+
public final static String RESERVED_ATTRIBUTE_PREFIX = "$opt_";
106+
103107
/**
104108
* Forced variations supersede any other mappings. They are transient and are not persistent or part of
105109
* the actual datafile. This contains all the forced variations
@@ -123,6 +127,7 @@ public ProjectConfig(String accountId, String projectId, String version, String
123127
this(
124128
accountId,
125129
anonymizeIP,
130+
null,
126131
projectId,
127132
revision,
128133
version,
@@ -140,6 +145,7 @@ public ProjectConfig(String accountId, String projectId, String version, String
140145
// v4 constructor
141146
public ProjectConfig(String accountId,
142147
boolean anonymizeIP,
148+
Boolean botFiltering,
143149
String projectId,
144150
String revision,
145151
String version,
@@ -157,6 +163,7 @@ public ProjectConfig(String accountId,
157163
this.version = version;
158164
this.revision = revision;
159165
this.anonymizeIP = anonymizeIP;
166+
this.botFiltering = botFiltering;
160167

161168
this.attributes = Collections.unmodifiableList(attributes);
162169
this.audiences = Collections.unmodifiableList(audiences);
@@ -285,6 +292,30 @@ private List<Experiment> aggregateGroupExperiments(List<Group> groups) {
285292
return groupExperiments;
286293
}
287294

295+
/**
296+
* Checks is attributeKey is reserved or not and if it exist in attributeKeyMapping
297+
* @param attributeKey
298+
* @return AttributeId corresponding to AttributeKeyMapping, AttributeKey when it's a reserved attribute and
299+
* null when attributeKey is equal to BOT_FILTERING_ATTRIBUTE key.
300+
*/
301+
public String getAttributeId(ProjectConfig projectConfig, String attributeKey) {
302+
String attributeIdOrKey = null;
303+
com.optimizely.ab.config.Attribute attribute = projectConfig.getAttributeKeyMapping().get(attributeKey);
304+
boolean hasReservedPrefix = attributeKey.startsWith(RESERVED_ATTRIBUTE_PREFIX);
305+
if (attribute != null) {
306+
if (hasReservedPrefix) {
307+
logger.warn("Attribute {} unexpectedly has reserved prefix {}; using attribute ID instead of reserved attribute name.",
308+
attributeKey, RESERVED_ATTRIBUTE_PREFIX);
309+
}
310+
attributeIdOrKey = attribute.getId();
311+
} else if (hasReservedPrefix) {
312+
attributeIdOrKey = attributeKey;
313+
} else {
314+
logger.debug("Unrecognized Attribute \"{}\"", attributeKey);
315+
}
316+
return attributeIdOrKey;
317+
}
318+
288319
public String getAccountId() {
289320
return accountId;
290321
}
@@ -305,6 +336,10 @@ public boolean getAnonymizeIP() {
305336
return anonymizeIP;
306337
}
307338

339+
public Boolean getBotFiltering() {
340+
return botFiltering;
341+
}
342+
308343
public List<Group> getGroups() {
309344
return groups;
310345
}
@@ -540,6 +575,7 @@ public String toString() {
540575
", revision='" + revision + '\'' +
541576
", version='" + version + '\'' +
542577
", anonymizeIP=" + anonymizeIP +
578+
", botFiltering=" + botFiltering +
543579
", attributes=" + attributes +
544580
", audiences=" + audiences +
545581
", events=" + events +

core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java

Lines changed: 5 additions & 1 deletion
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.
@@ -81,14 +81,18 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
8181

8282
List<FeatureFlag> featureFlags = null;
8383
List<Rollout> rollouts = null;
84+
Boolean botFiltering = null;
8485
if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) {
8586
featureFlags = parseFeatureFlags(rootObject.getJSONArray("featureFlags"));
8687
rollouts = parseRollouts(rootObject.getJSONArray("rollouts"));
88+
if(rootObject.has("botFiltering"))
89+
botFiltering = rootObject.getBoolean("botFiltering");
8790
}
8891

8992
return new ProjectConfig(
9093
accountId,
9194
anonymizeIP,
95+
botFiltering,
9296
projectId,
9397
revision,
9498
version,

core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java

Lines changed: 7 additions & 1 deletion
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.
@@ -83,14 +83,18 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
8383

8484
List<FeatureFlag> featureFlags = null;
8585
List<Rollout> rollouts = null;
86+
Boolean botFiltering = null;
8687
if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) {
8788
featureFlags = parseFeatureFlags((JSONArray) rootObject.get("featureFlags"));
8889
rollouts = parseRollouts((JSONArray) rootObject.get("rollouts"));
90+
if(rootObject.containsKey("botFiltering"))
91+
botFiltering = (Boolean) rootObject.get("botFiltering");
8992
}
9093

9194
return new ProjectConfig(
9295
accountId,
9396
anonymizeIP,
97+
botFiltering,
9498
projectId,
9599
revision,
96100
version,
@@ -103,6 +107,8 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
103107
liveVariables,
104108
rollouts
105109
);
110+
} catch (RuntimeException ex){
111+
throw new ConfigParseException("Unable to parse datafile: " + json, ex);
106112
} catch (Exception e) {
107113
throw new ConfigParseException("Unable to parse datafile: " + json, e);
108114
}

core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java

Lines changed: 5 additions & 1 deletion
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.
@@ -82,16 +82,20 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa
8282

8383
List<FeatureFlag> featureFlags = null;
8484
List<Rollout> rollouts = null;
85+
Boolean botFiltering = null;
8586
if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) {
8687
Type featureFlagsType = new TypeToken<List<FeatureFlag>>() {}.getType();
8788
featureFlags = context.deserialize(jsonObject.getAsJsonArray("featureFlags"), featureFlagsType);
8889
Type rolloutsType = new TypeToken<List<Rollout>>() {}.getType();
8990
rollouts = context.deserialize(jsonObject.get("rollouts").getAsJsonArray(), rolloutsType);
91+
if(jsonObject.has("botFiltering"))
92+
botFiltering = jsonObject.get("botFiltering").getAsBoolean();
9093
}
9194

9295
return new ProjectConfig(
9396
accountId,
9497
anonymizeIP,
98+
botFiltering,
9599
projectId,
96100
revision,
97101
version,

core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java

Lines changed: 5 additions & 1 deletion
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.
@@ -76,16 +76,20 @@ public ProjectConfig deserialize(JsonParser parser, DeserializationContext conte
7676

7777
List<FeatureFlag> featureFlags = null;
7878
List<Rollout> rollouts = null;
79+
Boolean botFiltering = null;
7980
if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) {
8081
featureFlags = mapper.readValue(node.get("featureFlags").toString(),
8182
new TypeReference<List<FeatureFlag>>() {});
8283
rollouts = mapper.readValue(node.get("rollouts").toString(),
8384
new TypeReference<List<Rollout>>(){});
85+
if (node.hasNonNull("botFiltering"))
86+
botFiltering = node.get("botFiltering").asBoolean();
8487
}
8588

8689
return new ProjectConfig(
8790
accountId,
8891
anonymizeIP,
92+
botFiltering,
8993
projectId,
9094
revision,
9195
version,

core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilder.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package com.optimizely.ab.event.internal;
1818

1919
import com.optimizely.ab.annotations.VisibleForTesting;
20-
import com.optimizely.ab.bucketing.DecisionService;
2120
import com.optimizely.ab.config.EventType;
2221
import com.optimizely.ab.config.Experiment;
2322
import com.optimizely.ab.config.ProjectConfig;
@@ -32,6 +31,7 @@
3231
import com.optimizely.ab.event.internal.serializer.DefaultJsonSerializer;
3332
import com.optimizely.ab.event.internal.serializer.Serializer;
3433
import com.optimizely.ab.internal.EventTagUtils;
34+
import com.optimizely.ab.internal.ControlAttribute;
3535
import org.slf4j.Logger;
3636
import org.slf4j.LoggerFactory;
3737
import javax.annotation.Nonnull;
@@ -44,7 +44,6 @@
4444

4545
public class EventBuilder {
4646
private static final Logger logger = LoggerFactory.getLogger(EventBuilder.class);
47-
static final String ATTRIBUTE_KEY_FOR_BUCKETING_ATTRIBUTE = "optimizely_bucketing_id";
4847
static final String EVENT_ENDPOINT = "https://logx.optimizely.com/v1/events";
4948
static final String ACTIVATE_EVENT_KEY = "campaign_activated";
5049

@@ -118,17 +117,25 @@ public LogEvent createConversionEvent(@Nonnull ProjectConfig projectConfig,
118117
private List<Attribute> buildAttributeList(ProjectConfig projectConfig, Map<String, String> attributes) {
119118
List<Attribute> attributesList = new ArrayList<Attribute>();
120119

121-
Map<String, com.optimizely.ab.config.Attribute> attributeMap = projectConfig.getAttributeKeyMapping();
122120
for (Map.Entry<String, String> entry : attributes.entrySet()) {
123-
com.optimizely.ab.config.Attribute projectAttribute = attributeMap.get(entry.getKey());
124-
Attribute attribute = new Attribute((projectAttribute != null ? projectAttribute.getId() : null),
125-
entry.getKey(), Attribute.CUSTOM_ATTRIBUTE_TYPE, entry.getValue());
126-
127-
if (entry.getKey() == DecisionService.BUCKETING_ATTRIBUTE) {
128-
attribute = new Attribute(com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE,
129-
ATTRIBUTE_KEY_FOR_BUCKETING_ATTRIBUTE, Attribute.CUSTOM_ATTRIBUTE_TYPE, entry.getValue());
121+
String attributeId = projectConfig.getAttributeId(projectConfig, entry.getKey());
122+
if(attributeId != null) {
123+
Attribute attribute = new Attribute(attributeId,
124+
entry.getKey(),
125+
Attribute.CUSTOM_ATTRIBUTE_TYPE,
126+
entry.getValue());
127+
attributesList.add(attribute);
130128
}
129+
}
131130

131+
//checks if botFiltering value is not set in the project config file.
132+
if(projectConfig.getBotFiltering() != null) {
133+
Attribute attribute = new Attribute(
134+
ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(),
135+
ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(),
136+
Attribute.CUSTOM_ATTRIBUTE_TYPE,
137+
projectConfig.getBotFiltering()
138+
);
132139
attributesList.add(attribute);
133140
}
134141

core-api/src/main/java/com/optimizely/ab/event/internal/payload/Attribute.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ public class Attribute {
2626
String entityId;
2727
String key;
2828
String type;
29-
String value;
29+
Object value;
3030

3131
public Attribute() {
3232

3333
}
3434

35-
public Attribute(String entityId, String key, String type, String value) {
35+
public Attribute(String entityId, String key, String type, Object value) {
3636
this.entityId = entityId;
3737
this.key = key;
3838
this.type = type;
@@ -63,7 +63,7 @@ public void setType(String type) {
6363
this.type = type;
6464
}
6565

66-
public String getValue() {
66+
public Object getValue() {
6767
return value;
6868
}
6969

core-api/src/main/java/com/optimizely/ab/event/internal/payload/Decision.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818

1919
import com.fasterxml.jackson.annotation.JsonProperty;
2020

21-
import java.util.Objects;
22-
2321
public class Decision {
2422
@JsonProperty("campaign_id")
2523
String campaignId;

0 commit comments

Comments
 (0)