This is an automated email from the ASF dual-hosted git repository.

jkevan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new 40130ee8d UNOMI-690, UNOMI-696: refactor control group (#531)
40130ee8d is described below

commit 40130ee8d498ea07ffd1f8b40a1e38671f6197a1
Author: kevan Jahanshahi <jke...@apache.org>
AuthorDate: Thu Nov 10 10:50:04 2022 +0100

    UNOMI-690, UNOMI-696: refactor control group (#531)
    
    * UNOMI-690, UNOMI-696: refactor control group
    
    * UNOMI-690, UNOMI-696: Fix itests
    
    * UNOMI-690, UNOMI-696: Add new control group integration test and cover 
the feature with strong integration tests
---
 .../java/org/apache/unomi/api/ContextResponse.java |  40 +-
 .../apache/unomi/api/PersonalizationResult.java    |  72 +-
 .../apache/unomi/api/PersonalizationStrategy.java  |   4 +-
 .../main/java/org/apache/unomi/api/Profile.java    |   2 +-
 .../main/java/org/apache/unomi/api/Session.java    |   2 +-
 ...zationResult.java => SystemPropertiesItem.java} |  29 +-
 .../org/apache/unomi/itests/ContextServletIT.java  | 338 ++++---
 .../resources/personalization-control-group.json   |  49 +
 .../resources/personalization-controlgroup.json    | 988 ---------------------
 .../personalization-no-control-group.json          |  45 +
 .../unomi/rest/endpoints/ContextJsonEndpoint.java  |   4 +-
 .../impl/personalization/ControlGroup.java         | 101 ---
 .../PersonalizationServiceImpl.java                |  90 +-
 .../sorts/ControlGroupPersonalizationStrategy.java | 117 +++
 .../sorts/FilterPersonalizationStrategy.java       |   5 +-
 .../sorts/RandomPersonalizationStrategy.java       |   8 +-
 .../sorts/ScorePersonalizationStrategy.java        |  15 +-
 17 files changed, 598 insertions(+), 1311 deletions(-)

diff --git a/api/src/main/java/org/apache/unomi/api/ContextResponse.java 
b/api/src/main/java/org/apache/unomi/api/ContextResponse.java
index b7467e018..2eaf098f3 100644
--- a/api/src/main/java/org/apache/unomi/api/ContextResponse.java
+++ b/api/src/main/java/org/apache/unomi/api/ContextResponse.java
@@ -20,11 +20,10 @@ package org.apache.unomi.api;
 import org.apache.unomi.api.conditions.Condition;
 import org.apache.unomi.api.services.RulesService;
 
+import javax.xml.bind.annotation.XmlTransient;
 import java.io.Serializable;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * A context server response resulting from the evaluation of a client's 
context request. Note that all returned values result of the evaluation of the 
data provided in the
@@ -52,7 +51,7 @@ public class ContextResponse implements Serializable {
 
     private int processedEvents;
 
-    private Map<String, List<String>> personalizations;
+    private Map<String, PersonalizationResult> personalizationResults;
 
     private Set<Condition> trackedConditions;
 
@@ -198,12 +197,39 @@ public class ContextResponse implements Serializable {
         this.processedEvents = processedEvents;
     }
 
+    /**
+     * @deprecated personalizations results are more complex since 2.1.0 and 
they are now available under: getPersonalizationResults()
+     */
+    @Deprecated
+    @XmlTransient
     public Map<String, List<String>> getPersonalizations() {
-        return personalizations;
+        if (personalizationResults != null) {
+            return personalizationResults.entrySet()
+                    .stream()
+                    .collect(Collectors.toMap(Map.Entry::getKey,
+                            entry -> entry.getValue().getContentIds()));
+        }
+        return null;
     }
 
+    /**
+     * @deprecated personalizations results are more complex since 2.1.0 and 
they are now available under: setPersonalizationResults(Map<String, 
PersonalizationResult> personalizationResults)
+     */
+    @Deprecated
     public void setPersonalizations(Map<String, List<String>> 
personalizations) {
-        this.personalizations = personalizations;
+        // do nothing, use setPersonalizationResults() instead
+    }
+
+    /**
+     * Get the result of the personalization resolutions done during the 
context request.
+     * @return a Map key/value pair (key:personalization id, value:the result 
that contains the matching content ids and additional information)
+     */
+    public Map<String, PersonalizationResult> getPersonalizationResults() {
+        return personalizationResults;
+    }
+
+    public void setPersonalizationResults(Map<String, PersonalizationResult> 
personalizationResults) {
+        this.personalizationResults = personalizationResults;
     }
 
     /**
diff --git a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java 
b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
index 446189a9a..15c6ab5a5 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
+++ b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
@@ -16,27 +16,89 @@
  */
 package org.apache.unomi.api;
 
+import org.apache.unomi.api.services.EventService;
+
+import javax.xml.bind.annotation.XmlTransient;
+import java.io.Serializable;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * A class to contain the result of a personalization, containing the list of 
content IDs as well as a changeType to
- * indicate if a profile and/or a session was modified (to store control group 
information).
+ * indicate if a profile and/or a session was modified.
  */
-public class PersonalizationResult {
+public class PersonalizationResult implements Serializable  {
+
+    public final static String ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP = 
"inControlGroup";
 
     List<String> contentIds;
-    int changeType;
 
-    public PersonalizationResult(List<String> contentIds, int changeType) {
+    Map<String, Object> additionalResultInfos = new HashMap<>();
+
+    int changeType = EventService.NO_CHANGE;
+
+    public PersonalizationResult() {
+    }
+
+    public PersonalizationResult(List<String> contentIds) {
         this.contentIds = contentIds;
-        this.changeType = changeType;
     }
 
+    /**
+     * List of matching ids for current personalization
+     * @return the list of matching ids
+     */
     public List<String> getContentIds() {
         return contentIds;
     }
 
+    public void setContentIds(List<String> contentIds) {
+        this.contentIds = contentIds;
+    }
+
+    /**
+     * Useful open map to return additional result information to the client
+     * @return map of key/value pair for additional information, like: 
inControlGroup
+     */
+    public Map<String, Object> getAdditionalResultInfos() {
+        return additionalResultInfos;
+    }
+
+    public void setAdditionalResultInfos(Map<String, Object> 
additionalResultInfos) {
+        this.additionalResultInfos = additionalResultInfos;
+    }
+
+    /**
+     * Is the current personalization result in a control group ?
+     * Control group are used to identify a profile or a session that should 
not get personalized results,
+     * instead the current profile/session should get a specific result 
(usually the same for all peoples falling in control group)
+     * Note: it's for now the responsibility of the client to decide what to 
do when the current personalization is under control group.
+     *
+     * @return true in case current profile or session is in control group for 
the personalization.
+     */
+    @XmlTransient
+    public boolean isInControlGroup() {
+        return 
additionalResultInfos.containsKey(ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP) &&
+                (Boolean) 
additionalResultInfos.get(ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP);
+    }
+
+    public void setInControlGroup(boolean inControlGroup) {
+        
this.additionalResultInfos.put(ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP, 
inControlGroup);
+    }
+
+    /**
+     * Change code in case the personalization resolution modified the profile 
or the session
+     * Only used internally, and will not be serialized either for storage or 
response payload.
+     *
+     * @return change code
+     */
+    @XmlTransient
     public int getChangeType() {
         return changeType;
     }
+
+    public void addChanges(int changes) {
+        this.changeType |= changes;
+    }
 }
diff --git 
a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java 
b/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
index 152625e3f..5ceb941f7 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
+++ b/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
@@ -32,7 +32,7 @@ public interface PersonalizationStrategy {
      * @param session the session to use for the personalization
      * @param personalizationRequest the request contains the contents to 
personalizes as well as the parameters for the
      *                               strategy (options)
-     * @return a list of content IDs resulting from the filtering/re-ordering
+     * @return the personalization result that contains the list of content 
IDs resulting from the filtering/re-ordering
      */
-    List<String> personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest);
+    PersonalizationResult personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest);
 }
diff --git a/api/src/main/java/org/apache/unomi/api/Profile.java 
b/api/src/main/java/org/apache/unomi/api/Profile.java
index ea9583850..133fc7599 100644
--- a/api/src/main/java/org/apache/unomi/api/Profile.java
+++ b/api/src/main/java/org/apache/unomi/api/Profile.java
@@ -38,7 +38,7 @@ import java.util.*;
  *
  * @see Segment
  */
-public class Profile extends Item {
+public class Profile extends Item implements SystemPropertiesItem {
 
     /**
      * The Profile ITEM_TYPE
diff --git a/api/src/main/java/org/apache/unomi/api/Session.java 
b/api/src/main/java/org/apache/unomi/api/Session.java
index 986323c12..8ca067720 100644
--- a/api/src/main/java/org/apache/unomi/api/Session.java
+++ b/api/src/main/java/org/apache/unomi/api/Session.java
@@ -27,7 +27,7 @@ import java.util.Map;
  * A time-bounded interaction between a user (via their associated {@link 
Profile}) and a unomi-enabled application. A session represents a sequence of 
operations the user
  * performed during its duration. In the context of web applications, sessions 
are usually linked to HTTP sessions.
  */
-public class Session extends Item implements TimestampedItem {
+public class Session extends Item implements TimestampedItem, 
SystemPropertiesItem {
 
     /**
      * The Session ITEM_TYPE.
diff --git a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java 
b/api/src/main/java/org/apache/unomi/api/SystemPropertiesItem.java
similarity index 57%
copy from api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
copy to api/src/main/java/org/apache/unomi/api/SystemPropertiesItem.java
index 446189a9a..e191562cc 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
+++ b/api/src/main/java/org/apache/unomi/api/SystemPropertiesItem.java
@@ -16,27 +16,18 @@
  */
 package org.apache.unomi.api;
 
-import java.util.List;
+import java.util.Map;
 
 /**
- * A class to contain the result of a personalization, containing the list of 
content IDs as well as a changeType to
- * indicate if a profile and/or a session was modified (to store control group 
information).
+ * An Item that is holding system properties.
  */
-public class PersonalizationResult {
+public interface SystemPropertiesItem {
 
-    List<String> contentIds;
-    int changeType;
-
-    public PersonalizationResult(List<String> contentIds, int changeType) {
-        this.contentIds = contentIds;
-        this.changeType = changeType;
-    }
-
-    public List<String> getContentIds() {
-        return contentIds;
-    }
-
-    public int getChangeType() {
-        return changeType;
-    }
+    /**
+     * Retrieves a Map of system property name - value pairs for this item. 
System properties can be used by implementations to store non-user visible 
properties needed for
+     * internal purposes.
+     *
+     * @return a Map of system property name - value pairs for this item
+     */
+    Map<String, Object> getSystemProperties();
 }
diff --git a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java 
b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
index cfa9275cd..c27a926d7 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
@@ -17,28 +17,14 @@
 
 package org.apache.unomi.itests;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.ContentType;
 import org.apache.http.entity.StringEntity;
-import org.apache.unomi.api.ContextRequest;
-import org.apache.unomi.api.ContextResponse;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.Scope;
-import org.apache.unomi.api.Session;
+import org.apache.unomi.api.*;
 import org.apache.unomi.api.conditions.Condition;
 import org.apache.unomi.api.segments.Scoring;
 import org.apache.unomi.api.segments.Segment;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.EventService;
-import org.apache.unomi.api.services.ProfileService;
-import org.apache.unomi.api.services.ScopeService;
-import org.apache.unomi.api.services.SegmentService;
 import org.apache.unomi.persistence.spi.CustomObjectMapper;
-import org.apache.unomi.persistence.spi.PersistenceService;
-import org.apache.unomi.schema.api.SchemaService;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -46,11 +32,8 @@ import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.PaxExam;
 import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
 import org.ops4j.pax.exam.spi.reactors.PerSuite;
-import org.ops4j.pax.exam.util.Filter;
 
-import javax.inject.Inject;
 import java.io.File;
-import java.io.IOException;
 import java.net.URI;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
@@ -70,7 +53,6 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
 
 /**
  * Created by Ron Barabash on 5/4/2020.
@@ -86,7 +68,9 @@ public class ContextServletIT extends BaseIT {
     private final static String TEST_EVENT_TYPE_SCHEMA = 
"schemas/events/test-event-type.json";
     private final static String FLOAT_PROPERTY_EVENT_TYPE = 
"floatPropertyType";
     private final static String FLOAT_PROPERTY_EVENT_TYPE_SCHEMA = 
"schemas/events/float-property-type.json";
+    private final static String TEST_SESSION_ID = "dummy-session";
     private final static String TEST_PROFILE_ID = "test-profile-id";
+    private final static String TEST_PROFILE_FIRST_NAME = 
"contextServletIT_profile";
 
     private final static String SEGMENT_ID = "test-segment-id";
     private final static int SEGMENT_NUMBER_OF_DAYS = 30;
@@ -113,6 +97,7 @@ public class ContextServletIT extends BaseIT {
         segmentService.setSegmentDefinition(segment);
 
         profile = new Profile(TEST_PROFILE_ID);
+        profile.setProperty("firstName", TEST_PROFILE_FIRST_NAME);
         profileService.save(profile);
 
         keepTrying("Profile " + TEST_PROFILE_ID + " not found in the required 
time", () -> profileService.load(TEST_PROFILE_ID),
@@ -138,6 +123,7 @@ public class ContextServletIT extends BaseIT {
         TestUtils.removeAllSessions(definitionsService, persistenceService);
         TestUtils.removeAllProfiles(definitionsService, persistenceService);
         profileService.delete(profile.getItemId(), false);
+        removeItems(Session.class);
         segmentService.removeSegmentDefinition(SEGMENT_ID, false);
 
         // cleanup schemas
@@ -560,6 +546,8 @@ public class ContextServletIT extends BaseIT {
         List<String> variants = 
contextResponse.getPersonalizations().get("perso-by-interest");
         assertEquals("Invalid response code", 200, response.getStatusCode());
         assertEquals("Perso should be empty, profile is empty", 0, 
variants.size());
+        variants = 
contextResponse.getPersonalizationResults().get("perso-by-interest").getContentIds();
+        assertEquals("Perso should be empty, profile is empty", 0, 
variants.size());
 
         // set profile for matching
         Profile profile = profileService.load(TEST_PROFILE_ID);
@@ -577,6 +565,9 @@ public class ContextServletIT extends BaseIT {
         assertEquals("Invalid response code", 200, response.getStatusCode());
         assertEquals("Perso should contains the good number of variants", 1, 
variants.size());
         assertEquals("Variant is not the expected one", 
"matching-fishing-interests-custom-score-100-variant-expected-score-120", 
variants.get(0));
+        variants = 
contextResponse.getPersonalizationResults().get("perso-by-interest").getContentIds();
+        assertEquals("Perso should contains the good number of variants", 1, 
variants.size());
+        assertEquals("Variant is not the expected one", 
"matching-fishing-interests-custom-score-100-variant-expected-score-120", 
variants.get(0));
 
         // modify profile to add interests
         profile = profileService.load(TEST_PROFILE_ID);
@@ -617,87 +608,15 @@ public class ContextServletIT extends BaseIT {
         assertEquals("Variant is not the expected one", 
"matching-football-interests-variant-expected-score-51", variants.get(4));
         assertEquals("Variant is not the expected one", 
"matching-tennis-interests-variant-expected-score-31", variants.get(5));
         assertEquals("Variant is not the expected one", 
"not-matching-tennis-interests-custom-score-100-variant-expected-score-30", 
variants.get(6));
-    }
-
-    @Test
-    public void testPersonalizationWithControlGroup() throws Exception {
-
-        Map<String, String> parameters = new HashMap<>();
-        parameters.put("storeInSession", "false");
-        HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
-        request.setEntity(
-                new 
StringEntity(getValidatedBundleJSON("personalization-controlgroup.json", 
parameters), ContentType.APPLICATION_JSON));
-        TestUtils.RequestResponse response = 
TestUtils.executeContextJSONRequest(request);
-        assertEquals("Invalid response code", 200, response.getStatusCode());
-
-        ContextResponse contextResponse = response.getContextResponse();
-
-        Map<String, List<String>> personalizations = 
contextResponse.getPersonalizations();
-
-        validatePersonalizations(personalizations);
-
-        // let's check that the persisted profile has the control groups;
-        Map<String, Object> profileProperties = 
contextResponse.getProfileProperties();
-        List<Map<String, Object>> profileControlGroups = (List<Map<String, 
Object>>) profileProperties.get("unomiControlGroups");
-        assertControlGroups(profileControlGroups);
-
-        String profileId = contextResponse.getProfileId();
-        Profile updatedProfile = keepTrying("Profile not found", () -> 
profileService.load(profileId), Objects::nonNull,
-                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
-        profileControlGroups = (List<Map<String, Object>>) 
updatedProfile.getProperty("unomiControlGroups");
-        assertNotNull("Profile control groups not found in persisted profile", 
profileControlGroups);
-        assertControlGroups(profileControlGroups);
-
-        // now let's test with session storage
-        parameters.put("storeInSession", "true");
-        request = new HttpPost(getFullUrl(CONTEXT_URL));
-        request.setEntity(
-                new 
StringEntity(getValidatedBundleJSON("personalization-controlgroup.json", 
parameters), ContentType.APPLICATION_JSON));
-        response = TestUtils.executeContextJSONRequest(request);
-        assertEquals("Invalid response code", 200, response.getStatusCode());
-        contextResponse = response.getContextResponse();
-
-        personalizations = contextResponse.getPersonalizations();
-
-        validatePersonalizations(personalizations);
-
-        Map<String, Object> sessionProperties = 
contextResponse.getSessionProperties();
-        List<Map<String, Object>> sessionControlGroups = (List<Map<String, 
Object>>) sessionProperties.get("unomiControlGroups");
-        assertControlGroups(sessionControlGroups);
-
-        Session updatedSession = 
profileService.loadSession(contextResponse.getSessionId(), new Date());
-        sessionControlGroups = (List<Map<String, Object>>) 
updatedSession.getProperty("unomiControlGroups");
-        assertNotNull("Session control groups not found in persisted session", 
sessionControlGroups);
-        assertControlGroups(sessionControlGroups);
-    }
-
-    private void validatePersonalizations(Map<String, List<String>> 
personalizations) {
-        assertEquals("Personalizations don't have expected size", 2, 
personalizations.size());
-
-        List<String> perso1Contents = personalizations.get("perso1");
-        assertEquals("Perso 1 content list size doesn't match", 10, 
perso1Contents.size());
-        List<String> expectedPerso1Contents = new ArrayList<>();
-        expectedPerso1Contents.add("perso1content1");
-        expectedPerso1Contents.add("perso1content2");
-        expectedPerso1Contents.add("perso1content3");
-        expectedPerso1Contents.add("perso1content4");
-        expectedPerso1Contents.add("perso1content5");
-        expectedPerso1Contents.add("perso1content6");
-        expectedPerso1Contents.add("perso1content7");
-        expectedPerso1Contents.add("perso1content8");
-        expectedPerso1Contents.add("perso1content9");
-        expectedPerso1Contents.add("perso1content10");
-        assertEquals("Perso1 contents do not match", expectedPerso1Contents, 
perso1Contents);
-    }
-
-    private void assertControlGroups(List<Map<String, Object>> 
profileControlGroups) {
-        assertNotNull("Couldn't find control groups for profile", 
profileControlGroups);
-        assertTrue("Control group size should be 1", 
profileControlGroups.size() == 1);
-        Map<String, Object> controlGroup = profileControlGroups.get(0);
-        assertEquals("Invalid ID for control group", "perso1", 
controlGroup.get("id"));
-        assertEquals("Invalid path for control group", "/home/perso1.html", 
controlGroup.get("path"));
-        assertEquals("Invalid displayName for control group", "First perso", 
controlGroup.get("displayName"));
-        assertNotNull("Null timestamp for control group", 
controlGroup.get("timeStamp"));
+        variants = 
contextResponse.getPersonalizationResults().get("perso-by-interest").getContentIds();
+        assertEquals("Perso should contains the good number of variants", 7, 
variants.size());
+        assertEquals("Variant is not the expected one", 
"matching-fishing-interests-custom-score-100-variant-expected-score-120", 
variants.get(0));
+        assertEquals("Variant is not the expected one", 
"matching-football-cars-interests-variant-expected-score-91", variants.get(1));
+        assertEquals("Variant is not the expected one", 
"not-matching-football-cars-interests-variant-expected-score-90", 
variants.get(2));
+        assertEquals("Variant is not the expected one", 
"not-matching-tennis-fishing-interests-variant-expected-score-50", 
variants.get(3));
+        assertEquals("Variant is not the expected one", 
"matching-football-interests-variant-expected-score-51", variants.get(4));
+        assertEquals("Variant is not the expected one", 
"matching-tennis-interests-variant-expected-score-31", variants.get(5));
+        assertEquals("Variant is not the expected one", 
"not-matching-tennis-interests-custom-score-100-variant-expected-score-30", 
variants.get(6));
     }
 
     @Test
@@ -737,4 +656,223 @@ public class ContextServletIT extends BaseIT {
 
         segmentService.removeScoringDefinition(scoring.getItemId(), false);
     }
+
+    @Test
+    public void test_no_ControlGroup() throws Exception {
+        performPersonalizationWithControlGroup(
+                null,
+                Collections.singletonList("no-condition"),
+                false,
+                false,
+                null,
+                null);
+    }
+
+    @Test
+    public void test_in_ControlGroup_profile_stored() throws Exception {
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "100.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                true,
+                null);
+
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "0.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                true,
+                null);
+    }
+
+    @Test
+    public void test_in_ControlGroup_session_stored() throws Exception {
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "100.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                null,
+                true);
+
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "0.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                null,
+                true);
+    }
+
+    @Test
+    public void test_out_ControlGroup_profile_stored() throws Exception {
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "0.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                false,
+                null);
+
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "100.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                false,
+                null);
+    }
+
+    @Test
+    public void test_out_ControlGroup_session_stored() throws Exception {
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "0.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                null,
+                false);
+
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "100.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                null,
+                false);
+    }
+
+    @Test
+    public void test_advanced_ControlGroup_test() throws Exception {
+        // STEP 1: start with no control group
+        performPersonalizationWithControlGroup(
+                null,
+                Collections.singletonList("no-condition"),
+                false,
+                false,
+                null,
+                null);
+
+        // STEP 2: then enable control group stored in profile
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "100.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                true,
+                null);
+
+        // STEP 3: then re disable control group
+        performPersonalizationWithControlGroup(
+                null,
+                Collections.singletonList("no-condition"),
+                false,
+                false,
+                /* We can see we still have old control group check stored in 
the profile */ true,
+                null);
+
+        // STEP 4: then re-enable control group, but session scoped this time, 
with a 0 percentage
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "0.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                /* We can see we still have old control group check stored in 
the profile */ true,
+                /* And now we also have a status saved in the session */ 
false);
+
+        // STEP 5: then re-enable control group, but profile scoped this time, 
with a 0 percentage
+        //         We should be in control group because of the STEP 2, the 
current profile already contains a persisted status for the perso.
+        //         So even if current config is 0, old check already flagged 
current profile to be in the control group.
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "0.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                /* We can see we still have old control group check stored in 
the profile */ true,
+                /*  We can see we still have old control group check stored in 
the session too */ false);
+
+        // STEP 6: then re-enable control group, but session scoped this time, 
with a 100 percentage
+        //         We should not be in control group because of the STEP 4, 
the current session already contains a persisted status for the perso.
+        //         So even if current config is 100, old check already flagged 
current profile to not be in the control group.
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "100.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                /* We can see we still have old control group check stored in 
the profile */ true,
+                /*  We can see we still have old control group check stored in 
the session too */ false);
+
+        // STEP 7: then re disable control group
+        performPersonalizationWithControlGroup(
+                null,
+                Collections.singletonList("no-condition"),
+                false,
+                false,
+                /* We can see we still have old control group check stored in 
the profile */ true,
+                /*  We can see we still have old control group check stored in 
the session too */ false);
+    }
+
+    private void performPersonalizationWithControlGroup(Map<String, String> 
controlGroupConfig, List<String> expectedVariants,
+                                                        boolean 
expectedControlGroupInfoInPersoResult, boolean 
expectedControlGroupValueInPersoResult,
+                                                        Boolean 
expectedControlGroupValueInProfile, Boolean expectedControlGroupValueInSession) 
throws Exception {
+        // Test normal personalization should not have control group info in 
response
+
+        HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
+        if (controlGroupConfig != null) {
+            request.setEntity(new 
StringEntity(getValidatedBundleJSON("personalization-control-group.json", 
controlGroupConfig), ContentType.APPLICATION_JSON));
+        } else {
+            request.setEntity(new 
StringEntity(getValidatedBundleJSON("personalization-no-control-group.json", 
null), ContentType.APPLICATION_JSON));
+        }
+
+        TestUtils.RequestResponse response = 
TestUtils.executeContextJSONRequest(request);
+        ContextResponse contextResponse = response.getContextResponse();
+
+        // Check variants
+        List<String> variants = 
contextResponse.getPersonalizations().get("perso-control-group");
+        assertEquals("Invalid response code", 200, response.getStatusCode());
+        assertEquals("Perso should contains the good number of variants", 
expectedVariants.size(), variants.size());
+        for (int i = 0; i < expectedVariants.size(); i++) {
+            assertEquals("Variant is not the expected one", 
expectedVariants.get(i), variants.get(i));
+        }
+        PersonalizationResult personalizationResult = 
contextResponse.getPersonalizationResults().get("perso-control-group");
+        variants = personalizationResult.getContentIds();
+        assertEquals("Perso should contains the good number of variants", 
expectedVariants.size(), variants.size());
+        for (int i = 0; i < expectedVariants.size(); i++) {
+            assertEquals("Variant is not the expected one", 
expectedVariants.get(i), variants.get(i));
+        }
+        // Check control group info
+        assertEquals("Perso result should contains control group info", 
expectedControlGroupInfoInPersoResult, 
personalizationResult.getAdditionalResultInfos().containsKey(PersonalizationResult.ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP));
+        assertEquals("Perso should not be in control group then", 
expectedControlGroupValueInPersoResult, 
contextResponse.getPersonalizationResults().get("perso-control-group").isInControlGroup());
+
+        // Check control group state on profile
+        keepTrying("Incorrect control group on profile",
+                () -> profileService.load(TEST_PROFILE_ID), storedProfile -> 
expectedControlGroupValueInProfile == 
getPersistedControlGroupStatus(storedProfile, "perso-control-group"),
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        // Check control group state on session
+        keepTrying("Incorrect control group status on session",
+                () -> persistenceService.load(TEST_SESSION_ID, Session.class), 
storedSession -> expectedControlGroupValueInSession == 
getPersistedControlGroupStatus(storedSession, "perso-control-group"),
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+    }
+
+    private Boolean getPersistedControlGroupStatus(SystemPropertiesItem 
systemPropertiesItem, String personalizationId) {
+        if(systemPropertiesItem.getSystemProperties() != null && 
systemPropertiesItem.getSystemProperties().containsKey("personalizationStrategyStatus"))
 {
+            List<Map<String, Object>> personalizationStrategyStatus = 
(List<Map<String, Object>>) 
systemPropertiesItem.getSystemProperties().get("personalizationStrategyStatus");
+            for (Map<String, Object> strategyStatus : 
personalizationStrategyStatus) {
+                if 
(personalizationId.equals(strategyStatus.get("personalizationId"))) {
+                    return strategyStatus.containsKey("inControlGroup") && 
((boolean) strategyStatus.get("inControlGroup"));
+                }
+            }
+        }
+        return null;
+    }
+
+    private Map<String, String> generateControlGroupConfig(String 
storeInSession, String percentage) {
+        Map<String, String> controlGroupConfig = new HashMap<>();
+        controlGroupConfig.put("storeInSession", storeInSession);
+        controlGroupConfig.put("percentage", percentage);
+        return controlGroupConfig;
+    }
 }
diff --git a/itests/src/test/resources/personalization-control-group.json 
b/itests/src/test/resources/personalization-control-group.json
new file mode 100644
index 000000000..e7b07eb2b
--- /dev/null
+++ b/itests/src/test/resources/personalization-control-group.json
@@ -0,0 +1,49 @@
+{
+  "source": {
+    "itemId": "CMSServer",
+    "itemType": "custom",
+    "scope": "acme",
+    "properties": {}
+  },
+  "requireSegments": true,
+  "requiredProfileProperties": [
+    "*"
+  ],
+  "requiredSessionProperties": [
+    "*"
+  ],
+  "personalizations": [
+    {
+      "id": "perso-control-group",
+      "strategy": "matching-first",
+      "strategyOptions": {
+        "controlGroup": {
+          "storeInSession": ###storeInSession###,
+          "percentage": ###percentage###
+        }
+      },
+      "contents": [
+        {
+          "id": "first-name-missing",
+          "filters": [
+            {
+              "condition": {
+                "type": "profilePropertyCondition",
+                "parameterValues": {
+                  "propertyName": "properties.firstName",
+                  "comparisonOperator": "missing"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "id": "no-condition",
+          "filters": null
+        }
+      ]
+    }
+  ],
+  "sessionId": "dummy-session",
+  "profileId": "test-profile-id"
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/personalization-controlgroup.json 
b/itests/src/test/resources/personalization-controlgroup.json
deleted file mode 100644
index c4a1bcf98..000000000
--- a/itests/src/test/resources/personalization-controlgroup.json
+++ /dev/null
@@ -1,988 +0,0 @@
-{
-  "source": {
-    "itemId": "CMSServer",
-    "itemType": "custom",
-    "scope": "acme",
-    "properties": {}
-  },
-  "requireSegments": true,
-  "requiredProfileProperties": [
-    "unomiControlGroups"
-  ],
-  "requiredSessionProperties": [
-    "unomiControlGroups"
-  ],
-  "personalizations": [
-    {
-      "id": "perso1",
-      "strategy": "score-sorted",
-      "strategyOptions": {
-        "threshold": -1,
-        "controlGroup" : {
-          "percentage" : 100.0,
-          "displayName" : "First perso",
-          "path" : "/home/perso1.html",
-          "storeInSession" : ###storeInSession###
-        }
-      },
-      "contents": [
-        {
-          "id": "perso1content1",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ],
-          "properties": {
-            "interests": "health food"
-          }
-        },
-        {
-          "id": "perso1content2",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/contactus.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso1content3",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/documentation.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso1content4",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/aboutus.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso1content5",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/products.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso1content6",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/services.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso1content7",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/community.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso1content8",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/projects.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso1content9",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/home.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso1content10",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/theend.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        }
-      ]
-    },
-    {
-      "id": "perso2",
-      "strategy": "score-sorted",
-      "strategyOptions": {
-        "threshold": -1
-      },
-      "contents": [
-        {
-          "id": "perso2content1",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ],
-          "properties": {
-            "interests": "health food"
-          }
-        },
-        {
-          "id": "perso2content2",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/contactus.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso1content3",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/documentation.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso2content4",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/aboutus.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso2content5",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/products.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso2content6",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/services.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso2content7",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/community.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso2content8",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/projects.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso2content9",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/home.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        },
-        {
-          "id": "perso2content10",
-          "filters": [
-            {
-              "condition": {
-                "parameterValues": {
-                  "minimumEventCount": 1,
-                  "eventCondition": {
-                    "type": "booleanCondition",
-                    "parameterValues": {
-                      "operator": "and",
-                      "subConditions" : [
-                        {
-                          "type": "eventTypeCondition",
-                          "parameterValues": {
-                            "eventTypeId": "view"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.pagePath",
-                            "propertyValue": "/theend.html",
-                            "comparisonOperator": "equals"
-                          }
-                        },
-                        {
-                          "type": "eventPropertyCondition",
-                          "parameterValues": {
-                            "propertyName": 
"target.properties.pageInfo.language",
-                            "propertyValue": "en",
-                            "comparisonOperator": "equals"
-                          }
-                        }
-                      ]
-                    }
-                  },
-                  "numberOfDays": 30
-                },
-                "type": "pastEventCondition"
-              },
-              "properties": {
-                "score": -1000
-              }
-            }
-          ]
-        }
-      ]
-    }
-  ],
-  "sessionId": "test-session-id"
-}
\ No newline at end of file
diff --git a/itests/src/test/resources/personalization-no-control-group.json 
b/itests/src/test/resources/personalization-no-control-group.json
new file mode 100644
index 000000000..790dbe71b
--- /dev/null
+++ b/itests/src/test/resources/personalization-no-control-group.json
@@ -0,0 +1,45 @@
+{
+  "source": {
+    "itemId": "CMSServer",
+    "itemType": "custom",
+    "scope": "acme",
+    "properties": {}
+  },
+  "requireSegments": true,
+  "requiredProfileProperties": [
+    "*"
+  ],
+  "requiredSessionProperties": [
+    "*"
+  ],
+  "personalizations": [
+    {
+      "id": "perso-control-group",
+      "strategy": "matching-first",
+      "strategyOptions": {
+      },
+      "contents": [
+        {
+          "id": "first-name-missing",
+          "filters": [
+            {
+              "condition": {
+                "type": "profilePropertyCondition",
+                "parameterValues": {
+                  "propertyName": "properties.firstName",
+                  "comparisonOperator": "missing"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "id": "no-condition",
+          "filters": null
+        }
+      ]
+    }
+  ],
+  "sessionId": "dummy-session",
+  "profileId": "test-profile-id"
+}
\ No newline at end of file
diff --git 
a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java 
b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
index 6207845f8..c6f98bd37 100644
--- 
a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
+++ 
b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
@@ -203,11 +203,11 @@ public class ContextJsonEndpoint {
 
         List<PersonalizationService.PersonalizationRequest> personalizations = 
contextRequest.getPersonalizations();
         if (personalizations != null) {
-            data.setPersonalizations(new HashMap<>());
+            data.setPersonalizationResults(new HashMap<>());
             for (PersonalizationService.PersonalizationRequest personalization 
: sanitizePersonalizations(personalizations)) {
                 PersonalizationResult personalizationResult = 
personalizationService.personalizeList(eventsRequestContext.getProfile(), 
eventsRequestContext.getSession(), personalization);
                 
eventsRequestContext.addChanges(personalizationResult.getChangeType());
-                data.getPersonalizations().put(personalization.getId(), 
personalizationResult.getContentIds());
+                data.getPersonalizationResults().put(personalization.getId(), 
personalizationResult);
             }
         }
 
diff --git 
a/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java
 
b/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java
deleted file mode 100644
index 795080e87..000000000
--- 
a/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.services.impl.personalization;
-
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.text.ParseException;
-import java.util.Date;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Represents a personalization control group, stored in a profile and/or a 
session
- */
-public class ControlGroup {
-
-    private static final Logger logger = 
LoggerFactory.getLogger(ControlGroup.class.getName());
-
-    String id;
-    String displayName;
-    String path;
-    Date timeStamp;
-
-    public ControlGroup(String id, String displayName, String path, Date 
timeStamp) {
-        this.id = id;
-        this.displayName = displayName;
-        this.path = path;
-        this.timeStamp = timeStamp;
-    }
-
-    public static ControlGroup fromMap(Map<String,Object> map) {
-        String id = (String) map.get("id");
-        String displayName = (String) map.get("displayName");
-        String path = (String) map.get("path");
-        String dateStr = (String) map.get("timeStamp");
-        Date date = null;
-        try {
-            date = 
CustomObjectMapper.getObjectMapper().getDateFormat().parse(dateStr);
-        } catch (ParseException e) {
-            logger.error("Error parsing control group date", e);
-        }
-        return new ControlGroup(id, displayName, path, date);
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getDisplayName() {
-        return displayName;
-    }
-
-    public void setDisplayName(String displayName) {
-        this.displayName = displayName;
-    }
-
-    public String getPath() {
-        return path;
-    }
-
-    public void setPath(String path) {
-        this.path = path;
-    }
-
-    public Date getTimeStamp() {
-        return timeStamp;
-    }
-
-    public void setTimeStamp(Date timeStamp) {
-        this.timeStamp = timeStamp;
-    }
-
-    public Map<String,Object> toMap() {
-        Map<String,Object> result = new LinkedHashMap<>();
-        result.put("id", id);
-        result.put("displayName", displayName);
-        result.put("path", path);
-        result.put("timeStamp", 
CustomObjectMapper.getObjectMapper().getDateFormat().format(timeStamp));
-        return result;
-    }
-}
diff --git 
a/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
 
b/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
index 1d2cfd760..494b8db66 100644
--- 
a/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
+++ 
b/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
@@ -17,30 +17,27 @@
 
 package org.apache.unomi.services.impl.personalization;
 
-import org.apache.unomi.api.PersonalizationResult;
-import org.apache.unomi.api.PersonalizationStrategy;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.Session;
+import org.apache.unomi.api.*;
 import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.services.EventService;
 import org.apache.unomi.api.services.PersonalizationService;
 import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.services.sorts.ControlGroupPersonalizationStrategy;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceReference;
 
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
+
+import static 
org.apache.unomi.services.sorts.ControlGroupPersonalizationStrategy.CONTROL_GROUP_CONFIG;
 
 public class PersonalizationServiceImpl implements PersonalizationService {
 
-    public static final String CONTROL_GROUPS_PROPERTY_NAME = 
"unomiControlGroups";
     private BundleContext bundleContext;
     private ProfileService profileService;
 
     private Map<String, PersonalizationStrategy> personalizationStrategies = 
new ConcurrentHashMap<>();
+    private final PersonalizationStrategy controlGroupStrategy = new 
ControlGroupPersonalizationStrategy();
 
-    private Random controlGroupRandom = new Random();
 
     public void setProfileService(ProfileService profileService) {
         this.profileService = profileService;
@@ -88,73 +85,26 @@ public class PersonalizationServiceImpl implements 
PersonalizationService {
     @Override
     public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationRequest personalizationRequest) {
         PersonalizationStrategy strategy = 
personalizationStrategies.get(personalizationRequest.getStrategy());
-        int changeType = EventService.NO_CHANGE;
 
         if (strategy != null) {
-            if (personalizationRequest.getStrategyOptions() != null && 
personalizationRequest.getStrategyOptions().containsKey("controlGroup")) {
-                Map<String,Object> controlGroupMap = (Map<String,Object>) 
personalizationRequest.getStrategyOptions().get("controlGroup");
-
-                boolean storeInSession = false;
-                if (controlGroupMap.containsKey("storeInSession")) {
-                    storeInSession = (Boolean) 
controlGroupMap.get("storeInSession");
+            // hook on control group if necessary
+            PersonalizationResult controlGroupStrategyResult = null;
+            if (personalizationRequest.getStrategyOptions() != null && 
personalizationRequest.getStrategyOptions().containsKey(CONTROL_GROUP_CONFIG)) {
+                controlGroupStrategyResult = 
controlGroupStrategy.personalizeList(profile, session, personalizationRequest);
+                if (controlGroupStrategyResult.isInControlGroup()) {
+                    return controlGroupStrategyResult;
                 }
+            }
 
-                boolean profileInControlGroup = false;
-                Optional<ControlGroup> currentControlGroup;
-
-                List<ControlGroup> controlGroups = null;
-                if (storeInSession) {
-                    if (session.getProperty(CONTROL_GROUPS_PROPERTY_NAME) != 
null) {
-                        controlGroups = ((List<Map<String, Object>>) 
session.getProperty(CONTROL_GROUPS_PROPERTY_NAME)).stream().map(ControlGroup::fromMap).collect(Collectors.toList());
-                    }
-                } else {
-                    if (profile.getProperty(CONTROL_GROUPS_PROPERTY_NAME) != 
null) {
-                        controlGroups = ((List<Map<String, Object>>) 
profile.getProperty(CONTROL_GROUPS_PROPERTY_NAME)).stream().map(ControlGroup::fromMap).collect(Collectors.toList());
-                    }
-                }
-                if (controlGroups == null) {
-                    controlGroups = new ArrayList<>();
-                }
-                currentControlGroup = 
controlGroups.stream().filter(controlGroup -> 
controlGroup.id.equals(personalizationRequest.getId())).findFirst();
-                if (currentControlGroup.isPresent()) {
-                    // we already have an entry for this personalization so 
this means the profile is in the control group
-                    profileInControlGroup = true;
-                } else {
-                    double randomDouble = controlGroupRandom.nextDouble() * 
100.0;
-                    Object percentageObject = 
controlGroupMap.get("percentage");
-                    Double controlGroupPercentage = null;
-                    if (percentageObject != null) {
-                        if (percentageObject instanceof Double) {
-                            controlGroupPercentage = (Double) percentageObject;
-                        } else if (percentageObject instanceof Integer) {
-                            controlGroupPercentage = ((Integer) 
percentageObject).doubleValue();
-                        }
-                    }
-
-                    if (randomDouble <= controlGroupPercentage) {
-                        // Profile is elected to be in control group
-                        profileInControlGroup = true;
-                        ControlGroup controlGroup = new 
ControlGroup(personalizationRequest.getId(),
-                                (String) controlGroupMap.get("displayName"),
-                                (String) controlGroupMap.get("path"),
-                                new Date());
-                        controlGroups.add(controlGroup);
-                        List<Map<String,Object>> controlGroupsMap = 
controlGroups.stream().map(ControlGroup::toMap).collect(Collectors.toList());
-                        if (storeInSession) {
-                            session.setProperty(CONTROL_GROUPS_PROPERTY_NAME, 
controlGroupsMap);
-                            changeType = EventService.SESSION_UPDATED;
-                        } else {
-                            profile.setProperty(CONTROL_GROUPS_PROPERTY_NAME, 
controlGroupsMap);
-                            changeType = EventService.PROFILE_UPDATED;
-                        }
-                    }
-                }
-                if (profileInControlGroup) {
-                    // if profile is in control group we return the unmodified 
list.
-                    return new 
PersonalizationResult(personalizationRequest.getContents().stream().map(PersonalizedContent::getId).collect(Collectors.toList()),
 changeType);
-                }
+            // Execute the original strategy
+            PersonalizationResult originalStrategyResult = 
strategy.personalizeList(profile, session, personalizationRequest);
+            // merge original strategy result with previous controlGroup hook 
in case it's needed
+            if (controlGroupStrategyResult != null) {
+                
originalStrategyResult.addChanges(controlGroupStrategyResult.getChangeType());
+                
originalStrategyResult.getAdditionalResultInfos().putAll(controlGroupStrategyResult.getAdditionalResultInfos());
             }
-            return new PersonalizationResult(strategy.personalizeList(profile, 
session, personalizationRequest), changeType);
+
+            return originalStrategyResult;
         }
 
         throw new IllegalArgumentException("Unknown strategy : "+ 
personalizationRequest.getStrategy());
diff --git 
a/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java
 
b/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java
new file mode 100644
index 000000000..3297b334c
--- /dev/null
+++ 
b/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.services.sorts;
+
+import org.apache.unomi.api.*;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.api.services.PersonalizationService;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * System strategy to calculate control group on personalization that would 
use such configuration
+ * The status is then stored in the current profile/session under: 
systemProperties.personalizationStrategyStatus
+ * A control group is used with a percentage to decide if the current 
profile/session should have personalized results or not.
+ * - in case of control group is true: the variants will be return untouched 
like received and an information will be added to the personalized results to 
warn the client
+ * - in case of control group is false: we will execute the personalization 
normally
+ */
+public class ControlGroupPersonalizationStrategy implements 
PersonalizationStrategy {
+    public static final String PERSONALIZATION_STRATEGY_STATUS = 
"personalizationStrategyStatus";
+    public static final String PERSONALIZATION_STRATEGY_STATUS_ID = 
"personalizationId";
+    public static final String PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP = 
"inControlGroup";
+    public static final String PERSONALIZATION_STRATEGY_STATUS_DATE = 
"timeStamp";
+
+    public static final String CONTROL_GROUP_CONFIG_STORE_IN_SESSION = 
"storeInSession";
+    public static final String CONTROL_GROUP_CONFIG_PERCENTAGE = "percentage";
+    public static final String CONTROL_GROUP_CONFIG = "controlGroup";
+
+    private final Random controlGroupRandom = new Random();
+
+    @Override
+    public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationService.PersonalizationRequest personalizationRequest) {
+        if (personalizationRequest.getStrategyOptions() != null && 
personalizationRequest.getStrategyOptions().containsKey(CONTROL_GROUP_CONFIG)) {
+            Map<String, Object> controlGroupMap = (Map<String, Object>) 
personalizationRequest.getStrategyOptions().get(CONTROL_GROUP_CONFIG);
+
+            return 
controlGroupMap.containsKey(CONTROL_GROUP_CONFIG_STORE_IN_SESSION) &&
+                    controlGroupMap.get(CONTROL_GROUP_CONFIG_STORE_IN_SESSION) 
instanceof Boolean &&
+                    ((Boolean) 
controlGroupMap.get(CONTROL_GROUP_CONFIG_STORE_IN_SESSION)) ?
+                    
getPersonalizationResultForControlGroup(personalizationRequest, session, 
controlGroupMap, EventService.SESSION_UPDATED) :
+                    
getPersonalizationResultForControlGroup(personalizationRequest, profile, 
controlGroupMap, EventService.PROFILE_UPDATED);
+        }
+
+        throw new IllegalArgumentException("Not possible to perform control 
group strategy without control group config");
+    }
+
+    private PersonalizationResult 
getPersonalizationResultForControlGroup(PersonalizationService.PersonalizationRequest
 personalizationRequest, SystemPropertiesItem systemPropertiesItem, Map<String, 
Object> controlGroupConfig, int changeType) {
+        // Control group will return the same untouched list of received 
content ids
+        PersonalizationResult personalizationResult = new 
PersonalizationResult(
+                
personalizationRequest.getContents().stream().map(PersonalizationService.PersonalizedContent::getId).collect(Collectors.toList()));
+
+        // get the list of existing personalization strategy status
+        List<Map<String, Object>> strategyStatuses;
+        if 
(systemPropertiesItem.getSystemProperties().get(PERSONALIZATION_STRATEGY_STATUS)
 == null) {
+            strategyStatuses = new ArrayList<>();
+            
systemPropertiesItem.getSystemProperties().put(PERSONALIZATION_STRATEGY_STATUS, 
strategyStatuses);
+        } else {
+            strategyStatuses = (List<Map<String, Object>>) 
systemPropertiesItem.getSystemProperties().get(PERSONALIZATION_STRATEGY_STATUS);
+        }
+
+        // Check if we need to update an old status that would not contains 
control group info
+        boolean inControlGroup;
+        for (Map<String, Object> oldStrategyStatus : strategyStatuses) {
+            if 
(personalizationRequest.getId().equals(oldStrategyStatus.get(PERSONALIZATION_STRATEGY_STATUS_ID)))
 {
+                // Check if we have to update the strategy status or not ?
+                if 
(!oldStrategyStatus.containsKey(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP)) 
{
+
+                    // Old status doesn't contain any control group check, we 
need to calculate it and update the old status with the result.
+                    inControlGroup = calculateControlGroup(controlGroupConfig);
+                    
oldStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP, 
inControlGroup);
+                    
oldStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_DATE, new Date());
+                    personalizationResult.addChanges(changeType);
+
+                } else {
+                    // Just read existing status about the control group
+                    inControlGroup = (boolean) 
oldStrategyStatus.get(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP);
+                }
+
+                personalizationResult.setInControlGroup(inControlGroup);
+                return personalizationResult;
+            }
+        }
+
+        // We didn't found any existing status for the current perso, we need 
to create a new one.
+        inControlGroup = calculateControlGroup(controlGroupConfig);
+        Map<String, Object> newStrategyStatus = new HashMap<>();
+        newStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_ID, 
personalizationRequest.getId());
+        newStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_DATE, new 
Date());
+        newStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP, 
inControlGroup);
+        strategyStatuses.add(newStrategyStatus);
+
+        personalizationResult.addChanges(changeType);
+        personalizationResult.setInControlGroup(inControlGroup);
+        return personalizationResult;
+    }
+
+    private boolean calculateControlGroup(Map<String,Object> 
controlGroupConfig) {
+        double percentage = 
(controlGroupConfig.get(CONTROL_GROUP_CONFIG_PERCENTAGE) != null && 
controlGroupConfig.get(CONTROL_GROUP_CONFIG_PERCENTAGE) instanceof Number) ?
+                ((Number) 
controlGroupConfig.get(CONTROL_GROUP_CONFIG_PERCENTAGE)).doubleValue() : 0;
+
+        double random = controlGroupRandom.nextDouble() * 100.0;
+        return random <= percentage;
+    }
+}
diff --git 
a/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java
 
b/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java
index 809a724d6..a79bc62bf 100644
--- 
a/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java
+++ 
b/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java
@@ -17,6 +17,7 @@
 
 package org.apache.unomi.services.sorts;
 
+import org.apache.unomi.api.PersonalizationResult;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.Session;
 import org.apache.unomi.api.PersonalizationStrategy;
@@ -39,7 +40,7 @@ public class FilterPersonalizationStrategy implements 
PersonalizationStrategy {
     }
 
     @Override
-    public List<String> personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest) {
+    public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationService.PersonalizationRequest personalizationRequest) {
         List<String> sortedContent = new ArrayList<>();
         for (PersonalizationService.PersonalizedContent personalizedContent : 
personalizationRequest.getContents()) {
             boolean result = true;
@@ -59,6 +60,6 @@ public class FilterPersonalizationStrategy implements 
PersonalizationStrategy {
             sortedContent.add(fallback);
         }
 
-        return sortedContent;
+        return new PersonalizationResult(sortedContent);
     }
 }
diff --git 
a/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java
 
b/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java
index 3dd31f78b..76bb168af 100644
--- 
a/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java
+++ 
b/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java
@@ -17,19 +17,19 @@
 
 package org.apache.unomi.services.sorts;
 
+import org.apache.unomi.api.PersonalizationResult;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.Session;
 import org.apache.unomi.api.services.PersonalizationService;
 
 import java.util.Collections;
-import java.util.List;
 
 public class RandomPersonalizationStrategy extends 
FilterPersonalizationStrategy {
 
     @Override
-    public List<String> personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest) {
-        List<String> r = super.personalizeList(profile, session, 
personalizationRequest);
-        Collections.shuffle(r);
+    public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationService.PersonalizationRequest personalizationRequest) {
+        PersonalizationResult r = super.personalizeList(profile, session, 
personalizationRequest);
+        Collections.shuffle(r.getContentIds());
         return r;
     }
 }
diff --git 
a/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java
 
b/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java
index 82bcc165b..93d6c0ebe 100644
--- 
a/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java
+++ 
b/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java
@@ -17,6 +17,7 @@
 
 package org.apache.unomi.services.sorts;
 
+import org.apache.unomi.api.PersonalizationResult;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.Session;
 import org.apache.unomi.api.PersonalizationStrategy;
@@ -35,7 +36,7 @@ public class ScorePersonalizationStrategy implements 
PersonalizationStrategy {
     }
 
     @Override
-    public List<String> personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest) {
+    public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationService.PersonalizationRequest personalizationRequest) {
         List<String> sortedContent = new ArrayList<>();
         final Map<String,Integer> t = new HashMap<>();
 
@@ -64,7 +65,7 @@ public class ScorePersonalizationStrategy implements 
PersonalizationStrategy {
 
             String scoringPlanList = (String) 
(personalizedContent.getProperties() != null ? 
personalizedContent.getProperties().get("scoringPlans") : null);
             if (scoringPlanList != null) {
-                Map<String,Integer> scoreValues = (Map<String, Integer>) 
profile.getScores();
+                Map<String,Integer> scoreValues = profile.getScores();
                 for (String scoringPlan : scoringPlanList.split(" ")) {
                     if (scoreValues.get(scoringPlan) != null) {
                         score += scoreValues.get(scoringPlan);
@@ -93,18 +94,14 @@ public class ScorePersonalizationStrategy implements 
PersonalizationStrategy {
                 sortedContent.add(personalizedContent.getId());
             }
         }
-        Collections.sort(sortedContent, new Comparator<String>() {
-            @Override
-            public int compare(String o1, String o2) {
-                return t.get(o2) - t.get(o1);
-            }
-        });
+
+        sortedContent.sort((o1, o2) -> t.get(o2) - t.get(o1));
 
         String fallback = (String) 
personalizationRequest.getStrategyOptions().get("fallback");
         if (fallback != null && !sortedContent.contains(fallback)) {
             sortedContent.add(fallback);
         }
 
-        return sortedContent;
+        return new PersonalizationResult(sortedContent);
     }
 }

Reply via email to