11<?php
22/**
3- * Copyright 2017, Optimizely
3+ * Copyright 2017, Optimizely Inc and Contributors
44 *
55 * Licensed under the Apache License, Version 2.0 (the "License");
66 * you may not use this file except in compliance with the License.
1616 */
1717namespace Optimizely \DecisionService ;
1818
19+ use Exception ;
1920use Monolog \Logger ;
2021use Optimizely \Bucketer ;
2122use Optimizely \Entity \Experiment ;
2223use Optimizely \Entity \Variation ;
2324use Optimizely \Logger \LoggerInterface ;
2425use Optimizely \ProjectConfig ;
26+ use Optimizely \UserProfile \Decision ;
27+ use Optimizely \UserProfile \UserProfileServiceInterface ;
28+ use Optimizely \UserProfile \UserProfile ;
29+ use Optimizely \UserProfile \UserProfileUtils ;
2530use Optimizely \Utils \Validator ;
2631
2732/**
2833 * Optimizely's decision service that determines which variation of an experiment the user will be allocated to.
2934 *
3035 * 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.
3541 *
3642 * @package Optimizely
3743 */
@@ -52,16 +58,22 @@ class DecisionService
5258 */
5359 private $ _bucketer ;
5460
61+ /**
62+ * @var UserProfileServiceInterface
63+ */
64+ private $ _userProfileService ;
65+
5566 /**
5667 * DecisionService constructor.
5768 * @param LoggerInterface $logger
5869 * @param ProjectConfig $projectConfig
5970 */
60- public function __construct (LoggerInterface $ logger , ProjectConfig $ projectConfig )
71+ public function __construct (LoggerInterface $ logger , ProjectConfig $ projectConfig, UserProfileServiceInterface $ userProfileService = null )
6172 {
6273 $ this ->_logger = $ logger ;
6374 $ this ->_projectConfig = $ projectConfig ;
6475 $ this ->_bucketer = new Bucketer ($ logger );
76+ $ this ->_userProfileService = $ userProfileService ;
6577 }
6678
6779 /**
@@ -85,6 +97,19 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null
8597 return $ variation ;
8698 }
8799
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+
88113 if (!Validator::isUserInExperiment ($ this ->_projectConfig , $ experiment , $ attributes )) {
89114 $ this ->_logger ->log (
90115 Logger::INFO ,
@@ -94,6 +119,9 @@ public function getVariation(Experiment $experiment, $userId, $attributes = null
94119 }
95120
96121 $ variation = $ this ->_bucketer ->bucket ($ this ->_projectConfig , $ experiment , $ userId );
122+ if (!is_null ($ variation )) {
123+ $ this ->saveVariation ($ experiment , $ variation , $ userProfile );
124+ }
97125 return $ variation ;
98126 }
99127
@@ -122,4 +150,123 @@ private function getWhitelistedVariation(Experiment $experiment, $userId)
122150 }
123151 return null ;
124152 }
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+ }
125272}
0 commit comments