diff --git a/CHANGELOG.md b/CHANGELOG.md index fb570b15..6573fca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ ## 24.1.4 - -* Updated user properties caching mechanism according to sessions. +* ! Minor breaking change ! User properties will now be automatically saved under the following conditions: + * When an event is recorded + * During an internal timer tick + * Upon flushing the event queue + * When a session call made * Cleaned up unused gradle dependencies from root build.gradle. ## 24.1.3 - * Extended minimum JDK support to 8. ## 24.1.2 diff --git a/sdk-java/src/main/java/ly/count/sdk/java/Config.java b/sdk-java/src/main/java/ly/count/sdk/java/Config.java index 953521ff..91a4a6c7 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/Config.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/Config.java @@ -243,7 +243,7 @@ public class Config { protected String city = null; protected String country = null; protected boolean locationEnabled = true; - protected boolean autoSendUserPropertiesOnSessions = true; + protected boolean autoSendUserProperties = true; // TODO: storage limits & configuration // protected int maxRequestsStored = 0; @@ -1482,13 +1482,19 @@ public String toString() { } } + // Disabling new Added features + /** - * Disable automatic sending of user properties on session begin, update and end + * Disable automatic sending of user properties on + * - When an event is recorded + * - During an internal timer tick + * - Upon flushing the event queue + * - When a session call made * * @return {@code this} instance for method chaining */ - public Config disableAutoSendUserPropertiesOnSessions() { - this.autoSendUserPropertiesOnSessions = false; + public Config disableAutoSendUserProperties() { + this.autoSendUserProperties = false; return this; } } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java index 9aa60e9e..d446315d 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/InternalConfig.java @@ -213,7 +213,7 @@ boolean isLocationDisabled() { return !locationEnabled; } - boolean isAutoSendUserPropertiesOnSessions() { - return autoSendUserPropertiesOnSessions; + boolean isAutoSendUserProperties() { + return autoSendUserProperties; } } diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java index 4cae3bb5..1e48a875 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleEvents.java @@ -109,6 +109,10 @@ protected void recordEventInternal(String key, int count, Double sum, Double dur Utils.removeInvalidDataFromSegments(segmentation, L); + if (internalConfig.isAutoSendUserProperties() && internalConfig.sdk.userProfile() != null) { + internalConfig.sdk.module(ModuleUserProfile.class).saveInternal(); + } + String eventId, pvid = null, cvid = null; if (Utils.isEmptyOrNull(eventIdOverride)) { L.d("[ModuleEvents] recordEventInternal, Generating new event id because it was null or empty"); @@ -139,7 +143,7 @@ private void addEventToQueue(EventImpl event) { checkEventQueueToSend(false); } - private void checkEventQueueToSend(boolean forceSend) { + void checkEventQueueToSend(boolean forceSend) { L.d("[ModuleEvents] queue size:[" + eventQueue.eqSize() + "] || forceSend: " + forceSend); if (forceSend || eventQueue.eqSize() >= internalConfig.getEventsBufferSize()) { addEventsToRequestQ(null); diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java index f783a29d..41aeb5da 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/ModuleUserProfile.java @@ -259,7 +259,17 @@ protected void saveInternal() { if (internalConfig.sdk.location() != null) { internalConfig.sdk.module(ModuleLocation.class).saveLocationToParamsLegacy(generatedParams); } + L.d("[ModuleUserProfile] saveInternal, generated params [" + generatedParams + "]"); + if (generatedParams.length() <= 0) { + L.d("[ModuleUserProfile] saveInternal, nothing to save returning"); + return; + } + + if (internalConfig.isAutoSendUserProperties() && internalConfig.sdk.events() != null) { + internalConfig.sdk.module(ModuleEvents.class).checkEventQueueToSend(true); + } + ModuleRequests.pushAsync(internalConfig, new Request(generatedParams)); clearInternal(); } @@ -288,6 +298,22 @@ public void stop(InternalConfig config, boolean clearData) { userProfileInterface = null; } + @Override + protected void onTimer() { + if (internalConfig.isAutoSendUserProperties()) { + saveInternal(); + } + } + + @Override + public void deviceIdChanged(String oldDeviceId, boolean withMerge) { + super.deviceIdChanged(oldDeviceId, withMerge); + L.d("[ModuleUserProfile] deviceIdChanged: oldDeviceId = " + oldDeviceId + ", withMerge = " + withMerge); + if (internalConfig.isAutoSendUserProperties() && !withMerge) { + saveInternal(); + } + } + public class UserProfile { /** diff --git a/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java b/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java index f966034f..295bbe7b 100644 --- a/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java +++ b/sdk-java/src/main/java/ly/count/sdk/java/internal/SessionImpl.java @@ -118,7 +118,7 @@ Future begin(Long now) { } this.consents = SDKCore.instance.consents; - if (config.isAutoSendUserPropertiesOnSessions() && config.sdk.userProfile() != null) { + if (config.isAutoSendUserProperties() && config.sdk.userProfile() != null) { config.sdk.module(ModuleUserProfile.class).saveInternal(); } @@ -160,7 +160,7 @@ Future update(Long now) { } this.consents = SDKCore.instance.consents; - if (config.isAutoSendUserPropertiesOnSessions() && config.sdk.userProfile() != null) { + if (config.isAutoSendUserProperties() && config.sdk.userProfile() != null) { config.sdk.module(ModuleUserProfile.class).saveInternal(); } @@ -198,7 +198,7 @@ Future end(Long now, final Tasks.Callback callback, String did ended = now == null ? System.nanoTime() : now; this.consents = SDKCore.instance.consents; - if (config.isAutoSendUserPropertiesOnSessions() && config.sdk.userProfile() != null) { + if (config.isAutoSendUserProperties() && config.sdk.userProfile() != null) { config.sdk.module(ModuleUserProfile.class).saveInternal(); } diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleEventsTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleEventsTests.java index 66f7c7ed..ac776174 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleEventsTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/ModuleEventsTests.java @@ -442,6 +442,137 @@ public void timedEventFlow() throws InterruptedException { TestUtils.validateEventInEQ(eKeys[0], null, 2, 4.0, 2.0, 1, 2, TestUtils.keysValues[1], null, "", TestUtils.keysValues[0]); } + /** + * Recording events with user properties and with flushing events + * Validating that if a user property set before a recordEvent call it is sent before adding the event to EQ + * And also user properties packed after flushing events. + * + * @throws InterruptedException when sleep is interrupted + */ + @Test + public void eventsUserProps() throws InterruptedException { + init(TestUtils.getConfigEvents(4).setUpdateSessionTimerDelay(2)); + + Countly.instance().userProfile().setProperty("before_event", "value1"); + Countly.instance().events().recordEvent(eKeys[0]); + + Map[] RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(1, RQ.length); + Assert.assertEquals(TestUtils.json("custom", TestUtils.map("before_event", "value1")), RQ[0].get("user_details")); + TestUtils.validateEventInEQ(eKeys[0], null, 1, null, null, 0, 1, "_CLY_", null, "", null); + + Countly.instance().userProfile().setProperty("after_event", "value2"); + Thread.sleep(2500); // wait for the tick + RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(3, RQ.length); + Assert.assertTrue(RQ[1].containsKey("events")); + Assert.assertEquals(TestUtils.json("custom", TestUtils.map("after_event", "value2")), RQ[2].get("user_details")); + } + + /** + * Recording events with user properties and with flushing events will not work because reversed + * + * @throws InterruptedException when sleep is interrupted + */ + @Test + public void eventsUserProps_reversed() throws InterruptedException { + init(TestUtils.getConfigEvents(4).setUpdateSessionTimerDelay(2).disableAutoSendUserProperties()); + + Countly.instance().userProfile().setProperty("before_event", "value1"); + Countly.instance().events().recordEvent(eKeys[0]); + + Map[] RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(0, RQ.length); + TestUtils.validateEventInEQ(eKeys[0], null, 1, null, null, 0, 1, "_CLY_", null, "", null); + + Countly.instance().userProfile().setProperty("after_event", "value2"); + Thread.sleep(2500); // wait for the tick + RQ = TestUtils.getCurrentRQ(); + + Assert.assertEquals(1, RQ.length); + Assert.assertTrue(RQ[0].containsKey("events")); + } + + /** + * Recording events with user properties and with flushing events + * Validating that if a user property save called, it flushes EQ before saving user properties + */ + @Test + public void eventsUserProps_propsSave() { + init(TestUtils.getConfigEvents(4)); + + Countly.instance().events().recordEvent(eKeys[0]); + + Map[] RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(0, RQ.length); + TestUtils.validateEventInEQ(eKeys[0], null, 1, null, null, 0, 1, "_CLY_", null, "", null); + + Countly.instance().userProfile().setProperty("after_event", "value2"); + Countly.instance().userProfile().save(); + + RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(2, RQ.length); + Assert.assertTrue(RQ[0].containsKey("events")); + Assert.assertEquals(TestUtils.json("custom", TestUtils.map("after_event", "value2")), RQ[1].get("user_details")); + } + + /** + * Recording events with user properties and with flushing events + * Validating that if a user property save called, it does not flush EQ before saving user properties + */ + @Test + public void eventsUserProps_propsSave_reversed() { + init(TestUtils.getConfigEvents(4).disableAutoSendUserProperties()); + + Countly.instance().events().recordEvent(eKeys[0]); + + Map[] RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(0, RQ.length); + TestUtils.validateEventInEQ(eKeys[0], null, 1, null, null, 0, 1, "_CLY_", null, "", null); + + Countly.instance().userProfile().setProperty("after_event", "value2"); + Countly.instance().userProfile().save(); + + RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(1, RQ.length); + Assert.assertEquals(TestUtils.json("custom", TestUtils.map("after_event", "value2")), RQ[0].get("user_details")); + } + + /** + * Validate that user properties are sent with timer tick if no events are recorded + */ + @Test + public void eventsUserProps_timer() throws InterruptedException { + init(TestUtils.getConfigEvents(4).setUpdateSessionTimerDelay(2)); + + Countly.instance().userProfile().setProperty("before_timer", "value1"); + + Map[] RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(0, RQ.length); + + Thread.sleep(2500); // wait for the tick + RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(1, RQ.length); + Assert.assertEquals(TestUtils.json("custom", TestUtils.map("before_timer", "value1")), RQ[0].get("user_details")); + } + + /** + * Validate that user properties does not send with timer tick if no events are recorded + */ + @Test + public void eventsUserProps_timer_reversed() throws InterruptedException { + init(TestUtils.getConfigEvents(4).setUpdateSessionTimerDelay(2).disableAutoSendUserProperties()); + + Countly.instance().userProfile().setProperty("before_timer", "value1"); + + Map[] RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(0, RQ.length); + + Thread.sleep(2500); // wait for the tick + RQ = TestUtils.getCurrentRQ(); + Assert.assertEquals(0, RQ.length); + } + private void validateTimedEventSize(int expectedQueueSize, int expectedTimedEventSize) { TestUtils.validateEQSize(expectedQueueSize, TestUtils.getCurrentEQ(), moduleEvents.eventQueue); Assert.assertEquals(expectedTimedEventSize, moduleEvents.timedEvents.size()); diff --git a/sdk-java/src/test/java/ly/count/sdk/java/internal/SessionImplTests.java b/sdk-java/src/test/java/ly/count/sdk/java/internal/SessionImplTests.java index 52e16e10..74b38403 100644 --- a/sdk-java/src/test/java/ly/count/sdk/java/internal/SessionImplTests.java +++ b/sdk-java/src/test/java/ly/count/sdk/java/internal/SessionImplTests.java @@ -680,7 +680,7 @@ public void userPropsOnSessions() throws InterruptedException { */ @Test public void userPropsOnSessions_reversed() throws InterruptedException { - Countly.instance().init(TestUtils.getConfigSessions(Config.Feature.UserProfiles).disableAutoSendUserPropertiesOnSessions()); + Countly.instance().init(TestUtils.getConfigSessions(Config.Feature.UserProfiles).disableAutoSendUserProperties()); Countly.instance().userProfile().setProperty("name", "John Doe"); Countly.instance().userProfile().setProperty("custom_key", "custom_value");