Skip to content
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 10 additions & 4 deletions sdk-java/src/main/java/ly/count/sdk/java/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ boolean isLocationDisabled() {
return !locationEnabled;
}

boolean isAutoSendUserPropertiesOnSessions() {
return autoSendUserPropertiesOnSessions;
boolean isAutoSendUserProperties() {
return autoSendUserProperties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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 {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Future<Boolean> 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();
}

Expand Down Expand Up @@ -160,7 +160,7 @@ Future<Boolean> 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();
}

Expand Down Expand Up @@ -198,7 +198,7 @@ Future<Boolean> end(Long now, final Tasks.Callback<Boolean> 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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>[] 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<String, String>[] 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<String, String>[] 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<String, String>[] 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<String, String>[] 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<String, String>[] 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down