Skip to content

Commit 92f3a79

Browse files
Kroach/oasis 1397 forced variation (#159)
* Add OptimizelyBucketId * Add stub setForcedVariation:userId:variationKey * Add getForcedVariation and setForcedVariation to OPTLYProjectConfig.m+.h * OPTLYDecisionService ---- check for forced variation ---- * Add setForcedVariation to Optimizely.m * Add testSetForcedVariationExperimentNotRunning * Add testGetVariationWithWhitelistedVariationOverriddenBySetForcedVariation * Add testGetVariationWithInvalidAudienceOverriddenBySetForcedVariation * Objective-C Implmentation of "Forced Bucketing" and "Bucketing ID" Design Docs Summary: OASIS-1397 Reference implementation for Forced variations and bucketing keys in one SDK Add OptimizelyBucketId Add stub setForcedVariation:userId:variationKey Add getForcedVariation and setForcedVariation to OPTLYProjectConfig.m+.h OPTLYDecisionService ---- check for forced variation ---- Add setForcedVariation to Optimizely.m Add testSetForcedVariationExperimentNotRunning Add testGetVariationWithWhitelistedVariationOverriddenBySetForcedVariation Add testGetVariationWithInvalidAudienceOverriddenBySetForcedVariation Add a couple more test asserts Test Plan: Added 3 tests which pass to OPTLYDecisionServiceTest.m Reviewers: alda JIRA Issues: OASIS-1397 Differential Revision: https://phabricator.optimizely.com/D16606 * Add fourth test and remove yellow caution when nil is passed as variationKey to setForcedVariation:userId:variationKey: * Add fifth test * Add sixth test * Improved error handling and logging in setForcedVariation:userId:variationKey: * Change setForcedVariation to return a BOOL, agreeing with revised spec. * preferredVariationMap should cache immutable variation id's not OPTLYVariation's which could go out-of-date across initialize's * Add getForcedVariation:userId: to Optimizely.m+.h * Add XCTAssert to testSetForcedVariationExperimentNotRunning * Add two tests against invalid experimentKey's * Add two tests against invalid variationKey's * Add testSetForcedVariationCalledOnInvalidUserId * Revert "Add OptimizelyBucketId" This reverts commit f8c60a8. * preferredVariationMap --> forcedVariationMap * Add '#pragma mark's to OPTLYDecisionServiceTest.m * Moving methods to different location, but not otherwise changing code * Add methods to OPTLYProjectConfigTest.m * Moving 4 methods to different section, OW code is unchanged. * Add testSetForcedVariationFollowedByGetForcedVariation * forcedVariationMap == userId --> experimentId --> variationId * @synchronized access to mutable forcedVariationMap * Add 2 new '@protocol Optimizely' methods to OPTLYClient.m
1 parent eafd873 commit 92f3a79

File tree

10 files changed

+585
-4
lines changed

10 files changed

+585
-4
lines changed

OptimizelySDKCore/OptimizelySDKCore/OPTLYDecisionService.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ - (OPTLYVariation *)getVariation:(NSString *)userId
6060
return nil;
6161
}
6262

63+
// ---- check for forced variation ----
64+
bucketedVariation = [self.config getForcedVariation:experimentKey userId:userId];
65+
if (bucketedVariation != nil) {
66+
return bucketedVariation;
67+
}
68+
6369
// ---- check if the experiment is whitelisted ----
6470
if ([self checkWhitelistingForUser:userId experiment:experiment]) {
6571
return [self getWhitelistedVariationForUser:userId

OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ extern NSString *const OPTLYLoggerMessagesExperimentUnknownForExperimentId;
161161
extern NSString *const OPTLYLoggerMessagesExperimentUnknownForExperimentKey;
162162
extern NSString *const OPTLYLoggerMessagesGroupUnknownForGroupId;
163163
extern NSString *const OPTLYLoggerMessagesGetVariationNilVariation;
164+
extern NSString *const OPTLYLoggerMessagesVariationKeyUnknownForExperimentKey;
165+
extern NSString *const OPTLYLoggerMessagesProjectConfigUserIdInvalid;
164166

165167
// ---- User Profile ----
166168
// debug

OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@
156156
NSString *const OPTLYLoggerMessagesExperimentUnknownForExperimentKey = @"[PROJECT CONFIG] Experiment key not found for experiment: %@. Experiment key is not in the datafile."; // experiment key
157157
NSString *const OPTLYLoggerMessagesGroupUnknownForGroupId = @"[PROJECT CONFIG] Group not found for group ID: %@. Group ID is not in the datafile."; // group id
158158
NSString *const OPTLYLoggerMessagesGetVariationNilVariation = @"[PROJECT CONFIG] Get variation returned a nil variation for user %@, experiment %@";
159+
NSString *const OPTLYLoggerMessagesVariationKeyUnknownForExperimentKey = @"[PROJECT CONFIG] Variation key %@ not found for experiment key %@.";
160+
NSString *const OPTLYLoggerMessagesProjectConfigUserIdInvalid = @"[PROJECT CONFIG] User ID cannot be an empty string.";
159161

160162
// ---- User Profile ----
161163
// debug

OptimizelySDKCore/OptimizelySDKCore/OPTLYProjectConfig.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,19 @@ NS_ASSUME_NONNULL_END
126126
*/
127127
- (nullable OPTLYVariable *)getVariableForVariableKey:(nonnull NSString *)variableKey;
128128

129+
/**
130+
* Get forced variation for a given experiment key and user id.
131+
*/
132+
- (nullable OPTLYVariation *)getForcedVariation:(nonnull NSString *)experimentKey
133+
userId:(nonnull NSString *)userId;
134+
135+
/**
136+
* Set forced variation for a given experiment key and user id according to a given variation key.
137+
*/
138+
- (BOOL)setForcedVariation:(nonnull NSString *)experimentKey
139+
userId:(nonnull NSString *)userId
140+
variationKey:(nonnull NSString *)variationKey;
141+
129142
/**
130143
* Get variation for experiment and user ID with user attributes.
131144
*/

OptimizelySDKCore/OptimizelySDKCore/OPTLYProjectConfig.m

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ @interface OPTLYProjectConfig()
4343
@property (nonatomic, strong) NSDictionary<NSString *, OPTLYGroup *><Ignore> *groupIdToGroupMap;
4444
@property (nonatomic, strong) NSDictionary<NSString *, OPTLYAttribute *><Ignore> *attributeKeyToAttributeMap;
4545
@property (nonatomic, strong) NSDictionary<NSString *, OPTLYVariable *><Ignore> *variableKeyToVariableMap;
46+
//@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, NSString *>><Ignore> *forcedVariationMap;
47+
// userId --> experimentId --> variationId
48+
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableDictionary *><Ignore> *forcedVariationMap;
4649

4750
@end
4851

@@ -230,6 +233,82 @@ - (OPTLYVariable *)getVariableForVariableKey:(NSString *)variableKey {
230233
return variable;
231234
}
232235

236+
#pragma mark -- Forced Variation Methods --
237+
238+
- (OPTLYVariation *)getForcedVariation:(nonnull NSString *)experimentKey
239+
userId:(nonnull NSString *)userId {
240+
// Get experiment from experimentKey .
241+
OPTLYExperiment *experiment = [self getExperimentForKey:experimentKey];
242+
if (!experiment) {
243+
// NOTE: getExperimentForKey: will log any non-existent experimentKey and return experiment == nil for us.
244+
return nil;
245+
}
246+
OPTLYVariation *variation = nil;
247+
@synchronized (self.forcedVariationMap) {
248+
NSMutableDictionary<NSString *, NSString *> *dictionary = self.forcedVariationMap[userId];
249+
if (dictionary != nil) {
250+
// Get variation from experimentId and variationId .
251+
NSString *experimentId = experiment.experimentId;
252+
NSString *variationId = dictionary[experimentId];
253+
variation = [experiment getVariationForVariationId:variationId];
254+
}
255+
}
256+
return variation;
257+
}
258+
259+
- (BOOL)setForcedVariation:(nonnull NSString *)experimentKey
260+
userId:(nonnull NSString *)userId
261+
variationKey:(nonnull NSString *)variationKey {
262+
// Return YES if there were no errors, OW return NO .
263+
// Get experiment from experimentKey .
264+
OPTLYExperiment *experiment = [self getExperimentForKey:experimentKey];
265+
if (!experiment) {
266+
// NOTE: getExperimentForKey: will log any non-existent experimentKey and return experiment == nil for us.
267+
return NO;
268+
}
269+
NSString *experimentId=experiment.experimentId;
270+
// Check for valid userId
271+
if ([userId length]==0) {
272+
[self.logger logMessage:OPTLYLoggerMessagesProjectConfigUserIdInvalid withLevel:OptimizelyLogLevelDebug];
273+
return NO;
274+
}
275+
// Get variation from experiment and non-nil variationKey, if applicable.
276+
OPTLYVariation *variation = nil;
277+
if (variationKey != nil) {
278+
variation = [experiment getVariationForVariationKey:variationKey];
279+
if (!variation) {
280+
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesVariationKeyUnknownForExperimentKey, variationKey, experimentKey];
281+
[self.logger logMessage:logMessage withLevel:OptimizelyLogLevelDebug];
282+
// Leave in current state, and report NO meaning there was an error.
283+
return NO;
284+
}
285+
}
286+
@synchronized (self.forcedVariationMap) {
287+
// Locate relevant dictionary inside forcedVariationMap
288+
NSMutableDictionary<NSString *, NSString *> *dictionary = self.forcedVariationMap[userId];
289+
if ((dictionary == nil) && (variationKey != nil)) {
290+
// We need a non-nil dictionary to store an OPTLYVariation .
291+
dictionary = [[NSMutableDictionary alloc] init];
292+
self.forcedVariationMap[userId] = dictionary;
293+
}
294+
// Apply change to dictionary
295+
if (variation == nil) {
296+
// NOTE: removeObjectForKey: "Does nothing if [experimentKey] does not exist."
297+
// https://developer.apple.com/documentation/foundation/nsmutabledictionary/1416518-removeobjectforkey?language=objc
298+
[dictionary removeObjectForKey:experimentId];
299+
if ([dictionary count] == 0) {
300+
// For elegance, we can remove empty dictionary for userId from self.forcedVariationMap
301+
[self.forcedVariationMap removeObjectForKey:userId];
302+
}
303+
} else {
304+
// If there is no OPTLYExperiment *experiment corresponding to experimentKey ,
305+
// then we will land in the "(variation == nil)" case above due to code above.
306+
dictionary[experimentId] = variation.variationId;
307+
};
308+
};
309+
return YES;
310+
}
311+
233312
#pragma mark -- Property Getters --
234313

235314
- (NSArray *)allExperiments
@@ -313,6 +392,16 @@ - (NSDictionary *)eventKeyToEventMap {
313392
return _variableKeyToVariableMap;
314393
}
315394

395+
//- (NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, NSString *>> *)forcedVariationMap {
396+
- (NSMutableDictionary<NSString *, NSMutableDictionary *> *)forcedVariationMap {
397+
@synchronized (self) {
398+
if (!_forcedVariationMap) {
399+
_forcedVariationMap = [[NSMutableDictionary alloc] init];
400+
}
401+
}
402+
return _forcedVariationMap;
403+
}
404+
316405
#pragma mark -- Generate Mappings --
317406

318407
- (NSDictionary *)generateAudienceIdToAudienceMap

OptimizelySDKCore/OptimizelySDKCore/Optimizely.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,39 @@ typedef NS_ENUM(NSInteger, OPTLYLiveVariableError) {
9595
userId:(nonnull NSString *)userId
9696
attributes:(nullable NSDictionary<NSString *, NSString *> *)attributes;
9797

98+
#pragma mark - Forced Variation Methods
99+
/**
100+
* Use the setForcedVariation method to force an experimentKey-userId
101+
* pair into a specific variation for QA purposes.
102+
* The forced bucketing feature allows customers to force users into
103+
* variations in real time for QA purposes without requiring datafile
104+
* downloads from the network. Methods activate and track are called
105+
* as usual after the variation is set, but the user will be bucketed
106+
* into the forced variation overriding any variation which would be
107+
* computed via the network datafile.
108+
*/
109+
110+
/**
111+
* Return forced variation for experiment and user ID.
112+
* @param experimentKey The key for the experiment.
113+
* @param userId The user ID to be used for bucketing.
114+
* @return forced variation if it exists, otherwise return nil.
115+
*/
116+
- (OPTLYVariation *_Nullable)getForcedVariation:(nonnull NSString *)experimentKey
117+
userId:(nonnull NSString *)userId;
118+
119+
/**
120+
* Set forced variation for experiment and user ID to variationKey.
121+
* @param experimentKey The key for the experiment.
122+
* @param userId The user ID to be used for bucketing.
123+
* @param variationKey The variation the user should be forced into.
124+
* This value can be nil, in which case, the forced variation is cleared.
125+
* @return YES if there were no errors, otherwise return NO.
126+
*/
127+
- (BOOL)setForcedVariation:(nonnull NSString *)experimentKey
128+
userId:(nonnull NSString *)userId
129+
variationKey:(nullable NSString *)variationKey;
130+
98131
#pragma mark - trackEvent methods
99132
/**
100133
* Track an event

OptimizelySDKCore/OptimizelySDKCore/Optimizely.m

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,21 @@ - (OPTLYVariation *)variation:(NSString *)experimentKey
194194
return bucketedVariation;
195195
}
196196

197+
#pragma mark Forced variation methods
198+
- (OPTLYVariation *)getForcedVariation:(nonnull NSString *)experimentKey
199+
userId:(nonnull NSString *)userId {
200+
return [self.config getForcedVariation:experimentKey
201+
userId:userId];
202+
}
203+
204+
- (BOOL)setForcedVariation:(nonnull NSString *)experimentKey
205+
userId:(nonnull NSString *)userId
206+
variationKey:(nullable NSString *)variationKey {
207+
return [self.config setForcedVariation:experimentKey
208+
userId:userId
209+
variationKey:variationKey];
210+
}
211+
197212
#pragma mark trackEvent methods
198213
- (void)track:(NSString *)eventKey userId:(NSString *)userId
199214
{

0 commit comments

Comments
 (0)