From 0e268a9ac627005f1ddef500a3a3d2b5f1fa9d5d Mon Sep 17 00:00:00 2001 From: Michael Ng Date: Wed, 14 Dec 2016 14:16:00 -0800 Subject: [PATCH 1/5] Merge pull request #42 from optimizely/mng/add-ip-anon Add IP anonymization to event payload. --- .../com/optimizely/ab/config/ProjectConfig.java | 11 +++++++++-- .../ab/config/parser/JsonConfigParser.java | 5 ++++- .../ab/config/parser/JsonSimpleConfigParser.java | 5 ++++- .../parser/ProjectConfigGsonDeserializer.java | 5 ++++- .../parser/ProjectConfigJacksonDeserializer.java | 4 +++- .../ab/event/internal/EventBuilderV2.java | 6 ++++-- .../ab/event/internal/payload/Conversion.java | 9 ++++++++- .../ab/event/internal/payload/Impression.java | 13 ++++++++++++- .../internal/serializer/JsonSimpleSerializer.java | 2 ++ .../com/optimizely/ab/config/ProjectConfigTest.java | 12 ++++++++++++ .../ab/config/ProjectConfigTestUtils.java | 4 ++-- .../ab/event/internal/EventBuilderV2Test.java | 2 ++ .../internal/serializer/SerializerTestUtils.java | 2 ++ .../resources/config/valid-project-config-v3.json | 1 + .../src/test/resources/serializer/conversion.json | 1 + .../src/test/resources/serializer/impression.json | 1 + 16 files changed, 71 insertions(+), 12 deletions(-) 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 77e1d086c..e4ac6c294 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 @@ -58,6 +58,7 @@ public String toString() { private final String projectId; private final String revision; private final String version; + private final boolean anonymizeIP; private final List groups; private final List experiments; private final List attributes; @@ -79,18 +80,19 @@ public String toString() { public ProjectConfig(String accountId, String projectId, String version, String revision, List groups, List experiments, List attributes, List eventType, List audiences) { - this(accountId, projectId, version, revision, groups, experiments, attributes, eventType, audiences, + this(accountId, projectId, version, revision, groups, experiments, attributes, eventType, audiences, false, null); } public ProjectConfig(String accountId, String projectId, String version, String revision, List groups, List experiments, List attributes, List eventType, - List audiences, List liveVariables) { + List audiences, boolean anonymizeIP, List liveVariables) { this.accountId = accountId; this.projectId = projectId; this.version = version; this.revision = revision; + this.anonymizeIP = anonymizeIP; this.groups = Collections.unmodifiableList(groups); List allExperiments = new ArrayList(); @@ -151,6 +153,10 @@ public String getRevision() { return revision; } + public boolean getAnonymizeIP() { + return anonymizeIP; + } + public List getGroups() { return groups; } @@ -233,6 +239,7 @@ public String toString() { ", projectId='" + projectId + '\'' + ", revision='" + revision + '\'' + ", version='" + version + '\'' + + ", anonymizeIP='" + anonymizeIP + '\'' + ", groups=" + groups + ", experiments=" + experiments + ", attributes=" + attributes + 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 538ac92ac..b12b0da45 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 @@ -73,13 +73,16 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse List audiences = parseAudiences(rootObject.getJSONArray("audiences")); List groups = parseGroups(rootObject.getJSONArray("groups")); + boolean anonymizeIP = false; List liveVariables = null; if (version.equals(ProjectConfig.Version.V3.toString())) { liveVariables = parseLiveVariables(rootObject.getJSONArray("variables")); + + anonymizeIP = rootObject.getBoolean("anonymizeIP"); } return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, - audiences, liveVariables); + audiences, anonymizeIP, liveVariables); } 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/JsonSimpleConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java index 551a08e6b..e3ba0236c 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 @@ -75,13 +75,16 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse List audiences = parseAudiences((JSONArray)parser.parse(rootObject.get("audiences").toString())); List groups = parseGroups((JSONArray)rootObject.get("groups")); + boolean anonymizeIP = false; List liveVariables = null; if (version.equals(ProjectConfig.Version.V3.toString())) { liveVariables = parseLiveVariables((JSONArray)rootObject.get("variables")); + + anonymizeIP = (Boolean)rootObject.get("anonymizeIP"); } return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, - audiences, liveVariables); + audiences, anonymizeIP, liveVariables); } 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 c552fa48d..283a96119 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 @@ -72,14 +72,17 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa List audiences = context.deserialize(jsonObject.get("audiences").getAsJsonArray(), audienceType); + boolean anonymizeIP = false; // live variables should be null if using V1 List liveVariables = null; if (version.equals(ProjectConfig.Version.V3.toString())) { Type liveVariablesType = new TypeToken>() {}.getType(); liveVariables = context.deserialize(jsonObject.getAsJsonArray("variables"), liveVariablesType); + + anonymizeIP = jsonObject.get("anonymizeIP").getAsBoolean(); } return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, - audiences, liveVariables); + audiences, anonymizeIP, liveVariables); } } 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 e2aded68f..ddc3675d1 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 @@ -68,13 +68,15 @@ public ProjectConfig deserialize(JsonParser parser, DeserializationContext conte List audiences = mapper.readValue(node.get("audiences").toString(), new TypeReference>() {}); + boolean anonymizeIP = false; List liveVariables = null; if (version.equals(ProjectConfig.Version.V3.toString())) { liveVariables = mapper.readValue(node.get("variables").toString(), new TypeReference>() {}); + anonymizeIP = node.get("anonymizeIP").asBoolean(); } return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, - audiences, liveVariables); + audiences, anonymizeIP, liveVariables); } } \ No newline at end of file diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilderV2.java b/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilderV2.java index 7a3478df4..329e2d74f 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilderV2.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilderV2.java @@ -51,8 +51,8 @@ public class EventBuilderV2 extends EventBuilder { private static final Logger logger = LoggerFactory.getLogger(EventBuilderV2.class); - static final String IMPRESSION_ENDPOINT = "https://p13nlog.dz.optimizely.com/log/decision"; - static final String CONVERSION_ENDPOINT = "https://p13nlog.dz.optimizely.com/log/event"; + static final String IMPRESSION_ENDPOINT = "https://logx.optimizely.com/log/decision"; + static final String CONVERSION_ENDPOINT = "https://logx.optimizely.com/log/event"; @VisibleForTesting public final ClientEngine clientEngine; @@ -95,6 +95,7 @@ public LogEvent createImpressionEvent(@Nonnull ProjectConfig projectConfig, impressionPayload.setUserFeatures(createFeatures(attributes, projectConfig)); impressionPayload.setClientEngine(clientEngine); impressionPayload.setClientVersion(clientVersion); + impressionPayload.setAnonymizeIP(projectConfig.getAnonymizeIP()); String payload = this.serializer.serialize(impressionPayload); return new LogEvent(RequestMethod.POST, IMPRESSION_ENDPOINT, Collections.emptyMap(), payload); @@ -133,6 +134,7 @@ public LogEvent createConversionEvent(@Nonnull ProjectConfig projectConfig, conversionPayload.setEventFeatures(Collections.emptyList()); conversionPayload.setIsGlobalHoldback(false); + conversionPayload.setAnonymizeIP(projectConfig.getAnonymizeIP()); conversionPayload.setClientEngine(clientEngine); conversionPayload.setClientVersion(clientVersion); diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Conversion.java b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Conversion.java index 187eed879..bf30d25c7 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Conversion.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Conversion.java @@ -31,12 +31,13 @@ public class Conversion extends Event { private List eventMetrics; private List eventFeatures; private boolean isGlobalHoldback; + private boolean anonymizeIP; public Conversion() { } public Conversion(String visitorId, long timestamp, String projectId, String accountId, List userFeatures, List layerStates, String eventEntityId, String eventName, - List eventMetrics, List eventFeatures, boolean isGlobalHoldback) { + List eventMetrics, List eventFeatures, boolean isGlobalHoldback, boolean anonymizeIP) { this.visitorId = visitorId; this.timestamp = timestamp; this.projectId = projectId; @@ -48,6 +49,7 @@ public Conversion(String visitorId, long timestamp, String projectId, String acc this.eventMetrics = eventMetrics; this.eventFeatures = eventFeatures; this.isGlobalHoldback = isGlobalHoldback; + this.anonymizeIP = anonymizeIP; } public String getVisitorId() { @@ -138,6 +140,10 @@ public void setIsGlobalHoldback(boolean globalHoldback) { this.isGlobalHoldback = globalHoldback; } + public boolean getAnonymizeIP() { return anonymizeIP; } + + public void setAnonymizeIP(boolean anonymizeIP) { this.anonymizeIP = anonymizeIP; } + @Override public boolean equals(Object other) { if (!(other instanceof Conversion)) @@ -192,6 +198,7 @@ public String toString() { ", eventMetrics=" + eventMetrics + ", eventFeatures=" + eventFeatures + ", isGlobalHoldback=" + isGlobalHoldback + + ", anonymizeIP=" + anonymizeIP + ", clientEngine='" + clientEngine + ", clientVersion='" + clientVersion + '}'; } diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Impression.java b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Impression.java index 92291c373..a4d3487fd 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Impression.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/payload/Impression.java @@ -28,11 +28,12 @@ public class Impression extends Event { private String layerId; private String accountId; private List userFeatures; + private boolean anonymizeIP; public Impression() { } public Impression(String visitorId, long timestamp, boolean isGlobalHoldback, String projectId, Decision decision, - String layerId, String accountId, List userFeatures) { + String layerId, String accountId, List userFeatures, boolean anonymizeIP) { this.visitorId = visitorId; this.timestamp = timestamp; this.isGlobalHoldback = isGlobalHoldback; @@ -41,6 +42,7 @@ public Impression(String visitorId, long timestamp, boolean isGlobalHoldback, St this.layerId = layerId; this.accountId = accountId; this.userFeatures = userFeatures; + this.anonymizeIP = anonymizeIP; } public String getVisitorId() { @@ -107,6 +109,14 @@ public void setUserFeatures(List userFeatures) { this.userFeatures = userFeatures; } + public boolean getAnonymizeIP() { + return anonymizeIP; + } + + public void setAnonymizeIP(boolean anonymizeIP) { + this.anonymizeIP = anonymizeIP; + } + @Override public boolean equals(Object other) { if (!(other instanceof Impression)) @@ -147,6 +157,7 @@ public String toString() { "visitorId='" + visitorId + '\'' + ", timestamp=" + timestamp + ", isGlobalHoldback=" + isGlobalHoldback + + ", anonymizeIP=" + anonymizeIP + ", projectId='" + projectId + '\'' + ", decision=" + decision + ", layerId='" + layerId + '\'' + diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/serializer/JsonSimpleSerializer.java b/core-api/src/main/java/com/optimizely/ab/event/internal/serializer/JsonSimpleSerializer.java index 5727b49b1..22f7671c7 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/serializer/JsonSimpleSerializer.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/serializer/JsonSimpleSerializer.java @@ -48,6 +48,7 @@ private JSONObject serializeImpression(Impression impression) { jsonObject.put("visitorId", impression.getVisitorId()); jsonObject.put("timestamp", impression.getTimestamp()); jsonObject.put("isGlobalHoldback", impression.getIsGlobalHoldback()); + jsonObject.put("anonymizeIP", impression.getAnonymizeIP()); jsonObject.put("projectId", impression.getProjectId()); jsonObject.put("decision", serializeDecision(impression.getDecision())); jsonObject.put("layerId", impression.getLayerId()); @@ -72,6 +73,7 @@ private JSONObject serializeConversion(Conversion conversion) { jsonObject.put("eventMetrics", serializeEventMetrics(conversion.getEventMetrics())); jsonObject.put("eventFeatures", serializeFeatures(conversion.getEventFeatures())); jsonObject.put("isGlobalHoldback", conversion.getIsGlobalHoldback()); + jsonObject.put("anonymizeIP", conversion.getAnonymizeIP()); jsonObject.put("clientEngine", conversion.getClientEngine()); jsonObject.put("clientVersion", conversion.getClientVersion()); 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 ef5726264..156ccf06f 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 @@ -30,6 +30,7 @@ import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; @@ -179,4 +180,15 @@ public void verifyGetVariationToLiveVariableUsageInstanceMapping() throws Except assertThat(projectConfig.getVariationToLiveVariableUsageInstanceMapping(), is(expectedVariationToLiveVariableUsageInstanceMapping)); } + + /** + * Asserts that anonymizeIP is set to false if not explicitly passed into the constructor (in the case of V1 or V2 + * projects). + * @throws Exception + */ + @Test + public void verifyAnonymizeIPIsFalseByDefault() throws Exception { + ProjectConfig v2ProjectConfig = ProjectConfigTestUtils.validProjectConfigV2(); + assertFalse(v2ProjectConfig.getAnonymizeIP()); + } } \ No newline at end of file diff --git a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java index 3d380b11c..009b68f1f 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java @@ -437,7 +437,7 @@ private static ProjectConfig generateValidProjectConfigV3() { ); return new ProjectConfig("789", "1234", "3", "42", groups, experiments, attributes, events, audiences, - liveVariables); + true, liveVariables); } private static final ProjectConfig NO_AUDIENCE_PROJECT_CONFIG_V3 = generateNoAudienceProjectConfigV3(); @@ -474,7 +474,7 @@ private static ProjectConfig generateNoAudienceProjectConfigV3() { new EventType("099", "clicked_purchase", multipleExperimentIds)); return new ProjectConfig("789", "1234", "3", "42", Collections.emptyList(), experiments, attributes, - events, Collections.emptyList(), Collections.emptyList()); + events, Collections.emptyList(), true, Collections.emptyList()); } private ProjectConfigTestUtils() { } diff --git a/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV2Test.java b/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV2Test.java index 958203e0c..1c507bec7 100644 --- a/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV2Test.java +++ b/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV2Test.java @@ -90,6 +90,7 @@ public void createImpressionEvent() throws Exception { assertThat(impression.getVisitorId(), is(userId)); assertThat((double)impression.getTimestamp(), closeTo((double)System.currentTimeMillis(), 60.0)); assertFalse(impression.getIsGlobalHoldback()); + assertThat(impression.getAnonymizeIP(), is(projectConfig.getAnonymizeIP())); assertThat(impression.getProjectId(), is(projectConfig.getProjectId())); assertThat(impression.getDecision(), is(expectedDecision)); assertThat(impression.getLayerId(), is(activatedExperiment.getLayerId())); @@ -207,6 +208,7 @@ public void createConversionEvent() throws Exception { assertThat(conversion.getEventMetrics(), is(Collections.emptyList())); assertThat(conversion.getEventFeatures(), is(Collections.emptyList())); assertFalse(conversion.getIsGlobalHoldback()); + assertThat(conversion.getAnonymizeIP(), is(projectConfig.getAnonymizeIP())); assertThat(conversion.getClientEngine(), is(ClientEngine.JAVA_SDK.getClientEngineValue())); assertThat(conversion.getClientVersion(), is(BuildVersionInfo.VERSION)); } diff --git a/core-api/src/test/java/com/optimizely/ab/event/internal/serializer/SerializerTestUtils.java b/core-api/src/test/java/com/optimizely/ab/event/internal/serializer/SerializerTestUtils.java index 86c9d66fa..b4f7e70f7 100644 --- a/core-api/src/test/java/com/optimizely/ab/event/internal/serializer/SerializerTestUtils.java +++ b/core-api/src/test/java/com/optimizely/ab/event/internal/serializer/SerializerTestUtils.java @@ -73,6 +73,7 @@ static Impression generateImpression() { impression.setDecision(decision); impression.setUserFeatures(userFeatures); impression.setClientVersion("0.1.1"); + impression.setAnonymizeIP(true); return impression; } @@ -91,6 +92,7 @@ static Conversion generateConversion() { conversion.setEventFeatures(eventFeatures); conversion.setIsGlobalHoldback(isGlobalHoldback); conversion.setClientVersion("0.1.1"); + conversion.setAnonymizeIP(true); return conversion; } diff --git a/core-api/src/test/resources/config/valid-project-config-v3.json b/core-api/src/test/resources/config/valid-project-config-v3.json index db8f7bc34..a12dc22e7 100644 --- a/core-api/src/test/resources/config/valid-project-config-v3.json +++ b/core-api/src/test/resources/config/valid-project-config-v3.json @@ -3,6 +3,7 @@ "projectId": "1234", "version": "3", "revision": "42", + "anonymizeIP": true, "experiments": [ { "id": "223", diff --git a/core-api/src/test/resources/serializer/conversion.json b/core-api/src/test/resources/serializer/conversion.json index 2e4f86e67..0a9ceacfd 100644 --- a/core-api/src/test/resources/serializer/conversion.json +++ b/core-api/src/test/resources/serializer/conversion.json @@ -33,6 +33,7 @@ ], "eventFeatures": [], "isGlobalHoldback": false, + "anonymizeIP": true, "clientEngine": "java-sdk", "clientVersion": "0.1.1" } \ No newline at end of file diff --git a/core-api/src/test/resources/serializer/impression.json b/core-api/src/test/resources/serializer/impression.json index 2cfe69b06..cb1900f57 100644 --- a/core-api/src/test/resources/serializer/impression.json +++ b/core-api/src/test/resources/serializer/impression.json @@ -2,6 +2,7 @@ "visitorId": "testvisitor", "timestamp": 12345, "isGlobalHoldback": false, + "anonymizeIP": true, "projectId": "1", "decision": { "variationId": "4", From 9167e5248aebcee98e78d71e2ffc86fc5a520e8e Mon Sep 17 00:00:00 2001 From: Vignesh Raja Date: Wed, 14 Dec 2016 14:51:43 -0800 Subject: [PATCH 2/5] Rename UserExperimentRecord to UserProfile (#43) --- CHANGELOG.md | 5 + .../java/com/optimizely/ab/Optimizely.java | 12 +-- .../com/optimizely/ab/bucketing/Bucketer.java | 36 +++---- ...ExperimentRecord.java => UserProfile.java} | 4 +- .../optimizely/ab/OptimizelyBuilderTest.java | 10 +- .../com/optimizely/ab/OptimizelyTestV1.java | 2 +- .../com/optimizely/ab/OptimizelyTestV2.java | 4 +- .../com/optimizely/ab/OptimizelyTestV3.java | 2 +- .../optimizely/ab/bucketing/BucketerTest.java | 102 +++++++++--------- gradle.properties | 2 +- 10 files changed, 91 insertions(+), 88 deletions(-) rename core-api/src/main/java/com/optimizely/ab/bucketing/{UserExperimentRecord.java => UserProfile.java} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77e9156ac..b35f3d214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.2.0 + +- Change `UserExperimentRecord` to `UserProfile` +- Add support for IP anonymization + ## 1.1.0 - Add support for live variables 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 1581a68b4..63ca72b79 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -18,7 +18,7 @@ import com.optimizely.ab.annotations.VisibleForTesting; import com.optimizely.ab.bucketing.Bucketer; -import com.optimizely.ab.bucketing.UserExperimentRecord; +import com.optimizely.ab.bucketing.UserProfile; import com.optimizely.ab.config.Attribute; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; @@ -104,7 +104,7 @@ private Optimizely(@Nonnull ProjectConfig projectConfig, // Do work here that should be done once per Optimizely lifecycle @VisibleForTesting void initialize() { - bucketer.cleanUserExperimentRecords(); + bucketer.cleanUserProfiles(); } //======== activate calls ========// @@ -596,7 +596,7 @@ public static class Builder { private String datafile; private Bucketer bucketer; - private UserExperimentRecord userExperimentRecord; + private UserProfile userProfile; private ErrorHandler errorHandler; private EventHandler eventHandler; private EventBuilder eventBuilder; @@ -615,8 +615,8 @@ public Builder withErrorHandler(ErrorHandler errorHandler) { return this; } - public Builder withUserExperimentRecord(UserExperimentRecord userExperimentRecord) { - this.userExperimentRecord = userExperimentRecord; + public Builder withUserProfile(UserProfile userProfile) { + this.userProfile = userProfile; return this; } @@ -653,7 +653,7 @@ public Optimizely build() throws ConfigParseException { // use the default bucketer and event builder, if no overrides were provided if (bucketer == null) { - bucketer = new Bucketer(projectConfig, userExperimentRecord); + bucketer = new Bucketer(projectConfig, userProfile); } if (clientEngine == null) { diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index 0560951f4..7ffb41e1a 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -47,7 +47,7 @@ public class Bucketer { private final ProjectConfig projectConfig; - @Nullable private final UserExperimentRecord userExperimentRecord; + @Nullable private final UserProfile userProfile; private static final Logger logger = LoggerFactory.getLogger(Bucketer.class); @@ -63,9 +63,9 @@ public Bucketer(ProjectConfig projectConfig) { this(projectConfig, null); } - public Bucketer(ProjectConfig projectConfig, @Nullable UserExperimentRecord userExperimentRecord) { + public Bucketer(ProjectConfig projectConfig, @Nullable UserProfile userProfile) { this.projectConfig = projectConfig; - this.userExperimentRecord = userExperimentRecord; + this.userProfile = userProfile; } private String bucketToEntity(int bucketValue, List trafficAllocations) { @@ -112,12 +112,12 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, String experimentKey = experiment.getKey(); String combinedBucketId = userId + experimentId; - // If a user experiment record instance is present then check it for a saved variation - if (userExperimentRecord != null) { - String variationKey = userExperimentRecord.lookup(userId, experimentKey); + // If a user profile instance is present then check it for a saved variation + if (userProfile != null) { + String variationKey = userProfile.lookup(userId, experimentKey); if (variationKey != null) { logger.info("Returning previously activated variation \"{}\" of experiment \"{}\" " - + "for user \"{}\" from user experiment record.", + + "for user \"{}\" from user profile.", variationKey, experimentKey, userId); // A variation is stored for this combined bucket id return projectConfig @@ -127,7 +127,7 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, .get(variationKey); } else { logger.info("No previously activated variation of experiment \"{}\" " - + "for user \"{}\" found in user experiment record.", + + "for user \"{}\" found in user profile.", experimentKey, userId); } } @@ -145,9 +145,9 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, logger.info("User \"{}\" is in variation \"{}\" of experiment \"{}\".", userId, variationKey, experimentKey); - // If a user experiment record is present give it a variation to store - if (userExperimentRecord != null) { - boolean saved = userExperimentRecord.save(userId, experiment.getKey(), variationKey); + // If a user profile is present give it a variation to store + if (userProfile != null) { + boolean saved = userProfile.save(userId, experiment.getKey(), variationKey); if (saved) { logger.info("Saved variation \"{}\" of experiment \"{}\" for user \"{}\".", variationKey, experimentKey, userId); @@ -224,23 +224,23 @@ int generateBucketValue(int hashCode) { } @Nullable - public UserExperimentRecord getUserExperimentRecord() { - return userExperimentRecord; + public UserProfile getUserProfile() { + return userProfile; } /** - * Gives implementations of {@link UserExperimentRecord} a chance to remove records + * Gives implementations of {@link UserProfile} a chance to remove records * of experiments that are deleted or not running. */ - public void cleanUserExperimentRecords() { - if (userExperimentRecord != null) { - Map> records = userExperimentRecord.getAllRecords(); + public void cleanUserProfiles() { + if (userProfile != null) { + Map> records = userProfile.getAllRecords(); if (records != null) { for (Map.Entry> record : records.entrySet()) { for (String experimentKey : record.getValue().keySet()) { Experiment experiment = projectConfig.getExperimentKeyMapping().get(experimentKey); if (experiment == null || !experiment.isRunning()) { - userExperimentRecord.remove(record.getKey(), experimentKey); + userProfile.remove(record.getKey(), experimentKey); } } } diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/UserExperimentRecord.java b/core-api/src/main/java/com/optimizely/ab/bucketing/UserProfile.java similarity index 96% rename from core-api/src/main/java/com/optimizely/ab/bucketing/UserExperimentRecord.java rename to core-api/src/main/java/com/optimizely/ab/bucketing/UserProfile.java index de39b627f..f06ed7b5d 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/UserExperimentRecord.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/UserProfile.java @@ -25,7 +25,7 @@ * user experience after changing traffic allocations. Also, this interface gives users * a hook to keep track of activation history. */ -public interface UserExperimentRecord { +public interface UserProfile { /** * Called when implementors should save an activation @@ -47,7 +47,7 @@ public interface UserExperimentRecord { String lookup(String userId, String experimentKey); /** - * Called when user experiment record should be removed + * Called when user profile should be removed * * Records should be removed when an experiment is not running or when an experiment has been * deleted. diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java index 1ea633ed9..59a5ffefb 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java @@ -16,7 +16,7 @@ */ package com.optimizely.ab; -import com.optimizely.ab.bucketing.UserExperimentRecord; +import com.optimizely.ab.bucketing.UserProfile; import com.optimizely.ab.config.ProjectConfigTestUtils; import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.error.ErrorHandler; @@ -112,13 +112,13 @@ public void withDefaultErrorHandler() throws Exception { } @Test - public void withUserExperimentRecord() throws Exception { - UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class); + public void withUserProfile() throws Exception { + UserProfile userProfile = mock(UserProfile.class); Optimizely optimizelyClient = Optimizely.builder(validConfigJsonV2(), mockEventHandler) - .withUserExperimentRecord(userExperimentRecord) + .withUserProfile(userProfile) .build(); - assertThat(optimizelyClient.bucketer.getUserExperimentRecord(), is(userExperimentRecord)); + assertThat(optimizelyClient.bucketer.getUserProfile(), is(userProfile)); } @Test diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV1.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV1.java index aef44d5cc..3b947fc59 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV1.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV1.java @@ -159,7 +159,7 @@ public void initializationOccursForBucketerWhenBuildingOptly() throws Exception .withErrorHandler(mockErrorHandler) .build(); - verify(mockBucketer).cleanUserExperimentRecords(); + verify(mockBucketer).cleanUserProfiles(); } /** diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java index 96ddba7d8..93f2fe5e7 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java @@ -31,10 +31,8 @@ import com.optimizely.ab.error.RaiseExceptionErrorHandler; import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.LogEvent; -import com.optimizely.ab.event.internal.BuildVersionInfo; import com.optimizely.ab.event.internal.EventBuilder; import com.optimizely.ab.event.internal.EventBuilderV2; -import com.optimizely.ab.event.internal.payload.Event.ClientEngine; import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.internal.ProjectValidationUtils; @@ -161,7 +159,7 @@ public void initializationOccursForBucketerWhenBuildingOptly() throws Exception .withErrorHandler(mockErrorHandler) .build(); - verify(mockBucketer).cleanUserExperimentRecords(); + verify(mockBucketer).cleanUserProfiles(); } /** diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java index 682ed74f1..45692a8d4 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java @@ -161,7 +161,7 @@ public void initializationOccursForBucketerWhenBuildingOptly() throws Exception .withErrorHandler(mockErrorHandler) .build(); - verify(mockBucketer).cleanUserExperimentRecords(); + verify(mockBucketer).cleanUserProfiles(); } /** diff --git a/core-api/src/test/java/com/optimizely/ab/bucketing/BucketerTest.java b/core-api/src/test/java/com/optimizely/ab/bucketing/BucketerTest.java index 48fdd6322..a726803f9 100644 --- a/core-api/src/test/java/com/optimizely/ab/bucketing/BucketerTest.java +++ b/core-api/src/test/java/com/optimizely/ab/bucketing/BucketerTest.java @@ -447,12 +447,12 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception { /** * Verify that {@link Bucketer#bucket(Experiment,String)} saves a variation of an experiment for a user - * when a {@link UserExperimentRecord} is present. + * when a {@link UserProfile} is present. */ - @Test public void bucketUserSaveActivationWithUserExperimentRecord() throws Exception { + @Test public void bucketUserSaveActivationWithUserProfile() throws Exception { final AtomicInteger bucketValue = new AtomicInteger(); - UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class); - Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord); + UserProfile userProfile = mock(UserProfile.class); + Bucketer algorithm = mockUserProfileAlgorithm(bucketValue, userProfile); bucketValue.set(3000); ProjectConfig projectConfig = validProjectConfigV2(); @@ -460,24 +460,24 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception { Experiment groupExperiment = groupExperiments.get(0); final Variation variation = groupExperiment.getVariations().get(0); - when(userExperimentRecord.save("blah", groupExperiment.getKey(), variation.getKey())).thenReturn(true); + when(userProfile.save("blah", groupExperiment.getKey(), variation.getKey())).thenReturn(true); assertThat(algorithm.bucket(groupExperiment, "blah"), is(variation)); logbackVerifier.expectMessage(Level.INFO, "Saved variation \"e2_vtag1\" of experiment \"group_etag2\" for user \"blah\"."); - verify(userExperimentRecord).save("blah", groupExperiment.getKey(), variation.getKey()); + verify(userProfile).save("blah", groupExperiment.getKey(), variation.getKey()); } /** * Verify that {@link Bucketer#bucket(Experiment,String)} logs correctly - * when a {@link UserExperimentRecord} is present and fails to save an activation. + * when a {@link UserProfile} is present and fails to save an activation. */ - @Test public void bucketUserSaveActivationFailWithUserExperimentRecord() throws Exception { + @Test public void bucketUserSaveActivationFailWithUserProfile() throws Exception { final AtomicInteger bucketValue = new AtomicInteger(); - UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class); - Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord); + UserProfile userProfile = mock(UserProfile.class); + Bucketer algorithm = mockUserProfileAlgorithm(bucketValue, userProfile); bucketValue.set(3000); ProjectConfig projectConfig = validProjectConfigV2(); @@ -485,24 +485,24 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception { Experiment groupExperiment = groupExperiments.get(0); final Variation variation = groupExperiment.getVariations().get(0); - when(userExperimentRecord.save("blah", groupExperiment.getKey(), variation.getKey())).thenReturn(false); + when(userProfile.save("blah", groupExperiment.getKey(), variation.getKey())).thenReturn(false); assertThat(algorithm.bucket(groupExperiment, "blah"), is(variation)); logbackVerifier.expectMessage(Level.WARN, "Failed to save variation \"e2_vtag1\" of experiment \"group_etag2\" for user \"blah\"."); - verify(userExperimentRecord).save("blah", groupExperiment.getKey(), variation.getKey()); + verify(userProfile).save("blah", groupExperiment.getKey(), variation.getKey()); } /** * Verify that {@link Bucketer#bucket(Experiment,String)} returns a variation that is - * stored in the provided {@link UserExperimentRecord}. + * stored in the provided {@link UserProfile}. */ - @Test public void bucketUserRestoreActivationWithUserExperimentRecord() throws Exception { + @Test public void bucketUserRestoreActivationWithUserProfile() throws Exception { final AtomicInteger bucketValue = new AtomicInteger(); - UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class); - Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord); + UserProfile userProfile = mock(UserProfile.class); + Bucketer algorithm = mockUserProfileAlgorithm(bucketValue, userProfile); bucketValue.set(3000); ProjectConfig projectConfig = validProjectConfigV2(); @@ -510,25 +510,25 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception { Experiment groupExperiment = groupExperiments.get(0); final Variation variation = groupExperiment.getVariations().get(0); - when(userExperimentRecord.lookup("blah", groupExperiment.getKey())).thenReturn(variation.getKey()); + when(userProfile.lookup("blah", groupExperiment.getKey())).thenReturn(variation.getKey()); assertThat(algorithm.bucket(groupExperiment, "blah"), is(variation)); logbackVerifier.expectMessage(Level.INFO, "Returning previously activated variation \"e2_vtag1\" of experiment \"group_etag2\"" - + " for user \"blah\" from user experiment record."); + + " for user \"blah\" from user profile."); - verify(userExperimentRecord).lookup("blah", groupExperiment.getKey()); + verify(userProfile).lookup("blah", groupExperiment.getKey()); } /** - * Verify {@link Bucketer#bucket(Experiment,String)} handles a present {@link UserExperimentRecord} + * Verify {@link Bucketer#bucket(Experiment,String)} handles a present {@link UserProfile} * returning null when looking up a variation. */ - @Test public void bucketUserRestoreActivationNullWithUserExperimentRecord() throws Exception { + @Test public void bucketUserRestoreActivationNullWithUserProfile() throws Exception { final AtomicInteger bucketValue = new AtomicInteger(); - UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class); - Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord); + UserProfile userProfile = mock(UserProfile.class); + Bucketer algorithm = mockUserProfileAlgorithm(bucketValue, userProfile); bucketValue.set(3000); ProjectConfig projectConfig = validProjectConfigV2(); @@ -536,91 +536,91 @@ public void bucketUserNotInOverlappingGroupExperiment() throws Exception { Experiment groupExperiment = groupExperiments.get(0); final Variation variation = groupExperiment.getVariations().get(0); - when(userExperimentRecord.lookup("blah", groupExperiment.getKey())).thenReturn(null); + when(userProfile.lookup("blah", groupExperiment.getKey())).thenReturn(null); assertThat(algorithm.bucket(groupExperiment, "blah"), is(variation)); logbackVerifier.expectMessage(Level.INFO, "No previously activated variation of experiment " + - "\"group_etag2\" for user \"blah\" found in user experiment record."); - verify(userExperimentRecord).lookup("blah", groupExperiment.getKey()); + "\"group_etag2\" for user \"blah\" found in user profile."); + verify(userProfile).lookup("blah", groupExperiment.getKey()); } /** - * Verify {@link Bucketer#cleanUserExperimentRecords()} handles a null {@link UserExperimentRecord}. + * Verify {@link Bucketer#cleanUserProfiles()} handles a null {@link UserProfile}. */ @Test - public void nullUserExperimentRecordWhenCleaning() { + public void nullUserProfileWhenCleaning() { final AtomicInteger bucketValue = new AtomicInteger(); Bucketer algorithm = mockBucketAlgorithm(bucketValue); bucketValue.set(3000); try { - algorithm.cleanUserExperimentRecords(); + algorithm.cleanUserProfiles(); } catch (NullPointerException e) { fail(); } } /** - * Verify {@link Bucketer#cleanUserExperimentRecords()} handles a null returned from - * {@link UserExperimentRecord#getAllRecords()}. + * Verify {@link Bucketer#cleanUserProfiles()} handles a null returned from + * {@link UserProfile#getAllRecords()}. */ @Test - public void nullUserExperimentRecords() { + public void nullUserProfiles() { final AtomicInteger bucketValue = new AtomicInteger(); - UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class); - Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord); + UserProfile userProfile = mock(UserProfile.class); + Bucketer algorithm = mockUserProfileAlgorithm(bucketValue, userProfile); bucketValue.set(3000); - when(userExperimentRecord.getAllRecords()).thenReturn(null); + when(userProfile.getAllRecords()).thenReturn(null); try { - algorithm.cleanUserExperimentRecords(); + algorithm.cleanUserProfiles(); } catch (NullPointerException e) { fail(); } } /** - * Verify {@link Bucketer#cleanUserExperimentRecords()} removes experiments + * Verify {@link Bucketer#cleanUserProfiles()} removes experiments * that are no longer in the {@link ProjectConfig}. */ @Test public void cleanRemovesRecordsOfExperimentsThatNoLongerExist() { final AtomicInteger bucketValue = new AtomicInteger(); - UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class); - Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord); + UserProfile userProfile = mock(UserProfile.class); + Bucketer algorithm = mockUserProfileAlgorithm(bucketValue, userProfile); bucketValue.set(3000); Map> records = new HashMap>(); Map activation = new HashMap(); activation.put("exp1", "var1"); records.put("blah", activation); - when(userExperimentRecord.getAllRecords()).thenReturn(records); + when(userProfile.getAllRecords()).thenReturn(records); - algorithm.cleanUserExperimentRecords(); + algorithm.cleanUserProfiles(); - verify(userExperimentRecord).remove("blah", "exp1"); + verify(userProfile).remove("blah", "exp1"); } /** - * Verify {@link Bucketer#cleanUserExperimentRecords()} removes experiments + * Verify {@link Bucketer#cleanUserProfiles()} removes experiments * that are paused in the {@link ProjectConfig}. */ @Test public void cleanRemovesRecordsOfExperimentsThatAreNotRunning() { final AtomicInteger bucketValue = new AtomicInteger(); - UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class); - Bucketer algorithm = mockUserExperimentRecordAlgorithm(bucketValue, userExperimentRecord); + UserProfile userProfile = mock(UserProfile.class); + Bucketer algorithm = mockUserProfileAlgorithm(bucketValue, userProfile); bucketValue.set(3000); Map> records = new HashMap>(); Map activation = new HashMap(); activation.put("exp1", "var1"); records.put("blah", activation); - when(userExperimentRecord.getAllRecords()).thenReturn(records); + when(userProfile.getAllRecords()).thenReturn(records); - algorithm.cleanUserExperimentRecords(); + algorithm.cleanUserProfiles(); - verify(userExperimentRecord).remove("blah", "exp1"); + verify(userProfile).remove("blah", "exp1"); } //======== Helper methods ========// @@ -642,13 +642,13 @@ int generateBucketValue(int hashCode) { /** * Sets up a mock algorithm that returns an expected bucket value. * - * Includes a composed {@link UserExperimentRecord} mock instance + * Includes a composed {@link UserProfile} mock instance * * @param bucketValue the expected bucket value holder * @return the mock bucket algorithm */ - private Bucketer mockUserExperimentRecordAlgorithm(final AtomicInteger bucketValue, final UserExperimentRecord userExperimentRecord) { - return new Bucketer(validProjectConfigV2(), userExperimentRecord) { + private Bucketer mockUserProfileAlgorithm(final AtomicInteger bucketValue, final UserProfile userProfile) { + return new Bucketer(validProjectConfigV2(), userProfile) { @Override int generateBucketValue(int hashCode) { return bucketValue.get(); diff --git a/gradle.properties b/gradle.properties index e76d555a2..338713112 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Maven version -version = 1.1.0-SNAPSHOT +version = 1.2.0-SNAPSHOT # Artifact paths mavenS3Bucket = optimizely-maven From 18ed45bc932407f77c7fe43551f6f95fe595d50d Mon Sep 17 00:00:00 2001 From: Elliot Kim Date: Fri, 9 Dec 2016 14:57:15 +0100 Subject: [PATCH 3/5] Implement notifications for Optimizely SDK events --- CHANGELOG.md | 1 + .../java/com/optimizely/ab/Optimizely.java | 32 ++++ .../ab/event/internal/EventBuilderV2.java | 2 +- .../notification/NotificationBroadcaster.java | 95 ++++++++++ .../ab/notification/NotificationListener.java | 50 ++++++ .../com/optimizely/ab/OptimizelyTestV3.java | 164 ++++++++++++++++++ .../NotificationBroadcasterTest.java | 125 +++++++++++++ 7 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 core-api/src/main/java/com/optimizely/ab/notification/NotificationBroadcaster.java create mode 100644 core-api/src/main/java/com/optimizely/ab/notification/NotificationListener.java create mode 100644 core-api/src/test/java/com/optimizely/ab/notification/NotificationBroadcasterTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b35f3d214..a48015d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Change `UserExperimentRecord` to `UserProfile` - Add support for IP anonymization +- Add `NotificationListener` for SDK events ## 1.1.0 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 63ca72b79..cae44b771 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -39,6 +39,8 @@ import com.optimizely.ab.event.internal.EventBuilderV2; import com.optimizely.ab.event.internal.payload.Event.ClientEngine; import com.optimizely.ab.internal.ProjectValidationUtils; +import com.optimizely.ab.notification.NotificationListener; +import com.optimizely.ab.notification.NotificationBroadcaster; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -89,6 +91,7 @@ public class Optimizely { @VisibleForTesting final ProjectConfig projectConfig; @VisibleForTesting final EventHandler eventHandler; @VisibleForTesting final ErrorHandler errorHandler; + @VisibleForTesting final NotificationBroadcaster notificationBroadcaster = new NotificationBroadcaster(); private Optimizely(@Nonnull ProjectConfig projectConfig, @Nonnull Bucketer bucketer, @@ -180,6 +183,8 @@ private Optimizely(@Nonnull ProjectConfig projectConfig, logger.error("Unexpected exception in event dispatcher", e); } + notificationBroadcaster.broadcastExperimentActivated(experiment, userId, attributes, variation); + return variation; } @@ -438,6 +443,33 @@ private static ProjectConfig getProjectConfig(String datafile) throws ConfigPars return DefaultConfigParser.getInstance().parseProjectConfig(datafile); } + //======== Notification listeners ========// + + /** + * Add a {@link NotificationListener} if it does not exist already. + * + * @param listener listener to add + */ + public void addNotificationListener(@Nonnull NotificationListener listener) { + notificationBroadcaster.addListener(listener); + } + + /** + * Remove a {@link NotificationListener} if it exists. + * + * @param listener listener to remove + */ + public void removeNotificationListener(@Nonnull NotificationListener listener) { + notificationBroadcaster.removeListener(listener); + } + + /** + * Remove all {@link NotificationListener}. + */ + public void clearNotificationListeners() { + notificationBroadcaster.clearListeners(); + } + //======== Helper methods ========// /** diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilderV2.java b/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilderV2.java index 329e2d74f..69837df83 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilderV2.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/EventBuilderV2.java @@ -209,4 +209,4 @@ private List createLayerStates(ProjectConfig projectConfig, Bucketer return layerStates; } -} \ No newline at end of file +} diff --git a/core-api/src/main/java/com/optimizely/ab/notification/NotificationBroadcaster.java b/core-api/src/main/java/com/optimizely/ab/notification/NotificationBroadcaster.java new file mode 100644 index 000000000..8d2549ba9 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/notification/NotificationBroadcaster.java @@ -0,0 +1,95 @@ +/** + * + * Copyright 2016, Optimizely + * + * 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.notification; + +import com.optimizely.ab.annotations.VisibleForTesting; +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.Variation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.Map; + +import javax.annotation.Nonnull; + +/** + * Manages Optimizely SDK notification listeners and broadcasts messages. + */ +public class NotificationBroadcaster { + + private static final Logger logger = LoggerFactory.getLogger(NotificationBroadcaster.class); + + @VisibleForTesting final HashSet listeners = + new HashSet(); + + /** + * Add a listener if it does not exist already. + * + * @param listener listener to add + */ + public void addListener(@Nonnull NotificationListener listener) { + if (listeners.contains(listener)) { + logger.debug("Notification listener was not added because it already existed"); + return; + } + + listeners.add(listener); + logger.debug("Notification listener was added"); + } + + /** + * Remove a listener if it exists. + * + * @param listener listener to remove + */ + public void removeListener(@Nonnull NotificationListener listener) { + if (listeners.contains(listener)) { + listeners.remove(listener); + logger.debug("Notification listener was removed"); + return; + } + + logger.debug("Notification listener was not removed because it did not exist"); + } + + /** + * Remove all listeners. + */ + public void clearListeners() { + listeners.clear(); + logger.debug("Notification listeners were cleared"); + } + + /** + * Notify listeners that an Optimizely experiment has been activated. + * + * @param experiment the key of the activated experiment + * @param userId the id of the user + * @param attributes a map of attributes about the user + * @param variation the key of the variation that was bucketed + */ + public void broadcastExperimentActivated(@Nonnull Experiment experiment, + @Nonnull String userId, + @Nonnull Map attributes, + @Nonnull Variation variation) { + for (final NotificationListener iterListener : listeners) { + iterListener.onExperimentActivated(experiment, userId, attributes, variation); + } + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/notification/NotificationListener.java b/core-api/src/main/java/com/optimizely/ab/notification/NotificationListener.java new file mode 100644 index 000000000..0164091c2 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/notification/NotificationListener.java @@ -0,0 +1,50 @@ +/** + * + * Copyright 2016, Optimizely + * + * 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.notification; + +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.Variation; + +import java.util.Map; + +import javax.annotation.Nonnull; + +/** + * Abstract class for Optimizely notification listeners. + *

+ * We use an abstract class here instead of an interface for convenience of use and backwards + * compatibility in the future. An interface would force consumers to implement every method defined + * on it as well as update their application code with new method implementations every time new + * methods are added to the interface in the SDK. An abstract classes allows consumers to override + * just the methods they need. + */ +public abstract class NotificationListener { + + /** + * Listener that is called after an experiment has been activated. + * + * @param experiment the activated experiment + * @param userId the id of the user + * @param attributes a map of attributes about the user + * @param variation the key of the variation that was bucketed + */ + public void onExperimentActivated(@Nonnull Experiment experiment, + @Nonnull String userId, + @Nonnull Map attributes, + @Nonnull Variation variation) { + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java index 45692a8d4..c5aac0f2d 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java @@ -36,6 +36,7 @@ import com.optimizely.ab.event.internal.EventBuilderV2; import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.internal.ProjectValidationUtils; +import com.optimizely.ab.notification.NotificationListener; import org.junit.Rule; import org.junit.Test; @@ -73,6 +74,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -1452,6 +1454,168 @@ public void getVariationExperimentStatusPrecedesForcedVariation() throws Excepti assertNull(optimizely.getVariation(experiment.getKey(), "testUser3")); } + //======== Notification listeners ========// + + /** + * Verify that {@link Optimizely#addNotificationListener(NotificationListener)} properly calls + * through to {@link com.optimizely.ab.notification.NotificationBroadcaster} and the listener is + * added and notified when an experiment is activated. + */ + @Test + public void addNotificationListener() throws Exception { + String datafile = validConfigJsonV3(); + ProjectConfig projectConfig = validProjectConfigV3(); + Experiment activatedExperiment = projectConfig.getExperiments().get(0); + Variation bucketedVariation = activatedExperiment.getVariations().get(0); + EventBuilder mockEventBuilder = mock(EventBuilder.class); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withBucketing(mockBucketer) + .withEventBuilder(mockEventBuilder) + .withConfig(projectConfig) + .withErrorHandler(mockErrorHandler) + .build(); + + String userId = "userId"; + Map attributes = Collections.singletonMap("browser_type", "chrome"); + + Map testParams = new HashMap(); + testParams.put("test", "params"); + LogEvent logEventToDispatch = new LogEvent(RequestMethod.GET, "test_url", testParams, ""); + when(mockEventBuilder.createImpressionEvent(projectConfig, activatedExperiment, + bucketedVariation, userId, attributes)) + .thenReturn(logEventToDispatch); + + when(mockBucketer.bucket(activatedExperiment, userId)) + .thenReturn(bucketedVariation); + + when(mockEventBuilder.createImpressionEvent(projectConfig, activatedExperiment, + bucketedVariation, userId, attributes)) + .thenReturn(logEventToDispatch); + + // Add listener + NotificationListener listener = mock(NotificationListener.class); + optimizely.addNotificationListener(listener); + + // Check if listener is notified when experiment is activated + Variation actualVariation = optimizely.activate(activatedExperiment, userId, attributes); + verify(listener, times(1)) + .onExperimentActivated(activatedExperiment, userId, attributes, actualVariation); + + // Check if listener is notified when live variable is accessed + boolean activateExperiment = true; + optimizely.getVariableString("string_variable", activateExperiment, userId, attributes); + verify(listener, times(2)) + .onExperimentActivated(activatedExperiment, userId, attributes, actualVariation); + } + + /** + * Verify that {@link Optimizely#removeNotificationListener(NotificationListener)} properly + * calls through to {@link com.optimizely.ab.notification.NotificationBroadcaster} and the + * listener is removed and no longer notified when an experiment is activated. + */ + @Test + public void removeNotificationListener() throws Exception { + String datafile = validConfigJsonV3(); + ProjectConfig projectConfig = validProjectConfigV3(); + Experiment activatedExperiment = projectConfig.getExperiments().get(0); + Variation bucketedVariation = activatedExperiment.getVariations().get(0); + EventBuilder mockEventBuilder = mock(EventBuilder.class); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withBucketing(mockBucketer) + .withEventBuilder(mockEventBuilder) + .withConfig(projectConfig) + .withErrorHandler(mockErrorHandler) + .build(); + + String userId = "userId"; + Map attributes = Collections.singletonMap("browser_type", "chrome"); + + Map testParams = new HashMap(); + testParams.put("test", "params"); + LogEvent logEventToDispatch = new LogEvent(RequestMethod.GET, "test_url", testParams, ""); + when(mockEventBuilder.createImpressionEvent(projectConfig, activatedExperiment, + bucketedVariation, userId, attributes)) + .thenReturn(logEventToDispatch); + + when(mockBucketer.bucket(activatedExperiment, userId)) + .thenReturn(bucketedVariation); + + when(mockEventBuilder.createImpressionEvent(projectConfig, activatedExperiment, + bucketedVariation, userId, attributes)) + .thenReturn(logEventToDispatch); + + // Add and remove listener + NotificationListener listener = mock(NotificationListener.class); + optimizely.addNotificationListener(listener); + optimizely.removeNotificationListener(listener); + + // Check if listener is notified after an experiment is activated + Variation actualVariation = optimizely.activate(activatedExperiment, userId, attributes); + verify(listener, never()) + .onExperimentActivated(activatedExperiment, userId, attributes, actualVariation); + + // Check if listener is notified after a live variable is accessed + boolean activateExperiment = true; + optimizely.getVariableString("string_variable", activateExperiment, userId, attributes); + verify(listener, never()) + .onExperimentActivated(activatedExperiment, userId, attributes, actualVariation); + } + + /** + * Verify that {@link Optimizely#clearNotificationListeners()} properly calls through to + * {@link com.optimizely.ab.notification.NotificationBroadcaster} and all listeners are removed + * and no longer notified when an experiment is activated. + */ + @Test + public void clearNotificationListeners() throws Exception { + String datafile = validConfigJsonV3(); + ProjectConfig projectConfig = validProjectConfigV3(); + Experiment activatedExperiment = projectConfig.getExperiments().get(0); + Variation bucketedVariation = activatedExperiment.getVariations().get(0); + EventBuilder mockEventBuilder = mock(EventBuilder.class); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withBucketing(mockBucketer) + .withEventBuilder(mockEventBuilder) + .withConfig(projectConfig) + .withErrorHandler(mockErrorHandler) + .build(); + + String userId = "userId"; + Map attributes = Collections.singletonMap("browser_type", "chrome"); + + Map testParams = new HashMap(); + testParams.put("test", "params"); + LogEvent logEventToDispatch = new LogEvent(RequestMethod.GET, "test_url", testParams, ""); + when(mockEventBuilder.createImpressionEvent(projectConfig, activatedExperiment, + bucketedVariation, userId, attributes)) + .thenReturn(logEventToDispatch); + + when(mockBucketer.bucket(activatedExperiment, userId)) + .thenReturn(bucketedVariation); + + when(mockEventBuilder.createImpressionEvent(projectConfig, activatedExperiment, + bucketedVariation, userId, attributes)) + .thenReturn(logEventToDispatch); + + NotificationListener listener = mock(NotificationListener.class); + optimizely.addNotificationListener(listener); + optimizely.clearNotificationListeners(); + + // Check if listener is notified after an experiment is activated + Variation actualVariation = optimizely.activate(activatedExperiment, userId, attributes); + verify(listener, never()) + .onExperimentActivated(activatedExperiment, userId, attributes, actualVariation); + + // Check if listener is notified after a live variable is accessed + boolean activateExperiment = true; + optimizely.getVariableString("string_variable", activateExperiment, userId, attributes); + verify(listener, never()) + .onExperimentActivated(activatedExperiment, userId, attributes, actualVariation); + } + //======== Helper methods ========// private Experiment createUnknownExperiment() { diff --git a/core-api/src/test/java/com/optimizely/ab/notification/NotificationBroadcasterTest.java b/core-api/src/test/java/com/optimizely/ab/notification/NotificationBroadcasterTest.java new file mode 100644 index 000000000..c15f1f6a9 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/notification/NotificationBroadcasterTest.java @@ -0,0 +1,125 @@ +/** + * + * Copyright 2016, Optimizely + * + * 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.notification; + +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.Variation; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link NotificationBroadcaster}. + */ +public class NotificationBroadcasterTest { + + private NotificationBroadcaster notificationBroadcaster; + private NotificationListener listener; + private NotificationListener listener2; + + @Before + public void initialize() { + notificationBroadcaster = new NotificationBroadcaster(); + listener = mock(NotificationListener.class); + listener2 = mock(NotificationListener.class); + } + + /** + * Verify that {@link NotificationBroadcaster#addListener(NotificationListener)} correctly adds + * a new listener. + */ + @Test + public void addListener() throws Exception { + notificationBroadcaster.addListener(listener); + assertEquals("addListener did not add the listener", + 1, notificationBroadcaster.listeners.size()); + } + + /** + * Verify that {@link NotificationBroadcaster#addListener(NotificationListener)} does not add a + * listener that has already been added. + */ + @Test + public void addListenerAlreadyExists() throws Exception { + notificationBroadcaster.addListener(listener); + notificationBroadcaster.addListener(listener); + assertEquals("addListener did not add the listener just once", + 1, notificationBroadcaster.listeners.size()); + } + + /** + * Verify that {@link NotificationBroadcaster#removeListener(NotificationListener)} removes a + * listener that has been added. + */ + @Test + public void removeListener() throws Exception { + notificationBroadcaster.addListener(listener); + notificationBroadcaster.removeListener(listener); + assertEquals("removeListener did not remove the listener", + 0, notificationBroadcaster.listeners.size()); + } + + /** + * Verify that {@link NotificationBroadcaster#removeListener(NotificationListener)} does not + * remove a listener that was not added. + */ + @Test + public void removeListenerNotAdded() throws Exception { + notificationBroadcaster.addListener(listener); + notificationBroadcaster.removeListener(listener2); + assertEquals("removeListener did not remove just the listener that was added", + 1, notificationBroadcaster.listeners.size()); + } + + /** + * Verify that {@link NotificationBroadcaster#clearListeners()} removes all listeners. + */ + @Test + public void clearListeners() throws Exception { + notificationBroadcaster.addListener(listener); + notificationBroadcaster.addListener(listener2); + notificationBroadcaster.clearListeners(); + assertEquals("clearListeners did not remove all listeners that were added", + 0, notificationBroadcaster.listeners.size()); + } + + /** + * Verify that {@link NotificationBroadcaster#broadcastExperimentActivated(Experiment, String, Map, Variation)} + * notifies all listeners. + */ + @Test + public void broadcastExperimentActivated() throws Exception { + notificationBroadcaster.addListener(listener); + notificationBroadcaster.addListener(listener2); + + Experiment experiment = mock(Experiment.class); + String userId = "user1"; + Map attributes = Collections.emptyMap(); + Variation variation = mock(Variation.class); + notificationBroadcaster.broadcastExperimentActivated( + experiment, userId, attributes, variation); + verify(listener).onExperimentActivated(experiment, userId, attributes, variation); + verify(listener2).onExperimentActivated(experiment, userId, attributes, variation); + } +} From f180ac519c266ddb8607d100a3c36cccb3addb5a Mon Sep 17 00:00:00 2001 From: Vignesh Raja Date: Thu, 15 Dec 2016 16:05:17 -0800 Subject: [PATCH 4/5] Change position of activateExperiment (#46) --- CHANGELOG.md | 1 + .../java/com/optimizely/ab/Optimizely.java | 46 ++++++++-------- .../com/optimizely/ab/OptimizelyTestV3.java | 54 +++++++++---------- 3 files changed, 51 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a48015d19..b0aefe2fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 1.2.0 +- Change position of `activateExperiment` parameter in the method signatures of `getVariableString`, `getVariableBoolean`, `getVariableInteger`, and `getVariableFloat` - Change `UserExperimentRecord` to `UserProfile` - Add support for IP anonymization - Add `NotificationListener` for SDK events 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 cae44b771..6fb6ea277 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -263,15 +263,15 @@ private void track(@Nonnull String eventName, //======== live variable getters ========// public @Nullable String getVariableString(@Nonnull String variableKey, - boolean activateExperiment, - @Nonnull String userId) throws UnknownLiveVariableException { - return getVariableString(variableKey, activateExperiment, userId, Collections.emptyMap()); + @Nonnull String userId, + boolean activateExperiment) throws UnknownLiveVariableException { + return getVariableString(variableKey, userId, Collections.emptyMap(), activateExperiment); } public @Nullable String getVariableString(@Nonnull String variableKey, - boolean activateExperiment, @Nonnull String userId, - @Nonnull Map attributes) + @Nonnull Map attributes, + boolean activateExperiment) throws UnknownLiveVariableException { LiveVariable variable = getLiveVariableOrThrow(projectConfig, variableKey); @@ -308,18 +308,18 @@ private void track(@Nonnull String eventName, } public @Nullable Boolean getVariableBoolean(@Nonnull String variableKey, - boolean activateExperiment, - @Nonnull String userId) throws UnknownLiveVariableException { - return getVariableBoolean(variableKey, activateExperiment, userId, Collections.emptyMap()); + @Nonnull String userId, + boolean activateExperiment) throws UnknownLiveVariableException { + return getVariableBoolean(variableKey, userId, Collections.emptyMap(), activateExperiment); } public @Nullable Boolean getVariableBoolean(@Nonnull String variableKey, - boolean activateExperiment, @Nonnull String userId, - @Nonnull Map attributes) + @Nonnull Map attributes, + boolean activateExperiment) throws UnknownLiveVariableException { - String variableValueString = getVariableString(variableKey, activateExperiment, userId, attributes); + String variableValueString = getVariableString(variableKey, userId, attributes, activateExperiment); if (variableValueString != null) { return Boolean.parseBoolean(variableValueString); } @@ -328,18 +328,18 @@ private void track(@Nonnull String eventName, } public @Nullable Integer getVariableInteger(@Nonnull String variableKey, - boolean activateExperiment, - @Nonnull String userId) throws UnknownLiveVariableException { - return getVariableInteger(variableKey, activateExperiment, userId, Collections.emptyMap()); + @Nonnull String userId, + boolean activateExperiment) throws UnknownLiveVariableException { + return getVariableInteger(variableKey, userId, Collections.emptyMap(), activateExperiment); } public @Nullable Integer getVariableInteger(@Nonnull String variableKey, - boolean activateExperiment, @Nonnull String userId, - @Nonnull Map attributes) + @Nonnull Map attributes, + boolean activateExperiment) throws UnknownLiveVariableException { - String variableValueString = getVariableString(variableKey, activateExperiment, userId, attributes); + String variableValueString = getVariableString(variableKey, userId, attributes, activateExperiment); if (variableValueString != null) { try { return Integer.parseInt(variableValueString); @@ -353,18 +353,18 @@ private void track(@Nonnull String eventName, } public @Nullable Float getVariableFloat(@Nonnull String variableKey, - boolean activateExperiment, - @Nonnull String userId) throws UnknownLiveVariableException { - return getVariableFloat(variableKey, activateExperiment, userId, Collections.emptyMap()); + @Nonnull String userId, + boolean activateExperiment) throws UnknownLiveVariableException { + return getVariableFloat(variableKey, userId, Collections.emptyMap(), activateExperiment); } public @Nullable Float getVariableFloat(@Nonnull String variableKey, - boolean activateExperiment, @Nonnull String userId, - @Nonnull Map attributes) + @Nonnull Map attributes, + boolean activateExperiment) throws UnknownLiveVariableException { - String variableValueString = getVariableString(variableKey, activateExperiment, userId, attributes); + String variableValueString = getVariableString(variableKey, userId, attributes, activateExperiment); if (variableValueString != null) { try { return Float.parseFloat(variableValueString); diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java index c5aac0f2d..2bdb8f4d6 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV3.java @@ -960,7 +960,7 @@ public void trackDispatchEventThrowsException() throws Exception { //======== live variable getters tests ========// /** - * Verify that {@link Optimizely#getVariableString(String, boolean, String)} returns null and logs properly when + * Verify that {@link Optimizely#getVariableString(String, String, boolean)} returns null and logs properly when * an invalid live variable key is provided and the {@link NoOpErrorHandler} is used. */ @Test @@ -973,11 +973,11 @@ public void getVariableInvalidVariableKeyNoOpErrorHandler() throws Exception { .build(); logbackVerifier.expectMessage(Level.ERROR, "Live variable \"invalid_key\" is not in the datafile."); - assertNull(optimizely.getVariableString("invalid_key", false, "userId")); + assertNull(optimizely.getVariableString("invalid_key", "userId", false)); } /** - * Verify that {@link Optimizely#getVariableString(String, boolean, String)} returns throws an + * Verify that {@link Optimizely#getVariableString(String, String, boolean)} returns throws an * {@link UnknownLiveVariableException} when an invalid live variable key is provided and the * {@link RaiseExceptionErrorHandler} is used. */ @@ -993,11 +993,11 @@ public void getVariableInvalidVariableKeyRaiseExceptionErrorHandler() throws Exc .withErrorHandler(new RaiseExceptionErrorHandler()) .build(); - optimizely.getVariableString("invalid_key", false, "userId"); + optimizely.getVariableString("invalid_key", "userId", false); } /** - * Verify that {@link Optimizely#getVariableString(String, boolean, String, Map)} returns a string live variable + * Verify that {@link Optimizely#getVariableString(String, String, Map, boolean)} returns a string live variable * value when an proper variable key is provided and dispatches an impression when activateExperiment is true. */ @Test @@ -1017,14 +1017,14 @@ public void getVariableStringActivateExperimentTrue() throws Exception { .withErrorHandler(new RaiseExceptionErrorHandler()) .build(); - assertThat(optimizely.getVariableString("string_variable", true, "userId", - Collections.singletonMap("browser_type", "chrome")), + assertThat(optimizely.getVariableString("string_variable", "userId", + Collections.singletonMap("browser_type", "chrome"), true), is("string_var_vtag1")); verify(mockEventHandler).dispatchEvent(any(LogEvent.class)); } /** - * Verify that {@link Optimizely#getVariableString(String, boolean, String, Map)} returns a string live variable + * Verify that {@link Optimizely#getVariableString(String, String, Map, boolean)} returns a string live variable * value when an proper variable key is provided and doesn't dispatch an impression when activateExperiment is * false. */ @@ -1045,14 +1045,14 @@ public void getVariableStringActivateExperimentFalse() throws Exception { .withErrorHandler(new RaiseExceptionErrorHandler()) .build(); - assertThat(optimizely.getVariableString("string_variable", false, "userId", - Collections.singletonMap("browser_type", "chrome")), + assertThat(optimizely.getVariableString("string_variable", "userId", + Collections.singletonMap("browser_type", "chrome"), false), is("string_var_vtag1")); verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class)); } /** - * Verify that {@link Optimizely#getVariableString(String, boolean, String)} returns the default value of + * Verify that {@link Optimizely#getVariableString(String, String, boolean)} returns the default value of * a live variable when no experiments are using the live variable. */ @Test @@ -1065,11 +1065,11 @@ public void getVariableStringReturnsDefaultValueNoExperimentsUsingLiveVariable() .build(); logbackVerifier.expectMessage(Level.WARN, "No experiment is using variable \"unused_string_variable\"."); - assertThat(optimizely.getVariableString("unused_string_variable", true, "userId"), is("unused_variable")); + assertThat(optimizely.getVariableString("unused_string_variable", "userId", true), is("unused_variable")); } /** - * Verify that {@link Optimizely#getVariableString(String, boolean, String, Map)} returns the default value when + * Verify that {@link Optimizely#getVariableString(String, String, Map, boolean)} returns the default value when * a user isn't bucketed into a variation in the experiment. */ @Test @@ -1086,13 +1086,13 @@ public void getVariableStringReturnsDefaultValueUserNotInVariation() throws Exce .withBucketing(mockBucketer) .build(); - assertThat(optimizely.getVariableString("string_variable", true, "userId", - Collections.singletonMap("browser_type", "chrome")), + assertThat(optimizely.getVariableString("string_variable", "userId", + Collections.singletonMap("browser_type", "chrome"), true), is("string_live_variable")); } /** - * Verify that {@link Optimizely#getVariableBoolean(String, boolean, String, Map)} returns a boolean live variable + * Verify that {@link Optimizely#getVariableBoolean(String, String, Map, boolean)} returns a boolean live variable * value when an proper variable key is provided and dispatches an impression when activateExperiment is true. */ @Test @@ -1111,12 +1111,12 @@ public void getVariableBoolean() throws Exception { .withBucketing(mockBucketer) .build(); - assertTrue(optimizely.getVariableBoolean("etag1_variable", true, "userId", - Collections.singletonMap("browser_type", "chrome"))); + assertTrue(optimizely.getVariableBoolean("etag1_variable", "userId", + Collections.singletonMap("browser_type", "chrome"), true)); } /** - * Verify that {@link Optimizely#getVariableFloat(String, boolean, String, Map)} returns a float live variable + * Verify that {@link Optimizely#getVariableFloat(String, String, Map, boolean)} returns a float live variable * value when an proper variable key is provided and dispatches an impression when activateExperiment is true. */ @Test @@ -1135,14 +1135,14 @@ public void getVariableFloat() throws Exception { .withBucketing(mockBucketer) .build(); - assertThat(optimizely.getVariableFloat("float_variable", true, "userId", - Collections.singletonMap("browser_type", "chrome")), + assertThat(optimizely.getVariableFloat("float_variable", "userId", + Collections.singletonMap("browser_type", "chrome"), true), is(5.3f)); verify(mockEventHandler).dispatchEvent(any(LogEvent.class)); } /** - * Verify that {@link Optimizely#getVariableInteger(String, boolean, String, Map)} returns a integer live variable + * Verify that {@link Optimizely#getVariableInteger(String, String, Map, boolean)} returns a integer live variable * value when an proper variable key is provided and dispatches an impression when activateExperiment is true. */ @Test @@ -1161,8 +1161,8 @@ public void getVariableInteger() throws Exception { .withBucketing(mockBucketer) .build(); - assertThat(optimizely.getVariableInteger("integer_variable", true, "userId", - Collections.singletonMap("browser_type", "chrome")), + assertThat(optimizely.getVariableInteger("integer_variable", "userId", + Collections.singletonMap("browser_type", "chrome"), true), is(10)); verify(mockEventHandler).dispatchEvent(any(LogEvent.class)); } @@ -1504,7 +1504,7 @@ public void addNotificationListener() throws Exception { // Check if listener is notified when live variable is accessed boolean activateExperiment = true; - optimizely.getVariableString("string_variable", activateExperiment, userId, attributes); + optimizely.getVariableString("string_variable", userId, attributes, activateExperiment); verify(listener, times(2)) .onExperimentActivated(activatedExperiment, userId, attributes, actualVariation); } @@ -1558,7 +1558,7 @@ public void removeNotificationListener() throws Exception { // Check if listener is notified after a live variable is accessed boolean activateExperiment = true; - optimizely.getVariableString("string_variable", activateExperiment, userId, attributes); + optimizely.getVariableString("string_variable", userId, attributes, activateExperiment); verify(listener, never()) .onExperimentActivated(activatedExperiment, userId, attributes, actualVariation); } @@ -1611,7 +1611,7 @@ public void clearNotificationListeners() throws Exception { // Check if listener is notified after a live variable is accessed boolean activateExperiment = true; - optimizely.getVariableString("string_variable", activateExperiment, userId, attributes); + optimizely.getVariableString("string_variable", userId, attributes, activateExperiment); verify(listener, never()) .onExperimentActivated(activatedExperiment, userId, attributes, actualVariation); } From 0442a0db71ec07b2e4a7a78a1df3143ab4fea58d Mon Sep 17 00:00:00 2001 From: Joshua Wang Date: Thu, 15 Dec 2016 16:43:11 -0800 Subject: [PATCH 5/5] Update .travis.yml (#44) --- .travis.yml | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4befc5144..22ff8e3bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,17 @@ language: java jdk: + - openjdk7 - oraclejdk7 + - oraclejdk8 install: true -before_script: - - git config user.name "Travis-CI" - - git config user.email "noreply@travis-ci.org" - - git checkout $TRAVIS_BRANCH script: - "./gradlew clean build check" - # - '[ "${TRAVIS_PULL_REQUEST}" != "false" ] || ./gradlew release -Prelease.useAutomaticVersion=true' -env: - global: - - TERM=dumb - - secure: "JVJ1w5ImgQkBwplEjlyTq0UaJQZncQEa35ggIbCffTon/FnWauy+HL9rzjBR6vTtSRlPxbKKxOdR0fGZQg5hQU10TEadwNBF5nLlFKK+mAz32z0RjgWAJhiFqjvFYdfGPuPZpAUZLIe7UxtcpQZPF2cAkc4Zw/r1HTI0GKS5L/H8FwJWlR1txu9O6GWfT58ntrD18LzrHQ9M5PC3gc4U/Af7kiPjeXQf06zliy7PMaMw8m1XLicjwzEHW2cNVXB0WrZwgcSrblnidRxPJj05k5clULu5VsetqMkLHCxuPbnjR0+1sZ6IX/3yhH5u/dm/vVtWikCqfpH2bLQw0vcYqisBPaJ/RlOa4r7ROL+G8LRo94LqwEVmZ+fbEFCqigP+Wv9GlLKL+LKTPDYWEnrBcHsE4EOwpLviEZRixHf/HtKvMfuwLyzyIAVyKjU0MrFDsOvDRorC1HIyg66CwjkKg5LJwW4IOTNTOldOpiGhB4PR4bcMUU8wuB5ts7eGGYCShthS4kOtuHlSuxfv6qKGIz4z5nD8/JPxVUU+P3RtrvoGYoeIqd6SGC40cvYupCzULJIYwrCKrRTpimrSwdrEEtLp91WjyrrDfznlGzoDOdCS5XaActcn4lh+ztkpYD/Qj2j31955kOkxq0/9TP5qi9qNvJAeFwkhW3VSmJVRbLw=" - - secure: "3UGbdSed51Ul024Wj1l2zojolyMxCzlTERSJjXrh5mdcNAHxImZTNURUuKaQsJghnirjB3q124woWyymvepE8XdSH0Y7DYkqV247vczXm1nHhbUpzVQ8xsDWTE6AXIyq1CzGzKFK8Ws0C+xXOZoBr2l9dy6oF4rBxTYTBlL8BvqGtcZw3d3eWNI9wQ+l6XGbQDw3Ex2iJaUr9NOI1kD4Q0gcUSX+Zgowe1xlaySrRLXg5iZVUCLNbitJEJ91Xd3oCksqfKBBe2b24M6edhCLNOMQ4u0MTEu28D6gltqojqmp1UHI6fJD8+P79h6PfpMPA+3vB3kTQ9vwG/5L5JoTe5JJPp+yUgbEBH5QzI7si6oVTFHb8oTN2/TMJV+N4QjILrasfreAYUA2kHXzxmxlodNsILNvl5OgF0ValwOvgPDZfkGytMnvLWow6gw8gSYtiTYJjNSMXnpa9kjCLMUyizDFRn4PpcMi2bfml/vR/qlVMnln3AwSukPGqQ4uZhRnIkzBJQdXoxoYTaVOcfsgF8dFWwOShin/RTygwwg5cjm0Nhn6RmkFtt81arLeVho/SeqOjEkvcCV6a7bhhmCZpZvYZu57dLfF/jPHYmW30X3Pg9e+7VsQ61xQWnlVV1g8KjM0qVnThoUVj8j62bXIo/P5scaKkxg9j5/Mdq6t02E=" cache: gradle: true directories: - "$HOME/.gradle/caches" - "$HOME/.gradle/wrapper" -after_script: -- git tag travis-build-$TRAVIS_BUILD_NUMBER -- git push --tags branches: only: - master + - devel