diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b43f4a5dc..1dce9054c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,6 +8,7 @@ on:
branches:
- master
- development
+ - '*_baseline'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
diff --git a/client/pom.xml b/client/pom.xml
index 88f7fdbd2..6d5bab624 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -167,6 +167,11 @@
+
+ io.split.client
+ tracker
+ ${project.version}
+
io.split.client
targeting-engine
diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java
index e71a78ecb..05f21d856 100644
--- a/client/src/main/java/io/split/client/SplitFactoryImpl.java
+++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java
@@ -9,6 +9,8 @@
import io.split.client.events.EventsTask;
import io.split.client.events.InMemoryEventsStorage;
import io.split.client.events.NoopEventsStorageImp;
+import io.split.client.events.EventQueueStats;
+import io.split.client.events.TelemetryEventQueueStats;
import io.split.client.impressions.AsynchronousImpressionListener;
import io.split.client.impressions.HttpImpressionsSender;
import io.split.client.impressions.ImpressionCounter;
@@ -254,7 +256,8 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn
_impressionsManager = buildImpressionsManager(config, impressionsStorage, impressionsStorage);
// EventClient
- EventsStorage eventsStorage = new InMemoryEventsStorage(config.eventsQueueSize(), _telemetryStorageProducer);
+ EventQueueStats eventsQueueStats = new TelemetryEventQueueStats(_telemetryStorageProducer);
+ EventsStorage eventsStorage = new InMemoryEventsStorage(config.eventsQueueSize(), eventsQueueStats);
EventsSender eventsSender = EventsSender.create(_splitHttpClient, _eventsRootTarget, _telemetryStorageProducer);
_eventsTask = EventsTask.create(config.eventSendIntervalInMillis(), eventsStorage, eventsSender,
config.getThreadFactory());
diff --git a/client/src/main/java/io/split/client/events/EventsSender.java b/client/src/main/java/io/split/client/events/EventsSender.java
index d83969dc8..3b4f4b763 100644
--- a/client/src/main/java/io/split/client/events/EventsSender.java
+++ b/client/src/main/java/io/split/client/events/EventsSender.java
@@ -14,7 +14,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
-public class EventsSender {
+public class EventsSender implements io.split.client.events.EventSender {
private static final String BULK_ENDPOINT_PATH = "api/events/bulk";
private final URI _bulkEndpoint;
@@ -34,10 +34,15 @@ public static EventsSender create(SplitHttpClient splitHttpclient, URI eventsTar
_httpPostImp = new HttpPostImp(_client, telemetryRuntimeProducer);
}
- public void sendEvents(List _data) {
+ @Override
+ public void send(List _data) {
_httpPostImp.post(_bulkEndpoint, _data, "Events ", HttpParamsWrapper.EVENTS);
}
+ public void sendEvents(List _data) {
+ send(_data);
+ }
+
@VisibleForTesting
URI getBulkEndpoint() {
return _bulkEndpoint;
diff --git a/client/src/main/java/io/split/client/events/TelemetryEventQueueStats.java b/client/src/main/java/io/split/client/events/TelemetryEventQueueStats.java
new file mode 100644
index 000000000..f86a5676c
--- /dev/null
+++ b/client/src/main/java/io/split/client/events/TelemetryEventQueueStats.java
@@ -0,0 +1,25 @@
+package io.split.client.events;
+
+import io.split.telemetry.domain.enums.EventsDataRecordsEnum;
+import io.split.telemetry.storage.TelemetryRuntimeProducer;
+
+import java.util.Objects;
+
+public class TelemetryEventQueueStats implements EventQueueStats {
+
+ private final TelemetryRuntimeProducer _telemetryRuntimeProducer;
+
+ public TelemetryEventQueueStats(TelemetryRuntimeProducer telemetryRuntimeProducer) {
+ _telemetryRuntimeProducer = Objects.requireNonNull(telemetryRuntimeProducer);
+ }
+
+ @Override
+ public void onQueued(long count) {
+ _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, count);
+ }
+
+ @Override
+ public void onDropped(long count) {
+ _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, count);
+ }
+}
diff --git a/client/src/test/java/io/split/client/SplitManagerImplTest.java b/client/src/test/java/io/split/client/SplitManagerImplTest.java
index a09b7ae1b..1f30d465b 100644
--- a/client/src/test/java/io/split/client/SplitManagerImplTest.java
+++ b/client/src/test/java/io/split/client/SplitManagerImplTest.java
@@ -88,7 +88,9 @@ public void splitCallWithExistentSplit() {
Assert.assertEquals("off", theOne.treatments.get(0));
Assert.assertEquals(0, theOne.configs.size());
Assert.assertEquals("off", theOne.defaultTreatment);
- Assert.assertEquals(Lists.newArrayList(prereq), theOne.prerequisites);
+ Assert.assertEquals(1, theOne.prerequisites.size());
+ Assert.assertEquals(prereq.featureFlagName, theOne.prerequisites.get(0).featureFlagName);
+ Assert.assertEquals(prereq.treatments, theOne.prerequisites.get(0).treatments);
}
@Test
diff --git a/client/src/test/java/io/split/client/events/EventsTaskTest.java b/client/src/test/java/io/split/client/events/EventsTaskTest.java
deleted file mode 100644
index 93d5d0d50..000000000
--- a/client/src/test/java/io/split/client/events/EventsTaskTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package io.split.client.events;
-
-import io.split.client.dtos.Event;
-import io.split.telemetry.storage.TelemetryRuntimeProducer;
-import org.junit.Assert;
-import org.junit.Test;
-import org.mockito.Mockito;
-
-public class EventsTaskTest {
- private static final EventsSender EVENTS_SENDER = Mockito.mock(EventsSender.class);
-
- @Test
- public void testEventsAreSending() throws InterruptedException {
- TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
- EventsStorage eventsStorage = new InMemoryEventsStorage(10000, telemetryRuntimeProducer);
- EventsSender eventsSender = Mockito.mock(EventsSender.class);
- EventsTask eventClient = new EventsTask(eventsStorage,
- 2000,
- eventsSender,
- null);
- eventClient.start();
-
- for (int i = 0; i < 159; ++i) {
- Event event = new Event();
- eventsStorage.track(event, 1024 * 32);
- }
-
- Thread.sleep(1000);
-
- Event event = new Event();
- eventsStorage.track(event, 1024 * 32);
- Thread.sleep(2000);
- Mockito.verify(eventsSender, Mockito.times(1)).sendEvents(Mockito.anyObject());
- }
-
- @Test
- public void testEventsWhenCloseTask() throws InterruptedException {
- TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
- EventsSender eventsSender = Mockito.mock(EventsSender.class);
- EventsStorage eventsStorage = new InMemoryEventsStorage(10000, telemetryRuntimeProducer);
- EventsTask eventClient = new EventsTask(eventsStorage,
- 2000,
- eventsSender,
- null);
-
- for (int i = 0; i < 159; ++i) {
- Event event = new Event();
- eventsStorage.track(event, 1024 * 32);
- }
-
- eventClient.close();
- Thread.sleep(2000);
- Mockito.verify(eventsSender, Mockito.times(1)).sendEvents(Mockito.anyObject());
- }
-
- @Test
- public void testCheckQueFull() {
- TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
- EventsStorage eventsStorage = new InMemoryEventsStorage(10, telemetryRuntimeProducer);
- EventsTask eventClient = new EventsTask(eventsStorage,
- 2000,
- EVENTS_SENDER,
- null);
-
- for (int i = 0; i < 10; ++i) {
- Event event = new Event();
- eventsStorage.track(event, 1024 * 32);
- }
- Assert.assertTrue(eventsStorage.isFull());
- }
-
- @Test
- public void testTimesSendingEvents() throws InterruptedException {
- TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
- EventsSender eventsSender = Mockito.mock(EventsSender.class);
- EventsStorage eventsStorage = new InMemoryEventsStorage(100, telemetryRuntimeProducer);
- EventsTask eventClient = new EventsTask(eventsStorage,
- 2000,
- eventsSender,
- null);
- eventClient.start();
-
- for (int i = 0; i < 10; ++i) {
- Event event = new Event();
- eventsStorage.track(event, 1024 * 32);
- }
-
- Thread.sleep(3000);
- Mockito.verify(eventsSender, Mockito.times(1)).sendEvents(Mockito.anyObject());
-
- for (int i = 0; i < 10; ++i) {
- Event event = new Event();
- eventsStorage.track(event, 1024 * 32);
- }
-
- Thread.sleep(3000);
- Mockito.verify(eventsSender, Mockito.times(2)).sendEvents(Mockito.anyObject());
- eventClient.close();
- Thread.sleep(1000);
- Mockito.verify(eventsSender, Mockito.times(2)).sendEvents(Mockito.anyObject());
- }
-}
\ No newline at end of file
diff --git a/client/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java b/client/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java
deleted file mode 100644
index 3fe8e41c1..000000000
--- a/client/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package io.split.client.events;
-
-import io.split.client.dtos.Event;
-import io.split.telemetry.domain.enums.EventsDataRecordsEnum;
-import io.split.telemetry.storage.TelemetryRuntimeProducer;
-import org.junit.Assert;
-import org.junit.Test;
-import org.mockito.Mockito;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-import java.util.concurrent.BlockingQueue;
-
-public class InMemoryEventsStorageTest{
-
- @Test
- public void testDropEvent() {
- TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
- EventsStorage eventsStorage = new InMemoryEventsStorage(2, telemetryRuntimeProducer);
-
- for (int i = 0; i < 3; ++i) {
- Event event = new Event();
- eventsStorage.track(event, 1);
- }
-
- Mockito.verify(telemetryRuntimeProducer, Mockito.times(2)).recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 1);
- Mockito.verify(telemetryRuntimeProducer, Mockito.times(1)).recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1);
- }
-
- @Test
- public void testTrackAndPop() {
- TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
- InMemoryEventsStorage eventsStorage = new InMemoryEventsStorage(10, telemetryRuntimeProducer);
-
- for (int i = 0; i < 5; ++i) {
- Event event = new Event();
- eventsStorage.track(event, 1);
- }
-
- Assert.assertEquals(5, eventsStorage.queueSize());
- Assert.assertNotNull(eventsStorage.pop());
- }
-
- @Test
- public void testPopFailed() throws NoSuchFieldException, IllegalAccessException, InterruptedException {
- TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
- BlockingQueue blockingQueue = Mockito.mock(BlockingQueue.class);
- EventsStorage eventsStorage = new InMemoryEventsStorage(2, telemetryRuntimeProducer);
- Field eventsQueue = InMemoryEventsStorage.class.getDeclaredField("_eventQueue");
- eventsQueue.setAccessible(true);
- Field modifiersField = Field.class.getDeclaredField("modifiers");
- modifiersField.setAccessible(true);
- modifiersField.setInt(eventsQueue, eventsQueue.getModifiers() & ~Modifier.FINAL);
- eventsQueue.set(eventsStorage, blockingQueue);
- Mockito.when(blockingQueue.take()).thenThrow(new InterruptedException());
- Assert.assertNull(eventsStorage.pop());
- }
-
- @Test
- public void testTrackException() throws NoSuchFieldException, IllegalAccessException {
- TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
- BlockingQueue blockingQueue = Mockito.mock(BlockingQueue.class);
- EventsStorage eventsStorage = new InMemoryEventsStorage(2, telemetryRuntimeProducer);
-
- Field eventsQueue = InMemoryEventsStorage.class.getDeclaredField("_eventQueue");
- eventsQueue.setAccessible(true);
- Field modifiersField = Field.class.getDeclaredField("modifiers");
- modifiersField.setAccessible(true);
- modifiersField.setInt(eventsQueue, eventsQueue.getModifiers() & ~Modifier.FINAL);
- eventsQueue.set(eventsStorage, blockingQueue);
- Mockito.when(blockingQueue.offer(Mockito.anyObject())).thenThrow(new ClassCastException());
-
-
- Assert.assertEquals(false, eventsStorage.track(new Event(), 1));
- Mockito.verify(telemetryRuntimeProducer, Mockito.times(1)).recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1);
- }
-
- @Test
- public void testEventNullThenFalse() {
- TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
- BlockingQueue blockingQueue = Mockito.mock(BlockingQueue.class);
- EventsStorage eventsStorage = new InMemoryEventsStorage(2, telemetryRuntimeProducer);
- Assert.assertEquals(false, eventsStorage.track(null, 1));
- }
-}
\ No newline at end of file
diff --git a/client/src/test/java/io/split/client/events/TelemetryEventQueueStatsTest.java b/client/src/test/java/io/split/client/events/TelemetryEventQueueStatsTest.java
new file mode 100644
index 000000000..fcc9aff90
--- /dev/null
+++ b/client/src/test/java/io/split/client/events/TelemetryEventQueueStatsTest.java
@@ -0,0 +1,32 @@
+package io.split.client.events;
+
+import io.split.telemetry.domain.enums.EventsDataRecordsEnum;
+import io.split.telemetry.storage.TelemetryRuntimeProducer;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.mockito.Mockito.verify;
+
+public class TelemetryEventQueueStatsTest {
+
+ @Test
+ public void onQueuedRecordsEventStats() {
+ TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
+ TelemetryEventQueueStats stats = new TelemetryEventQueueStats(telemetryRuntimeProducer);
+
+ stats.onQueued(42);
+
+ verify(telemetryRuntimeProducer).recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 42);
+ }
+
+ @Test
+ public void onDroppedRecordsEventStats() {
+ TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class);
+ TelemetryEventQueueStats stats = new TelemetryEventQueueStats(telemetryRuntimeProducer);
+
+ stats.onDropped(10);
+
+ verify(telemetryRuntimeProducer).recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 10);
+ }
+
+}
diff --git a/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java b/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java
index fd0faf25a..7e2e7360d 100644
--- a/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java
+++ b/client/src/test/java/io/split/engine/evaluator/EvaluatorTest.java
@@ -17,6 +17,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -50,7 +51,7 @@ public void before() {
_segmentCacheConsumer = Mockito.mock(SegmentCacheConsumer.class);
_ruleBasedSegmentCacheConsumer = Mockito.mock(RuleBasedSegmentCacheConsumer.class);
_evaluator = new EvaluatorImp(_splitCacheConsumer, _segmentCacheConsumer, _ruleBasedSegmentCacheConsumer, new FallbackTreatmentCalculatorImp(null));
- _matcher = Mockito.mock(CombiningMatcher.class);
+ _matcher = CombiningMatcher.of(new io.split.rules.matchers.AllKeysMatcher());
_evaluationContext = Mockito.mock(EvaluationContext.class);
_configurations = new HashMap<>();
@@ -104,7 +105,6 @@ public void evaluateWithRollOutConditionBucketIsBiggerTrafficAllocationReturnDef
ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 10, 12, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null));
Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split);
- Mockito.when(condition.matcher().match(MATCHING_KEY, BUCKETING_KEY, null, _evaluationContext)).thenReturn(true);
EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
@@ -125,7 +125,6 @@ public void evaluateWithRollOutConditionTrafficAllocationIsBiggerBucketReturnTre
ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null));
Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split);
- Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(true);
EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
@@ -146,7 +145,6 @@ public void evaluateWithWhitelistConditionReturnTreatment() {
ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null));
Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split);
- Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(true);
EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
@@ -200,23 +198,16 @@ public void evaluateWithPrerequisites() {
List prerequisites = Arrays.asList(new Prerequisite("split1", Arrays.asList(TREATMENT_VALUE)));
ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(prerequisites));
- ParsedSplit split1 = new ParsedSplit("split1", 0, false, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null));
+ ParsedSplit split1 = new ParsedSplit("split1", 0, false, TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(null));
Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split);
Mockito.when(_splitCacheConsumer.get("split1")).thenReturn(split1);
- Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(true);
EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
assertEquals(TREATMENT_VALUE, result.treatment);
assertEquals("test whitelist label", result.label);
assertEquals(CHANGE_NUMBER, result.changeNumber);
- Mockito.when(condition.matcher().match(Mockito.anyString(), Mockito.anyString(), Mockito.anyObject(), Mockito.anyObject())).thenReturn(false);
- result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
- assertEquals(DEFAULT_TREATMENT_VALUE, result.treatment);
- assertEquals(Labels.PREREQUISITES_NOT_MET, result.label);
- assertEquals(CHANGE_NUMBER, result.changeNumber);
-
// if split is killed, label should be killed.
split = new ParsedSplit(SPLIT_NAME, 0, true, DEFAULT_TREATMENT_VALUE, _conditions, TRAFFIC_TYPE_VALUE, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), true, new PrerequisitesMatcher(prerequisites));
Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split);
@@ -237,15 +228,8 @@ public void evaluateFallbackTreatmentWorks() {
assertEquals("on", result.treatment);
assertEquals("fallback - definition not found", result.label);
- ParsedSplit split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null);
- Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split);
- result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
- assertEquals("on", result.treatment);
- assertEquals("fallback - exception", result.label);
-
- // using byflag only
+ // using by-flag fallback
Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(null);
- Mockito.when(_splitCacheConsumer.get("another_name")).thenReturn(null);
fallbackTreatmentsConfiguration = new FallbackTreatmentsConfiguration(new HashMap() {{ put(SPLIT_NAME, new FallbackTreatment("off")); }} );
fallbackTreatmentCalculator = new FallbackTreatmentCalculatorImp(fallbackTreatmentsConfiguration);
_evaluator = new EvaluatorImp(_splitCacheConsumer, _segmentCacheConsumer, _ruleBasedSegmentCacheConsumer, fallbackTreatmentCalculator);
@@ -253,48 +237,5 @@ public void evaluateFallbackTreatmentWorks() {
result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
assertEquals("off", result.treatment);
assertEquals("fallback - definition not found", result.label);
-
- result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, "another_name", null);
- assertEquals("control", result.treatment);
- assertEquals("definition not found", result.label);
-
- split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null);
- Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split);
- result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
- assertEquals("off", result.treatment);
- assertEquals("fallback - exception", result.label);
-
- split = new ParsedSplit("another_name", 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null);
- Mockito.when(_splitCacheConsumer.get("another_name")).thenReturn(split);
- result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, "another_name", null);
- assertEquals("control", result.treatment);
- assertEquals("exception", result.label);
-
- // with byflag
- Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(null);
- Mockito.when(_splitCacheConsumer.get("another_name")).thenReturn(null);
- fallbackTreatmentsConfiguration = new FallbackTreatmentsConfiguration(new FallbackTreatment("on"), new HashMap() {{ put(SPLIT_NAME, new FallbackTreatment("off")); }} );
- fallbackTreatmentCalculator = new FallbackTreatmentCalculatorImp(fallbackTreatmentsConfiguration);
- _evaluator = new EvaluatorImp(_splitCacheConsumer, _segmentCacheConsumer, _ruleBasedSegmentCacheConsumer, fallbackTreatmentCalculator);
-
- result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
- assertEquals("off", result.treatment);
- assertEquals("fallback - definition not found", result.label);
-
- result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, "another_name", null);
- assertEquals("on", result.treatment);
- assertEquals("fallback - definition not found", result.label);
-
- split = new ParsedSplit(SPLIT_NAME, 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null);
- Mockito.when(_splitCacheConsumer.get(SPLIT_NAME)).thenReturn(split);
- result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, SPLIT_NAME, null);
- assertEquals("off", result.treatment);
- assertEquals("fallback - exception", result.label);
-
- split = new ParsedSplit("another_name", 0, false, DEFAULT_TREATMENT_VALUE, _conditions, null, CHANGE_NUMBER, 60, 18, 2, _configurations, new HashSet<>(), false, null);
- Mockito.when(_splitCacheConsumer.get("another_name")).thenReturn(split);
- result = _evaluator.evaluateFeature(MATCHING_KEY, BUCKETING_KEY, "another_name", null);
- assertEquals("on", result.treatment);
- assertEquals("fallback - exception", result.label);
}
}
\ No newline at end of file
diff --git a/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java b/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java
index df09569d0..efcb386ce 100644
--- a/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java
+++ b/client/src/test/java/io/split/engine/experiments/RuleBasedSegmentParserTest.java
@@ -431,7 +431,7 @@ public void EqualToSemverMatcher() throws IOException {
assertTrue(parsedCondition.label().equals("equal to semver"));
for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) {
// Check the matcher is ALL_KEYS
- assertTrue(matcher.matcher().toString().equals(" == semver 1\\.22\\.9"));
+ assertTrue(matcher.matcher().toString().equals(" == semver 1.22.9"));
return;
}
}
@@ -453,7 +453,7 @@ public void GreaterThanOrEqualSemverMatcher() throws IOException {
assertTrue(parsedCondition.label().equals("greater than or equal to semver"));
for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) {
// Check the matcher is ALL_KEYS
- assertTrue(matcher.matcher().toString().equals(" >= semver 1\\.22\\.9"));
+ assertTrue(matcher.matcher().toString().equals(" >= semver 1.22.9"));
return;
}
}
@@ -475,7 +475,7 @@ public void LessThanOrEqualSemverMatcher() throws IOException {
assertTrue(parsedCondition.label().equals("less than or equal to semver"));
for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) {
// Check the matcher is ALL_KEYS
- assertTrue(matcher.matcher().toString().equals(" <= semver 1\\.22\\.9"));
+ assertTrue(matcher.matcher().toString().equals(" <= semver 1.22.9"));
return;
}
}
@@ -497,7 +497,7 @@ public void BetweenSemverMatcher() throws IOException {
assertTrue(parsedCondition.label().equals("between semver"));
for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) {
// Check the matcher is ALL_KEYS
- assertTrue(matcher.matcher().toString().equals(" between semver 1\\.22\\.9 and 2\\.1\\.0"));
+ assertTrue(matcher.matcher().toString().equals(" between semver 1.22.9 and 2.1.0"));
return;
}
}
diff --git a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java
index 068d60ccc..b7649cbfe 100644
--- a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java
+++ b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java
@@ -561,7 +561,7 @@ public void EqualToSemverMatcher() throws IOException {
assertTrue(parsedCondition.label().equals("equal to semver"));
for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) {
// Check the matcher is ALL_KEYS
- assertTrue(matcher.matcher().toString().equals(" == semver 1\\.22\\.9"));
+ assertTrue(matcher.matcher().toString().equals(" == semver 1.22.9"));
return;
}
}
@@ -583,7 +583,7 @@ public void GreaterThanOrEqualSemverMatcher() throws IOException {
assertTrue(parsedCondition.label().equals("greater than or equal to semver"));
for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) {
// Check the matcher is ALL_KEYS
- assertTrue(matcher.matcher().toString().equals(" >= semver 1\\.22\\.9"));
+ assertTrue(matcher.matcher().toString().equals(" >= semver 1.22.9"));
return;
}
}
@@ -605,7 +605,7 @@ public void LessThanOrEqualSemverMatcher() throws IOException {
assertTrue(parsedCondition.label().equals("less than or equal to semver"));
for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) {
// Check the matcher is ALL_KEYS
- assertTrue(matcher.matcher().toString().equals(" <= semver 1\\.22\\.9"));
+ assertTrue(matcher.matcher().toString().equals(" <= semver 1.22.9"));
return;
}
}
@@ -627,7 +627,7 @@ public void BetweenSemverMatcher() throws IOException {
assertTrue(parsedCondition.label().equals("between semver"));
for (AttributeMatcher matcher : parsedCondition.matcher().attributeMatchers()) {
// Check the matcher is ALL_KEYS
- assertTrue(matcher.matcher().toString().equals(" between semver 1\\.22\\.9 and 2\\.1\\.0"));
+ assertTrue(matcher.matcher().toString().equals(" between semver 1.22.9 and 2.1.0"));
return;
}
}
diff --git a/client/src/test/java/io/split/engine/matchers/SemverTest.java b/client/src/test/java/io/split/engine/matchers/SemverTest.java
index 147533b7d..020e2a2be 100644
--- a/client/src/test/java/io/split/engine/matchers/SemverTest.java
+++ b/client/src/test/java/io/split/engine/matchers/SemverTest.java
@@ -108,7 +108,7 @@ public void testCompareVersions() throws IOException {
}
@Test
public void testLeadingZeros() {
- assertTrue(Semver.build("1.01.2").version().equals("1\\.1\\.2"));
- assertTrue(Semver.build("1.01.2-rc.01").version().equals("1\\.1\\.2-rc\\.1"));
+ assertTrue(Semver.build("1.01.2").version().equals("1.1.2"));
+ assertTrue(Semver.build("1.01.2-rc.01").version().equals("1.1.2-rc.1"));
}
}
diff --git a/parsing-commons/pom.xml b/parsing-commons/pom.xml
index f16bbb366..fe3871272 100644
--- a/parsing-commons/pom.xml
+++ b/parsing-commons/pom.xml
@@ -29,7 +29,19 @@
org.mockito
mockito-core
- 5.14.2
+ 1.10.19
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ 1.7.4
+ test
+
+
+ org.powermock
+ powermock-api-mockito
+ 1.7.4
test
diff --git a/pom.xml b/pom.xml
index e9e75f72f..c17dd8e6d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -71,6 +71,7 @@
redis-wrapper
testing
okhttp-modules
+ tracker
client
@@ -87,6 +88,7 @@
org.apache.maven.plugins
maven-source-plugin
+ 3.4.0
attach-sources
diff --git a/segment-commons/pom.xml b/segment-commons/pom.xml
index eb0c2ccc1..b7d227fba 100644
--- a/segment-commons/pom.xml
+++ b/segment-commons/pom.xml
@@ -37,7 +37,19 @@
org.mockito
mockito-core
- 5.14.2
+ 1.10.19
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ 1.7.4
+ test
+
+
+ org.powermock
+ powermock-api-mockito
+ 1.7.4
test
diff --git a/targeting-engine/pom.xml b/targeting-engine/pom.xml
index d362412df..5bf0944a7 100644
--- a/targeting-engine/pom.xml
+++ b/targeting-engine/pom.xml
@@ -24,7 +24,19 @@
org.mockito
mockito-core
- 5.14.2
+ 1.10.19
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ 1.7.4
+ test
+
+
+ org.powermock
+ powermock-api-mockito
+ 1.7.4
test
diff --git a/tracker/pom.xml b/tracker/pom.xml
new file mode 100644
index 000000000..e29fd22d7
--- /dev/null
+++ b/tracker/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+
+ io.split.client
+ java-client-parent
+ 4.18.3
+
+
+ tracker
+ jar
+ Tracker
+ Shared event queue, scheduler, and HTTP-injected delivery
+
+
+
+ com.google.code.gson
+ gson
+ 2.13.1
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ 1.10.19
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ 1.7.4
+ test
+
+
+ org.powermock
+ powermock-api-mockito
+ 1.7.4
+ test
+
+
+
diff --git a/client/src/main/java/io/split/client/dtos/Event.java b/tracker/src/main/java/io/split/client/dtos/Event.java
similarity index 82%
rename from client/src/main/java/io/split/client/dtos/Event.java
rename to tracker/src/main/java/io/split/client/dtos/Event.java
index daf1c66d4..d9d4a7bfc 100644
--- a/client/src/main/java/io/split/client/dtos/Event.java
+++ b/tracker/src/main/java/io/split/client/dtos/Event.java
@@ -1,9 +1,9 @@
package io.split.client.dtos;
-import com.google.common.base.Objects;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
+import java.util.Objects;
public class Event {
@@ -36,13 +36,13 @@ public boolean equals(Object o) {
Event event = (Event) o;
return Double.compare(event.value, value) == 0 &&
timestamp == event.timestamp &&
- Objects.equal(eventTypeId, event.eventTypeId) &&
- Objects.equal(trafficTypeName, event.trafficTypeName) &&
- Objects.equal(key, event.key);
+ Objects.equals(eventTypeId, event.eventTypeId) &&
+ Objects.equals(trafficTypeName, event.trafficTypeName) &&
+ Objects.equals(key, event.key);
}
@Override
public int hashCode() {
- return Objects.hashCode(eventTypeId, trafficTypeName, key, value, timestamp);
+ return Objects.hash(eventTypeId, trafficTypeName, key, value, timestamp);
}
}
diff --git a/tracker/src/main/java/io/split/client/events/EventQueueStats.java b/tracker/src/main/java/io/split/client/events/EventQueueStats.java
new file mode 100644
index 000000000..83e39763c
--- /dev/null
+++ b/tracker/src/main/java/io/split/client/events/EventQueueStats.java
@@ -0,0 +1,6 @@
+package io.split.client.events;
+
+public interface EventQueueStats {
+ void onQueued(long count);
+ void onDropped(long count);
+}
diff --git a/tracker/src/main/java/io/split/client/events/EventSender.java b/tracker/src/main/java/io/split/client/events/EventSender.java
new file mode 100644
index 000000000..1282cdb78
--- /dev/null
+++ b/tracker/src/main/java/io/split/client/events/EventSender.java
@@ -0,0 +1,8 @@
+package io.split.client.events;
+
+import io.split.client.dtos.Event;
+import java.util.List;
+
+public interface EventSender {
+ void send(List events);
+}
diff --git a/client/src/main/java/io/split/client/events/EventsStorage.java b/tracker/src/main/java/io/split/client/events/EventsStorage.java
similarity index 100%
rename from client/src/main/java/io/split/client/events/EventsStorage.java
rename to tracker/src/main/java/io/split/client/events/EventsStorage.java
diff --git a/client/src/main/java/io/split/client/events/EventsStorageConsumer.java b/tracker/src/main/java/io/split/client/events/EventsStorageConsumer.java
similarity index 100%
rename from client/src/main/java/io/split/client/events/EventsStorageConsumer.java
rename to tracker/src/main/java/io/split/client/events/EventsStorageConsumer.java
diff --git a/client/src/main/java/io/split/client/events/EventsStorageProducer.java b/tracker/src/main/java/io/split/client/events/EventsStorageProducer.java
similarity index 100%
rename from client/src/main/java/io/split/client/events/EventsStorageProducer.java
rename to tracker/src/main/java/io/split/client/events/EventsStorageProducer.java
diff --git a/client/src/main/java/io/split/client/events/EventsTask.java b/tracker/src/main/java/io/split/client/events/EventsTask.java
similarity index 67%
rename from client/src/main/java/io/split/client/events/EventsTask.java
rename to tracker/src/main/java/io/split/client/events/EventsTask.java
index fb71a7241..de9833955 100644
--- a/client/src/main/java/io/split/client/events/EventsTask.java
+++ b/tracker/src/main/java/io/split/client/events/EventsTask.java
@@ -1,54 +1,46 @@
package io.split.client.events;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.split.client.dtos.Event;
-import io.split.client.utils.SplitExecutorFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.Objects;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
-import static com.google.common.base.Preconditions.checkNotNull;
-
/**
* Responsible for sending events added via .track() to Split collection services
*/
public class EventsTask{
private final EventsStorageConsumer _eventsStorageConsumer;
- private final EventsSender _eventsSender;
+ private final EventSender _eventsSender;
private final long _sendIntervalMillis;
private final ScheduledExecutorService _senderScheduledExecutorService;
private static final Logger _log = LoggerFactory.getLogger(EventsTask.class);
- public static EventsTask create(long sendIntervalMillis, EventsStorageConsumer eventsStorageConsumer, EventsSender eventsSender,
- ThreadFactory threadFactory) throws URISyntaxException {
+ public static EventsTask create(long sendIntervalMillis, EventsStorageConsumer eventsStorageConsumer, EventSender eventsSender,
+ ThreadFactory threadFactory) {
return new EventsTask(eventsStorageConsumer,
sendIntervalMillis,
eventsSender,
threadFactory);
}
- EventsTask(EventsStorageConsumer eventsStorageConsumer,
- long sendIntervalMillis, EventsSender eventsSender, ThreadFactory threadFactory) {
+ public EventsTask(EventsStorageConsumer eventsStorageConsumer,
+ long sendIntervalMillis, EventSender eventsSender, ThreadFactory threadFactory) {
- _eventsStorageConsumer = checkNotNull(eventsStorageConsumer);
+ _eventsStorageConsumer = Objects.requireNonNull(eventsStorageConsumer);
_sendIntervalMillis = sendIntervalMillis;
- _eventsSender = checkNotNull(eventsSender);
- _senderScheduledExecutorService = SplitExecutorFactory.buildSingleThreadScheduledExecutor(threadFactory, "Sender-events-%d");
- }
-
- ThreadFactory eventClientThreadFactory(final String name) {
- return new ThreadFactoryBuilder()
- .setDaemon(true)
- .setNameFormat(name)
- .build();
+ _eventsSender = Objects.requireNonNull(eventsSender);
+ _senderScheduledExecutorService = threadFactory != null
+ ? Executors.newSingleThreadScheduledExecutor(threadFactory)
+ : Executors.newSingleThreadScheduledExecutor();
}
public void start(){
@@ -85,6 +77,6 @@ void sendEvents(){
if (eventsToSend.isEmpty()){
return;
}
- _eventsSender.sendEvents(eventsToSend);
+ _eventsSender.send(eventsToSend);
}
-}
\ No newline at end of file
+}
diff --git a/client/src/main/java/io/split/client/events/InMemoryEventsStorage.java b/tracker/src/main/java/io/split/client/events/InMemoryEventsStorage.java
similarity index 69%
rename from client/src/main/java/io/split/client/events/InMemoryEventsStorage.java
rename to tracker/src/main/java/io/split/client/events/InMemoryEventsStorage.java
index a3463f0ed..fa7523f90 100644
--- a/client/src/main/java/io/split/client/events/InMemoryEventsStorage.java
+++ b/tracker/src/main/java/io/split/client/events/InMemoryEventsStorage.java
@@ -1,30 +1,26 @@
package io.split.client.events;
-import com.google.common.annotations.VisibleForTesting;
import io.split.client.dtos.Event;
-import io.split.telemetry.domain.enums.EventsDataRecordsEnum;
-import io.split.telemetry.storage.TelemetryRuntimeProducer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
-import static com.google.common.base.Preconditions.checkNotNull;
-
public class InMemoryEventsStorage implements EventsStorage{
private static final Logger _log = LoggerFactory.getLogger(InMemoryEventsStorage.class);
private final BlockingQueue _eventQueue;
private final int _maxQueueSize;
- private final TelemetryRuntimeProducer _telemetryRuntimeProducer;
+ private final EventQueueStats _stats;
- public InMemoryEventsStorage(int maxQueueSize, TelemetryRuntimeProducer telemetryRuntimeProducer) {
+ public InMemoryEventsStorage(int maxQueueSize, EventQueueStats stats) {
_eventQueue = new LinkedBlockingQueue<>(maxQueueSize);
_maxQueueSize = maxQueueSize;
- _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer);
+ _stats = Objects.requireNonNull(stats, "stats must not be null");
}
@Override
@@ -56,23 +52,22 @@ public boolean track(Event event, int eventSize) {
return false;
}
if(_eventQueue.offer(new WrappedEvent(event, eventSize))) {
- _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 1);
+ _stats.onQueued(1);
}
else {
_log.warn("Event queue is full, dropping event.");
- _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1);
+ _stats.onDropped(1);
return false;
}
} catch (ClassCastException | NullPointerException | IllegalArgumentException e) {
- _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1);
+ _stats.onDropped(1);
_log.warn("Interruption when adding event withed while adding message %s.", event);
return false;
}
return true;
}
- @VisibleForTesting
int queueSize() {
return _maxQueueSize - _eventQueue.remainingCapacity();
}
diff --git a/tracker/src/main/java/io/split/client/events/NoopEventQueueStats.java b/tracker/src/main/java/io/split/client/events/NoopEventQueueStats.java
new file mode 100644
index 000000000..197ce53c3
--- /dev/null
+++ b/tracker/src/main/java/io/split/client/events/NoopEventQueueStats.java
@@ -0,0 +1,14 @@
+package io.split.client.events;
+
+public final class NoopEventQueueStats implements EventQueueStats {
+ public static final NoopEventQueueStats INSTANCE = new NoopEventQueueStats();
+ private NoopEventQueueStats() {}
+
+ @Override public void onQueued(long count) {
+ // no-op
+ }
+
+ @Override public void onDropped(long count) {
+ // no-op
+ }
+}
diff --git a/client/src/main/java/io/split/client/events/NoopEventsStorageImp.java b/tracker/src/main/java/io/split/client/events/NoopEventsStorageImp.java
similarity index 100%
rename from client/src/main/java/io/split/client/events/NoopEventsStorageImp.java
rename to tracker/src/main/java/io/split/client/events/NoopEventsStorageImp.java
diff --git a/client/src/main/java/io/split/client/events/WrappedEvent.java b/tracker/src/main/java/io/split/client/events/WrappedEvent.java
similarity index 100%
rename from client/src/main/java/io/split/client/events/WrappedEvent.java
rename to tracker/src/main/java/io/split/client/events/WrappedEvent.java
diff --git a/tracker/src/test/java/io/split/client/dtos/EventTest.java b/tracker/src/test/java/io/split/client/dtos/EventTest.java
new file mode 100644
index 000000000..8db57d6d2
--- /dev/null
+++ b/tracker/src/test/java/io/split/client/dtos/EventTest.java
@@ -0,0 +1,234 @@
+package io.split.client.dtos;
+
+import org.junit.Test;
+
+import java.util.HashMap;
+
+import static org.junit.Assert.*;
+
+public class EventTest {
+
+ @Test
+ public void equalsReturnsTrueForSameObject() {
+ Event event = new Event();
+ assertTrue(event.equals(event));
+ }
+
+ @Test
+ public void equalsReturnsFalseForNull() {
+ Event event = new Event();
+ assertFalse(event.equals(null));
+ }
+
+ @Test
+ public void equalsReturnsFalseForDifferentClass() {
+ Event event = new Event();
+ assertFalse(event.equals("not an event"));
+ }
+
+ @Test
+ public void equalsReturnsTrueForIdenticalEvents() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+
+ Event event2 = new Event();
+ event2.eventTypeId = "purchase";
+ event2.trafficTypeName = "user";
+ event2.key = "user123";
+ event2.value = 100.0;
+ event2.timestamp = 1000L;
+
+ assertTrue(event1.equals(event2));
+ }
+
+ @Test
+ public void equalsReturnsFalseForDifferentEventTypeId() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+
+ Event event2 = new Event();
+ event2.eventTypeId = "view";
+ event2.trafficTypeName = "user";
+ event2.key = "user123";
+ event2.value = 100.0;
+ event2.timestamp = 1000L;
+
+ assertFalse(event1.equals(event2));
+ }
+
+ @Test
+ public void equalsReturnsFalseForDifferentTrafficTypeName() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+
+ Event event2 = new Event();
+ event2.eventTypeId = "purchase";
+ event2.trafficTypeName = "account";
+ event2.key = "user123";
+ event2.value = 100.0;
+ event2.timestamp = 1000L;
+
+ assertFalse(event1.equals(event2));
+ }
+
+ @Test
+ public void equalsReturnsFalseForDifferentKey() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+
+ Event event2 = new Event();
+ event2.eventTypeId = "purchase";
+ event2.trafficTypeName = "user";
+ event2.key = "user456";
+ event2.value = 100.0;
+ event2.timestamp = 1000L;
+
+ assertFalse(event1.equals(event2));
+ }
+
+ @Test
+ public void equalsReturnsFalseForDifferentValue() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+
+ Event event2 = new Event();
+ event2.eventTypeId = "purchase";
+ event2.trafficTypeName = "user";
+ event2.key = "user123";
+ event2.value = 200.0;
+ event2.timestamp = 1000L;
+
+ assertFalse(event1.equals(event2));
+ }
+
+ @Test
+ public void equalsReturnsFalseForDifferentTimestamp() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+
+ Event event2 = new Event();
+ event2.eventTypeId = "purchase";
+ event2.trafficTypeName = "user";
+ event2.key = "user123";
+ event2.value = 100.0;
+ event2.timestamp = 2000L;
+
+ assertFalse(event1.equals(event2));
+ }
+
+ @Test
+ public void equalsIgnoresPropertiesField() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+ event1.properties = new HashMap<>();
+ event1.properties.put("color", "red");
+
+ Event event2 = new Event();
+ event2.eventTypeId = "purchase";
+ event2.trafficTypeName = "user";
+ event2.key = "user123";
+ event2.value = 100.0;
+ event2.timestamp = 1000L;
+ event2.properties = new HashMap<>();
+ event2.properties.put("size", "large");
+
+ assertTrue(event1.equals(event2));
+ }
+
+ @Test
+ public void equalsIsSelfConsistent() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+
+ Event event2 = new Event();
+ event2.eventTypeId = "purchase";
+ event2.trafficTypeName = "user";
+ event2.key = "user123";
+ event2.value = 100.0;
+ event2.timestamp = 1000L;
+
+ assertTrue(event1.equals(event2));
+ assertTrue(event1.equals(event2));
+ }
+
+ @Test
+ public void equalsIsSymmetric() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+
+ Event event2 = new Event();
+ event2.eventTypeId = "purchase";
+ event2.trafficTypeName = "user";
+ event2.key = "user123";
+ event2.value = 100.0;
+ event2.timestamp = 1000L;
+
+ assertTrue(event1.equals(event2));
+ assertTrue(event2.equals(event1));
+ }
+
+ @Test
+ public void equalsIsTransitive() {
+ Event event1 = new Event();
+ event1.eventTypeId = "purchase";
+ event1.trafficTypeName = "user";
+ event1.key = "user123";
+ event1.value = 100.0;
+ event1.timestamp = 1000L;
+
+ Event event2 = new Event();
+ event2.eventTypeId = "purchase";
+ event2.trafficTypeName = "user";
+ event2.key = "user123";
+ event2.value = 100.0;
+ event2.timestamp = 1000L;
+
+ Event event3 = new Event();
+ event3.eventTypeId = "purchase";
+ event3.trafficTypeName = "user";
+ event3.key = "user123";
+ event3.value = 100.0;
+ event3.timestamp = 1000L;
+
+ assertTrue(event1.equals(event2));
+ assertTrue(event2.equals(event3));
+ assertTrue(event1.equals(event3));
+ }
+
+}
diff --git a/tracker/src/test/java/io/split/client/events/EventsTaskTest.java b/tracker/src/test/java/io/split/client/events/EventsTaskTest.java
new file mode 100644
index 000000000..875009d44
--- /dev/null
+++ b/tracker/src/test/java/io/split/client/events/EventsTaskTest.java
@@ -0,0 +1,86 @@
+package io.split.client.events;
+
+import io.split.client.dtos.Event;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadFactory;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+public class EventsTaskTest {
+
+ private EventsStorageConsumer _storage;
+ private EventSender _sender;
+ private EventsTask _task;
+
+ @Before
+ public void setup() {
+ _storage = mock(EventsStorageConsumer.class);
+ _sender = mock(EventSender.class);
+ ThreadFactory tf = r -> {
+ Thread t = new Thread(r, "test-events-thread");
+ t.setDaemon(true);
+ return t;
+ };
+ _task = new EventsTask(_storage, 30_000L, _sender, tf);
+ }
+
+ @Test
+ public void sendEventsDoesNothingWhenQueueEmpty() {
+ when(_storage.popAll()).thenReturn(Collections.emptyList());
+ _task.sendEvents();
+ verifyZeroInteractions(_sender);
+ }
+
+ @Test
+ public void sendEventsBatchesAllQueuedEvents() {
+ Event e1 = makeEvent("click");
+ Event e2 = makeEvent("purchase");
+ when(_storage.popAll()).thenReturn(Arrays.asList(
+ new WrappedEvent(e1, 10), new WrappedEvent(e2, 20)));
+
+ _task.sendEvents();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(List.class);
+ verify(_sender).send(captor.capture());
+ assertEquals(2, captor.getValue().size());
+ assertTrue(captor.getValue().contains(e1));
+ assertTrue(captor.getValue().contains(e2));
+ }
+
+ @Test
+ public void closeFlushesRemainingEventsBeforeShutdown() {
+ Event e = makeEvent("close-event");
+ when(_storage.popAll()).thenReturn(
+ Collections.singletonList(new WrappedEvent(e, 10)));
+
+ _task.close();
+
+ verify(_sender, atLeastOnce()).send(anyList());
+ }
+
+ @Test
+ public void sendEventsDoesNotThrowWhenQueueIsFull() {
+ when(_storage.isFull()).thenReturn(true);
+ when(_storage.popAll()).thenReturn(Collections.emptyList());
+ _task.sendEvents();
+ verify(_storage).isFull();
+ verify(_storage).popAll();
+ verifyZeroInteractions(_sender);
+ }
+
+ private static Event makeEvent(String type) {
+ Event e = new Event();
+ e.eventTypeId = type;
+ e.key = "k";
+ e.trafficTypeName = "user";
+ e.timestamp = System.currentTimeMillis();
+ return e;
+ }
+}
diff --git a/tracker/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java b/tracker/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java
new file mode 100644
index 000000000..8f2eca6c9
--- /dev/null
+++ b/tracker/src/test/java/io/split/client/events/InMemoryEventsStorageTest.java
@@ -0,0 +1,74 @@
+package io.split.client.events;
+
+import io.split.client.dtos.Event;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+public class InMemoryEventsStorageTest {
+
+ private EventQueueStats _stats;
+ private InMemoryEventsStorage _storage;
+
+ @Before
+ public void setup() {
+ _stats = Mockito.mock(EventQueueStats.class);
+ _storage = new InMemoryEventsStorage(5, _stats);
+ }
+
+ @Test
+ public void trackReturnsTrue_andNotifiesQueued() {
+ assertTrue(_storage.track(makeEvent("myType"), 100));
+ verify(_stats).onQueued(1);
+ verifyNoMoreInteractions(_stats);
+ }
+
+ @Test
+ public void trackNullEventReturnsFalse() {
+ assertFalse(_storage.track(null, 0));
+ verifyZeroInteractions(_stats);
+ }
+
+ @Test
+ public void isFullWhenCapacityReached() {
+ for (int i = 0; i < 5; i++) _storage.track(makeEvent("t" + i), 10);
+ assertTrue(_storage.isFull());
+ }
+
+ @Test
+ public void dropWhenFullNotifiesDropped() {
+ for (int i = 0; i < 5; i++) _storage.track(makeEvent("t" + i), 10);
+ assertFalse(_storage.track(makeEvent("overflow"), 10));
+ verify(_stats).onDropped(1);
+ }
+
+ @Test
+ public void popAllDrainsQueue() {
+ _storage.track(makeEvent("a"), 10);
+ _storage.track(makeEvent("b"), 20);
+ List popped = _storage.popAll();
+ assertEquals(2, popped.size());
+ assertEquals("a", popped.get(0).event().eventTypeId);
+ assertEquals("b", popped.get(1).event().eventTypeId);
+ assertTrue(_storage.popAll().isEmpty());
+ }
+
+ @Test
+ public void popAllReturnsEmptyWhenQueueIsEmpty() {
+ assertTrue(_storage.popAll().isEmpty());
+ }
+
+ private static Event makeEvent(String eventTypeId) {
+ Event e = new Event();
+ e.eventTypeId = eventTypeId;
+ e.key = "key";
+ e.trafficTypeName = "user";
+ e.timestamp = System.currentTimeMillis();
+ return e;
+ }
+}