Skip to content

Commit df9a062

Browse files
author
Vignesh Raja
authored
Add live variables (#38)
* Add live variables parsing (#36) * Add live variable getters (#37)
1 parent b9c671a commit df9a062

24 files changed

+2935
-47
lines changed

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

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
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;
@@ -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,
@@ -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+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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.config;
18+
19+
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
21+
import com.fasterxml.jackson.annotation.JsonProperty;
22+
import com.fasterxml.jackson.annotation.JsonValue;
23+
import com.google.gson.annotations.SerializedName;
24+
25+
/**
26+
* Represents a live variable definition at the project level
27+
*/
28+
@JsonIgnoreProperties(ignoreUnknown = true)
29+
public class LiveVariable implements IdKeyMapped {
30+
31+
public enum VariableStatus {
32+
@SerializedName("active")
33+
ACTIVE ("active"),
34+
35+
@SerializedName("archived")
36+
ARCHIVED ("archived");
37+
38+
private final String variableStatus;
39+
40+
VariableStatus(String variableStatus) {
41+
this.variableStatus = variableStatus;
42+
}
43+
44+
@JsonValue
45+
public String getVariableStatus() {
46+
return variableStatus;
47+
}
48+
49+
public static VariableStatus fromString(String variableStatusString) {
50+
if (variableStatusString != null) {
51+
for (VariableStatus variableStatusEnum : VariableStatus.values()) {
52+
if (variableStatusString.equals(variableStatusEnum.getVariableStatus())) {
53+
return variableStatusEnum;
54+
}
55+
}
56+
}
57+
58+
return null;
59+
}
60+
}
61+
62+
public enum VariableType {
63+
@SerializedName("boolean")
64+
BOOLEAN ("boolean"),
65+
66+
@SerializedName("integer")
67+
INTEGER ("integer"),
68+
69+
@SerializedName("string")
70+
STRING ("string"),
71+
72+
@SerializedName("float")
73+
FLOAT ("float");
74+
75+
private final String variableType;
76+
77+
VariableType(String variableType) {
78+
this.variableType = variableType;
79+
}
80+
81+
@JsonValue
82+
public String getVariableType() {
83+
return variableType;
84+
}
85+
86+
public static VariableType fromString(String variableTypeString) {
87+
if (variableTypeString != null) {
88+
for (VariableType variableTypeEnum : VariableType.values()) {
89+
if (variableTypeString.equals(variableTypeEnum.getVariableType())) {
90+
return variableTypeEnum;
91+
}
92+
}
93+
}
94+
95+
return null;
96+
}
97+
}
98+
99+
private final String id;
100+
private final String key;
101+
private final String defaultValue;
102+
private final VariableType type;
103+
private final VariableStatus status;
104+
105+
@JsonCreator
106+
public LiveVariable(@JsonProperty("id") String id,
107+
@JsonProperty("key") String key,
108+
@JsonProperty("defaultValue") String defaultValue,
109+
@JsonProperty("status") VariableStatus status,
110+
@JsonProperty("type") VariableType type) {
111+
this.id = id;
112+
this.key = key;
113+
this.defaultValue = defaultValue;
114+
this.status = status;
115+
this.type = type;
116+
}
117+
118+
public VariableStatus getStatus() {
119+
return status;
120+
}
121+
122+
public String getId() {
123+
return id;
124+
}
125+
126+
public String getKey() {
127+
return key;
128+
}
129+
130+
public String getDefaultValue() {
131+
return defaultValue;
132+
}
133+
134+
public VariableType getType() {
135+
return type;
136+
}
137+
138+
@Override
139+
public String toString() {
140+
return "LiveVariable{" +
141+
"id='" + id + '\'' +
142+
", key='" + key + '\'' +
143+
", defaultValue='" + defaultValue + '\'' +
144+
", type=" + type +
145+
", status=" + status +
146+
'}';
147+
}
148+
}

0 commit comments

Comments
 (0)