Skip to content

Commit 8c37f7e

Browse files
authored
feat(decide): add a new set of decide apis (#352)
Add a new set of Decide APIs: - add createUserContext API to OptimizelyClient. - add defaultDecideOption to OptimizelyClient constructor and builder. - upgrade core java-sdk to use its decide-apis support.
1 parent b0123c3 commit 8c37f7e

File tree

7 files changed

+258
-26
lines changed

7 files changed

+258
-26
lines changed

android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2017-2020, Optimizely, Inc. and contributors *
2+
* Copyright 2017-2021, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -23,6 +23,7 @@
2323
import com.google.gson.JsonElement;
2424
import com.google.gson.JsonParser;
2525
import com.optimizely.ab.Optimizely;
26+
import com.optimizely.ab.OptimizelyUserContext;
2627
import com.optimizely.ab.android.event_handler.DefaultEventHandler;
2728
import com.optimizely.ab.bucketing.Bucketer;
2829
import com.optimizely.ab.bucketing.DecisionService;
@@ -42,6 +43,9 @@
4243
import com.optimizely.ab.notification.TrackNotificationListener;
4344
import com.optimizely.ab.notification.UpdateConfigNotification;
4445
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
46+
import com.optimizely.ab.optimizelydecision.DecisionResponse;
47+
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
48+
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
4549
import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
4650

4751
import org.junit.Assert;
@@ -68,10 +72,13 @@
6872
import static junit.framework.Assert.assertNotNull;
6973
import static junit.framework.Assert.assertNull;
7074
import static junit.framework.Assert.assertTrue;
75+
import static org.hamcrest.Matchers.anyOf;
7176
import static org.hamcrest.Matchers.hasEntry;
7277
import static org.junit.Assert.assertNotEquals;
7378
import static org.junit.Assert.assertThat;
7479
import static org.junit.Assume.assumeTrue;
80+
import static org.mockito.Matchers.anyObject;
81+
import static org.mockito.Matchers.eq;
7582
import static org.mockito.Mockito.mock;
7683
import static org.mockito.Mockito.spy;
7784
import static org.mockito.Mockito.verify;
@@ -122,10 +129,24 @@ public OptimizelyClientTest(int datafileVersion,String datafile){
122129
this.datafileVersion = datafileVersion;
123130
eventHandler = spy(DefaultEventHandler.getInstance(InstrumentationRegistry.getInstrumentation().getTargetContext()));
124131
optimizely = Optimizely.builder(datafile, eventHandler).build();
132+
133+
// set to return DecisionResponse with null variation by default (instead of null DecisionResponse)
134+
when(bucketer.bucket(anyObject(), anyObject(), anyObject())).thenReturn(DecisionResponse.nullNoReasons());
135+
125136
if(datafileVersion==3) {
126-
when(bucketer.bucket(optimizely.getProjectConfig().getExperiments().get(0), GENERIC_USER_ID, optimizely.getProjectConfig())).thenReturn(optimizely.getProjectConfig().getExperiments().get(0).getVariations().get(0));
137+
Variation variation = optimizely.getProjectConfig().getExperiments().get(0).getVariations().get(0);
138+
when(bucketer.bucket(
139+
optimizely.getProjectConfig().getExperiments().get(0),
140+
GENERIC_USER_ID,
141+
optimizely.getProjectConfig())
142+
).thenReturn(DecisionResponse.responseNoReasons(variation));
127143
} else {
128-
when(bucketer.bucket(optimizely.getProjectConfig().getExperimentKeyMapping().get(FEATURE_MULTI_VARIATE_EXPERIMENT_KEY), GENERIC_USER_ID, optimizely.getProjectConfig())).thenReturn(optimizely.getProjectConfig().getExperimentKeyMapping().get(FEATURE_MULTI_VARIATE_EXPERIMENT_KEY).getVariations().get(1));
144+
Variation variation = optimizely.getProjectConfig().getExperimentKeyMapping().get(FEATURE_MULTI_VARIATE_EXPERIMENT_KEY).getVariations().get(1);
145+
when(bucketer.bucket(
146+
optimizely.getProjectConfig().getExperimentKeyMapping().get(FEATURE_MULTI_VARIATE_EXPERIMENT_KEY),
147+
GENERIC_USER_ID,
148+
optimizely.getProjectConfig())
149+
).thenReturn(DecisionResponse.responseNoReasons(variation));
129150
}
130151
spyOnConfig();
131152
} catch (Exception configException) {
@@ -2151,6 +2172,88 @@ public void testAddLogEventNotificationHandlerWithInvalidOptimizely() {
21512172
.getNotificationManager(LogEvent.class).remove(notificationId));
21522173
}
21532174

2175+
// OptimizelyUserContext + Decide API
2176+
2177+
@Test
2178+
public void testCreateUserContext() {
2179+
OptimizelyClient optimizelyClient = new OptimizelyClient(optimizely, logger);
2180+
OptimizelyUserContext userContext = optimizelyClient.createUserContext(GENERIC_USER_ID);
2181+
assertEquals(userContext.getUserId(), GENERIC_USER_ID);
2182+
assert(userContext.getAttributes().isEmpty());
2183+
}
2184+
2185+
@Test
2186+
public void testCreateUserContext_withAttributes() {
2187+
Map<String, Object> attributes = Collections.singletonMap("house", "Gryffindor");
2188+
2189+
OptimizelyClient optimizelyClient = new OptimizelyClient(optimizely, logger);
2190+
OptimizelyUserContext userContext = optimizelyClient.createUserContext(GENERIC_USER_ID, attributes);
2191+
assertEquals(userContext.getUserId(), GENERIC_USER_ID);
2192+
assertEquals(userContext.getAttributes(), attributes);
2193+
}
2194+
2195+
@Test
2196+
// this should be enough to validate connection to the core java-sdk
2197+
public void testDecide() {
2198+
assumeTrue(datafileVersion == Integer.parseInt(ProjectConfig.Version.V4.toString()));
2199+
2200+
String flagKey = INTEGER_FEATURE_KEY;
2201+
Map<String, Object> attributes = Collections.singletonMap("house", "Gryffindor");
2202+
2203+
OptimizelyClient optimizelyClient = new OptimizelyClient(optimizely, logger);
2204+
OptimizelyUserContext userContext = optimizelyClient.createUserContext(GENERIC_USER_ID, attributes);
2205+
OptimizelyDecision decision = userContext.decide(flagKey);
2206+
OptimizelyJSON variablesExpected = new OptimizelyJSON(Collections.singletonMap("integer_variable", 2));
2207+
2208+
assertEquals(decision.getVariationKey(), "Feorge");
2209+
assertTrue(decision.getEnabled());
2210+
assertEquals(decision.getVariables().toMap(), variablesExpected.toMap());
2211+
assertEquals(decision.getRuleKey(), FEATURE_MULTI_VARIATE_EXPERIMENT_KEY);
2212+
assertEquals(decision.getFlagKey(), flagKey);
2213+
assertEquals(decision.getUserContext(), userContext);
2214+
assertTrue(decision.getReasons().isEmpty());
2215+
}
2216+
2217+
@Test
2218+
// this should be enough to validate connection to the core java-sdk
2219+
public void testDecide_withoutDefaultDecideOptions() throws IOException {
2220+
assumeTrue(datafileVersion == Integer.parseInt(ProjectConfig.Version.V4.toString()));
2221+
2222+
String datafile = loadRawResource(InstrumentationRegistry.getInstrumentation().getTargetContext(),R.raw.validprojectconfigv4);
2223+
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
2224+
OptimizelyManager optimizelyManager = OptimizelyManager.builder(testProjectId).build(context);
2225+
optimizelyManager.initialize(context, datafile);
2226+
2227+
OptimizelyClient optimizelyClient = optimizelyManager.getOptimizely();
2228+
OptimizelyUserContext userContext = optimizelyClient.createUserContext(GENERIC_USER_ID);
2229+
OptimizelyDecision decision = userContext.decide(INTEGER_FEATURE_KEY);
2230+
2231+
assertTrue(decision.getReasons().isEmpty());
2232+
}
2233+
2234+
@Test
2235+
// this should be enough to validate connection to the core java-sdk
2236+
public void testDecide_withDefaultDecideOptions() throws IOException {
2237+
assumeTrue(datafileVersion == Integer.parseInt(ProjectConfig.Version.V4.toString()));
2238+
2239+
List<OptimizelyDecideOption> defaultDecideOptions = Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS);
2240+
2241+
String datafile = loadRawResource(InstrumentationRegistry.getInstrumentation().getTargetContext(),R.raw.validprojectconfigv4);
2242+
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
2243+
OptimizelyManager optimizelyManager = OptimizelyManager.builder(testProjectId)
2244+
.withDefaultDecideOptions(defaultDecideOptions)
2245+
.build(context);
2246+
optimizelyManager.initialize(context, datafile);
2247+
2248+
OptimizelyClient optimizelyClient = optimizelyManager.getOptimizely();
2249+
OptimizelyUserContext userContext = optimizelyClient.createUserContext(GENERIC_USER_ID);
2250+
OptimizelyDecision decision = userContext.decide(INTEGER_FEATURE_KEY);
2251+
2252+
assertTrue(decision.getReasons().size() > 0);
2253+
}
2254+
2255+
// Utils
2256+
21542257
private boolean compareJsonStrings(String str1, String str2) {
21552258
JsonParser parser = new JsonParser();
21562259

android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2017-2018, Optimizely, Inc. and contributors *
2+
* Copyright 2017-2021, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -166,7 +166,7 @@ public void initializeSyncWithEnvironment() {
166166
EventHandler eventHandler = mock(DefaultEventHandler.class);
167167
EventProcessor eventProcessor = mock(EventProcessor.class);
168168
OptimizelyManager optimizelyManager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, 3600L, datafileHandler, null, 3600L,
169-
eventHandler, eventProcessor, null, null);
169+
eventHandler, eventProcessor, null, null, null);
170170
/*
171171
* Scenario#1: when datafile is not Empty
172172
* Scenario#2: when datafile is Empty
@@ -225,7 +225,7 @@ public void initializeAsyncWithEnvironment() {
225225
EventHandler eventHandler = mock(DefaultEventHandler.class);
226226
EventProcessor eventProcessor = mock(EventProcessor.class);
227227
final OptimizelyManager optimizelyManager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, 3600L, datafileHandler, null, 3600L,
228-
eventHandler, eventProcessor, null, null);
228+
eventHandler, eventProcessor, null, null, null);
229229

230230
/*
231231
* Scenario#1: when datafile is not Empty
@@ -515,7 +515,7 @@ public void initializeSyncWithUpdateOnNewDatafileDisabled() {
515515
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
516516

517517
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
518-
null, null, null, null);
518+
null, null, null, null, null);
519519

520520
doAnswer(
521521
new Answer<Object>() {
@@ -548,7 +548,7 @@ public void initializeSyncWithUpdateOnNewDatafileEnabled() {
548548
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
549549

550550
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
551-
null, null, null, null);
551+
null, null, null, null, null);
552552

553553
doAnswer(
554554
new Answer<Object>() {
@@ -581,7 +581,7 @@ public void initializeSyncWithDownloadToCacheDisabled() {
581581
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
582582

583583
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
584-
null, null, null, null);
584+
null, null, null, null, null);
585585

586586
doAnswer(
587587
new Answer<Object>() {
@@ -614,7 +614,7 @@ public void initializeSyncWithUpdateOnNewDatafileDisabledWithPeriodicPollingEnab
614614
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
615615

616616
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
617-
null, null, null, null);
617+
null, null, null, null, null);
618618

619619
doAnswer(
620620
(Answer<Object>) invocation -> {
@@ -646,7 +646,7 @@ public void initializeSyncWithUpdateOnNewDatafileEnabledWithPeriodicPollingEnabl
646646
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
647647

648648
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
649-
null, null, null, null);
649+
null, null, null, null, null);
650650

651651
doAnswer(
652652
new Answer<Object>() {
@@ -679,7 +679,7 @@ public void initializeSyncWithUpdateOnNewDatafileDisabledWithPeriodicPollingDisa
679679
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
680680

681681
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
682-
null, null, null, null);
682+
null, null, null, null, null);
683683

684684
doAnswer(
685685
new Answer<Object>() {
@@ -713,7 +713,7 @@ public void initializeSyncWithUpdateOnNewDatafileEnabledWithPeriodicPollingDisab
713713
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
714714

715715
OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
716-
null, null, null, null);
716+
null, null, null, null, null);
717717

718718
doAnswer(
719719
new Answer<Object>() {
@@ -746,7 +746,7 @@ public void initializeSyncWithResourceDatafileNoCache() {
746746
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
747747

748748
OptimizelyManager manager = spy(new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
749-
null, null, null, null));
749+
null, null, null, null, null));
750750

751751
datafileHandler.removeSavedDatafile(context, manager.getDatafileConfig());
752752
OptimizelyClient client = manager.initialize(context, R.raw.datafile, downloadToCache, updateConfigOnNewDatafile);
@@ -763,7 +763,7 @@ public void initializeSyncWithResourceDatafileNoCacheWithDefaultParams() {
763763
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
764764

765765
OptimizelyManager manager = spy(new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0,
766-
null, null, null, null));
766+
null, null, null, null, null));
767767

768768
datafileHandler.removeSavedDatafile(context, manager.getDatafileConfig());
769769
OptimizelyClient client = manager.initialize(context, R.raw.datafile);

android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClient.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2017-2020, Optimizely, Inc. and contributors *
2+
* Copyright 2017-2021, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -21,6 +21,7 @@
2121
import androidx.annotation.Nullable;
2222

2323
import com.optimizely.ab.Optimizely;
24+
import com.optimizely.ab.OptimizelyUserContext;
2425
import com.optimizely.ab.UnknownEventTypeException;
2526
import com.optimizely.ab.config.ProjectConfig;
2627
import com.optimizely.ab.config.Variation;
@@ -771,6 +772,7 @@ public OptimizelyJSON getAllFeatureVariables(@NonNull String featureKey,
771772
*
772773
* @return {@link OptimizelyConfig}
773774
*/
775+
@Nullable
774776
public OptimizelyConfig getOptimizelyConfig() {
775777
if (isValid()) {
776778
return optimizely.getOptimizelyConfig();
@@ -780,6 +782,30 @@ public OptimizelyConfig getOptimizelyConfig() {
780782
}
781783
}
782784

785+
/**
786+
* Create a context of the user for which decision APIs will be called.
787+
*
788+
* A user context will be created successfully even when the SDK is not fully configured yet.
789+
*
790+
* @param userId The user ID to be used for bucketing.
791+
* @param attributes: A map of attribute names to current user attribute values.
792+
* @return An OptimizelyUserContext associated with this OptimizelyClient.
793+
*/
794+
@Nullable
795+
public OptimizelyUserContext createUserContext(@NonNull String userId,
796+
@NonNull Map<String, Object> attributes) {
797+
if (isValid()) {
798+
return optimizely.createUserContext(userId, attributes);
799+
} else {
800+
logger.warn("Optimizely is not initialized, could not create a user context");
801+
return null;
802+
}
803+
}
804+
805+
public OptimizelyUserContext createUserContext(@NonNull String userId) {
806+
return createUserContext(userId, null);
807+
}
808+
783809
//======== Notification APIs ========//
784810

785811
/**

0 commit comments

Comments
 (0)