You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2022/01/17 09:43:49 UTC

[unomi] branch unomi-1.6.x updated: UNOMI-537 Add control groups to personalizations (#368)

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

shuber pushed a commit to branch unomi-1.6.x
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/unomi-1.6.x by this push:
     new 305bfd0  UNOMI-537 Add control groups to personalizations (#368)
305bfd0 is described below

commit 305bfd08db229743ab0d6d411f50c5382cb59359
Author: Serge Huber <sh...@jahia.com>
AuthorDate: Mon Jan 17 10:41:10 2022 +0100

    UNOMI-537 Add control groups to personalizations (#368)
    
    - Personalizations will now store a control group election in the session or in the profile
    - Updated documentation for personalization
    - Implemented integration tests for control groups
    
    (cherry picked from commit 3054bcc2730af37c64cb248e338e69e3ea8acd5c)
---
 ...ionStrategy.java => PersonalizationResult.java} |   24 +-
 .../apache/unomi/api/PersonalizationStrategy.java  |   12 +-
 .../unomi/api/services/PersonalizationService.java |    3 +-
 .../org/apache/unomi/itests/ContextServletIT.java  |   83 +-
 .../resources/personalization-controlgroup.json    | 1031 ++++++++++++++++++++
 itests/src/test/resources/personalization.json     |    2 +-
 .../src/main/asciidoc/samples/twitter-sample.adoc  |   29 +-
 .../unomi/rest/endpoints/ContextJsonEndpoint.java  |   46 +-
 .../main/java/org/apache/unomi/utils/Changes.java  |    4 +
 .../impl/personalization/ControlGroup.java         |   68 ++
 .../PersonalizationServiceImpl.java                |   69 +-
 .../sorts/FilterPersonalizationStrategy.java       |    3 +
 12 files changed, 1326 insertions(+), 48 deletions(-)

diff --git a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
similarity index 59%
copy from api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
copy to api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
index 9c76d41..446189a 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
+++ b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
@@ -16,13 +16,27 @@
  */
 package org.apache.unomi.api;
 
-import org.apache.unomi.api.services.PersonalizationService;
-
 import java.util.List;
 
 /**
- *
+ * 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).
  */
-public interface PersonalizationStrategy {
-    List<String> personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest);
+public class PersonalizationResult {
+
+    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;
+    }
 }
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 9c76d41..152625e 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
+++ b/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
@@ -21,8 +21,18 @@ import org.apache.unomi.api.services.PersonalizationService;
 import java.util.List;
 
 /**
- *
+ * Interface for personalization strategies. Will filter and reorder the content list according to the strategy
+ * implementation
  */
 public interface PersonalizationStrategy {
+
+    /**
+     * Filters and personalizes the list of contents passed as a parameter using the strategy's implementation.
+     * @param profile the profile to use for the personalization
+     * @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
+     */
     List<String> personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest);
 }
diff --git a/api/src/main/java/org/apache/unomi/api/services/PersonalizationService.java b/api/src/main/java/org/apache/unomi/api/services/PersonalizationService.java
index ad6e3a6..f4dea3d 100644
--- a/api/src/main/java/org/apache/unomi/api/services/PersonalizationService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/PersonalizationService.java
@@ -17,6 +17,7 @@
 
 package org.apache.unomi.api.services;
 
+import org.apache.unomi.api.PersonalizationResult;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.Session;
 import org.apache.unomi.api.conditions.Condition;
@@ -57,7 +58,7 @@ public interface PersonalizationService {
      * @param personalizationRequest Personalization request, containing the list of variants and the required strategy
      * @return List of ids, based on user profile
      */
-    List<String> personalizeList(Profile profile, Session session, PersonalizationRequest personalizationRequest);
+    PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationRequest personalizationRequest);
 
     /**
      * Personalization request
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 d9e5490..45acb10 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
@@ -24,7 +24,6 @@ import org.apache.http.entity.StringEntity;
 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.ScoringElement;
 import org.apache.unomi.api.segments.Segment;
 import org.apache.unomi.api.services.DefinitionsService;
 import org.apache.unomi.api.services.EventService;
@@ -405,6 +404,88 @@ public class ContextServletIT extends BaseIT {
 	}
 
     @Test
+    public void testPersonalizationWithControlGroup() throws IOException, InterruptedException {
+
+        Map<String,String> parameters = new HashMap<>();
+        parameters.put("storeInSession", "false");
+        HttpPost request = new HttpPost(URL + CONTEXT_URL);
+        request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-controlgroup.json", parameters), ContentType.create("application/json")));
+        TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request);
+        assertEquals("Invalid response code", 200, response.getStatusCode());
+        refreshPersistence();
+        Thread.sleep(2000); //Making sure event is updated in DB
+        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);
+
+        Profile updatedProfile = profileService.load(contextResponse.getProfileId());
+        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(URL + CONTEXT_URL);
+        request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-controlgroup.json", parameters), ContentType.create("application/json")));
+        response = TestUtils.executeContextJSONRequest(request);
+        assertEquals("Invalid response code", 200, response.getStatusCode());
+        refreshPersistence();
+        Thread.sleep(2000); //Making sure event is updated in DB
+        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"));
+    }
+
+
+    @Test
     public void testRequireScoring() throws IOException, InterruptedException {
 
         Map<String,String> parameters = new HashMap<>();
diff --git a/itests/src/test/resources/personalization-controlgroup.json b/itests/src/test/resources/personalization-controlgroup.json
new file mode 100644
index 0000000..8a931d7
--- /dev/null
+++ b/itests/src/test/resources/personalization-controlgroup.json
@@ -0,0 +1,1031 @@
+{
+  "source": {
+    "itemId": "CMSServer",
+    "itemType": "custom",
+    "scope": "acme",
+    "version": null,
+    "properties": {}
+  },
+  "requireSegments": true,
+  "requiredProfileProperties": [
+    "unomiControlGroups"
+  ],
+  "requiredSessionProperties": [
+    "unomiControlGroups"
+  ],
+  "events": null,
+  "filters": null,
+  "personalizations": [
+    {
+      "id": "perso1",
+      "strategy": "score-sorted",
+      "strategyOptions": {
+        "threshold": -1,
+        "controlGroup" : {
+          "percentage" : 1.0,
+          "displayName" : "First perso",
+          "path" : "/home/perso1.html",
+          "storeInSession" : ###storeInSession###
+        }
+      },
+      "contents": [
+        {
+          "id": "perso1content1",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso1content3",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso1content4",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso1content5",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso1content6",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso1content7",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso1content8",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso1content9",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso1content10",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        }
+      ]
+    },
+    {
+      "id": "perso2",
+      "strategy": "score-sorted",
+      "strategyOptions": {
+        "threshold": -1
+      },
+      "contents": [
+        {
+          "id": "perso2content1",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso1content3",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso2content4",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso2content5",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso2content6",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso2content7",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso2content8",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso2content9",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        },
+        {
+          "id": "perso2content10",
+          "filters": [
+            {
+              "appliesOn": null,
+              "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
+              }
+            }
+          ],
+          "properties": null
+        }
+      ]
+    }
+  ],
+  "profileOverrides": null,
+  "sessionPropertiesOverrides": null,
+  "sessionId": "test-session-id"
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/personalization.json b/itests/src/test/resources/personalization.json
index d0461e9..89ac156 100644
--- a/itests/src/test/resources/personalization.json
+++ b/itests/src/test/resources/personalization.json
@@ -8,7 +8,7 @@
   },
   "requireSegments": true,
   "requiredProfileProperties": [
-    "interests"
+    "*"
   ],
   "requiredSessionProperties": [
     "*"
diff --git a/manual/src/main/asciidoc/samples/twitter-sample.adoc b/manual/src/main/asciidoc/samples/twitter-sample.adoc
index ac1057d..7d7eeba 100644
--- a/manual/src/main/asciidoc/samples/twitter-sample.adoc
+++ b/manual/src/main/asciidoc/samples/twitter-sample.adoc
@@ -288,7 +288,7 @@ curl --location --request POST 'http://localhost:8181/context.json' \
     "source": null,
     "requireSegments": false,
     "requiredProfileProperties": null,
-    "requiredSessionProperties": null,
+    "requiredSessionProperties": [ "unomiControlGroups" ],
     "events": null,
     "filters": null,
     "personalizations": [
@@ -296,7 +296,13 @@ curl --location --request POST 'http://localhost:8181/context.json' \
             "id": "gender-test",
             "strategy": "matching-first",
             "strategyOptions": {
-                "fallback": "var2"
+                "fallback": "var2",
+                "controlGroup" : {
+                  "percentage" : 0.1,
+                  "displayName" : "Gender test control group",
+                  "path" : "/gender-test",
+                  "storeInSession" : true
+                }
             },
             "contents": [
                 {
@@ -333,13 +339,12 @@ curl --location --request POST 'http://localhost:8181/context.json' \
 
 In the above example, we basically setup two variants : `var1` and `var2` and setup the `var2` to be the fallback variant
 in case no variant is matched. We could of course specify more than a variant. The `strategy` indicates to the
-personalization service how to calculate the "winning" variant. In this case the strategy `matching-first` will return
-the first variant that matches the current profile.
+personalization service how to calculate the "winning" variant. In this case the strategy `matching-first` will return variants that match the current profile. We also use the `controlGroups` option to specify that we want to have a control group for this personalization. The `0.1` percentage value represents 10% (0 to 1) of traffic that will be assigned randomly to the control group. The control group will be stored in the profile and the session of the visitors if they were assigned to [...]
 
 Currently the following strategies are available:
 
-- `matching-first`: will return the first matching variant.
-- `random`: will return a random variant
+- `matching-first`: will return the variant IDs that match the current profile (using the initial content order)
+- `random`: will return a shuffled list of variant IDs (ignoring any conditions)
 - `score-sorted`: allows to sort the variants based on scores associated with the filtering conditions, effectively
 sorting them by the highest scoring condition first.
 
@@ -351,7 +356,16 @@ Here is the result of the above example:
     "profileId": "01060c4c-a055-4c8f-9692-8a699d0c434a",
     "sessionId": "demo-session-id",
     "profileProperties": null,
-    "sessionProperties": null,
+    "sessionProperties": {
+        "unomiControlGroups": [
+            {
+                "id": "previousPerso",
+                "displayName": "Previous perso",
+                "path": "/home/previousPerso.html",
+                "timeStamp": "2021-12-15T13:52:38Z"
+            }
+        ]
+    },
     "profileSegments": null,
     "filteringResults": null,
     "processedEvents": 0,
@@ -367,6 +381,7 @@ Here is the result of the above example:
 }
 ----
 
+In the above example we can see the profile and session were assigned to other control groups but not the current one (the ids are different).
 
 ====== Overrides
 
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 525dd3a..6648d9a 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
@@ -20,13 +20,7 @@ package org.apache.unomi.rest.endpoints;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
-import org.apache.unomi.api.ContextRequest;
-import org.apache.unomi.api.ContextResponse;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.Persona;
-import org.apache.unomi.api.PersonaWithSessions;
-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.ConfigSharingService;
 import org.apache.unomi.api.services.EventService;
@@ -367,6 +361,26 @@ public class ContextJsonEndpoint {
         Changes changes = restServiceUtils.handleEvents(contextRequest.getEvents(), session, profile, request, response, timestamp);
         data.setProcessedEvents(changes.getProcessedItems());
 
+        List<PersonalizationService.PersonalizedContent> filterNodes = contextRequest.getFilters();
+        if (filterNodes != null) {
+            data.setFilteringResults(new HashMap<>());
+            for (PersonalizationService.PersonalizedContent personalizedContent : sanitizePersonalizedContentObjects(filterNodes)) {
+                data.getFilteringResults()
+                        .put(personalizedContent.getId(), personalizationService.filter(profile, session, personalizedContent));
+            }
+        }
+
+        List<PersonalizationService.PersonalizationRequest> personalizations = contextRequest.getPersonalizations();
+        if (personalizations != null) {
+            data.setPersonalizations(new HashMap<>());
+            for (PersonalizationService.PersonalizationRequest personalization : sanitizePersonalizations(personalizations)) {
+                PersonalizationResult personalizationResult = personalizationService.personalizeList(profile, session, personalization);
+                changes.setChangeType(changes.getChangeType() | personalizationResult.getChangeType());
+                data.getPersonalizations()
+                        .put(personalization.getId(), personalizationResult.getContentIds());
+            }
+        }
+
         profile = changes.getProfile();
 
         if (contextRequest.isRequireSegments()) {
@@ -397,24 +411,6 @@ public class ContextJsonEndpoint {
 
         processOverrides(contextRequest, profile, session);
 
-        List<PersonalizationService.PersonalizedContent> filterNodes = contextRequest.getFilters();
-        if (filterNodes != null) {
-            data.setFilteringResults(new HashMap<>());
-            for (PersonalizationService.PersonalizedContent personalizedContent : sanitizePersonalizedContentObjects(filterNodes)) {
-                data.getFilteringResults()
-                        .put(personalizedContent.getId(), personalizationService.filter(profile, session, personalizedContent));
-            }
-        }
-
-        List<PersonalizationService.PersonalizationRequest> personalizations = contextRequest.getPersonalizations();
-        if (personalizations != null) {
-            data.setPersonalizations(new HashMap<>());
-            for (PersonalizationService.PersonalizationRequest personalization : sanitizePersonalizations(personalizations)) {
-                data.getPersonalizations()
-                        .put(personalization.getId(), personalizationService.personalizeList(profile, session, personalization));
-            }
-        }
-
         if (!(profile instanceof Persona)) {
             data.setTrackedConditions(rulesService.getTrackedConditions(contextRequest.getSource()));
         } else {
diff --git a/rest/src/main/java/org/apache/unomi/utils/Changes.java b/rest/src/main/java/org/apache/unomi/utils/Changes.java
index 3ed75a6..b333430 100644
--- a/rest/src/main/java/org/apache/unomi/utils/Changes.java
+++ b/rest/src/main/java/org/apache/unomi/utils/Changes.java
@@ -43,6 +43,10 @@ public class Changes {
         return changeType;
     }
 
+    public void setChangeType(int changeType) {
+        this.changeType = changeType;
+    }
+
     public int getProcessedItems() {
         return processedItems;
     }
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
new file mode 100644
index 0000000..d6435d0
--- /dev/null
+++ b/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java
@@ -0,0 +1,68 @@
+/*
+ * 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 java.util.Date;
+
+/**
+ * Represents a personalization control group, stored in a profile and/or a session
+ */
+public class ControlGroup {
+    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 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;
+    }
+}
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 a4ae8f3..eeef772 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,18 +17,20 @@
 
 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.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.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceReference;
 
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
 
 public class PersonalizationServiceImpl implements PersonalizationService {
 
@@ -37,6 +39,8 @@ public class PersonalizationServiceImpl implements PersonalizationService {
 
     private Map<String, PersonalizationStrategy> personalizationStrategies = new ConcurrentHashMap<>();
 
+    private Random controlGroupRandom = new Random();
+
     public void setProfileService(ProfileService profileService) {
         this.profileService = profileService;
     }
@@ -73,19 +77,70 @@ public class PersonalizationServiceImpl implements PersonalizationService {
 
     @Override
     public String bestMatch(Profile profile, Session session, PersonalizationRequest personalizationRequest) {
-        List<String> sorted = personalizeList(profile,session,personalizationRequest);
-        if (sorted.size() > 0) {
-            return sorted.get(0);
+        PersonalizationResult result = personalizeList(profile,session,personalizationRequest);
+        if (result.getContentIds().size() > 0) {
+            return result.getContentIds().get(0);
         }
         return null;
     }
 
     @Override
-    public List<String> personalizeList(Profile profile, Session session, PersonalizationRequest personalizationRequest) {
+    public PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationRequest personalizationRequest) {
         PersonalizationStrategy strategy = personalizationStrategies.get(personalizationRequest.getStrategy());
+        int changeType = EventService.NO_CHANGE;
 
         if (strategy != null) {
-            return strategy.personalizeList(profile, session, personalizationRequest);
+            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");
+                }
+
+                boolean profileInControlGroup = false;
+                Optional<ControlGroup> currentControlGroup;
+
+                List<ControlGroup> controlGroups = null;
+                if (storeInSession) {
+                    controlGroups = (List<ControlGroup>) session.getProperty("unomiControlGroups");
+                } else {
+                    controlGroups = (List<ControlGroup>) profile.getProperty("unomiControlGroups");
+                }
+                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();
+                    Double controlGroupPercentage = (Double) controlGroupMap.get("percentage");
+
+                    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);
+                        if (storeInSession) {
+                            session.setProperty("unomiControlGroups", controlGroups);
+                            changeType = EventService.SESSION_UPDATED;
+                        } else {
+                            profile.setProperty("unomiControlGroups", controlGroups);
+                            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);
+                }
+            }
+            return new PersonalizationResult(strategy.personalizeList(profile, session, personalizationRequest), changeType);
         }
 
         throw new IllegalArgumentException("Unknown strategy : "+ personalizationRequest.getStrategy());
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 41ac9b5..809a724 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
@@ -27,6 +27,9 @@ import org.apache.unomi.api.services.ProfileService;
 import java.util.ArrayList;
 import java.util.List;
 
+/**
+ * This strategy will use filters to only keep the contents that match all their associated filters
+ */
 public class FilterPersonalizationStrategy implements PersonalizationStrategy {
 
     private ProfileService profileService;