Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,23 @@ private Optimizely(@Nonnull ProjectConfig projectConfig,
return null;
}

LogEvent impressionEvent =
eventBuilder.createImpressionEvent(projectConfig, experiment, variation, userId, attributes);
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());
try {
eventHandler.dispatchEvent(impressionEvent);
} catch (Exception e) {
logger.error("Unexpected exception in event dispatcher", e);
}
if (experiment.isRunning()) {
LogEvent impressionEvent =
eventBuilder.createImpressionEvent(projectConfig, experiment, variation, userId, attributes);
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());
try {
eventHandler.dispatchEvent(impressionEvent);
} catch (Exception e) {
logger.error("Unexpected exception in event dispatcher", e);
}

notificationBroadcaster.broadcastExperimentActivated(experiment, userId, attributes, variation);
notificationBroadcaster.broadcastExperimentActivated(experiment, userId, attributes, variation);
} else {
logger.info("Experiment has \"Launched\" status so not dispatching event during activation.");
}

return variation;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public void cleanUserProfiles() {
for (Map.Entry<String,Map<String,String>> record : records.entrySet()) {
for (String experimentId : record.getValue().keySet()) {
Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId);
if (experiment == null || !experiment.isRunning()) {
if (experiment == null || !experiment.isActive()) {
userProfile.remove(record.getKey(), experimentId);
}
}
Expand Down
31 changes: 27 additions & 4 deletions core-api/src/main/java/com/optimizely/ab/config/Experiment.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,23 @@ public class Experiment implements IdKeyMapped {
private final Map<String, Variation> variationIdToVariationMap;
private final Map<String, String> userIdToVariationKeyMap;

// constant storing the status of a running experiment. Other possible statuses for an experiment
// include 'Not started', 'Paused', and 'Archived'
private static final String STATUS_RUNNING = "Running";
public enum ExperimentStatus {
RUNNING ("Running"),
LAUNCHED ("Launched"),
PAUSED ("Paused"),
NOT_STARTED ("Not started"),
ARCHIVED ("Archived");

private final String experimentStatus;

ExperimentStatus(String experimentStatus) {
this.experimentStatus = experimentStatus;
}

public String toString() {
return experimentStatus;
}
}

@JsonCreator
public Experiment(@JsonProperty("id") String id,
Expand Down Expand Up @@ -133,8 +147,17 @@ public String getGroupId() {
return groupId;
}

public boolean isActive() {
return status.equals(ExperimentStatus.RUNNING.toString()) ||
status.equals(ExperimentStatus.LAUNCHED.toString());
}

public boolean isRunning() {
return status.equals(STATUS_RUNNING);
return status.equals(ExperimentStatus.RUNNING.toString());
}

public boolean isLaunched() {
return status.equals(ExperimentStatus.LAUNCHED.toString());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,16 @@ private List<LayerState> createLayerStates(ProjectConfig projectConfig, Bucketer
for (Experiment experiment : allExperiments) {
if (experimentIds.contains(experiment.getId()) &&
ProjectValidationUtils.validatePreconditions(projectConfig, experiment, userId, attributes)) {
Variation bucketedVariation = bucketer.bucket(experiment, userId);
if (bucketedVariation != null) {
Decision decision = new Decision(bucketedVariation.getId(), false, experiment.getId());
layerStates.add(new LayerState(experiment.getLayerId(), decision, true));
if (experiment.isRunning()) {
Variation bucketedVariation = bucketer.bucket(experiment, userId);
if (bucketedVariation != null) {
Decision decision = new Decision(bucketedVariation.getId(), false, experiment.getId());
layerStates.add(new LayerState(experiment.getLayerId(), decision, true));
}
} else {
logger.info(
"Not tracking event \"{}\" for experiment \"{}\" because experiment has status \"Launched\".",
eventKey, experiment.getKey());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private ProjectValidationUtils() {}
*/
public static boolean validatePreconditions(ProjectConfig projectConfig, Experiment experiment, String userId,
Map<String, String> attributes) {
if (!experiment.isRunning()) {
if (!experiment.isActive()) {
logger.info("Experiment \"{}\" is not running.", experiment.getKey(), userId);
return false;
}
Expand Down
51 changes: 51 additions & 0 deletions core-api/src/test/java/com/optimizely/ab/OptimizelyTestV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,37 @@ public void activateDispatchEventThrowsException() throws Exception {
optimizely.activate(experiment.getKey(), "userId");
}

/**
* Verify that {@link Optimizely#activate(String, String)} doesn't dispatch an event for an experiment with a
* "Launched" status.
*/
@Test
public void activateLaunchedExperimentDoesNotDispatchEvent() throws Exception {
String datafile = noAudienceProjectConfigJsonV2();
ProjectConfig projectConfig = noAudienceProjectConfigV2();
Experiment launchedExperiment = projectConfig.getExperiments().get(2);

Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler)
.withBucketing(mockBucketer)
.withConfig(projectConfig)
.build();

Variation expectedVariation = launchedExperiment.getVariations().get(0);

when(mockBucketer.bucket(launchedExperiment, "userId"))
.thenReturn(launchedExperiment.getVariations().get(0));

logbackVerifier.expectMessage(Level.INFO,
"Experiment has \"Launched\" status so not dispatching event during activation.");
Variation variation = optimizely.activate(launchedExperiment.getKey(), "userId");

assertNotNull(variation);
assertThat(variation.getKey(), is(expectedVariation.getKey()));

// verify that we did NOT dispatch an event
verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class));
}

//======== track tests ========//

/**
Expand Down Expand Up @@ -953,6 +984,26 @@ public void trackDispatchEventThrowsException() throws Exception {
optimizely.track(eventType.getKey(), "userId");
}

/**
* Verify that {@link Optimizely#track(String, String)} doesn't make a dispatch for an event being used by a
* single experiment with a "Launched" status.
*/
@Test
public void trackLaunchedExperimentDoesNotDispatchEvent() throws Exception {
String datafile = noAudienceProjectConfigJsonV2();
ProjectConfig projectConfig = noAudienceProjectConfigV2();
EventType eventType = projectConfig.getEventTypes().get(3);

Optimizely optimizely = Optimizely.builder(datafile, mockEventHandler)
.withConfig(projectConfig)
.build();

optimizely.track(eventType.getKey(), "userId");

// verify that we did NOT dispatch an event
verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class));
}

//======== getVariation tests ========//

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,14 @@ private static ProjectConfig generateNoAudienceProjectConfigV2() {
Collections.<String, String>emptyMap(),
asList(new TrafficAllocation("278", 4500),
new TrafficAllocation("279", 9000)),
""),
new Experiment("119", "etag3", "Launched", "3",
Collections.<String>emptyList(),
asList(new Variation("280", "vtag5"),
new Variation("281", "vtag6")),
Collections.<String, String>emptyMap(),
asList(new TrafficAllocation("280", 5000),
new TrafficAllocation("281", 10000)),
"")
);

Expand All @@ -304,7 +312,8 @@ private static ProjectConfig generateNoAudienceProjectConfigV2() {
List<String> multipleExperimentIds = asList("118", "223");
List<EventType> 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", "launched_exp_event", singletonList("119")));

return new ProjectConfig("789", "1234", "2", "42", Collections.<Group>emptyList(), experiments, attributes,
events, Collections.<Audience>emptyList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package com.optimizely.ab.event.internal;

import ch.qos.logback.classic.Level;
import com.google.gson.Gson;

import com.optimizely.ab.bucketing.Bucketer;
Expand All @@ -33,8 +34,10 @@
import com.optimizely.ab.event.internal.payload.Feature;
import com.optimizely.ab.event.internal.payload.Impression;
import com.optimizely.ab.event.internal.payload.LayerState;
import com.optimizely.ab.internal.LogbackVerifier;
import com.optimizely.ab.internal.ProjectValidationUtils;

import org.junit.Rule;
import org.junit.Test;

import java.util.ArrayList;
Expand All @@ -58,6 +61,9 @@
*/
public class EventBuilderV2Test {

@Rule
public LogbackVerifier logbackVerifier = new LogbackVerifier();

private Gson gson = new Gson();
private EventBuilderV2 builder = new EventBuilderV2();

Expand Down Expand Up @@ -364,4 +370,32 @@ public void createConversionEventCustomClientEngineClientVersion() throws Except
assertThat(conversion.getClientEngine(), is(ClientEngine.ANDROID_SDK.getClientEngineValue()));
assertThat(conversion.getClientVersion(), is("0.0.0"));
}

/**
* Verify that {@link EventBuilderV2} doesn't add experiments with a "Launched" status to the bucket map
*/
@Test
public void createConversionEventForEventUsingLaunchedExperiment() throws Exception {
EventBuilderV2 builder = new EventBuilderV2();
ProjectConfig projectConfig = ProjectConfigTestUtils.noAudienceProjectConfigV2();
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));
}

logbackVerifier.expectMessage(Level.INFO,
"Not tracking event \"launched_exp_event\" for experiment \"etag3\" because experiment has status " +
"\"Launched\".");
LogEvent conversionEvent = builder.createConversionEvent(projectConfig, mockBucketAlgorithm, userId,
eventType.getId(), eventType.getKey(),
Collections.<String, String>emptyMap());

// only 1 experiment uses the event and it has a "Launched" status so the bucket map is empty and the returned
// event will be null
assertNull(conversionEvent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@
"entityId": "279",
"endOfRange": 9000
}]
},
{
"id": "119",
"key": "etag3",
"status": "Launched",
"layerId": "3",
"audienceIds": [],
"variations": [{
"id": "280",
"key": "vtag5"
}, {
"id": "281",
"key": "vtag6"
}],
"forcedVariations": {},
"trafficAllocation": [{
"entityId": "280",
"endOfRange": 5000
}, {
"entityId": "281",
"endOfRange": 10000
}]
}
],
"groups": [],
Expand Down Expand Up @@ -83,6 +105,13 @@
"118",
"223"
]
},
{
"id": "100",
"key": "launched_exp_event",
"experimentIds": [
"119"
]
}
]
}