From 26b1929aac901f6e037fd15e008b3386af07907a Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Nov 2025 16:33:43 +0300 Subject: [PATCH 01/10] feat: add check to prevent empty user details --- .../java/ly/count/sdk/java/internal/ModuleUserProfile.java | 6 ++++++ 1 file changed, 6 insertions(+) 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..b860caa6 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,13 @@ 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; + } + ModuleRequests.pushAsync(internalConfig, new Request(generatedParams)); clearInternal(); } From 0d63a433fd2092f48f01b30b846ffa0e31342e6e Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Nov 2025 16:34:06 +0300 Subject: [PATCH 02/10] feat: save events before user props sending --- .../main/java/ly/count/sdk/java/internal/ModuleEvents.java | 2 +- .../java/ly/count/sdk/java/internal/ModuleUserProfile.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) 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..1db63aa0 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 @@ -139,7 +139,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 b860caa6..e0c77ea6 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 @@ -266,6 +266,10 @@ protected void saveInternal() { return; } + if (internalConfig.sdk.events() != null) { + internalConfig.sdk.module(ModuleEvents.class).checkEventQueueToSend(true); + } + ModuleRequests.pushAsync(internalConfig, new Request(generatedParams)); clearInternal(); } From d304bbf8849023eec4caa8606c37e4e670e92dfc Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Nov 2025 16:52:13 +0300 Subject: [PATCH 03/10] feat: user prop save on timer and without merge --- .../count/sdk/java/internal/ModuleUserProfile.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 e0c77ea6..b6975ae5 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 @@ -298,6 +298,20 @@ public void stop(InternalConfig config, boolean clearData) { userProfileInterface = null; } + @Override + protected void onTimer() { + saveInternal(); + } + + @Override + public void deviceIdChanged(String oldDeviceId, boolean withMerge) { + super.deviceIdChanged(oldDeviceId, withMerge); + L.d("[ModuleUserProfile] deviceIdChanged: oldDeviceId = " + oldDeviceId + ", withMerge = " + withMerge); + if (!withMerge) { + saveInternal(); + } + } + public class UserProfile { /** From fe7add182c13b2cf832b5ec73bbb98d2e1c4c5d3 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Nov 2025 18:21:29 +0300 Subject: [PATCH 04/10] feat: user prop save on event recording --- .../main/java/ly/count/sdk/java/internal/ModuleEvents.java | 4 ++++ 1 file changed, 4 insertions(+) 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 1db63aa0..f67ed04e 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.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"); From 7b3517b554c3727445047853f64728182adc5e1e Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 24 Nov 2025 18:22:58 +0300 Subject: [PATCH 05/10] feat: user prop auto-save changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbdeb7c8..cf650a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ +## XX.XX.XX +* ! 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 + ## 24.1.3 - * Extended minimum JDK support to 8. ## 24.1.2 From a362a5f5527e19e627780caec0191f6061de3256 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 3 Dec 2025 11:16:00 +0300 Subject: [PATCH 06/10] feat: reverse config --- .../src/main/java/ly/count/sdk/java/Config.java | 16 ++++++++++++++++ .../count/sdk/java/internal/InternalConfig.java | 5 +++++ 2 files changed, 21 insertions(+) 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 c4f2372b..b982b450 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,6 +243,7 @@ public class Config { protected String city = null; protected String country = null; protected boolean locationEnabled = true; + protected boolean autoSendUserProperties = true; // TODO: storage limits & configuration // protected int maxRequestsStored = 0; @@ -1480,4 +1481,19 @@ public String toString() { return "DID " + id + " ( " + strategy + ")"; } } + + // Disabling new Added features + + /** + * Disable automatic sending of user properties on + * - When an event is recorded + * - During an internal timer tick + * - Upon flushing the event queue + * + * @return {@code this} instance for method chaining + */ + 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 5c51185b..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 @@ -32,6 +32,7 @@ public class InternalConfig extends Config { protected IdGenerator viewIdGenerator; protected IdGenerator eventIdGenerator; protected ViewIdProvider viewIdProvider; + /** * Shouldn't be used! */ @@ -211,4 +212,8 @@ String[] getLocationParams() { boolean isLocationDisabled() { return !locationEnabled; } + + boolean isAutoSendUserProperties() { + return autoSendUserProperties; + } } From 97e609481b366c1ebdf4982455449a65862a29c0 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 3 Dec 2025 12:11:16 +0300 Subject: [PATCH 07/10] feat: make it reversable --- .../main/java/ly/count/sdk/java/internal/ModuleEvents.java | 2 +- .../java/ly/count/sdk/java/internal/ModuleUserProfile.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 f67ed04e..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,7 +109,7 @@ protected void recordEventInternal(String key, int count, Double sum, Double dur Utils.removeInvalidDataFromSegments(segmentation, L); - if (internalConfig.sdk.userProfile() != null) { + if (internalConfig.isAutoSendUserProperties() && internalConfig.sdk.userProfile() != null) { internalConfig.sdk.module(ModuleUserProfile.class).saveInternal(); } 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 b6975ae5..f878ef84 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 @@ -266,7 +266,7 @@ protected void saveInternal() { return; } - if (internalConfig.sdk.events() != null) { + if (internalConfig.isAutoSendUserProperties() && internalConfig.sdk.events() != null) { internalConfig.sdk.module(ModuleEvents.class).checkEventQueueToSend(true); } @@ -307,7 +307,7 @@ protected void onTimer() { public void deviceIdChanged(String oldDeviceId, boolean withMerge) { super.deviceIdChanged(oldDeviceId, withMerge); L.d("[ModuleUserProfile] deviceIdChanged: oldDeviceId = " + oldDeviceId + ", withMerge = " + withMerge); - if (!withMerge) { + if (internalConfig.isAutoSendUserProperties() && !withMerge) { saveInternal(); } } From acdc4a7097162c9a5f2d832c99720a9d5ceb7219 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 5 Dec 2025 10:21:19 +0300 Subject: [PATCH 08/10] feat: tests for the user props auto save --- .../sdk/java/internal/ModuleEventsTests.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) 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..70fe8fb1 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,74 @@ 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 + * 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")); + } + + /** + * 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")); + } + private void validateTimedEventSize(int expectedQueueSize, int expectedTimedEventSize) { TestUtils.validateEQSize(expectedQueueSize, TestUtils.getCurrentEQ(), moduleEvents.eventQueue); Assert.assertEquals(expectedTimedEventSize, moduleEvents.timedEvents.size()); From cf4fa094c8b60b53983c35474573da8efe530827 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 5 Dec 2025 13:11:34 +0300 Subject: [PATCH 09/10] fix: missing call --- .../sdk/java/internal/ModuleUserProfile.java | 4 +++- .../sdk/java/internal/ModuleEventsTests.java | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) 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 f878ef84..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 @@ -300,7 +300,9 @@ public void stop(InternalConfig config, boolean clearData) { @Override protected void onTimer() { - saveInternal(); + if (internalConfig.isAutoSendUserProperties()) { + saveInternal(); + } } @Override 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 70fe8fb1..606bfd9f 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 @@ -469,6 +469,30 @@ public void eventsUserProps() throws InterruptedException { 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 From cb538c067d95244446c7abf11207bbd7db3837db Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 5 Dec 2025 13:25:07 +0300 Subject: [PATCH 10/10] fix: gather all under one flag --- .../count/sdk/java/internal/SessionImpl.java | 6 +-- .../sdk/java/internal/ModuleEventsTests.java | 39 +++++++++++++++++++ .../sdk/java/internal/SessionImplTests.java | 2 +- 3 files changed, 43 insertions(+), 4 deletions(-) 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 606bfd9f..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 @@ -516,6 +516,28 @@ public void eventsUserProps_propsSave() { 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 */ @@ -534,6 +556,23 @@ public void eventsUserProps_timer() throws InterruptedException { 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");