You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by jk...@apache.org on 2022/08/05 14:18:58 UTC

[unomi] branch eventCollectorSessionProfileHandling created (now 3aa41e8c5)

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

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


      at 3aa41e8c5 UNOMI-522: event collector endpoint is now handling current visitor session and profile exactly like context.json endpoint

This branch includes the following new commits:

     new 3aa41e8c5 UNOMI-522: event collector endpoint is now handling current visitor session and profile exactly like context.json endpoint

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[unomi] 01/01: UNOMI-522: event collector endpoint is now handling current visitor session and profile exactly like context.json endpoint

Posted by jk...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 3aa41e8c5ce596634c54161a28c9ce77564a54a4
Author: Kevan <ke...@jahia.com>
AuthorDate: Fri Aug 5 16:18:42 2022 +0200

    UNOMI-522: event collector endpoint is now handling current visitor session and profile exactly like context.json endpoint
---
 .../apache/unomi/api/EventsCollectorRequest.java   |  21 ++
 .../unomi/rest/endpoints/ContextJsonEndpoint.java  | 286 +++------------------
 .../rest/endpoints/EventsCollectorEndpoint.java    | 121 ++-------
 .../unomi/rest/service/RestServiceUtils.java       |  62 ++++-
 .../rest/service/impl/RestServiceUtilsImpl.java    | 255 +++++++++++++++---
 .../main/java/org/apache/unomi/utils/Changes.java  |  57 ----
 .../apache/unomi/utils/EventsRequestContext.java   | 121 +++++++++
 7 files changed, 484 insertions(+), 439 deletions(-)

diff --git a/api/src/main/java/org/apache/unomi/api/EventsCollectorRequest.java b/api/src/main/java/org/apache/unomi/api/EventsCollectorRequest.java
index 9eb4ee9ac..74f922eec 100644
--- a/api/src/main/java/org/apache/unomi/api/EventsCollectorRequest.java
+++ b/api/src/main/java/org/apache/unomi/api/EventsCollectorRequest.java
@@ -34,6 +34,9 @@ public class EventsCollectorRequest {
     @Pattern(regexp = ValidationPattern.TEXT_VALID_CHARACTERS_PATTERN)
     private String sessionId;
 
+    @Pattern(regexp = ValidationPattern.TEXT_VALID_CHARACTERS_PATTERN)
+    private String profileId;
+
     /**
      * Retrieves the events to be processed.
      *
@@ -67,4 +70,22 @@ public class EventsCollectorRequest {
         this.sessionId = sessionId;
     }
 
+    /**
+     * Retrieve the profileId passed along with the request. All events will be processed with this profileId as a
+     * default
+     *
+     * @return the identifier for the profile
+     */
+    public String getProfileId() {
+        return profileId;
+    }
+
+    /**
+     * Sets the profileId in the request.
+     *
+     * @param profileId an unique identifier for the profile
+     */
+    public void setProfileId(String profileId) {
+        this.profileId = profileId;
+    }
 }
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 da3debb20..6207845f8 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,7 +20,6 @@ package org.apache.unomi.rest.endpoints;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
 import org.apache.unomi.api.*;
 import org.apache.unomi.api.conditions.Condition;
@@ -29,8 +28,7 @@ import org.apache.unomi.persistence.spi.CustomObjectMapper;
 import org.apache.unomi.rest.exception.InvalidRequestException;
 import org.apache.unomi.rest.service.RestServiceUtils;
 import org.apache.unomi.schema.api.SchemaService;
-import org.apache.unomi.utils.Changes;
-import org.apache.unomi.utils.HttpUtils;
+import org.apache.unomi.utils.EventsRequestContext;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
@@ -38,8 +36,6 @@ import org.slf4j.LoggerFactory;
 
 import javax.jws.WebService;
 import javax.servlet.ServletContext;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.*;
@@ -53,7 +49,6 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.UUID;
 
 @WebService
 @Consumes(MediaType.APPLICATION_JSON)
@@ -63,9 +58,7 @@ import java.util.UUID;
 public class ContextJsonEndpoint {
     private static final Logger logger = LoggerFactory.getLogger(ContextJsonEndpoint.class.getName());
 
-    private static final String DEFAULT_CLIENT_ID = "defaultClientId";
-
-    private boolean sanitizeConditions = Boolean
+    private final boolean sanitizeConditions = Boolean
             .parseBoolean(System.getProperty("org.apache.unomi.security.personalization.sanitizeConditions", "true"));
 
     @Context
@@ -75,19 +68,13 @@ public class ContextJsonEndpoint {
     @Context
     HttpServletResponse response;
 
-    @Reference
-    private ProfileService profileService;
     @Reference
     private PrivacyService privacyService;
     @Reference
-    private EventService eventService;
-    @Reference
     private RulesService rulesService;
     @Reference
     private PersonalizationService personalizationService;
     @Reference
-    private ConfigSharingService configSharingService;
-    @Reference
     private RestServiceUtils restServiceUtils;
     @Reference
     private SchemaService schemaService;
@@ -147,10 +134,11 @@ public class ContextJsonEndpoint {
     @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
     @Path("/context.json")
     public ContextResponse contextJSONAsPost(ContextRequest contextRequest,
-            @QueryParam("personaId") String personaId,
-            @QueryParam("sessionId") String sessionId,
-            @QueryParam("timestamp") Long timestampAsLong, @QueryParam("invalidateProfile") boolean invalidateProfile,
-            @QueryParam("invalidateSession") boolean invalidateSession) {
+                                             @QueryParam("personaId") String personaId,
+                                             @QueryParam("sessionId") String sessionId,
+                                             @QueryParam("timestamp") Long timestampAsLong,
+                                             @QueryParam("invalidateProfile") boolean invalidateProfile,
+                                             @QueryParam("invalidateSession") boolean invalidateSession) {
 
         // Schema validation
         ObjectNode paramsAsJson = JsonNodeFactory.instance.objectNode();
@@ -159,233 +147,57 @@ public class ContextJsonEndpoint {
         if (!schemaService.isValid(paramsAsJson.toString(), "https://unomi.apache.org/schemas/json/rest/requestIds/1-0-0")) {
             throw new InvalidRequestException("Invalid parameter", "Invalid received data");
         }
+
+        // Generate timestamp
         Date timestamp = new Date();
         if (timestampAsLong != null) {
             timestamp = new Date(timestampAsLong);
         }
 
-        // Handle persona
-        Profile profile = null;
-        Session session = null;
+        // init ids
         String profileId = null;
-        if (personaId != null) {
-            PersonaWithSessions personaWithSessions = profileService.loadPersonaWithSessions(personaId);
-            if (personaWithSessions == null) {
-                logger.error("Couldn't find persona, please check your personaId parameter");
-                profile = null;
-            } else {
-                profile = personaWithSessions.getPersona();
-                session = personaWithSessions.getLastSession();
-            }
-        }
-
         String scope = null;
         if (contextRequest != null) {
-            if (contextRequest.getSource() != null) {
-                scope = contextRequest.getSource().getScope();
-            }
-
-            if (contextRequest.getSessionId() != null) {
-                sessionId = contextRequest.getSessionId();
-            }
-
+            scope = contextRequest.getSource() != null ? contextRequest.getSource().getScope() : scope;
+            sessionId = contextRequest.getSessionId() != null ? contextRequest.getSessionId() : sessionId;
             profileId = contextRequest.getProfileId();
         }
-        if (profileId == null) {
-            // Get profile id from the cookie
-            profileId = restServiceUtils.getProfileIdCookieValue(request);
-        }
-
-        if (profileId == null && sessionId == null && personaId == null) {
-            logger.error(
-                    "Couldn't find profileId, sessionId or personaId in incoming request! Stopped processing request. See debug level for more information");
-            if (logger.isDebugEnabled()) {
-                logger.debug("Request dump: {}", HttpUtils.dumpRequestInfo(request));
-            }
-            throw new BadRequestException("Couldn't find profileId, sessionId or personaId in incoming request!");
-        }
-
-        int changes = EventService.NO_CHANGE;
-
-        // Not a persona, resolve profile now
-        boolean profileCreated = false;
-
-        if (profile == null) {
-            if (profileId == null || invalidateProfile) {
-                // no profileId cookie was found or the profile has to be invalidated, we generate a new one and create the profile in the profile service
-                profile = createNewProfile(null, timestamp);
-                profileCreated = true;
-            } else {
-                profile = profileService.load(profileId);
-                if (profile == null) {
-                    // this can happen if we have an old cookie but have reset the server,
-                    // or if we merged the profiles and somehow this cookie didn't get updated.
-                    profile = createNewProfile(profileId, timestamp);
-                    profileCreated = true;
-                } else {
-                    Changes changesObject = checkMergedProfile(profile, session);
-                    changes |= changesObject.getChangeType();
-                    profile = changesObject.getProfile();
-                }
-            }
-
-            Profile sessionProfile;
-            if (StringUtils.isNotBlank(sessionId) && !invalidateSession) {
-                session = profileService.loadSession(sessionId, timestamp);
-                if (session != null) {
-                    sessionProfile = session.getProfile();
-
-                    boolean anonymousSessionProfile = sessionProfile.isAnonymousProfile();
-                    if (!profile.isAnonymousProfile() && !anonymousSessionProfile && !profile.getItemId()
-                            .equals(sessionProfile.getItemId())) {
-                        // Session user has been switched, profile id in cookie is not up to date
-                        // We must reload the profile with the session ID as some properties could be missing from the session profile
-                        // #personalIdentifier
-                        profile = profileService.load(sessionProfile.getItemId());
-                    }
-
-                    // Handle anonymous situation
-                    Boolean requireAnonymousBrowsing = privacyService.isRequireAnonymousBrowsing(profile);
-                    if (requireAnonymousBrowsing && anonymousSessionProfile) {
-                        // User wants to browse anonymously, anonymous profile is already set.
-                    } else if (requireAnonymousBrowsing && !anonymousSessionProfile) {
-                        // User wants to browse anonymously, update the sessionProfile to anonymous profile
-                        sessionProfile = privacyService.getAnonymousProfile(profile);
-                        session.setProfile(sessionProfile);
-                        changes |= EventService.SESSION_UPDATED;
-                    } else if (!requireAnonymousBrowsing && anonymousSessionProfile) {
-                        // User does not want to browse anonymously anymore, update the sessionProfile to real profile
-                        sessionProfile = profile;
-                        session.setProfile(sessionProfile);
-                        changes |= EventService.SESSION_UPDATED;
-                    } else if (!requireAnonymousBrowsing && !anonymousSessionProfile) {
-                        // User does not want to browse anonymously, use the real profile. Check that session contains the current profile.
-                        sessionProfile = profile;
-                        if (!session.getProfileId().equals(sessionProfile.getItemId())) {
-                            changes |= EventService.SESSION_UPDATED;
-                        }
-                        session.setProfile(sessionProfile);
-                    }
-                }
-            }
 
-            if (session == null || invalidateSession) {
-                sessionProfile = privacyService.isRequireAnonymousBrowsing(profile) ? privacyService.getAnonymousProfile(profile) : profile;
-
-                if (StringUtils.isNotBlank(sessionId)) {
-                    // Only save session and send event if a session id was provided, otherwise keep transient session
-                    session = new Session(sessionId, sessionProfile, timestamp, scope);
-                    changes |= EventService.SESSION_UPDATED;
-                    Event event = new Event("sessionCreated", session, profile, scope, null, session, null, timestamp, false);
-                    if (sessionProfile.isAnonymousProfile()) {
-                        // Do not keep track of profile in event
-                        event.setProfileId(null);
-                    }
-                    event.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, request);
-                    event.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, response);
-                    if (logger.isDebugEnabled()) {
-                        logger.debug("Received event {} for profile={} session={} target={} timestamp={}", event.getEventType(),
-                                profile.getItemId(), session.getItemId(), event.getTarget(), timestamp);
-                    }
-                    changes |= eventService.send(event);
-                }
-            }
-
-            if (profileCreated) {
-                changes |= EventService.PROFILE_UPDATED;
-
-                Event profileUpdated = new Event("profileUpdated", session, profile, scope, null, profile, timestamp);
-                profileUpdated.setPersistent(false);
-                profileUpdated.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, request);
-                profileUpdated.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, response);
-                profileUpdated.getAttributes().put(Event.CLIENT_ID_ATTRIBUTE, DEFAULT_CLIENT_ID);
-
-                if (logger.isDebugEnabled()) {
-                    logger.debug("Received event {} for profile={} {} target={} timestamp={}", profileUpdated.getEventType(),
-                            profile.getItemId(), " session=" + (session != null ? session.getItemId() : null), profileUpdated.getTarget(),
-                            timestamp);
-                }
-                changes |= eventService.send(profileUpdated);
-            }
-        }
+        // build public context, profile + session creation/anonymous etc ...
+        EventsRequestContext eventsRequestContext = restServiceUtils.initEventsRequest(scope, sessionId, profileId,
+                personaId, invalidateProfile, invalidateSession, request, response, timestamp);
 
+        // Build response
         ContextResponse contextResponse = new ContextResponse();
-        contextResponse.setProfileId(profile.getItemId());
-        if (session != null) {
-            contextResponse.setSessionId(session.getItemId());
-        } else if (sessionId != null) {
-            contextResponse.setSessionId(sessionId);
-        }
-
         if (contextRequest != null) {
-            Changes changesObject = handleRequest(contextRequest, session, profile, contextResponse, request, response, timestamp);
-            changes |= changesObject.getChangeType();
-            profile = changesObject.getProfile();
+            eventsRequestContext = processContextRequest(contextRequest, contextResponse, eventsRequestContext);
         }
 
-        if ((changes & EventService.PROFILE_UPDATED) == EventService.PROFILE_UPDATED) {
-            profileService.save(profile);
-            contextResponse.setProfileId(profile.getItemId());
+        // finalize request, save profile and session if necessary and return profileId cookie in response
+        restServiceUtils.finalizeEventsRequest(eventsRequestContext, false);
 
-            if (profileCreated) {
-                String clientId = contextRequest != null && contextRequest.getClientId() != null ? contextRequest.getClientId() : DEFAULT_CLIENT_ID;
-                String profileMasterId = profile.getMergedWith() != null ? profile.getMergedWith() : profile.getItemId();
-                profileService.addAliasToProfile(profileMasterId, profile.getItemId(), clientId );
-            }
-        }
-        if ((changes & EventService.SESSION_UPDATED) == EventService.SESSION_UPDATED && session != null) {
-            profileService.saveSession(session);
-            contextResponse.setSessionId(session.getItemId());
-        }
-
-        if ((changes & EventService.ERROR) == EventService.ERROR) {
-            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-        }
-        // Set profile cookie
-        if (!(profile instanceof Persona)) {
-            response.setHeader("Set-Cookie", HttpUtils.getProfileCookieString(profile, configSharingService, request.isSecure()));
+        contextResponse.setProfileId(eventsRequestContext.getProfile().getItemId());
+        if (eventsRequestContext.getSession() != null) {
+            contextResponse.setSessionId(eventsRequestContext.getSession().getItemId());
+        } else if (sessionId != null) {
+            contextResponse.setSessionId(sessionId);
         }
         return contextResponse;
     }
 
-    private Changes checkMergedProfile(Profile profile, Session session) {
-        int changes = EventService.NO_CHANGE;
-        if (profile.getMergedWith() != null && !privacyService.isRequireAnonymousBrowsing(profile) && !profile.isAnonymousProfile()) {
-            Profile currentProfile = profile;
-            String masterProfileId = profile.getMergedWith();
-            Profile masterProfile = profileService.load(masterProfileId);
-            if (masterProfile != null) {
-                logger.info("Current profile {} was merged with profile {}, replacing profile in session", currentProfile.getItemId(),
-                        masterProfileId);
-                profile = masterProfile;
-                if (session != null) {
-                    session.setProfile(profile);
-                    changes = EventService.SESSION_UPDATED;
-                }
-            } else {
-                logger.warn("Couldn't find merged profile {}, falling back to profile {}", masterProfileId, currentProfile.getItemId());
-                profile.setMergedWith(null);
-                changes = EventService.PROFILE_UPDATED;
-            }
-        }
-
-        return new Changes(changes, profile);
-    }
-
-    private Changes handleRequest(ContextRequest contextRequest, Session session, Profile profile, ContextResponse data,
-            ServletRequest request, ServletResponse response, Date timestamp) {
+    private EventsRequestContext processContextRequest(ContextRequest contextRequest, ContextResponse data, EventsRequestContext eventsRequestContext) {
 
-        processOverrides(contextRequest, profile, session);
+        processOverrides(contextRequest, eventsRequestContext.getProfile(), eventsRequestContext.getSession());
 
-        Changes changes = restServiceUtils.handleEvents(contextRequest.getEvents(), session, profile, request, response, timestamp);
-        data.setProcessedEvents(changes.getProcessedItems());
+        eventsRequestContext = restServiceUtils.performEventsRequest(contextRequest.getEvents(), eventsRequestContext);
+        data.setProcessedEvents(eventsRequestContext.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));
+                        .put(personalizedContent.getId(), personalizationService.filter(eventsRequestContext.getProfile(), eventsRequestContext.getSession(), personalizedContent));
             }
         }
 
@@ -393,34 +205,31 @@ public class ContextJsonEndpoint {
         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());
+                PersonalizationResult personalizationResult = personalizationService.personalizeList(eventsRequestContext.getProfile(), eventsRequestContext.getSession(), personalization);
+                eventsRequestContext.addChanges(personalizationResult.getChangeType());
+                data.getPersonalizations().put(personalization.getId(), personalizationResult.getContentIds());
             }
         }
 
-        profile = changes.getProfile();
-
         if (contextRequest.isRequireSegments()) {
-            data.setProfileSegments(profile.getSegments());
+            data.setProfileSegments(eventsRequestContext.getProfile().getSegments());
         }
         if (contextRequest.isRequireScores()) {
-            data.setProfileScores(profile.getScores());
+            data.setProfileScores(eventsRequestContext.getProfile().getScores());
         }
 
         if (contextRequest.getRequiredProfileProperties() != null) {
-            Map<String, Object> profileProperties = new HashMap<>(profile.getProperties());
+            Map<String, Object> profileProperties = new HashMap<>(eventsRequestContext.getProfile().getProperties());
             if (!contextRequest.getRequiredProfileProperties().contains("*")) {
                 profileProperties.keySet().retainAll(contextRequest.getRequiredProfileProperties());
             }
             data.setProfileProperties(profileProperties);
         }
 
-        if (session != null) {
-            data.setSessionId(session.getItemId());
+        if (eventsRequestContext.getSession() != null) {
+            data.setSessionId(eventsRequestContext.getSession().getItemId());
             if (contextRequest.getRequiredSessionProperties() != null) {
-                Map<String, Object> sessionProperties = new HashMap<>(session.getProperties());
+                Map<String, Object> sessionProperties = new HashMap<>(eventsRequestContext.getSession().getProperties());
                 if (!contextRequest.getRequiredSessionProperties().contains("*")) {
                     sessionProperties.keySet().retainAll(contextRequest.getRequiredSessionProperties());
                 }
@@ -428,16 +237,16 @@ public class ContextJsonEndpoint {
             }
         }
 
-        if (!(profile instanceof Persona)) {
+        if (!(eventsRequestContext.getProfile() instanceof Persona)) {
             data.setTrackedConditions(rulesService.getTrackedConditions(contextRequest.getSource()));
         } else {
             data.setTrackedConditions(Collections.emptySet());
         }
 
-        data.setAnonymousBrowsing(privacyService.isRequireAnonymousBrowsing(profile));
-        data.setConsents(profile.getConsents());
+        data.setAnonymousBrowsing(privacyService.isRequireAnonymousBrowsing(eventsRequestContext.getProfile()));
+        data.setConsents(eventsRequestContext.getProfile().getConsents());
 
-        return changes;
+        return eventsRequestContext;
     }
 
     /**
@@ -467,17 +276,6 @@ public class ContextJsonEndpoint {
         }
     }
 
-    private Profile createNewProfile(String existingProfileId, Date timestamp) {
-        Profile profile;
-        String profileId = existingProfileId;
-        if (profileId == null) {
-            profileId = UUID.randomUUID().toString();
-        }
-        profile = new Profile(profileId);
-        profile.setProperty("firstVisit", timestamp);
-        return profile;
-    }
-
     public void destroy() {
         logger.info("Context servlet shutdown.");
     }
diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/EventsCollectorEndpoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/EventsCollectorEndpoint.java
index 8911402a2..f727b352f 100644
--- a/rest/src/main/java/org/apache/unomi/rest/endpoints/EventsCollectorEndpoint.java
+++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/EventsCollectorEndpoint.java
@@ -21,28 +21,16 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
 import org.apache.unomi.api.Event;
 import org.apache.unomi.api.EventsCollectorRequest;
-import org.apache.unomi.api.Persona;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.Session;
-import org.apache.unomi.api.services.ConfigSharingService;
-import org.apache.unomi.api.services.EventService;
-import org.apache.unomi.api.services.PrivacyService;
-import org.apache.unomi.api.services.ProfileService;
 import org.apache.unomi.rest.exception.InvalidRequestException;
 import org.apache.unomi.rest.models.EventCollectorResponse;
 import org.apache.unomi.rest.service.RestServiceUtils;
-import org.apache.unomi.utils.Changes;
+import org.apache.unomi.utils.EventsRequestContext;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import javax.jws.WebService;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
-import javax.ws.rs.BadRequestException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.OPTIONS;
@@ -54,7 +42,6 @@ import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.util.Date;
-import java.util.UUID;
 
 @WebService
 @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
@@ -63,16 +50,7 @@ import java.util.UUID;
 @Path("/")
 @Component(service = EventsCollectorEndpoint.class, property = "osgi.jaxrs.resource=true")
 public class EventsCollectorEndpoint {
-    private static final Logger logger = LoggerFactory.getLogger(EventsCollectorEndpoint.class.getName());
 
-    @Reference
-    private EventService eventService;
-    @Reference
-    private ProfileService profileService;
-    @Reference
-    private PrivacyService privacyService;
-    @Reference
-    private ConfigSharingService configSharingService;
     @Reference
     private RestServiceUtils restServiceUtils;
 
@@ -114,87 +92,34 @@ public class EventsCollectorEndpoint {
         if (sessionId == null) {
             sessionId = request.getParameter("sessionId");
         }
-        Session session = null;
-        if (sessionId != null) {
-            session = profileService.loadSession(sessionId, timestamp);
-        }
-        Profile profile = null;
-        if (session == null) {
-            String scope = "systemscope";
-            // Get the first available scope that is not equal to systemscope to create the session otherwise systemscope will be used
-            for (Event event : eventsCollectorRequest.getEvents()) {
-                if (StringUtils.isNotBlank(event.getEventType())) {
-                    if (StringUtils.isNotBlank(event.getScope()) && !event.getScope().equals("systemscope")) {
-                        scope = event.getScope();
-                        break;
-                    } else if (event.getSource() != null && StringUtils.isNotBlank(event.getSource().getScope()) && !event.getSource()
-                            .getScope().equals("systemscope")) {
-                        scope = event.getSource().getScope();
-                        break;
-                    }
-                }
-            }
-            logger.debug("scope is now {}", scope);
-            String cookieProfileId = restServiceUtils.getProfileIdCookieValue(request);
-            if (StringUtils.isNotBlank(cookieProfileId)) {
-                profile = profileService.load(cookieProfileId);
-            }
-            if (profile == null) {
-                // Create non persisted profile to create the session
-                profile = new Profile("temp_" + UUID.randomUUID().toString());
-                profile.setProperty("firstVisit", timestamp);
-            }
-            /*
-            // Create anonymous profile so we don't keep track of the temp profile anywhere
-            Profile anonymousProfile = privacyService.getAnonymousProfile(profile);
-            // Create new session which should not be persisted as well as the temp profile
-            session = new Session(sessionId, anonymousProfile, timestamp, scope);
-            if (logger.isDebugEnabled()) {
-                logger.debug("No session found for sessionId={}, creating new session!", sessionId);
-            }
-            */
-        } else {
-            Profile sessionProfile = session.getProfile();
-            final String errorMessage = String
-                    .format("No valid profile found or persona found for profileId=%s, aborting request !", session.getProfileId());
-            if (sessionProfile.getItemId() != null) {
-                // Reload up-to-date profile
-                profile = profileService.load(sessionProfile.getItemId());
-                if (profile == null || profile instanceof Persona) {
-                    logger.error(errorMessage);
-                    throw new BadRequestException(errorMessage);
-                }
-            } else {
-                // Session uses anonymous profile, try to find profile from cookie
-                String cookieProfileId = restServiceUtils.getProfileIdCookieValue(request);
-                if (StringUtils.isNotBlank(cookieProfileId)) {
-                    profile = profileService.load(cookieProfileId);
-                }
 
-                if (profile == null) {
-                    logger.error(errorMessage);
-                    throw new BadRequestException(errorMessage);
+        String profileId = eventsCollectorRequest.getProfileId();
+        // Get the first available scope that is not equal to systemscope otherwise systemscope will be used
+        String scope = "systemscope";
+        for (Event event : eventsCollectorRequest.getEvents()) {
+            if (StringUtils.isNotBlank(event.getEventType())) {
+                if (StringUtils.isNotBlank(event.getScope()) && !event.getScope().equals("systemscope")) {
+                    scope = event.getScope();
+                    break;
+                } else if (event.getSource() != null &&
+                        StringUtils.isNotBlank(event.getSource().getScope()) &&
+                        !event.getSource().getScope().equals("systemscope")) {
+                    scope = event.getSource().getScope();
+                    break;
                 }
             }
         }
 
-        Changes changesObject = restServiceUtils
-                .handleEvents(eventsCollectorRequest.getEvents(), session, profile, request, response, timestamp);
-        int changes = changesObject.getChangeType();
-        profile = changesObject.getProfile();
+        // build public context, profile + session creation/anonymous etc ...
+        EventsRequestContext eventsRequestContext = restServiceUtils.initEventsRequest(scope, sessionId, profileId,
+                null, false, false, request, response, timestamp);
 
-        if ((changes & EventService.PROFILE_UPDATED) == EventService.PROFILE_UPDATED) {
-            profileService.save(profile);
-        }
-        if ((changes & EventService.SESSION_UPDATED) == EventService.SESSION_UPDATED && session != null) {
-            profileService.saveSession(session);
-        }
-        if ((changes & EventService.ERROR) == EventService.ERROR) {
-            String errorMessage = "Error processing events. Total number of processed events: " + changesObject.getProcessedItems() + "/"
-                    + eventsCollectorRequest.getEvents().size();
-            throw new BadRequestException(errorMessage);
-        }
+        // process events
+        eventsRequestContext = restServiceUtils.performEventsRequest(eventsCollectorRequest.getEvents(), eventsRequestContext);
+
+        // finalize request
+        restServiceUtils.finalizeEventsRequest(eventsRequestContext, true);
 
-        return new EventCollectorResponse(changes);
+        return new EventCollectorResponse(eventsRequestContext.getChanges());
     }
 }
diff --git a/rest/src/main/java/org/apache/unomi/rest/service/RestServiceUtils.java b/rest/src/main/java/org/apache/unomi/rest/service/RestServiceUtils.java
index e64db1485..0e6a9ba11 100644
--- a/rest/src/main/java/org/apache/unomi/rest/service/RestServiceUtils.java
+++ b/rest/src/main/java/org/apache/unomi/rest/service/RestServiceUtils.java
@@ -18,19 +18,65 @@
 package org.apache.unomi.rest.service;
 
 import org.apache.unomi.api.Event;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.Session;
-import org.apache.unomi.utils.Changes;
+import org.apache.unomi.utils.EventsRequestContext;
 
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import java.util.Date;
 import java.util.List;
 
+/**
+ * Utility service for Public REST endpoints
+ */
 public interface RestServiceUtils {
-    String getProfileIdCookieValue(HttpServletRequest httpServletRequest);
+    /**
+     * This method is used to initialize the context for a request that would require executing events.
+     *
+     * It will load existing profile/session for current user or build new ones if necessary
+     * IT will also handle anonymous profile/session preferences in case there is specific ones.
+     * It will also handle persona in case it is provided
+     * And finally it will provide a contextual bean named: EventsRequestContext,
+     * that will contain all the required information for the next steps of the request, like: processing the events
+     *
+     * @param scope the current scope (mandatory, in case session need to be created)
+     * @param sessionId the current sessionId (mandatory)
+     * @param profileId the current profileId (optional in case profile doesn't exists yet for incoming visitor)
+     * @param personaId the current personaId (optional in case we don't want to apply persona on current request)
+     * @param invalidateProfile true in case we want to invalidate the current visitor profile, false otherwise
+     * @param invalidateSession true in case we want to invalidate the current visitor session, false otherwise
+     * @param request the current request
+     * @param response the current request response
+     * @param timestamp the current date, for timestamp the current visitor data
+     *
+     * @return the built EventsRequestContext
+     */
+    EventsRequestContext initEventsRequest(String scope, String sessionId, String profileId, String personaId,
+                                           boolean invalidateProfile, boolean invalidateSession,
+                                           HttpServletRequest request, HttpServletResponse response,
+                                           Date timestamp);
+
+    /**
+     * Execute the list of events using the dedicated eventsRequestContext
+     * @param events the list of events to he executed
+     * @param eventsRequestContext the current EventsRequestContext
+     * @return an updated version of the current eventsRequestContext
+     */
+    EventsRequestContext performEventsRequest(List<Event> events, EventsRequestContext eventsRequestContext);
 
-    Changes handleEvents(List<Event> events, Session session, Profile profile, ServletRequest request, ServletResponse response,
-            Date timestamp);
+    /**
+     * At the end of an events requests we want to save/update the profile and/or the session depending on the changes
+     * Also we want to return a cookie about current visitor profile ID
+     *
+     * @param eventsRequestContext the current EventsRequestContext
+     * @param crashOnError true if we want to throw an Exception in case of errors during events execution,
+     *                     false otherwise (otherwise, no exception, but just an error code directly returned to the HTTP response)
+     */
+    void finalizeEventsRequest(EventsRequestContext eventsRequestContext, boolean crashOnError);
+
+    /**
+     * Try to extract the current visitor profileId from the current request cookies.
+     * @param httpServletRequest the current HTTP request
+     * @return the profileId if found in the cookies, null otherwise
+     */
+    String getProfileIdCookieValue(HttpServletRequest httpServletRequest);
 }
diff --git a/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java b/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
index fae86daa2..75a98d1e1 100644
--- a/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
+++ b/rest/src/main/java/org/apache/unomi/rest/service/impl/RestServiceUtilsImpl.java
@@ -17,32 +17,35 @@
 package org.apache.unomi.rest.service.impl;
 
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.Persona;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.Session;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.*;
 import org.apache.unomi.api.services.ConfigSharingService;
 import org.apache.unomi.api.services.EventService;
 import org.apache.unomi.api.services.PrivacyService;
+import org.apache.unomi.api.services.ProfileService;
 import org.apache.unomi.rest.exception.InvalidRequestException;
 import org.apache.unomi.rest.service.RestServiceUtils;
 import org.apache.unomi.schema.api.SchemaService;
-import org.apache.unomi.utils.Changes;
+import org.apache.unomi.utils.HttpUtils;
+import org.apache.unomi.utils.EventsRequestContext;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.BadRequestException;
 import java.util.Date;
 import java.util.List;
+import java.util.UUID;
 
 @Component(service = RestServiceUtils.class)
 public class RestServiceUtilsImpl implements RestServiceUtils {
 
+    private static final String DEFAULT_CLIENT_ID = "defaultClientId";
+
     private static final Logger logger = LoggerFactory.getLogger(RestServiceUtilsImpl.class.getName());
 
     @Reference
@@ -54,9 +57,13 @@ public class RestServiceUtilsImpl implements RestServiceUtils {
     @Reference
     private EventService eventService;
 
+    @Reference
+    private ProfileService profileService;
+
     @Reference
     SchemaService schemaService;
 
+    @Override
     public String getProfileIdCookieValue(HttpServletRequest httpServletRequest) {
         String cookieProfileId = null;
 
@@ -78,59 +85,243 @@ public class RestServiceUtilsImpl implements RestServiceUtils {
     }
 
     @Override
-    public Changes handleEvents(List<Event> events, Session session, Profile profile, ServletRequest request, ServletResponse response,
-            Date timestamp) {
-        List<String> filteredEventTypes = privacyService.getFilteredEventTypes(profile);
+    public EventsRequestContext initEventsRequest(String scope, String sessionId, String profileId, String personaId,
+                                                  boolean invalidateProfile, boolean invalidateSession,
+                                                  HttpServletRequest request, HttpServletResponse response, Date timestamp) {
+
+        // Build context
+        EventsRequestContext eventsRequestContext = new EventsRequestContext(timestamp, null, null, request, response);
+
+        // Handle persona
+        if (personaId != null) {
+            PersonaWithSessions personaWithSessions = profileService.loadPersonaWithSessions(personaId);
+            if (personaWithSessions == null) {
+                logger.error("Couldn't find persona, please check your personaId parameter");
+            } else {
+                eventsRequestContext.setProfile(personaWithSessions.getPersona());
+                eventsRequestContext.setSession(personaWithSessions.getLastSession());
+            }
+        }
+
+        if (profileId == null) {
+            // Get profile id from the cookie
+            profileId = getProfileIdCookieValue(request);
+        }
+
+        if (profileId == null && sessionId == null && personaId == null) {
+            logger.error("Couldn't find profileId, sessionId or personaId in incoming request! Stopped processing request. See debug level for more information");
+            if (logger.isDebugEnabled()) {
+                logger.debug("Request dump: {}", HttpUtils.dumpRequestInfo(request));
+            }
+            throw new BadRequestException("Couldn't find profileId, sessionId or personaId in incoming request!");
+        }
+
+        boolean profileCreated = false;
+        if (eventsRequestContext.getProfile() == null) {
+            if (profileId == null || invalidateProfile) {
+                // no profileId cookie was found or the profile has to be invalidated, we generate a new one and create the profile in the profile service
+                eventsRequestContext.setProfile(createNewProfile(null, timestamp));
+                profileCreated = true;
+            } else {
+                eventsRequestContext.setProfile(profileService.load(profileId));
+                if (eventsRequestContext.getProfile() == null) {
+                    // this can happen if we have an old cookie but have reset the server,
+                    // or if we merged the profiles and somehow this cookie didn't get updated.
+                    eventsRequestContext.setProfile(createNewProfile(profileId, timestamp));
+                    profileCreated = true;
+                }
+            }
+
+            // Try to recover existing session
+            Profile sessionProfile;
+            if (StringUtils.isNotBlank(sessionId) && !invalidateSession) {
+
+                eventsRequestContext.setSession(profileService.loadSession(sessionId, timestamp));
+                if (eventsRequestContext.getSession() != null) {
+
+                    sessionProfile = eventsRequestContext.getSession().getProfile();
+                    boolean anonymousSessionProfile = sessionProfile.isAnonymousProfile();
+                    if (!eventsRequestContext.getProfile().isAnonymousProfile() &&
+                            !anonymousSessionProfile &&
+                            !eventsRequestContext.getProfile().getItemId().equals(sessionProfile.getItemId())) {
+                        // Session user has been switched, profile id in cookie is not up to date
+                        // We must reload the profile with the session ID as some properties could be missing from the session profile
+                        // #personalIdentifier
+                        eventsRequestContext.setProfile(profileService.load(sessionProfile.getItemId()));
+                    }
+
+                    // Handle anonymous situation
+                    Boolean requireAnonymousBrowsing = privacyService.isRequireAnonymousBrowsing(eventsRequestContext.getProfile());
+                    if (requireAnonymousBrowsing && anonymousSessionProfile) {
+                        // User wants to browse anonymously, anonymous profile is already set.
+                    } else if (requireAnonymousBrowsing && !anonymousSessionProfile) {
+                        // User wants to browse anonymously, update the sessionProfile to anonymous profile
+                        sessionProfile = privacyService.getAnonymousProfile(eventsRequestContext.getProfile());
+                        eventsRequestContext.getSession().setProfile(sessionProfile);
+                        eventsRequestContext.addChanges(EventService.SESSION_UPDATED);
+                    } else if (!requireAnonymousBrowsing && anonymousSessionProfile) {
+                        // User does not want to browse anonymously anymore, update the sessionProfile to real profile
+                        sessionProfile = eventsRequestContext.getProfile();
+                        eventsRequestContext.getSession().setProfile(sessionProfile);
+                        eventsRequestContext.addChanges(EventService.SESSION_UPDATED);
+                    } else if (!requireAnonymousBrowsing && !anonymousSessionProfile) {
+                        // User does not want to browse anonymously, use the real profile. Check that session contains the current profile.
+                        sessionProfile = eventsRequestContext.getProfile();
+                        if (!eventsRequestContext.getSession().getProfileId().equals(sessionProfile.getItemId())) {
+                            eventsRequestContext.addChanges(EventService.SESSION_UPDATED);
+                        }
+                        eventsRequestContext.getSession().setProfile(sessionProfile);
+                    }
+                }
+            }
+
+            // Try to create new session
+            if (eventsRequestContext.getSession() == null || invalidateSession) {
+                sessionProfile = privacyService.isRequireAnonymousBrowsing(eventsRequestContext.getProfile()) ?
+                        privacyService.getAnonymousProfile(eventsRequestContext.getProfile()) : eventsRequestContext.getProfile();
+
+                if (StringUtils.isNotBlank(sessionId)) {
+                    // Only save session and send event if a session id was provided, otherwise keep transient session
+                    eventsRequestContext.setSession(new Session(sessionId, sessionProfile, timestamp, scope));
+                    eventsRequestContext.addChanges(EventService.SESSION_UPDATED);
+
+                    Event event = new Event("sessionCreated", eventsRequestContext.getSession(), eventsRequestContext.getProfile(),
+                            scope, null, eventsRequestContext.getSession(), null, timestamp, false);
+                    if (sessionProfile.isAnonymousProfile()) {
+                        // Do not keep track of profile in event
+                        event.setProfileId(null);
+                    }
+                    event.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, request);
+                    event.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, response);
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Received event {} for profile={} session={} target={} timestamp={}", event.getEventType(),
+                                eventsRequestContext.getProfile().getItemId(), eventsRequestContext.getSession().getItemId(), event.getTarget(), timestamp);
+                    }
+                    eventsRequestContext.addChanges(eventService.send(event));
+                }
+            }
+
+            // Handle new profile creation
+            if (profileCreated) {
+                eventsRequestContext.addChanges(EventService.PROFILE_UPDATED);
 
-        String thirdPartyId = eventService
-                .authenticateThirdPartyServer(((HttpServletRequest) request).getHeader("X-Unomi-Peer"), request.getRemoteAddr());
+                Event profileUpdated = new Event("profileUpdated", eventsRequestContext.getSession(), eventsRequestContext.getProfile(),
+                        scope, null, eventsRequestContext.getProfile(), timestamp);
+                profileUpdated.setPersistent(false);
+                profileUpdated.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, request);
+                profileUpdated.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, response);
+                profileUpdated.getAttributes().put(Event.CLIENT_ID_ATTRIBUTE, DEFAULT_CLIENT_ID);
+
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Received event {} for profile={} {} target={} timestamp={}", profileUpdated.getEventType(),
+                            eventsRequestContext.getProfile().getItemId(),
+                            " session=" + (eventsRequestContext.getSession() != null ? eventsRequestContext.getSession().getItemId() : null),
+                            profileUpdated.getTarget(), timestamp);
+                }
+                eventsRequestContext.addChanges(eventService.send(profileUpdated));
+            }
+        }
+
+        return eventsRequestContext;
+    }
+
+    @Override
+    public EventsRequestContext performEventsRequest(List<Event> events, EventsRequestContext eventsRequestContext) {
+        List<String> filteredEventTypes = privacyService.getFilteredEventTypes(eventsRequestContext.getProfile());
+        String thirdPartyId = eventService.authenticateThirdPartyServer(eventsRequestContext.getRequest().getHeader("X-Unomi-Peer"),
+                eventsRequestContext.getRequest().getRemoteAddr());
 
-        int changes = EventService.NO_CHANGE;
         // execute provided events if any
-        int processedEventsCnt = 0;
-        if (events != null && !(profile instanceof Persona)) {
+        if (events != null && !(eventsRequestContext.getProfile() instanceof Persona)) {
+            // set Total items on context
+            eventsRequestContext.setTotalItems(events.size());
+
             for (Event event : events) {
-                processedEventsCnt++;
+                eventsRequestContext.setProcessedItems(eventsRequestContext.getProcessedItems() + 1);
+
                 if (event.getEventType() != null) {
-                    Event eventToSend = new Event(event.getEventType(), session, profile, event.getScope(), event.getSource(),
-                            event.getTarget(), event.getProperties(), timestamp, event.isPersistent());
+                    Event eventToSend = new Event(event.getEventType(), eventsRequestContext.getSession(), eventsRequestContext.getProfile(), event.getScope(), event.getSource(),
+                            event.getTarget(), event.getProperties(), eventsRequestContext.getTimestamp(), event.isPersistent());
                     eventToSend.setFlattenedProperties(event.getFlattenedProperties());
                     if (!eventService.isEventAllowed(event, thirdPartyId)) {
                         logger.warn("Event is not allowed : {}", event.getEventType());
                         continue;
                     }
                     if (thirdPartyId != null && event.getItemId() != null) {
-                        eventToSend = new Event(event.getItemId(), event.getEventType(), session, profile, event.getScope(),
-                                event.getSource(), event.getTarget(), event.getProperties(), timestamp, event.isPersistent());
+                        eventToSend = new Event(event.getItemId(), event.getEventType(), eventsRequestContext.getSession(), eventsRequestContext.getProfile(), event.getScope(),
+                                event.getSource(), event.getTarget(), event.getProperties(), eventsRequestContext.getTimestamp(), event.isPersistent());
                         eventToSend.setFlattenedProperties(event.getFlattenedProperties());
                     }
                     if (filteredEventTypes != null && filteredEventTypes.contains(event.getEventType())) {
                         logger.debug("Profile is filtering event type {}", event.getEventType());
                         continue;
                     }
-                    if (profile.isAnonymousProfile()) {
+                    if (eventsRequestContext.getProfile().isAnonymousProfile()) {
                         // Do not keep track of profile in event
                         eventToSend.setProfileId(null);
                     }
 
-                    eventToSend.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, request);
-                    eventToSend.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, response);
-                    logger.debug("Received event " + event.getEventType() + " for profile=" + profile.getItemId() + " session=" + (
-                            session != null ? session.getItemId() : null) + " target=" + event.getTarget() + " timestamp=" + timestamp);
-                    changes |= eventService.send(eventToSend);
+                    eventToSend.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, eventsRequestContext.getRequest());
+                    eventToSend.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, eventsRequestContext.getResponse());
+                    logger.debug("Received event " + event.getEventType() + " for profile=" + eventsRequestContext.getProfile().getItemId() + " session=" + (
+                            eventsRequestContext.getSession() != null ? eventsRequestContext.getSession().getItemId() : null) +
+                            " target=" + event.getTarget() + " timestamp=" + eventsRequestContext.getTimestamp());
+                    eventsRequestContext.addChanges(eventService.send(eventToSend));
                     // If the event execution changes the profile we need to update it so the next event use the right profile
-                    if ((changes & EventService.PROFILE_UPDATED) == EventService.PROFILE_UPDATED) {
-                        profile = eventToSend.getProfile();
+                    if ((eventsRequestContext.getChanges() & EventService.PROFILE_UPDATED) == EventService.PROFILE_UPDATED) {
+                        eventsRequestContext.setProfile(eventToSend.getProfile());
                     }
-                    if ((changes & EventService.ERROR) == EventService.ERROR) {
+                    if ((eventsRequestContext.getChanges() & EventService.ERROR) == EventService.ERROR) {
                         //Don't count the event that failed
-                        processedEventsCnt--;
-                        logger.error("Error processing events. Total number of processed events: {}/{}", processedEventsCnt, events.size());
+                        eventsRequestContext.setProcessedItems(eventsRequestContext.getProcessedItems() - 1);
+                        logger.error("Error processing events. Total number of processed events: {}/{}", eventsRequestContext.getProcessedItems(), eventsRequestContext.getTotalItems());
                         break;
                     }
                 }
             }
         }
-        return new Changes(changes, processedEventsCnt, profile);
+
+        return eventsRequestContext;
+    }
+
+    @Override
+    public void finalizeEventsRequest(EventsRequestContext eventsRequestContext, boolean crashOnError) {
+        // in case of changes on profile, persist the profile
+        if ((eventsRequestContext.getChanges() & EventService.PROFILE_UPDATED) == EventService.PROFILE_UPDATED) {
+            profileService.save(eventsRequestContext.getProfile());
+        }
+
+        // in case of changes on session, persist the session
+        if ((eventsRequestContext.getChanges() & EventService.SESSION_UPDATED) == EventService.SESSION_UPDATED && eventsRequestContext.getSession() != null) {
+            profileService.saveSession(eventsRequestContext.getSession());
+        }
+
+        // In case of error, return an error message
+        if ((eventsRequestContext.getChanges() & EventService.ERROR) == EventService.ERROR) {
+            if (crashOnError) {
+                String errorMessage = "Error processing events. Total number of processed events: " + eventsRequestContext.getProcessedItems() + "/"
+                        + eventsRequestContext.getTotalItems();
+                throw new BadRequestException(errorMessage);
+            } else {
+                eventsRequestContext.getResponse().setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            }
+        }
+
+        // Set profile cookie
+        if (!(eventsRequestContext.getProfile() instanceof Persona)) {
+            eventsRequestContext.getResponse().setHeader("Set-Cookie",
+                    HttpUtils.getProfileCookieString(eventsRequestContext.getProfile(), configSharingService, eventsRequestContext.getRequest().isSecure()));
+        }
+    }
+
+    private Profile createNewProfile(String existingProfileId, Date timestamp) {
+        Profile profile;
+        String profileId = existingProfileId;
+        if (profileId == null) {
+            profileId = UUID.randomUUID().toString();
+        }
+        profile = new Profile(profileId);
+        profile.setProperty("firstVisit", timestamp);
+        return profile;
     }
 }
diff --git a/rest/src/main/java/org/apache/unomi/utils/Changes.java b/rest/src/main/java/org/apache/unomi/utils/Changes.java
deleted file mode 100644
index b3334307d..000000000
--- a/rest/src/main/java/org/apache/unomi/utils/Changes.java
+++ /dev/null
@@ -1,57 +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.utils;
-
-import org.apache.unomi.api.Profile;
-
-/**
- * This class is a simple object to get the updated profile without the need of reloading it
- *
- * @author dgaillard
- */
-public class Changes {
-    private int changeType;
-    private int processedItems;
-    private Profile profile;
-
-    public Changes(int changeType, Profile profile) {
-        this(changeType,0,profile);
-    }
-
-    public Changes(int changeType, int processedItems, Profile profile) {
-        this.changeType = changeType;
-        this.processedItems = processedItems;
-        this.profile = profile;
-    }
-
-    public int getChangeType() {
-        return changeType;
-    }
-
-    public void setChangeType(int changeType) {
-        this.changeType = changeType;
-    }
-
-    public int getProcessedItems() {
-        return processedItems;
-    }
-
-    public Profile getProfile() {
-        return profile;
-    }
-}
diff --git a/rest/src/main/java/org/apache/unomi/utils/EventsRequestContext.java b/rest/src/main/java/org/apache/unomi/utils/EventsRequestContext.java
new file mode 100644
index 000000000..188f42da2
--- /dev/null
+++ b/rest/src/main/java/org/apache/unomi/utils/EventsRequestContext.java
@@ -0,0 +1,121 @@
+/*
+ * 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.utils;
+
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.Session;
+import org.apache.unomi.api.services.EventService;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Date;
+
+/**
+ * This is a bean that maintain the current situation during a request that contains events to be processed.
+ * It's in charge to hold an up to date Session + Profile for the current request, but also the status of the events executions:
+ * - changes
+ * - number of events processed
+ */
+public class EventsRequestContext {
+
+    private Date timestamp;
+    private Profile profile;
+    private Session session;
+    private HttpServletRequest request;
+    private HttpServletResponse response;
+    private int changes;
+    private int totalItems;
+    private int processedItems;
+
+    private EventsRequestContext() {
+    }
+
+    public EventsRequestContext(Date timestamp, Profile profile, Session session, HttpServletRequest request, HttpServletResponse response) {
+        this.timestamp = timestamp;
+        this.profile = profile;
+        this.session = session;
+        this.request = request;
+        this.response = response;
+        this.changes = EventService.NO_CHANGE;
+        this.totalItems = 0;
+        this.processedItems = 0;
+    }
+
+    public Date getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(Date timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public Profile getProfile() {
+        return profile;
+    }
+
+    public void setProfile(Profile profile) {
+        this.profile = profile;
+    }
+
+    public Session getSession() {
+        return session;
+    }
+
+    public void setSession(Session session) {
+        this.session = session;
+    }
+
+    public int getChanges() {
+        return changes;
+    }
+
+    public void addChanges(int changes) {
+        this.changes |= changes;
+    }
+
+    public int getTotalItems() {
+        return totalItems;
+    }
+
+    public void setTotalItems(int totalItems) {
+        this.totalItems = totalItems;
+    }
+
+    public int getProcessedItems() {
+        return processedItems;
+    }
+
+    public void setProcessedItems(int processedItems) {
+        this.processedItems = processedItems;
+    }
+
+    public HttpServletRequest getRequest() {
+        return request;
+    }
+
+    public void setRequest(HttpServletRequest request) {
+        this.request = request;
+    }
+
+    public HttpServletResponse getResponse() {
+        return response;
+    }
+
+    public void setResponse(HttpServletResponse response) {
+        this.response = response;
+    }
+}