From 0fd0352a075214b884a1a3c16a02072c7b716aaa Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 25 Jun 2026 13:33:07 -0300 Subject: [PATCH 1/3] Nullable event value --- .../java/tests/database/TrackDaoTest.java | 2 +- .../java/tests/integration/TrackTest.java | 70 ++++++++++++++++++- .../split/android/client/SplitClientImpl.java | 12 ++-- .../client/dtos/SerializableEvent.java | 6 +- .../utils/deserializer/EventDeserializer.java | 3 +- .../SqLitePersistentEventsStorageTest.java | 2 +- .../client/tracker/DefaultTracker.java | 2 +- .../split/android/client/tracker/Tracker.java | 2 +- .../android/client/tracker/TrackerEvent.java | 2 +- .../client/tracker/DefaultTrackerTest.java | 14 ++++ 10 files changed, 98 insertions(+), 17 deletions(-) diff --git a/main/src/androidTest/java/tests/database/TrackDaoTest.java b/main/src/androidTest/java/tests/database/TrackDaoTest.java index d569a3bba..ead15eaf2 100644 --- a/main/src/androidTest/java/tests/database/TrackDaoTest.java +++ b/main/src/androidTest/java/tests/database/TrackDaoTest.java @@ -156,7 +156,7 @@ private List generateData(int from, int to, long timestamp, boolean trackEvent.trafficTypeName = "traffic_" + i; trackEvent.eventTypeId = "type_" + i; trackEvent.key = "key"; - trackEvent.value = i; + trackEvent.value = (double) i; trackEvent.timestamp = timestamp + i; EventEntity eventEntity = new EventEntity(); diff --git a/main/src/androidTest/java/tests/integration/TrackTest.java b/main/src/androidTest/java/tests/integration/TrackTest.java index 968ee3459..1dc76113d 100644 --- a/main/src/androidTest/java/tests/integration/TrackTest.java +++ b/main/src/androidTest/java/tests/integration/TrackTest.java @@ -2,11 +2,16 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import android.content.Context; +import android.util.Log; import androidx.test.platform.app.InstrumentationRegistry; +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; + import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -49,6 +54,7 @@ public class TrackTest { ArrayList mJsonChanges = null; ArrayList mLatchs; ArrayList> mEventsHits; + ArrayList mRawEventsHitBodies; @Before public void setup() { @@ -56,6 +62,7 @@ public void setup() { mCurReqId = 0; mLatchs = new ArrayList<>(); mEventsHits = new ArrayList<>(); + mRawEventsHitBodies = new ArrayList<>(); for (int i = 0; i < 6; i++) { mLatchs.add(new CountDownLatch(1)); } @@ -89,7 +96,9 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio if (index > 0 && index < 4) { code = 500; } else { - List data = IntegrationHelper.buildEventsFromJson(request.getBody().readUtf8()); + String body = request.getBody().readUtf8(); + mRawEventsHitBodies.add(body); + List data = IntegrationHelper.buildEventsFromJson(body); mEventsHits.add(data); code = 200; } @@ -261,6 +270,65 @@ public void largeNumberInPropertiesTest() throws InterruptedException, SplitInst assertEquals(158576837, event.properties.get("id")); } + @Test + public void trackWithNullValueSendsNullInPayload() throws InterruptedException, SplitInstantiationException { + CountDownLatch latch = new CountDownLatch(1); + String apiKey = IntegrationHelper.dummyApiKey(); + SplitRoomDatabase splitRoomDatabase = DatabaseHelper.getTestDatabase(mContext); + splitRoomDatabase.clearAllTables(); + + final String url = mWebServer.url("/").url().toString(); + + Key key = new Key("CUSTOMER_ID", null); + SplitClientConfig config = new TestableSplitConfigBuilder() + .serviceEndpoints(ServiceEndpoints + .builder() + .apiEndpoint(url) + .eventsEndpoint(url) + .build()) + .ready(30000) + .eventFlushInterval(5) + .eventsPerPush(5) + .eventsQueueSize(1000) + .enableDebug() + .trafficType("client") + .build(); + + SplitFactory splitFactory = SplitFactoryBuilder.build(apiKey, key, config, mContext); + + SplitClient client = splitFactory.client(); + + SplitEventTaskHelper readyTask = new SplitEventTaskHelper(latch); + SplitEventTaskHelper readyTimeOutTask = new SplitEventTaskHelper(latch); + + client.on(SplitEvent.SDK_READY, readyTask); + client.on(SplitEvent.SDK_READY_TIMED_OUT, readyTimeOutTask); + + latch.await(20, TimeUnit.SECONDS); + + client.track("trafficType", "no-value-event"); + mLatchs.get(0).await(20, TimeUnit.SECONDS); + Thread.sleep(500); + + assertNotNull("Expected at least one events/bulk hit", mRawEventsHitBodies.isEmpty() ? null : mRawEventsHitBodies.get(0)); + String rawBody = mRawEventsHitBodies.get(0); + Log.d("Track_test", rawBody); + JsonArray events = JsonParser.parseString(rawBody).getAsJsonArray(); + boolean found = false; + for (int i = 0; i < events.size(); i++) { + if ("no-value-event".equals(events.get(i).getAsJsonObject().get("eventTypeId").getAsString())) { + assertTrue("Expected value to be null in JSON payload, but got: " + + events.get(i).getAsJsonObject().get("value"), + events.get(i).getAsJsonObject().get("value").isJsonNull()); + found = true; + break; + } + } + assertTrue("Event 'no-value-event' not found in payload", found); + + client.destroy(); + } + private String emptyChanges() { return IntegrationHelper.emptySplitChanges(9567456937869L, 9567456937869L); } diff --git a/main/src/main/java/io/split/android/client/SplitClientImpl.java b/main/src/main/java/io/split/android/client/SplitClientImpl.java index c3795a416..e96eb3f23 100644 --- a/main/src/main/java/io/split/android/client/SplitClientImpl.java +++ b/main/src/main/java/io/split/android/client/SplitClientImpl.java @@ -38,8 +38,6 @@ public final class SplitClientImpl implements SplitClient { private final AttributesManager mAttributesManager; private final Tracker mEventsTracker; - private static final double TRACK_DEFAULT_VALUE = 0.0; - private boolean mIsClientDestroyed = false; public SplitClientImpl(SplitFactory container, @@ -219,7 +217,7 @@ public void addEventListener(@NonNull SplitEventListener listener) { @Override public boolean track(String trafficType, String eventType) { - return track(mKey.matchingKey(), trafficType, eventType, TRACK_DEFAULT_VALUE, null); + return track(mKey.matchingKey(), trafficType, eventType, null, null); } @Override @@ -229,7 +227,7 @@ public boolean track(String trafficType, String eventType, double value) { @Override public boolean track(String eventType) { - return track(mKey.matchingKey(), mConfig.trafficType(), eventType, TRACK_DEFAULT_VALUE, null); + return track(mKey.matchingKey(), mConfig.trafficType(), eventType, null, null); } @Override @@ -239,7 +237,7 @@ public boolean track(String eventType, double value) { @Override public boolean track(String trafficType, String eventType, Map properties) { - return track(mKey.matchingKey(), trafficType, eventType, TRACK_DEFAULT_VALUE, properties); + return track(mKey.matchingKey(), trafficType, eventType, null, properties); } @Override @@ -249,7 +247,7 @@ public boolean track(String trafficType, String eventType, double value, Map properties) { - return track(mKey.matchingKey(), mConfig.trafficType(), eventType, TRACK_DEFAULT_VALUE, properties); + return track(mKey.matchingKey(), mConfig.trafficType(), eventType, null, properties); } @Override @@ -257,7 +255,7 @@ public boolean track(String eventType, double value, Map propert return track(mKey.matchingKey(), mConfig.trafficType(), eventType, value, properties); } - private boolean track(String key, String trafficType, String eventType, double value, Map properties) { + private boolean track(String key, String trafficType, String eventType, Double value, Map properties) { if (mIsClientDestroyed) { mValidationLogger.e("Client has already been destroyed - no calls possible", "track"); return false; diff --git a/main/src/main/java/io/split/android/client/dtos/SerializableEvent.java b/main/src/main/java/io/split/android/client/dtos/SerializableEvent.java index 5e2799905..c5d36e252 100644 --- a/main/src/main/java/io/split/android/client/dtos/SerializableEvent.java +++ b/main/src/main/java/io/split/android/client/dtos/SerializableEvent.java @@ -21,7 +21,7 @@ public class SerializableEvent { @SerializedName(KEY_FIELD) public String key; @SerializedName(VALUE_FIELD) - public double value; + public Double value; @SerializedName(TIMESTAMP_FIELD) public long timestamp; @SerializedName(PROPERTIES_FIELD) @@ -31,8 +31,8 @@ public class SerializableEvent { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Event event = (Event) o; - return Double.compare(event.value, value) == 0 && + SerializableEvent event = (SerializableEvent) o; + return Objects.equals(value, event.value) && timestamp == event.timestamp && Objects.equals(eventTypeId, event.eventTypeId) && Objects.equals(trafficTypeName, event.trafficTypeName) && diff --git a/main/src/main/java/io/split/android/client/utils/deserializer/EventDeserializer.java b/main/src/main/java/io/split/android/client/utils/deserializer/EventDeserializer.java index 45941e441..dc82b862b 100644 --- a/main/src/main/java/io/split/android/client/utils/deserializer/EventDeserializer.java +++ b/main/src/main/java/io/split/android/client/utils/deserializer/EventDeserializer.java @@ -28,7 +28,8 @@ public Event deserialize(JsonElement json, Type typeOfT, JsonDeserializationCont event.eventTypeId = jsonObject.get(Event.EVENT_TYPE_FIELD).getAsString(); event.trafficTypeName = jsonObject.get(Event.TRAFFIC_TYPE_NAME_FIELD).getAsString(); event.key = jsonObject.get(Event.KEY_FIELD).getAsString(); - event.value = jsonObject.get(Event.VALUE_FIELD).getAsDouble(); + JsonElement valueElement = jsonObject.get(Event.VALUE_FIELD); + event.value = (valueElement != null && !valueElement.isJsonNull()) ? valueElement.getAsDouble() : null; event.timestamp = jsonObject.get(Event.TIMESTAMP_FIELD).getAsLong(); event.properties = buildMappedProperties(properties); diff --git a/main/src/test/java/io/split/android/client/storage/events/SqLitePersistentEventsStorageTest.java b/main/src/test/java/io/split/android/client/storage/events/SqLitePersistentEventsStorageTest.java index d7f732895..47982146a 100644 --- a/main/src/test/java/io/split/android/client/storage/events/SqLitePersistentEventsStorageTest.java +++ b/main/src/test/java/io/split/android/client/storage/events/SqLitePersistentEventsStorageTest.java @@ -130,7 +130,7 @@ public void entityForModelEncryptsBody() { mStorage.entityForModel(event); verify(mSplitCipher).encrypt("{\"sizeInBytes\":0,\"eventTypeId\":\"test_event_0\"," + - "\"trafficTypeName\":\"custom\",\"key\":\"key1\",\"value\":0,\"timestamp\":0," + + "\"trafficTypeName\":\"custom\",\"key\":\"key1\",\"value\":null,\"timestamp\":0," + "\"properties\":{}}"); } diff --git a/tracker/src/main/java/io/split/android/client/tracker/DefaultTracker.java b/tracker/src/main/java/io/split/android/client/tracker/DefaultTracker.java index 8e06a1e26..d97a43702 100644 --- a/tracker/src/main/java/io/split/android/client/tracker/DefaultTracker.java +++ b/tracker/src/main/java/io/split/android/client/tracker/DefaultTracker.java @@ -56,7 +56,7 @@ public void enableTracking(boolean enable) { @Override public boolean track(String key, String trafficType, String eventType, - double value, Map properties, boolean isSdkReady) { + Double value, Map properties, boolean isSdkReady) { if (!isTrackingEnabled.get()) { mTrackerLogger.v("Event not tracked because tracking is disabled"); return false; diff --git a/tracker/src/main/java/io/split/android/client/tracker/Tracker.java b/tracker/src/main/java/io/split/android/client/tracker/Tracker.java index aa2d2401f..2d9accbea 100644 --- a/tracker/src/main/java/io/split/android/client/tracker/Tracker.java +++ b/tracker/src/main/java/io/split/android/client/tracker/Tracker.java @@ -5,6 +5,6 @@ public interface Tracker { void enableTracking(boolean enable); - boolean track(String key, String trafficType, String eventType, double value, + boolean track(String key, String trafficType, String eventType, Double value, Map properties, boolean isSdkReady); } diff --git a/tracker/src/main/java/io/split/android/client/tracker/TrackerEvent.java b/tracker/src/main/java/io/split/android/client/tracker/TrackerEvent.java index dcdade61a..18865390d 100644 --- a/tracker/src/main/java/io/split/android/client/tracker/TrackerEvent.java +++ b/tracker/src/main/java/io/split/android/client/tracker/TrackerEvent.java @@ -10,7 +10,7 @@ public class TrackerEvent { public String trafficType; public String eventType; public String key; - public double value; + public Double value; public long timestamp; public Map properties; public int sizeInBytes; diff --git a/tracker/src/test/java/io/split/android/client/tracker/DefaultTrackerTest.java b/tracker/src/test/java/io/split/android/client/tracker/DefaultTrackerTest.java index 4c2a9fb62..b5c7aef65 100644 --- a/tracker/src/test/java/io/split/android/client/tracker/DefaultTrackerTest.java +++ b/tracker/src/test/java/io/split/android/client/tracker/DefaultTrackerTest.java @@ -205,6 +205,20 @@ public void successfulTrackPopulatesEventFieldsCorrectly() { assertEquals(512, captured.sizeInBytes); } + @Test + public void trackWithNullValueSucceeds() { + boolean result = mTracker.track("key", "traffic", "eventType", null, null, true); + + assertTrue(result); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TrackerEvent.class); + verify(mOnEventPush).accept(captor.capture()); + + TrackerEvent captured = captor.getValue(); + assertNotNull(captured); + assertEquals(null, captured.value); + } + // Helper matcher for verifying TrackerEvent fields private static T argThat(ArgumentMatcherWithReturn matcher) { return org.mockito.ArgumentMatchers.argThat(matcher::matches); From 436cc8daba9ec89d434c742dd4b2c4d69a0b5539 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 25 Jun 2026 13:39:42 -0300 Subject: [PATCH 2/3] Version 5.5.2-rc1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 537a7ae0c..d8b1410d4 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ if (rootProject.name == 'android-client') { } ext { - splitVersion = '5.5.1' + splitVersion = '5.5.2-rc1' jacocoVersion = '0.8.8' } From 592323923c2cc9f6b77952fa0658a73807f65da9 Mon Sep 17 00:00:00 2001 From: Gaston Thea Date: Thu, 25 Jun 2026 14:12:50 -0300 Subject: [PATCH 3/3] Fix test comparison --- main/src/androidTest/java/tests/integration/TrackTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main/src/androidTest/java/tests/integration/TrackTest.java b/main/src/androidTest/java/tests/integration/TrackTest.java index 1dc76113d..1b924642e 100644 --- a/main/src/androidTest/java/tests/integration/TrackTest.java +++ b/main/src/androidTest/java/tests/integration/TrackTest.java @@ -35,6 +35,8 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; +import java.util.Objects; + import io.split.android.client.SplitFactoryBuilder; import io.split.android.client.api.Key; import io.split.android.client.dtos.Event; @@ -339,7 +341,7 @@ private Event findEvent(String type, Double value) { List events = mEventsHits.get(i); if (events != null) { Optional oe = events.stream() - .filter(event -> event.eventTypeId.equals(type) && event.value == value).findFirst(); + .filter(event -> event.eventTypeId.equals(type) && Objects.equals(event.value, value)).findFirst(); if (oe.isPresent()) { return oe.get(); }