diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 908cacdfd..da43b6fbb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,6 +5,11 @@ on: branches: - 'development' - '*_baseline' + - 'release_5.2.0-alpha.1' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build-app: @@ -15,16 +20,16 @@ jobs: ARTIFACTORY_TOKEN: ${{ secrets.ARTIFACTORY_TOKEN }} steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Gradle cache - uses: gradle/gradle-build-action@v2.4.2 + uses: gradle/gradle-build-action@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: 11 + java-version: 17 cache: 'gradle' - name: Publish diff --git a/.github/workflows/instrumented.yml b/.github/workflows/instrumented.yml index ad07f29d9..2fa860c9f 100644 --- a/.github/workflows/instrumented.yml +++ b/.github/workflows/instrumented.yml @@ -7,9 +7,13 @@ on: - 'master' - '*_baseline' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: fail-fast: false @@ -18,23 +22,17 @@ jobs: shard: [ 0, 1, 2, 3 ] steps: - - name: Enable KVM group perms - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: checkout uses: actions/checkout@v4 - name: Gradle cache uses: gradle/gradle-build-action@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 11 + java-version: 17 cache: 'gradle' - name: AVD cache @@ -46,6 +44,12 @@ jobs: ~/.android/adb* key: avd-${{ matrix.api-level }} + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99bd6aa16..05f003a70 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,22 +4,26 @@ on: branches: - '*' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-app: name: Build App runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Gradle cache uses: gradle/gradle-build-action@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 11 + java-version: 17 cache: 'gradle' - name: Test with Gradle diff --git a/.gitignore b/.gitignore index b54870c45..f09c2a403 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ gradle.properties *.iml .DS_Store .settings/org.eclipse.buildship.core.prefs -.gradle \ No newline at end of file +.gradle +.vscode/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5b9545e55..03b1c2416 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '5.1.2-rc1' + splitVersion = '5.2.0-alpha.1' } android { diff --git a/src/androidTest/java/fake/SplitClientStub.java b/src/androidTest/java/fake/SplitClientStub.java index d500ef366..4acebddbc 100644 --- a/src/androidTest/java/fake/SplitClientStub.java +++ b/src/androidTest/java/fake/SplitClientStub.java @@ -3,10 +3,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import io.split.android.client.EvaluationOptions; import io.split.android.client.SplitClient; import io.split.android.client.SplitResult; import io.split.android.client.events.SplitEvent; @@ -23,39 +25,79 @@ public String getTreatment(String featureFlagName, Map attribute return "control"; } + @Override + public String getTreatment(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions) { + return getTreatment(featureFlagName, attributes); + } + @Override public SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes) { + return getTreatmentWithConfig(featureFlagName, attributes, null); + } + + @Override + public SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions) { return null; } @Override public Map getTreatments(List featureFlagNames, Map attributes) { - return null; + return getTreatments(featureFlagNames, attributes, null); + } + + @Override + public Map getTreatments(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions) { + return Collections.emptyMap(); } @Override public Map getTreatmentsWithConfig(List featureFlagNames, Map attributes) { - return null; + return getTreatmentsWithConfig(featureFlagNames, attributes, null); + } + + @Override + public Map getTreatmentsWithConfig(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions) { + return Collections.emptyMap(); } @Override public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { - return null; + return getTreatmentsByFlagSet(flagSet, attributes, null); + } + + @Override + public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return Collections.emptyMap(); } @Override public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { - return null; + return getTreatmentsByFlagSets(flagSets, attributes, null); + } + + @Override + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return Collections.emptyMap(); } @Override public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { - return null; + return getTreatmentsWithConfigByFlagSet(flagSet, attributes, null); + } + + @Override + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return Collections.emptyMap(); } @Override public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { - return null; + return getTreatmentsWithConfigByFlagSets(flagSets, attributes, null); + } + + @Override + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return Collections.emptyMap(); } @Override diff --git a/src/androidTest/java/io/split/android/client/service/impressions/ImpressionPropertiesIntegrationTest.java b/src/androidTest/java/io/split/android/client/service/impressions/ImpressionPropertiesIntegrationTest.java new file mode 100644 index 000000000..abde66aa4 --- /dev/null +++ b/src/androidTest/java/io/split/android/client/service/impressions/ImpressionPropertiesIntegrationTest.java @@ -0,0 +1,567 @@ +package io.split.android.client.service.impressions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static helper.IntegrationHelper.getSinceFromUri; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.google.gson.reflect.TypeToken; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import fake.LifecycleManagerStub; +import fake.SynchronizerSpyImpl; +import helper.DatabaseHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.EvaluationOptions; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitFactory; +import io.split.android.client.api.Key; +import io.split.android.client.dtos.KeyImpression; +import io.split.android.client.dtos.TestImpressions; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.impressions.Impression; +import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.service.synchronizer.SynchronizerSpy; +import io.split.android.client.storage.db.ImpressionEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import tests.integration.shared.TestingHelper; + +public class ImpressionPropertiesIntegrationTest { + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private AtomicInteger mImpressionsLoggedCount; + private AtomicBoolean mPropertiesReceived; + private HttpClientMock mHttpClient; + private SplitRoomDatabase mDatabase; + private LifecycleManagerStub mLifecycleManager; + private SynchronizerSpy mSynchronizerSpy; + + @Before + public void setUp() throws IOException { + mImpressionsLoggedCount = new AtomicInteger(0); + mPropertiesReceived = new AtomicBoolean(false); + mDatabase = DatabaseHelper.getTestDatabase(mContext); + mDatabase.clearAllTables(); + mHttpClient = new HttpClientMock(getDispatcher()); + mLifecycleManager = new LifecycleManagerStub(); + mSynchronizerSpy = new SynchronizerSpyImpl(); + mLifecycleManager.register(mSynchronizerSpy); + } + + /** + * Tests that impressions include properties when provided during evaluation. + * Verifies that properties are correctly passed to the impression listener + * and stored in the database. + */ + @Test + public void impressionsIncludePropertiesWhenProvided() throws InterruptedException { + // Initialize Split SDK with impression listener + CountDownLatch countDownLatch = new CountDownLatch(1); + SplitClient client = initSplitFactory(new TestableSplitConfigBuilder() + .impressionsMode(ImpressionsMode.OPTIMIZED) + .enableDebug() + .impressionListener(new ImpressionListener() { + @Override + public void log(Impression impression) { + mImpressionsLoggedCount.incrementAndGet(); + if (impression.properties() != null && !impression.properties().isEmpty()) { + mPropertiesReceived.set(true); + } + countDownLatch.countDown(); + } + + @Override + public void close() { + // No-op + } + }), mHttpClient).client(); + + // Create properties map + Map properties = new HashMap<>(); + properties.put("string_prop", "value"); + properties.put("number_prop", 42); + properties.put("bool_prop", true); + + // Get treatment with properties + evaluateWithProperties(client, properties); + + boolean await = countDownLatch.await(5, TimeUnit.SECONDS); + + // Verify impressions were recorded with properties + Thread.sleep(200); + List impressionEntities = mDatabase.impressionDao().getAll(); + assertEquals(1, impressionEntities.size()); + assertEquals(1, mImpressionsLoggedCount.get()); + assertTrue(await); + assertTrue(mPropertiesReceived.get()); + } + + /** + * Tests that impressions without properties do not include a properties field. + * Verifies that when evaluations are done without properties, the impression + * listener and database do not receive properties. + */ + @Test + public void impressionsWithoutPropertiesDoNotIncludePropertiesField() throws InterruptedException { + // Initialize Split SDK with impression listener + CountDownLatch countDownLatch = new CountDownLatch(1); + SplitClient client = initSplitFactory(new TestableSplitConfigBuilder() + .impressionsMode(ImpressionsMode.DEBUG) + .enableDebug() + .impressionListener(new ImpressionListener() { + @Override + public void log(Impression impression) { + mImpressionsLoggedCount.incrementAndGet(); + if (impression.properties() != null && !impression.properties().isEmpty()) { + mPropertiesReceived.set(true); + } + countDownLatch.countDown(); + } + + @Override + public void close() { + // No-op + } + }), mHttpClient).client(); + + // Get treatment without properties + client.getTreatment("FACUNDO_TEST"); + + // Wait for impression processing + boolean await = countDownLatch.await(5, TimeUnit.SECONDS); + + // Verify impressions were recorded without properties + Thread.sleep(200); + List impressionEntities = mDatabase.impressionDao().getAll(); + assertTrue(await); + assertEquals(1, impressionEntities.size()); + assertEquals(1, mImpressionsLoggedCount.get()); + assertFalse(mPropertiesReceived.get()); + } + + /** + * Tests that impressions with different properties are not deduplicated in OPTIMIZED mode. + * Verifies that when the same feature flag is evaluated multiple times with different + * properties, each impression is tracked separately. + */ + @Test + public void impressionsWithPropertiesAreNotDeduped() throws InterruptedException, IOException { + // Setup HTTP client to capture impressions requests + final AtomicReference capturedImpressionPayload = new AtomicReference<>(); + CountDownLatch impressionsLatch = new CountDownLatch(1); + + // Create HTTP client with impression capture + HttpClientMock httpClient = new HttpClientMock(createDispatcher( + (uri, httpMethod, body) -> { + capturedImpressionPayload.set(body); + impressionsLatch.countDown(); + return new HttpResponseMock(200, "{}"); + } + )); + + // Initialize Split SDK with OPTIMIZED mode + SplitClient client = initSplitFactory(getOptimizedConfigBuilder(), httpClient).client(); + + // Evaluate the same flag multiple times with different properties + Map properties1 = createTestProperties("test_value1", 42, true); + Map properties2 = createTestProperties("test_value2", 43, false); + + evaluateWithProperties(client, properties1); + evaluateWithProperties(client, properties2); + evaluateWithProperties(client, properties1); // Repeat with same properties + + Thread.sleep(500); + client.flush(); + + // Wait for impressions to be sent + boolean await = impressionsLatch.await(5, TimeUnit.SECONDS); + assertTrue(await); + + // Verify the payload + String payload = capturedImpressionPayload.get(); + assertNotNull("Impressions payload should not be null", payload); + + // Count impressions with each property set + countAndVerifyImpressions(payload, 2, 1); + } + + /** + * Tests that impression properties are correctly included in the network payload. + * Verifies that properties provided during evaluation are serialized and sent + * to the backend in the correct format. + */ + @Test + public void impressionsPayloadIncludesProperties() throws InterruptedException, IOException { + // Setup HTTP client to capture impressions requests + final AtomicReference capturedImpressionPayload = new AtomicReference<>(); + CountDownLatch impressionsLatch = new CountDownLatch(1); + + // Create HTTP client with impression capture + HttpClientMock httpClient = new HttpClientMock(createDispatcher( + (uri, httpMethod, body) -> { + capturedImpressionPayload.set(body); + impressionsLatch.countDown(); + return new HttpResponseMock(200, "{}"); + } + )); + + // Initialize Split SDK with DEBUG mode + SplitClient client = initSplitFactory(getDebugConfigBuilder(), httpClient).client(); + + // Evaluate flags with and without properties + evaluateWithoutProperties(client); + evaluateWithDifferentProperties(client); + + Thread.sleep(500); + client.flush(); + + // Wait for impressions to be sent + boolean await = impressionsLatch.await(5, TimeUnit.SECONDS); + assertTrue(await); + + // Verify the payload + String payload = capturedImpressionPayload.get(); + assertNotNull("Impressions payload should not be null", payload); + + // Deserialize and verify impressions + verifyImpressionPayload(payload); + } + + /** + * Tests that impressions in NONE mode still track properties in the impression listener. + * Verifies that even when impressions are not sent to the backend (NONE mode), + * properties are still passed to the impression listener. + */ + @Test + public void impressionsInNoneModeStillTrackPropertiesInListener() throws InterruptedException { + // Reset counters for the test + mImpressionsLoggedCount.set(0); + mPropertiesReceived.set(false); + mDatabase.clearAllTables(); + + // Create a latch to wait for impression listener + CountDownLatch listenerLatch = new CountDownLatch(1); + AtomicReference capturedProperties = new AtomicReference<>(); + + // Initialize Split SDK with NONE mode and impression listener + SplitClient client = initSplitFactory(new TestableSplitConfigBuilder() + .impressionsMode(ImpressionsMode.NONE) // Use NONE mode which doesn't send impressions to backend + .enableDebug() + .impressionListener(new ImpressionListener() { + @Override + public void log(Impression impression) { + mImpressionsLoggedCount.incrementAndGet(); + if (impression.properties() != null && !impression.properties().isEmpty()) { + capturedProperties.set(impression.properties()); + mPropertiesReceived.set(true); + listenerLatch.countDown(); + } + } + + @Override + public void close() { + // No-op + } + }), mHttpClient).client(); + + // Create test properties + Map properties = createTestProperties("test_value", 42, true); + + // Evaluate with properties + evaluateWithProperties(client, properties); + + // Wait for impression listener to be called + boolean await = listenerLatch.await(5, TimeUnit.SECONDS); + + // Verify impressions were tracked in listener but not in storage + assertTrue("Impression listener should be called", await); + assertEquals("Should have 1 impression logged", 1, mImpressionsLoggedCount.get()); + assertTrue("Properties should be received in listener", mPropertiesReceived.get()); + assertNotNull("Properties should be captured", capturedProperties.get()); + assertTrue("Properties should contain test value", capturedProperties.get().contains("test_value")); + + // Verify no impressions were stored (NONE mode) + Thread.sleep(200); // Wait for any potential DB operations + List impressionEntities = mDatabase.impressionDao().getAll(); + assertEquals("No impressions should be stored in NONE mode", 0, impressionEntities.size()); + } + + /** + * Tests that impressions in DEBUG mode track all properties without deduplication. + * Verifies that in DEBUG mode, all impressions with properties are tracked and + * sent to the backend, regardless of frequency or similarity. + */ + @Test + public void impressionsInDebugModeTrackAllProperties() throws InterruptedException, IOException { + // Setup HTTP client to capture impressions requests + final AtomicReference capturedImpressionPayload = new AtomicReference<>(); + CountDownLatch impressionsLatch = new CountDownLatch(1); + + // Create HTTP client with impression capture + HttpClientMock httpClient = new HttpClientMock(createDispatcher( + (uri, httpMethod, body) -> { + capturedImpressionPayload.set(body); + impressionsLatch.countDown(); + return new HttpResponseMock(200, "{}"); + } + )); + + // Initialize Split SDK with DEBUG mode + SplitClient client = initSplitFactory(getDebugConfigBuilder(), httpClient).client(); + + // Create different property sets + Map properties1 = createTestProperties("test_value1", 42, true); + Map properties2 = createTestProperties("test_value2", 43, false); + Map properties3 = createTestProperties("test_value3", 44, true); + + // Evaluate with multiple property sets in quick succession + evaluateWithProperties(client, properties1); + evaluateWithProperties(client, properties2); + evaluateWithProperties(client, properties3); + + // Add a small delay before flushing to ensure impressions are queued + Thread.sleep(500); + + // Explicitly flush to ensure impressions are sent + client.flush(); + + // Wait for impressions to be sent with increased timeout + boolean await = impressionsLatch.await(10, TimeUnit.SECONDS); + assertTrue("Impressions should be sent after flush", await); + + // Verify the payload + String payload = capturedImpressionPayload.get(); + assertNotNull("Impressions payload should not be null", payload); + + // Verify all impressions were sent + Type testImpressionsListType = new TypeToken>(){}.getType(); + List testImpressions = Json.fromJson(payload, testImpressionsListType); + + // Count total impressions with properties + int totalImpressionsWithProperties = 0; + for (TestImpressions testImpression : testImpressions) { + for (KeyImpression keyImpression : testImpression.keyImpressions) { + if (keyImpression.properties != null && !keyImpression.properties.isEmpty()) { + totalImpressionsWithProperties++; + } + } + } + + // In DEBUG mode, all impressions should be tracked (not deduplicated) + assertEquals("All impressions with properties should be tracked in DEBUG mode", + 3, totalImpressionsWithProperties); + + // Verify each property set is present + boolean foundProperties1 = false; + boolean foundProperties2 = false; + boolean foundProperties3 = false; + + for (TestImpressions testImpression : testImpressions) { + for (KeyImpression keyImpression : testImpression.keyImpressions) { + if (keyImpression.properties != null) { + if (keyImpression.properties.contains("test_value1")) { + foundProperties1 = true; + } else if (keyImpression.properties.contains("test_value2")) { + foundProperties2 = true; + } else if (keyImpression.properties.contains("test_value3")) { + foundProperties3 = true; + } + } + } + } + + assertTrue("Should find impression with first property set", foundProperties1); + assertTrue("Should find impression with second property set", foundProperties2); + assertTrue("Should find impression with third property set", foundProperties3); + } + + private HttpResponseMockDispatcher getDispatcher() { + return createDispatcher(null); + } + + private HttpResponseMockDispatcher createDispatcher(IntegrationHelper.ResponseClosure impressionsHandler) { + Map responses = new HashMap<>(); + + // Add standard responses + responses.put(IntegrationHelper.ServicePath.SPLIT_CHANGES, (uri, httpMethod, body) -> { + String since = getSinceFromUri(uri); + if (since.equals("-1")) { + return new HttpResponseMock(200, loadSplitChanges()); + } else { + return new HttpResponseMock(200, IntegrationHelper.emptySplitChanges(1602796638344L, 1602796638344L)); + } + }); + + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/CUSTOMER_ID", (uri, httpMethod, body) -> + new HttpResponseMock(200, IntegrationHelper.emptyMySegments())); + + // Add custom impressions handler if provided + if (impressionsHandler != null) { + responses.put(IntegrationHelper.ServicePath.IMPRESSIONS, impressionsHandler); + } + + return IntegrationHelper.buildDispatcher(responses); + } + + private SplitFactory initSplitFactory(TestableSplitConfigBuilder builder, HttpClientMock httpClient) throws InterruptedException { + CountDownLatch innerLatch = new CountDownLatch(1); + SplitFactory factory = IntegrationHelper.buildFactory( + "sdk_key_1", + new Key("CUSTOMER_ID"), + builder.build(), + mContext, + httpClient, + mDatabase, + mSynchronizerSpy, + null, + mLifecycleManager); + + SplitClient client = factory.client(); + client.on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(innerLatch)); + boolean await = innerLatch.await(5, TimeUnit.SECONDS); + if (!await) { + fail("Client is not ready"); + } + return factory; + } + + private String loadSplitChanges() { + return IntegrationHelper.loadSplitChanges(mContext, "split_changes_1.json"); + } + + private static void evaluateWithProperties(SplitClient client, Map properties) { + client.getTreatment("FACUNDO_TEST", null, new EvaluationOptions(properties)); + } + + private Map createTestProperties(String stringValue, int numberValue, boolean boolValue) { + Map properties = new HashMap<>(); + properties.put("string_prop", stringValue); + properties.put("number_prop", numberValue); + properties.put("bool_prop", boolValue); + return properties; + } + + private void evaluateWithoutProperties(SplitClient client) { + client.getTreatment("FACUNDO_TEST"); + } + + private void evaluateWithDifferentProperties(SplitClient client) { + Map properties1 = createTestProperties("test_value1", 42, true); + Map properties2 = createTestProperties("test_value2", 43, false); + + evaluateWithProperties(client, properties1); + evaluateWithProperties(client, properties2); + } + + private void verifyImpressionPayload(String payload) { + // Deserialize the payload to verify properties + Type testImpressionsListType = new TypeToken>(){}.getType(); + List testImpressions = Json.fromJson(payload, testImpressionsListType); + + // Verify we have impressions + assertNotNull("Deserialized impressions should not be null", testImpressions); + assertFalse("Impressions list should not be empty", testImpressions.isEmpty()); + + // Check for impressions with and without properties + boolean foundWithoutProperties = false; + boolean foundWithProperties1 = false; + boolean foundWithProperties2 = false; + + for (TestImpressions testImpression : testImpressions) { + for (KeyImpression keyImpression : testImpression.keyImpressions) { + if (keyImpression.properties == null) { + foundWithoutProperties = true; + } else if (keyImpression.properties.contains("test_value1") && + keyImpression.properties.contains("42") && + keyImpression.properties.contains("true")) { + foundWithProperties1 = true; + } else if (keyImpression.properties.contains("test_value2") && + keyImpression.properties.contains("43") && + keyImpression.properties.contains("false")) { + foundWithProperties2 = true; + } + } + } + + assertTrue("Should find impression without properties", foundWithoutProperties); + assertTrue("Should find impression with first set of properties", foundWithProperties1); + assertTrue("Should find impression with second set of properties", foundWithProperties2); + } + + /** + * Counts and verifies impressions with different property sets + * @param payload The JSON payload to analyze + * @param expectedCount1 Expected count of impressions with first property set + * @param expectedCount2 Expected count of impressions with second property set + */ + private void countAndVerifyImpressions(String payload, int expectedCount1, int expectedCount2) { + // Deserialize the payload to verify properties + Type testImpressionsListType = new TypeToken>(){}.getType(); + List testImpressions = Json.fromJson(payload, testImpressionsListType); + + // Verify we have impressions + assertNotNull("Deserialized impressions should not be null", testImpressions); + assertFalse("Impressions list should not be empty", testImpressions.isEmpty()); + + // Count impressions with each property set + int impressionsWithProperties1 = 0; + int impressionsWithProperties2 = 0; + + for (TestImpressions testImpression : testImpressions) { + for (KeyImpression keyImpression : testImpression.keyImpressions) { + if (keyImpression.properties != null) { + if (keyImpression.properties.contains("test_value1") && + keyImpression.properties.contains("42") && + keyImpression.properties.contains("true")) { + impressionsWithProperties1++; + } else if (keyImpression.properties.contains("test_value2") && + keyImpression.properties.contains("43") && + keyImpression.properties.contains("false")) { + impressionsWithProperties2++; + } + } + } + } + + assertEquals("Unexpected count of impressions with first property set", + expectedCount1, impressionsWithProperties1); + assertEquals("Unexpected count of impressions with second property set", + expectedCount2, impressionsWithProperties2); + } + + private TestableSplitConfigBuilder getDebugConfigBuilder() { + return new TestableSplitConfigBuilder() + .impressionsMode(ImpressionsMode.DEBUG) + .enableDebug(); + } + + private TestableSplitConfigBuilder getOptimizedConfigBuilder() { + return new TestableSplitConfigBuilder() + .impressionsMode(ImpressionsMode.OPTIMIZED) + .enableDebug(); + } +} diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java b/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java index 250044137..bd6fae1bf 100644 --- a/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java +++ b/src/androidTest/java/io/split/android/client/service/impressions/observer/ImpressionsObserverTest.java @@ -47,6 +47,7 @@ private List generateImpressions(long count) { System.currentTimeMillis(), (i % 2 == 0) ? "in segment all" : "whitelisted", i * i, + null, null); imps.add(imp); } @@ -61,6 +62,7 @@ public void testBasicFunctionality() { "on", System.currentTimeMillis(), "in segment all", 1234L, + null, null); // Add 5 new impressions so that the old one is evicted and re-try the test. @@ -80,13 +82,14 @@ public void testValuesArePersistedAcrossInstances() throws InterruptedException "on", System.currentTimeMillis(), "in segment all", 1234L, + null, null); Impression imp2 = new Impression("someOtherKey", null, "someOtherFeature", "on", System.currentTimeMillis(), "in segment all", 1234L, - null); + null, null); // These are not in the cache, so they should return null Long firstImp = observer.testAndSet(imp); @@ -167,6 +170,23 @@ public void persistCallsPersistOnStorage() { verify(cache).persist(); } + @Test + public void previousTimeIsAlwaysNullWhenImpressionHasProperties() { + ImpressionsObserver observer = new ImpressionsObserverImpl(mStorage, 1); + Impression i = new Impression("key", + null, + "feature", + "on", + System.currentTimeMillis(), + "label", + 1234567L, + null, + "{\"key\":\"value\"}"); + for (int j = 0; j < 10; j++) { + assertNull(observer.testAndSet(i)); + } + } + private void caller(ImpressionsObserver o, int count, ConcurrentLinkedQueue imps) { while (count-- > 0) { @@ -177,6 +197,7 @@ private void caller(ImpressionsObserver o, int count, ConcurrentLinkedQueue getTreatments(List featureFlagNames, Map getTreatments(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions) { + return Collections.emptyMap(); + } + @Override public Map getTreatmentsWithConfig(List featureFlagNames, Map attributes) { Map results = new HashMap<>(); @@ -50,36 +55,71 @@ public Map getTreatmentsWithConfig(List featureFlag return results; } + @Override + public Map getTreatmentsWithConfig(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions) { + return Collections.emptyMap(); + } + @Override public String getTreatment(String featureFlagName, Map attributes) { return Treatments.CONTROL; } + @Override + public String getTreatment(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions) { + return getTreatment(featureFlagName, attributes); + } + @Override public SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes) { return new SplitResult(Treatments.CONTROL); } + @Override + public SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions) { + return getTreatmentWithConfig(featureFlagName, attributes); + } + @Override public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { return Collections.emptyMap(); } + @Override + public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return Collections.emptyMap(); + } + @Override public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { return Collections.emptyMap(); } + @Override + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return getTreatmentsByFlagSets(flagSets, attributes); + } + @Override public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { return Collections.emptyMap(); } + @Override + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return getTreatmentsWithConfigByFlagSet(flagSet, attributes); + } + @Override public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { return Collections.emptyMap(); } + @Override + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return getTreatmentsWithConfigByFlagSets(flagSets, attributes); + } + @Override public boolean setAttribute(String attributeName, Object value) { return true; diff --git a/src/main/java/io/split/android/client/EvaluationOptions.java b/src/main/java/io/split/android/client/EvaluationOptions.java new file mode 100644 index 000000000..c78532e88 --- /dev/null +++ b/src/main/java/io/split/android/client/EvaluationOptions.java @@ -0,0 +1,42 @@ +package io.split.android.client; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class EvaluationOptions { + + private final Map mProperties; + + public EvaluationOptions(Map properties) { + mProperties = properties != null ? new HashMap<>(properties) : null; + } + + public Map getProperties() { + return mProperties != null ? new HashMap<>(mProperties) : null; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof EvaluationOptions)) { + return false; + } + EvaluationOptions other = (EvaluationOptions) obj; + if (mProperties == null) { + return other.mProperties == null; + } + return mProperties.equals(other.mProperties); + } + + @Override + public int hashCode() { + return mProperties != null ? mProperties.hashCode() : 0; + } +} diff --git a/src/main/java/io/split/android/client/EventPropertiesProcessor.java b/src/main/java/io/split/android/client/EventPropertiesProcessor.java deleted file mode 100644 index fd7bf359d..000000000 --- a/src/main/java/io/split/android/client/EventPropertiesProcessor.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.split.android.client; - -import java.util.Map; - -public interface EventPropertiesProcessor { - ProcessedEventProperties process(Map properties); -} diff --git a/src/main/java/io/split/android/client/EventsTrackerImpl.java b/src/main/java/io/split/android/client/EventsTrackerImpl.java index aa572a04e..0b8d18982 100644 --- a/src/main/java/io/split/android/client/EventsTrackerImpl.java +++ b/src/main/java/io/split/android/client/EventsTrackerImpl.java @@ -13,6 +13,7 @@ import io.split.android.client.telemetry.storage.TelemetryStorageProducer; import io.split.android.client.utils.logger.Logger; import io.split.android.client.validators.EventValidator; +import io.split.android.client.validators.PropertyValidator; import io.split.android.client.validators.ValidationErrorInfo; import io.split.android.client.validators.ValidationMessageLogger; @@ -23,26 +24,27 @@ public class EventsTrackerImpl implements EventsTracker { private final EventValidator mEventValidator; private final ValidationMessageLogger mValidationLogger; private final TelemetryStorageProducer mTelemetryStorageProducer; - private final EventPropertiesProcessor mEventPropertiesProcessor; + private final PropertyValidator mPropertyValidator; private final SyncManager mSyncManager; private final AtomicBoolean isTrackingEnabled = new AtomicBoolean(true); public EventsTrackerImpl(@NonNull EventValidator eventValidator, @NonNull ValidationMessageLogger validationLogger, @NonNull TelemetryStorageProducer telemetryStorageProducer, - @NonNull EventPropertiesProcessor eventPropertiesProcessor, + @NonNull PropertyValidator eventPropertiesProcessor, @NonNull SyncManager syncManager) { mEventValidator = checkNotNull(eventValidator); mValidationLogger = checkNotNull(validationLogger); mTelemetryStorageProducer = checkNotNull(telemetryStorageProducer); - mEventPropertiesProcessor = checkNotNull(eventPropertiesProcessor); + mPropertyValidator = checkNotNull(eventPropertiesProcessor); mSyncManager = checkNotNull(syncManager); } public void enableTracking(boolean enable) { isTrackingEnabled.set(enable); } + public boolean track(String key, String trafficType, String eventType, double value, Map properties, boolean isSdkReady) { @@ -73,8 +75,8 @@ public boolean track(String key, String trafficType, String eventType, event.trafficTypeName = event.trafficTypeName.toLowerCase(); } - ProcessedEventProperties processedProperties = - mEventPropertiesProcessor.process(event.properties); + PropertyValidator.Result processedProperties = + mPropertyValidator.validate(event.properties, validationTag); if (!processedProperties.isValid()) { return false; } diff --git a/src/main/java/io/split/android/client/EventPropertiesProcessorImpl.java b/src/main/java/io/split/android/client/PropertyValidatorImpl.java similarity index 66% rename from src/main/java/io/split/android/client/EventPropertiesProcessorImpl.java rename to src/main/java/io/split/android/client/PropertyValidatorImpl.java index a6fd3430b..01cc06ef6 100644 --- a/src/main/java/io/split/android/client/EventPropertiesProcessorImpl.java +++ b/src/main/java/io/split/android/client/PropertyValidatorImpl.java @@ -4,32 +4,32 @@ import java.util.Map; import io.split.android.client.utils.logger.Logger; +import io.split.android.client.validators.PropertyValidator; import io.split.android.client.validators.ValidationConfig; -public class EventPropertiesProcessorImpl implements EventPropertiesProcessor { +public class PropertyValidatorImpl implements PropertyValidator { - private static final String VALIDATION_TAG = "track"; private final static int MAX_PROPS_COUNT = 300; private final static int MAXIMUM_EVENT_PROPERTY_BYTES = ValidationConfig.getInstance().getMaximumEventPropertyBytes(); @Override - public ProcessedEventProperties process(Map properties) { + public Result validate(Map properties, String validationTag) { if (properties == null) { - return new ProcessedEventProperties(true, null, 0); + return Result.valid(null, 0); } if (properties.size() > MAX_PROPS_COUNT) { - Logger.w(VALIDATION_TAG + "Event has more than " + MAX_PROPS_COUNT + + Logger.w(validationTag + "Event has more than " + MAX_PROPS_COUNT + " properties. Some of them will be trimmed when processed"); } int sizeInBytes = 0; Map finalProperties = new HashMap<>(properties); - for (Map.Entry entry : properties.entrySet()) { + for (Map.Entry entry : properties.entrySet()) { Object value = entry.getValue(); - String key = entry.getKey().toString(); + String key = entry.getKey(); if (value != null && isInvalidValueType(value)) { finalProperties.put(key, null); @@ -37,29 +37,27 @@ public ProcessedEventProperties process(Map properties) { sizeInBytes += calculateEventSizeInBytes(key, value); if (sizeInBytes > MAXIMUM_EVENT_PROPERTY_BYTES) { - Logger.w(VALIDATION_TAG + + Logger.w(validationTag + "The maximum size allowed for the " + " properties is 32kb. Current is " + key + ". Event not queued"); - return ProcessedEventProperties.InvalidProperties(); + return Result.invalid("Event properties size is too large", sizeInBytes); } } - return new ProcessedEventProperties(true, finalProperties, sizeInBytes); + return Result.valid(finalProperties, sizeInBytes); } - private boolean isInvalidValueType(Object value) { + private static boolean isInvalidValueType(Object value) { return !(value instanceof Number) && !(value instanceof Boolean) && !(value instanceof String); } - private int calculateEventSizeInBytes(String key, Object value) { + private static int calculateEventSizeInBytes(String key, Object value) { int valueSize = 0; if(value != null && value.getClass() == String.class) { valueSize = value.toString().getBytes().length; } return valueSize + key.getBytes().length; } - - } diff --git a/src/main/java/io/split/android/client/SplitClient.java b/src/main/java/io/split/android/client/SplitClient.java index 7214ffcc6..63d35f457 100644 --- a/src/main/java/io/split/android/client/SplitClient.java +++ b/src/main/java/io/split/android/client/SplitClient.java @@ -59,6 +59,7 @@ public interface SplitClient extends AttributesManager { */ String getTreatment(String featureFlagName, Map attributes); + String getTreatment(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions); /** * This method is useful when you want to determine the treatment to show @@ -77,6 +78,8 @@ public interface SplitClient extends AttributesManager { */ SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes); + SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions); + /** * This method is useful when you want to determine the treatment of several feature flags at * the same time. @@ -90,6 +93,7 @@ public interface SplitClient extends AttributesManager { */ Map getTreatments(List featureFlagNames, Map attributes); + Map getTreatments(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions); /** * This method is useful when you want to determine the treatment of several feature flags at @@ -105,6 +109,8 @@ public interface SplitClient extends AttributesManager { */ Map getTreatmentsWithConfig(List featureFlagNames, Map attributes); + Map getTreatmentsWithConfig(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions); + /** * This method is useful when you want to determine the treatment of several feature flags * belonging to a specific Flag Set at the same time. @@ -115,6 +121,8 @@ public interface SplitClient extends AttributesManager { */ Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes); + Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions); + /** * This method is useful when you want to determine the treatment of several feature flags * belonging to a specific list of Flag Sets at the same time. @@ -125,6 +133,8 @@ public interface SplitClient extends AttributesManager { */ Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes); + Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions); + /** * This method is useful when you want to determine the treatment of several feature flags * belonging to a specific Flag Set @@ -135,6 +145,8 @@ public interface SplitClient extends AttributesManager { */ Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes); + Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions); + /** * This method is useful when you want to determine the treatment of several feature flags * belonging to a specific list of Flag Sets @@ -145,6 +157,8 @@ public interface SplitClient extends AttributesManager { */ Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes); + Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions); + /** * Destroys the background processes and clears the cache, releasing the resources used by * any instances of SplitClient or SplitManager generated by the client's parent SplitFactory diff --git a/src/main/java/io/split/android/client/SplitClientImpl.java b/src/main/java/io/split/android/client/SplitClientImpl.java index 7b4c097f6..913bd005e 100644 --- a/src/main/java/io/split/android/client/SplitClientImpl.java +++ b/src/main/java/io/split/android/client/SplitClientImpl.java @@ -34,7 +34,6 @@ public final class SplitClientImpl implements SplitClient { private final TreatmentManager mTreatmentManager; private final ValidationMessageLogger mValidationLogger; private final AttributesManager mAttributesManager; - private final SplitValidator mSplitValidator; private final EventsTracker mEventsTracker; private static final double TRACK_DEFAULT_VALUE = 0.0; @@ -64,7 +63,6 @@ public SplitClientImpl(SplitFactory container, mValidationLogger = new ValidationMessageLoggerImpl(); mTreatmentManager = treatmentManager; mAttributesManager = checkNotNull(attributesManager); - mSplitValidator = checkNotNull(splitValidator); } @Override @@ -110,42 +108,82 @@ public String getTreatment(String featureFlagName) { @Override public String getTreatment(String featureFlagName, Map attributes) { - return mTreatmentManager.getTreatment(featureFlagName, attributes, mIsClientDestroyed); + return getTreatment(featureFlagName, attributes, null); + } + + @Override + public String getTreatment(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions) { + return mTreatmentManager.getTreatment(featureFlagName, attributes, evaluationOptions, mIsClientDestroyed); } @Override public SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes) { - return mTreatmentManager.getTreatmentWithConfig(featureFlagName, attributes, mIsClientDestroyed); + return getTreatmentWithConfig(featureFlagName, attributes, null); + } + + @Override + public SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions) { + return mTreatmentManager.getTreatmentWithConfig(featureFlagName, attributes, evaluationOptions, mIsClientDestroyed); } @Override public Map getTreatments(List featureFlagNames, Map attributes) { - return mTreatmentManager.getTreatments(featureFlagNames, attributes, mIsClientDestroyed); + return getTreatments(featureFlagNames, attributes, null); + } + + @Override + public Map getTreatments(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions) { + return mTreatmentManager.getTreatments(featureFlagNames, attributes, evaluationOptions, mIsClientDestroyed); } @Override public Map getTreatmentsWithConfig(List featureFlagNames, Map attributes) { - return mTreatmentManager.getTreatmentsWithConfig(featureFlagNames, attributes, mIsClientDestroyed); + return getTreatmentsWithConfig(featureFlagNames, attributes, null); + } + + @Override + public Map getTreatmentsWithConfig(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions) { + return mTreatmentManager.getTreatmentsWithConfig(featureFlagNames, attributes, evaluationOptions, mIsClientDestroyed); } @Override public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { - return mTreatmentManager.getTreatmentsByFlagSet(flagSet, attributes, mIsClientDestroyed); + return getTreatmentsByFlagSet(flagSet, attributes, null); + } + + @Override + public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return mTreatmentManager.getTreatmentsByFlagSet(flagSet, attributes, evaluationOptions, mIsClientDestroyed); } @Override public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { - return mTreatmentManager.getTreatmentsByFlagSets(flagSets, attributes, mIsClientDestroyed); + return getTreatmentsByFlagSets(flagSets, attributes, null); + } + + @Override + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return mTreatmentManager.getTreatmentsByFlagSets(flagSets, attributes, evaluationOptions, mIsClientDestroyed); } @Override public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { - return mTreatmentManager.getTreatmentsWithConfigByFlagSet(flagSet, attributes, mIsClientDestroyed); + return getTreatmentsWithConfigByFlagSet(flagSet, attributes, null); + } + + @Override + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return mTreatmentManager.getTreatmentsWithConfigByFlagSet(flagSet, attributes, evaluationOptions, mIsClientDestroyed); } @Override public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { - return mTreatmentManager.getTreatmentsWithConfigByFlagSets(flagSets, attributes, mIsClientDestroyed); + return getTreatmentsWithConfigByFlagSets(flagSets, attributes, null); + } + + @Override + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions) { + return mTreatmentManager.getTreatmentsWithConfigByFlagSets(flagSets, attributes, evaluationOptions, mIsClientDestroyed); } public void on(SplitEvent event, SplitEventTask task) { diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index 9fb574d7f..d2630edbc 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -501,7 +501,7 @@ public EventsTracker getEventsTracker() { if (mEventsTracker == null) { EventValidator eventsValidator = new EventValidatorImpl(new KeyValidatorImpl(), mSplitsStorage); mEventsTracker = new EventsTrackerImpl(eventsValidator, new ValidationMessageLoggerImpl(), mTelemetryStorage, - new EventPropertiesProcessorImpl(), mSyncManager); + new PropertyValidatorImpl(), mSyncManager); } } } diff --git a/src/main/java/io/split/android/client/dtos/KeyImpression.java b/src/main/java/io/split/android/client/dtos/KeyImpression.java index 9294b78ce..8bf7f2e7e 100644 --- a/src/main/java/io/split/android/client/dtos/KeyImpression.java +++ b/src/main/java/io/split/android/client/dtos/KeyImpression.java @@ -3,6 +3,8 @@ import com.google.gson.annotations.SerializedName; +import java.util.Objects; + import io.split.android.client.service.ServiceConstants; import io.split.android.client.storage.common.InBytesSizable; import io.split.android.client.impressions.Impression; @@ -17,6 +19,7 @@ public class KeyImpression implements InBytesSizable, Identifiable { /* package private */ static final String FIELD_TIME = "m"; /* package private */ static final String FIELD_CHANGE_NUMBER = "c"; /* package private */ static final String FIELD_PREVIOUS_TIME = "pt"; + /* package private */ static final String FIELD_PROPERTIES = "properties"; public transient String feature; // Non-serializable @@ -41,6 +44,9 @@ public class KeyImpression implements InBytesSizable, Identifiable { @SerializedName(FIELD_PREVIOUS_TIME) public Long previousTime; + @SerializedName(FIELD_PROPERTIES) + public String properties; + public KeyImpression() { } @@ -53,6 +59,7 @@ public KeyImpression(Impression impression) { this.time = impression.time(); this.changeNumber = impression.changeNumber(); this.previousTime = impression.previousTime(); + this.properties = impression.properties(); } @Override @@ -63,7 +70,7 @@ public boolean equals(Object o) { KeyImpression that = (KeyImpression) o; if (time != that.time) return false; - if (feature != null ? !feature.equals(that.feature) : that.feature != null) return false; + if (!Objects.equals(feature, that.feature)) return false; if (!keyName.equals(that.keyName)) return false; if (!treatment.equals(that.treatment)) return false; @@ -71,6 +78,7 @@ public boolean equals(Object o) { return that.bucketingKey == null; } if (!previousTime.equals(that.previousTime)) return false; + if (!Objects.equals(properties, that.properties)) return false; return bucketingKey.equals(that.bucketingKey); } @@ -101,6 +109,9 @@ public static KeyImpression fromImpression(Impression impression) { keyImpression.treatment = impression.treatment(); keyImpression.label = impression.appliedRule(); keyImpression.previousTime = impression.previousTime(); + if (impression.properties() != null) { + keyImpression.properties = impression.properties(); + } return keyImpression; } diff --git a/src/main/java/io/split/android/client/impressions/Impression.java b/src/main/java/io/split/android/client/impressions/Impression.java index 42b0a7b78..89ded4504 100644 --- a/src/main/java/io/split/android/client/impressions/Impression.java +++ b/src/main/java/io/split/android/client/impressions/Impression.java @@ -1,5 +1,7 @@ package io.split.android.client.impressions; +import androidx.annotation.Nullable; + import java.util.Map; public class Impression { @@ -13,9 +15,11 @@ public class Impression { private final Long _changeNumber; private Long _previousTime; private final Map _attributes; + @Nullable + private final String _propertiesJson; - public Impression(String key, String bucketingKey, String split, String treatment, long time, String appliedRule, Long changeNumber, Map atributes) { + public Impression(String key, String bucketingKey, String split, String treatment, long time, String appliedRule, Long changeNumber, Map attributes, String propertiesJson) { _key = key; _bucketingKey = bucketingKey; _split = split; @@ -23,7 +27,8 @@ public Impression(String key, String bucketingKey, String split, String treatmen _time = time; _appliedRule = appliedRule; _changeNumber = changeNumber; - _attributes = atributes; + _attributes = attributes; + _propertiesJson = propertiesJson; } public String key() { @@ -58,6 +63,11 @@ public Map attributes() { return _attributes; } + @Nullable + public String properties() { + return _propertiesJson; + } + public Long previousTime() { return _previousTime; } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java b/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java index 77c394308..83b1bd4fe 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java @@ -12,8 +12,10 @@ import java.util.Map; import java.util.Set; +import io.split.android.client.EvaluationOptions; import io.split.android.client.EvaluatorImpl; import io.split.android.client.FlagSetsFilter; +import io.split.android.client.PropertyValidatorImpl; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; @@ -74,24 +76,24 @@ public LocalhostSplitClient(@NonNull LocalhostSplitFactory container, new EvaluatorImpl(splitsStorage, splitParser), new KeyValidatorImpl(), new SplitValidatorImpl(), getImpressionsListener(splitClientConfig), splitClientConfig.labelsEnabled(), eventsManager, attributesManager, attributesMerger, - telemetryStorageProducer, flagSetsFilter, splitsStorage, new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl()); + telemetryStorageProducer, flagSetsFilter, splitsStorage, new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl(), + new PropertyValidatorImpl()); } @Override public String getTreatment(String featureFlagName) { - try { - return mTreatmentManager.getTreatment(featureFlagName, null, mIsClientDestroyed); - } catch (Exception exception) { - Logger.e(exception); - - return Treatments.CONTROL; - } + return getTreatment(featureFlagName, Collections.emptyMap(), null); } @Override public String getTreatment(String featureFlagName, Map attributes) { + return getTreatment(featureFlagName, attributes, null); + } + + @Override + public String getTreatment(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions) { try { - return mTreatmentManager.getTreatment(featureFlagName, attributes, mIsClientDestroyed); + return mTreatmentManager.getTreatment(featureFlagName, attributes, evaluationOptions, mIsClientDestroyed); } catch (Exception exception) { Logger.e(exception); @@ -101,8 +103,13 @@ public String getTreatment(String featureFlagName, Map attribute @Override public SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes) { + return getTreatmentWithConfig(featureFlagName, attributes, null); + } + + @Override + public SplitResult getTreatmentWithConfig(String featureFlagName, Map attributes, EvaluationOptions evaluationOptions) { try { - return mTreatmentManager.getTreatmentWithConfig(featureFlagName, attributes, mIsClientDestroyed); + return mTreatmentManager.getTreatmentWithConfig(featureFlagName, attributes, evaluationOptions, mIsClientDestroyed); } catch (Exception exception) { Logger.e(exception); @@ -112,8 +119,13 @@ public SplitResult getTreatmentWithConfig(String featureFlagName, Map getTreatments(List featureFlagNames, Map attributes) { + return getTreatments(featureFlagNames, attributes, null); + } + + @Override + public Map getTreatments(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions) { try { - return mTreatmentManager.getTreatments(featureFlagNames, attributes, mIsClientDestroyed); + return mTreatmentManager.getTreatments(featureFlagNames, attributes, evaluationOptions, mIsClientDestroyed); } catch (Exception exception) { Logger.e(exception); @@ -129,8 +141,13 @@ public Map getTreatments(List featureFlagNames, Map getTreatmentsWithConfig(List featureFlagNames, Map attributes) { + return getTreatmentsWithConfig(featureFlagNames, attributes, null); + } + + @Override + public Map getTreatmentsWithConfig(List featureFlagNames, Map attributes, EvaluationOptions evaluationOptions) { try { - return mTreatmentManager.getTreatmentsWithConfig(featureFlagNames, attributes, mIsClientDestroyed); + return mTreatmentManager.getTreatmentsWithConfig(featureFlagNames, attributes, evaluationOptions, mIsClientDestroyed); } catch (Exception exception) { Logger.e(exception); @@ -146,8 +163,13 @@ public Map getTreatmentsWithConfig(List featureFlag @Override public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + return getTreatmentsByFlagSet(flagSet, attributes, null); + } + + @Override + public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions) { try { - return mTreatmentManager.getTreatmentsByFlagSet(flagSet, attributes, mIsClientDestroyed); + return mTreatmentManager.getTreatmentsByFlagSet(flagSet, attributes, evaluationOptions, mIsClientDestroyed); } catch (Exception exception) { Logger.e(exception); @@ -157,8 +179,13 @@ public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Null @Override public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + return getTreatmentsByFlagSets(flagSets, attributes, null); + } + + @Override + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions) { try { - return mTreatmentManager.getTreatmentsByFlagSets(flagSets, attributes, mIsClientDestroyed); + return mTreatmentManager.getTreatmentsByFlagSets(flagSets, attributes, evaluationOptions, mIsClientDestroyed); } catch (Exception exception) { Logger.e(exception); @@ -168,8 +195,13 @@ public Map getTreatmentsByFlagSets(@NonNull List flagSet @Override public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes) { + return getTreatmentsWithConfigByFlagSet(flagSet, attributes, null); + } + + @Override + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions) { try { - return mTreatmentManager.getTreatmentsWithConfigByFlagSet(flagSet, attributes, mIsClientDestroyed); + return mTreatmentManager.getTreatmentsWithConfigByFlagSet(flagSet, attributes, evaluationOptions, mIsClientDestroyed); } catch (Exception exception) { Logger.e(exception); @@ -179,8 +211,13 @@ public Map getTreatmentsWithConfigByFlagSet(@NonNull String @Override public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes) { + return getTreatmentsWithConfigByFlagSets(flagSets, attributes, null); + } + + @Override + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions) { try { - return mTreatmentManager.getTreatmentsWithConfigByFlagSets(flagSets, attributes, mIsClientDestroyed); + return mTreatmentManager.getTreatmentsWithConfigByFlagSets(flagSets, attributes, evaluationOptions, mIsClientDestroyed); } catch (Exception exception) { Logger.e(exception); @@ -216,7 +253,7 @@ public void on(SplitEvent event, SplitEventTask task) { checkNotNull(task); if (!event.equals(SplitEvent.SDK_READY_FROM_CACHE) && mEventsManager.eventAlreadyTriggered(event)) { - Logger.w(String.format("A listener was added for %s on the SDK, which has already fired and won’t be emitted again. The callback won’t be executed.", event.toString())); + Logger.w(String.format("A listener was added for %s on the SDK, which has already fired and won’t be emitted again. The callback won’t be executed.", event)); return; } diff --git a/src/main/java/io/split/android/client/service/impressions/KeyImpressionSerializer.java b/src/main/java/io/split/android/client/service/impressions/KeyImpressionSerializer.java new file mode 100644 index 000000000..92432e0f3 --- /dev/null +++ b/src/main/java/io/split/android/client/service/impressions/KeyImpressionSerializer.java @@ -0,0 +1,35 @@ +package io.split.android.client.service.impressions; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; + +import io.split.android.client.dtos.KeyImpression; + +public class KeyImpressionSerializer implements JsonSerializer { + + private final Gson mGson; + + public KeyImpressionSerializer() { + mGson = new GsonBuilder() + .serializeNulls() + .create(); + } + + @Override + public JsonElement serialize(KeyImpression src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = (JsonObject) mGson.toJsonTree(src); + + // If properties is null, remove it from the JSON object + if (src.properties == null) { + jsonObject.remove("properties"); + } + + return jsonObject; + } +} diff --git a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java index 1e3df51d1..4c590d44b 100644 --- a/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java +++ b/src/main/java/io/split/android/client/service/impressions/observer/ImpressionsObserverImpl.java @@ -27,6 +27,10 @@ public Long testAndSet(Impression impression) { if (null == impression) { return null; } + final String properties = impression.properties(); + if (properties != null && !properties.isEmpty()) { + return null; + } Long hash = ImpressionHasher.process(impression); @Nullable diff --git a/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java b/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java index f0dcda0e3..42458f16e 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java +++ b/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java @@ -5,13 +5,9 @@ import androidx.annotation.NonNull; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; -import io.split.android.client.SplitClientFactoryImpl; -import io.split.android.client.SplitFactoryImpl; import io.split.android.client.utils.logger.Logger; public class SplitQueryDaoImpl implements SplitQueryDao { @@ -55,13 +51,13 @@ int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) { public Map getAllAsMap() { // Fast path - if the map is already initialized, return it immediately - if (mIsInitialized) { + if (mIsInitialized && !mCachedSplitsMap.isEmpty()) { return new HashMap<>(mCachedSplitsMap); } // Wait for initialization to complete if it's in progress synchronized (mLock) { - if (mIsInitialized) { + if (mIsInitialized && !mCachedSplitsMap.isEmpty()) { return new HashMap<>(mCachedSplitsMap); } diff --git a/src/main/java/io/split/android/client/utils/Json.java b/src/main/java/io/split/android/client/utils/Json.java index 4505210e2..a4c4e2e9c 100644 --- a/src/main/java/io/split/android/client/utils/Json.java +++ b/src/main/java/io/split/android/client/utils/Json.java @@ -4,6 +4,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; @@ -13,6 +14,8 @@ import java.util.Map; import java.util.Set; +import io.split.android.client.dtos.KeyImpression; +import io.split.android.client.service.impressions.KeyImpressionSerializer; import io.split.android.client.utils.serializer.DoubleSerializer; public class Json { @@ -20,6 +23,7 @@ public class Json { private static final Gson mJson = new GsonBuilder() .serializeNulls() .registerTypeAdapter(Double.class, new DoubleSerializer()) + .registerTypeAdapter(KeyImpression.class, new KeyImpressionSerializer()) .create(); private static volatile Gson mNonNullJson; @@ -58,6 +62,10 @@ public static Map genericValueMapFromJson(String json, Type attr return map; } + public static JsonElement toJsonTree(Object obj) { + return mJson.toJsonTree(obj); + } + private static Gson getNonNullsGsonInstance() { if (mNonNullJson == null) { synchronized (Json.class) { diff --git a/src/main/java/io/split/android/client/validators/PropertyValidator.java b/src/main/java/io/split/android/client/validators/PropertyValidator.java new file mode 100644 index 000000000..093e7b895 --- /dev/null +++ b/src/main/java/io/split/android/client/validators/PropertyValidator.java @@ -0,0 +1,56 @@ +package io.split.android.client.validators; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public interface PropertyValidator { + + Result validate(Map properties, String validationTag); + + class Result { + + private final boolean mIsValid; + @Nullable + private final Map mValidatedProperties; + private final int mSizeInBytes; + @Nullable + private final String mErrorMessage; + + private Result(boolean isValid, @Nullable Map properties, int sizeInBytes, @Nullable String errorMessage) { + mIsValid = isValid; + mValidatedProperties = properties; + mSizeInBytes = sizeInBytes; + mErrorMessage = errorMessage; + } + + public boolean isValid() { + return mIsValid; + } + + @Nullable + public Map getProperties() { + return mValidatedProperties; + } + + public int getSizeInBytes() { + return mSizeInBytes; + } + + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + @NonNull + public static Result valid(Map properties, int sizeInBytes) { + return new Result(true, properties, sizeInBytes, null); + } + + @NonNull + public static Result invalid(String errorMessage, int sizeInBytes) { + return new Result(false, null, sizeInBytes, errorMessage); + } + } +} diff --git a/src/main/java/io/split/android/client/validators/TreatmentManager.java b/src/main/java/io/split/android/client/validators/TreatmentManager.java index fbb790052..49890357d 100644 --- a/src/main/java/io/split/android/client/validators/TreatmentManager.java +++ b/src/main/java/io/split/android/client/validators/TreatmentManager.java @@ -5,23 +5,25 @@ import java.util.List; import java.util.Map; + +import io.split.android.client.EvaluationOptions; import io.split.android.client.SplitResult; public interface TreatmentManager { - String getTreatment(String split, Map attributes, boolean isClientDestroyed); + String getTreatment(String split, Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed); - SplitResult getTreatmentWithConfig(String split, Map attributes, boolean isClientDestroyed); + SplitResult getTreatmentWithConfig(String split, Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed); - Map getTreatments(List splits, Map attributes, boolean isClientDestroyed); + Map getTreatments(List splits, Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed); - Map getTreatmentsWithConfig(List splits, Map attributes, boolean isClientDestroyed); + Map getTreatmentsWithConfig(List splits, Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed); - Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, boolean isClientDestroyed); + Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed); - Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, boolean isClientDestroyed); + Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed); - Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, boolean isClientDestroyed); + Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed); - Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, boolean isClientDestroyed); + Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed); } diff --git a/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java b/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java index 3fce796d9..e9a24631a 100644 --- a/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java +++ b/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java @@ -8,6 +8,7 @@ import io.split.android.client.Evaluator; import io.split.android.client.EvaluatorImpl; import io.split.android.client.FlagSetsFilter; +import io.split.android.client.PropertyValidatorImpl; import io.split.android.client.api.Key; import io.split.android.client.attributes.AttributesManager; import io.split.android.client.attributes.AttributesMerger; @@ -30,6 +31,7 @@ public class TreatmentManagerFactoryImpl implements TreatmentManagerFactory { private final SplitsStorage mSplitsStorage; private final ValidationMessageLogger mValidationMessageLogger; private final SplitFilterValidator mFlagSetsValidator; + private final PropertyValidator mPropertyValidator; public TreatmentManagerFactoryImpl(@NonNull KeyValidator keyValidator, @NonNull SplitValidator splitValidator, @@ -51,6 +53,7 @@ public TreatmentManagerFactoryImpl(@NonNull KeyValidator keyValidator, mSplitsStorage = checkNotNull(splitsStorage); mValidationMessageLogger = new ValidationMessageLoggerImpl(); mFlagSetsValidator = new FlagSetsValidatorImpl(); + mPropertyValidator = new PropertyValidatorImpl(); } @Override @@ -70,7 +73,8 @@ public TreatmentManager getTreatmentManager(Key key, ListenableEventsManager eve mFlagSetsFilter, mSplitsStorage, mValidationMessageLogger, - mFlagSetsValidator + mFlagSetsValidator, + mPropertyValidator ); } } diff --git a/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java b/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java index 307dd310a..7493e2a7c 100644 --- a/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java +++ b/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Set; +import io.split.android.client.EvaluationOptions; import io.split.android.client.EvaluationResult; import io.split.android.client.Evaluator; import io.split.android.client.FlagSetsFilter; @@ -27,6 +28,7 @@ import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.Method; import io.split.android.client.telemetry.storage.TelemetryStorageProducer; +import io.split.android.client.utils.Json; import io.split.android.client.utils.logger.Logger; import io.split.android.grammar.Treatments; @@ -49,6 +51,7 @@ public class TreatmentManagerImpl implements TreatmentManager { private final FlagSetsFilter mFlagSetsFilter; private final SplitsStorage mSplitsStorage; private final SplitFilterValidator mFlagSetsValidator; + private final PropertyValidator mPropertyValidator; public TreatmentManagerImpl(String matchingKey, String bucketingKey, @@ -64,7 +67,8 @@ public TreatmentManagerImpl(String matchingKey, @Nullable FlagSetsFilter flagSetsFilter, @NonNull SplitsStorage splitsStorage, @NonNull ValidationMessageLogger validationLogger, - @NonNull SplitFilterValidator flagSetsValidator) { + @NonNull SplitFilterValidator flagSetsValidator, + @NonNull PropertyValidator propertyValidator) { mEvaluator = evaluator; mKeyValidator = keyValidator; mSplitValidator = splitValidator; @@ -80,15 +84,17 @@ public TreatmentManagerImpl(String matchingKey, mFlagSetsFilter = flagSetsFilter; mSplitsStorage = checkNotNull(splitsStorage); mFlagSetsValidator = checkNotNull(flagSetsValidator); + mPropertyValidator = checkNotNull(propertyValidator); } @Override - public String getTreatment(String split, Map attributes, boolean isClientDestroyed) { + public String getTreatment(String split, Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed) { try { String treatment = getTreatmentsWithConfigGeneric( Collections.singletonList(split), null, attributes, + evaluationOptions, isClientDestroyed, SplitResult::treatment, Method.TREATMENT @@ -106,12 +112,13 @@ public String getTreatment(String split, Map attributes, boolean } @Override - public SplitResult getTreatmentWithConfig(String split, Map attributes, boolean isClientDestroyed) { + public SplitResult getTreatmentWithConfig(String split, Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed) { try { SplitResult splitResult = getTreatmentsWithConfigGeneric( Collections.singletonList(split), null, attributes, + evaluationOptions, isClientDestroyed, ResultTransformer::identity, Method.TREATMENT_WITH_CONFIG @@ -128,66 +135,72 @@ public SplitResult getTreatmentWithConfig(String split, Map attr } @Override - public Map getTreatments(List splits, Map attributes, boolean isClientDestroyed) { + public Map getTreatments(List splits, Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed) { return getTreatmentsWithConfigGeneric( splits, null, attributes, + evaluationOptions, isClientDestroyed, SplitResult::treatment, Method.TREATMENTS); } @Override - public Map getTreatmentsWithConfig(List splits, Map attributes, boolean isClientDestroyed) { + public Map getTreatmentsWithConfig(List splits, Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed) { return getTreatmentsWithConfigGeneric( splits, null, attributes, + evaluationOptions, isClientDestroyed, ResultTransformer::identity, Method.TREATMENTS_WITH_CONFIG); } @Override - public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, boolean isClientDestroyed) { + public Map getTreatmentsByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed) { return getTreatmentsWithConfigGeneric( null, Collections.singletonList(flagSet), attributes, + evaluationOptions, isClientDestroyed, SplitResult::treatment, Method.TREATMENTS_BY_FLAG_SET); } @Override - public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, boolean isClientDestroyed) { + public Map getTreatmentsByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed) { return getTreatmentsWithConfigGeneric( null, flagSets, attributes, + evaluationOptions, isClientDestroyed, SplitResult::treatment, Method.TREATMENTS_BY_FLAG_SETS); } @Override - public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, boolean isClientDestroyed) { + public Map getTreatmentsWithConfigByFlagSet(@NonNull String flagSet, @Nullable Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed) { return getTreatmentsWithConfigGeneric( null, Collections.singletonList(flagSet), attributes, + evaluationOptions, isClientDestroyed, ResultTransformer::identity, Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET); } @Override - public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, boolean isClientDestroyed) { + public Map getTreatmentsWithConfigByFlagSets(@NonNull List flagSets, @Nullable Map attributes, EvaluationOptions evaluationOptions, boolean isClientDestroyed) { return getTreatmentsWithConfigGeneric( null, flagSets, attributes, + evaluationOptions, isClientDestroyed, ResultTransformer::identity, Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS); @@ -196,6 +209,7 @@ public Map getTreatmentsWithConfigByFlagSets(@NonNull List< private Map getTreatmentsWithConfigGeneric(@Nullable List names, @Nullable List flagSets, @Nullable Map attributes, + EvaluationOptions evaluationOptions, boolean isClientDestroyed, ResultTransformer resultTransformer, Method telemetryMethodName) { @@ -238,7 +252,7 @@ private Map getTreatmentsWithConfigGeneric(@Nullable List // Perform evaluations for every feature flag for (String featureFlagName : names) { - TreatmentResult evaluationResult = getTreatmentWithConfigWithoutMetrics(featureFlagName, mergedAttributes, validationTag); + TreatmentResult evaluationResult = getTreatmentWithConfigWithoutMetrics(featureFlagName, mergedAttributes, validationTag, evaluationOptions); result.put(featureFlagName, resultTransformer.transform(evaluationResult.getSplitResult())); if (evaluationResult.isException()) { @@ -261,7 +275,7 @@ private Map getTreatmentsWithConfigGeneric(@Nullable List } } - private TreatmentResult getTreatmentWithConfigWithoutMetrics(String split, Map mergedAttributes, String validationTag) { + private TreatmentResult getTreatmentWithConfigWithoutMetrics(String split, Map mergedAttributes, String validationTag, EvaluationOptions evaluationOptions) { EvaluationResult evaluationResult = null; try { @@ -296,7 +310,9 @@ private TreatmentResult getTreatmentWithConfigWithoutMetrics(String split, Map attributes, boolean impressionsDisabled) { + private void logImpression(String matchingKey, String bucketingKey, String splitName, String result, String label, Long changeNumber, Map attributes, boolean impressionsDisabled, EvaluationOptions evaluationOptions, String validationTag) { try { - Impression impression = new Impression(matchingKey, bucketingKey, splitName, result, System.currentTimeMillis(), label, changeNumber, attributes); + String propertiesJson = serializeProperties(evaluationOptions, validationTag); + Impression impression = new Impression(matchingKey, bucketingKey, splitName, result, System.currentTimeMillis(), label, changeNumber, attributes, propertiesJson); DecoratedImpression decoratedImpression = new DecoratedImpression(impression, impressionsDisabled); mImpressionListener.log(decoratedImpression); mImpressionListener.log(impression); @@ -328,6 +347,32 @@ private void logImpression(String matchingKey, String bucketingKey, String split } } + @Nullable + private String serializeProperties(@Nullable EvaluationOptions evaluationOptions, String validationTag) { + if (evaluationOptions == null || evaluationOptions.getProperties() == null || evaluationOptions.getProperties().isEmpty()) { + return null; + } + + // validate using property validator + PropertyValidator.Result result = mPropertyValidator.validate(evaluationOptions.getProperties(), validationTag); + + if (!result.isValid()) { + mValidationLogger.e("Properties validation failed: " + (result.getErrorMessage() != null ? result.getErrorMessage() : "Unknown error"), validationTag); + return null; + } + + if (result.getProperties() == null || result.getProperties().isEmpty()) { + return null; + } + + try { + return Json.toJson(result.getProperties()); + } catch (Exception e) { + mValidationLogger.e("Failed to serialize properties to JSON: " + e.getLocalizedMessage(), validationTag); + return null; + } + } + @NonNull private Map getControlTreatmentsForSplitsWithConfig(@Nullable List names, String validationTag, ResultTransformer resultTransformer) { return TreatmentManagerHelper.controlTreatmentsForSplitsWithConfig( diff --git a/src/test/java/io/split/android/client/EvaluationOptionsTest.java b/src/test/java/io/split/android/client/EvaluationOptionsTest.java new file mode 100644 index 000000000..3c57ada2d --- /dev/null +++ b/src/test/java/io/split/android/client/EvaluationOptionsTest.java @@ -0,0 +1,80 @@ +package io.split.android.client; + +import org.junit.Test; +import static org.junit.Assert.*; + +import java.util.HashMap; +import java.util.Map; + +public class EvaluationOptionsTest { + + @Test + public void equalsWithSamePropertiesReturnsTrue() { + Map props = mapOf("key1", "value1", "key2", 2); + EvaluationOptions opt1 = new EvaluationOptions(props); + EvaluationOptions opt2 = new EvaluationOptions(props); + assertEquals(opt1, opt2); + assertEquals(opt1.hashCode(), opt2.hashCode()); + } + + @Test + public void equalsWithNullPropertiesReturnsTrue() { + EvaluationOptions opt1 = optionsWithNullProps(); + EvaluationOptions opt2 = optionsWithNullProps(); + assertEquals(opt1, opt2); + assertEquals(opt1.hashCode(), opt2.hashCode()); + } + + @Test + public void equalsWithDifferentPropertiesReturnsFalse() { + EvaluationOptions opt1 = optionsWithProps("key1", "value1"); + EvaluationOptions opt2 = optionsWithProps("key1", "value2"); + assertNotEquals(opt1, opt2); + } + + @Test + public void equalsWithNullAndNonNullPropertiesReturnsFalse() { + EvaluationOptions opt1 = optionsWithProps("k", "v"); + EvaluationOptions opt2 = optionsWithNullProps(); + assertNotEquals(opt1, opt2); + assertNotEquals(opt2, opt1); + } + + @Test + public void inputMapModificationDoesNotAffectInternalState() { + Map props = mapOf("key", "value"); + EvaluationOptions opt = new EvaluationOptions(props); + props.put("key2", "value2"); + // opt's properties should not include key2 + Map optProps = opt.getProperties(); + assertFalse(optProps.containsKey("key2")); + } + + @Test + public void getPropertiesReturnsDefensiveCopy() { + EvaluationOptions opt = optionsWithProps("key", "value"); + Map first = opt.getProperties(); + Map second = opt.getProperties(); + + assertNotSame(first, second); + // Modifying the returned map does not affect internal state + first.put("another", "thing"); + assertFalse(opt.getProperties().containsKey("another")); + } + + private static Map mapOf(Object... keyValuePairs) { + Map map = new HashMap<>(); + for (int i = 0; i < keyValuePairs.length - 1; i += 2) { + map.put((String) keyValuePairs[i], keyValuePairs[i+1]); + } + return map; + } + + private static EvaluationOptions optionsWithProps(Object... keyValuePairs) { + return new EvaluationOptions(mapOf(keyValuePairs)); + } + + private static EvaluationOptions optionsWithNullProps() { + return new EvaluationOptions(null); + } +} diff --git a/src/test/java/io/split/android/client/SplitClientImplBaseTest.java b/src/test/java/io/split/android/client/SplitClientImplBaseTest.java index 7b8076a18..1b6fe4a0d 100644 --- a/src/test/java/io/split/android/client/SplitClientImplBaseTest.java +++ b/src/test/java/io/split/android/client/SplitClientImplBaseTest.java @@ -13,7 +13,6 @@ import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; import io.split.android.client.storage.splits.SplitsStorage; -import io.split.android.client.telemetry.storage.TelemetryStorageProducer; import io.split.android.client.validators.SplitValidator; import io.split.android.client.validators.TreatmentManager; import io.split.android.engine.experiments.SplitParser; diff --git a/src/test/java/io/split/android/client/SplitClientImplEvaluationOptionsTest.java b/src/test/java/io/split/android/client/SplitClientImplEvaluationOptionsTest.java new file mode 100644 index 000000000..2e1bae71e --- /dev/null +++ b/src/test/java/io/split/android/client/SplitClientImplEvaluationOptionsTest.java @@ -0,0 +1,105 @@ +package io.split.android.client; + +import static org.mockito.Mockito.verify; + +import androidx.annotation.NonNull; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SplitClientImplEvaluationOptionsTest extends SplitClientImplBaseTest { + + @Test + public void getTreatmentDelegatesToTreatmentManager() { + Map attrs = getAttrs(); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + splitClient.getTreatment("test", attrs, evaluationOptions); + + verify(treatmentManager).getTreatment("test", attrs, evaluationOptions, false); + } + + @Test + public void getTreatmentsDelegatesToTreatmentManager() { + Map attrs = getAttrs(); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + List flags = Arrays.asList("test", "test2"); + splitClient.getTreatments(flags, attrs, evaluationOptions); + + verify(treatmentManager).getTreatments(flags, attrs, evaluationOptions, false); + } + + @Test + public void getTreatmentWithConfigDelegatesToTreatmentManager() { + Map attrs = getAttrs(); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + splitClient.getTreatmentWithConfig("test", attrs, evaluationOptions); + + verify(treatmentManager).getTreatmentWithConfig("test", attrs, evaluationOptions, false); + } + + @Test + public void getTreatmentsWithConfigDelegatesToTreatmentManager() { + Map attrs = getAttrs(); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + List flags = Arrays.asList("test", "test2"); + splitClient.getTreatmentsWithConfig(flags, attrs, evaluationOptions); + + verify(treatmentManager).getTreatmentsWithConfig(flags, attrs, evaluationOptions, false); + } + + @Test + public void getTreatmentsByFlagSetDelegatesToTreatmentManager() { + Map attrs = getAttrs(); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + splitClient.getTreatmentsByFlagSet("test", attrs, evaluationOptions); + + verify(treatmentManager).getTreatmentsByFlagSet("test", attrs, evaluationOptions, false); + } + + @Test + public void getTreatmentsByFlagSetsDelegatesToTreatmentManager() { + Map attrs = getAttrs(); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + List flagSets = Arrays.asList("test", "test2"); + splitClient.getTreatmentsByFlagSets(flagSets, attrs, evaluationOptions); + + verify(treatmentManager).getTreatmentsByFlagSets(flagSets, attrs, evaluationOptions, false); + } + + @Test + public void getTreatmentsWithConfigByFlagSetDelegatesToTreatmentManager() { + Map attrs = getAttrs(); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + splitClient.getTreatmentsWithConfigByFlagSet("test", attrs, evaluationOptions); + + verify(treatmentManager).getTreatmentsWithConfigByFlagSet("test", attrs, evaluationOptions, false); + } + + @Test + public void getTreatmentsWithConfigByFlagSetsDelegatesToTreatmentManager() { + Map attrs = getAttrs(); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + List flagSets = Arrays.asList("test", "test2"); + splitClient.getTreatmentsWithConfigByFlagSets(flagSets, attrs, evaluationOptions); + + verify(treatmentManager).getTreatmentsWithConfigByFlagSets(flagSets, attrs, evaluationOptions, false); + } + + @NonNull + private static EvaluationOptions getEvaluationOptions() { + HashMap properties = new HashMap<>(); + properties.put("key", "value"); + properties.put("key2", 2); + return new EvaluationOptions(properties); + } + + private static Map getAttrs() { + Map attrs = new HashMap<>(); + attrs.put("key", "value"); + return attrs; + } +} diff --git a/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java b/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java index a3f621553..7fcad6e3d 100644 --- a/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java +++ b/src/test/java/io/split/android/client/SplitClientImplFlagSetsTest.java @@ -14,7 +14,7 @@ public void getTreatmentsByFlagSetDelegatesToTreatmentManager() { Map attributes = Collections.singletonMap("key", "value"); splitClient.getTreatmentsByFlagSet("set", attributes); - verify(treatmentManager).getTreatmentsByFlagSet("set", attributes, false); + verify(treatmentManager).getTreatmentsByFlagSet("set", attributes, null, false); } @Test @@ -22,7 +22,7 @@ public void getTreatmentsByFlagSetsDelegatesToTreatmentManager() { Map attributes = Collections.singletonMap("key", "value"); splitClient.getTreatmentsByFlagSets(Collections.singletonList("set"), attributes); - verify(treatmentManager).getTreatmentsByFlagSets(Collections.singletonList("set"), attributes, false); + verify(treatmentManager).getTreatmentsByFlagSets(Collections.singletonList("set"), attributes, null, false); } @Test @@ -30,7 +30,7 @@ public void getTreatmentsWithConfigByFlagSetDelegatesToTreatmentManager() { Map attributes = Collections.singletonMap("key", "value"); splitClient.getTreatmentsWithConfigByFlagSet("set", attributes); - verify(treatmentManager).getTreatmentsWithConfigByFlagSet("set", attributes, false); + verify(treatmentManager).getTreatmentsWithConfigByFlagSet("set", attributes, null, false); } @Test @@ -38,6 +38,6 @@ public void getTreatmentsWithConfigByFlagSetsDelegatesToTreatmentManager() { Map attributes = Collections.singletonMap("key", "value"); splitClient.getTreatmentsWithConfigByFlagSets(Collections.singletonList("set"), attributes); - verify(treatmentManager).getTreatmentsWithConfigByFlagSets(Collections.singletonList("set"), attributes, false); + verify(treatmentManager).getTreatmentsWithConfigByFlagSets(Collections.singletonList("set"), attributes, null, false); } } diff --git a/src/test/java/io/split/android/client/TreatmentManagerEvaluationOptionsTest.java b/src/test/java/io/split/android/client/TreatmentManagerEvaluationOptionsTest.java new file mode 100644 index 000000000..91fb30c09 --- /dev/null +++ b/src/test/java/io/split/android/client/TreatmentManagerEvaluationOptionsTest.java @@ -0,0 +1,151 @@ +package io.split.android.client; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +import java.util.HashMap; + +import io.split.android.client.attributes.AttributesManager; +import io.split.android.client.attributes.AttributesMerger; +import io.split.android.client.events.ListenableEventsManager; +import io.split.android.client.impressions.Impression; +import io.split.android.client.impressions.ImpressionListener; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.telemetry.storage.TelemetryStorageProducer; +import io.split.android.client.validators.FlagSetsValidatorImpl; +import io.split.android.client.validators.KeyValidator; +import io.split.android.client.validators.PropertyValidator; +import io.split.android.client.validators.SplitValidator; +import io.split.android.client.validators.TreatmentManagerImpl; +import io.split.android.client.validators.ValidationMessageLogger; + +public class TreatmentManagerEvaluationOptionsTest { + + private ImpressionListener.FederatedImpressionListener mImpressionListener; + private TreatmentManagerImpl mTreatmentManager; + private PropertyValidator mPropertyValidator; + private ValidationMessageLogger mValidationMessageLogger; + private Evaluator mEvaluator; + + @Before + public void setUp() { + mEvaluator = mock(Evaluator.class); + KeyValidator mKeyValidator = mock(KeyValidator.class); + SplitValidator mSplitValidator = mock(SplitValidator.class); + mImpressionListener = mock(ImpressionListener.FederatedImpressionListener.class); + ListenableEventsManager mEventsManager = mock(ListenableEventsManager.class); + AttributesManager mAttributesManager = mock(AttributesManager.class); + AttributesMerger mAttributesMerger = mock(AttributesMerger.class); + TelemetryStorageProducer mTelemetryStorageProducer = mock(TelemetryStorageProducer.class); + FlagSetsFilter mFlagSetsFilter = mock(FlagSetsFilter.class); + SplitsStorage mSplitsStorage = mock(SplitsStorage.class); + mPropertyValidator = mock(PropertyValidator.class); + mValidationMessageLogger = mock(ValidationMessageLogger.class); + mTreatmentManager = new TreatmentManagerImpl( + "matching_key", + "bucketing_key", + mEvaluator, + mKeyValidator, + mSplitValidator, + mImpressionListener, + SplitClientConfig.builder().build().labelsEnabled(), + mEventsManager, + mAttributesManager, + mAttributesMerger, + mTelemetryStorageProducer, + mFlagSetsFilter, + mSplitsStorage, + mValidationMessageLogger, + new FlagSetsValidatorImpl(), + mPropertyValidator); + } + + @Test + public void evaluationWithValidPropertiesAddsThemToImpressionAsJsonString() { + when(mEvaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label")); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + when(mPropertyValidator.validate(any(), any())).thenReturn(PropertyValidator.Result.valid(evaluationOptions.getProperties(), 0)); + + mTreatmentManager.getTreatmentWithConfig("test", null, evaluationOptions, false); + + verify(mImpressionListener).log(argThat(new ArgumentMatcher() { + @Override + public boolean matches(Impression argument) { + return (argument.properties().equals("{\"key\":\"value\",\"key2\":2}") || + argument.properties().equals("{\"key2\":2,\"key\":\"value\"}")) && + argument.split().equals("test"); + } + })); + } + + @Test + public void evaluationWithEmptyPropertiesAddsNullPropertiesToImpression() { + when(mEvaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label")); + when(mPropertyValidator.validate(any(), any())).thenReturn(PropertyValidator.Result.valid(null, 0)); + + mTreatmentManager.getTreatmentWithConfig("test", null, new EvaluationOptions(new HashMap<>()), false); + + verify(mImpressionListener).log(argThat(new ArgumentMatcher() { + @Override + public boolean matches(Impression argument) { + return argument.properties() == null && argument.split().equals("test"); + } + })); + } + + @Test + public void invalidPropertiesAreNotAddedToImpression() { + when(mEvaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label")); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + when(mPropertyValidator.validate(any(), any())).thenReturn(PropertyValidator.Result.invalid("Invalid properties", 0)); + + mTreatmentManager.getTreatmentWithConfig("test", null, evaluationOptions, false); + + verify(mImpressionListener).log(argThat(new ArgumentMatcher() { + @Override + public boolean matches(Impression argument) { + return argument.properties() == null && argument.split().equals("test"); + } + })); + } + + @Test + public void invalidPropertiesLogsMessageInValidationMessageLogger() { + when(mEvaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label")); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + when(mPropertyValidator.validate(any(), any())).thenReturn(PropertyValidator.Result.invalid("Invalid properties", 0)); + + mTreatmentManager.getTreatmentWithConfig("test", null, evaluationOptions, false); + + verify(mValidationMessageLogger).e("Properties validation failed: Invalid properties", "getTreatmentWithConfig"); + } + + @Test + public void propertiesAreValidatedWithPropertyValidator() { + when(mEvaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label")); + EvaluationOptions evaluationOptions = getEvaluationOptions(); + + mTreatmentManager.getTreatmentWithConfig("test", null, evaluationOptions, false); + + verify(mPropertyValidator).validate(evaluationOptions.getProperties(), "getTreatmentWithConfig"); + } + + @NonNull + private static EvaluationOptions getEvaluationOptions() { + HashMap properties = new HashMap<>(); + properties.put("key", "value"); + properties.put("key2", 2); + return new EvaluationOptions(properties); + } +} diff --git a/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java b/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java index e130f233c..e0f494bca 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java @@ -82,7 +82,8 @@ public void setUp() { mFlagSetsFilter, mSplitsStorage, new ValidationMessageLoggerImpl(), - mFlagSetsValidator); + mFlagSetsValidator, + new PropertyValidatorImpl()); when(evaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label")); } @@ -101,7 +102,7 @@ public void getTreatmentLogsImpressionWithExceptionLabelWhenExceptionOccurs() { when(evaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenThrow(new RuntimeException("test")); when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(true); - treatmentManager.getTreatment("test", Collections.emptyMap(), false); + treatmentManager.getTreatment("test", Collections.emptyMap(), null, false); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Impression.class); verify(impressionListener, times(1)).log(argumentCaptor.capture()); @@ -115,7 +116,7 @@ public void getTreatmentsLogsImpressionWithExceptionLabelWhenExceptionOccurs() { when(evaluator.getTreatment(anyString(), anyString(), eq("test2"), anyMap())).thenReturn(new EvaluationResult("on", "default")); when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(true); - Map treatments = treatmentManager.getTreatments(Arrays.asList("test", "test2"), Collections.emptyMap(), false); + Map treatments = treatmentManager.getTreatments(Arrays.asList("test", "test2"), Collections.emptyMap(), null, false); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Impression.class); verify(impressionListener, times(2)).log(argumentCaptor.capture()); @@ -134,7 +135,7 @@ public void getTreatmentWithConfigLogsImpressionWithExceptionLabelWhenExceptionO when(evaluator.getTreatment(anyString(), anyString(), eq("test"), anyMap())).thenThrow(new RuntimeException("test")); when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(true); - treatmentManager.getTreatmentWithConfig("test", Collections.emptyMap(), false); + treatmentManager.getTreatmentWithConfig("test", Collections.emptyMap(), null, false); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Impression.class); verify(impressionListener, times(1)).log(argumentCaptor.capture()); @@ -148,7 +149,7 @@ public void getTreatmentsWithConfigLogsImpressionWithExceptionLabelWhenException when(evaluator.getTreatment(anyString(), anyString(), eq("test2"), anyMap())).thenReturn(new EvaluationResult("on", "default")); when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(true); - Map treatments = treatmentManager.getTreatmentsWithConfig(Arrays.asList("test", "test2"), Collections.emptyMap(), false); + Map treatments = treatmentManager.getTreatmentsWithConfig(Arrays.asList("test", "test2"), Collections.emptyMap(), null, false); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Impression.class); verify(impressionListener, times(2)).log(argumentCaptor.capture()); @@ -170,7 +171,7 @@ public void getTreatmentsByFlagSetLogsImpressionWithExceptionLabelWhenExceptionO when(mSplitsStorage.getNamesByFlagSets(any())).thenReturn(new HashSet<>(Arrays.asList("test", "test2"))); when(mFlagSetsValidator.items(any(), any(), any())).thenReturn(Collections.singleton("set")); - Map treatments = treatmentManager.getTreatmentsByFlagSet("set", null, false); + Map treatments = treatmentManager.getTreatmentsByFlagSet("set", null, null, false); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Impression.class); verify(impressionListener, times(2)).log(argumentCaptor.capture()); @@ -192,7 +193,7 @@ public void getTreatmentsByFlagSetsLogsImpressionWithExceptionLabelWhenException when(mSplitsStorage.getNamesByFlagSets(any())).thenReturn(new HashSet<>(Arrays.asList("test", "test2"))); when(mFlagSetsValidator.items(any(), any(), any())).thenReturn(Collections.singleton("set")); - Map treatments = treatmentManager.getTreatmentsByFlagSets(Collections.singletonList("set"), null, false); + Map treatments = treatmentManager.getTreatmentsByFlagSets(Collections.singletonList("set"), null, null, false); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Impression.class); verify(impressionListener, times(2)).log(argumentCaptor.capture()); @@ -214,7 +215,7 @@ public void getTreatmentsWithConfigByFlagSetLogsImpressionWithExceptionLabelWhen when(mSplitsStorage.getNamesByFlagSets(any())).thenReturn(new HashSet<>(Arrays.asList("test", "test2"))); when(mFlagSetsValidator.items(any(), any(), any())).thenReturn(Collections.singleton("set")); - Map treatments = treatmentManager.getTreatmentsWithConfigByFlagSet("set", null, false); + Map treatments = treatmentManager.getTreatmentsWithConfigByFlagSet("set", null, null, false); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Impression.class); verify(impressionListener, times(2)).log(argumentCaptor.capture()); @@ -236,7 +237,7 @@ public void getTreatmentsWithConfigByFlagSetsLogsImpressionWithExceptionLabelWhe when(mSplitsStorage.getNamesByFlagSets(any())).thenReturn(new HashSet<>(Arrays.asList("test", "test2"))); when(mFlagSetsValidator.items(any(), any(), any())).thenReturn(Collections.singleton("set")); - Map treatments = treatmentManager.getTreatmentsWithConfigByFlagSets(Collections.singletonList("set"), null, false); + Map treatments = treatmentManager.getTreatmentsWithConfigByFlagSets(Collections.singletonList("set"), null, null, false); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Impression.class); verify(impressionListener, times(2)).log(argumentCaptor.capture()); diff --git a/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java b/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java index d4b917084..8d15f22af 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java @@ -74,7 +74,9 @@ public void setUp() { attributesMerger, telemetryStorageProducer, mFlagSetsFilter, - mSplitsStorage, new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl()); + mSplitsStorage, new ValidationMessageLoggerImpl(), + new FlagSetsValidatorImpl(), + new PropertyValidatorImpl()); when(evaluator.getTreatment(anyString(), anyString(), anyString(), anyMap())).thenReturn(new EvaluationResult("test", "label")); } @@ -91,7 +93,7 @@ public void tearDown() { @Test public void getTreatmentRecordsLatencyInTelemetry() { - treatmentManager.getTreatment("split", new HashMap<>(), false); + treatmentManager.getTreatment("split", new HashMap<>(), null, false); verify(telemetryStorageProducer).recordLatency(eq(Method.TREATMENT), anyLong()); } @@ -99,21 +101,21 @@ public void getTreatmentRecordsLatencyInTelemetry() { @Test public void getTreatmentsRecordsLatencyInTelemetry() { - treatmentManager.getTreatments(Arrays.asList("split"), new HashMap<>(), false); + treatmentManager.getTreatments(Arrays.asList("split"), new HashMap<>(), null, false); verify(telemetryStorageProducer).recordLatency(eq(Method.TREATMENTS), anyLong()); } @Test public void getTreatmentWithConfigRecordsLatencyInTelemetry() { - treatmentManager.getTreatmentWithConfig("split", new HashMap<>(), false); + treatmentManager.getTreatmentWithConfig("split", new HashMap<>(), null, false); verify(telemetryStorageProducer).recordLatency(eq(Method.TREATMENT_WITH_CONFIG), anyLong()); } @Test public void getTreatmentsWithConfigRecordsLatencyInTelemetry() { - treatmentManager.getTreatmentsWithConfig(Arrays.asList("split"), new HashMap<>(), false); + treatmentManager.getTreatmentsWithConfig(Arrays.asList("split"), new HashMap<>(), null, false); verify(telemetryStorageProducer).recordLatency(eq(Method.TREATMENTS_WITH_CONFIG), anyLong()); } @@ -122,7 +124,7 @@ public void getTreatmentsWithConfigRecordsLatencyInTelemetry() { public void nonReadyUsagesAreRecordedInProducer() { when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(false); - treatmentManager.getTreatment("test", Collections.emptyMap(), false); + treatmentManager.getTreatment("test", Collections.emptyMap(), null, false); verify(telemetryStorageProducer).recordNonReadyUsage(); } @@ -131,7 +133,7 @@ public void nonReadyUsagesAreRecordedInProducer() { public void getTreatmentRecordsException() { when(keyValidator.validate(anyString(), anyString())).thenThrow(new RuntimeException("test")); - treatmentManager.getTreatment("test", Collections.emptyMap(), false); + treatmentManager.getTreatment("test", Collections.emptyMap(), null, false); verify(telemetryStorageProducer).recordException(Method.TREATMENT); } @@ -140,7 +142,7 @@ public void getTreatmentRecordsException() { public void getTreatmentsRecordsException() { when(keyValidator.validate(anyString(), anyString())).thenThrow(new RuntimeException("test")); - treatmentManager.getTreatments(Arrays.asList("test", "test2"), Collections.emptyMap(), false); + treatmentManager.getTreatments(Arrays.asList("test", "test2"), Collections.emptyMap(), null, false); verify(telemetryStorageProducer).recordException(Method.TREATMENTS); } @@ -149,7 +151,7 @@ public void getTreatmentsRecordsException() { public void getTreatmentWithConfigRecordsException() { when(keyValidator.validate(anyString(), anyString())).thenThrow(new RuntimeException("test")); - treatmentManager.getTreatmentWithConfig("test", Collections.emptyMap(), false); + treatmentManager.getTreatmentWithConfig("test", Collections.emptyMap(), null, false); verify(telemetryStorageProducer).recordException(Method.TREATMENT_WITH_CONFIG); } @@ -158,7 +160,7 @@ public void getTreatmentWithConfigRecordsException() { public void getTreatmentsWithConfigRecordsException() { when(keyValidator.validate(anyString(), anyString())).thenThrow(new RuntimeException("test")); - treatmentManager.getTreatmentsWithConfig(Arrays.asList("test", "test2"), Collections.emptyMap(), false); + treatmentManager.getTreatmentsWithConfig(Arrays.asList("test", "test2"), Collections.emptyMap(), null, false); verify(telemetryStorageProducer).recordException(Method.TREATMENTS_WITH_CONFIG); } diff --git a/src/test/java/io/split/android/client/TreatmentManagerTest.java b/src/test/java/io/split/android/client/TreatmentManagerTest.java index db68254ca..2791953dc 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerTest.java @@ -100,7 +100,7 @@ public void testBasicEvaluationNoConfig() { String splitName = "FACUNDO_TEST"; TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, false); + SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, null, false); Assert.assertNotNull(splitResult); Assert.assertEquals("off", splitResult.treatment()); @@ -113,7 +113,7 @@ public void testBasicEvaluationWithConfig() { String splitName = "Test"; TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, false); + SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, null, false); Assert.assertNotNull(splitResult); Assert.assertEquals("off", splitResult.treatment()); @@ -126,7 +126,7 @@ public void testBasicEvaluations() { List splitList = Arrays.asList("FACUNDO_TEST", "testo2222", "Test"); TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, false); + Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, null, false); SplitResult r1 = splitResultList.get("FACUNDO_TEST"); SplitResult r2 = splitResultList.get("testo2222"); @@ -152,10 +152,10 @@ public void testClientIsDestroyed() { List splitList = Arrays.asList("FACUNDO_TEST", "a_new_split_2", "benchmark_jw_1"); TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - String treatment = treatmentManager.getTreatment(splitName, null, true); - SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, true); - Map treatmentList = treatmentManager.getTreatments(splitList, null, true); - Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, true); + String treatment = treatmentManager.getTreatment(splitName, null, null, true); + SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, null, true); + Map treatmentList = treatmentManager.getTreatments(splitList, null, null, true); + Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, null, true); assertControl(splitList, treatment, treatmentList, splitResult, splitResultList); } @@ -166,10 +166,10 @@ public void testNonExistingSplits() { List splitList = Arrays.asList("NON_EXISTING_1", "NON_EXISTING_2", "NON_EXISTING_3"); TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - String treatment = treatmentManager.getTreatment(splitName, null, false); - SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, false); - Map treatmentList = treatmentManager.getTreatments(splitList, null, false); - Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, false); + String treatment = treatmentManager.getTreatment(splitName, null, null, false); + SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, null, false); + Map treatmentList = treatmentManager.getTreatments(splitList, null, null, false); + Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, null, false); assertControl(splitList, treatment, treatmentList, splitResult, splitResultList); } @@ -180,10 +180,10 @@ public void testEmptySplit() { List splitList = new ArrayList<>(); TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - String treatment = treatmentManager.getTreatment(splitName, null, false); - SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, false); - Map treatmentList = treatmentManager.getTreatments(splitList, null, false); - Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, false); + String treatment = treatmentManager.getTreatment(splitName, null, null, false); + SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, null, false); + Map treatmentList = treatmentManager.getTreatments(splitList, null, null, false); + Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, null, false); assertControl(splitList, treatment, treatmentList, splitResult, splitResultList); } @@ -195,10 +195,10 @@ public void testNullKey() { List splitList = Arrays.asList("FACUNDO_TEST", "a_new_split_2", "benchmark_jw_1"); TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - String treatment = treatmentManager.getTreatment(splitName, null, false); - SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, false); - Map treatmentList = treatmentManager.getTreatments(splitList, null, false); - Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, false); + String treatment = treatmentManager.getTreatment(splitName, null, null, false); + SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, null, false); + Map treatmentList = treatmentManager.getTreatments(splitList, null, null, false); + Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, null, false); assertControl(splitList, treatment, treatmentList, splitResult, splitResultList); } @@ -210,10 +210,10 @@ public void testEmptyKey() { List splitList = new ArrayList<>(); TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - String treatment = treatmentManager.getTreatment(splitName, null, false); - SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, false); - Map treatmentList = treatmentManager.getTreatments(splitList, null, false); - Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, false); + String treatment = treatmentManager.getTreatment(splitName, null, null, false); + SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, null, false); + Map treatmentList = treatmentManager.getTreatments(splitList, null, null, false); + Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, null, false); assertControl(splitList, treatment, treatmentList, splitResult, splitResultList); } @@ -225,10 +225,10 @@ public void testLongKey() { List splitList = new ArrayList<>(); TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - String treatment = treatmentManager.getTreatment(splitName, null, false); - SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, false); - Map treatmentList = treatmentManager.getTreatments(splitList, null, false); - Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, false); + String treatment = treatmentManager.getTreatment(splitName, null, null, false); + SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, null, false); + Map treatmentList = treatmentManager.getTreatments(splitList, null, null, false); + Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, null, false); assertControl(splitList, treatment, treatmentList, splitResult, splitResultList); } @@ -240,10 +240,10 @@ public void testNullSplit() { List splitList = null; TreatmentManager treatmentManager = createTreatmentManager(matchingKey, matchingKey); - String treatment = treatmentManager.getTreatment(splitName, null, false); - SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, false); - Map treatmentList = treatmentManager.getTreatments(splitList, null, false); - Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, false); + String treatment = treatmentManager.getTreatment(splitName, null, null, false); + SplitResult splitResult = treatmentManager.getTreatmentWithConfig(splitName, null, null, false); + Map treatmentList = treatmentManager.getTreatments(splitList, null, null, false); + Map splitResultList = treatmentManager.getTreatmentsWithConfig(splitList, null, null, false); Assert.assertNotNull(treatment); Assert.assertEquals(Treatments.CONTROL, treatment); @@ -257,14 +257,14 @@ public void testDefinitionNotFoundLabel() { TreatmentManagerImpl tManager = initializeTreatmentManager(evaluatorMock); - tManager.getTreatment("FACUNDO_TEST", null, false); + tManager.getTreatment("FACUNDO_TEST", null, null, false); verifyNoInteractions(impressionListener); } @Test public void getTreatmentTakesValuesFromAttributesManagerIntoAccount() { - treatmentManager.getTreatment("test_split", new HashMap<>(), false); + treatmentManager.getTreatment("test_split", new HashMap<>(), null, false); verify(attributesManager).getAllAttributes(); } @@ -272,7 +272,7 @@ public void getTreatmentTakesValuesFromAttributesManagerIntoAccount() { @Test public void getTreatmentWithConfigTakesValuesFromAttributesManagerIntoAccount() { - treatmentManager.getTreatmentWithConfig("test_split", new HashMap<>(), false); + treatmentManager.getTreatmentWithConfig("test_split", new HashMap<>(), null, false); verify(attributesManager).getAllAttributes(); } @@ -283,7 +283,7 @@ public void getTreatmentsTakesValuesFromAttributesManagerIntoAccount() { splits.add("test_split_1"); splits.add("test_split_2"); - treatmentManager.getTreatments(splits, new HashMap<>(), false); + treatmentManager.getTreatments(splits, new HashMap<>(), null, false); verify(attributesManager).getAllAttributes(); } @@ -294,7 +294,7 @@ public void getTreatmentsWithConfigTakesValuesFromAttributesManagerIntoAccount() splits.add("test_split_1"); splits.add("test_split_2"); - treatmentManager.getTreatmentsWithConfig(splits, new HashMap<>(), false); + treatmentManager.getTreatmentsWithConfig(splits, new HashMap<>(), null, false); verify(attributesManager).getAllAttributes(); } @@ -312,7 +312,7 @@ public void evaluationWhenNotReadyLogsCorrectMessage() { when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY)).thenReturn(false); when(eventsManager.eventAlreadyTriggered(SplitEvent.SDK_READY_FROM_CACHE)).thenReturn(false); createTreatmentManager("my_key", null, validationMessageLogger, splitValidator, evaluatorMock, eventsManager) - .getTreatment("test_split", null, false); + .getTreatment("test_split", null, null, false); verify(validationMessageLogger).w(eq("the SDK is not ready, results may be incorrect for feature flag test_split. Make sure to wait for SDK readiness before using this method"), any()); } @@ -324,7 +324,7 @@ public void trackValueFromEvaluationResultGetsPassedInToImpression() { .thenReturn(new EvaluationResult("test", "test", true)); TreatmentManagerImpl tManager = initializeTreatmentManager(evaluatorMock); - tManager.getTreatment("test_impressions_disabled", null, false); + tManager.getTreatment("test_impressions_disabled", null, null, false); verify(impressionListener).log(argThat((DecoratedImpression decoratedImpression) -> { return decoratedImpression.isImpressionsDisabled(); @@ -367,7 +367,7 @@ private TreatmentManager createTreatmentManager(String matchingKey, String bucke new KeyValidatorImpl(), splitValidator, mock(ImpressionListener.FederatedImpressionListener.class), config.labelsEnabled(), eventsManager, mock(AttributesManager.class), mock(AttributesMerger.class), - mock(TelemetryStorageProducer.class), mFlagSetsFilter, mSplitsStorage, validationLogger, new FlagSetsValidatorImpl()); + mock(TelemetryStorageProducer.class), mFlagSetsFilter, mSplitsStorage, validationLogger, new FlagSetsValidatorImpl(), new PropertyValidatorImpl()); } private TreatmentManagerImpl initializeTreatmentManager() { @@ -397,7 +397,7 @@ private TreatmentManagerImpl initializeTreatmentManager(Evaluator evaluator) { telemetryStorageProducer, mFlagSetsFilter, mSplitsStorage, - new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl()); + new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl(), new PropertyValidatorImpl()); } private Map splitsMap(List splits) { diff --git a/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java b/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java index bdcbec7db..4588b8cb2 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java @@ -88,7 +88,7 @@ public void tearDown() { @Test public void getTreatmentsByFlagSetDestroyedDoesNotUseEvaluator() { - mTreatmentManager.getTreatmentsByFlagSet("set_1", null, true); + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, null, true); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -99,7 +99,7 @@ public void getTreatmentsByFlagSetWithNoConfiguredSetsQueriesStorageAndUsesEvalu when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); - mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); verify(mEvaluator).getTreatment(eq("matching_key"), eq("bucketing_key"), eq("test_1"), anyMap()); @@ -110,7 +110,7 @@ public void getTreatmentsByFlagSetWithNoConfiguredSetsInvalidSetDoesNotQueryStor when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_split"))); - mTreatmentManager.getTreatmentsByFlagSet("SET!", null, false); + mTreatmentManager.getTreatmentsByFlagSet("SET!", null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -122,7 +122,7 @@ public void getTreatmentsByFlagSetWithConfiguredSetsExistingSetQueriesStorageAnd when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); - mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); verify(mEvaluator).getTreatment(eq("matching_key"), eq("bucketing_key"), eq("test_1"), anyMap()); @@ -135,7 +135,7 @@ public void getTreatmentsByFlagSetWithConfiguredSetsNonExistingSetDoesNotQuerySt when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_split"))); - mTreatmentManager.getTreatmentsByFlagSet("set_2", null, false); + mTreatmentManager.getTreatmentsByFlagSet("set_2", null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -155,7 +155,7 @@ private void initializeTreatmentManager() { mAttributesMerger, mTelemetryStorageProducer, mFlagSetsFilter, - mSplitsStorage, new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl()); + mSplitsStorage, new ValidationMessageLoggerImpl(), new FlagSetsValidatorImpl(), new PropertyValidatorImpl()); } @Test @@ -166,7 +166,7 @@ public void getTreatmentsByFlagSetReturnsCorrectFormat() { when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(mockNames); mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); - Map result = mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + Map result = mTreatmentManager.getTreatmentsByFlagSet("set_1", null, null, false); assertEquals(2, result.size()); assertEquals("result_1", result.get("test_1")); @@ -177,7 +177,7 @@ public void getTreatmentsByFlagSetReturnsCorrectFormat() { public void getTreatmentsByFlagSetRecordsTelemetry() { when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(Collections.singleton("test_1")); - mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, null, false); verify(mTelemetryStorageProducer).recordLatency(eq(Method.TREATMENTS_BY_FLAG_SET), anyLong()); } @@ -185,7 +185,7 @@ public void getTreatmentsByFlagSetRecordsTelemetry() { /// @Test public void getTreatmentsByFlagSetsDestroyedDoesNotUseEvaluator() { - mTreatmentManager.getTreatmentsByFlagSets(Collections.singletonList("set_1"), null, true); + mTreatmentManager.getTreatmentsByFlagSets(Collections.singletonList("set_1"), null, null, true); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -196,7 +196,7 @@ public void getTreatmentsByFlagSetsWithNoConfiguredSetsQueriesStorageAndUsesEval when(mSplitsStorage.getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2")))) .thenReturn(new HashSet<>(Arrays.asList("test_1", "test_2"))); - mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); verify(mSplitsStorage).getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2"))); verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_1"), anyMap()); @@ -208,7 +208,7 @@ public void getTreatmentsByFlagSetsWithNoConfiguredSetsInvalidSetDoesNotQuerySto when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); - mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "SET!"), null, false); + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "SET!"), null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); verify(mEvaluator).getTreatment(any(), any(), eq("test_1"), anyMap()); @@ -221,7 +221,7 @@ public void getTreatmentsByFlagSetsWithConfiguredSetsExistingSetQueriesStorageFo when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); - mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_1"), anyMap()); @@ -232,7 +232,7 @@ public void getTreatmentsByFlagSetsWithConfiguredSetsNonExistingSetDoesNotQueryS mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); initializeTreatmentManager(); - mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_2", "set_3"), null, false); + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_2", "set_3"), null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -245,7 +245,7 @@ public void getTreatmentsByFlagSetsReturnsCorrectFormat() { mockNames.add("test_2"); when(mSplitsStorage.getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2")))).thenReturn(mockNames); - Map result = mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + Map result = mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); assertEquals(2, result.size()); assertEquals("result_1", result.get("test_1")); @@ -254,14 +254,14 @@ public void getTreatmentsByFlagSetsReturnsCorrectFormat() { @Test public void getTreatmentsByFlagSetsWithDuplicatedSetDeduplicates() { - mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_1"), null, false); + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_1"), null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); } @Test public void getTreatmentsByFlagSetsWithNullSetListReturnsEmpty() { - Map result = mTreatmentManager.getTreatmentsByFlagSets(null, null, false); + Map result = mTreatmentManager.getTreatmentsByFlagSets(null, null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -272,7 +272,7 @@ public void getTreatmentsByFlagSetsWithNullSetListReturnsEmpty() { public void getTreatmentsByFlagSetsRecordsTelemetry() { when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(Collections.singleton("test_1")); - mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); verify(mTelemetryStorageProducer).recordLatency(eq(Method.TREATMENTS_BY_FLAG_SETS), anyLong()); } @@ -280,7 +280,7 @@ public void getTreatmentsByFlagSetsRecordsTelemetry() { /// @Test public void getTreatmentsWithConfigByFlagSetDestroyedDoesNotUseEvaluator() { - mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, true); + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, null, true); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -291,7 +291,7 @@ public void getTreatmentsWithConfigByFlagSetWithNoConfiguredSetsQueriesStorageAn when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(Collections.singleton("test_1")); - mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); verify(mEvaluator).getTreatment(eq("matching_key"), eq("bucketing_key"), eq("test_1"), anyMap()); @@ -302,7 +302,7 @@ public void getTreatmentsWithConfigByFlagSetWithNoConfiguredSetsInvalidSetDoesNo when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_split"))); - mTreatmentManager.getTreatmentsWithConfigByFlagSet("SET!", null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSet("SET!", null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -314,7 +314,7 @@ public void getTreatmentsWithConfigByFlagSetWithConfiguredSetsExistingSetQueries when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); - mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); verify(mEvaluator).getTreatment(eq("matching_key"), eq("bucketing_key"), eq("test_1"), anyMap()); @@ -327,7 +327,7 @@ public void getTreatmentsWithConfigByFlagSetWithConfiguredSetsNonExistingSetDoes when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_split"))); - mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_2", null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_2", null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -341,7 +341,7 @@ public void getTreatmentsWithConfigByFlagSetReturnsCorrectFormat() { when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(mockNames); mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); - Map result = mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + Map result = mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, null, false); assertEquals(2, result.size()); assertEquals("result_1", result.get("test_1").treatment()); @@ -352,7 +352,7 @@ public void getTreatmentsWithConfigByFlagSetReturnsCorrectFormat() { public void getTreatmentsWithConfigByFlagSetRecordsTelemetry() { when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(Collections.singleton("test_1")); - mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, null, false); verify(mTelemetryStorageProducer).recordLatency(eq(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET), anyLong()); } @@ -360,7 +360,7 @@ public void getTreatmentsWithConfigByFlagSetRecordsTelemetry() { /// @Test public void getTreatmentsWithConfigByFlagSetsDestroyedDoesNotUseEvaluator() { - mTreatmentManager.getTreatmentsWithConfigByFlagSets(Collections.singletonList("set_1"), null, true); + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Collections.singletonList("set_1"), null, null, true); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -371,7 +371,7 @@ public void getTreatmentsWithConfigByFlagSetsWithNoConfiguredSetsQueriesStorageA when(mSplitsStorage.getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2")))) .thenReturn(new HashSet<>(Arrays.asList("test_1", "test_2"))); - mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); verify(mSplitsStorage).getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2"))); verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_1"), anyMap()); @@ -383,7 +383,7 @@ public void getTreatmentsWithConfigByFlagSetsWithNoConfiguredSetsInvalidSetDoesN when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); - mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "SET!"), null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "SET!"), null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); verify(mEvaluator).getTreatment(any(), any(), eq("test_1"), anyMap()); @@ -397,7 +397,7 @@ public void getTreatmentsWithConfigByFlagSetsWithConfiguredSetsExistingSetQuerie when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))) .thenReturn(new HashSet<>(Collections.singletonList("test_1"))); - mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); verify(mEvaluator).getTreatment(anyString(), anyString(), eq("test_1"), anyMap()); @@ -408,7 +408,7 @@ public void getTreatmentsWithConfigByFlagSetsWithConfiguredSetsNonExistingSetDoe mFlagSetsFilter = new FlagSetsFilterImpl(Collections.singleton("set_1")); initializeTreatmentManager(); - mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_2", "set_3"), null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_2", "set_3"), null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -421,7 +421,7 @@ public void getTreatmentsWithConfigByFlagSetsReturnsCorrectFormat() { mockNames.add("test_2"); when(mSplitsStorage.getNamesByFlagSets(new HashSet<>(Arrays.asList("set_1", "set_2")))).thenReturn(mockNames); - Map result = mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + Map result = mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); assertEquals(2, result.size()); assertEquals("result_1", result.get("test_1").treatment()); @@ -430,14 +430,14 @@ public void getTreatmentsWithConfigByFlagSetsReturnsCorrectFormat() { @Test public void getTreatmentsWithConfigByFlagSetsWithDuplicatedSetDeduplicates() { - mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_1"), null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_1"), null, null, false); verify(mSplitsStorage).getNamesByFlagSets(Collections.singleton("set_1")); } @Test public void getTreatmentsWithConfigByFlagSetsWithNullSetListReturnsEmpty() { - Map result = mTreatmentManager.getTreatmentsWithConfigByFlagSets(null, null, false); + Map result = mTreatmentManager.getTreatmentsWithConfigByFlagSets(null, null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -448,7 +448,7 @@ public void getTreatmentsWithConfigByFlagSetsWithNullSetListReturnsEmpty() { public void getTreatmentsWithConfigByFlagSetsRecordsTelemetry() { when(mSplitsStorage.getNamesByFlagSets(Collections.singleton("set_1"))).thenReturn(Collections.singleton("test_1")); - mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); verify(mTelemetryStorageProducer).recordLatency(eq(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS), anyLong()); } @@ -457,7 +457,7 @@ public void getTreatmentsWithConfigByFlagSetsRecordsTelemetry() { public void getTreatmentsByFlagSetExceptionIsRecordedInTelemetry() { when(mSplitsStorage.getNamesByFlagSets(any())).thenThrow(new RuntimeException("test")); - mTreatmentManager.getTreatmentsByFlagSet("set_1", null, false); + mTreatmentManager.getTreatmentsByFlagSet("set_1", null, null, false); verify(mTelemetryStorageProducer).recordException(eq(Method.TREATMENTS_BY_FLAG_SET)); } @@ -466,7 +466,7 @@ public void getTreatmentsByFlagSetExceptionIsRecordedInTelemetry() { public void getTreatmentsByFlagSetsExceptionIsRecordedInTelemetry() { when(mSplitsStorage.getNamesByFlagSets(any())).thenThrow(new RuntimeException("test")); - mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + mTreatmentManager.getTreatmentsByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); verify(mTelemetryStorageProducer).recordException(eq(Method.TREATMENTS_BY_FLAG_SETS)); } @@ -475,7 +475,7 @@ public void getTreatmentsByFlagSetsExceptionIsRecordedInTelemetry() { public void getTreatmentsWithConfigByFlagSetExceptionIsRecordedInTelemetry() { when(mSplitsStorage.getNamesByFlagSets(any())).thenThrow(new RuntimeException("test")); - mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSet("set_1", null, null, false); verify(mTelemetryStorageProducer).recordException(eq(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SET)); } @@ -484,14 +484,14 @@ public void getTreatmentsWithConfigByFlagSetExceptionIsRecordedInTelemetry() { public void getTreatmentsWithConfigByFlagSetsExceptionIsRecordedInTelemetry() { when(mSplitsStorage.getNamesByFlagSets(any())).thenThrow(new RuntimeException("test")); - mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSets(Arrays.asList("set_1", "set_2"), null, null, false); verify(mTelemetryStorageProducer).recordException(eq(Method.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS)); } @Test public void getTreatmentsByFlagSetWithNullFlagSet() { - mTreatmentManager.getTreatmentsByFlagSet(null, null, false); + mTreatmentManager.getTreatmentsByFlagSet(null, null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -499,7 +499,7 @@ public void getTreatmentsByFlagSetWithNullFlagSet() { @Test public void getTreatmentsByFlagSetsWithNullFlagSets() { - mTreatmentManager.getTreatmentsByFlagSets(null, null, false); + mTreatmentManager.getTreatmentsByFlagSets(null, null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -507,7 +507,7 @@ public void getTreatmentsByFlagSetsWithNullFlagSets() { @Test public void getTreatmentsWithConfigByFlagSetWithNullFlagSet() { - mTreatmentManager.getTreatmentsWithConfigByFlagSet(null, null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSet(null, null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); @@ -515,7 +515,7 @@ public void getTreatmentsWithConfigByFlagSetWithNullFlagSet() { @Test public void getTreatmentsWithConfigByFlagSetsWithNullFlagSets() { - mTreatmentManager.getTreatmentsWithConfigByFlagSets(null, null, false); + mTreatmentManager.getTreatmentsWithConfigByFlagSets(null, null, null, false); verify(mSplitsStorage, times(0)).getNamesByFlagSets(any()); verify(mEvaluator, times(0)).getTreatment(any(), any(), any(), anyMap()); diff --git a/src/test/java/io/split/android/client/events/EventPropertiesProcessorTest.java b/src/test/java/io/split/android/client/events/PropertyValidatorTest.java similarity index 75% rename from src/test/java/io/split/android/client/events/EventPropertiesProcessorTest.java rename to src/test/java/io/split/android/client/events/PropertyValidatorTest.java index 46606774a..0841e2d0d 100644 --- a/src/test/java/io/split/android/client/events/EventPropertiesProcessorTest.java +++ b/src/test/java/io/split/android/client/events/PropertyValidatorTest.java @@ -7,18 +7,16 @@ import java.util.HashMap; import java.util.Map; -import io.split.android.client.EventPropertiesProcessor; -import io.split.android.client.EventPropertiesProcessorImpl; -import io.split.android.client.ProcessedEventProperties; +import io.split.android.client.PropertyValidatorImpl; import io.split.android.client.dtos.Split; import io.split.android.client.utils.Utils; +import io.split.android.client.validators.PropertyValidator; import io.split.android.client.validators.ValidationConfig; -public class EventPropertiesProcessorTest { +public class PropertyValidatorTest { - private EventPropertiesProcessor processor = new EventPropertiesProcessorImpl(); + private final PropertyValidator processor = new PropertyValidatorImpl(); private final static long MAX_BYTES = ValidationConfig.getInstance().getMaximumEventPropertyBytes(); - private final static int MAX_COUNT = 300; @Before public void setup() { @@ -33,7 +31,7 @@ public void sizeInBytesValidation() { properties.put("key" + count, Utils.repeat("a", 1021)); // 1025 bytes count++; } - ProcessedEventProperties result = processor.process(properties); + PropertyValidator.Result result = validate(properties); Assert.assertFalse(result.isValid()); } @@ -47,7 +45,7 @@ public void invalidPropertyType() { for (int i = 0; i < 10; i++) { properties.put("key" + i, new Split()); } - ProcessedEventProperties result = processor.process(properties); + PropertyValidator.Result result = validate(properties); Assert.assertTrue(result.isValid()); Assert.assertEquals(10, result.getProperties().size()); @@ -62,7 +60,7 @@ public void nullValues() { for (int i = 10; i < 20; i++) { properties.put("key" + i + 10, null); } - ProcessedEventProperties result = processor.process(properties); + PropertyValidator.Result result = validate(properties); Assert.assertTrue(result.isValid()); Assert.assertEquals(20, result.getProperties().size()); @@ -74,9 +72,13 @@ public void totalBytes() { for (int i = 0; i < 10; i++) { properties.put("k" + i, "10 bytes"); } - ProcessedEventProperties result = processor.process(properties); + PropertyValidator.Result result = validate(properties); Assert.assertTrue(result.isValid()); Assert.assertEquals(100, result.getSizeInBytes()); } + + private PropertyValidator.Result validate(Map properties) { + return processor.validate(properties, "test"); + } } diff --git a/src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java b/src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java index 713f38c13..a3d7c8d0c 100644 --- a/src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java +++ b/src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java @@ -50,6 +50,6 @@ public void unsuccessfulExecutionDoesNotCrash() { } private static DecoratedImpression createImpression() { - return new DecoratedImpression(new Impression("key", "feature", "treatment", "on", 1402040204L, "label", 123123L, new HashMap<>()), true); + return new DecoratedImpression(new Impression("key", "feature", "treatment", "on", 1402040204L, "label", 123123L, new HashMap<>(), null), true); } } diff --git a/src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java b/src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java index 4131e445f..6a57fab16 100644 --- a/src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java +++ b/src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java @@ -44,6 +44,6 @@ public void errorWhileSubmittingTaskIsHandled() { } private static DecoratedImpression createImpression() { - return new DecoratedImpression(new Impression("key", "feature", "treatment", "on", 1402040204L, "label", 123123L, new HashMap<>()), true); + return new DecoratedImpression(new Impression("key", "feature", "treatment", "on", 1402040204L, "label", 123123L, new HashMap<>(), null), true); } } diff --git a/src/test/java/io/split/android/client/service/ImpressionHasherTest.java b/src/test/java/io/split/android/client/service/ImpressionHasherTest.java index 318a35117..291e82c1b 100644 --- a/src/test/java/io/split/android/client/service/ImpressionHasherTest.java +++ b/src/test/java/io/split/android/client/service/ImpressionHasherTest.java @@ -28,6 +28,7 @@ public void differentFeature() { System.currentTimeMillis(), "someLabel", 123L, + null, null); Long hash2 = ImpressionHasher.process(imp2); @@ -46,6 +47,7 @@ public void differentKey() { System.currentTimeMillis(), "someLabel", 123L, + null, null); Long hash2 = ImpressionHasher.process(imp2); @@ -65,6 +67,7 @@ public void differentChangeNumber() { System.currentTimeMillis(), "someLabel", 456L, + null, null); Long hash2 = ImpressionHasher.process(imp2); @@ -81,6 +84,7 @@ public void differentLabel() { System.currentTimeMillis(), "someOtherLabel", 123L, + null, null); Long hash2 = ImpressionHasher.process(imp2); @@ -97,6 +101,7 @@ public void differentTreatment() { System.currentTimeMillis(), "someLabel", 123L, + null, null); Long hash2 = ImpressionHasher.process(imp2); @@ -113,6 +118,7 @@ public void noCrashWhenSplitNull() { System.currentTimeMillis(), "someLabel", 123L, + null, null); Long hash = ImpressionHasher.process(imp1); @@ -130,6 +136,7 @@ public void noCrashWhenSplitAndKeyNull() { System.currentTimeMillis(), "someLabel", 123L, + null, null); Long hash = ImpressionHasher.process(imp1); @@ -147,6 +154,7 @@ public void noCrashWhenKeySplitChangeNumberNull() { System.currentTimeMillis(), "someLabel", null, + null, null); Long hash = ImpressionHasher.process(imp1); @@ -164,6 +172,7 @@ public void noCrashWhenKeySplitChangeNumberAppliedRuleNull() { System.currentTimeMillis(), null, null, + null, null); Long hash = ImpressionHasher.process(imp1); @@ -181,6 +190,7 @@ public void noCrashWhenOnlyAppliedRuleNotNull() { System.currentTimeMillis(), "someLabel", null, + null, null); Assert.assertNotNull(imp1); @@ -202,6 +212,7 @@ private Impression baseImpression() { System.currentTimeMillis(), "someLabel", 123L, + null, null); } } \ No newline at end of file diff --git a/src/test/java/io/split/android/client/service/SynchronizerTest.java b/src/test/java/io/split/android/client/service/SynchronizerTest.java index 79347551f..98a18c766 100644 --- a/src/test/java/io/split/android/client/service/SynchronizerTest.java +++ b/src/test/java/io/split/android/client/service/SynchronizerTest.java @@ -798,12 +798,12 @@ public void tearDown() { private DecoratedImpression createImpression() { return new DecoratedImpression(new Impression("key", "bkey", "split", "on", - 100L, "default rule", 999L, null), true); + 100L, "default rule", 999L, null, null), true); } private DecoratedImpression createUniqueImpression() { return new DecoratedImpression(new Impression("key", "bkey", UUID.randomUUID().toString(), "on", - 100L, "default rule", 999L, null), true); + 100L, "default rule", 999L, null, null), true); } private KeyImpression keyImpression(Impression impression) { diff --git a/src/test/java/io/split/android/client/service/events/EventsTrackerTest.java b/src/test/java/io/split/android/client/service/events/EventsTrackerTest.java index d1711c7a0..bf0b601e6 100644 --- a/src/test/java/io/split/android/client/service/events/EventsTrackerTest.java +++ b/src/test/java/io/split/android/client/service/events/EventsTrackerTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -17,9 +16,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.HashMap; - -import io.split.android.client.EventPropertiesProcessor; import io.split.android.client.EventsTracker; import io.split.android.client.EventsTrackerImpl; import io.split.android.client.ProcessedEventProperties; @@ -28,6 +24,7 @@ import io.split.android.client.telemetry.model.Method; import io.split.android.client.telemetry.storage.TelemetryStorageProducer; import io.split.android.client.validators.EventValidator; +import io.split.android.client.validators.PropertyValidator; import io.split.android.client.validators.ValidationMessageLogger; public class EventsTrackerTest { @@ -40,7 +37,7 @@ public class EventsTrackerTest { @Mock private TelemetryStorageProducer mTelemetryStorageProducer; @Mock - private EventPropertiesProcessor mEventPropertiesProcessor; + private PropertyValidator mPropertyValidator; @Mock private SyncManager mSyncManager; @@ -51,10 +48,10 @@ public void setup() { MockitoAnnotations.openMocks(this); when(mEventValidator.validate(any(), anyBoolean())).thenReturn(null); when(mEventsManager.eventAlreadyTriggered(any())).thenReturn(true); - when(mEventPropertiesProcessor.process(any())).thenReturn(new ProcessedEventProperties(true, null, 0)); + when(mPropertyValidator.validate(any(), any())).thenReturn(PropertyValidator.Result.valid(null, 0)); mEventsTracker = new EventsTrackerImpl(mEventValidator, mValidationLogger, mTelemetryStorageProducer, - mEventPropertiesProcessor, mSyncManager); + mPropertyValidator, mSyncManager); } @Test @@ -92,7 +89,7 @@ public void trackRecordsLatencyInEvaluationProducer() { @Test public void trackRecordsExceptionInCaseThereIsOne() { - when(mEventPropertiesProcessor.process(any())).thenAnswer(invocation -> { + when(mPropertyValidator.validate(any(), any())).thenAnswer(invocation -> { throw new Exception("test exception"); }); diff --git a/src/test/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializerTest.java b/src/test/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializerTest.java new file mode 100644 index 000000000..50ff549c1 --- /dev/null +++ b/src/test/java/io/split/android/client/service/impressions/ImpressionsRequestBodySerializerTest.java @@ -0,0 +1,125 @@ +package io.split.android.client.service.impressions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.split.android.client.dtos.KeyImpression; + +public class ImpressionsRequestBodySerializerTest { + + private ImpressionsRequestBodySerializer mSerializer; + + @Before + public void setUp() { + mSerializer = new ImpressionsRequestBodySerializer(); + } + + @Test + public void impressionWithoutPropertiesDoesNotIncludePropertiesField() { + KeyImpression impression = createBasicImpression("user123", "test_feature", "on"); + impression.changeNumber = 1234567L; + impression.label = "default rule"; + impression.bucketingKey = "bucketKey"; + + String serialized = serialize(impression); + + String expected = + "[{" + + "\"f\":\"test_feature\"," + + "\"i\":[{" + + "\"k\":\"user123\"," + + "\"b\":\"bucketKey\"," + + "\"t\":\"on\"," + + "\"r\":\"default rule\"," + + "\"m\":1650000000," + + "\"c\":1234567," + + "\"pt\":null" + + "}]" + + "}]"; + + assertEquals(expected, serialized); + } + + @Test + public void serializeImpressionWithProperties() { + KeyImpression impression = createBasicImpression("user123", "test_feature", "on"); + impression.properties = "{\"string_prop\":\"value\",\"number_prop\":42,\"bool_prop\":true}"; + + String serialized = serialize(impression); + + String expected = + "[{" + + "\"f\":\"test_feature\"," + + "\"i\":[{" + + "\"k\":\"user123\"," + + "\"b\":null," + + "\"t\":\"on\"," + + "\"r\":null," + + "\"m\":1650000000," + + "\"c\":null," + + "\"pt\":null," + + "\"properties\":\"{\\\"string_prop\\\":\\\"value\\\",\\\"number_prop\\\":42,\\\"bool_prop\\\":true}\"" + + "}]" + + "}]"; + + assertEquals(expected, serialized); + } + + @Test + public void serializeMultipleImpressionsGroupedByFeature() { + KeyImpression impression1 = createBasicImpression("user1", "feature1", "on"); + KeyImpression impression2 = createBasicImpression("user2", "feature1", "off"); + KeyImpression impression3 = createBasicImpression("user1", "feature2", "control"); + + impression1.time = 1000L; + impression2.time = 2000L; + impression3.time = 3000L; + + String serialized = serialize(impression1, impression2, impression3); + + assertTrue(serialized.contains("\"f\":\"feature1\"")); + assertTrue(serialized.contains("\"f\":\"feature2\"")); + assertTrue(serialized.contains("\"k\":\"user1\"")); + assertTrue(serialized.contains("\"k\":\"user2\"")); + assertTrue(serialized.contains("\"t\":\"on\"")); + assertTrue(serialized.contains("\"t\":\"off\"")); + assertTrue(serialized.contains("\"t\":\"control\"")); + assertTrue(serialized.contains("\"m\":1000")); + assertTrue(serialized.contains("\"m\":2000")); + assertTrue(serialized.contains("\"m\":3000")); + } + + @Test + public void serializeEmptyImpressionsList() { + String serialized = serialize(); + assertEquals("[]", serialized); + } + + /** + * Helper method to create a basic KeyImpression with common fields + */ + private KeyImpression createBasicImpression(String keyName, String feature, String treatment) { + KeyImpression impression = new KeyImpression(); + impression.keyName = keyName; + impression.feature = feature; + impression.treatment = treatment; + impression.time = 1650000000L; + return impression; + } + + /** + * Helper method to serialize impressions + */ + private String serialize(KeyImpression... impressions) { + List impressionsList = new ArrayList<>(Arrays.asList(impressions)); + + return mSerializer.serialize(impressionsList); + } +} diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/NoneStrategyTest.kt b/src/test/java/io/split/android/client/service/impressions/strategy/NoneStrategyTest.kt index daaeb89aa..8acf6de77 100644 --- a/src/test/java/io/split/android/client/service/impressions/strategy/NoneStrategyTest.kt +++ b/src/test/java/io/split/android/client/service/impressions/strategy/NoneStrategyTest.kt @@ -93,5 +93,6 @@ fun createUniqueImpression( time, "default rule", 999L, + null, null )