From 5b7b533ecc1bdada4025c0db05de5db2841d359e Mon Sep 17 00:00:00 2001 From: Vignesh Raja Date: Fri, 23 Sep 2016 10:56:52 -0700 Subject: [PATCH 1/6] Give precedence to whitelisting over audience evaluation (#15) * Experiment status > Whitelisting > Audience evaluation --- .../java/com/optimizely/ab/Optimizely.java | 3 +- .../ab/internal/ProjectValidationUtils.java | 4 + .../com/optimizely/ab/OptimizelyTestV1.java | 79 +++++++++++++++++++ .../com/optimizely/ab/OptimizelyTestV2.java | 79 +++++++++++++++++++ .../ab/config/ProjectConfigTestUtils.java | 14 ++-- .../ab/event/internal/EventBuilderV1Test.java | 68 ++++++++++++++++ .../ab/event/internal/EventBuilderV2Test.java | 70 ++++++++++++++++ .../ProjectValidationUtilsTestV1.java | 68 ++++++++++++++++ .../ProjectValidationUtilsTestV2.java | 68 ++++++++++++++++ .../config/valid-project-config-v1.json | 15 +++- .../config/valid-project-config-v2.json | 15 +++- 11 files changed, 472 insertions(+), 11 deletions(-) create mode 100644 core-api/src/test/java/com/optimizely/ab/internal/ProjectValidationUtilsTestV1.java create mode 100644 core-api/src/test/java/com/optimizely/ab/internal/ProjectValidationUtilsTestV2.java 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 294a55038..50bdf03b9 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -246,7 +246,8 @@ private void track(@Nonnull String eventName, public @Nullable Variation getVariation(@Nonnull Experiment experiment, @Nonnull String userId) throws UnknownExperimentException { - return bucketer.bucket(experiment, userId); + + return getVariation(getProjectConfig(), experiment, Collections.emptyMap(), userId); } public @Nullable Variation getVariation(@Nonnull String experimentKey, diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ProjectValidationUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ProjectValidationUtils.java index f6a7e0a1a..69bc648eb 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ProjectValidationUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ProjectValidationUtils.java @@ -47,6 +47,10 @@ public static boolean validatePreconditions(ProjectConfig projectConfig, Experim return false; } + if (experiment.getUserIdToVariationKeyMap().containsKey(userId)) { + return true; + } + if (!isUserInExperiment(projectConfig, experiment, attributes)) { logger.info("User \"{}\" does not meet conditions to be in experiment \"{}\".", userId, experiment.getKey()); return false; 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 15b0f27c2..d41bee716 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV1.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV1.java @@ -608,6 +608,46 @@ public void activateForGroupExperimentWithNonMatchingAttributes() throws Excepti Collections.singletonMap("browser_type", "firefox"))); } + /** + * Verify that {@link Optimizely#activate(String, String, Map)} gives precedence to forced variation bucketing + * over audience evaluation. + */ + @Test + public void activateForcedVariationPrecedesAudienceEval() throws Exception { + String datafile = validConfigJsonV1(); + ProjectConfig projectConfig = validProjectConfigV1(); + Experiment experiment = projectConfig.getExperiments().get(0); + Variation expectedVariation = experiment.getVariations().get(0); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.INFO, "User \"testUser1\" is forced in variation \"vtag1\"."); + // no attributes provided for a experiment that has an audience + assertThat(optimizely.activate(experiment.getKey(), "testUser1"), is(expectedVariation)); + } + + /** + * Verify that {@link Optimizely#activate(String, String)} gives precedence to experiment status over forced + * variation bucketing. + */ + @Test + public void activateExperimentStatusPrecedesForcedVariation() throws Exception { + String datafile = validConfigJsonV1(); + ProjectConfig projectConfig = validProjectConfigV1(); + Experiment experiment = projectConfig.getExperiments().get(1); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.INFO, "Experiment \"etag2\" is not running."); + logbackVerifier.expectMessage(Level.INFO, "Not activating user \"testUser3\" for experiment \"etag2\"."); + // testUser3 has a corresponding forced variation, but experiment status should be checked first + assertNull(optimizely.activate(experiment.getKey(), "testUser3")); + } + //======== track tests ========// /** @@ -1120,6 +1160,45 @@ public void getVariationForGroupExperimentWithNonMatchingAttributes() throws Exc Collections.singletonMap("browser_type", "firefox"))); } + /** + * Verify that {@link Optimizely#getVariation(String, String, Map)} gives precedence to forced variation bucketing + * over audience evaluation. + */ + @Test + public void getVariationForcedVariationPrecedesAudienceEval() throws Exception { + String datafile = validConfigJsonV1(); + ProjectConfig projectConfig = validProjectConfigV1(); + Experiment experiment = projectConfig.getExperiments().get(0); + Variation expectedVariation = experiment.getVariations().get(0); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.INFO, "User \"testUser1\" is forced in variation \"vtag1\"."); + // no attributes provided for a experiment that has an audience + assertThat(optimizely.getVariation(experiment.getKey(), "testUser1"), is(expectedVariation)); + } + + /** + * Verify that {@link Optimizely#getVariation(String, String)} gives precedence to experiment status over forced + * variation bucketing. + */ + @Test + public void getVariationExperimentStatusPrecedesForcedVariation() throws Exception { + String datafile = validConfigJsonV1(); + ProjectConfig projectConfig = validProjectConfigV1(); + Experiment experiment = projectConfig.getExperiments().get(1); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.INFO, "Experiment \"etag2\" is not running."); + // testUser3 has a corresponding forced variation, but experiment status should be checked first + assertNull(optimizely.getVariation(experiment.getKey(), "testUser3")); + } + //======== Helper methods ========// private Experiment createUnknownExperiment() { 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 d5f154cc9..b7287706d 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java @@ -608,6 +608,46 @@ public void activateForGroupExperimentWithNonMatchingAttributes() throws Excepti Collections.singletonMap("browser_type", "firefox"))); } + /** + * Verify that {@link Optimizely#activate(String, String, Map)} gives precedence to forced variation bucketing + * over audience evaluation. + */ + @Test + public void activateForcedVariationPrecedesAudienceEval() throws Exception { + String datafile = validConfigJsonV2(); + ProjectConfig projectConfig = validProjectConfigV2(); + Experiment experiment = projectConfig.getExperiments().get(0); + Variation expectedVariation = experiment.getVariations().get(0); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.INFO, "User \"testUser1\" is forced in variation \"vtag1\"."); + // no attributes provided for a experiment that has an audience + assertThat(optimizely.activate(experiment.getKey(), "testUser1"), is(expectedVariation)); + } + + /** + * Verify that {@link Optimizely#activate(String, String)} gives precedence to experiment status over forced + * variation bucketing. + */ + @Test + public void activateExperimentStatusPrecedesForcedVariation() throws Exception { + String datafile = validConfigJsonV2(); + ProjectConfig projectConfig = validProjectConfigV2(); + Experiment experiment = projectConfig.getExperiments().get(1); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.INFO, "Experiment \"etag2\" is not running."); + logbackVerifier.expectMessage(Level.INFO, "Not activating user \"testUser3\" for experiment \"etag2\"."); + // testUser3 has a corresponding forced variation, but experiment status should be checked first + assertNull(optimizely.activate(experiment.getKey(), "testUser3")); + } + //======== track tests ========// /** @@ -1120,6 +1160,45 @@ public void getVariationForGroupExperimentWithNonMatchingAttributes() throws Exc Collections.singletonMap("browser_type", "firefox"))); } + /** + * Verify that {@link Optimizely#getVariation(String, String, Map)} gives precedence to forced variation bucketing + * over audience evaluation. + */ + @Test + public void getVariationForcedVariationPrecedesAudienceEval() throws Exception { + String datafile = validConfigJsonV2(); + ProjectConfig projectConfig = validProjectConfigV2(); + Experiment experiment = projectConfig.getExperiments().get(0); + Variation expectedVariation = experiment.getVariations().get(0); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.INFO, "User \"testUser1\" is forced in variation \"vtag1\"."); + // no attributes provided for a experiment that has an audience + assertThat(optimizely.getVariation(experiment.getKey(), "testUser1"), is(expectedVariation)); + } + + /** + * Verify that {@link Optimizely#getVariation(String, String)} gives precedence to experiment status over forced + * variation bucketing. + */ + @Test + public void getVariationExperimentStatusPrecedesForcedVariation() throws Exception { + String datafile = validConfigJsonV2(); + ProjectConfig projectConfig = validProjectConfigV2(); + Experiment experiment = projectConfig.getExperiments().get(1); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.INFO, "Experiment \"etag2\" is not running."); + // testUser3 has a corresponding forced variation, but experiment status should be checked first + assertNull(optimizely.getVariation(experiment.getKey(), "testUser3")); + } + //======== Helper methods ========// private Experiment createUnknownExperiment() { 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 5c3bb0d06..5357f7da2 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 @@ -54,7 +54,7 @@ private static ProjectConfig generateValidProjectConfigV1() { singletonList("100"), asList(new Variation("276", "vtag1"), new Variation("277", "vtag2")), - Collections.emptyMap(), + Collections.singletonMap("testUser1", "vtag1"), asList(new TrafficAllocation("276", 3500), new TrafficAllocation("277", 9000)), ""), @@ -62,7 +62,7 @@ private static ProjectConfig generateValidProjectConfigV1() { singletonList("100"), asList(new Variation("278", "vtag3"), new Variation("279", "vtag4")), - Collections.emptyMap(), + Collections.singletonMap("testUser3", "vtag3"), asList(new TrafficAllocation("278", 4500), new TrafficAllocation("279", 9000)), "") @@ -74,7 +74,8 @@ private static ProjectConfig generateValidProjectConfigV1() { List multipleExperimentIds = asList("118", "223"); List events = asList(new EventType("971", "clicked_cart", singleExperimentId), new EventType("098", "Total Revenue", singleExperimentId), - new EventType("099", "clicked_purchase", multipleExperimentIds)); + new EventType("099", "clicked_purchase", multipleExperimentIds), + new EventType("100", "no_running_experiments", singletonList("118"))); List userAttributes = new ArrayList(); userAttributes.add(new UserAttribute("browser_type", "custom_dimension", "firefox")); @@ -184,7 +185,7 @@ private static ProjectConfig generateValidProjectConfigV2() { singletonList("100"), asList(new Variation("276", "vtag1"), new Variation("277", "vtag2")), - Collections.emptyMap(), + Collections.singletonMap("testUser1", "vtag1"), asList(new TrafficAllocation("276", 3500), new TrafficAllocation("277", 9000)), ""), @@ -192,7 +193,7 @@ private static ProjectConfig generateValidProjectConfigV2() { singletonList("100"), asList(new Variation("278", "vtag3"), new Variation("279", "vtag4")), - Collections.emptyMap(), + Collections.singletonMap("testUser3", "vtag3"), asList(new TrafficAllocation("278", 4500), new TrafficAllocation("279", 9000)), "") @@ -204,7 +205,8 @@ private static ProjectConfig generateValidProjectConfigV2() { List multipleExperimentIds = asList("118", "223"); List events = asList(new EventType("971", "clicked_cart", singleExperimentId), new EventType("098", "Total Revenue", singleExperimentId), - new EventType("099", "clicked_purchase", multipleExperimentIds)); + new EventType("099", "clicked_purchase", multipleExperimentIds), + new EventType("100", "no_running_experiments", singletonList("118"))); List userAttributes = new ArrayList(); userAttributes.add(new UserAttribute("browser_type", "custom_dimension", "firefox")); diff --git a/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV1Test.java b/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV1Test.java index 08f33df94..88e40951d 100644 --- a/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV1Test.java +++ b/core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV1Test.java @@ -226,6 +226,74 @@ public void createImpressionParamsIgnoresUnknownAttributes() throws Exception { assertThat(impressionEvent.getRequestParams(), not(hasValue("blahValue"))); } + /** + * Verify that precedence is given to forced variation bucketing over audience evaluation when constructing the + * experiment bucket map. + */ + @Test + public void createConversionParamsForcedVariationBucketingPrecedesAudienceEval() { + EventBuilderV1 builder = new EventBuilderV1(); + + ProjectConfig projectConfig = ProjectConfigTestUtils.validProjectConfigV1(); + EventType eventType = projectConfig.getEventTypes().get(0); + String userId = "testUser1"; + + List experimentIds = projectConfig.getExperimentIdsForGoal(eventType.getKey()); + + Bucketer mockBucketAlgorithm = mock(Bucketer.class); + for (Experiment experiment : projectConfig.getExperiments()) { + when(mockBucketAlgorithm.bucket(experiment, userId)) + .thenReturn(experiment.getVariations().get(0)); + } + + // attributes are empty so user won't be in the audience for experiment using the event, but bucketing + // will still take place + LogEvent conversionEvent = builder.createConversionEvent(projectConfig, mockBucketAlgorithm, userId, + eventType.getId(), eventType.getKey(), + Collections.emptyMap()); + + for (Experiment experiment : projectConfig.getExperiments()) { + if (experimentIds.contains(experiment.getId()) && + ProjectValidationUtils.validatePreconditions(projectConfig, experiment, userId, + Collections.emptyMap())) { + verify(mockBucketAlgorithm).bucket(experiment, userId); + } else { + verify(mockBucketAlgorithm, never()).bucket(experiment, userId); + } + } + + assertThat(conversionEvent.getRequestParams().size(), is(9)); + } + + /** + * Verify that precedence is given to experiment status over forced variation bucketing when constructing the + * experiment bucket map. + */ + @Test + public void createConversionParamsExperimentStatusPrecedesForcedVariation() { + EventBuilderV1 builder = new EventBuilderV1(); + + ProjectConfig projectConfig = ProjectConfigTestUtils.validProjectConfigV1(); + EventType eventType = projectConfig.getEventTypes().get(3); + String userId = "userId"; + + Bucketer mockBucketAlgorithm = mock(Bucketer.class); + for (Experiment experiment : projectConfig.getExperiments()) { + when(mockBucketAlgorithm.bucket(experiment, userId)) + .thenReturn(experiment.getVariations().get(0)); + } + + LogEvent conversionEvent = builder.createConversionEvent(projectConfig, mockBucketAlgorithm, userId, + eventType.getId(), eventType.getKey(), + Collections.emptyMap()); + + for (Experiment experiment : projectConfig.getExperiments()) { + verify(mockBucketAlgorithm, never()).bucket(experiment, userId); + } + + assertNull(conversionEvent); + } + //======== Helper methods ========// private void verifyCommonRequestParams(ProjectConfig projectConfig, Map requestParams, 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 bf987e1f8..1e6a3add9 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 @@ -245,4 +245,74 @@ public void createConversionParamsUserNotInAudience() throws Exception { assertNull(conversionEvent); } + + /** + * Verify that precedence is given to forced variation bucketing over audience evaluation when constructing a + * conversion event. + */ + @Test + public void createConversionEventForcedVariationBucketingPrecedesAudienceEval() { + EventBuilderV2 builder = new EventBuilderV2(); + + ProjectConfig projectConfig = ProjectConfigTestUtils.validProjectConfigV2(); + EventType eventType = projectConfig.getEventTypes().get(0); + String userId = "testUser1"; + + List experimentIds = projectConfig.getExperimentIdsForGoal(eventType.getKey()); + + Bucketer mockBucketAlgorithm = mock(Bucketer.class); + for (Experiment experiment : projectConfig.getExperiments()) { + when(mockBucketAlgorithm.bucket(experiment, userId)) + .thenReturn(experiment.getVariations().get(0)); + } + + // attributes are empty so user won't be in the audience for experiment using the event, but bucketing + // will still take place + LogEvent conversionEvent = builder.createConversionEvent(projectConfig, mockBucketAlgorithm, userId, + eventType.getId(), eventType.getKey(), + Collections.emptyMap()); + + for (Experiment experiment : projectConfig.getExperiments()) { + if (experimentIds.contains(experiment.getId()) && + ProjectValidationUtils.validatePreconditions(projectConfig, experiment, userId, + Collections.emptyMap())) { + verify(mockBucketAlgorithm).bucket(experiment, userId); + } else { + verify(mockBucketAlgorithm, never()).bucket(experiment, userId); + } + } + + Conversion conversion = gson.fromJson(conversionEvent.getBody(), Conversion.class); + // 1 experiment uses the event + assertThat(conversion.getLayerStates().size(), is(1)); + } + + /** + * Verify that precedence is given to experiment status over forced variation bucketing when constructing a + * conversion event. + */ + @Test + public void createConversionEventExperimentStatusPrecedesForcedVariation() { + EventBuilderV2 builder = new EventBuilderV2(); + + ProjectConfig projectConfig = ProjectConfigTestUtils.validProjectConfigV2(); + EventType eventType = projectConfig.getEventTypes().get(3); + String userId = "userId"; + + Bucketer mockBucketAlgorithm = mock(Bucketer.class); + for (Experiment experiment : projectConfig.getExperiments()) { + when(mockBucketAlgorithm.bucket(experiment, userId)) + .thenReturn(experiment.getVariations().get(0)); + } + + LogEvent conversionEvent = builder.createConversionEvent(projectConfig, mockBucketAlgorithm, userId, + eventType.getId(), eventType.getKey(), + Collections.emptyMap()); + + for (Experiment experiment : projectConfig.getExperiments()) { + verify(mockBucketAlgorithm, never()).bucket(experiment, userId); + } + + assertNull(conversionEvent); + } } diff --git a/core-api/src/test/java/com/optimizely/ab/internal/ProjectValidationUtilsTestV1.java b/core-api/src/test/java/com/optimizely/ab/internal/ProjectValidationUtilsTestV1.java new file mode 100644 index 000000000..036c703a1 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/internal/ProjectValidationUtilsTestV1.java @@ -0,0 +1,68 @@ +/** + * + * 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.internal; + +import ch.qos.logback.classic.Level; + +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ProjectConfig; + +import org.junit.Rule; +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV1; +import static com.optimizely.ab.internal.ProjectValidationUtils.validatePreconditions; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ProjectValidationUtilsTestV1 { + + @Rule + public LogbackVerifier logbackVerifier = new LogbackVerifier(); + + /** + * Verify that {@link ProjectValidationUtils#validatePreconditions(ProjectConfig, Experiment, String, Map)} gives + * precedence to forced variation bucketing over audience evaluation. + */ + @Test + public void validatePreconditionsForcedVariationPrecedesAudienceEval() throws Exception { + ProjectConfig projectConfig = validProjectConfigV1(); + Experiment experiment = projectConfig.getExperiments().get(0); + + assertTrue( + validatePreconditions(projectConfig, experiment, "testUser1", Collections.emptyMap())); + } + + /** + * Verify that {@link ProjectValidationUtils#validatePreconditions(ProjectConfig, Experiment, String, Map)} gives + * precedence to experiment status over forced variation bucketing. + */ + @Test + public void validatePreconditionsExperimentStatusPrecedesForcedVariation() throws Exception { + ProjectConfig projectConfig = validProjectConfigV1(); + Experiment experiment = projectConfig.getExperiments().get(1); + + logbackVerifier.expectMessage(Level.INFO, "Experiment \"etag2\" is not running."); + // testUser3 has a corresponding forced variation, but experiment status should be checked first + assertFalse( + validatePreconditions(projectConfig, experiment, "testUser3", Collections.emptyMap())); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/internal/ProjectValidationUtilsTestV2.java b/core-api/src/test/java/com/optimizely/ab/internal/ProjectValidationUtilsTestV2.java new file mode 100644 index 000000000..e98806a11 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/internal/ProjectValidationUtilsTestV2.java @@ -0,0 +1,68 @@ +/** + * + * 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.internal; + +import ch.qos.logback.classic.Level; + +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ProjectConfig; + +import org.junit.Rule; +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV2; +import static com.optimizely.ab.internal.ProjectValidationUtils.validatePreconditions; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ProjectValidationUtilsTestV2 { + + @Rule + public LogbackVerifier logbackVerifier = new LogbackVerifier(); + + /** + * Verify that {@link ProjectValidationUtils#validatePreconditions(ProjectConfig, Experiment, String, Map)} gives + * precedence to forced variation bucketing over audience evaluation. + */ + @Test + public void validatePreconditionsForcedVariationPrecedesAudienceEval() throws Exception { + ProjectConfig projectConfig = validProjectConfigV2(); + Experiment experiment = projectConfig.getExperiments().get(0); + + assertTrue( + validatePreconditions(projectConfig, experiment, "testUser1", Collections.emptyMap())); + } + + /** + * Verify that {@link ProjectValidationUtils#validatePreconditions(ProjectConfig, Experiment, String, Map)} gives + * precedence to experiment status over forced variation bucketing. + */ + @Test + public void validatePreconditionsExperimentStatusPrecedesForcedVariation() throws Exception { + ProjectConfig projectConfig = validProjectConfigV2(); + Experiment experiment = projectConfig.getExperiments().get(1); + + logbackVerifier.expectMessage(Level.INFO, "Experiment \"etag2\" is not running."); + // testUser3 has a corresponding forced variation, but experiment status should be checked first + assertFalse( + validatePreconditions(projectConfig, experiment, "testUser3", Collections.emptyMap())); + } +} diff --git a/core-api/src/test/resources/config/valid-project-config-v1.json b/core-api/src/test/resources/config/valid-project-config-v1.json index 175279f25..4cd7fa947 100644 --- a/core-api/src/test/resources/config/valid-project-config-v1.json +++ b/core-api/src/test/resources/config/valid-project-config-v1.json @@ -19,7 +19,9 @@ "id": "277", "key": "vtag2" }], - "forcedVariations": {}, + "forcedVariations": { + "testUser1": "vtag1" + }, "trafficAllocation": [{ "entityId": "276", "endOfRange": 3500 @@ -42,7 +44,9 @@ "id": "279", "key": "vtag4" }], - "forcedVariations": {}, + "forcedVariations": { + "testUser3": "vtag3" + }, "trafficAllocation": [{ "entityId": "278", "endOfRange": 4500 @@ -183,6 +187,13 @@ "118", "223" ] + }, + { + "id": "100", + "key": "no_running_experiments", + "experimentIds": [ + "118" + ] } ] } \ No newline at end of file diff --git a/core-api/src/test/resources/config/valid-project-config-v2.json b/core-api/src/test/resources/config/valid-project-config-v2.json index 397750aae..e597e6493 100644 --- a/core-api/src/test/resources/config/valid-project-config-v2.json +++ b/core-api/src/test/resources/config/valid-project-config-v2.json @@ -20,7 +20,9 @@ "id": "277", "key": "vtag2" }], - "forcedVariations": {}, + "forcedVariations": { + "testUser1": "vtag1" + }, "trafficAllocation": [{ "entityId": "276", "endOfRange": 3500 @@ -44,7 +46,9 @@ "id": "279", "key": "vtag4" }], - "forcedVariations": {}, + "forcedVariations": { + "testUser3": "vtag3" + }, "trafficAllocation": [{ "entityId": "278", "endOfRange": 4500 @@ -187,6 +191,13 @@ "118", "223" ] + }, + { + "id": "100", + "key": "no_running_experiments", + "experimentIds": [ + "118" + ] } ] } \ No newline at end of file From 61df9d520faaeead660a4405df61fbd20835a40b Mon Sep 17 00:00:00 2001 From: Vignesh Raja Date: Tue, 27 Sep 2016 13:50:20 -0700 Subject: [PATCH 2/6] Add exception handling for datafile parsing and event dispatcher (#16) --- .../java/com/optimizely/ab/Optimizely.java | 18 +++++--- .../config/parser/ConfigParseException.java | 5 +-- .../ab/config/parser/GsonConfigParser.java | 2 +- .../ab/config/parser/JacksonConfigParser.java | 2 +- .../ab/config/parser/JsonConfigParser.java | 2 +- .../config/parser/JsonSimpleConfigParser.java | 2 +- .../com/optimizely/ab/event/EventHandler.java | 4 +- .../optimizely/ab/OptimizelyBuilderTest.java | 18 +++++++- .../com/optimizely/ab/OptimizelyTestV1.java | 45 ++++++++++++++++++- .../com/optimizely/ab/OptimizelyTestV2.java | 41 +++++++++++++++++ 10 files changed, 119 insertions(+), 20 deletions(-) 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 50bdf03b9..2f6f45968 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -24,6 +24,7 @@ import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.Variation; +import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.config.parser.DefaultConfigParser; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.error.NoOpErrorHandler; @@ -169,7 +170,11 @@ private Optimizely(@Nonnull ProjectConfig projectConfig, logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey()); logger.debug("Dispatching impression event to URL {} with params {} and payload \"{}\".", impressionEvent.getEndpointUrl(), impressionEvent.getRequestParams(), impressionEvent.getBody()); - eventHandler.dispatchEvent(impressionEvent); + try { + eventHandler.dispatchEvent(impressionEvent); + } catch (Exception e) { + logger.error("Unexpected exception in event dispatcher", e); + } return variation; } @@ -239,7 +244,11 @@ private void track(@Nonnull String eventName, logger.info("Tracking event \"{}\" for user \"{}\".", eventName, userId); logger.debug("Dispatching conversion event to URL {} with params {} and payload \"{}\".", conversionEvent.getEndpointUrl(), conversionEvent.getRequestParams(), conversionEvent.getBody()); - eventHandler.dispatchEvent(conversionEvent); + try { + eventHandler.dispatchEvent(conversionEvent); + } catch (Exception e) { + logger.error("Unexpected exception in event dispatcher", e); + } } //======== getVariation calls ========// @@ -297,8 +306,7 @@ private void track(@Nonnull String eventName, /** * @return a {@link ProjectConfig} instance given a json string */ - private static ProjectConfig getProjectConfig(String datafile) { - //TODO(vignesh): add validation logic here + private static ProjectConfig getProjectConfig(String datafile) throws ConfigParseException { return DefaultConfigParser.getInstance().parseProjectConfig(datafile); } @@ -467,7 +475,7 @@ protected Builder withConfig(ProjectConfig projectConfig) { return this; } - public Optimizely build() { + public Optimizely build() throws ConfigParseException { if (projectConfig == null) { projectConfig = Optimizely.getProjectConfig(datafile); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParseException.java b/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParseException.java index 9797726f5..ef51680ca 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParseException.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/ConfigParseException.java @@ -16,13 +16,10 @@ */ package com.optimizely.ab.config.parser; -import com.optimizely.ab.OptimizelyRuntimeException; - /** * Wrapper around all types of JSON parser exceptions. */ -public final class ConfigParseException extends OptimizelyRuntimeException { - +public final class ConfigParseException extends Exception { public ConfigParseException(String message) { super(message); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java index b58b13a37..fe7e4ffb0 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java @@ -44,7 +44,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse try { return gson.fromJson(json, ProjectConfig.class); } catch (JsonParseException e) { - throw new ConfigParseException("unable to parse project config: " + json, e); + throw new ConfigParseException("Unable to parse datafile: " + json, e); } } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java index 5f143c4a0..2c73eaa8f 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JacksonConfigParser.java @@ -40,7 +40,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse try { return mapper.readValue(json, ProjectConfig.class); } catch (IOException e) { - throw new ConfigParseException("unable to parse project config: " + json, e); + throw new ConfigParseException("Unable to parse datafile: " + json, e); } } } 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 d1a2e2f46..5840d5a1f 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 @@ -74,7 +74,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, audiences); } catch (JSONException e) { - throw new ConfigParseException("unable to parse project config: " + json, 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 dd775218e..b1b287143 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 @@ -76,7 +76,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse return new ProjectConfig(accountId, projectId, version, revision, groups, experiments, attributes, events, audiences); } catch (ParseException e) { - throw new ConfigParseException("unable to parse project config: " + json, e); + throw new ConfigParseException("Unable to parse datafile: " + json, e); } } diff --git a/core-api/src/main/java/com/optimizely/ab/event/EventHandler.java b/core-api/src/main/java/com/optimizely/ab/event/EventHandler.java index 81351ba6b..1a4d2a67c 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/EventHandler.java +++ b/core-api/src/main/java/com/optimizely/ab/event/EventHandler.java @@ -16,12 +16,10 @@ */ package com.optimizely.ab.event; -import java.util.Map; - /** * Implementations are responsible for dispatching event's to the Optimizely event end-point. */ public interface EventHandler { - void dispatchEvent(LogEvent logEvent); + void dispatchEvent(LogEvent logEvent) throws Exception; } 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 6eb53052c..c22c217f7 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyBuilderTest.java @@ -16,11 +16,15 @@ */ package com.optimizely.ab; +import ch.qos.logback.classic.Level; + import com.optimizely.ab.bucketing.UserExperimentRecord; import com.optimizely.ab.config.ProjectConfigTestUtils; +import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.error.NoOpErrorHandler; import com.optimizely.ab.event.EventHandler; +import com.optimizely.ab.internal.LogbackVerifier; import org.junit.Rule; import org.junit.Test; @@ -37,6 +41,7 @@ import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV2; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; @@ -52,6 +57,9 @@ public class OptimizelyBuilderTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); + @Rule + public LogbackVerifier logbackVerifier = new LogbackVerifier(); + @Mock private EventHandler mockEventHandler; @Mock private ErrorHandler mockErrorHandler; @@ -101,9 +109,15 @@ public void withDefaultErrorHandler() throws Exception { public void withUserExperimentRecord() throws Exception { UserExperimentRecord userExperimentRecord = mock(UserExperimentRecord.class); Optimizely optimizelyClient = Optimizely.builder(validConfigJsonV2(), mockEventHandler) - .withUserExperimentRecord(userExperimentRecord) - .build(); + .withUserExperimentRecord(userExperimentRecord) + .build(); assertThat(optimizelyClient.bucketer.getUserExperimentRecord(), is(userExperimentRecord)); } + + @Test + public void builderThrowsConfigParseExceptionForInvalidDatafile() throws Exception { + thrown.expect(ConfigParseException.class); + Optimizely.builder("{invalidDatafile}", mockEventHandler).build(); + } } 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 d41bee716..aef44d5cc 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV1.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV1.java @@ -68,6 +68,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -648,6 +649,26 @@ public void activateExperimentStatusPrecedesForcedVariation() throws Exception { assertNull(optimizely.activate(experiment.getKey(), "testUser3")); } + /** + * Verify that {@link Optimizely#activate(String, String)} handles exceptions thrown by + * {@link EventHandler#dispatchEvent(LogEvent)} gracefully. + */ + @Test + public void activateDispatchEventThrowsException() throws Exception { + String datafile = noAudienceProjectConfigJsonV1(); + ProjectConfig projectConfig = noAudienceProjectConfigV1(); + Experiment experiment = projectConfig.getExperiments().get(0); + + doThrow(new Exception("Test Exception")).when(mockEventHandler).dispatchEvent(any(LogEvent.class)); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.ERROR, "Unexpected exception in event dispatcher"); + optimizely.activate(experiment.getKey(), "userId"); + } + //======== track tests ========// /** @@ -882,8 +903,8 @@ public void trackEventWithRevenue() throws Exception { // verify that the event builder was called with the expected revenue verify(mockEventBuilder).createConversionEvent(eq(projectConfig), eq(mockBucketer), eq("userId"), eq(eventType.getId()), eq(eventType.getKey()), - eq(Collections.emptyMap()), - revenueCaptor.capture()); + eq(Collections.emptyMap()), + revenueCaptor.capture()); Long actualValue = revenueCaptor.getValue(); assertThat(actualValue, is(revenue)); @@ -912,6 +933,26 @@ public void trackEventWithNoValidExperiments() throws Exception { verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class)); } + /** + * Verify that {@link Optimizely#track(String, String)} handles exceptions thrown by + * {@link EventHandler#dispatchEvent(LogEvent)} gracefully. + */ + @Test + public void trackDispatchEventThrowsException() throws Exception { + String datafile = noAudienceProjectConfigJsonV1(); + ProjectConfig projectConfig = noAudienceProjectConfigV1(); + EventType eventType = projectConfig.getEventTypes().get(0); + + doThrow(new Exception("Test Exception")).when(mockEventHandler).dispatchEvent(any(LogEvent.class)); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.ERROR, "Unexpected exception in event dispatcher"); + optimizely.track(eventType.getKey(), "userId"); + } + //======== getVariation tests ========// /** 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 b7287706d..8ce293535 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java @@ -68,6 +68,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyMapOf; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -648,6 +649,26 @@ public void activateExperimentStatusPrecedesForcedVariation() throws Exception { assertNull(optimizely.activate(experiment.getKey(), "testUser3")); } + /** + * Verify that {@link Optimizely#activate(String, String)} handles exceptions thrown by + * {@link EventHandler#dispatchEvent(LogEvent)} gracefully. + */ + @Test + public void activateDispatchEventThrowsException() throws Exception { + String datafile = noAudienceProjectConfigJsonV2(); + ProjectConfig projectConfig = noAudienceProjectConfigV2(); + Experiment experiment = projectConfig.getExperiments().get(0); + + doThrow(new Exception("Test Exception")).when(mockEventHandler).dispatchEvent(any(LogEvent.class)); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.ERROR, "Unexpected exception in event dispatcher"); + optimizely.activate(experiment.getKey(), "userId"); + } + //======== track tests ========// /** @@ -912,6 +933,26 @@ public void trackEventWithNoValidExperiments() throws Exception { verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class)); } + /** + * Verify that {@link Optimizely#track(String, String)} handles exceptions thrown by + * {@link EventHandler#dispatchEvent(LogEvent)} gracefully. + */ + @Test + public void trackDispatchEventThrowsException() throws Exception { + String datafile = noAudienceProjectConfigJsonV2(); + ProjectConfig projectConfig = noAudienceProjectConfigV2(); + EventType eventType = projectConfig.getEventTypes().get(0); + + doThrow(new Exception("Test Exception")).when(mockEventHandler).dispatchEvent(any(LogEvent.class)); + + Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler) + .withConfig(projectConfig) + .build(); + + logbackVerifier.expectMessage(Level.ERROR, "Unexpected exception in event dispatcher"); + optimizely.track(eventType.getKey(), "userId"); + } + //======== getVariation tests ========// /** From a0ba051e62a5d8525a54830a9db236009c4ab922 Mon Sep 17 00:00:00 2001 From: Vignesh Raja Date: Tue, 27 Sep 2016 17:04:05 -0700 Subject: [PATCH 3/6] Add CLA to contributing section (#17) --- CONTRIBUTING.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52a026f02..48fd6c26f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,15 @@ # Contributing to the Optimizely Java SDK -We welcome contributions and feedback! Please read the [README](README.md) to set up your development environment, -then read the guidelines below for information on submitting your code. +We welcome contributions and feedback! All contributors must sign our [Contributor License Agreement (CLA)](https://docs.google.com/a/optimizely.com/forms/d/e/1FAIpQLSf9cbouWptIpMgukAKZZOIAhafvjFCV8hS00XJLWQnWDFtwtA/viewform) to be eligible to contribute. Please read the [README](README.md) to set up your development environment, then read the guidelines below for information on submitting your code. ## Development process -1. Create a branch off of `master`: `git checkout -b YOUR_NAME/branch_name`. +1. Create a branch off of `devel`: `git checkout -b YOUR_NAME/branch_name`. 2. Commit your changes. Make sure to add tests! 3. Run `./gradlew clean check` to make sure there are no possible bugs. 4. `git push` your changes to GitHub. -5. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `master`. -6. Open a pull request from `YOUR_NAME/branch_name` to `master`. +5. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `devel`. +6. Open a pull request from `YOUR_NAME/branch_name` to `devel`. 7. A repository maintainer will review your pull request and, if all goes well, merge it! ## Pull request acceptance criteria From 7ad5dc44da640720cad21cbe33322098997c62b6 Mon Sep 17 00:00:00 2001 From: Vignesh Raja Date: Mon, 3 Oct 2016 13:15:55 -0700 Subject: [PATCH 4/6] Fix benchmark tests (#19) --- .../src/jmh/java/com/optimizely/ab/OptimizelyBenchmark.java | 3 ++- .../jmh/java/com/optimizely/ab/OptimizelyBuilderBenchmark.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core-api/src/jmh/java/com/optimizely/ab/OptimizelyBenchmark.java b/core-api/src/jmh/java/com/optimizely/ab/OptimizelyBenchmark.java index 15c9a5ada..3638c70bf 100644 --- a/core-api/src/jmh/java/com/optimizely/ab/OptimizelyBenchmark.java +++ b/core-api/src/jmh/java/com/optimizely/ab/OptimizelyBenchmark.java @@ -17,6 +17,7 @@ package com.optimizely.ab; import com.optimizely.ab.config.Variation; +import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.event.NoopEventHandler; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -65,7 +66,7 @@ public class OptimizelyBenchmark { @Setup @SuppressFBWarnings(value="OBL_UNSATISFIED_OBLIGATION_EXCEPTION_EDGE", justification="stream is safely closed") - public void setup() throws IOException { + public void setup() throws IOException, ConfigParseException { Properties properties = new Properties(); InputStream propertiesStream = getClass().getResourceAsStream("/benchmark.properties"); properties.load(propertiesStream); diff --git a/core-api/src/jmh/java/com/optimizely/ab/OptimizelyBuilderBenchmark.java b/core-api/src/jmh/java/com/optimizely/ab/OptimizelyBuilderBenchmark.java index 98e825971..2240c5bed 100644 --- a/core-api/src/jmh/java/com/optimizely/ab/OptimizelyBuilderBenchmark.java +++ b/core-api/src/jmh/java/com/optimizely/ab/OptimizelyBuilderBenchmark.java @@ -16,6 +16,7 @@ */ package com.optimizely.ab; +import com.optimizely.ab.config.parser.ConfigParseException; import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.NoopEventHandler; @@ -68,7 +69,7 @@ public void setup() throws IOException { } @Benchmark - public Optimizely measureOptimizelyCreation() throws IOException { + public Optimizely measureOptimizelyCreation() throws IOException, ConfigParseException { return Optimizely.builder(datafile, eventHandler).build(); } } From badaf62916cc4a18790f0b2e1171d049470713e0 Mon Sep 17 00:00:00 2001 From: Vignesh Raja Date: Mon, 3 Oct 2016 14:57:56 -0700 Subject: [PATCH 5/6] Update the README (#20) --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0bf818122..7d0195b05 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Optimizely Java SDK [![Build Status](https://travis-ci.org/optimizely/java-sdk.svg?branch=master)](https://travis-ci.org/optimizely/java-sdk) [![Apache 2.0](https://img.shields.io/github/license/nebula-plugins/gradle-extra-configurations-plugin.svg)](http://www.apache.org/licenses/LICENSE-2.0) -This repository houses the Java SDK for Optimizely's server-side testing product, which is currently in private beta. +This repository houses the Java SDK for Optimizely's Full Stack product. ## Getting Started @@ -18,14 +18,19 @@ following in your `build.gradle` and substitute `VERSION` for the latest SDK ver ``` repositories { maven { + mavenCentral() url "http://optimizely.bintray.com/optimizely" } } dependencies { compile 'com.optimizely.ab:core-api:{VERSION}' - // optional event dispatcher implementation compile 'com.optimizely.ab:core-httpclient-impl:{VERSION}' + // The SDK integrates with multiple JSON parsers, here we use + // Jackson. + compile 'com.fasterxml.jackson.core:jackson-core:2.7.1' + compile 'com.fasterxml.jackson.core:jackson-annotations:2.7.1' + compile 'com.fasterxml.jackson.core:jackson-databind:2.7.1' } ``` @@ -40,9 +45,8 @@ The supplied `pom` files on Bintray define module dependencies. ### Using the SDK -See the Optimizely server-side testing [developer documentation](http://developers.optimizely.com/server/reference/index.html) to learn how to set -up your first custom project and use the SDK. **Please note that you must be a member of the private server-side testing beta to create custom -projects and use this SDK.** +See the Optimizely Full Stack [developer documentation](http://developers.optimizely.com/server/reference/index.html) to learn how to set +up your first Java project and use the SDK. ## Development From 6ed3710ad667a515a6c07ee9f62666d4d459fd82 Mon Sep 17 00:00:00 2001 From: Vignesh Raja Date: Mon, 3 Oct 2016 15:16:19 -0700 Subject: [PATCH 6/6] Update CHANGELOG and bump version (#21) --- CHANGELOG.md | 6 ++++++ gradle.properties | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70f746d6..8ffba54ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.0 + +- Introduce support for Full Stack projects in Optimizely X with no breaking changes from previous version +- Update whitelisting to take precedence over audience condition evaluation +- Introduce more graceful exception handling in instantiation and core methods + ## 0.1.71 - Add support for v2 backend endpoint and datafile diff --git a/gradle.properties b/gradle.properties index 0f87bf860..5e266e120 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Maven version -version = 0.1.71-SNAPSHOT +version = 1.0.0-SNAPSHOT # Artifact paths mavenS3Bucket = optimizely-maven