Skip to content

Commit 4fc4c15

Browse files
author
Vignesh Raja
authored
Add support for "Launched" experiment status (#66)
1 parent 1b9a948 commit 4fc4c15

File tree

9 files changed

+179
-22
lines changed

9 files changed

+179
-22
lines changed

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

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -172,18 +172,23 @@ private Optimizely(@Nonnull ProjectConfig projectConfig,
172172
return null;
173173
}
174174

175-
LogEvent impressionEvent =
176-
eventBuilder.createImpressionEvent(projectConfig, experiment, variation, userId, attributes);
177-
logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey());
178-
logger.debug("Dispatching impression event to URL {} with params {} and payload \"{}\".",
179-
impressionEvent.getEndpointUrl(), impressionEvent.getRequestParams(), impressionEvent.getBody());
180-
try {
181-
eventHandler.dispatchEvent(impressionEvent);
182-
} catch (Exception e) {
183-
logger.error("Unexpected exception in event dispatcher", e);
184-
}
175+
if (experiment.isRunning()) {
176+
LogEvent impressionEvent =
177+
eventBuilder.createImpressionEvent(projectConfig, experiment, variation, userId, attributes);
178+
logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey());
179+
logger.debug(
180+
"Dispatching impression event to URL {} with params {} and payload \"{}\".",
181+
impressionEvent.getEndpointUrl(), impressionEvent.getRequestParams(), impressionEvent.getBody());
182+
try {
183+
eventHandler.dispatchEvent(impressionEvent);
184+
} catch (Exception e) {
185+
logger.error("Unexpected exception in event dispatcher", e);
186+
}
185187

186-
notificationBroadcaster.broadcastExperimentActivated(experiment, userId, attributes, variation);
188+
notificationBroadcaster.broadcastExperimentActivated(experiment, userId, attributes, variation);
189+
} else {
190+
logger.info("Experiment has \"Launched\" status so not dispatching event during activation.");
191+
}
187192

188193
return variation;
189194
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public void cleanUserProfiles() {
240240
for (Map.Entry<String,Map<String,String>> record : records.entrySet()) {
241241
for (String experimentId : record.getValue().keySet()) {
242242
Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId);
243-
if (experiment == null || !experiment.isRunning()) {
243+
if (experiment == null || !experiment.isActive()) {
244244
userProfile.remove(record.getKey(), experimentId);
245245
}
246246
}

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,23 @@ public class Experiment implements IdKeyMapped {
5151
private final Map<String, Variation> variationIdToVariationMap;
5252
private final Map<String, String> userIdToVariationKeyMap;
5353

54-
// constant storing the status of a running experiment. Other possible statuses for an experiment
55-
// include 'Not started', 'Paused', and 'Archived'
56-
private static final String STATUS_RUNNING = "Running";
54+
public enum ExperimentStatus {
55+
RUNNING ("Running"),
56+
LAUNCHED ("Launched"),
57+
PAUSED ("Paused"),
58+
NOT_STARTED ("Not started"),
59+
ARCHIVED ("Archived");
60+
61+
private final String experimentStatus;
62+
63+
ExperimentStatus(String experimentStatus) {
64+
this.experimentStatus = experimentStatus;
65+
}
66+
67+
public String toString() {
68+
return experimentStatus;
69+
}
70+
}
5771

5872
@JsonCreator
5973
public Experiment(@JsonProperty("id") String id,
@@ -133,8 +147,17 @@ public String getGroupId() {
133147
return groupId;
134148
}
135149

150+
public boolean isActive() {
151+
return status.equals(ExperimentStatus.RUNNING.toString()) ||
152+
status.equals(ExperimentStatus.LAUNCHED.toString());
153+
}
154+
136155
public boolean isRunning() {
137-
return status.equals(STATUS_RUNNING);
156+
return status.equals(ExperimentStatus.RUNNING.toString());
157+
}
158+
159+
public boolean isLaunched() {
160+
return status.equals(ExperimentStatus.LAUNCHED.toString());
138161
}
139162

140163
@Override

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,16 @@ private List<LayerState> createLayerStates(ProjectConfig projectConfig, Bucketer
199199
for (Experiment experiment : allExperiments) {
200200
if (experimentIds.contains(experiment.getId()) &&
201201
ProjectValidationUtils.validatePreconditions(projectConfig, experiment, userId, attributes)) {
202-
Variation bucketedVariation = bucketer.bucket(experiment, userId);
203-
if (bucketedVariation != null) {
204-
Decision decision = new Decision(bucketedVariation.getId(), false, experiment.getId());
205-
layerStates.add(new LayerState(experiment.getLayerId(), decision, true));
202+
if (experiment.isRunning()) {
203+
Variation bucketedVariation = bucketer.bucket(experiment, userId);
204+
if (bucketedVariation != null) {
205+
Decision decision = new Decision(bucketedVariation.getId(), false, experiment.getId());
206+
layerStates.add(new LayerState(experiment.getLayerId(), decision, true));
207+
}
208+
} else {
209+
logger.info(
210+
"Not tracking event \"{}\" for experiment \"{}\" because experiment has status \"Launched\".",
211+
eventKey, experiment.getKey());
206212
}
207213
}
208214
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ private ProjectValidationUtils() {}
4242
*/
4343
public static boolean validatePreconditions(ProjectConfig projectConfig, Experiment experiment, String userId,
4444
Map<String, String> attributes) {
45-
if (!experiment.isRunning()) {
45+
if (!experiment.isActive()) {
4646
logger.info("Experiment \"{}\" is not running.", experiment.getKey(), userId);
4747
return false;
4848
}

core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,37 @@ public void activateDispatchEventThrowsException() throws Exception {
669669
optimizely.activate(experiment.getKey(), "userId");
670670
}
671671

672+
/**
673+
* Verify that {@link Optimizely#activate(String, String)} doesn't dispatch an event for an experiment with a
674+
* "Launched" status.
675+
*/
676+
@Test
677+
public void activateLaunchedExperimentDoesNotDispatchEvent() throws Exception {
678+
String datafile = noAudienceProjectConfigJsonV2();
679+
ProjectConfig projectConfig = noAudienceProjectConfigV2();
680+
Experiment launchedExperiment = projectConfig.getExperiments().get(2);
681+
682+
Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler)
683+
.withBucketing(mockBucketer)
684+
.withConfig(projectConfig)
685+
.build();
686+
687+
Variation expectedVariation = launchedExperiment.getVariations().get(0);
688+
689+
when(mockBucketer.bucket(launchedExperiment, "userId"))
690+
.thenReturn(launchedExperiment.getVariations().get(0));
691+
692+
logbackVerifier.expectMessage(Level.INFO,
693+
"Experiment has \"Launched\" status so not dispatching event during activation.");
694+
Variation variation = optimizely.activate(launchedExperiment.getKey(), "userId");
695+
696+
assertNotNull(variation);
697+
assertThat(variation.getKey(), is(expectedVariation.getKey()));
698+
699+
// verify that we did NOT dispatch an event
700+
verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class));
701+
}
702+
672703
//======== track tests ========//
673704

674705
/**
@@ -953,6 +984,26 @@ public void trackDispatchEventThrowsException() throws Exception {
953984
optimizely.track(eventType.getKey(), "userId");
954985
}
955986

987+
/**
988+
* Verify that {@link Optimizely#track(String, String)} doesn't make a dispatch for an event being used by a
989+
* single experiment with a "Launched" status.
990+
*/
991+
@Test
992+
public void trackLaunchedExperimentDoesNotDispatchEvent() throws Exception {
993+
String datafile = noAudienceProjectConfigJsonV2();
994+
ProjectConfig projectConfig = noAudienceProjectConfigV2();
995+
EventType eventType = projectConfig.getEventTypes().get(3);
996+
997+
Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler)
998+
.withConfig(projectConfig)
999+
.build();
1000+
1001+
optimizely.track(eventType.getKey(), "userId");
1002+
1003+
// verify that we did NOT dispatch an event
1004+
verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class));
1005+
}
1006+
9561007
//======== getVariation tests ========//
9571008

9581009
/**

core-api/src/test/java/com/optimizely/ab/config/ProjectConfigTestUtils.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,14 @@ private static ProjectConfig generateNoAudienceProjectConfigV2() {
295295
Collections.<String, String>emptyMap(),
296296
asList(new TrafficAllocation("278", 4500),
297297
new TrafficAllocation("279", 9000)),
298+
""),
299+
new Experiment("119", "etag3", "Launched", "3",
300+
Collections.<String>emptyList(),
301+
asList(new Variation("280", "vtag5"),
302+
new Variation("281", "vtag6")),
303+
Collections.<String, String>emptyMap(),
304+
asList(new TrafficAllocation("280", 5000),
305+
new TrafficAllocation("281", 10000)),
298306
"")
299307
);
300308

@@ -304,7 +312,8 @@ private static ProjectConfig generateNoAudienceProjectConfigV2() {
304312
List<String> multipleExperimentIds = asList("118", "223");
305313
List<EventType> events = asList(new EventType("971", "clicked_cart", singleExperimentId),
306314
new EventType("098", "Total Revenue", singleExperimentId),
307-
new EventType("099", "clicked_purchase", multipleExperimentIds));
315+
new EventType("099", "clicked_purchase", multipleExperimentIds),
316+
new EventType("100", "launched_exp_event", singletonList("119")));
308317

309318
return new ProjectConfig("789", "1234", "2", "42", Collections.<Group>emptyList(), experiments, attributes,
310319
events, Collections.<Audience>emptyList());

core-api/src/test/java/com/optimizely/ab/event/internal/EventBuilderV2Test.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package com.optimizely.ab.event.internal;
1818

19+
import ch.qos.logback.classic.Level;
1920
import com.google.gson.Gson;
2021

2122
import com.optimizely.ab.bucketing.Bucketer;
@@ -33,8 +34,10 @@
3334
import com.optimizely.ab.event.internal.payload.Feature;
3435
import com.optimizely.ab.event.internal.payload.Impression;
3536
import com.optimizely.ab.event.internal.payload.LayerState;
37+
import com.optimizely.ab.internal.LogbackVerifier;
3638
import com.optimizely.ab.internal.ProjectValidationUtils;
3739

40+
import org.junit.Rule;
3841
import org.junit.Test;
3942

4043
import java.util.ArrayList;
@@ -58,6 +61,9 @@
5861
*/
5962
public class EventBuilderV2Test {
6063

64+
@Rule
65+
public LogbackVerifier logbackVerifier = new LogbackVerifier();
66+
6167
private Gson gson = new Gson();
6268
private EventBuilderV2 builder = new EventBuilderV2();
6369

@@ -364,4 +370,32 @@ public void createConversionEventCustomClientEngineClientVersion() throws Except
364370
assertThat(conversion.getClientEngine(), is(ClientEngine.ANDROID_SDK.getClientEngineValue()));
365371
assertThat(conversion.getClientVersion(), is("0.0.0"));
366372
}
373+
374+
/**
375+
* Verify that {@link EventBuilderV2} doesn't add experiments with a "Launched" status to the bucket map
376+
*/
377+
@Test
378+
public void createConversionEventForEventUsingLaunchedExperiment() throws Exception {
379+
EventBuilderV2 builder = new EventBuilderV2();
380+
ProjectConfig projectConfig = ProjectConfigTestUtils.noAudienceProjectConfigV2();
381+
EventType eventType = projectConfig.getEventTypes().get(3);
382+
String userId = "userId";
383+
384+
Bucketer mockBucketAlgorithm = mock(Bucketer.class);
385+
for (Experiment experiment : projectConfig.getExperiments()) {
386+
when(mockBucketAlgorithm.bucket(experiment, userId))
387+
.thenReturn(experiment.getVariations().get(0));
388+
}
389+
390+
logbackVerifier.expectMessage(Level.INFO,
391+
"Not tracking event \"launched_exp_event\" for experiment \"etag3\" because experiment has status " +
392+
"\"Launched\".");
393+
LogEvent conversionEvent = builder.createConversionEvent(projectConfig, mockBucketAlgorithm, userId,
394+
eventType.getId(), eventType.getKey(),
395+
Collections.<String, String>emptyMap());
396+
397+
// only 1 experiment uses the event and it has a "Launched" status so the bucket map is empty and the returned
398+
// event will be null
399+
assertNull(conversionEvent);
400+
}
367401
}

core-api/src/test/resources/config/no-audience-project-config-v2.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,28 @@
5151
"entityId": "279",
5252
"endOfRange": 9000
5353
}]
54+
},
55+
{
56+
"id": "119",
57+
"key": "etag3",
58+
"status": "Launched",
59+
"layerId": "3",
60+
"audienceIds": [],
61+
"variations": [{
62+
"id": "280",
63+
"key": "vtag5"
64+
}, {
65+
"id": "281",
66+
"key": "vtag6"
67+
}],
68+
"forcedVariations": {},
69+
"trafficAllocation": [{
70+
"entityId": "280",
71+
"endOfRange": 5000
72+
}, {
73+
"entityId": "281",
74+
"endOfRange": 10000
75+
}]
5476
}
5577
],
5678
"groups": [],
@@ -83,6 +105,13 @@
83105
"118",
84106
"223"
85107
]
108+
},
109+
{
110+
"id": "100",
111+
"key": "launched_exp_event",
112+
"experimentIds": [
113+
"119"
114+
]
86115
}
87116
]
88117
}

0 commit comments

Comments
 (0)