diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index f93214a99..788250528 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -37,6 +37,7 @@ import com.optimizely.ab.event.internal.BuildVersionInfo; import com.optimizely.ab.event.internal.EventBuilder; import com.optimizely.ab.event.internal.payload.EventBatch.ClientEngine; +import com.optimizely.ab.internal.ControlAttribute; import com.optimizely.ab.notification.NotificationCenter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -748,8 +749,8 @@ public UserProfileService getUserProfileService() { * {@link ProjectConfig}. * * @param projectConfig the current project config - * @param attributes the attributes map to validate and potentially filter. The reserved key for bucketing id - * {@link DecisionService#BUCKETING_ATTRIBUTE} is kept. + * @param attributes the attributes map to validate and potentially filter. Attributes which starts with reserved key + * {@link ProjectConfig#RESERVED_ATTRIBUTE_PREFIX} are 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 */ @@ -765,7 +766,7 @@ private Map filterAttributes(@Nonnull ProjectConfig projectConfi Map attributeKeyMapping = projectConfig.getAttributeKeyMapping(); for (Map.Entry attribute : attributes.entrySet()) { if (!attributeKeyMapping.containsKey(attribute.getKey()) && - attribute.getKey() != com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE) { + !attribute.getKey().startsWith(ProjectConfig.RESERVED_ATTRIBUTE_PREFIX)) { if (unknownAttributes == null) { unknownAttributes = new ArrayList(); } diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index 2e8ffa9a1..0dc895feb 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2017, Optimizely, Inc. and contributors * + * Copyright 2017-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. * @@ -24,6 +24,7 @@ import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.internal.ExperimentUtils; +import com.optimizely.ab.internal.ControlAttribute; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,7 +47,6 @@ */ public class DecisionService { - public static final String BUCKETING_ATTRIBUTE = "$opt_bucketing_id"; private final Bucketer bucketer; private final ErrorHandler errorHandler; private final ProjectConfig projectConfig; @@ -130,8 +130,8 @@ public DecisionService(@Nonnull Bucketer bucketer, if (ExperimentUtils.isUserInExperiment(projectConfig, experiment, filteredAttributes)) { String bucketingId = userId; - if (filteredAttributes.containsKey(BUCKETING_ATTRIBUTE)) { - bucketingId = filteredAttributes.get(BUCKETING_ATTRIBUTE); + if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) { + bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()); } variation = bucketer.bucket(experiment, bucketingId); @@ -211,8 +211,8 @@ public DecisionService(@Nonnull Bucketer bucketer, // for all rules before the everyone else rule int rolloutRulesLength = rollout.getExperiments().size(); String bucketingId = userId; - if (filteredAttributes.containsKey(BUCKETING_ATTRIBUTE)) { - bucketingId = filteredAttributes.get(BUCKETING_ATTRIBUTE); + if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) { + bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()); } Variation variation; for (int i = 0; i < rolloutRulesLength - 1; i++) { diff --git a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java index 4f2689d8f..8a81af72f 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2017, Optimizely and contributors + * Copyright 2016-2018, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.error.NoOpErrorHandler; import com.optimizely.ab.error.RaiseExceptionErrorHandler; +import com.optimizely.ab.internal.ControlAttribute; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,6 +74,7 @@ public String toString() { private final String revision; private final String version; private final boolean anonymizeIP; + private final Boolean botFiltering; private final List attributes; private final List audiences; private final List events; @@ -100,6 +102,8 @@ public String toString() { private final Map> variationToLiveVariableUsageInstanceMapping; private final Map variationIdToExperimentMapping; + public final static String RESERVED_ATTRIBUTE_PREFIX = "$opt_"; + /** * Forced variations supersede any other mappings. They are transient and are not persistent or part of * the actual datafile. This contains all the forced variations @@ -123,6 +127,7 @@ public ProjectConfig(String accountId, String projectId, String version, String this( accountId, anonymizeIP, + null, projectId, revision, version, @@ -140,6 +145,7 @@ public ProjectConfig(String accountId, String projectId, String version, String // v4 constructor public ProjectConfig(String accountId, boolean anonymizeIP, + Boolean botFiltering, String projectId, String revision, String version, @@ -157,6 +163,7 @@ public ProjectConfig(String accountId, this.version = version; this.revision = revision; this.anonymizeIP = anonymizeIP; + this.botFiltering = botFiltering; this.attributes = Collections.unmodifiableList(attributes); this.audiences = Collections.unmodifiableList(audiences); @@ -285,6 +292,30 @@ private List aggregateGroupExperiments(List groups) { return groupExperiments; } + /** + * Checks is attributeKey is reserved or not and if it exist in attributeKeyMapping + * @param attributeKey + * @return AttributeId corresponding to AttributeKeyMapping, AttributeKey when it's a reserved attribute and + * null when attributeKey is equal to BOT_FILTERING_ATTRIBUTE key. + */ + public String getAttributeId(ProjectConfig projectConfig, String attributeKey) { + String attributeIdOrKey = null; + com.optimizely.ab.config.Attribute attribute = projectConfig.getAttributeKeyMapping().get(attributeKey); + boolean hasReservedPrefix = attributeKey.startsWith(RESERVED_ATTRIBUTE_PREFIX); + if (attribute != null) { + if (hasReservedPrefix) { + logger.warn("Attribute {} unexpectedly has reserved prefix {}; using attribute ID instead of reserved attribute name.", + attributeKey, RESERVED_ATTRIBUTE_PREFIX); + } + attributeIdOrKey = attribute.getId(); + } else if (hasReservedPrefix) { + attributeIdOrKey = attributeKey; + } else { + logger.debug("Unrecognized Attribute \"{}\"", attributeKey); + } + return attributeIdOrKey; + } + public String getAccountId() { return accountId; } @@ -305,6 +336,10 @@ public boolean getAnonymizeIP() { return anonymizeIP; } + public Boolean getBotFiltering() { + return botFiltering; + } + public List getGroups() { return groups; } @@ -540,6 +575,7 @@ public String toString() { ", revision='" + revision + '\'' + ", version='" + version + '\'' + ", anonymizeIP=" + anonymizeIP + + ", botFiltering=" + botFiltering + ", attributes=" + attributes + ", audiences=" + audiences + ", events=" + events + diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java index 5dc33d0df..593fa56ae 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2017, Optimizely and contributors + * Copyright 2016-2018, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,14 +81,18 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse List featureFlags = null; List rollouts = null; + Boolean botFiltering = null; if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { featureFlags = parseFeatureFlags(rootObject.getJSONArray("featureFlags")); rollouts = parseRollouts(rootObject.getJSONArray("rollouts")); + if(rootObject.has("botFiltering")) + botFiltering = rootObject.getBoolean("botFiltering"); } return new ProjectConfig( accountId, anonymizeIP, + botFiltering, projectId, revision, version, diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java index e0bb745d6..c4784b5c4 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2017, Optimizely and contributors + * Copyright 2016-2018, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,14 +83,18 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse List featureFlags = null; List rollouts = null; + Boolean botFiltering = null; if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { featureFlags = parseFeatureFlags((JSONArray) rootObject.get("featureFlags")); rollouts = parseRollouts((JSONArray) rootObject.get("rollouts")); + if(rootObject.containsKey("botFiltering")) + botFiltering = (Boolean) rootObject.get("botFiltering"); } return new ProjectConfig( accountId, anonymizeIP, + botFiltering, projectId, revision, version, @@ -103,6 +107,8 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse liveVariables, rollouts ); + } catch (RuntimeException ex){ + throw new ConfigParseException("Unable to parse datafile: " + json, ex); } catch (Exception e) { throw new ConfigParseException("Unable to parse datafile: " + json, e); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java index c9718d851..c556b9063 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigGsonDeserializer.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2017, Optimizely and contributors + * Copyright 2016-2018, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,16 +82,20 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa List featureFlags = null; List rollouts = null; + Boolean botFiltering = null; if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { Type featureFlagsType = new TypeToken>() {}.getType(); featureFlags = context.deserialize(jsonObject.getAsJsonArray("featureFlags"), featureFlagsType); Type rolloutsType = new TypeToken>() {}.getType(); rollouts = context.deserialize(jsonObject.get("rollouts").getAsJsonArray(), rolloutsType); + if(jsonObject.has("botFiltering")) + botFiltering = jsonObject.get("botFiltering").getAsBoolean(); } return new ProjectConfig( accountId, anonymizeIP, + botFiltering, projectId, revision, version, diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java index 6ebd3c4ec..76cac7412 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ProjectConfigJacksonDeserializer.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2017, Optimizely and contributors + * Copyright 2016-2018, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,16 +76,20 @@ public ProjectConfig deserialize(JsonParser parser, DeserializationContext conte List featureFlags = null; List rollouts = null; + Boolean botFiltering = null; if (datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) { featureFlags = mapper.readValue(node.get("featureFlags").toString(), new TypeReference>() {}); rollouts = mapper.readValue(node.get("rollouts").toString(), new TypeReference>(){}); + if (node.hasNonNull("botFiltering")) + botFiltering = node.get("botFiltering").asBoolean(); } return new ProjectConfig( accountId, anonymizeIP, + botFiltering, projectId, revision, version, diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilder.java b/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilder.java index 20206d343..96f8161d9 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilder.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilder.java @@ -17,7 +17,6 @@ package com.optimizely.ab.event.internal; import com.optimizely.ab.annotations.VisibleForTesting; -import com.optimizely.ab.bucketing.DecisionService; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.ProjectConfig; @@ -32,6 +31,7 @@ import com.optimizely.ab.event.internal.serializer.DefaultJsonSerializer; import com.optimizely.ab.event.internal.serializer.Serializer; import com.optimizely.ab.internal.EventTagUtils; +import com.optimizely.ab.internal.ControlAttribute; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; @@ -44,7 +44,6 @@ public class EventBuilder { private static final Logger logger = LoggerFactory.getLogger(EventBuilder.class); - static final String ATTRIBUTE_KEY_FOR_BUCKETING_ATTRIBUTE = "optimizely_bucketing_id"; static final String EVENT_ENDPOINT = "https://logx.optimizely.com/v1/events"; static final String ACTIVATE_EVENT_KEY = "campaign_activated"; @@ -118,17 +117,25 @@ public LogEvent createConversionEvent(@Nonnull ProjectConfig projectConfig, private List buildAttributeList(ProjectConfig projectConfig, Map attributes) { List attributesList = new ArrayList(); - Map attributeMap = projectConfig.getAttributeKeyMapping(); for (Map.Entry entry : attributes.entrySet()) { - com.optimizely.ab.config.Attribute projectAttribute = attributeMap.get(entry.getKey()); - Attribute attribute = new Attribute((projectAttribute != null ? projectAttribute.getId() : null), - entry.getKey(), Attribute.CUSTOM_ATTRIBUTE_TYPE, entry.getValue()); - - if (entry.getKey() == DecisionService.BUCKETING_ATTRIBUTE) { - attribute = new Attribute(com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE, - ATTRIBUTE_KEY_FOR_BUCKETING_ATTRIBUTE, Attribute.CUSTOM_ATTRIBUTE_TYPE, entry.getValue()); + String attributeId = projectConfig.getAttributeId(projectConfig, entry.getKey()); + if(attributeId != null) { + Attribute attribute = new Attribute(attributeId, + entry.getKey(), + Attribute.CUSTOM_ATTRIBUTE_TYPE, + entry.getValue()); + attributesList.add(attribute); } + } + //checks if botFiltering value is not set in the project config file. + if(projectConfig.getBotFiltering() != null) { + Attribute attribute = new Attribute( + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + Attribute.CUSTOM_ATTRIBUTE_TYPE, + projectConfig.getBotFiltering() + ); attributesList.add(attribute); } diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Attribute.java b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Attribute.java index 71e93adba..74580cc1c 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Attribute.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Attribute.java @@ -26,13 +26,13 @@ public class Attribute { String entityId; String key; String type; - String value; + Object value; public Attribute() { } - public Attribute(String entityId, String key, String type, String value) { + public Attribute(String entityId, String key, String type, Object value) { this.entityId = entityId; this.key = key; this.type = type; @@ -63,7 +63,7 @@ public void setType(String type) { this.type = type; } - public String getValue() { + public Object getValue() { return value; } diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Decision.java b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Decision.java index 8c3341a47..b5af81da9 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Decision.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Decision.java @@ -18,8 +18,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Objects; - public class Decision { @JsonProperty("campaign_id") String campaignId; diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ControlAttribute.java b/core-api/src/main/java/com/optimizely/ab/internal/ControlAttribute.java new file mode 100644 index 000000000..d1c3278c5 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/internal/ControlAttribute.java @@ -0,0 +1,34 @@ +/** + * + * Copyright 2018, Optimizely 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.internal; + +public enum ControlAttribute { + BOT_FILTERING_ATTRIBUTE("$opt_bot_filtering"), + USER_AGENT_ATTRIBUTE("$opt_user_agent"), + BUCKETING_ATTRIBUTE("$opt_bucketing_id"); + + private final String key; + + ControlAttribute(String key) { + this.key = key; + } + + @Override + public String toString() { + return key; + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index f08e8561f..20baeb674 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -37,6 +37,7 @@ import com.optimizely.ab.event.LogEvent; import com.optimizely.ab.event.internal.EventBuilder; import com.optimizely.ab.internal.LogbackVerifier; +import com.optimizely.ab.internal.ControlAttribute; import com.optimizely.ab.notification.ActivateNotificationListener; import com.optimizely.ab.notification.NotificationCenter; import com.optimizely.ab.notification.NotificationListener; @@ -179,7 +180,7 @@ public static Collection data() throws IOException { private static final String genericUserId = "genericUserId"; private static final String testUserId = "userId"; private static final String testBucketingId = "bucketingId"; - private static final String testBucketingIdKey = DecisionService.BUCKETING_ATTRIBUTE; + private static final String testBucketingIdKey = ControlAttribute.BUCKETING_ATTRIBUTE.toString(); private int datafileVersion; private String validDatafile; diff --git a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java index e8f2359fa..0b65e9f91 100644 --- a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2017, Optimizely, Inc. and contributors * + * Copyright 2017-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. * @@ -26,6 +26,7 @@ import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.internal.LogbackVerifier; +import com.optimizely.ab.internal.ControlAttribute; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -1050,7 +1051,7 @@ public void getVariationBucketingId() throws Exception { when(bucketer.bucket(experiment, "bucketId")).thenReturn(expectedVariation); Map attr = new HashMap(); - attr.put(DecisionService.BUCKETING_ATTRIBUTE, "bucketId"); + attr.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), "bucketId"); // user excluded without audiences and whitelisting assertThat(decisionService.getVariation(experiment, genericUserId, attr), is(expectedVariation)); @@ -1068,7 +1069,7 @@ public void getVariationForRolloutWithBucketingId() { String bucketingId = "user_bucketing_id"; String userId = "user_id"; Map attributes = new HashMap(); - attributes.put(DecisionService.BUCKETING_ATTRIBUTE, bucketingId); + attributes.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), bucketingId); Bucketer bucketer = mock(Bucketer.class); when(bucketer.bucket(rolloutRuleExperiment, userId)).thenReturn(null); diff --git a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTest.java b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTest.java index 609dfddba..2d64e71fe 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016-2017, Optimizely and contributors + * Copyright 2016-2018, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ */ package com.optimizely.ab.config; +import ch.qos.logback.classic.Level; import com.optimizely.ab.config.audience.AndCondition; import com.optimizely.ab.config.audience.Condition; import com.optimizely.ab.config.audience.NotCondition; @@ -37,7 +38,10 @@ import static org.junit.Assert.assertEquals; +import com.optimizely.ab.internal.LogbackVerifier; +import com.optimizely.ab.internal.ControlAttribute; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -49,6 +53,9 @@ public class ProjectConfigTest { private ProjectConfig projectConfig; + @Rule + public LogbackVerifier logbackVerifier = new LogbackVerifier(); + @Before public void initialize() { projectConfig = ProjectConfigTestUtils.validProjectConfigV3(); @@ -143,7 +150,7 @@ public void verifyGetLiveVariableIdToExperimentsMapping() throws Exception { expectedLiveVariableIdToExperimentsMapping.put("7", Collections.singletonList(groupedExpWithVariables)); assertThat(projectConfig.getLiveVariableIdToExperimentsMapping(), - is(expectedLiveVariableIdToExperimentsMapping)); + is(expectedLiveVariableIdToExperimentsMapping)); } /** @@ -185,12 +192,13 @@ public void verifyGetVariationToLiveVariableUsageInstanceMapping() throws Except expectedVariationToLiveVariableUsageInstanceMapping.put("281", groupedVariation281VariableValues); assertThat(projectConfig.getVariationToLiveVariableUsageInstanceMapping(), - is(expectedVariationToLiveVariableUsageInstanceMapping)); + is(expectedVariationToLiveVariableUsageInstanceMapping)); } /** * Asserts that anonymizeIP is set to false if not explicitly passed into the constructor (in the case of V2 * projects). + * * @throws Exception */ @Test @@ -202,23 +210,23 @@ public void verifyAnonymizeIPIsFalseByDefault() throws Exception { /** * Invalid User IDs - User ID is null - User ID is an empty string - Invalid Experiment IDs + User ID is null + User ID is an empty string + Invalid Experiment IDs - Experiment key does not exist in the datafile - Experiment key is null - Experiment key is an empty string - Invalid Variation IDs [set only] + Experiment key does not exist in the datafile + Experiment key is null + Experiment key is an empty string + Invalid Variation IDs [set only] - Variation key does not exist in the datafile - Variation key is null - Variation key is an empty string - Multiple set calls [set only] + Variation key does not exist in the datafile + Variation key is null + Variation key is an empty string + Multiple set calls [set only] - Call set variation with different variations on one user/experiment to confirm that each set is expected. - Set variation on multiple variations for one user. - Set variations for multiple users. + Call set variation with different variations on one user/experiment to confirm that each set is expected. + Set variation on multiple variations for one user. + Set variations for multiple users. */ /* UserID test */ @Test @@ -227,6 +235,7 @@ public void setForcedVariationNullUserId() { boolean b = projectConfig.setForcedVariation("etag1", null, "vtag1"); assertFalse(b); } + @Test @SuppressFBWarnings("NP") public void getForcedVariationNullUserId() { @@ -237,6 +246,7 @@ public void getForcedVariationNullUserId() { public void setForcedVariationEmptyUserId() { assertFalse(projectConfig.setForcedVariation("etag1", "", "vtag1")); } + @Test public void getForcedVariationEmptyUserId() { assertNull(projectConfig.getForcedVariation("etag1", "")); @@ -248,6 +258,7 @@ public void getForcedVariationEmptyUserId() { public void setForcedVariationNullExperimentKey() { assertFalse(projectConfig.setForcedVariation(null, "testUser1", "vtag1")); } + @Test @SuppressFBWarnings("NP") public void getForcedVariationNullExperimentKey() { @@ -259,6 +270,7 @@ public void setForcedVariationWrongExperimentKey() { assertFalse(projectConfig.setForcedVariation("wrongKey", "testUser1", "vtag1")); } + @Test public void getForcedVariationWrongExperimentKey() { assertNull(projectConfig.getForcedVariation("wrongKey", "testUser1")); @@ -269,6 +281,7 @@ public void setForcedVariationEmptyExperimentKey() { assertFalse(projectConfig.setForcedVariation("", "testUser1", "vtag1")); } + @Test public void getForcedVariationEmptyExperimentKey() { assertNull(projectConfig.getForcedVariation("", "testUser1")); @@ -345,4 +358,38 @@ public void setForcedVariationMultipleUsers() { assertNull(projectConfig.getForcedVariation("etag2", "testUser2")); } + + @Test + public void getAttributeIDWhenAttributeKeyIsFromAttributeKeyMapping() { + ProjectConfig projectConfig = ProjectConfigTestUtils.validProjectConfigV4(); + String attributeID = projectConfig.getAttributeId(projectConfig, "house"); + assertEquals(attributeID, "553339214"); + } + + @Test + public void getAttributeIDWhenAttributeKeyIsUsingReservedKey() { + ProjectConfig projectConfig = ProjectConfigTestUtils.validProjectConfigV4(); + String attributeID = projectConfig.getAttributeId(projectConfig, "$opt_user_agent"); + assertEquals(attributeID, ControlAttribute.USER_AGENT_ATTRIBUTE.toString()); + } + + @Test + public void getAttributeIDWhenAttributeKeyUnrecognizedAttribute() { + ProjectConfig projectConfig = ProjectConfigTestUtils.validProjectConfigV4(); + String invalidAttribute = "empty"; + String attributeID = projectConfig.getAttributeId(projectConfig, invalidAttribute); + assertNull(attributeID); + logbackVerifier.expectMessage(Level.DEBUG, "Unrecognized Attribute \""+invalidAttribute+"\""); + } + + @Test + public void getAttributeIDWhenAttributeKeyPrefixIsMatched() { + ProjectConfig projectConfig = ProjectConfigTestUtils.validProjectConfigV4(); + String attributeWithReservedPrefix = "$opt_test"; + String attributeID = projectConfig.getAttributeId(projectConfig, attributeWithReservedPrefix); + assertEquals(attributeID,"583394100"); + logbackVerifier.expectMessage(Level.WARN, "Attribute "+attributeWithReservedPrefix +" unexpectedly" + + " has reserved prefix $opt_; using attribute ID instead of reserved attribute name."); + } + } \ No newline at end of file diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index c43f73c1d..19a65dcba 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017, Optimizely and contributors + * Copyright 2017-2018, Optimizely and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ public class ValidProjectConfigV4 { // simple properties private static final String ACCOUNT_ID = "2360254204"; private static final boolean ANONYMIZE_IP = true; + private static final boolean BOT_FILTERING = true; private static final String PROJECT_ID = "3918735994"; private static final String REVISION = "1480511547"; private static final String VERSION = "4"; @@ -44,6 +45,10 @@ public class ValidProjectConfigV4 { public static final String ATTRIBUTE_NATIONALITY_KEY = "nationality"; private static final Attribute ATTRIBUTE_NATIONALITY = new Attribute(ATTRIBUTE_NATIONALITY_ID, ATTRIBUTE_NATIONALITY_KEY); + private static final String ATTRIBUTE_OPT_ID = "583394100"; + public static final String ATTRIBUTE_OPT_KEY = "$opt_test"; + private static final Attribute ATTRIBUTE_OPT = new Attribute(ATTRIBUTE_OPT_ID, ATTRIBUTE_OPT_KEY); + // audiences private static final String CUSTOM_DIMENSION_TYPE = "custom_dimension"; private static final String AUDIENCE_GRYFFINDOR_ID = "3468206642"; @@ -1027,6 +1032,7 @@ public static ProjectConfig generateValidProjectConfigV4() { List attributes = new ArrayList(); attributes.add(ATTRIBUTE_HOUSE); attributes.add(ATTRIBUTE_NATIONALITY); + attributes.add(ATTRIBUTE_OPT); // list audiences List audiences = new ArrayList(); @@ -1073,6 +1079,7 @@ public static ProjectConfig generateValidProjectConfigV4() { return new ProjectConfig( ACCOUNT_ID, ANONYMIZE_IP, + BOT_FILTERING, PROJECT_ID, REVISION, VERSION, diff --git a/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderTest.java b/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderTest.java index b976bed29..879386a44 100644 --- a/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderTest.java +++ b/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderTest.java @@ -1,3 +1,19 @@ +/** + * + * Copyright 2016-2018, Optimizely 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.event.internal; import com.google.gson.FieldNamingPolicy; @@ -16,6 +32,7 @@ import com.optimizely.ab.event.LogEvent; import com.optimizely.ab.event.internal.payload.Decision; import com.optimizely.ab.event.internal.payload.EventBatch; +import com.optimizely.ab.internal.ControlAttribute; import com.optimizely.ab.internal.ReservedEventKey; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,11 +50,7 @@ import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV2; import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV4; -import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; -import static com.optimizely.ab.config.ValidProjectConfigV4.EVENT_BASIC_EVENT_KEY; -import static com.optimizely.ab.config.ValidProjectConfigV4.EVENT_PAUSED_EXPERIMENT_KEY; -import static com.optimizely.ab.config.ValidProjectConfigV4.MULTIVARIATE_EXPERIMENT_FORCED_VARIATION_USER_ID_GRED; -import static com.optimizely.ab.config.ValidProjectConfigV4.PAUSED_EXPERIMENT_FORCED_VARIATION_USER_ID_CONTROL; +import static com.optimizely.ab.config.ValidProjectConfigV4.*; import static junit.framework.Assert.assertNotNull; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.closeTo; @@ -85,25 +98,93 @@ public EventBuilderTest(int datafileVersion, this.validProjectConfig = validProjectConfig; } + /** + * Verify {@link com.optimizely.ab.event.internal.payload.EventBatch} event creation + */ + @Test + public void createImpressionEventPassingUserAgentAttribute() throws Exception { + // use the "valid" project config and its associated experiment, variation, and attributes + Experiment activatedExperiment = validProjectConfig.getExperiments().get(0); + Variation bucketedVariation = activatedExperiment.getVariations().get(0); + Attribute attribute = validProjectConfig.getAttributes().get(0); + String userId = "userId"; + Map attributeMap = new HashMap(); + attributeMap.put(attribute.getKey(), "value"); + attributeMap.put(ControlAttribute.USER_AGENT_ATTRIBUTE.toString(), "Chrome"); + Decision expectedDecision = new Decision(activatedExperiment.getLayerId(), activatedExperiment.getId(), bucketedVariation.getId(), false); + com.optimizely.ab.event.internal.payload.Attribute feature = new com.optimizely.ab.event.internal.payload.Attribute(attribute.getId(), + attribute.getKey(), com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + "value"); + com.optimizely.ab.event.internal.payload.Attribute userAgentFeature = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.USER_AGENT_ATTRIBUTE.toString(), + ControlAttribute.USER_AGENT_ATTRIBUTE.toString(), + com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + "Chrome"); + + com.optimizely.ab.event.internal.payload.Attribute botFilteringFeature = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + validProjectConfig.getBotFiltering()); + List expectedUserFeatures; + + if(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) + expectedUserFeatures = Arrays.asList(userAgentFeature, feature, botFilteringFeature); + else + expectedUserFeatures = Arrays.asList(userAgentFeature, feature); + + LogEvent impressionEvent = builder.createImpressionEvent(validProjectConfig, activatedExperiment, bucketedVariation, + userId, attributeMap); + + // verify that request endpoint is correct + assertThat(impressionEvent.getEndpointUrl(), is(EventBuilder.EVENT_ENDPOINT)); + + EventBatch eventBatch = gson.fromJson(impressionEvent.getBody(), EventBatch.class); + + // verify payload information + assertThat(eventBatch.getVisitors().get(0).getVisitorId(), is(userId)); + assertThat((double) eventBatch.getVisitors().get(0).getSnapshots().get(0).getEvents().get(0).getTimestamp(), closeTo((double)System.currentTimeMillis(), 1000.0)); + assertFalse(eventBatch.getVisitors().get(0).getSnapshots().get(0).getDecisions().get(0).getIsCampaignHoldback()); + assertThat(eventBatch.getAnonymizeIp(), is(validProjectConfig.getAnonymizeIP())); + assertThat(eventBatch.getProjectId(), is(validProjectConfig.getProjectId())); + assertThat(eventBatch.getVisitors().get(0).getSnapshots().get(0).getDecisions().get(0), is(expectedDecision)); + assertThat(eventBatch.getVisitors().get(0).getSnapshots().get(0).getDecisions().get(0).getCampaignId(), + is(activatedExperiment.getLayerId())); + assertThat(eventBatch.getAccountId(), is(validProjectConfig.getAccountId())); + assertThat(eventBatch.getVisitors().get(0).getAttributes(), is(expectedUserFeatures)); + assertThat(eventBatch.getClientName(), is(EventBatch.ClientEngine.JAVA_SDK.getClientEngineValue())); + assertThat(eventBatch.getClientVersion(), is(BuildVersionInfo.VERSION)); + assertNull(eventBatch.getVisitors().get(0).getSessionId()); + } + /** * Verify {@link com.optimizely.ab.event.internal.payload.EventBatch} event creation */ @Test public void createImpressionEvent() throws Exception { // use the "valid" project config and its associated experiment, variation, and attributes - ProjectConfig projectConfig = validProjectConfigV2(); - Experiment activatedExperiment = projectConfig.getExperiments().get(0); + Experiment activatedExperiment = validProjectConfig.getExperiments().get(0); Variation bucketedVariation = activatedExperiment.getVariations().get(0); - Attribute attribute = projectConfig.getAttributes().get(0); + Attribute attribute = validProjectConfig.getAttributes().get(0); String userId = "userId"; Map attributeMap = Collections.singletonMap(attribute.getKey(), "value"); Decision expectedDecision = new Decision(activatedExperiment.getLayerId(), activatedExperiment.getId(), bucketedVariation.getId(), false); com.optimizely.ab.event.internal.payload.Attribute feature = new com.optimizely.ab.event.internal.payload.Attribute(attribute.getId(), attribute.getKey(), com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, "value"); - List expectedUserFeatures = Collections.singletonList(feature); + com.optimizely.ab.event.internal.payload.Attribute botFilteringFeature = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + validProjectConfig.getBotFiltering()); + List expectedUserFeatures; - LogEvent impressionEvent = builder.createImpressionEvent(projectConfig, activatedExperiment, bucketedVariation, + if(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) + expectedUserFeatures = Arrays.asList(feature, botFilteringFeature); + else + expectedUserFeatures = Arrays.asList(feature); + + LogEvent impressionEvent = builder.createImpressionEvent(validProjectConfig, activatedExperiment, bucketedVariation, userId, attributeMap); // verify that request endpoint is correct @@ -115,12 +196,12 @@ public void createImpressionEvent() throws Exception { assertThat(eventBatch.getVisitors().get(0).getVisitorId(), is(userId)); assertThat((double) eventBatch.getVisitors().get(0).getSnapshots().get(0).getEvents().get(0).getTimestamp(), closeTo((double)System.currentTimeMillis(), 1000.0)); assertFalse(eventBatch.getVisitors().get(0).getSnapshots().get(0).getDecisions().get(0).getIsCampaignHoldback()); - assertThat(eventBatch.getAnonymizeIp(), is(projectConfig.getAnonymizeIP())); - assertThat(eventBatch.getProjectId(), is(projectConfig.getProjectId())); + assertThat(eventBatch.getAnonymizeIp(), is(validProjectConfig.getAnonymizeIP())); + assertThat(eventBatch.getProjectId(), is(validProjectConfig.getProjectId())); assertThat(eventBatch.getVisitors().get(0).getSnapshots().get(0).getDecisions().get(0), is(expectedDecision)); assertThat(eventBatch.getVisitors().get(0).getSnapshots().get(0).getDecisions().get(0).getCampaignId(), is(activatedExperiment.getLayerId())); - assertThat(eventBatch.getAccountId(), is(projectConfig.getAccountId())); + assertThat(eventBatch.getAccountId(), is(validProjectConfig.getAccountId())); assertThat(eventBatch.getVisitors().get(0).getAttributes(), is(expectedUserFeatures)); assertThat(eventBatch.getClientName(), is(EventBatch.ClientEngine.JAVA_SDK.getClientEngineValue())); assertThat(eventBatch.getClientVersion(), is(BuildVersionInfo.VERSION)); @@ -134,7 +215,7 @@ public void createImpressionEvent() throws Exception { @Test public void createImpressionEventIgnoresUnknownAttributes() throws Exception { // use the "valid" project config and its associated experiment, variation, and attributes - ProjectConfig projectConfig = validProjectConfigV2(); + ProjectConfig projectConfig = validProjectConfig; Experiment activatedExperiment = projectConfig.getExperiments().get(0); Variation bucketedVariation = activatedExperiment.getVariations().get(0); @@ -268,7 +349,121 @@ public void createConversionEvent() throws Exception { com.optimizely.ab.event.internal.payload.Attribute feature = new com.optimizely.ab.event.internal.payload.Attribute(attribute.getId(), attribute.getKey(), com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, AUDIENCE_GRYFFINDOR_VALUE); - List expectedUserFeatures = Collections.singletonList(feature); + com.optimizely.ab.event.internal.payload.Attribute feature2 = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + validProjectConfig.getBotFiltering()); + List expectedUserFeatures; + + if(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) + expectedUserFeatures = Arrays.asList(feature, feature2); + else + expectedUserFeatures = Arrays.asList(feature); + + assertEquals(conversion.getVisitors().get(0).getAttributes(), expectedUserFeatures); + assertThat(conversion.getVisitors().get(0).getSnapshots().get(0).getDecisions(), containsInAnyOrder(expectedDecisions.toArray())); + assertEquals(conversion.getVisitors().get(0).getSnapshots().get(0).getEvents().get(0).getEntityId(), eventType.getId()); + assertEquals(conversion.getVisitors().get(0).getSnapshots().get(0).getEvents().get(0).getKey(), eventType.getKey()); + assertEquals(conversion.getVisitors().get(0).getSnapshots().get(0).getEvents().get(0).getRevenue(), null); + assertTrue(conversion.getVisitors().get(0).getAttributes().containsAll(expectedUserFeatures)); + assertTrue(conversion.getVisitors().get(0).getSnapshots().get(0).getEvents().get(0).getTags().equals(eventTagMap)); + assertFalse(conversion.getVisitors().get(0).getSnapshots().get(0).getDecisions().get(0).getIsCampaignHoldback()); + assertEquals(conversion.getAnonymizeIp(), validProjectConfig.getAnonymizeIP()); + assertEquals(conversion.getClientName(), EventBatch.ClientEngine.JAVA_SDK.getClientEngineValue()); + assertEquals(conversion.getClientVersion(), BuildVersionInfo.VERSION); + } + + /** + * Verify {@link com.optimizely.ab.event.internal.payload.EventBatch} event creation + * passing User Agent reserved attribute in attribute map and to check it exist in visitors.attributes + */ + @Test + public void createConversionEventPassingUserAgentAttribute() throws Exception { + // use the "valid" project config and its associated experiment, variation, and attributes + Attribute attribute = validProjectConfig.getAttributes().get(0); + EventType eventType = validProjectConfig.getEventTypes().get(0); + String userId = "userId"; + + Bucketer mockBucketAlgorithm = mock(Bucketer.class); + + List allExperiments = validProjectConfig.getExperiments(); + List experimentsForEventKey = validProjectConfig.getExperimentsForEventKey(eventType.getKey()); + + // Bucket to the first variation for all experiments. However, only a subset of the experiments will actually + // call the bucket function. + for (Experiment experiment : allExperiments) { + when(mockBucketAlgorithm.bucket(experiment, userId)) + .thenReturn(experiment.getVariations().get(0)); + } + DecisionService decisionService = new DecisionService( + mockBucketAlgorithm, + mock(ErrorHandler.class), + validProjectConfig, + mock(UserProfileService.class) + ); + + Map attributeMap = new HashMap(); + attributeMap.put(attribute.getKey(), AUDIENCE_GRYFFINDOR_VALUE); + attributeMap.put(ControlAttribute.USER_AGENT_ATTRIBUTE.toString(), "Chrome"); + Map eventTagMap = new HashMap(); + eventTagMap.put("boolean_param", false); + eventTagMap.put("string_param", "123"); + Map experimentVariationMap = createExperimentVariationMap( + validProjectConfig, + decisionService, + eventType.getKey(), + userId, + attributeMap); + LogEvent conversionEvent = builder.createConversionEvent( + validProjectConfig, + experimentVariationMap, + userId, + eventType.getId(), + eventType.getKey(), + attributeMap, + eventTagMap); + + List expectedDecisions = new ArrayList(); + + for (Experiment experiment : experimentsForEventKey) { + if (experiment.isRunning()) { + Decision layerState = new Decision(experiment.getLayerId(), experiment.getId(), + experiment.getVariations().get(0).getId(), false); + expectedDecisions.add(layerState); + } + } + + // verify that the request endpoint is correct + assertThat(conversionEvent.getEndpointUrl(), is(EventBuilder.EVENT_ENDPOINT)); + + EventBatch conversion = gson.fromJson(conversionEvent.getBody(), EventBatch.class); + + // verify payload information + assertThat(conversion.getVisitors().get(0).getVisitorId(), is(userId)); + assertThat(conversion.getProjectId(), is(validProjectConfig.getProjectId())); + assertThat(conversion.getAccountId(), is(validProjectConfig.getAccountId())); + + com.optimizely.ab.event.internal.payload.Attribute feature = new com.optimizely.ab.event.internal.payload.Attribute( + attribute.getId(), attribute.getKey(), + com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + AUDIENCE_GRYFFINDOR_VALUE); + com.optimizely.ab.event.internal.payload.Attribute userAgentFeature = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.USER_AGENT_ATTRIBUTE.toString(), + ControlAttribute.USER_AGENT_ATTRIBUTE.toString(), + com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + "Chrome"); + com.optimizely.ab.event.internal.payload.Attribute botFilteringFeature = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + validProjectConfig.getBotFiltering()); + List expectedUserFeatures; + + if(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) + expectedUserFeatures = Arrays.asList(userAgentFeature, feature, botFilteringFeature); + else + expectedUserFeatures = Arrays.asList(userAgentFeature, feature); assertEquals(conversion.getVisitors().get(0).getAttributes(), expectedUserFeatures); assertThat(conversion.getVisitors().get(0).getSnapshots().get(0).getDecisions(), containsInAnyOrder(expectedDecisions.toArray())); @@ -542,7 +737,7 @@ public void createConversionEventReturnsNullWhenExperimentVariationMapIsEmpty() @Test public void createImpressionEventWithBucketingId() throws Exception { // use the "valid" project config and its associated experiment, variation, and attributes - ProjectConfig projectConfig = validProjectConfigV2(); + ProjectConfig projectConfig = validProjectConfig; Experiment activatedExperiment = projectConfig.getExperiments().get(0); Variation bucketedVariation = activatedExperiment.getVariations().get(0); Attribute attribute = projectConfig.getAttributes().get(0); @@ -550,19 +745,30 @@ public void createImpressionEventWithBucketingId() throws Exception { Map attributeMap = new HashMap(); attributeMap.put(attribute.getKey(), "value"); - attributeMap.put(com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE, "variation"); + attributeMap.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), "variation"); Decision expectedDecision = new Decision(activatedExperiment.getLayerId(), activatedExperiment.getId(), bucketedVariation.getId(), false); com.optimizely.ab.event.internal.payload.Attribute feature = new com.optimizely.ab.event.internal.payload.Attribute(attribute.getId(), attribute.getKey(), com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, "value"); - com.optimizely.ab.event.internal.payload.Attribute feature1 = new com.optimizely.ab.event.internal.payload.Attribute(com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE, - EventBuilder.ATTRIBUTE_KEY_FOR_BUCKETING_ATTRIBUTE, + com.optimizely.ab.event.internal.payload.Attribute feature1 = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.BUCKETING_ATTRIBUTE.toString(), + ControlAttribute.BUCKETING_ATTRIBUTE.toString(), com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, "variation"); + com.optimizely.ab.event.internal.payload.Attribute feature2 = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + validProjectConfig.getBotFiltering()); + + List expectedUserFeatures; - List expectedUserFeatures = Arrays.asList(feature, feature1); + if(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) + expectedUserFeatures = Arrays.asList(feature, feature1, feature2); + else + expectedUserFeatures = Arrays.asList(feature, feature1); LogEvent impressionEvent = builder.createImpressionEvent(projectConfig, activatedExperiment, bucketedVariation, userId, attributeMap); @@ -619,7 +825,7 @@ public void createConversionEventWithBucketingId() throws Exception { Map attributeMap = new java.util.HashMap(); attributeMap.put(attribute.getKey(), AUDIENCE_GRYFFINDOR_VALUE); - attributeMap.put(com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE, bucketingId); + attributeMap.put(ControlAttribute.BUCKETING_ATTRIBUTE.toString(), bucketingId); Map eventTagMap = new HashMap(); eventTagMap.put("boolean_param", false); @@ -663,11 +869,22 @@ public void createConversionEventWithBucketingId() throws Exception { com.optimizely.ab.event.internal.payload.Attribute attribute1 = new com.optimizely.ab.event.internal.payload.Attribute(attribute.getId(), attribute.getKey(), com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, AUDIENCE_GRYFFINDOR_VALUE); - com.optimizely.ab.event.internal.payload.Attribute attribute2 = new com.optimizely.ab.event.internal.payload.Attribute(com.optimizely.ab.bucketing.DecisionService.BUCKETING_ATTRIBUTE, - EventBuilder.ATTRIBUTE_KEY_FOR_BUCKETING_ATTRIBUTE, + com.optimizely.ab.event.internal.payload.Attribute attribute2 = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.BUCKETING_ATTRIBUTE.toString(), + ControlAttribute.BUCKETING_ATTRIBUTE.toString(), com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, bucketingId); - List expectedUserFeatures = Arrays.asList(attribute1, attribute2); + com.optimizely.ab.event.internal.payload.Attribute attribute3 = new com.optimizely.ab.event.internal.payload.Attribute( + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + ControlAttribute.BOT_FILTERING_ATTRIBUTE.toString(), + com.optimizely.ab.event.internal.payload.Attribute.CUSTOM_ATTRIBUTE_TYPE, + validProjectConfig.getBotFiltering()); + List expectedUserFeatures; + + if(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString())) + expectedUserFeatures = Arrays.asList(attribute1, attribute2, attribute3); + else + expectedUserFeatures = Arrays.asList(attribute1, attribute2); assertEquals(conversion.getVisitors().get(0).getAttributes(), expectedUserFeatures); assertThat(conversion.getVisitors().get(0).getSnapshots().get(0).getDecisions(), containsInAnyOrder(expectedDecisions.toArray())); diff --git a/core-api/src/test/resources/config/valid-project-config-v4.json b/core-api/src/test/resources/config/valid-project-config-v4.json index bfd683888..db961012e 100644 --- a/core-api/src/test/resources/config/valid-project-config-v4.json +++ b/core-api/src/test/resources/config/valid-project-config-v4.json @@ -1,6 +1,7 @@ { "accountId": "2360254204", "anonymizeIP": true, + "botFiltering": true, "projectId": "3918735994", "revision": "1480511547", "version": "4", @@ -34,6 +35,10 @@ { "id": "58339410", "key": "nationality" + }, + { + "id": "583394100", + "key": "$opt_test" } ], "events": [