This is an automated email from the ASF dual-hosted git repository. jkevan pushed a commit to branch visitPropertiesNewImplem in repository https://gitbox.apache.org/repos/asf/unomi.git
commit 352ba68bca9a53210a51fe41c2e44a650440c1b4 Author: Kevan <[email protected]> AuthorDate: Fri Jul 30 13:11:29 2021 +0200 UNOMI-338, UNOMI-482: better implementation for lastVisit, firstVisit and previousVisit properties --- .../test/java/org/apache/unomi/itests/BasicIT.java | 17 +++- .../unomi/itests/PropertiesUpdateActionIT.java | 92 +++++++++++++++++++++ .../test/resources/testSetPropertyActionRule.json | 31 +++++++ .../actions/EvaluateVisitPropertiesAction.java | 96 ++++++++++++++++++++++ .../baseplugin/actions/SetPropertyAction.java | 44 ++++------ .../cxs/actions/evaluateVisitPropertiesAction.json | 13 +++ .../META-INF/cxs/actions/setPropertyAction.json | 10 +++ .../META-INF/cxs/rules/sessionAssigned.json | 16 +--- .../resources/OSGI-INF/blueprint/blueprint.xml | 7 ++ 9 files changed, 281 insertions(+), 45 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/BasicIT.java b/itests/src/test/java/org/apache/unomi/itests/BasicIT.java index b40ecb8..1977508 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BasicIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BasicIT.java @@ -50,6 +50,7 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.File; import java.io.IOException; +import java.text.SimpleDateFormat; import java.util.*; @@ -165,6 +166,8 @@ public class BasicIT extends BaseIT { ContentType.create("application/json"))); TestUtils.RequestResponse requestResponsePageView1 = executeContextJSONRequest(requestPageView1, SESSION_ID_3); String profileIdVisitor1 = requestResponsePageView1.getContextResponse().getProfileId(); + String lastVisit = (String) requestResponsePageView1.getContextResponse().getProfileProperties().get("lastVisit"); + Assert.assertNotNull("Context profile properties should contains a lastVisit property", lastVisit); Thread.sleep(1000); // Initialize VISITOR_1 properties @@ -185,6 +188,8 @@ public class BasicIT extends BaseIT { Assert.assertEquals("Context profile id should be the same", profileIdVisitor1, requestResponseLoginVisitor1.getContextResponse().getProfileId()); checkVisitor1ResponseProperties(requestResponseLoginVisitor1.getContextResponse().getProfileProperties()); + Assert.assertEquals("LastVisit property should not be updated as we are on the same session", lastVisit, + requestResponseLoginVisitor1.getContextResponse().getProfileProperties().get("lastVisit")); Thread.sleep(1000); // Lets add a page view with VISITOR_1 to simulate reloading the page after login and be able to check the profile properties @@ -196,6 +201,8 @@ public class BasicIT extends BaseIT { Assert.assertEquals("Context profile id should be the same", profileIdVisitor1, requestResponsePageView2.getContextResponse().getProfileId()); checkVisitor1ResponseProperties(requestResponsePageView2.getContextResponse().getProfileProperties()); + Assert.assertEquals("LastVisit property should not be updated as we are on the same session", lastVisit, + requestResponsePageView2.getContextResponse().getProfileProperties().get("lastVisit")); Thread.sleep(1000); // Lets simulate a logout by requesting the context with a new page view event and a new session id @@ -209,6 +216,12 @@ public class BasicIT extends BaseIT { Assert.assertEquals("Context profile id should be the same", profileIdVisitor1, requestResponsePageView3.getContextResponse().getProfileId()); checkVisitor1ResponseProperties(requestResponsePageView3.getContextResponse().getProfileProperties()); + Assert.assertEquals("previousVisit property should be updated as we are on a new session", lastVisit, + requestResponsePageView3.getContextResponse().getProfileProperties().get("previousVisit")); + Assert.assertNotEquals("lastVisit property should be updated as we are on a new session", lastVisit, + requestResponsePageView3.getContextResponse().getProfileProperties().get("lastVisit")); + lastVisit = (String) requestResponsePageView3.getContextResponse().getProfileProperties().get("lastVisit"); + Assert.assertNotNull("Context profile properties should contains a lastVisit property", lastVisit); Thread.sleep(1000); // Initialize VISITOR_2 properties @@ -265,7 +278,7 @@ public class BasicIT extends BaseIT { contextRequest.setSource(sourceSite); contextRequest.setRequireSegments(false); contextRequest.setEvents(Collections.singletonList(loginEvent)); - contextRequest.setRequiredProfileProperties(Arrays.asList(FIRST_NAME, LAST_NAME, EMAIL)); + contextRequest.setRequiredProfileProperties(Arrays.asList(FIRST_NAME, LAST_NAME, EMAIL, "firstVisit", "lastVisit", "previousVisit")); contextRequest.setSessionId(sessionId); return contextRequest; } @@ -290,7 +303,7 @@ public class BasicIT extends BaseIT { contextRequest.setSource(customPageItem); contextRequest.setRequireSegments(false); contextRequest.setEvents(Collections.singletonList(pageViewEvent)); - contextRequest.setRequiredProfileProperties(Arrays.asList(FIRST_NAME, LAST_NAME, EMAIL)); + contextRequest.setRequiredProfileProperties(Arrays.asList(FIRST_NAME, LAST_NAME, EMAIL, "firstVisit", "lastVisit", "previousVisit")); return contextRequest; } diff --git a/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java b/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java index f161632..65f69c8 100644 --- a/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java @@ -19,8 +19,11 @@ package org.apache.unomi.itests; import org.apache.unomi.api.Event; import org.apache.unomi.api.Profile; +import org.apache.unomi.api.rules.Rule; import org.apache.unomi.api.services.EventService; import org.apache.unomi.api.services.ProfileService; +import org.apache.unomi.api.services.RulesService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.apache.unomi.plugins.baseplugin.actions.UpdatePropertiesAction; import org.junit.Assert; import org.junit.Before; @@ -34,7 +37,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; +import java.io.File; import java.io.IOException; +import java.net.MalformedURLException; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.*; /** @@ -53,6 +61,8 @@ public class PropertiesUpdateActionIT extends BaseIT { protected ProfileService profileService; @Inject @Filter(timeout = 600000) protected EventService eventService; + @Inject @Filter(timeout = 600000) + protected RulesService rulesService; @Before public void setUp() throws IOException, InterruptedException { @@ -235,4 +245,86 @@ public class PropertiesUpdateActionIT extends BaseIT { Assert.assertEquals("New property 2", profile.getProperty("prop2")); Assert.assertEquals("New property 3", profile.getProperty("prop3")); } + + @Test + public void testVisitsDatePropertiesUpdate() throws InterruptedException { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + + Profile profile = profileService.load(PROFILE_TEST_ID); + + // Test init of lastVisit and firstVisit + Date eventTimeStamp = new Date(); + String eventTimeStamp1 = dateFormat.format(eventTimeStamp); + Event sessionReassigned = new Event("sessionReassigned", null, profile, null, null, profile, eventTimeStamp); + sessionReassigned.setPersistent(false); + eventService.send(sessionReassigned); + profileService.save(profile); + refreshPersistence(); + profile = profileService.load(PROFILE_TEST_ID); + Assert.assertEquals("lastVisit should be updated", eventTimeStamp1, profile.getProperty("lastVisit")); + Assert.assertEquals("firstVisit should be updated", eventTimeStamp1, profile.getProperty("firstVisit")); + Assert.assertNull("previousVisit should be null", profile.getProperty("previousVisit")); + + // test event dated -1 day: should update only firstVisit + eventTimeStamp = new Date(); + LocalDateTime ldt = LocalDateTime.ofInstant(eventTimeStamp.toInstant(), ZoneId.systemDefault()); + ldt = ldt.minusDays(1); + eventTimeStamp = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + String eventTimeStamp2 = dateFormat.format(eventTimeStamp); + sessionReassigned = new Event("sessionReassigned", null, profile, null, null, profile, eventTimeStamp); + sessionReassigned.setPersistent(false); + eventService.send(sessionReassigned); + profileService.save(profile); + refreshPersistence(); + profile = profileService.load(PROFILE_TEST_ID); + Assert.assertEquals("lastVisit should not be updated", eventTimeStamp1, profile.getProperty("lastVisit")); + Assert.assertEquals("firstVisit should be updated", eventTimeStamp2, profile.getProperty("firstVisit")); + Assert.assertNull("previousVisit should be null", profile.getProperty("previousVisit")); + + // test event dated +1 day: should update only lastVisit and previousVisit + eventTimeStamp = new Date(); + ldt = LocalDateTime.ofInstant(eventTimeStamp.toInstant(), ZoneId.systemDefault()); + ldt = ldt.plusDays(1); + eventTimeStamp = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + String eventTimeStamp3 = dateFormat.format(eventTimeStamp); + sessionReassigned = new Event("sessionReassigned", null, profile, null, null, profile, eventTimeStamp); + sessionReassigned.setPersistent(false); + eventService.send(sessionReassigned); + profileService.save(profile); + refreshPersistence(); + profile = profileService.load(PROFILE_TEST_ID); + Assert.assertEquals("lastVisit should be updated", eventTimeStamp3, profile.getProperty("lastVisit")); + Assert.assertEquals("firstVisit should not be updated", eventTimeStamp2, profile.getProperty("firstVisit")); + Assert.assertEquals("previousVisit should be updated", eventTimeStamp1, profile.getProperty("previousVisit")); + } + + @Test + public void testSetPropertyActionDates() throws InterruptedException, IOException { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + + // register test rule + Rule rule = CustomObjectMapper.getObjectMapper().readValue(getValidatedBundleJSON("testSetPropertyActionRule.json", new HashMap<>()), Rule.class); + rulesService.setRule(rule); + Thread.sleep(2000); + + try { + Profile profile = profileService.load(PROFILE_TEST_ID); + Date eventTimeStamp = new Date(); + String eventTimeStamp1 = dateFormat.format(eventTimeStamp); + Event sessionReassigned = new Event("sessionReassigned", null, profile, null, null, profile, eventTimeStamp); + sessionReassigned.setPersistent(false); + Thread.sleep(4000); // small sleep to create time dif between eventTimeStamp and current system date + eventService.send(sessionReassigned); + profileService.save(profile); + refreshPersistence(); + profile = profileService.load(PROFILE_TEST_ID); + Assert.assertEquals("currentEventTimeStamp should be the exact date of the event timestamp", eventTimeStamp1, profile.getProperty("currentEventTimeStamp")); + Assert.assertNotEquals("currentDate should be the current system date", eventTimeStamp1, profile.getProperty("currentDate")); + Assert.assertNotNull("currentDate should be set", profile.getProperty("currentDate")); + } finally { + rulesService.removeRule(rule.getItemId()); + } + } } diff --git a/itests/src/test/resources/testSetPropertyActionRule.json b/itests/src/test/resources/testSetPropertyActionRule.json new file mode 100644 index 0000000..d618289 --- /dev/null +++ b/itests/src/test/resources/testSetPropertyActionRule.json @@ -0,0 +1,31 @@ +{ + "metadata": { + "id": "testSetPropertyAction", + "name": "testSetPropertyAction", + "description": "" + }, + "condition": { + "type": "eventTypeCondition", + "parameterValues": { + "eventTypeId": "sessionReassigned" + } + }, + "actions": [ + { + "parameterValues": { + "setPropertyName": "properties.currentEventTimeStamp", + "setPropertyValueCurrentEventTimestamp": true, + "storeInSession": false + }, + "type": "setPropertyAction" + }, + { + "parameterValues": { + "setPropertyName": "properties.currentDate", + "setPropertyValueCurrentDate": true, + "storeInSession": false + }, + "type": "setPropertyAction" + } + ] +} diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/EvaluateVisitPropertiesAction.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/EvaluateVisitPropertiesAction.java new file mode 100644 index 0000000..353e74f --- /dev/null +++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/EvaluateVisitPropertiesAction.java @@ -0,0 +1,96 @@ +/* + * 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.plugins.baseplugin.actions; + +import org.apache.unomi.api.Event; +import org.apache.unomi.api.Profile; +import org.apache.unomi.api.actions.Action; +import org.apache.unomi.api.actions.ActionExecutor; +import org.apache.unomi.api.services.EventService; +import org.apache.unomi.persistence.spi.PropertyHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +/** + * This action is used to calculate the firstVisit, lastVisit and previousVisit date properties on the profile + * Depending on the event timestamp it will adjust one or multiples of this properties accordingly to the logical chronology. + */ +public class EvaluateVisitPropertiesAction implements ActionExecutor { + private static final Logger logger = LoggerFactory.getLogger(EvaluateVisitPropertiesAction.class.getName()); + + public int execute(Action action, Event event) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + + Date currentEventTimeStamp = event.getTimeStamp(); + Date currentProfileFirstVisit = extractDateFromProperty(event.getProfile(), "firstVisit", dateFormat); + Date currentProfileLastVisit = extractDateFromProperty(event.getProfile(), "lastVisit", dateFormat); + + int result = EventService.NO_CHANGE; + + // check update firstVisit + if (currentProfileFirstVisit == null || currentProfileFirstVisit.after(currentEventTimeStamp)) { + result = PropertyHelper.setProperty(event.getProfile(), "properties.firstVisit", dateFormat.format(currentEventTimeStamp), "alwaysSet") ? + EventService.PROFILE_UPDATED : result; + } + + // check update lastVisit and previousVisit + if (currentProfileLastVisit == null || currentProfileLastVisit.before(currentEventTimeStamp)) { + // update lastVisit + if (PropertyHelper.setProperty(event.getProfile(), "properties.lastVisit", dateFormat.format(currentEventTimeStamp), "alwaysSet")) { + result = EventService.PROFILE_UPDATED; + + // check update previousVisit + if (currentProfileLastVisit != null) { + PropertyHelper.setProperty(event.getProfile(), "properties.previousVisit", dateFormat.format(currentProfileLastVisit), "alwaysSet"); + } + } + } + + return result; + } + + private Date extractDateFromProperty(Profile profile, String propertyName, DateFormat dateFormat) { + Object property = profile.getProperties().get(propertyName); + Date date = null; + try { + if (property != null) { + if (property instanceof String) { + date = dateFormat.parse((String) property); + } else if (property instanceof Date) { + date = (Date) property; + } else { + date = dateFormat.parse(property.toString()); + } + } + } catch (ParseException e) { + logger.error("Error parsing {} date property. See debug log level for more information", propertyName); + if (logger.isDebugEnabled()) { + logger.debug("Error parsing date: {}, on profile: {}", property, profile.getItemId(), e); + } + } + + return date; + } +} \ No newline at end of file diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetPropertyAction.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetPropertyAction.java index 6bbc9d8..f518dba 100644 --- a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetPropertyAction.java +++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetPropertyAction.java @@ -58,6 +58,11 @@ public class SetPropertyAction implements ActionExecutor { Object propertyValueInteger = action.getParameterValues().get("setPropertyValueInteger"); Object setPropertyValueMultiple = action.getParameterValues().get("setPropertyValueMultiple"); Object setPropertyValueBoolean = action.getParameterValues().get("setPropertyValueBoolean"); + Object setPropertyValueCurrentEventTimestamp = action.getParameterValues().get("setPropertyValueCurrentEventTimestamp"); + Object setPropertyValueCurrentDate = action.getParameterValues().get("setPropertyValueCurrentDate"); + + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); if (propertyValue == null) { if (propertyValueInteger != null) { @@ -69,37 +74,18 @@ public class SetPropertyAction implements ActionExecutor { if (setPropertyValueBoolean != null) { propertyValue = PropertyHelper.getBooleanValue(setPropertyValueBoolean); } - } - if (propertyValue != null && propertyValue.equals("now")) { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - - Date date = new Date(); - Date firstVisit = new Date(); - - Object propertyFirstVisit = event.getProfile().getProperties().get("firstVisit"); - try { - if (propertyFirstVisit != null) { - if (propertyFirstVisit instanceof String) { - firstVisit = format.parse((String) propertyFirstVisit); - } else if (propertyFirstVisit instanceof Date) { - firstVisit = (Date) propertyFirstVisit; - } else { - firstVisit = format.parse(propertyFirstVisit.toString()); - } - } - - if (event.getTimeStamp().after(firstVisit)) { - date = event.getTimeStamp(); - } - } catch (ParseException e) { - logger.error("Error parsing firstVisit date property. See debug log level for more information"); - if (logger.isDebugEnabled()) { - logger.debug("Error parsing date: {}", propertyFirstVisit, e); - } + if (setPropertyValueCurrentEventTimestamp != null && PropertyHelper.getBooleanValue(setPropertyValueCurrentEventTimestamp)) { + propertyValue = format.format(event.getTimeStamp()); + } + if (setPropertyValueCurrentDate != null && PropertyHelper.getBooleanValue(setPropertyValueCurrentDate)) { + propertyValue = format.format(new Date()); } + } - propertyValue = format.format(date); + if (propertyValue != null && propertyValue.equals("now")) { + logger.warn("SetPropertyAction with setPropertyValue: 'now' is deprecated, " + + "please use 'setPropertyValueCurrentEventTimestamp' or 'setPropertyValueCurrentDate' instead of 'setPropertyValue'"); + propertyValue = format.format(event.getTimeStamp()); } if (storeInSession) { diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/evaluateVisitPropertiesAction.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/evaluateVisitPropertiesAction.json new file mode 100644 index 0000000..37b8ac4 --- /dev/null +++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/evaluateVisitPropertiesAction.json @@ -0,0 +1,13 @@ +{ + "metadata": { + "id": "evaluateVisitPropertiesAction", + "name": "evaluateVisitPropertiesAction", + "description": "", + "systemTags": [ + "profileTags" + ], + "readOnly": true + }, + "actionExecutor": "evaluateVisitProperties", + "parameters": [] +} \ No newline at end of file diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/setPropertyAction.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/setPropertyAction.json index dad270d..0606bdd 100644 --- a/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/setPropertyAction.json +++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/setPropertyAction.json @@ -39,6 +39,16 @@ "multivalued": true }, { + "id": "setPropertyValueCurrentEventTimestamp", + "type": "boolean", + "multivalued": false + }, + { + "id": "setPropertyValueCurrentDate", + "type": "boolean", + "multivalued": false + }, + { "id": "setPropertyStrategy", "type": "string", "multivalued": false diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/rules/sessionAssigned.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/rules/sessionAssigned.json index 53cfef5..d453f21 100644 --- a/plugins/baseplugin/src/main/resources/META-INF/cxs/rules/sessionAssigned.json +++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/rules/sessionAssigned.json @@ -31,20 +31,8 @@ "actions": [ { - "parameterValues": { - "setPropertyName": "properties.previousVisit", - "setPropertyValue": "profileProperty::lastVisit", - "storeInSession": false - }, - "type": "setPropertyAction" - }, - { - "parameterValues": { - "setPropertyName": "properties.lastVisit", - "setPropertyValue": "now", - "storeInSession": false - }, - "type": "setPropertyAction" + "parameterValues": {}, + "type": "evaluateVisitPropertiesAction" }, { "parameterValues": { diff --git a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml index 36dd112..f2c3846 100644 --- a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -221,6 +221,13 @@ <service interface="org.apache.unomi.api.actions.ActionExecutor"> <service-properties> + <entry key="actionExecutorId" value="evaluateVisitProperties"/> + </service-properties> + <bean class="org.apache.unomi.plugins.baseplugin.actions.EvaluateVisitPropertiesAction"/> + </service> + + <service interface="org.apache.unomi.api.actions.ActionExecutor"> + <service-properties> <entry key="actionExecutorId" value="updateProperties"/> </service-properties> <bean class="org.apache.unomi.plugins.baseplugin.actions.UpdatePropertiesAction">
