1
1
<?php
2
2
/**
3
- * Copyright 2017, Optimizely
3
+ * Copyright 2017, Optimizely Inc and Contributors
4
4
*
5
5
* Licensed under the Apache License, Version 2.0 (the "License");
6
6
* you may not use this file except in compliance with the License.
16
16
*/
17
17
namespace Optimizely \DecisionService ;
18
18
19
+ use Exception ;
19
20
use Monolog \Logger ;
20
21
use Optimizely \Bucketer ;
21
22
use Optimizely \Entity \Experiment ;
22
23
use Optimizely \Entity \Variation ;
23
24
use Optimizely \Logger \LoggerInterface ;
24
25
use Optimizely \ProjectConfig ;
26
+ use Optimizely \UserProfile \Decision ;
27
+ use Optimizely \UserProfile \UserProfileServiceInterface ;
28
+ use Optimizely \UserProfile \UserProfile ;
29
+ use Optimizely \UserProfile \UserProfileUtils ;
25
30
use Optimizely \Utils \Validator ;
26
31
27
32
/**
28
33
* Optimizely's decision service that determines which variation of an experiment the user will be allocated to.
29
34
*
30
35
* The decision service contains all logic around how a user decision is made. This includes all of the following (in order):
31
- * 1. Checking experiment status
32
- * 2. Checking whitelisting
33
- * 3. Checking audience targeting
34
- * 4. Using Murmurhash3 to bucket the user.
36
+ * 1. Checking experiment status.
37
+ * 2. Checking whitelisting.
38
+ * 3. Check sticky bucketing.
39
+ * 4. Checking audience targeting.
40
+ * 5. Using Murmurhash3 to bucket the user.
35
41
*
36
42
* @package Optimizely
37
43
*/
@@ -52,16 +58,22 @@ class DecisionService
52
58
*/
53
59
private $ _bucketer ;
54
60
61
+ /**
62
+ * @var UserProfileServiceInterface
63
+ */
64
+ private $ _userProfileService ;
65
+
55
66
/**
56
67
* DecisionService constructor.
57
68
* @param LoggerInterface $logger
58
69
* @param ProjectConfig $projectConfig
59
70
*/
60
- public function __construct (LoggerInterface $ logger , ProjectConfig $ projectConfig )
71
+ public function __construct (LoggerInterface $ logger , ProjectConfig $ projectConfig, UserProfileServiceInterface $ userProfileService = null )
61
72
{
62
73
$ this ->_logger = $ logger ;
63
74
$ this ->_projectConfig = $ projectConfig ;
64
75
$ this ->_bucketer = new Bucketer ($ logger );
76
+ $ this ->_userProfileService = $ userProfileService ;
65
77
}
66
78
67
79
/**
@@ -85,6 +97,19 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null
85
97
return $ variation ;
86
98
}
87
99
100
+ // check for sticky bucketing
101
+ $ userProfile = new UserProfile ($ userId );
102
+ if (!is_null ($ this ->_userProfileService )) {
103
+ $ storedUserProfile = $ this ->getStoredUserProfile ($ userId );
104
+ if (!is_null ($ storedUserProfile )) {
105
+ $ userProfile = $ storedUserProfile ;
106
+ $ variation = $ this ->getStoredVariation ($ experiment , $ userProfile );
107
+ if (!is_null ($ variation )) {
108
+ return $ variation ;
109
+ }
110
+ }
111
+ }
112
+
88
113
if (!Validator::isUserInExperiment ($ this ->_projectConfig , $ experiment , $ attributes )) {
89
114
$ this ->_logger ->log (
90
115
Logger::INFO ,
@@ -94,6 +119,9 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null
94
119
}
95
120
96
121
$ variation = $ this ->_bucketer ->bucket ($ this ->_projectConfig , $ experiment , $ userId );
122
+ if (!is_null ($ variation )) {
123
+ $ this ->saveVariation ($ experiment , $ variation , $ userProfile );
124
+ }
97
125
return $ variation ;
98
126
}
99
127
@@ -122,4 +150,123 @@ private function getWhitelistedVariation(Experiment $experiment, $userId)
122
150
}
123
151
return null ;
124
152
}
153
+
154
+ /**
155
+ * Get the stored user profile for the given user ID.
156
+ *
157
+ * @param $userId string the ID of the user.
158
+ *
159
+ * @return null|UserProfile the stored user profile.
160
+ */
161
+ private function getStoredUserProfile ($ userId )
162
+ {
163
+ if (is_null ($ this ->_userProfileService )) {
164
+ return null ;
165
+ }
166
+
167
+ try {
168
+ $ userProfileMap = $ this ->_userProfileService ->lookup ($ userId );
169
+ if (is_null ($ userProfileMap )) {
170
+ $ this ->_logger ->log (
171
+ Logger::INFO ,
172
+ sprintf ('No user profile found for user with ID "%s". ' , $ userId )
173
+ );
174
+ } else if (UserProfileUtils::isValidUserProfileMap ($ userProfileMap )) {
175
+ return UserProfileUtils::convertMapToUserProfile ($ userProfileMap );
176
+ } else {
177
+ $ this ->_logger ->log (
178
+ Logger::WARNING ,
179
+ 'The User Profile Service returned an invalid user profile map. '
180
+ );
181
+ }
182
+ } catch (Exception $ e ) {
183
+ $ this ->_logger ->log (
184
+ Logger::ERROR ,
185
+ sprintf ('The User Profile Service lookup method failed: %s. ' , $ e ->getMessage ())
186
+ );
187
+ }
188
+
189
+ return null ;
190
+ }
191
+
192
+ /**
193
+ * Get the stored variation for the given experiment from the user profile.
194
+ *
195
+ * @param $experiment Experiment The experiment for which we are getting the stored variation.
196
+ * @param $userProfile UserProfile The user profile from which we are getting the stored variation.
197
+ *
198
+ * @return null|Variation the stored variation or null if not found.
199
+ */
200
+ private function getStoredVariation (Experiment $ experiment , UserProfile $ userProfile )
201
+ {
202
+ $ experimentKey = $ experiment ->getKey ();
203
+ $ userId = $ userProfile ->getUserId ();
204
+ $ variationId = $ userProfile ->getVariationForExperiment ($ experiment ->getId ());
205
+
206
+ if (is_null ($ variationId )) {
207
+ $ this ->_logger ->log (
208
+ Logger::INFO ,
209
+ sprintf ('No previously activated variation of experiment "%s" for user "%s" found in user profile. ' , $ experimentKey , $ userId )
210
+ );
211
+ return null ;
212
+ }
213
+
214
+ if (!$ this ->_projectConfig ->isVariationIdValid ($ experimentKey , $ variationId )) {
215
+ $ this ->_logger ->log (
216
+ Logger::INFO ,
217
+ sprintf ('User "%s" was previously bucketed into variation with ID "%s" for experiment "%s", but no matching variation was found for that user. We will re-bucket the user. ' ,
218
+ $ userId , $ variationId , $ experimentKey )
219
+ );
220
+ return null ;
221
+ }
222
+
223
+ $ variation = $ this ->_projectConfig ->getVariationFromId ($ experimentKey , $ variationId );
224
+ $ this ->_logger ->log (
225
+ Logger::INFO ,
226
+ sprintf ('Returning previously activated variation "%s" of experiment "%s" for user "%s" from user profile. ' ,
227
+ $ variation ->getKey (), $ experimentKey , $ userId )
228
+ );
229
+ return $ variation ;
230
+ }
231
+
232
+ /**
233
+ * Save the given variation assignment to the given user profile.
234
+ *
235
+ * @param $experiment Experiment Experiment for which we are storing the variation.
236
+ * @param $variation Variation Variation the user is bucketed into.
237
+ * @param $userProfile UserProfile User profile object to which we are persisting the variation assignment.
238
+ */
239
+ private function saveVariation (Experiment $ experiment , Variation $ variation , UserProfile $ userProfile )
240
+ {
241
+ if (is_null ($ this ->_userProfileService )) {
242
+ return ;
243
+ }
244
+
245
+ $ experimentId = $ experiment ->getId ();
246
+ $ decision = $ userProfile ->getDecisionForExperiment ($ experimentId );
247
+ $ variationId = $ variation ->getId ();
248
+ if (is_null ($ decision )) {
249
+ $ decision = new Decision ($ variationId );
250
+ } else {
251
+ $ decision ->setVariationId ($ variationId );
252
+ }
253
+
254
+ $ userProfile ->saveDecisionForExperiment ($ experimentId , $ decision );
255
+ $ userProfileMap = UserProfileUtils::convertUserProfileToMap ($ userProfile );
256
+
257
+ try {
258
+ $ this ->_userProfileService ->save ($ userProfileMap );
259
+ $ this ->_logger ->log (
260
+ Logger::INFO ,
261
+ sprintf ('Saved variation "%s" of experiment "%s" for user "%s". ' ,
262
+ $ variation ->getKey (), $ experiment ->getKey (), $ userProfile ->getUserId ())
263
+ );
264
+ } catch (Exception $ e ) {
265
+ $ this ->_logger ->log (
266
+ Logger::WARNING ,
267
+ sprintf ('Failed to save variation "%s" of experiment "%s" for user "%s". ' ,
268
+ $ variation ->getKey (), $ experiment ->getKey (), $ userProfile ->getUserId ())
269
+ );
270
+ }
271
+ }
125
272
}
0 commit comments