Skip to content

Commit 4711f4a

Browse files
authored
Merge pull request #43 from optimizely/josh.wang/user_profile/check_sticky_bucketing
Sticky Bucketing
2 parents e8d70e3 + 195a9e7 commit 4711f4a

File tree

17 files changed

+474
-20
lines changed

17 files changed

+474
-20
lines changed

OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ extern NSString *const OPTLYLoggerMessagesDataStoreDatabaseGetNumberEventsTVOSWa
122122
extern NSString *const OPTLYLoggerMessagesDataStoreDatabaseRemoveTVOSWarning;
123123

124124
// ---- User Profile ----
125+
// warning
126+
extern NSString *const OPTLYLoggerMessagesUserProfileVariationNoLongerInDatafile;
125127
// Debug
126128
extern NSString *const OPTLYLoggerMessagesUserProfileVariation;
127129
extern NSString *const OPTLYLoggerMessagesUserProfileNoVariation;

OptimizelySDKCore/OptimizelySDKCore/OPTLYLoggerMessages.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122
NSString *const OPTLYLoggerMessagesDataStoreDatabaseRemoveTVOSWarning = @"[DATABASE] tvOS can only remove data from cache> Number of events: %ld, eventType: %ld.";
123123

124124
// ---- User Profile ----
125+
//warning
126+
NSString *const OPTLYLoggerMessagesUserProfileVariationNoLongerInDatafile = @"Variation %@ for experiment %@ no longer found in datafile.";
125127
// Debug
126128
NSString *const OPTLYLoggerMessagesUserProfileVariation = @"Variation %@ for user %@, experiment %@ found.";
127129
NSString *const OPTLYLoggerMessagesUserProfileNoVariation = @"Variation for user %@, experiment %@ not found.";

OptimizelySDKCore/OptimizelySDKCore/Optimizely.m

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#import "OPTLYExperiment.h"
2929
#import "OPTLYLogger.h"
3030
#import "OPTLYProjectConfig.h"
31+
#import "OPTLYUserProfile.h"
3132
#import "OPTLYValidator.h"
3233
#import "OPTLYVariable.h"
3334
#import "OPTLYVariation.h"
@@ -141,11 +142,29 @@ - (OPTLYVariation *)getVariationForExperiment:(NSString *)experimentKey
141142
userId:(NSString *)userId
142143
attributes:(NSDictionary<NSString *,NSString *> *)attributes
143144
{
145+
if (self.userProfile != nil) {
146+
NSString *storedVariationKey = [self.userProfile getVariationForUser:userId experiment:experimentKey];
147+
if (storedVariationKey != nil) {
148+
OPTLYVariation *storedVariation = [[self.config getExperimentForKey:experimentKey]
149+
getVariationForVariationKey:storedVariationKey];
150+
if (storedVariation != nil) {
151+
return storedVariation;
152+
}
153+
else { // stored variation is no longer in datafile
154+
[self.userProfile removeUser:userId experiment:experimentKey];
155+
[self.logger logMessage:[NSString stringWithFormat:OPTLYLoggerMessagesUserProfileVariationNoLongerInDatafile, storedVariationKey, experimentKey]
156+
withLevel:OptimizelyLogLevelWarning];
157+
}
158+
}
159+
}
144160
OPTLYVariation *bucketedVariation = nil;
145161
bucketedVariation = [self.config getVariationForExperiment:experimentKey
146162
userId:userId
147163
attributes:attributes
148164
bucketer:self.bucketer];
165+
[self.userProfile saveUser:userId
166+
experiment:experimentKey
167+
variation:bucketedVariation.variationKey];
149168
return bucketedVariation;
150169
}
151170

OptimizelySDKCore/OptimizelySDKCore/OptimizelySDKCore.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#import "Optimizely.h"
1818
#import "OPTLYBucketer.h"
1919
#import "OPTLYBuilder.h"
20+
#import "OPTLYDatafileManager.h"
2021
#import "OPTLYErrorHandler.h"
2122
#import "OPTLYErrorHandlerMessages.h"
2223
#import "OPTLYEventBuilder.h"

OptimizelySDKShared/OptimizelySDKShared/OPTLYClientBuilder.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
#import <Foundation/Foundation.h>
1818

1919
@class Optimizely, OPTLYProjectConfig, OPTLYBucketer, OPTLYEventBuilder, OPTLYEventBuilderDefault;
20-
@protocol OPTLYErrorHandler, OPTLYEventBuilder, OPTLYEventDispatcher, OPTLYLogger;
20+
@protocol OPTLYErrorHandler, OPTLYEventBuilder, OPTLYEventDispatcher, OPTLYLogger, OPTLYUserProfile;
2121

2222
/**
2323
* This class contains the informaation on how your Optimizely Client instance will be built.
@@ -39,6 +39,8 @@ typedef void (^OPTLYClientBuilderBlock)(OPTLYClientBuilder * _Nonnull builder);
3939
@property (nonatomic, readwrite, strong, nullable) id<OPTLYEventDispatcher> eventDispatcher;
4040
/// The logger is by default set to one that is created by Optimizely. This default logger can be overridden by any object that conforms to the OPTLYLogger protocol.
4141
@property (nonatomic, readwrite, strong, nullable) id<OPTLYLogger> logger;
42+
/// User profile to be used by the Optimizely instance to store user-specific data.
43+
@property (nonatomic, readwrite, strong, nullable) id<OPTLYUserProfile> userProfile;
4244

4345
/// Create an Optimizely Client object.
4446
+ (nonnull instancetype)builderWithBlock:(nonnull OPTLYClientBuilderBlock)block;

OptimizelySDKShared/OptimizelySDKShared/OPTLYClientBuilder.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ - (id)initWithBlock:(OPTLYClientBuilderBlock)block {
3737
builder.errorHandler = _errorHandler;
3838
builder.eventDispatcher = _eventDispatcher;
3939
builder.logger = _logger;
40+
builder.userProfile = _userProfile;
4041
}];
4142
_logger = _optimizely.logger;
4243
if (!_logger) {

OptimizelySDKShared/OptimizelySDKShared/OPTLYManager.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
@property (nonatomic, readonly, strong, nullable) id<OPTLYEventDispatcher> eventDispatcher;
3434
/// The logger to be used for the manager, client, and all subcomponents
3535
@property (nonatomic, readonly, strong, nullable) id<OPTLYLogger> logger;
36+
/// User profile to be used by the client to store user-specific data.
37+
@property (nonatomic, readonly, strong, nullable) id<OPTLYUserProfile> userProfile;
3638

3739
/**
3840
* Init with builder block

OptimizelySDKShared/OptimizelySDKShared/OPTLYManager.m

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,13 @@ - (instancetype)initWithBuilder:(OPTLYManagerBuilder *)builder {
4747
withLevel:OptimizelyLogLevelError];
4848
return nil;
4949
}
50-
_projectId = builder.projectId;
5150
_datafile = builder.datafile;
51+
_datafileManager = builder.datafileManager;
5252
_errorHandler = builder.errorHandler;
5353
_eventDispatcher = builder.eventDispatcher;
5454
_logger = builder.logger;
55-
// initialize datafile manager
56-
_datafileManager = builder.datafileManager;
57-
// TODO: Josh W. initialize event dispatcher
58-
// TODO: Josh W. initialize user experiment record
55+
_projectId = builder.projectId;
56+
_userProfile = builder.userProfile;
5957
}
6058
return self;
6159
}
@@ -80,19 +78,15 @@ - (instancetype)initWithBuilder:(OPTLYManagerBuilder *)builder {
8078
}
8179

8280
- (OPTLYClient *)initializeClient {
83-
OPTLYClient *client = [OPTLYClient initWithBuilderBlock:^(OPTLYClientBuilder * _Nonnull builder) {
84-
builder.datafile = self.datafile;
85-
}];
81+
OPTLYClient *client = [self initializeClientWithManagerSettingsAndDatafile:self.datafile];
8682
if (client.optimizely != nil) {
8783
self.optimizelyClient = client;
8884
}
8985
return client;
9086
}
9187

9288
- (OPTLYClient *)initializeClientWithDatafile:(NSData *)datafile {
93-
OPTLYClient *client = [OPTLYClient initWithBuilderBlock:^(OPTLYClientBuilder * _Nonnull builder) {
94-
builder.datafile = datafile;
95-
}];
89+
OPTLYClient *client = [self initializeClientWithManagerSettingsAndDatafile:datafile];
9690
if (client.optimizely != nil) {
9791
self.optimizelyClient = client;
9892
return client;
@@ -104,11 +98,8 @@ - (OPTLYClient *)initializeClientWithDatafile:(NSData *)datafile {
10498

10599
- (void)initializeClientWithCallback:(void (^)(NSError * _Nullable, OPTLYClient * _Nullable))callback {
106100
[self.datafileManager downloadDatafile:self.projectId completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
107-
OPTLYClient *client = nil;
108101
if (!error) {
109-
client = [OPTLYClient initWithBuilderBlock:^(OPTLYClientBuilder * _Nonnull builder) {
110-
builder.datafile = data;
111-
}];
102+
OPTLYClient *client = [self initializeClientWithManagerSettingsAndDatafile:data];
112103
if (client.optimizely) {
113104
self.optimizelyClient = client;
114105
}
@@ -117,7 +108,7 @@ - (void)initializeClientWithCallback:(void (^)(NSError * _Nullable, OPTLYClient
117108
}
118109

119110
if (callback) {
120-
callback(error, client);
111+
callback(error, self.optimizelyClient);
121112
}
122113
}];
123114
}
@@ -126,4 +117,15 @@ - (OPTLYClient *)getOptimizely {
126117
return self.optimizelyClient;
127118
}
128119

120+
- (OPTLYClient *)initializeClientWithManagerSettingsAndDatafile:(NSData *)datafile {
121+
OPTLYClient *client = [OPTLYClient initWithBuilderBlock:^(OPTLYClientBuilder * _Nonnull builder) {
122+
builder.datafile = datafile;
123+
builder.errorHandler = self.errorHandler;
124+
builder.eventDispatcher = self.eventDispatcher;
125+
builder.logger = self.logger;
126+
builder.userProfile = self.userProfile;
127+
}];
128+
return client;
129+
}
130+
129131
@end

OptimizelySDKShared/OptimizelySDKShared/OPTLYManagerBuilder.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-
@protocol OPTLYDatafileManager, OPTLYErrorHandler, OPTLYEventDispatcher, OPTLYLogger;
19+
@protocol OPTLYDatafileManager, OPTLYErrorHandler, OPTLYEventDispatcher, OPTLYLogger, OPTLYUserProfile;
2020
@class OPTLYManagerBuilder;
2121

2222
typedef void (^OPTLYManagerBuilderBlock)(OPTLYManagerBuilder * _Nullable builder);
@@ -37,6 +37,8 @@ typedef void (^OPTLYManagerBuilderBlock)(OPTLYManagerBuilder * _Nullable builder
3737
@property (nonatomic, readwrite, strong, nullable) id<OPTLYEventDispatcher> eventDispatcher;
3838
/// The logger to be used for the manager, client, and all subcomponents
3939
@property (nonatomic, readwrite, strong, nullable) id<OPTLYLogger> logger;
40+
/// User profile to be used by the client to store user-specific data.
41+
@property (nonatomic, readwrite, strong, nullable) id<OPTLYUserProfile> userProfile;
4042

4143
/// Create the Optimizely Manager object.
4244
+ (nullable instancetype)builderWithBlock:(nonnull OPTLYManagerBuilderBlock)block;

OptimizelySDKShared/OptimizelySDKSharedTests/OPTLYManagerTest.m

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

1717
#import <XCTest/XCTest.h>
1818
#import <OHHTTPStubs/OHHTTPStubs.h>
19-
#import <OptimizelySDKCore/OPTLYDatafileManager.h>
19+
20+
#import <OptimizelySDKCore/OptimizelySDKCore.h>
2021
#import <OptimizelySDKCore/OPTLYNetworkService.h>
2122
#import <OptimizelySDKCore/OPTLYProjectConfig.h>
2223
#import "OPTLYClient.h"
@@ -49,6 +50,39 @@ - (void)tearDown {
4950
self.alternateDatafile = nil;
5051
}
5152

53+
- (void)testInitializationSettingsGetPropogatedToClientAndCore {
54+
// initialize manager settings
55+
id<OPTLYDatafileManager> datafileManager = [[OPTLYDatafileManagerNoOp alloc] init];
56+
id<OPTLYErrorHandler> errorHandler = [[OPTLYErrorHandlerNoOp alloc] init];
57+
id<OPTLYEventDispatcher> eventDispatcher = [[OPTLYEventDispatcherDefault alloc] init];
58+
id<OPTLYLogger> logger = [[OPTLYLoggerDefault alloc] initWithLogLevel:OptimizelyLogLevelOff];
59+
id<OPTLYUserProfile> userProfile = [[OPTLYUserProfileNoOp alloc] init];
60+
61+
// initialize Manager
62+
OPTLYManager *manager = [OPTLYManager initWithBuilderBlock:^(OPTLYManagerBuilder * _Nullable builder) {
63+
builder.datafile = self.defaultDatafile;
64+
builder.datafileManager = datafileManager;
65+
builder.errorHandler = errorHandler;
66+
builder.eventDispatcher = eventDispatcher;
67+
builder.logger = logger;
68+
builder.projectId = kProjectId;
69+
builder.userProfile = userProfile;
70+
}];
71+
XCTAssertEqual(manager.datafileManager, datafileManager);
72+
73+
// get the client
74+
OPTLYClient *client = [manager initializeClient];
75+
XCTAssertEqual(client.logger, logger);
76+
77+
// check optimizely core has been initialized correctly
78+
Optimizely *optimizely = client.optimizely;
79+
XCTAssertNotNil(optimizely);
80+
XCTAssertEqual(optimizely.errorHandler, errorHandler);
81+
XCTAssertEqual(optimizely.eventDispatcher, eventDispatcher);
82+
XCTAssertEqual(optimizely.logger, logger);
83+
XCTAssertEqual(optimizely.userProfile, userProfile);
84+
}
85+
5286
- (void)testInitializeClientWithoutDatafileReturnsDummy {
5387
// initialize manager without datafile
5488
OPTLYManager *manager = [OPTLYManager initWithBuilderBlock:^(OPTLYManagerBuilder * _Nullable builder) {

0 commit comments

Comments
 (0)