Skip to content

Commit 9acfab2

Browse files
authored
internal feature variable accessor support (#131)
Internal Methods * Bucketing method for getting the variation a user is bucketed into for a feature * Get the value of a feature variable of a specific type with type checking Unit test improvements * add test to make sure null is returned when getVariationForFeature is called with a feature without attached experiments * add test to make sure getVariationForFeature returns null when user is not bucketed into any experiments * add test data for a new feature flag with mutex group stuff * add test for getVariationForFeature returns the variation a user is bucketed into when there are multiple experiments * update unit tests to test the getFeatureValueForVariableType method instead of the string method. test all cases
1 parent 40ead9a commit 9acfab2

File tree

7 files changed

+633
-19
lines changed

7 files changed

+633
-19
lines changed

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

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
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.FeatureFlag;
26+
import com.optimizely.ab.config.LiveVariable;
27+
import com.optimizely.ab.config.LiveVariableUsageInstance;
2528
import com.optimizely.ab.config.ProjectConfig;
2629
import com.optimizely.ab.config.Variation;
2730
import com.optimizely.ab.config.parser.ConfigParseException;
@@ -433,7 +436,57 @@ public void track(@Nonnull String eventName,
433436
@Nonnull String variableKey,
434437
@Nonnull String userId,
435438
@Nonnull Map<String, String> attributes) {
436-
return null;
439+
return getFeatureVariableValueForType(
440+
featureKey,
441+
variableKey,
442+
userId,
443+
attributes,
444+
LiveVariable.VariableType.STRING);
445+
}
446+
447+
@VisibleForTesting
448+
String getFeatureVariableValueForType(@Nonnull String featureKey,
449+
@Nonnull String variableKey,
450+
@Nonnull String userId,
451+
@Nonnull Map<String, String> attributes,
452+
@Nonnull LiveVariable.VariableType variableType) {
453+
FeatureFlag featureFlag = projectConfig.getFeatureKeyMapping().get(featureKey);
454+
if (featureFlag == null) {
455+
logger.info("No feature flag was found for key \"" + featureKey + "\".");
456+
return null;
457+
}
458+
459+
LiveVariable variable = featureFlag.getVariableKeyToLiveVariableMap().get(variableKey);
460+
if (variable == null) {
461+
logger.info("No feature variable was found for key \"" + variableKey + "\" in feature flag \"" +
462+
featureKey + "\".");
463+
return null;
464+
}
465+
else if (!variable.getType().equals(variableType)) {
466+
logger.info("The feature variable \"" + variableKey +
467+
"\" is actually of type \"" + variable.getType().toString() +
468+
"\" type. You tried to access it as type \"" + variableType.toString() +
469+
"\". Please use the appropriate feature variable accessor.");
470+
return null;
471+
}
472+
473+
String variableValue = variable.getDefaultValue();
474+
475+
Variation variation = decisionService.getVariationForFeature(featureFlag, userId, attributes);
476+
477+
if (variation != null) {
478+
LiveVariableUsageInstance liveVariableUsageInstance =
479+
variation.getVariableIdToLiveVariableUsageInstanceMap().get(variable.getId());
480+
variableValue = liveVariableUsageInstance.getValue();
481+
}
482+
else {
483+
logger.info("User \"" + userId +
484+
"\" was not bucketed into any variation for feature flag \"" + featureKey +
485+
"\". The default value is being returned."
486+
);
487+
}
488+
489+
return variableValue;
437490
}
438491

439492
//======== getVariation calls ========//

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.optimizely.ab.OptimizelyRuntimeException;
2020
import com.optimizely.ab.config.Experiment;
21+
import com.optimizely.ab.config.FeatureFlag;
2122
import com.optimizely.ab.config.ProjectConfig;
2223
import com.optimizely.ab.config.Variation;
2324
import com.optimizely.ab.error.ErrorHandler;
@@ -134,6 +135,33 @@ public DecisionService(@Nonnull Bucketer bucketer,
134135
return null;
135136
}
136137

138+
/**
139+
* Get the variation the user is bucketed into for the FeatureFlag
140+
* @param featureFlag The feature flag the user wants to access.
141+
* @param userId User Identifier
142+
* @param filteredAttributes A map of filtered attributes.
143+
* @return null if the user is not bucketed into any variation
144+
* {@link Variation} the user is bucketed into if the user is successfully bucketed.
145+
*/
146+
public @Nullable Variation getVariationForFeature(@Nonnull FeatureFlag featureFlag,
147+
@Nonnull String userId,
148+
@Nonnull Map<String, String> filteredAttributes) {
149+
if (!featureFlag.getExperimentIds().isEmpty()) {
150+
for (String experimentId : featureFlag.getExperimentIds()) {
151+
Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId);
152+
Variation variation = this.getVariation(experiment, userId, filteredAttributes);
153+
if (variation != null) {
154+
return variation;
155+
}
156+
}
157+
}
158+
else {
159+
logger.info("The feature flag \"" + featureFlag.getKey() + "\" is not used in any experiments.");
160+
}
161+
162+
return null;
163+
}
164+
137165
/**
138166
* Get the variation the user has been whitelisted into.
139167
* @param experiment {@link Experiment} in which user is to be bucketed.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ public Map<String, Map<String, LiveVariableUsageInstance>> getVariationToLiveVar
286286
return variationToLiveVariableUsageInstanceMapping;
287287
}
288288

289+
public Map<String, FeatureFlag> getFeatureKeyMapping() {
290+
return featureKeyMapping;
291+
}
292+
289293
@Override
290294
public String toString() {
291295
return "ProjectConfig{" +

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

Lines changed: 230 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import com.optimizely.ab.config.Attribute;
2424
import com.optimizely.ab.config.EventType;
2525
import com.optimizely.ab.config.Experiment;
26+
import com.optimizely.ab.config.LiveVariable;
2627
import com.optimizely.ab.config.LiveVariableUsageInstance;
2728
import com.optimizely.ab.config.ProjectConfig;
2829
import com.optimizely.ab.config.TrafficAllocation;
2930
import com.optimizely.ab.config.Variation;
31+
import com.optimizely.ab.config.parser.ConfigParseException;
3032
import com.optimizely.ab.error.ErrorHandler;
3133
import com.optimizely.ab.error.NoOpErrorHandler;
3234
import com.optimizely.ab.error.RaiseExceptionErrorHandler;
@@ -74,8 +76,16 @@
7476
import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_LAUNCHED_EXPERIMENT_KEY;
7577
import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY;
7678
import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_PAUSED_EXPERIMENT_KEY;
79+
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_MULTI_VARIATE_FEATURE;
80+
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_MULTI_VARIATE_FEATURE_KEY;
81+
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_SINGLE_VARIABLE_STRING_KEY;
7782
import static com.optimizely.ab.config.ValidProjectConfigV4.MULTIVARIATE_EXPERIMENT_FORCED_VARIATION_USER_ID_GRED;
7883
import static com.optimizely.ab.config.ValidProjectConfigV4.PAUSED_EXPERIMENT_FORCED_VARIATION_USER_ID_CONTROL;
84+
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_FIRST_LETTER_DEFAULT_VALUE;
85+
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_FIRST_LETTER_KEY;
86+
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_STRING_VARIABLE_DEFAULT_VALUE;
87+
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_STRING_VARIABLE_KEY;
88+
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_MULTIVARIATE_EXPERIMENT_GRED;
7989
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_MULTIVARIATE_EXPERIMENT_GRED_KEY;
8090
import static com.optimizely.ab.event.LogEvent.RequestMethod;
8191
import static com.optimizely.ab.event.internal.EventBuilderV2Test.createExperimentVariationMap;
@@ -84,17 +94,17 @@
8494
import static org.hamcrest.CoreMatchers.is;
8595
import static org.hamcrest.CoreMatchers.not;
8696
import static org.hamcrest.MatcherAssert.assertThat;
87-
import static org.hamcrest.Matchers.array;
8897
import static org.hamcrest.Matchers.hasEntry;
8998
import static org.hamcrest.Matchers.hasKey;
9099
import static org.junit.Assert.assertEquals;
91100
import static org.junit.Assert.assertNotNull;
92101
import static org.junit.Assert.assertNull;
102+
import static org.junit.Assume.assumeTrue;
93103
import static org.mockito.Matchers.any;
94-
import static org.mockito.Matchers.anyMap;
95104
import static org.mockito.Matchers.anyMapOf;
96105
import static org.mockito.Matchers.anyString;
97106
import static org.mockito.Matchers.eq;
107+
import static org.mockito.Mockito.doReturn;
98108
import static org.mockito.Mockito.doThrow;
99109
import static org.mockito.Mockito.mock;
100110
import static org.mockito.Mockito.never;
@@ -2143,6 +2153,224 @@ public void clearNotificationListeners() throws Exception {
21432153
.onEventTracked(eventKey, genericUserId, attributes, null, logEventToDispatch);
21442154
}
21452155

2156+
//======== Feature Accessor Tests ========//
2157+
2158+
/**
2159+
* Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)}
2160+
* returns null and logs a message
2161+
* when it is called with a feature key that has no corresponding feature in the datafile.
2162+
* @throws ConfigParseException
2163+
*/
2164+
@Test
2165+
public void getFeatureVariableValueForTypeReturnsNullWhenFeatureNotFound() throws ConfigParseException {
2166+
2167+
String invalidFeatureKey = "nonexistent feature key";
2168+
String invalidVariableKey = "nonexistent variable key";
2169+
Map<String, String> attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE);
2170+
2171+
Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
2172+
.withConfig(validProjectConfig)
2173+
.withDecisionService(mockDecisionService)
2174+
.build();
2175+
2176+
String value = optimizely.getFeatureVariableValueForType(
2177+
invalidFeatureKey,
2178+
invalidVariableKey,
2179+
genericUserId,
2180+
Collections.<String, String>emptyMap(),
2181+
LiveVariable.VariableType.STRING);
2182+
assertNull(value);
2183+
2184+
value = optimizely.getFeatureVariableString(invalidFeatureKey, invalidVariableKey, genericUserId, attributes);
2185+
assertNull(value);
2186+
2187+
logbackVerifier.expectMessage(Level.INFO,
2188+
"No feature flag was found for key \"" + invalidFeatureKey + "\".",
2189+
times(2));
2190+
2191+
verify(mockDecisionService, never()).getVariation(
2192+
any(Experiment.class),
2193+
anyString(),
2194+
anyMapOf(String.class, String.class));
2195+
}
2196+
2197+
/**
2198+
* Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)}
2199+
* returns null and logs a message
2200+
* when the feature key is valid, but no variable could be found for the variable key in the feature.
2201+
* @throws ConfigParseException
2202+
*/
2203+
@Test
2204+
public void getFeatureVariableValueForTypeReturnsNullWhenVariableNotFoundInValidFeature() throws ConfigParseException {
2205+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
2206+
2207+
String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY;
2208+
String invalidVariableKey = "nonexistent variable key";
2209+
2210+
Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
2211+
.withConfig(validProjectConfig)
2212+
.withDecisionService(mockDecisionService)
2213+
.build();
2214+
2215+
String value = optimizely.getFeatureVariableValueForType(
2216+
validFeatureKey,
2217+
invalidVariableKey,
2218+
genericUserId,
2219+
Collections.<String, String>emptyMap(),
2220+
LiveVariable.VariableType.STRING);
2221+
assertNull(value);
2222+
2223+
logbackVerifier.expectMessage(Level.INFO,
2224+
"No feature variable was found for key \"" + invalidVariableKey + "\" in feature flag \"" +
2225+
validFeatureKey + "\".");
2226+
2227+
verify(mockDecisionService, never()).getVariation(
2228+
any(Experiment.class),
2229+
anyString(),
2230+
anyMapOf(String.class, String.class)
2231+
);
2232+
}
2233+
2234+
/**
2235+
* Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)}
2236+
* returns null when the variable's type does not match the type with which it was attempted to be accessed.
2237+
* @throws ConfigParseException
2238+
*/
2239+
@Test
2240+
public void getFeatureVariableValueReturnsNullWhenVariableTypeDoesNotMatch() throws ConfigParseException {
2241+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
2242+
2243+
String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY;
2244+
String validVariableKey = VARIABLE_FIRST_LETTER_KEY;
2245+
2246+
Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
2247+
.withConfig(validProjectConfig)
2248+
.withDecisionService(mockDecisionService)
2249+
.build();
2250+
2251+
String value = optimizely.getFeatureVariableValueForType(
2252+
validFeatureKey,
2253+
validVariableKey,
2254+
genericUserId,
2255+
Collections.<String, String>emptyMap(),
2256+
LiveVariable.VariableType.INTEGER
2257+
);
2258+
assertNull(value);
2259+
2260+
logbackVerifier.expectMessage(
2261+
Level.INFO,
2262+
"The feature variable \"" + validVariableKey +
2263+
"\" is actually of type \"" + LiveVariable.VariableType.STRING.toString() +
2264+
"\" type. You tried to access it as type \"" + LiveVariable.VariableType.INTEGER.toString() +
2265+
"\". Please use the appropriate feature variable accessor."
2266+
);
2267+
}
2268+
2269+
/**
2270+
* Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)}
2271+
* returns the String default value of a live variable
2272+
* when the feature is not attached to an experiment.
2273+
* @throws ConfigParseException
2274+
*/
2275+
@Test
2276+
public void getFeatureVariableValueForTypeReturnsDefaultValueWhenFeatureIsNotAttached() throws ConfigParseException {
2277+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
2278+
2279+
String validFeatureKey = FEATURE_SINGLE_VARIABLE_STRING_KEY;
2280+
String validVariableKey = VARIABLE_STRING_VARIABLE_KEY;
2281+
String defaultValue = VARIABLE_STRING_VARIABLE_DEFAULT_VALUE;
2282+
Map<String, String> attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE);
2283+
2284+
Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
2285+
.withConfig(validProjectConfig)
2286+
.build();
2287+
2288+
String value = optimizely.getFeatureVariableValueForType(
2289+
validFeatureKey,
2290+
validVariableKey,
2291+
genericUserId,
2292+
attributes,
2293+
LiveVariable.VariableType.STRING);
2294+
assertEquals(defaultValue, value);
2295+
2296+
logbackVerifier.expectMessage(
2297+
Level.INFO,
2298+
"The feature flag \"" + validFeatureKey + "\" is not used in any experiments."
2299+
);
2300+
}
2301+
2302+
/**
2303+
* Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)}
2304+
* returns the String default value for a live variable
2305+
* when the feature is attached to an experiment, but the user is excluded from the experiment.
2306+
* @throws ConfigParseException
2307+
*/
2308+
@Test
2309+
public void getFeatureVariableValueReturnsDefaultValueWhenFeatureIsAttachedToOneExperimentButFailsTargeting() throws ConfigParseException {
2310+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
2311+
2312+
String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY;
2313+
String validVariableKey = VARIABLE_FIRST_LETTER_KEY;
2314+
String expectedValue = VARIABLE_FIRST_LETTER_DEFAULT_VALUE;
2315+
2316+
Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
2317+
.withConfig(validProjectConfig)
2318+
.build();
2319+
2320+
String valueWithImproperAttributes = optimizely.getFeatureVariableValueForType(
2321+
validFeatureKey,
2322+
validVariableKey,
2323+
genericUserId,
2324+
Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, "Slytherin"),
2325+
LiveVariable.VariableType.STRING
2326+
);
2327+
assertEquals(expectedValue, valueWithImproperAttributes);
2328+
2329+
logbackVerifier.expectMessage(
2330+
Level.INFO,
2331+
"User \"" + genericUserId +
2332+
"\" was not bucketed into any variation for feature flag \"" + validFeatureKey +
2333+
"\". The default value is being returned."
2334+
);
2335+
}
2336+
2337+
/**
2338+
* Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)}
2339+
* returns the variable value of the variation the user is bucketed into
2340+
* if the variation is not null and the variable has a usage within the variation.
2341+
* @throws ConfigParseException
2342+
*/
2343+
@Test
2344+
public void getFeatureVariableValueReturnsVariationValueWhenUserGetsBucketedToVariation() throws ConfigParseException {
2345+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
2346+
2347+
String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY;
2348+
String validVariableKey = VARIABLE_FIRST_LETTER_KEY;
2349+
LiveVariable variable = FEATURE_FLAG_MULTI_VARIATE_FEATURE.getVariableKeyToLiveVariableMap().get(validVariableKey);
2350+
String expectedValue = VARIATION_MULTIVARIATE_EXPERIMENT_GRED.getVariableIdToLiveVariableUsageInstanceMap().get(variable.getId()).getValue();
2351+
2352+
Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
2353+
.withConfig(validProjectConfig)
2354+
.withDecisionService(mockDecisionService)
2355+
.build();
2356+
2357+
doReturn(VARIATION_MULTIVARIATE_EXPERIMENT_GRED).when(mockDecisionService).getVariationForFeature(
2358+
FEATURE_FLAG_MULTI_VARIATE_FEATURE,
2359+
genericUserId,
2360+
Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE)
2361+
);
2362+
2363+
String value = optimizely.getFeatureVariableValueForType(
2364+
validFeatureKey,
2365+
validVariableKey,
2366+
genericUserId,
2367+
Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE),
2368+
LiveVariable.VariableType.STRING
2369+
);
2370+
2371+
assertEquals(expectedValue, value);
2372+
}
2373+
21462374
//======== Helper methods ========//
21472375

21482376
private Experiment createUnknownExperiment() {

0 commit comments

Comments
 (0)