Skip to content

Commit 13e3bb3

Browse files
author
Vignesh Raja
authored
Release version 1.1.0 (#40)
2 parents 128dc80 + 4cb751d commit 13e3bb3

31 files changed

+3025
-134
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.1.0
2+
3+
- Add support for live variables
4+
- Change `UserExperimentRecord` to `UserProfile`
5+
16
## 1.0.3
27

38
- Remove extraneous log message in `AsyncEventHandler`

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

Lines changed: 165 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
import com.optimizely.ab.annotations.VisibleForTesting;
2020
import com.optimizely.ab.bucketing.Bucketer;
21-
import com.optimizely.ab.bucketing.UserExperimentRecord;
21+
import com.optimizely.ab.bucketing.UserProfile;
2222
import com.optimizely.ab.config.Attribute;
2323
import com.optimizely.ab.config.EventType;
2424
import com.optimizely.ab.config.Experiment;
25+
import com.optimizely.ab.config.LiveVariable;
26+
import com.optimizely.ab.config.LiveVariableUsageInstance;
2527
import com.optimizely.ab.config.ProjectConfig;
2628
import com.optimizely.ab.config.Variation;
2729
import com.optimizely.ab.config.parser.ConfigParseException;
@@ -102,7 +104,7 @@ private Optimizely(@Nonnull ProjectConfig projectConfig,
102104

103105
// Do work here that should be done once per Optimizely lifecycle
104106
@VisibleForTesting void initialize() {
105-
bucketer.cleanUserExperimentRecords();
107+
bucketer.cleanUserProfiles();
106108
}
107109

108110
//======== activate calls ========//
@@ -253,12 +255,136 @@ private void track(@Nonnull String eventName,
253255
}
254256
}
255257

258+
//======== live variable getters ========//
259+
260+
public @Nullable String getVariableString(@Nonnull String variableKey,
261+
boolean activateExperiment,
262+
@Nonnull String userId) throws UnknownLiveVariableException {
263+
return getVariableString(variableKey, activateExperiment, userId, Collections.<String, String>emptyMap());
264+
}
265+
266+
public @Nullable String getVariableString(@Nonnull String variableKey,
267+
boolean activateExperiment,
268+
@Nonnull String userId,
269+
@Nonnull Map<String, String> attributes)
270+
throws UnknownLiveVariableException {
271+
272+
LiveVariable variable = getLiveVariableOrThrow(projectConfig, variableKey);
273+
if (variable == null) {
274+
return null;
275+
}
276+
277+
List<Experiment> experimentsUsingLiveVariable =
278+
projectConfig.getLiveVariableIdToExperimentsMapping().get(variable.getId());
279+
Map<String, Map<String, LiveVariableUsageInstance>> variationToLiveVariableUsageInstanceMapping =
280+
projectConfig.getVariationToLiveVariableUsageInstanceMapping();
281+
282+
if (experimentsUsingLiveVariable == null) {
283+
logger.warn("No experiment is using variable \"{}\".", variable.getKey());
284+
return variable.getDefaultValue();
285+
}
286+
287+
for (Experiment experiment : experimentsUsingLiveVariable) {
288+
Variation variation;
289+
if (activateExperiment) {
290+
variation = activate(experiment, userId, attributes);
291+
} else {
292+
variation = getVariation(experiment, userId, attributes);
293+
}
294+
295+
if (variation != null) {
296+
LiveVariableUsageInstance usageInstance =
297+
variationToLiveVariableUsageInstanceMapping.get(variation.getId()).get(variable.getId());
298+
return usageInstance.getValue();
299+
}
300+
}
301+
302+
return variable.getDefaultValue();
303+
}
304+
305+
public @Nullable Boolean getVariableBoolean(@Nonnull String variableKey,
306+
boolean activateExperiment,
307+
@Nonnull String userId) throws UnknownLiveVariableException {
308+
return getVariableBoolean(variableKey, activateExperiment, userId, Collections.<String, String>emptyMap());
309+
}
310+
311+
public @Nullable Boolean getVariableBoolean(@Nonnull String variableKey,
312+
boolean activateExperiment,
313+
@Nonnull String userId,
314+
@Nonnull Map<String, String> attributes)
315+
throws UnknownLiveVariableException {
316+
317+
String variableValueString = getVariableString(variableKey, activateExperiment, userId, attributes);
318+
if (variableValueString != null) {
319+
return Boolean.parseBoolean(variableValueString);
320+
}
321+
322+
return null;
323+
}
324+
325+
public @Nullable Integer getVariableInteger(@Nonnull String variableKey,
326+
boolean activateExperiment,
327+
@Nonnull String userId) throws UnknownLiveVariableException {
328+
return getVariableInteger(variableKey, activateExperiment, userId, Collections.<String, String>emptyMap());
329+
}
330+
331+
public @Nullable Integer getVariableInteger(@Nonnull String variableKey,
332+
boolean activateExperiment,
333+
@Nonnull String userId,
334+
@Nonnull Map<String, String> attributes)
335+
throws UnknownLiveVariableException {
336+
337+
String variableValueString = getVariableString(variableKey, activateExperiment, userId, attributes);
338+
if (variableValueString != null) {
339+
try {
340+
return Integer.parseInt(variableValueString);
341+
} catch (NumberFormatException e) {
342+
logger.error("Variable value \"{}\" for live variable \"{}\" is not an integer.", variableValueString,
343+
variableKey);
344+
}
345+
}
346+
347+
return null;
348+
}
349+
350+
public @Nullable Float getVariableFloat(@Nonnull String variableKey,
351+
boolean activateExperiment,
352+
@Nonnull String userId) throws UnknownLiveVariableException {
353+
return getVariableFloat(variableKey, activateExperiment, userId, Collections.<String, String>emptyMap());
354+
}
355+
356+
public @Nullable Float getVariableFloat(@Nonnull String variableKey,
357+
boolean activateExperiment,
358+
@Nonnull String userId,
359+
@Nonnull Map<String, String> attributes)
360+
throws UnknownLiveVariableException {
361+
362+
String variableValueString = getVariableString(variableKey, activateExperiment, userId, attributes);
363+
if (variableValueString != null) {
364+
try {
365+
return Float.parseFloat(variableValueString);
366+
} catch (NumberFormatException e) {
367+
logger.error("Variable value \"{}\" for live variable \"{}\" is not a float.", variableValueString,
368+
variableKey);
369+
}
370+
}
371+
372+
return null;
373+
}
374+
256375
//======== getVariation calls ========//
257376

258377
public @Nullable Variation getVariation(@Nonnull Experiment experiment,
259378
@Nonnull String userId) throws UnknownExperimentException {
260379

261-
return getVariation(getProjectConfig(), experiment, Collections.<String, String>emptyMap(), userId);
380+
return getVariation(experiment, userId, Collections.<String, String>emptyMap());
381+
}
382+
383+
public @Nullable Variation getVariation(@Nonnull Experiment experiment,
384+
@Nonnull String userId,
385+
@Nonnull Map<String, String> attributes) throws UnknownExperimentException {
386+
387+
return getVariation(getProjectConfig(), experiment, attributes, userId);
262388
}
263389

264390
public @Nullable Variation getVariation(@Nonnull String experimentKey,
@@ -372,6 +498,36 @@ private EventType getEventTypeOrThrow(ProjectConfig projectConfig, String eventN
372498
return eventType;
373499
}
374500

501+
/**
502+
* Helper method to retrieve the {@link LiveVariable} for the given variable key.
503+
* If {@link RaiseExceptionErrorHandler} is provided, either a live variable is returned, or an exception is
504+
* thrown.
505+
* If {@link NoOpErrorHandler} is used, either a live variable or {@code null} is returned.
506+
*
507+
* @param projectConfig the current project config
508+
* @param variableKey the key for the live variable being retrieved from the current project config
509+
* @return the live variable to retrieve for the given variable key
510+
*
511+
* @throws UnknownLiveVariableException if there are no event types in the current project config with the given
512+
* name
513+
*/
514+
private LiveVariable getLiveVariableOrThrow(ProjectConfig projectConfig, String variableKey)
515+
throws UnknownLiveVariableException {
516+
517+
LiveVariable liveVariable = projectConfig
518+
.getLiveVariableKeyMapping()
519+
.get(variableKey);
520+
521+
if (liveVariable == null) {
522+
String unknownLiveVariableKeyError =
523+
String.format("Live variable \"%s\" is not in the datafile.", variableKey);
524+
logger.error(unknownLiveVariableKeyError);
525+
errorHandler.handleError(new UnknownLiveVariableException(unknownLiveVariableKeyError));
526+
}
527+
528+
return liveVariable;
529+
}
530+
375531
/**
376532
* Helper method to verify that the given attributes map contains only keys that are present in the
377533
* {@link ProjectConfig}.
@@ -420,6 +576,7 @@ private boolean validateUserId(String userId) {
420576

421577
return true;
422578
}
579+
423580
//======== Builder ========//
424581

425582
public static Builder builder(@Nonnull String datafile,
@@ -439,7 +596,7 @@ public static class Builder {
439596

440597
private String datafile;
441598
private Bucketer bucketer;
442-
private UserExperimentRecord userExperimentRecord;
599+
private UserProfile userProfile;
443600
private ErrorHandler errorHandler;
444601
private EventHandler eventHandler;
445602
private EventBuilder eventBuilder;
@@ -458,8 +615,8 @@ public Builder withErrorHandler(ErrorHandler errorHandler) {
458615
return this;
459616
}
460617

461-
public Builder withUserExperimentRecord(UserExperimentRecord userExperimentRecord) {
462-
this.userExperimentRecord = userExperimentRecord;
618+
public Builder withUserProfile(UserProfile userProfile) {
619+
this.userProfile = userProfile;
463620
return this;
464621
}
465622

@@ -496,7 +653,7 @@ public Optimizely build() throws ConfigParseException {
496653

497654
// use the default bucketer and event builder, if no overrides were provided
498655
if (bucketer == null) {
499-
bucketer = new Bucketer(projectConfig, userExperimentRecord);
656+
bucketer = new Bucketer(projectConfig, userProfile);
500657
}
501658

502659
if (clientEngine == null) {
@@ -508,7 +665,7 @@ public Optimizely build() throws ConfigParseException {
508665
}
509666

510667
if (eventBuilder == null) {
511-
if (projectConfig.getVersion().equals(ProjectConfig.V1)) {
668+
if (projectConfig.getVersion().equals(ProjectConfig.Version.V1.toString())) {
512669
eventBuilder = new EventBuilderV1();
513670
} else {
514671
eventBuilder = new EventBuilderV2(clientEngine, clientVersion);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
*
3+
* Copyright 2016, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab;
18+
19+
import com.optimizely.ab.config.LiveVariable;
20+
import com.optimizely.ab.config.ProjectConfig;
21+
22+
/**
23+
* Exception thrown when attempting to use/refer to a {@link LiveVariable} that isn't present in the current
24+
* {@link ProjectConfig}.
25+
*/
26+
public class UnknownLiveVariableException extends OptimizelyRuntimeException {
27+
28+
public UnknownLiveVariableException(String message) {
29+
super(message);
30+
}
31+
32+
public UnknownLiveVariableException(String message, Throwable cause) {
33+
super(message, cause);
34+
}
35+
}

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

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public class Bucketer {
4747

4848
private final ProjectConfig projectConfig;
4949

50-
@Nullable private final UserExperimentRecord userExperimentRecord;
50+
@Nullable private final UserProfile userProfile;
5151

5252
private static final Logger logger = LoggerFactory.getLogger(Bucketer.class);
5353

@@ -63,9 +63,9 @@ public Bucketer(ProjectConfig projectConfig) {
6363
this(projectConfig, null);
6464
}
6565

66-
public Bucketer(ProjectConfig projectConfig, @Nullable UserExperimentRecord userExperimentRecord) {
66+
public Bucketer(ProjectConfig projectConfig, @Nullable UserProfile userProfile) {
6767
this.projectConfig = projectConfig;
68-
this.userExperimentRecord = userExperimentRecord;
68+
this.userProfile = userProfile;
6969
}
7070

7171
private String bucketToEntity(int bucketValue, List<TrafficAllocation> trafficAllocations) {
@@ -112,12 +112,12 @@ private Variation bucketToVariation(@Nonnull Experiment experiment,
112112
String experimentKey = experiment.getKey();
113113
String combinedBucketId = userId + experimentId;
114114

115-
// If a user experiment record instance is present then check it for a saved variation
116-
if (userExperimentRecord != null) {
117-
String variationKey = userExperimentRecord.lookup(userId, experimentKey);
115+
// If a user profile instance is present then check it for a saved variation
116+
if (userProfile != null) {
117+
String variationKey = userProfile.lookup(userId, experimentKey);
118118
if (variationKey != null) {
119119
logger.info("Returning previously activated variation \"{}\" of experiment \"{}\" "
120-
+ "for user \"{}\" from user experiment record.",
120+
+ "for user \"{}\" from user profile.",
121121
variationKey, experimentKey, userId);
122122
// A variation is stored for this combined bucket id
123123
return projectConfig
@@ -127,7 +127,7 @@ private Variation bucketToVariation(@Nonnull Experiment experiment,
127127
.get(variationKey);
128128
} else {
129129
logger.info("No previously activated variation of experiment \"{}\" "
130-
+ "for user \"{}\" found in user experiment record.",
130+
+ "for user \"{}\" found in user profile.",
131131
experimentKey, userId);
132132
}
133133
}
@@ -145,9 +145,9 @@ private Variation bucketToVariation(@Nonnull Experiment experiment,
145145
logger.info("User \"{}\" is in variation \"{}\" of experiment \"{}\".", userId, variationKey,
146146
experimentKey);
147147

148-
// If a user experiment record is present give it a variation to store
149-
if (userExperimentRecord != null) {
150-
boolean saved = userExperimentRecord.save(userId, experiment.getKey(), variationKey);
148+
// If a user profile is present give it a variation to store
149+
if (userProfile != null) {
150+
boolean saved = userProfile.save(userId, experiment.getKey(), variationKey);
151151
if (saved) {
152152
logger.info("Saved variation \"{}\" of experiment \"{}\" for user \"{}\".",
153153
variationKey, experimentKey, userId);
@@ -224,23 +224,23 @@ int generateBucketValue(int hashCode) {
224224
}
225225

226226
@Nullable
227-
public UserExperimentRecord getUserExperimentRecord() {
228-
return userExperimentRecord;
227+
public UserProfile getUserProfile() {
228+
return userProfile;
229229
}
230230

231231
/**
232-
* Gives implementations of {@link UserExperimentRecord} a chance to remove records
232+
* Gives implementations of {@link UserProfile} a chance to remove records
233233
* of experiments that are deleted or not running.
234234
*/
235-
public void cleanUserExperimentRecords() {
236-
if (userExperimentRecord != null) {
237-
Map<String, Map<String,String>> records = userExperimentRecord.getAllRecords();
235+
public void cleanUserProfiles() {
236+
if (userProfile != null) {
237+
Map<String, Map<String,String>> records = userProfile.getAllRecords();
238238
if (records != null) {
239239
for (Map.Entry<String,Map<String,String>> record : records.entrySet()) {
240240
for (String experimentKey : record.getValue().keySet()) {
241241
Experiment experiment = projectConfig.getExperimentKeyMapping().get(experimentKey);
242242
if (experiment == null || !experiment.isRunning()) {
243-
userExperimentRecord.remove(record.getKey(), experimentKey);
243+
userProfile.remove(record.getKey(), experimentKey);
244244
}
245245
}
246246
}

core-api/src/main/java/com/optimizely/ab/bucketing/UserExperimentRecord.java renamed to core-api/src/main/java/com/optimizely/ab/bucketing/UserProfile.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
* user experience after changing traffic allocations. Also, this interface gives users
2626
* a hook to keep track of activation history.
2727
*/
28-
public interface UserExperimentRecord {
28+
public interface UserProfile {
2929

3030
/**
3131
* Called when implementors should save an activation
@@ -47,7 +47,7 @@ public interface UserExperimentRecord {
4747
String lookup(String userId, String experimentKey);
4848

4949
/**
50-
* Called when user experiment record should be removed
50+
* Called when user profile should be removed
5151
*
5252
* Records should be removed when an experiment is not running or when an experiment has been
5353
* deleted.

0 commit comments

Comments
 (0)