Skip to content

Commit e7698e0

Browse files
isFeatureEnabled API (#207)
* Feature flag & rollout parsing * New Models added * variation variable usage mapping * Feature variable and its usage parsing * demoTestDatafile version upgraded to 4 and maintain compatibility to v3 * Test cases added for new models * get back to Asynchronous Initialization of OPTLYManager * CR updated * Feature flag & rollout bucketing decision * Decision Service Logic added * Test cases for getVariationForFeatureExperiment * Test cases for getVariationForFeatureRollout * CR updated * fix - file references * replaced experimentId & variationId with their objects in FeatureDecision * isFeatureEnabled API * implemented API with test cases * added a test case for mutex group experiments * Fixed crash with nil check of completion block * Removed xcode analyze warnings * Merge branch 'master' into arafay/isFeatureEnabled-API * Merge branch 'master' into arafay/isFeatureEnabled-API * master: Feature flag & rollout bucket decision (#206) Feature flag & rollout parsing (#205) # Conflicts: # OptimizelySDKCore/OptimizelySDKCore/OPTLYDecisionService.m # OptimizelySDKCore/OptimizelySDKCore/OPTLYFeatureDecision.h # OptimizelySDKCore/OptimizelySDKCore/OPTLYFeatureDecision.m # OptimizelySDKCore/OptimizelySDKCore/OPTLYFeatureFlag.h # OptimizelySDKCore/OptimizelySDKCore/OPTLYFeatureFlag.m # OptimizelySDKCore/OptimizelySDKCore/OPTLYFeatureVariable.h # OptimizelySDKCore/OptimizelySDKCore/OPTLYFeatureVariable.m # OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.h # OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.m # OptimizelySDKCore/OptimizelySDKCore/OPTLYProjectConfig.m # OptimizelySDKCore/OptimizelySDKCore/OPTLYRollout.h # OptimizelySDKCore/OptimizelySDKCore/OPTLYRollout.m # OptimizelySDKCore/OptimizelySDKCore/OPTLYVariableUsage.h # OptimizelySDKCore/OptimizelySDKCore/OPTLYVariableUsage.m # OptimizelySDKCore/OptimizelySDKCoreTests/OPTLYDecisionServiceTest.m # OptimizelySDKCore/OptimizelySDKCoreTests/OPTLYProjectConfigTest.m # OptimizelySDKCore/OptimizelySDKCoreTests/OptimizelyTest.m # OptimizelySDKCore/OptimizelySDKCoreTests/TestData/optimizely_6372300739_v4.json # OptimizelySDKCore/OptimizelySDKCoreTests/TestData/test_data_10_experiments.json # OptimizelySDKUniversal/OptimizelySDKUniversal.xcodeproj/project.pbxproj * fixes - after merge
1 parent 2332741 commit e7698e0

File tree

13 files changed

+1140
-861
lines changed

13 files changed

+1140
-861
lines changed

OptimizelySDKCore/OptimizelySDKCore/OPTLYBuilder.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
#import <Foundation/Foundation.h>
1818

19-
@class OPTLYBucketer, OPTLYEventBuilder, OPTLYEventBuilderDefault, OPTLYProjectConfig;
19+
@class OPTLYBucketer, OPTLYEventBuilder, OPTLYEventBuilderDefault, OPTLYProjectConfig, OPTLYDecisionService;
2020
@protocol OPTLYDatafileManager, OPTLYErrorHandler, OPTLYEventBuilder, OPTLYEventDispatcher, OPTLYLogger, OPTLYUserProfileService;
2121

2222
/**
@@ -36,6 +36,8 @@ typedef void (^OPTLYBuilderBlock)(OPTLYBuilder * _Nullable builder);
3636
@property (nonatomic, readonly, strong, nullable) OPTLYProjectConfig *config;
3737
/// The bucketer created by the builder.
3838
@property (nonatomic, readonly, strong, nullable) OPTLYBucketer *bucketer;
39+
/// The decision service created by the builder.
40+
@property (nonatomic, readonly, strong, nullable) OPTLYDecisionService *decisionService;
3941
/// The event builder created by the builder.
4042
@property (nonatomic, readonly, strong, nullable) OPTLYEventBuilderDefault *eventBuilder;
4143
/// The error handler is by default set to one that is created by Optimizely. This default error handler can be overridden by any object that conforms to the OPTLYErrorHandler protocol.

OptimizelySDKCore/OptimizelySDKCore/OPTLYBuilder.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#import "OPTLYEventDispatcherBasic.h"
2222
#import "OPTLYLogger.h"
2323
#import "OPTLYProjectConfig.h"
24+
#import "OPTLYDecisionService.h"
2425

2526
@implementation OPTLYBuilder
2627

@@ -71,6 +72,7 @@ - (id)initWithBlock:(OPTLYBuilderBlock)block {
7172
}
7273

7374
_bucketer = [[OPTLYBucketer alloc] initWithConfig:_config];
75+
_decisionService = [[OPTLYDecisionService alloc] initWithProjectConfig:_config bucketer:_bucketer];
7476
_eventBuilder = [[OPTLYEventBuilderDefault alloc] init];
7577

7678
return self;

OptimizelySDKCore/OptimizelySDKCore/OPTLYDatafileKeys.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ extern NSString * const OPTLYDatafileKeysFeatureFlagKey;
8080
extern NSString * const OPTLYDatafileKeysFeatureFlagRolloutId;
8181
extern NSString * const OPTLYDatafileKeysFeatureFlagExperimentIds;
8282
extern NSString * const OPTLYDatafileKeysFeatureFlagVariables;
83+
extern NSString * const OPTLYDatafileKeysFeatureFlagGroupId;
8384
// Feature Variable
8485
extern NSString * const OPTLYDatafileKeysFeatureVariableId;
8586
extern NSString * const OPTLYDatafileKeysFeatureVariableKey;

OptimizelySDKCore/OptimizelySDKCore/OPTLYDatafileKeys.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
NSString * const OPTLYDatafileKeysFeatureFlagRolloutId = @"rolloutId";
7777
NSString * const OPTLYDatafileKeysFeatureFlagExperimentIds = @"experimentIds";
7878
NSString * const OPTLYDatafileKeysFeatureFlagVariables = @"variables";
79+
NSString * const OPTLYDatafileKeysFeatureFlagGroupId = @"groupId";
7980
// Feature Variable
8081
NSString * const OPTLYDatafileKeysFeatureVariableId = @"id";
8182
NSString * const OPTLYDatafileKeysFeatureVariableKey = @"key";

OptimizelySDKCore/OptimizelySDKCore/OPTLYFeatureFlag.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#import <OptimizelySDKCore/OPTLYJSONModelLib.h>
2222
#endif
2323

24+
@class OPTLYProjectConfig;
2425
@protocol OPTLYFeatureVariable;
2526
@protocol OPTLYFeatureFlag
2627
@end
@@ -40,4 +41,11 @@
4041
/// an NSString to hold the group Id the feature belongs to.
4142
@property (nonatomic, strong, nullable) NSString<Optional> *groupId;
4243

44+
/**
45+
* Determines whether all the experiments in the feature flag belongs to the same mutex group
46+
* @param config The project config object.
47+
* @return YES if feature belongs to the same mutex group.
48+
*/
49+
- (BOOL)isValid:(nonnull OPTLYProjectConfig *)config;
50+
4351
@end

OptimizelySDKCore/OptimizelySDKCore/OPTLYFeatureFlag.m

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
#import "OPTLYFeatureFlag.h"
1818
#import "OPTLYDatafileKeys.h"
19+
#import "OPTLYProjectConfig.h"
20+
#import "OPTLYExperiment.h"
1921

2022
@implementation OPTLYFeatureFlag
2123

@@ -25,8 +27,33 @@ + (OPTLYJSONKeyMapper*)keyMapper
2527
OPTLYDatafileKeysFeatureFlagKey : @"key",
2628
OPTLYDatafileKeysFeatureFlagRolloutId : @"rolloutId",
2729
OPTLYDatafileKeysFeatureFlagExperimentIds : @"experimentIds",
28-
OPTLYDatafileKeysFeatureFlagVariables : @"variables"
30+
OPTLYDatafileKeysFeatureFlagVariables : @"variables",
31+
OPTLYDatafileKeysFeatureFlagGroupId : @"groupId"
2932
}];
3033
}
3134

35+
- (BOOL)isValid:(OPTLYProjectConfig *)config {
36+
if ([OPTLYFeatureFlag isEmptyArray:self.experimentIds]) {
37+
return true;
38+
}
39+
if (self.experimentIds.count == 1) {
40+
return true;
41+
}
42+
43+
NSString *groupId = [config getExperimentForId:[self.experimentIds firstObject]].groupId;
44+
45+
for (int i = 1; i < self.experimentIds.count; i++)
46+
{
47+
// Every experiment should have the same group Id.
48+
if ([config getExperimentForId:self.experimentIds[i]].groupId != groupId)
49+
return false;
50+
}
51+
return true;
52+
}
53+
54+
+ (BOOL)isEmptyArray:(NSObject*)array {
55+
return (!array
56+
|| ![array isKindOfClass:[NSArray class]]
57+
|| (((NSArray *)array).count == 0));
58+
}
3259
@end

OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ extern NSString *const OPTLYLoggerMessagesVariationUserAssigned;
2626
// info
2727
extern NSString *const OPTLYLoggerMessagesActivationSuccess;
2828
extern NSString *const OPTLYLoggerMessagesConversionSuccess;
29+
// error
30+
extern NSString *const OPTLYLoggerMessagesFeatureDisabledUserIdInvalid;
31+
extern NSString *const OPTLYLoggerMessagesFeatureDisabledFlagKeyInvalid;
32+
extern NSString *const OPTLYLoggerMessagesFeatureDisabled;
33+
extern NSString *const OPTLYLoggerMessagesFeatureEnabledNotExperimented;
34+
extern NSString *const OPTLYLoggerMessagesFeatureEnabled;
2935

3036
// ---- Bucketer ----
3137
// debug

OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
// info
2323
NSString *const OPTLYLoggerMessagesActivationSuccess = @"[OPTIMIZELY] Activating user %@ in experiment %@.";
2424
NSString *const OPTLYLoggerMessagesConversionSuccess = @"[OPTIMIZELY] Tracking event %@ for user %@.";
25+
// error
26+
NSString *const OPTLYLoggerMessagesFeatureDisabledUserIdInvalid = @"[OPTIMIZELY] User ID must not be empty for feature enabled.";
27+
NSString *const OPTLYLoggerMessagesFeatureDisabledFlagKeyInvalid = @"[OPTIMIZELY] Feature flag key must not be empty for feature enabled.";
28+
NSString *const OPTLYLoggerMessagesFeatureDisabled = @"[OPTIMIZELY] Feature flag %@ is not enabled for user %@.";
29+
NSString *const OPTLYLoggerMessagesFeatureEnabledNotExperimented = @"[OPTIMIZELY] The user %@ is not being experimented on feature %@.";
30+
NSString *const OPTLYLoggerMessagesFeatureEnabled = @"[OPTIMIZELY] Feature flag %@ is enabled for user %@.";
2531

2632
// ---- Bucketer ----
2733
// debug

OptimizelySDKCore/OptimizelySDKCore/Optimizely.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extern NSString * _Nonnull const OptimizelyNotificationsUserDictionaryEventNameK
2727
extern NSString * _Nonnull const OptimizelyNotificationsUserDictionaryEventValueKey;
2828
extern NSString * _Nonnull const OptimizelyNotificationsUserDictionaryExperimentVariationMappingKey;
2929

30-
@class OPTLYProjectConfig, OPTLYVariation;
30+
@class OPTLYProjectConfig, OPTLYVariation, OPTLYDecisionService;
3131
@protocol OPTLYBucketer, OPTLYErrorHandler, OPTLYEventBuilder, OPTLYEventDispatcher, OPTLYLogger;
3232

3333
@protocol Optimizely <NSObject>
@@ -121,6 +121,18 @@ extern NSString * _Nonnull const OptimizelyNotificationsUserDictionaryExperiment
121121
userId:(nonnull NSString *)userId
122122
variationKey:(nullable NSString *)variationKey;
123123

124+
#pragma mark - Feature Flag Methods
125+
126+
/**
127+
* Determine whether a feature is enabled.
128+
* Send an impression event if the user is bucketed into an experiment using the feature.
129+
* @param featureKey The key for the feature flag.
130+
* @param userId The user ID to be used for bucketing.
131+
* @param attributes The user's attributes.
132+
* @return YES if feature is enabled, false otherwise.
133+
*/
134+
- (BOOL)isFeatureEnabled:(nullable NSString *)featureKey userId:(nullable NSString *)userId attributes:(nullable NSDictionary<NSString *, NSString *> *)attributes;
135+
124136
#pragma mark - trackEvent methods
125137
/**
126138
* Track an event
@@ -193,6 +205,7 @@ extern NSString * _Nonnull const OptimizelyNotificationsUserDictionaryExperiment
193205
@interface Optimizely : NSObject <Optimizely>
194206

195207
@property (nonatomic, strong, readonly, nullable) id<OPTLYBucketer> bucketer;
208+
@property (nonatomic, strong, readonly, nullable) OPTLYDecisionService *decisionService;
196209
@property (nonatomic, strong, readonly, nullable) OPTLYProjectConfig *config;
197210
@property (nonatomic, strong, readonly, nullable) id<OPTLYErrorHandler> errorHandler;
198211
@property (nonatomic, strong, readonly, nullable) id<OPTLYEventBuilder> eventBuilder;

OptimizelySDKCore/OptimizelySDKCore/Optimizely.m

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
#import "OPTLYProjectConfig.h"
3232
#import "OPTLYUserProfileServiceBasic.h"
3333
#import "OPTLYVariation.h"
34+
#import "OPTLYFeatureFlag.h"
35+
#import "OPTLYFeatureDecision.h"
36+
#import "OPTLYDecisionService.h"
3437

3538
NSString *const OptimizelyDidActivateExperimentNotification = @"OptimizelyExperimentActivated";
3639
NSString *const OptimizelyDidTrackEventNotification = @"OptimizelyEventTracked";
@@ -57,6 +60,7 @@ - (instancetype)initWithBuilder:(OPTLYBuilder *)builder {
5760
if (self != nil) {
5861
if (builder != nil) {
5962
_bucketer = builder.bucketer;
63+
_decisionService = builder.decisionService;
6064
_config = builder.config;
6165
_eventBuilder = builder.eventBuilder;
6266
_eventDispatcher = builder.eventDispatcher;
@@ -104,10 +108,11 @@ - (OPTLYVariation *)activate:(NSString *)experimentKey
104108
callback:(void (^)(NSError *))callback {
105109

106110
// get variation
107-
OPTLYVariation *variation = [self variation:experimentKey
108-
userId:userId
109-
attributes:attributes];
111+
OPTLYVariation *variation = [self variation:experimentKey userId:userId attributes:attributes];
110112

113+
// get experiment
114+
OPTLYExperiment *experiment = [self.config getExperimentForKey:experimentKey];
115+
111116
if (!variation) {
112117
NSError *error = [self handleErrorLogsForActivateUser:userId experiment:experimentKey];
113118
if (callback) {
@@ -117,38 +122,24 @@ - (OPTLYVariation *)activate:(NSString *)experimentKey
117122
}
118123

119124
// send impression event
120-
NSDictionary *impressionEventParams = [self.eventBuilder buildImpressionEventTicket:self.config
121-
userId:userId
122-
experimentKey:experimentKey
123-
variationId:variation.variationId
124-
attributes:attributes];
125+
__weak typeof(self) weakSelf = self;
126+
OPTLYVariation *sentVariation = [self sendImpressionEventFor:experiment variation:variation userId:userId attributes:attributes callback:^(NSError *error) {
127+
if (error) {
128+
[weakSelf handleErrorLogsForActivateUser:userId experiment:experimentKey];
129+
}
130+
if (callback) {
131+
callback(error);
132+
}
133+
}];
125134

126-
if ([impressionEventParams count] == 0) {
135+
if (!sentVariation) {
127136
NSError *error = [self handleErrorLogsForActivateUser:userId experiment:experimentKey];
128137
if (callback) {
129138
callback(error);
130139
}
131140
return nil;
132141
}
133142

134-
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesEventDispatcherAttemptingToSendImpressionEvent, userId, experimentKey];
135-
[self.logger logMessage:logMessage withLevel:OptimizelyLogLevelInfo];
136-
137-
__weak typeof(self) weakSelf = self;
138-
[self.eventDispatcher dispatchImpressionEvent:impressionEventParams
139-
callback:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
140-
if (error) {
141-
[weakSelf handleErrorLogsForActivateUser:userId experiment:experimentKey];
142-
} else {
143-
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesEventDispatcherActivationSuccess, userId, experimentKey];
144-
[weakSelf.logger logMessage:logMessage
145-
withLevel:OptimizelyLogLevelInfo];
146-
}
147-
if (callback) {
148-
callback(error);
149-
}
150-
}];
151-
152143
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{
153144
OptimizelyNotificationsUserDictionaryVariationKey: variation
154145
}];
@@ -203,6 +194,48 @@ - (BOOL)setForcedVariation:(nonnull NSString *)experimentKey
203194
variationKey:variationKey];
204195
}
205196

197+
#pragma mark - Feature Flag Methods
198+
199+
- (BOOL)isFeatureEnabled:(NSString *)featureKey userId:(NSString *)userId attributes:(nullable NSDictionary<NSString *, NSString *> *)attributes {
200+
if ([Optimizely isEmptyString:userId]) {
201+
[self.logger logMessage:OPTLYLoggerMessagesFeatureDisabledUserIdInvalid withLevel:OptimizelyLogLevelError];
202+
return false;
203+
}
204+
if ([Optimizely isEmptyString:featureKey]) {
205+
[self.logger logMessage:OPTLYLoggerMessagesFeatureDisabledFlagKeyInvalid withLevel:OptimizelyLogLevelError];
206+
return false;
207+
}
208+
209+
OPTLYFeatureFlag *featureFlag = [self.config getFeatureFlagForKey:featureKey];
210+
if ([Optimizely isEmptyString:featureFlag.key]) {
211+
[self.logger logMessage:OPTLYLoggerMessagesFeatureDisabledFlagKeyInvalid withLevel:OptimizelyLogLevelError];
212+
return false;
213+
}
214+
if (![featureFlag isValid:self.config]) {
215+
return false;
216+
}
217+
218+
OPTLYFeatureDecision *decision = [self.decisionService getVariationForFeature:featureFlag userId:userId attributes:attributes];
219+
220+
if (!decision) {
221+
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesFeatureDisabled, featureKey, userId];
222+
[self.logger logMessage:logMessage withLevel:OptimizelyLogLevelInfo];
223+
return false;
224+
}
225+
226+
if ([decision.source isEqualToString:DecisionSourceExperiment]) {
227+
[self sendImpressionEventFor:decision.experiment variation:decision.variation userId:userId attributes:attributes callback:nil];
228+
} else {
229+
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesFeatureEnabledNotExperimented, userId, featureKey];
230+
[self.logger logMessage:logMessage withLevel:OptimizelyLogLevelInfo];
231+
}
232+
233+
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesFeatureEnabled, featureKey, userId];
234+
[self.logger logMessage:logMessage withLevel:OptimizelyLogLevelInfo];
235+
236+
return true;
237+
}
238+
206239
#pragma mark trackEvent methods
207240
- (void)track:(NSString *)eventKey userId:(NSString *)userId
208241
{
@@ -357,4 +390,51 @@ - (NSError *)handleErrorLogsForActivateUser:(NSString *)userId
357390
- (NSString *)description {
358391
return [NSString stringWithFormat:@"config:%@\nlogger:%@\nerrorHandler:%@\neventDispatcher:%@\nuserProfile:%@", self.config, self.logger, self.errorHandler, self.eventDispatcher, self.userProfileService];
359392
}
393+
394+
- (OPTLYVariation *)sendImpressionEventFor:(OPTLYExperiment *)experiment
395+
variation:(OPTLYVariation *)variation
396+
userId:(NSString *)userId
397+
attributes:(NSDictionary<NSString *,NSString *> *)attributes
398+
callback:(void (^)(NSError *))callback {
399+
400+
// send impression event
401+
NSDictionary *impressionEventParams = [self.eventBuilder buildImpressionEventTicket:self.config
402+
userId:userId
403+
experimentKey:experiment.experimentKey
404+
variationId:variation.variationId
405+
attributes:attributes];
406+
407+
if ([Optimizely isEmptyDictionary:impressionEventParams]) {
408+
return nil;
409+
}
410+
411+
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesEventDispatcherAttemptingToSendImpressionEvent, userId, experiment.experimentKey];
412+
[self.logger logMessage:logMessage withLevel:OptimizelyLogLevelInfo];
413+
414+
__weak typeof(self) weakSelf = self;
415+
[self.eventDispatcher dispatchImpressionEvent:impressionEventParams
416+
callback:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
417+
if (!error) {
418+
NSString *logMessage = [NSString stringWithFormat:OPTLYLoggerMessagesEventDispatcherActivationSuccess, userId, experiment.experimentKey];
419+
[weakSelf.logger logMessage:logMessage
420+
withLevel:OptimizelyLogLevelInfo];
421+
}
422+
if (callback) {
423+
callback(error);
424+
}
425+
}];
426+
return variation;
427+
}
428+
429+
+ (BOOL)isEmptyString:(NSObject*)string {
430+
return (!string
431+
|| ![string isKindOfClass:[NSString class]]
432+
|| [(NSString *)string isEqualToString:@""]);
433+
}
434+
435+
+ (BOOL)isEmptyDictionary:(NSObject*)dict {
436+
return (!dict
437+
|| ![dict isKindOfClass:[NSDictionary class]]
438+
|| (((NSDictionary *)dict).count == 0));
439+
}
360440
@end

0 commit comments

Comments
 (0)