diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 908cacdfd..c2ee017b0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,6 +6,10 @@ on: - 'development' - '*_baseline' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-app: name: Build App @@ -15,16 +19,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..c4f08418d 100644 --- a/.github/workflows/instrumented.yml +++ b/.github/workflows/instrumented.yml @@ -4,12 +4,14 @@ on: pull_request: branches: - 'development' - - 'master' - - '*_baseline' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -30,11 +32,11 @@ jobs: - 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 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/CHANGES.txt b/CHANGES.txt index 90d2171e6..8bda92fa7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +5.2.0 (Apr 25, 2025) +- Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. +- Added improvements for SDK ready status. + 5.1.1 (Feb 11, 2025) - Fixed issue when calling destroy() before the SDK has initialized. diff --git a/LICENSE.txt b/LICENSE.txt index c022e9200..df08de3fb 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright © 2024 Split Software, Inc. +Copyright © 2025 Split Software, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/build.gradle b/build.gradle index 7eb29e1da..2f6e4d3fb 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '5.1.1' + splitVersion = '5.2.0' } android { @@ -25,6 +25,10 @@ android { targetCompatibility = '1.8' sourceCompatibility = '1.8' + buildFeatures { + buildConfig true + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 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..2f09a1f2a 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); @@ -177,6 +180,7 @@ private void caller(ImpressionsObserver o, int count, ConcurrentLinkedQueue !db.endsWith("-journal")).toArray(String[]::new); + return Arrays.stream(context.databaseList()) + .filter(db -> !db.endsWith("-journal") && !db.endsWith("-wal") && !db.endsWith("-shm")) + .toArray(String[]::new); } } diff --git a/src/androidTest/java/tests/database/ExclusiveTransactionTest.kt b/src/androidTest/java/tests/database/ExclusiveTransactionTest.kt deleted file mode 100644 index 0fdfd0120..000000000 --- a/src/androidTest/java/tests/database/ExclusiveTransactionTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package tests.database - -import androidx.test.platform.app.InstrumentationRegistry -import io.split.android.client.storage.db.SplitEntity -import io.split.android.client.storage.db.SplitRoomDatabase -import io.split.android.client.storage.db.attributes.AttributesEntity -import org.junit.Before -import org.junit.Test -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.test.fail - -class ExclusiveTransactionTest { - - private lateinit var db: SplitRoomDatabase - - @Before - fun setUp() { - db = SplitRoomDatabase.getDatabase( - InstrumentationRegistry.getInstrumentation().context, - "test_db" - ) - } - - @Test - fun transactionPreventsReads() { - val writeFinished = AtomicBoolean(false) - val readFinished = AtomicBoolean(false) - - // Insert multiple values in multiple DAOs inside a transaction - val updateThread = Thread() { - db.runInTransaction { - for (i in 0..400) { - db.splitDao().insert( - SplitEntity().apply { - name = "split${i}" - body = "body${i}" - updatedAt = System.currentTimeMillis() - } - ) - - db.attributesDao().update( - AttributesEntity().apply { - userKey = "key${i}" - attributes = "value${i}" - } - ) - } - writeFinished.set(true) - } - } - - // Read values from multiple DAOs - val readThread = Thread() { - db.splitDao().all - db.attributesDao().all - readFinished.set(true) - } - - // Monitor the operations - val monitorThread = Thread() { - while (true) { - if (readFinished.get() && !writeFinished.get()) { - fail("Values were read before update was done") - } else if (writeFinished.get() && readFinished.get()) { - break - } - } - } - - monitorThread.start() - readThread.start() - updateThread.start() - - monitorThread.join(2000) - readThread.join(2000) - updateThread.join(2000) - } -} diff --git a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java index be821fde0..81cbc29de 100644 --- a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java +++ b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java @@ -181,6 +181,8 @@ private void test(long timestampDaysAgo, RolloutCacheConfiguration.Builder confi private void verify(SplitFactory factory, CountDownLatch readyLatch, List initialFlags, List initialSegments, List initialLargeSegments, long initialChangeNumber) throws InterruptedException { // Track final values + Thread.sleep(10000); + List finalFlags = mRoomDb.splitDao().getAll(); List finalSegments = mRoomDb.mySegmentDao().getAll(); List finalLargeSegments = mRoomDb.myLargeSegmentDao().getAll(); diff --git a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java index 30b748d0d..3b70f4a82 100644 --- a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java +++ b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java @@ -153,7 +153,7 @@ public void testCleanUp() throws IOException, InterruptedException { mClient.on(SplitEvent.SDK_READY, readyTask); latch.await(40, TimeUnit.SECONDS); - Thread.sleep(1000); + Thread.sleep(8000); // Load all records again after cleanup List remainingKeys = mUniqueKeysDao.getBy(0, StorageRecordStatus.ACTIVE, 10); diff --git a/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java b/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java index 2066bb2cd..7af0d2802 100644 --- a/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java +++ b/src/androidTest/java/tests/storage/PersistentSplitsStorageTest.java @@ -12,6 +12,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import helper.DatabaseHelper; import io.split.android.client.dtos.Split; @@ -32,6 +33,8 @@ public class PersistentSplitsStorageTest { SplitRoomDatabase mRoomDb; Context mContext; PersistentSplitsStorage mPersistentSplitsStorage; + private final Map> mFlagSets = new HashMap<>(); + private final Map mTrafficTypes = new HashMap<>(); @Before public void setUp() { @@ -94,7 +97,7 @@ public void addSplits() { split.status = Status.ACTIVE; splits.add(split); } - mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, new ArrayList<>(), 1L, 0L)); + mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, new ArrayList<>(), 1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -114,7 +117,7 @@ public void addSplits() { @Test public void updateEmptySplit() { List splits = new ArrayList<>(); - mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, splits, 1L, 0L)); + mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, splits, 1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -134,7 +137,7 @@ public void updateEmptySplit() { @Test public void addNullSplitList() { List splits = new ArrayList<>(); - boolean res = mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits,1L, 0L)); + boolean res = mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits,1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -155,7 +158,7 @@ public void addNullSplitList() { @Test public void deleteNullSplitList() { List splits = new ArrayList<>(); - boolean res = mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, null,1L, 0L)); + boolean res = mPersistentSplitsStorage.update(new ProcessedSplitChange(splits, null,1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -183,7 +186,7 @@ public void deleteSplits() { split.status = Status.ARCHIVED; splits.add(split); } - mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits, 1L, 0L)); + mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits, 1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); Map splitMap = listToMap(snapshot.getSplits()); @@ -220,7 +223,7 @@ public void deleteAllSplits() { split.status = Status.ARCHIVED; splits.add(split); } - mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits, 1L, 0L)); + mPersistentSplitsStorage.update(new ProcessedSplitChange(null, splits, 1L, 0L), mTrafficTypes, mFlagSets); SplitsSnapshot snapshot = mPersistentSplitsStorage.getSnapshot(); List loadedSlits = snapshot.getSplits(); diff --git a/src/androidTest/java/tests/storage/SplitsStorageTest.java b/src/androidTest/java/tests/storage/SplitsStorageTest.java index d5370f911..d68127cdb 100644 --- a/src/androidTest/java/tests/storage/SplitsStorageTest.java +++ b/src/androidTest/java/tests/storage/SplitsStorageTest.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -35,6 +36,7 @@ import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.storage.splits.SplitsStorageImpl; import io.split.android.client.storage.splits.SqLitePersistentSplitsStorage; +import io.split.android.client.utils.Json; public class SplitsStorageTest { @@ -334,6 +336,11 @@ public void trafficTypesAreLoadedInMemoryWhenLoadingLocalSplits() { mRoomDb.clearAllTables(); mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type"), newSplitEntity("split_test_2", "test_type_2"))); + Map trafficTypes = new HashMap<>(); + trafficTypes.put("test_type", 1); + trafficTypes.put("test_type_2", 1); + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, Json.toJson(trafficTypes))); + mSplitsStorage.loadLocal(); assertTrue(mSplitsStorage.isValidTrafficType("test_type")); @@ -346,6 +353,11 @@ public void loadedFromStorageTrafficTypesAreCorrectlyUpdated() { mRoomDb.clearAllTables(); mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type"), newSplitEntity("split_test_2", "test_type_2"))); + Map trafficTypes = new HashMap<>(); + trafficTypes.put("test_type", 1); + trafficTypes.put("test_type_2", 1); + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, Json.toJson(trafficTypes))); + mSplitsStorage.loadLocal(); Split updatedSplit = newSplit("split_test", Status.ACTIVE, "new_type"); @@ -365,6 +377,12 @@ public void flagSetsAreUpdatedWhenCallingLoadLocal() { newSplitEntity("split_test_3", "test_type_2", Collections.singleton("set_2")), newSplitEntity("split_test_4", "test_type_2", Collections.singleton("set_1")))); + Map> flagSets = new HashMap<>(); + flagSets.put("set_1", new HashSet<>(Arrays.asList("split_test", "split_test_4"))); + flagSets.put("set_2", new HashSet<>(Arrays.asList("split_test_2", "split_test_3"))); + + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, Json.toJson(flagSets))); + mSplitsStorage.loadLocal(); Assert.assertEquals(new HashSet<>(Arrays.asList("split_test", "split_test_4")), mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1"))); @@ -378,6 +396,13 @@ public void flagSetsAreRemovedWhenUpdating() { newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")), newSplitEntity("split_test_3", "test_type_2", Collections.singleton("set_2")))); + + Map> flagSets = new HashMap<>(); + flagSets.put("set_1", new HashSet<>(Arrays.asList("split_test"))); + flagSets.put("set_2", new HashSet<>(Arrays.asList("split_test_2", "split_test_3"))); + + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, Json.toJson(flagSets))); + mSplitsStorage.loadLocal(); Set initialSet1 = mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1")); @@ -396,6 +421,13 @@ public void flagSetsAreRemovedWhenUpdating() { public void updateWithoutChecksRemovesFromFlagSet() { mRoomDb.clearAllTables(); mRoomDb.splitDao().insert(Arrays.asList(newSplitEntity("split_test", "test_type", Collections.singleton("set_1")), newSplitEntity("split_test_2", "test_type_2", Collections.singleton("set_2")))); + + Map> flagSets = new HashMap<>(); + flagSets.put("set_1", new HashSet<>(Arrays.asList("split_test"))); + flagSets.put("set_2", new HashSet<>(Arrays.asList("split_test_2"))); + + mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, Json.toJson(flagSets))); + mSplitsStorage.loadLocal(); Set initialSet1 = mSplitsStorage.getNamesByFlagSets(Collections.singletonList("set_1")); diff --git a/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java b/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java index 2275622fb..19d7017ba 100644 --- a/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java +++ b/src/main/java/io/split/android/client/AlwaysReturnControlSplitClient.java @@ -37,6 +37,11 @@ public Map 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/SplitClientFactoryImpl.java b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java index 544aab48b..2e7109b08 100644 --- a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java @@ -45,7 +45,7 @@ public class SplitClientFactoryImpl implements SplitClientFactory { private final TreatmentManagerFactory mTreatmentManagerFactory; private final ImpressionListener.FederatedImpressionListener mCustomerImpressionListener; private final SplitValidatorImpl mSplitValidator; - private final EventsTracker mEventsTracker; + private final SplitFactoryImpl.EventsTrackerProvider mEventsTrackerProvider; public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, @NonNull SplitClientContainer clientContainer, @@ -56,7 +56,7 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, @NonNull SplitTaskExecutor splitTaskExecutor, @NonNull ValidationMessageLogger validationLogger, @NonNull KeyValidator keyValidator, - @NonNull EventsTracker eventsTracker, + @NonNull SplitFactoryImpl.EventsTrackerProvider eventsTrackerProvider, @NonNull ImpressionListener.FederatedImpressionListener customerImpressionListener, @Nullable FlagSetsFilter flagSetsFilter) { mSplitFactory = checkNotNull(splitFactory); @@ -67,7 +67,7 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, mStorageContainer = checkNotNull(storageContainer); mTelemetrySynchronizer = checkNotNull(telemetrySynchronizer); mCustomerImpressionListener = checkNotNull(customerImpressionListener); - mEventsTracker = checkNotNull(eventsTracker); + mEventsTrackerProvider = checkNotNull(eventsTrackerProvider); mAttributesManagerFactory = getAttributesManagerFactory(config.persistentAttributesEnabled(), validationLogger, @@ -106,7 +106,7 @@ public SplitClient getClient(@NonNull Key key, mCustomerImpressionListener, mConfig, eventsManager, - mEventsTracker, + mEventsTrackerProvider.getEventsTracker(), mAttributesManagerFactory.getManager(key.matchingKey(), attributesStorage), mSplitValidator, mTreatmentManagerFactory.getTreatmentManager(key, 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/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index be33b61c3..770cc3249 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -98,7 +98,6 @@ class SplitFactoryHelper { private static final int DB_MAGIC_CHARS_COUNT = 4; String getDatabaseName(SplitClientConfig config, String apiToken, Context context) { - String dbName = buildDatabaseName(config, apiToken); File dbPath = context.getDatabasePath(dbName); if (dbPath.exists()) { @@ -162,7 +161,8 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, SplitCipher splitCipher, TelemetryStorage telemetryStorage, long observerCacheExpirationPeriod, - ScheduledThreadPoolExecutor impressionsObserverExecutor) { + ScheduledThreadPoolExecutor impressionsObserverExecutor, + SplitsStorage splitsStorage) { boolean isPersistenceEnabled = userConsentStatus == UserConsent.GRANTED; PersistentEventsStorage persistentEventsStorage = @@ -170,7 +170,7 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, PersistentImpressionsStorage persistentImpressionsStorage = StorageFactory.getPersistentImpressionsStorage(splitRoomDatabase, splitCipher); return new SplitStorageContainer( - StorageFactory.getSplitsStorage(splitRoomDatabase, splitCipher), + splitsStorage, StorageFactory.getMySegmentsStorage(splitRoomDatabase, splitCipher), StorageFactory.getMyLargeSegmentsStorage(splitRoomDatabase, splitCipher), StorageFactory.getPersistentSplitsStorage(splitRoomDatabase, splitCipher), @@ -504,7 +504,6 @@ static class Initializer implements Runnable { this(new RolloutCacheManagerImpl(config, storageContainer, - splitTaskFactory.createCleanUpDatabaseTask(System.currentTimeMillis() / 1000), splitTaskFactory.createEncryptionMigrationTask(apiToken, splitDatabase, config.encryptionEnabled(), splitCipher)), new Listener(eventsManagerCoordinator, splitTaskExecutor, splitSingleThreadTaskExecutor, syncManager, lifecycleManager, initLock), initLock); @@ -550,13 +549,13 @@ static class Listener implements SplitTaskExecutionListener { @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { try { - mEventsManagerCoordinator.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); - mSplitTaskExecutor.resume(); mSplitSingleThreadTaskExecutor.resume(); + mEventsManagerCoordinator.notifyInternalEvent(SplitInternalEvent.ENCRYPTION_MIGRATION_DONE); mSyncManager.start(); mLifecycleManager.register(mSyncManager); + Logger.i("Android SDK initialized!"); } catch (Exception e) { Logger.e("Error initializing Android SDK", e); diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index ea65577f1..d2630edbc 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -32,6 +32,7 @@ import io.split.android.client.lifecycle.SplitLifecycleManagerImpl; import io.split.android.client.network.HttpClient; import io.split.android.client.network.HttpClientImpl; +import io.split.android.client.service.CleanUpDatabaseTask; import io.split.android.client.service.SplitApiFacade; import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor; import io.split.android.client.service.executor.SplitTaskExecutor; @@ -57,6 +58,8 @@ import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.common.SplitStorageContainer; import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.db.StorageFactory; +import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.TelemetrySynchronizer; import io.split.android.client.telemetry.storage.TelemetryStorage; import io.split.android.client.utils.logger.Logger; @@ -88,9 +91,14 @@ public class SplitFactoryImpl implements SplitFactory { private final SplitStorageContainer mStorageContainer; private final SplitClientContainer mClientContainer; - private final UserConsentManager mUserConsentManager; + private volatile UserConsentManager mUserConsentManager; private final ReentrantLock mInitLock = new ReentrantLock(); + private final EventsTrackerProvider mEventsTrackerProvider; + private final StrategyImpressionManager mImpressionManager; + private final SplitTaskExecutor mSplitTaskExecutor; + private final SplitClientConfig mConfig; + public SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull SplitClientConfig config, @NonNull Context context) throws URISyntaxException { this(apiToken, key, config, context, @@ -113,23 +121,6 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp KeyValidator keyValidator = new KeyValidatorImpl(); ValidationMessageLogger validationLogger = new ValidationMessageLoggerImpl(); - HttpClient defaultHttpClient; - if (httpClient == null) { - HttpClientImpl.Builder builder = new HttpClientImpl.Builder() - .setConnectionTimeout(config.connectionTimeout()) - .setReadTimeout(config.readTimeout()) - .setProxy(config.proxy()) - .setDevelopmentSslConfig(config.developmentSslConfig()) - .setContext(context) - .setProxyAuthenticator(config.authenticator()); - if (config.certificatePinningConfiguration() != null) { - builder.setCertificatePinningConfiguration(config.certificatePinningConfiguration()); - } - - defaultHttpClient = builder.build(); - } else { - defaultHttpClient = httpClient; - } ValidationErrorInfo errorInfo = keyValidator.validate(key.matchingKey(), key.bucketingKey()); String validationTag = "factory instantiation"; if (errorInfo != null) { @@ -161,27 +152,46 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp splitDatabase = testDatabase; Logger.d("Using test database"); } - - defaultHttpClient.addHeaders(factoryHelper.buildHeaders(config, apiToken)); - defaultHttpClient.addStreamingHeaders(factoryHelper.buildStreamingHeaders(apiToken)); - - SplitTaskExecutor splitTaskExecutor = new SplitTaskExecutorImpl(); - splitTaskExecutor.pause(); - - EventsManagerCoordinator mEventsManagerCoordinator = new EventsManagerCoordinator(); - + mConfig = config; SplitCipher splitCipher = factoryHelper.getCipher(apiToken, config.encryptionEnabled()); + SplitsStorage splitsStorage = getSplitsStorage(splitDatabase, splitCipher); + ScheduledThreadPoolExecutor impressionsObserverExecutor = new ScheduledThreadPoolExecutor(1, new ThreadPoolExecutor.CallerRunsPolicy()); + mStorageContainer = factoryHelper.buildStorageContainer(config.userConsent(), - splitDatabase, config.shouldRecordTelemetry(), splitCipher, telemetryStorage, config.observerCacheExpirationPeriod(), impressionsObserverExecutor); + splitDatabase, config.shouldRecordTelemetry(), splitCipher, telemetryStorage, config.observerCacheExpirationPeriod(), impressionsObserverExecutor, splitsStorage); + + mSplitTaskExecutor = new SplitTaskExecutorImpl(); + mSplitTaskExecutor.pause(); + + EventsManagerCoordinator mEventsManagerCoordinator = new EventsManagerCoordinator(); Pair, String> filtersConfig = factoryHelper.getFilterConfiguration(config.syncConfig()); Map filters = filtersConfig.first; String splitsFilterQueryStringFromConfig = filtersConfig.second; String flagsSpec = getFlagsSpec(testingConfig); + HttpClient defaultHttpClient; + if (httpClient == null) { + HttpClientImpl.Builder builder = new HttpClientImpl.Builder() + .setConnectionTimeout(config.connectionTimeout()) + .setReadTimeout(config.readTimeout()) + .setProxy(config.proxy()) + .setDevelopmentSslConfig(config.developmentSslConfig()) + .setContext(context) + .setProxyAuthenticator(config.authenticator()); + if (config.certificatePinningConfiguration() != null) { + builder.setCertificatePinningConfiguration(config.certificatePinningConfiguration()); + } + + defaultHttpClient = builder.build(); + } else { + defaultHttpClient = httpClient; + } + defaultHttpClient.addHeaders(factoryHelper.buildHeaders(config, apiToken)); + defaultHttpClient.addStreamingHeaders(factoryHelper.buildStreamingHeaders(apiToken)); SplitApiFacade splitApiFacade = factoryHelper.buildApiFacade( config, defaultHttpClient, splitsFilterQueryStringFromConfig); @@ -192,19 +202,22 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp getFlagsSpec(testingConfig), mEventsManagerCoordinator, filters, flagSetsFilter, testingConfig); WorkManagerWrapper workManagerWrapper = factoryHelper.buildWorkManagerWrapper(context, config, apiToken, databaseName, filters); + SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor = new SplitSingleThreadTaskExecutor(); splitSingleThreadTaskExecutor.pause(); - ImpressionStrategyProvider impressionStrategyProvider = factoryHelper.getImpressionStrategyProvider(splitTaskExecutor, splitTaskFactory, mStorageContainer, config); + ImpressionStrategyProvider impressionStrategyProvider = factoryHelper.getImpressionStrategyProvider(mSplitTaskExecutor, splitTaskFactory, mStorageContainer, config); Pair noneComponents = impressionStrategyProvider.getNoneComponents(); - StrategyImpressionManager impressionManager = new StrategyImpressionManager(noneComponents, impressionStrategyProvider.getStrategy(config.impressionsMode())); + + mImpressionManager = new StrategyImpressionManager(noneComponents, impressionStrategyProvider.getStrategy(config.impressionsMode())); final RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory = new RetryBackoffCounterTimerFactory(); - StreamingComponents streamingComponents = factoryHelper.buildStreamingComponents(splitTaskExecutor, + StreamingComponents streamingComponents = factoryHelper.buildStreamingComponents(mSplitTaskExecutor, splitTaskFactory, config, defaultHttpClient, splitApiFacade, mStorageContainer, flagsSpec); + Synchronizer mSynchronizer = new SynchronizerImpl( config, - splitTaskExecutor, + mSplitTaskExecutor, splitSingleThreadTaskExecutor, splitTaskFactory, workManagerWrapper, @@ -212,7 +225,7 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp mStorageContainer.getTelemetryStorage(), new AttributesSynchronizerRegistryImpl(), new MySegmentsSynchronizerRegistryImpl(), - impressionManager, + mImpressionManager, mStorageContainer.getEventsStorage(), mEventsManagerCoordinator, streamingComponents.getPushManagerEventBroadcaster() @@ -225,18 +238,18 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp CompressionUtilProvider compressionProvider = new CompressionUtilProvider(); - TelemetrySynchronizer telemetrySynchronizer = factoryHelper.getTelemetrySynchronizer(splitTaskExecutor, + TelemetrySynchronizer telemetrySynchronizer = factoryHelper.getTelemetrySynchronizer(mSplitTaskExecutor, splitTaskFactory, config.telemetryRefreshRate(), config.shouldRecordTelemetry()); mSyncManager = factoryHelper.buildSyncManager( config, - splitTaskExecutor, + mSplitTaskExecutor, mSynchronizer, telemetrySynchronizer, streamingComponents.getPushNotificationManager(), streamingComponents.getPushManagerEventBroadcaster(), factoryHelper.getSplitUpdatesWorker(config, - splitTaskExecutor, + mSplitTaskExecutor, splitTaskFactory, mSynchronizer, streamingComponents.getSplitsUpdateNotificationQueue(), @@ -262,22 +275,22 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp } else { customerImpressionListener = new ImpressionListener.FederatedImpressionListener(splitImpressionListener, impressionListeners); } - EventsTracker eventsTracker = buildEventsTracker(); - mUserConsentManager = new UserConsentManagerImpl(config, - mStorageContainer.getImpressionsStorage(), - mStorageContainer.getEventsStorage(), - mSyncManager, eventsTracker, impressionManager, splitTaskExecutor); - ClientComponentsRegister componentsRegister = factoryHelper.getClientComponentsRegister(config, splitTaskExecutor, + mEventsTrackerProvider = new EventsTrackerProvider(mStorageContainer.getSplitsStorage(), + mStorageContainer.getTelemetryStorage(), mSyncManager); + + ClientComponentsRegister componentsRegister = factoryHelper.getClientComponentsRegister(config, mSplitTaskExecutor, mEventsManagerCoordinator, mSynchronizer, streamingComponents.getNotificationParser(), streamingComponents.getNotificationProcessor(), streamingComponents.getSseAuthenticator(), mStorageContainer, mSyncManager, compressionProvider); + mClientContainer = new SplitClientContainerImpl( mDefaultClientKey.matchingKey(), this, config, mSyncManager, - telemetrySynchronizer, mStorageContainer, splitTaskExecutor, splitApiFacade, + telemetrySynchronizer, mStorageContainer, mSplitTaskExecutor, splitApiFacade, validationLogger, keyValidator, customerImpressionListener, streamingComponents.getPushNotificationManager(), componentsRegister, workManagerWrapper, - eventsTracker, flagSetsFilter); + mEventsTrackerProvider, flagSetsFilter); + mDestroyer = new Runnable() { public void run() { mInitLock.lock(); @@ -307,7 +320,7 @@ public void run() { Logger.d("Successful shutdown of httpclient"); mManager.destroy(); Logger.d("Successful shutdown of manager"); - splitTaskExecutor.stop(); + mSplitTaskExecutor.stop(); splitSingleThreadTaskExecutor.stop(); Logger.d("Successful shutdown of task executor"); mStorageContainer.getAttributesStorageContainer().destroy(); @@ -329,7 +342,6 @@ public void run() { SplitFactoryImpl.this.destroy(); } }); - // Set up async initialization final SplitFactoryHelper.Initializer initializer = new SplitFactoryHelper.Initializer(apiToken, config, @@ -337,7 +349,7 @@ public void run() { splitDatabase, splitCipher, mEventsManagerCoordinator, - splitTaskExecutor, + mSplitTaskExecutor, splitSingleThreadTaskExecutor, mStorageContainer, mSyncManager, @@ -353,13 +365,20 @@ public void run() { // Run initializer new Thread(initializer).start(); + CleanUpDatabaseTask cleanUpDatabaseTask = splitTaskFactory.createCleanUpDatabaseTask(System.currentTimeMillis() / 1000); + mSplitTaskExecutor.schedule(cleanUpDatabaseTask, 5L, null); + // Initialize default client client(); SplitParser mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer(), mStorageContainer.getMyLargeSegmentsStorageContainer()); mManager = new SplitManagerImpl( mStorageContainer.getSplitsStorage(), new SplitValidatorImpl(), mSplitParser); + } + @NonNull + private static SplitsStorage getSplitsStorage(SplitRoomDatabase splitDatabase, SplitCipher splitCipher) { + return StorageFactory.getSplitsStorage(splitDatabase, splitCipher); } private static String getFlagsSpec(@Nullable TestingConfig testingConfig) { @@ -427,16 +446,31 @@ public void flush() { @Override public void setUserConsent(boolean enabled) { UserConsent newMode = (enabled ? UserConsent.GRANTED : UserConsent.DECLINED); - if (mUserConsentManager == null) { + if (getUserConsentManager() == null) { Logger.e("User consent manager not initialized. Unable to set mode " + newMode.toString()); return; } - mUserConsentManager.setStatus(newMode); + getUserConsentManager().setStatus(newMode); + } + + private UserConsentManager getUserConsentManager() { + if (mUserConsentManager == null) { + synchronized (mConfig) { + if (mUserConsentManager == null) { + mUserConsentManager = new UserConsentManagerImpl(mConfig, + mStorageContainer.getImpressionsStorage(), + mStorageContainer.getEventsStorage(), + mSyncManager, mEventsTrackerProvider, mImpressionManager, mSplitTaskExecutor); + } + } + } + + return mUserConsentManager; } @Override public UserConsent getUserConsent() { - return mUserConsentManager.getStatus(); + return getUserConsentManager().getStatus(); } void checkClients() { @@ -444,14 +478,35 @@ void checkClients() { } private void setupValidations(SplitClientConfig splitClientConfig) { - ValidationConfig.getInstance().setMaximumKeyLength(splitClientConfig.maximumKeyLength()); ValidationConfig.getInstance().setTrackEventNamePattern(splitClientConfig.trackEventNamePattern()); } - private EventsTracker buildEventsTracker() { - EventValidator eventsValidator = new EventValidatorImpl(new KeyValidatorImpl(), mStorageContainer.getSplitsStorage()); - return new EventsTrackerImpl(eventsValidator, new ValidationMessageLoggerImpl(), mStorageContainer.getTelemetryStorage(), - new EventPropertiesProcessorImpl(), mSyncManager); + public static class EventsTrackerProvider { + + private final SplitsStorage mSplitsStorage; + private final TelemetryStorage mTelemetryStorage; + private final SyncManager mSyncManager; + private volatile EventsTracker mEventsTracker; + + public EventsTrackerProvider(SplitsStorage splitsStorage, TelemetryStorage telemetryStorage, SyncManager syncManager) { + mSplitsStorage = splitsStorage; + mTelemetryStorage = telemetryStorage; + mSyncManager = syncManager; + } + + public EventsTracker getEventsTracker() { + if (mEventsTracker == null) { + synchronized (this) { + if (mEventsTracker == null) { + EventValidator eventsValidator = new EventValidatorImpl(new KeyValidatorImpl(), mSplitsStorage); + mEventsTracker = new EventsTrackerImpl(eventsValidator, new ValidationMessageLoggerImpl(), mTelemetryStorage, + new PropertyValidatorImpl(), mSyncManager); + } + } + } + + return mEventsTracker; + } } } diff --git a/src/main/java/io/split/android/client/UserConsentManagerImpl.java b/src/main/java/io/split/android/client/UserConsentManagerImpl.java index 7ebc7d03e..0a33e1304 100644 --- a/src/main/java/io/split/android/client/UserConsentManagerImpl.java +++ b/src/main/java/io/split/android/client/UserConsentManagerImpl.java @@ -20,7 +20,7 @@ public class UserConsentManagerImpl implements UserConsentManager { private final ImpressionsStorage mImpressionsStorage; private final EventsStorage mEventsStorage; private final SyncManager mSyncManager; - private final EventsTracker mEventsTracker; + private final SplitFactoryImpl.EventsTrackerProvider mEventsTracker; private final ImpressionManager mImpressionManager; private UserConsent mCurrentStatus; private final SplitTaskExecutor mTaskExecutor; @@ -30,7 +30,7 @@ public UserConsentManagerImpl(@NonNull SplitClientConfig splitConfig, @NonNull ImpressionsStorage impressionsStorage, @NonNull EventsStorage eventsStorage, @NonNull SyncManager syncManager, - @NonNull EventsTracker eventsTracker, + @NonNull SplitFactoryImpl.EventsTrackerProvider eventsTracker, @NonNull ImpressionManager impressionManager, @NonNull SplitTaskExecutor taskExecutor) { mSplitConfig = checkNotNull(splitConfig); @@ -67,7 +67,7 @@ public UserConsent getStatus() { private void enableTracking(UserConsent status) { final boolean enable = (status != UserConsent.DECLINED); - mEventsTracker.enableTracking(enable); + mEventsTracker.getEventsTracker().enableTracking(enable); mImpressionManager.enableTracking(enable); Logger.d("Tracking has been set to " + enable ); } 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/dtos/Split.java b/src/main/java/io/split/android/client/dtos/Split.java index 3af9030de..e22838048 100644 --- a/src/main/java/io/split/android/client/dtos/Split.java +++ b/src/main/java/io/split/android/client/dtos/Split.java @@ -52,4 +52,15 @@ public class Split { @SerializedName("impressionsDisabled") public boolean impressionsDisabled = false; + + public String json = null; + + public Split() { + + } + + public Split(String name, String json) { + this.name = name; + this.json = json; + } } diff --git a/src/main/java/io/split/android/client/events/BaseEventsManager.java b/src/main/java/io/split/android/client/events/BaseEventsManager.java index 32f591180..e22b9a7dc 100644 --- a/src/main/java/io/split/android/client/events/BaseEventsManager.java +++ b/src/main/java/io/split/android/client/events/BaseEventsManager.java @@ -7,8 +7,8 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; -import io.split.android.client.service.executor.ThreadFactoryBuilder; import io.split.android.client.utils.logger.Logger; import io.split.android.engine.scheduler.PausableThreadPoolExecutor; import io.split.android.engine.scheduler.PausableThreadPoolExecutorImpl; @@ -16,27 +16,35 @@ public abstract class BaseEventsManager implements Runnable { private final static int QUEUE_CAPACITY = 20; + // Shared thread factory for all instances + private static final ThreadFactory EVENTS_THREAD_FACTORY = createThreadFactory(); protected final ArrayBlockingQueue mQueue; protected final Set mTriggered; - public BaseEventsManager() { - - mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); - mTriggered = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - ThreadFactory threadFactory = new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("Split-FactoryEventsManager-%d") - .setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + private static ThreadFactory createThreadFactory() { + final AtomicInteger threadNumber = new AtomicInteger(1); + return new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, "Split-FactoryEventsManager-" + threadNumber.getAndIncrement()); + thread.setDaemon(true); + thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { Logger.e("Unexpected error " + e.getLocalizedMessage()); } - }) - .build(); - launch(threadFactory); + }); + return thread; + } + }; + } + + public BaseEventsManager() { + mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); + mTriggered = Collections.newSetFromMap(new ConcurrentHashMap<>()); + launch(EVENTS_THREAD_FACTORY); } @Override 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/executor/SplitBaseTaskExecutor.java b/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java index 997cfbbab..b45737a8b 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java +++ b/src/main/java/io/split/android/client/service/executor/SplitBaseTaskExecutor.java @@ -125,6 +125,10 @@ public void submitOnMainThread(SplitTask splitTask) { } public void pause() { + long start = System.currentTimeMillis(); + while (!mScheduledTasks.isEmpty() && (System.currentTimeMillis() - start) < 500L) { + try { Thread.sleep(50); } catch (InterruptedException e) { break; } + } mScheduler.pause(); } diff --git a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java index 7b40dd611..20e7b9003 100644 --- a/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/executor/parallel/SplitParallelTaskExecutorFactoryImpl.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; import io.split.android.client.service.executor.ThreadFactoryBuilder; import io.split.android.client.utils.logger.Logger; @@ -14,14 +16,22 @@ public class SplitParallelTaskExecutorFactoryImpl implements SplitParallelTaskEx private final int mThreads = Runtime.getRuntime().availableProcessors(); private final ExecutorService mScheduler = Executors.newFixedThreadPool(mThreads, - new ThreadFactoryBuilder() - .setNameFormat("Split-ParallelTaskExecutor-%d") - .setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { - Logger.e("Unexpected error " + e.getLocalizedMessage()); - } - }).build()); + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final Thread.UncaughtExceptionHandler exceptionHandler = new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { + Logger.e("Unexpected error " + e.getLocalizedMessage()); + } + }; + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setName("Split-ParallelTaskExecutor-" + threadNumber.getAndIncrement()); + thread.setUncaughtExceptionHandler(exceptionHandler); + return thread; + } + }); @Override public SplitParallelTaskExecutor> createForList(Class type) { 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/strategy/DebugStrategy.java b/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java index 1a0c0855d..79ea6dc1c 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java @@ -1,5 +1,6 @@ package io.split.android.client.service.impressions.strategy; +import static io.split.android.client.service.impressions.strategy.Utils.hasProperties; import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; @@ -59,7 +60,8 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { @Override public void apply(@NonNull Impression impression) { - @Nullable Long previousTime = mImpressionsObserver.testAndSet(impression); + @Nullable Long previousTime = hasProperties(impression) ? null : + mImpressionsObserver.testAndSet(impression); impression = impression.withPreviousTime(previousTime); KeyImpression keyImpression = KeyImpression.fromImpression(impression); if (mImpressionsSyncHelper.pushAndCheckIfFlushNeeded(keyImpression) && mIsSynchronizing.get()) { diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java b/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java index 6adcd40bf..23f7c4b7c 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java @@ -1,8 +1,10 @@ package io.split.android.client.service.impressions.strategy; +import static io.split.android.client.service.impressions.strategy.Utils.hasProperties; import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.concurrent.atomic.AtomicBoolean; @@ -68,7 +70,8 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { @Override public void apply(@NonNull Impression impression) { - Long previousTime = mImpressionsObserver.testAndSet(impression); + @Nullable Long previousTime = hasProperties(impression) ? null : + mImpressionsObserver.testAndSet(impression); impression = impression.withPreviousTime(previousTime); if (previousTimeIsValid(previousTime)) { diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/Utils.java b/src/main/java/io/split/android/client/service/impressions/strategy/Utils.java new file mode 100644 index 000000000..7a08334af --- /dev/null +++ b/src/main/java/io/split/android/client/service/impressions/strategy/Utils.java @@ -0,0 +1,17 @@ +package io.split.android.client.service.impressions.strategy; + +import androidx.annotation.NonNull; + +import io.split.android.client.impressions.Impression; + +class Utils { + + static boolean hasProperties(@NonNull Impression impression) { + if (impression == null) { // safety check + return false; + } + + String properties = impression.properties(); + return properties != null && !properties.isEmpty(); + } +} diff --git a/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java b/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java index b420833e1..f5546470e 100644 --- a/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java +++ b/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java @@ -33,6 +33,8 @@ public LoadSplitsTask(@NonNull SplitsStorage splitsStorage, @Nullable String spl @Override @NonNull public SplitTaskExecutionInfo execute() { + long startTime = System.currentTimeMillis(); + // This call loads the feature flags from the DB into memory, as well as the // filter and flags spec values mSplitsStorage.loadLocal(); @@ -48,9 +50,12 @@ public SplitTaskExecutionInfo execute() { } // If change number is not the initial one, and the filter and flags spec have not changed, we don't need to do anything - boolean isNotInitialChangeNumber = mSplitsStorage.getTill() > -1; + long till = mSplitsStorage.getTill(); + boolean isNotInitialChangeNumber = till > -1; + boolean filterHasNotChanged = mSplitsFilterQueryStringFromConfig.equals(queryStringFromStorage); boolean flagsSpecHasNotChanged = mFlagsSpecFromConfig.equals(flagsSpecFromStorage); + if (isNotInitialChangeNumber && filterHasNotChanged && flagsSpecHasNotChanged) { return SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS); } diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java index 2c9b9db04..4f3f2a5d1 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java @@ -10,7 +10,6 @@ import io.split.android.client.RolloutCacheConfiguration; import io.split.android.client.SplitClientConfig; -import io.split.android.client.service.CleanUpDatabaseTask; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; @@ -23,7 +22,7 @@ public class RolloutCacheManagerImpl implements RolloutCacheManager, SplitTask { - public static final int MIN_CACHE_CLEAR_DAYS = 1; // TODO + public static final int MIN_CACHE_CLEAR_DAYS = 1; @NonNull private final GeneralInfoStorage mGeneralInfoStorage; @@ -32,17 +31,13 @@ public class RolloutCacheManagerImpl implements RolloutCacheManager, SplitTask { @NonNull private final RolloutDefinitionsCache[] mStorages; @NonNull - private final CleanUpDatabaseTask mCleanUpDatabaseTask; - @NonNull private final EncryptionMigrationTask mEncryptionMigrationTask; public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull SplitStorageContainer storageContainer, - @NonNull CleanUpDatabaseTask cleanUpDatabaseTask, @NonNull EncryptionMigrationTask encryptionMigrationTask) { this(storageContainer.getGeneralInfoStorage(), splitClientConfig.rolloutCacheConfiguration(), - cleanUpDatabaseTask, encryptionMigrationTask, storageContainer.getSplitsStorage(), storageContainer.getMySegmentsStorageContainer(), @@ -52,11 +47,9 @@ public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @VisibleForTesting RolloutCacheManagerImpl(@NonNull GeneralInfoStorage generalInfoStorage, @NonNull RolloutCacheConfiguration config, - @NonNull CleanUpDatabaseTask cleanUpDatabaseTask, @NonNull EncryptionMigrationTask encryptionMigrationTask, @NonNull RolloutDefinitionsCache... storages) { mGeneralInfoStorage = checkNotNull(generalInfoStorage); - mCleanUpDatabaseTask = checkNotNull(cleanUpDatabaseTask); mEncryptionMigrationTask = checkNotNull(encryptionMigrationTask); mStorages = checkNotNull(storages); mConfig = checkNotNull(config); @@ -66,8 +59,6 @@ public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, @Override public void validateCache(SplitTaskExecutionListener listener) { try { - Logger.v("Rollout cache manager: Executing clearing task"); - mCleanUpDatabaseTask.execute(); Logger.v("Rollout cache manager: Validating cache"); execute(); Logger.v("Rollout cache manager: Migrating encryption"); diff --git a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java index 43b91a593..6a4f28e97 100644 --- a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java @@ -8,7 +8,6 @@ import java.util.concurrent.atomic.AtomicBoolean; -import io.split.android.client.EventsTracker; import io.split.android.client.FlagSetsFilter; import io.split.android.client.SplitClient; import io.split.android.client.SplitClientConfig; @@ -73,7 +72,7 @@ public SplitClientContainerImpl(@NonNull String defaultMatchingKey, @Nullable PushNotificationManager pushNotificationManager, @NonNull ClientComponentsRegister clientComponentsRegister, @NonNull MySegmentsWorkManagerWrapper workManagerWrapper, - @NonNull EventsTracker eventsTracker, + @NonNull SplitFactoryImpl.EventsTrackerProvider eventsTrackerProvider, @Nullable FlagSetsFilter flagSetsFilter) { mDefaultMatchingKey = checkNotNull(defaultMatchingKey); mPushNotificationManager = pushNotificationManager; @@ -91,7 +90,7 @@ public SplitClientContainerImpl(@NonNull String defaultMatchingKey, splitTaskExecutor, validationLogger, keyValidator, - eventsTracker, + eventsTrackerProvider, customerImpressionListener, flagSetsFilter ); @@ -118,8 +117,7 @@ public SplitClientContainerImpl(String defaultMatchingKey, SplitClientConfig config, SplitClientFactory splitClientFactory, ClientComponentsRegister clientComponentsRegister, - MySegmentsWorkManagerWrapper workManagerWrapper, - EventsTracker eventsTracker) { + MySegmentsWorkManagerWrapper workManagerWrapper) { mDefaultMatchingKey = checkNotNull(defaultMatchingKey); mPushNotificationManager = pushNotificationManager; mStreamingEnabled = streamingEnabled; diff --git a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java index e1e5928a1..8efd4c2ef 100644 --- a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java +++ b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java @@ -9,6 +9,8 @@ import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.storage.db.EventDao; import io.split.android.client.storage.db.EventEntity; +import io.split.android.client.storage.db.GeneralInfoDao; +import io.split.android.client.storage.db.GeneralInfoEntity; import io.split.android.client.storage.db.ImpressionDao; import io.split.android.client.storage.db.ImpressionEntity; import io.split.android.client.storage.db.ImpressionsCountDao; @@ -50,7 +52,7 @@ public SplitTaskExecutionInfo execute() { @Override public void run() { updateAttributes(mSplitDatabase.attributesDao()); - updateSplits(mSplitDatabase.splitDao()); + updateSplits(mSplitDatabase.splitDao(), mSplitDatabase.generalInfoDao()); updateSegments(mSplitDatabase.mySegmentDao()); updateLargeSegments(mSplitDatabase.myLargeSegmentDao()); updateImpressions(mSplitDatabase.impressionDao()); @@ -187,7 +189,7 @@ private void updateEvents(EventDao eventDao) { } } - private void updateSplits(SplitDao dao) { + private void updateSplits(SplitDao dao, GeneralInfoDao generalInfoDao) { List items = dao.getAll(); for (SplitEntity item : items) { @@ -204,5 +206,27 @@ private void updateSplits(SplitDao dao) { Logger.e("Error applying cipher to split storage"); } } + + GeneralInfoEntity trafficTypesEntity = generalInfoDao.getByName(GeneralInfoEntity.TRAFFIC_TYPES_MAP); + if (trafficTypesEntity != null) { + String fromTrafficTypes = mFromCipher.decrypt(trafficTypesEntity.getStringValue()); + String toTrafficTypes = mToCipher.encrypt(fromTrafficTypes); + if (toTrafficTypes != null) { + generalInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, toTrafficTypes)); + } else { + Logger.e("Error applying cipher to traffic types"); + } + } + + GeneralInfoEntity flagSetsEntity = generalInfoDao.getByName(GeneralInfoEntity.FLAG_SETS_MAP); + if (flagSetsEntity != null) { + String fromFlagSets = mFromCipher.decrypt(flagSetsEntity.getStringValue()); + String toFlagSets = mToCipher.encrypt(fromFlagSets); + if (toFlagSets != null) { + generalInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, toFlagSets)); + } else { + Logger.e("Error applying cipher to flag sets"); + } + } } } diff --git a/src/main/java/io/split/android/client/storage/cipher/DBCipher.java b/src/main/java/io/split/android/client/storage/cipher/DBCipher.java index d5cd55eb7..6cd9a09a1 100644 --- a/src/main/java/io/split/android/client/storage/cipher/DBCipher.java +++ b/src/main/java/io/split/android/client/storage/cipher/DBCipher.java @@ -7,7 +7,6 @@ import androidx.annotation.WorkerThread; import io.split.android.client.storage.db.SplitRoomDatabase; -import io.split.android.client.utils.logger.Logger; public class DBCipher { @@ -41,6 +40,7 @@ public DBCipher(@NonNull SplitRoomDatabase splitDatabase, if (mMustApply) { mFromCipher = SplitCipherFactory.create(apiKey, fromLevel); + mToCipher = checkNotNull(toCipher); mSplitDatabase = checkNotNull(splitDatabase); mTaskProvider = checkNotNull(taskProvider); @@ -50,11 +50,9 @@ public DBCipher(@NonNull SplitRoomDatabase splitDatabase, @WorkerThread public void apply() { if (mMustApply) { - Logger.d("Migrating encryption mode"); - mTaskProvider.get(mSplitDatabase, mFromCipher, mToCipher).execute(); - Logger.d("Encryption mode migration done"); - } else { - Logger.d("No need to migrate encryption mode"); + ApplyCipherTask task = mTaskProvider.get(mSplitDatabase, mFromCipher, mToCipher); + + task.execute(); } } diff --git a/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java b/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java index 3a84d6be4..953147007 100644 --- a/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java +++ b/src/main/java/io/split/android/client/storage/cipher/EncryptionMigrationTask.java @@ -33,14 +33,18 @@ public EncryptionMigrationTask(String apiKey, @Override public SplitTaskExecutionInfo execute() { try { + long startTime = System.currentTimeMillis(); + // Get current encryption level SplitEncryptionLevel fromLevel = getFromLevel(mSplitDatabase.generalInfoDao(), mEncryptionEnabled); // Determine target encryption level SplitEncryptionLevel toLevel = getLevel(mEncryptionEnabled); + DBCipher dbCipher = new DBCipher(mApiKey, mSplitDatabase, fromLevel, toLevel, mToCipher); + // Apply encryption - new DBCipher(mApiKey, mSplitDatabase, fromLevel, toLevel, mToCipher).apply(); + dbCipher.apply(); // Update encryption level updateCurrentLevel(toLevel); @@ -65,8 +69,7 @@ private static SplitEncryptionLevel getFromLevel(GeneralInfoDao generalInfoDao, .getByName(GeneralInfoEntity.DATABASE_ENCRYPTION_MODE); if (entity != null) { - return SplitEncryptionLevel.fromString( - entity.getStringValue()); + return SplitEncryptionLevel.fromString(entity.getStringValue()); } return getLevel(encryptionEnabled); diff --git a/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java b/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java index ce7f25696..b5754bcdf 100644 --- a/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java +++ b/src/main/java/io/split/android/client/storage/db/GeneralInfoEntity.java @@ -14,6 +14,8 @@ public class GeneralInfoEntity { public static final String SPLITS_FILTER_QUERY_STRING = "splitsFilterQueryString"; public static final String DATABASE_ENCRYPTION_MODE = "databaseEncryptionMode"; public static final String FLAGS_SPEC = "flagsSpec"; + public static final String TRAFFIC_TYPES_MAP = "trafficTypesMap"; + public static final String FLAG_SETS_MAP = "flagSetsMap"; @PrimaryKey() @NonNull diff --git a/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java b/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java index 6d4ea5fd5..784e6f863 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java +++ b/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java @@ -1,7 +1,7 @@ package io.split.android.client.storage.db; -import java.util.List; +import java.util.Map; public interface SplitQueryDao { - public List get(long rowIdFrom, int maxRows); + Map getAllAsMap(); } \ No newline at end of file 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 68ecb7940..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 @@ -1,57 +1,142 @@ package io.split.android.client.storage.db; import android.database.Cursor; +import android.os.Process; import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; import io.split.android.client.utils.logger.Logger; public class SplitQueryDaoImpl implements SplitQueryDao { private final SplitRoomDatabase mDatabase; + private volatile Map mCachedSplitsMap; + private final Object mLock = new Object(); + private boolean mIsInitialized = false; + private final Thread mInitializationThread; public SplitQueryDaoImpl(SplitRoomDatabase mDatabase) { this.mDatabase = mDatabase; + // Start prefilling the map in a background thread + mInitializationThread = new Thread(() -> { + try { + android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); + } catch (Exception ignore) { + // Ignore + } + long startTime = System.currentTimeMillis(); + + Map result = loadSplitsMap(); + + synchronized (mLock) { + mCachedSplitsMap = result; + mIsInitialized = true; + mLock.notifyAll(); // Notify any waiting threads + } + }); + mInitializationThread.setName("SplitMapPrefill"); + mInitializationThread.start(); } - public List get(long rowIdFrom, int maxRows) { + int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) { + final int index = c.getColumnIndex(name); + if (index >= 0) { + return index; + } + return c.getColumnIndexOrThrow("`" + name + "`"); + } - String sql = "SELECT rowid, name, body, updated_at FROM splits WHERE rowId > ? ORDER BY rowId LIMIT ?"; - Object[] arguments = {rowIdFrom, maxRows}; - Cursor cursor = mDatabase.query(sql, arguments); + public Map getAllAsMap() { + // Fast path - if the map is already initialized, return it immediately + if (mIsInitialized && !mCachedSplitsMap.isEmpty()) { + return new HashMap<>(mCachedSplitsMap); + } + + // Wait for initialization to complete if it's in progress + synchronized (mLock) { + if (mIsInitialized && !mCachedSplitsMap.isEmpty()) { + return new HashMap<>(mCachedSplitsMap); + } + + // If initialization thread is running, wait for it + if (mInitializationThread != null && mInitializationThread.isAlive()) { + try { + mLock.wait(5000); // Wait up to 5 seconds + + if (mIsInitialized) { + return new HashMap<>(mCachedSplitsMap); + } + } catch (InterruptedException e) { + } + } + + // If we get here, either initialization failed or timed out + // Load the map directly + Map result = loadSplitsMap(); + + // Cache the result for future calls + mCachedSplitsMap = result; + mIsInitialized = true; + + return new HashMap<>(result); + } + } + + /** + * Internal method to load the splits map from the database. + * This contains the actual loading logic separated from the caching/synchronization. + */ + private Map loadSplitsMap() { + final String sql = "SELECT name, body FROM splits"; + + Cursor cursor = mDatabase.query(sql, null); + + final int ESTIMATED_CAPACITY = 2000; + Map result = new HashMap<>(ESTIMATED_CAPACITY); + try { - final int rowIdIndex = getColumnIndexOrThrow(cursor, "rowid"); final int nameIndex = getColumnIndexOrThrow(cursor, "name"); final int bodyIndex = getColumnIndexOrThrow(cursor, "body"); - final int updatedAtIndex = getColumnIndexOrThrow(cursor, "updated_at"); - final List entities = new ArrayList(cursor.getCount()); + + final int BATCH_SIZE = 100; + String[] names = new String[BATCH_SIZE]; + String[] bodies = new String[BATCH_SIZE]; + int batchCount = 0; + while (cursor.moveToNext()) { - final SplitEntity item; - item = new SplitEntity(); - item.setRowId(cursor.getLong(rowIdIndex)); - item.setName(cursor.getString(nameIndex)); - item.setBody(cursor.getString(bodyIndex)); - item.setUpdatedAt(cursor.getLong(updatedAtIndex)); - entities.add(item); + names[batchCount] = cursor.getString(nameIndex); + bodies[batchCount] = cursor.getString(bodyIndex); + batchCount++; + + // Process in batches + if (batchCount == BATCH_SIZE) { + for (int i = 0; i < BATCH_SIZE; i++) { + SplitEntity entity = new SplitEntity(); + entity.setName(names[i]); + entity.setBody(bodies[i]); + result.put(names[i], entity); + } + batchCount = 0; + } + } + + // Process any remaining items + for (int i = 0; i < batchCount; i++) { + SplitEntity entity = new SplitEntity(); + entity.setName(names[i]); + entity.setBody(bodies[i]); + result.put(names[i], entity); } - return entities; } catch (Exception e) { - Logger.e("Error executing splits query: " + e.getLocalizedMessage()); + Logger.e("Error executing loadSplitsMap query: " + e.getLocalizedMessage()); } finally { cursor.close(); } - return new ArrayList<>(); - } - int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) { - final int index = c.getColumnIndex(name); - if (index >= 0) { - return index; - } - return c.getColumnIndexOrThrow("`" + name + "`"); + return result; } } diff --git a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java index 273208e52..e17ee3db9 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java +++ b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java @@ -5,12 +5,16 @@ import android.content.Context; +import androidx.annotation.NonNull; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; +import androidx.sqlite.db.SupportSQLiteDatabase; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; import io.split.android.client.storage.db.attributes.AttributesDao; import io.split.android.client.storage.db.attributes.AttributesEntity; @@ -18,6 +22,7 @@ import io.split.android.client.storage.db.impressions.observer.ImpressionsObserverCacheEntity; import io.split.android.client.storage.db.impressions.unique.UniqueKeyEntity; import io.split.android.client.storage.db.impressions.unique.UniqueKeysDao; +import io.split.android.client.utils.logger.Logger; @Database( entities = { @@ -54,6 +59,21 @@ public abstract class SplitRoomDatabase extends RoomDatabase { private static volatile Map mInstances = new ConcurrentHashMap<>(); + /** + * Get the SplitQueryDao instance for optimized split queries. + * This uses direct cursor access for better performance. + */ + public SplitQueryDao getSplitQueryDao() { + if (mSplitQueryDao == null) { + synchronized (this) { + if (mSplitQueryDao == null) { + mSplitQueryDao = new SplitQueryDaoImpl(this); + } + } + } + return mSplitQueryDao; + } + public static SplitRoomDatabase getDatabase(final Context context, final String databaseName) { checkNotNull(context); checkNotNull(databaseName); @@ -63,23 +83,32 @@ public static SplitRoomDatabase getDatabase(final Context context, final String instance = mInstances.get(databaseName); if (instance == null) { instance = Room.databaseBuilder(context.getApplicationContext(), - SplitRoomDatabase.class, databaseName) - .setJournalMode(JournalMode.TRUNCATE) + SplitRoomDatabase.class, databaseName) + .setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) .fallbackToDestructiveMigration() .build(); + + try { + SupportSQLiteDatabase db = instance.getOpenHelper().getWritableDatabase(); + + db.execSQL("PRAGMA cache_size = -3000"); + db.execSQL("PRAGMA automatic_index = ON"); + db.execSQL("PRAGMA foreign_keys = OFF"); + } catch (Exception e) { + Logger.i("Failed to set optimized pragma"); + } + + mInstances.put(databaseName, instance); + new Thread(() -> { + try { + mInstances.get(databaseName).getSplitQueryDao(); + } catch (Exception e) { + Logger.i("Failed to preload query DAO"); + } + }).start(); } } return instance; } - - public SplitQueryDao splitQueryDao() { - if (mSplitQueryDao != null) { - return mSplitQueryDao; - } - synchronized (this) { - mSplitQueryDao = new SplitQueryDaoImpl(this); - return mSplitQueryDao; - } - } } diff --git a/src/main/java/io/split/android/client/storage/splits/MetadataHelper.java b/src/main/java/io/split/android/client/storage/splits/MetadataHelper.java new file mode 100644 index 000000000..743e07552 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/splits/MetadataHelper.java @@ -0,0 +1,89 @@ +package io.split.android.client.storage.splits; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.split.android.client.dtos.Split; + +class MetadataHelper { + + static void increaseTrafficTypeCount(@Nullable String name, Map outputTrafficTypes) { + if (name == null) { + return; + } + + String lowercaseName = name.toLowerCase(); + int count = countForTrafficType(lowercaseName, outputTrafficTypes); + outputTrafficTypes.put(lowercaseName, ++count); + } + + static void decreaseTrafficTypeCount(@Nullable String name, Map outputTrafficTypes) { + if (name == null) { + return; + } + String lowercaseName = name.toLowerCase(); + + int count = countForTrafficType(lowercaseName, outputTrafficTypes); + if (count > 1) { + outputTrafficTypes.put(lowercaseName, --count); + } else { + outputTrafficTypes.remove(lowercaseName); + } + } + + static int countForTrafficType(@NonNull String name, Map outputTrafficTypes) { + int count = 0; + Integer countValue = outputTrafficTypes.get(name); + if (countValue != null) { + count = countValue; + } + return count; + } + + static void addOrUpdateFlagSets(Split split, Map> outputFlagSets) { + if (split.sets == null) { + return; + } + + for (String set : split.sets) { + Set splitsForSet = outputFlagSets.get(set); + if (splitsForSet == null) { + splitsForSet = new HashSet<>(); + outputFlagSets.put(set, splitsForSet); + } + splitsForSet.add(split.name); + } + + deleteFromFlagSetsIfNecessary(split, outputFlagSets); + } + + static void deleteFromFlagSetsIfNecessary(Split featureFlag, Map> outputFlagSets) { + if (featureFlag.sets == null) { + return; + } + + for (String set : outputFlagSets.keySet()) { + if (featureFlag.sets.contains(set)) { + continue; + } + + Set flagsForSet = outputFlagSets.get(set); + if (flagsForSet != null) { + flagsForSet.remove(featureFlag.name); + } + } + } + + static void deleteFromFlagSets(Split featureFlag, Map> outputFlagSets) { + for (String set : outputFlagSets.keySet()) { + Set flagsForSet = outputFlagSets.get(set); + if (flagsForSet != null) { + flagsForSet.remove(featureFlag.name); + } + } + } +} diff --git a/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java index fc5fe1203..079eb7abf 100644 --- a/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/PersistentSplitsStorage.java @@ -3,11 +3,13 @@ import androidx.annotation.Nullable; import java.util.List; +import java.util.Map; +import java.util.Set; import io.split.android.client.dtos.Split; public interface PersistentSplitsStorage { - boolean update(ProcessedSplitChange splitChange); + boolean update(ProcessedSplitChange splitChange, Map mTrafficTypes, Map> mFlagSets); SplitsSnapshot getSnapshot(); List getAll(); diff --git a/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java b/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java index 40ace1db2..9c4253139 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformer.java @@ -1,7 +1,6 @@ package io.split.android.client.storage.splits; import static io.split.android.client.utils.Utils.checkNotNull; -import static io.split.android.client.utils.Utils.partition; import androidx.annotation.NonNull; @@ -9,92 +8,54 @@ import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Callable; +import java.util.Map; import io.split.android.client.dtos.Split; -import io.split.android.client.service.executor.parallel.SplitDeferredTaskItem; -import io.split.android.client.service.executor.parallel.SplitParallelTaskExecutor; import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.db.SplitEntity; -import io.split.android.client.utils.Json; import io.split.android.client.utils.logger.Logger; public class SplitEntityToSplitTransformer implements SplitListTransformer { - private final SplitParallelTaskExecutor> mTaskExecutor; private final SplitCipher mSplitCipher; - public SplitEntityToSplitTransformer(@NonNull SplitParallelTaskExecutor> taskExecutor, - @NonNull SplitCipher splitCipher) { - mTaskExecutor = checkNotNull(taskExecutor); + public SplitEntityToSplitTransformer(@NonNull SplitCipher splitCipher) { mSplitCipher = checkNotNull(splitCipher); } @Override + @Deprecated public List transform(List entities) { - if (entities == null) { - return new ArrayList<>(); - } - - int entitiesCount = entities.size(); - - if (entitiesCount > mTaskExecutor.getAvailableThreads()) { - List> result = mTaskExecutor.execute(getSplitDeserializationTasks(entities, entitiesCount)); - List splits = new ArrayList<>(); - for (List subList : result) { - splits.addAll(subList); - } - - return splits; - } else { - return convertEntitiesToSplitList(entities, mSplitCipher); - } + return new ArrayList<>(); // no - op } - @NonNull - private List>> getSplitDeserializationTasks(List allEntities, int entitiesCount) { - int availableThreads = mTaskExecutor.getAvailableThreads(); - int partitionSize = availableThreads > 0 ? entitiesCount / availableThreads : 1; - List> partitions = partition(allEntities, partitionSize); - List>> taskList = new ArrayList<>(partitions.size()); - - for (List partition : partitions) { - taskList.add(new SplitDeferredTaskItem<>( - new Callable>() { - @Override - public List call() { - return convertEntitiesToSplitList(partition, mSplitCipher); - } - })); - } - - return taskList; - } - - @NonNull - private static List convertEntitiesToSplitList(List entities, - SplitCipher cipher) { - List splits = new ArrayList<>(); - - if (entities == null) { - return splits; + @Override + public List transform(Map allNamesAndBodies) { + if (allNamesAndBodies == null) { + return new ArrayList<>(); } - for (SplitEntity entity : entities) { - String name; - String json; + List splits = new ArrayList<>(allNamesAndBodies.size()); + for (Map.Entry entry : allNamesAndBodies.entrySet()) { + if (entry == null || entry.getValue() == null) { + continue; + } try { - name = cipher.decrypt(entity.getName()); - json = cipher.decrypt(entity.getBody()); - if (name != null && json != null) { - Split split = Json.fromJson(json, Split.class); - split.name = name; - splits.add(split); + String decryptedName = mSplitCipher.decrypt(entry.getKey()); + if (decryptedName == null) { + continue; + } + String decryptedBody = mSplitCipher.decrypt(entry.getValue().getBody()); + if (decryptedBody == null) { + continue; } + + splits.add(new Split(decryptedName, decryptedBody)); } catch (JsonSyntaxException e) { - Logger.e("Could not parse entity to split: " + entity.getName()); + Logger.e("Could not parse entity to split: " + entry.getKey()); } } + return splits; } } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java b/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java index 57a486d79..2b191d70d 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitListTransformer.java @@ -1,6 +1,7 @@ package io.split.android.client.storage.splits; import java.util.List; +import java.util.Map; /** * Used to map a list of values of type {@link I} to a list of type {@link O} @@ -11,4 +12,6 @@ public interface SplitListTransformer { List transform(List inputList); + + List transform(Map allNamesAndBodies); } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java b/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java index f72b2f342..0c43d4056 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformer.java @@ -6,7 +6,9 @@ import androidx.annotation.NonNull; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.Callable; import io.split.android.client.dtos.Split; @@ -21,7 +23,7 @@ public class SplitToSplitEntityTransformer implements SplitListTransformer> mTaskExecutor; private final SplitCipher mSplitCipher; - + public SplitToSplitEntityTransformer(@NonNull SplitParallelTaskExecutor> taskExecutor, @NonNull SplitCipher splitCipher) { mTaskExecutor = checkNotNull(taskExecutor); @@ -30,6 +32,7 @@ public SplitToSplitEntityTransformer(@NonNull SplitParallelTaskExecutor transform(List splits) { + List splitEntities = new ArrayList<>(); if (splits == null) { @@ -38,7 +41,8 @@ public List transform(List splits) { int splitsSize = splits.size(); if (splitsSize > mTaskExecutor.getAvailableThreads()) { - List> subLists = mTaskExecutor.execute(getSplitEntityTasks(splits, splitsSize)); + List>> tasks = getSplitEntityTasks(splits, splitsSize); + List> subLists = mTaskExecutor.execute(tasks); for (List subList : subLists) { splitEntities.addAll(subList); @@ -51,11 +55,18 @@ public List transform(List splits) { } } + @Override + @Deprecated + public List transform(Map allNamesAndBodies) { + return Collections.emptyList(); // to be removed + } + @NonNull - private static List getSplitEntities(List partition, SplitCipher cipher) { + private List getSplitEntities(List partition, SplitCipher cipher) { List result = new ArrayList<>(); for (Split split : partition) { + // Create entity String encryptedName = cipher.encrypt(split.name); String encryptedJson = cipher.encrypt(Json.toJson(split)); if (encryptedName == null || encryptedJson == null) { @@ -65,6 +76,7 @@ private static List getSplitEntities(List partition, SplitCi SplitEntity entity = new SplitEntity(); entity.setName(encryptedName); entity.setBody(encryptedJson); + entity.setUpdatedAt(System.currentTimeMillis() / 1000); result.add(entity); } @@ -91,4 +103,5 @@ public List call() { return taskList; } + } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java b/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java index c73ef1beb..19fe8c082 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsSnapshot.java @@ -3,7 +3,10 @@ import androidx.annotation.NonNull; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import io.split.android.client.dtos.Split; @@ -14,13 +17,19 @@ public class SplitsSnapshot { private final long mUpdateTimestamp; private final String mSplitsFilterQueryString; private final String mFlagsSpec; + private final Map mTrafficTypesMap; + private final Map> mFlagSetsMap; - public SplitsSnapshot(List splits, long changeNumber, long updateTimestamp, String splitsFilterQueryString, String flagsSpec) { + public SplitsSnapshot(List splits, long changeNumber, long updateTimestamp, + String splitsFilterQueryString, String flagsSpec, + Map trafficTypesMap, Map> flagSetsMap) { mChangeNumber = changeNumber; mSplits = splits; mUpdateTimestamp = updateTimestamp; mSplitsFilterQueryString = splitsFilterQueryString; mFlagsSpec = flagsSpec; + mTrafficTypesMap = trafficTypesMap != null ? trafficTypesMap : new HashMap<>(); + mFlagSetsMap = flagSetsMap != null ? flagSetsMap : new HashMap<>(); } public long getChangeNumber() { @@ -42,4 +51,12 @@ public String getSplitsFilterQueryString() { public String getFlagsSpec() { return mFlagsSpec; } + + public @NonNull Map getTrafficTypesMap() { + return mTrafficTypesMap; + } + + public @NonNull Map> getFlagSetsMap() { + return mFlagSetsMap; + } } diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java b/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java index add0148ff..3c6d48b91 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsStorageImpl.java @@ -1,11 +1,18 @@ package io.split.android.client.storage.splits; +import static io.split.android.client.storage.splits.MetadataHelper.addOrUpdateFlagSets; +import static io.split.android.client.storage.splits.MetadataHelper.decreaseTrafficTypeCount; +import static io.split.android.client.storage.splits.MetadataHelper.deleteFromFlagSets; +import static io.split.android.client.storage.splits.MetadataHelper.deleteFromFlagSetsIfNecessary; +import static io.split.android.client.storage.splits.MetadataHelper.increaseTrafficTypeCount; import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import com.google.gson.JsonSyntaxException; + import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -13,8 +20,10 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import io.split.android.client.dtos.Split; +import io.split.android.client.utils.Json; public class SplitsStorageImpl implements SplitsStorage { @@ -26,8 +35,10 @@ public class SplitsStorageImpl implements SplitsStorage { private String mSplitsFilterQueryString; private String mFlagsSpec; private final Map mTrafficTypes; + private final AtomicBoolean mInitialized; public SplitsStorageImpl(@NonNull PersistentSplitsStorage persistentStorage) { + mInitialized = new AtomicBoolean(false); mPersistentStorage = checkNotNull(persistentStorage); mInMemorySplits = new ConcurrentHashMap<>(); mTrafficTypes = new ConcurrentHashMap<>(); @@ -36,35 +47,69 @@ public SplitsStorageImpl(@NonNull PersistentSplitsStorage persistentStorage) { @Override @WorkerThread - public void loadLocal() { - SplitsSnapshot snapshot = mPersistentStorage.getSnapshot(); - List splits = snapshot.getSplits(); - mChangeNumber = snapshot.getChangeNumber(); - mUpdateTimestamp = snapshot.getUpdateTimestamp(); - mSplitsFilterQueryString = snapshot.getSplitsFilterQueryString(); - mFlagsSpec = snapshot.getFlagsSpec(); - for (Split split : splits) { - mInMemorySplits.put(split.name, split); - addOrUpdateFlagSets(split); - increaseTrafficTypeCount(split.trafficTypeName); + public synchronized void loadLocal() { + if (mInitialized.get()) { + return; + } + + try { + long startTime = System.currentTimeMillis(); + + SplitsSnapshot snapshot = mPersistentStorage.getSnapshot(); + List splits = snapshot.getSplits(); + + mChangeNumber = snapshot.getChangeNumber(); + mUpdateTimestamp = snapshot.getUpdateTimestamp(); + mSplitsFilterQueryString = snapshot.getSplitsFilterQueryString(); + mFlagsSpec = snapshot.getFlagsSpec(); + + // Populate traffic types and flag sets + mTrafficTypes.putAll(snapshot.getTrafficTypesMap()); + for (Map.Entry> entry : snapshot.getFlagSetsMap().entrySet()) { + mFlagSets.put(entry.getKey(), new HashSet<>(entry.getValue())); + } + + for (Split split : splits) { + mInMemorySplits.put(split.name, split); + } + } finally { + mInitialized.compareAndSet(false, true); } } @Override public Split get(@NonNull String name) { - return mInMemorySplits.get(name); + Split split = mInMemorySplits.get(name); + if (split == null) { + return null; + } + + if (split.json == null) { + return split; + } + + try { + Split parsedSplit = Json.fromJson(split.json, Split.class); + parsedSplit.json = null; + mInMemorySplits.put(name, parsedSplit); + return mInMemorySplits.get(name); + } catch (JsonSyntaxException e) { + return null; + } } @Override public Map getMany(@Nullable List splitNames) { Map splits = new HashMap<>(); if (splitNames == null || splitNames.isEmpty()) { - splits.putAll(mInMemorySplits); + for (String name : mInMemorySplits.keySet()) { + splits.put(name, get(name)); + } return splits; } for (String name : splitNames) { - Split split = mInMemorySplits.get(name); + Split split = get(name); if (split != null) { splits.put(name, split); } @@ -94,13 +139,13 @@ public boolean update(ProcessedSplitChange splitChange) { appliedUpdates = true; } for (Split split : activeSplits) { - Split loadedSplit = mInMemorySplits.get(split.name); + Split loadedSplit = get(split.name); if (loadedSplit != null && loadedSplit.trafficTypeName != null) { - decreaseTrafficTypeCount(loadedSplit.trafficTypeName); + decreaseTrafficTypeCount(loadedSplit.trafficTypeName, mTrafficTypes); } - increaseTrafficTypeCount(split.trafficTypeName); + increaseTrafficTypeCount(split.trafficTypeName, mTrafficTypes); mInMemorySplits.put(split.name, split); - addOrUpdateFlagSets(split); + addOrUpdateFlagSets(split, mFlagSets); } } @@ -109,15 +154,16 @@ public boolean update(ProcessedSplitChange splitChange) { if (mInMemorySplits.remove(split.name) != null) { // The flag was in memory, so it will be updated appliedUpdates = true; - decreaseTrafficTypeCount(split.trafficTypeName); - deleteFromFlagSetsIfNecessary(split); + decreaseTrafficTypeCount(split.trafficTypeName, mTrafficTypes); + deleteFromFlagSetsIfNecessary(split, mFlagSets); } } } mChangeNumber = splitChange.getChangeNumber(); mUpdateTimestamp = splitChange.getUpdateTimestamp(); - mPersistentStorage.update(splitChange); + + mPersistentStorage.update(splitChange, mTrafficTypes, mFlagSets); return appliedUpdates; } @@ -127,7 +173,7 @@ public boolean update(ProcessedSplitChange splitChange) { public void updateWithoutChecks(Split split) { mInMemorySplits.put(split.name, split); mPersistentStorage.update(split); - deleteFromFlagSets(split); + deleteFromFlagSets(split, mFlagSets); } @Override @@ -197,80 +243,4 @@ public boolean isValidTrafficType(@Nullable String name) { } return (mTrafficTypes.get(name.toLowerCase()) != null); } - - private void increaseTrafficTypeCount(@Nullable String name) { - if (name == null) { - return; - } - - String lowercaseName = name.toLowerCase(); - int count = countForTrafficType(lowercaseName); - mTrafficTypes.put(lowercaseName, ++count); - } - - private void decreaseTrafficTypeCount(@Nullable String name) { - if (name == null) { - return; - } - String lowercaseName = name.toLowerCase(); - - int count = countForTrafficType(lowercaseName); - if (count > 1) { - mTrafficTypes.put(lowercaseName, --count); - } else { - mTrafficTypes.remove(lowercaseName); - } - } - - private int countForTrafficType(@NonNull String name) { - int count = 0; - Integer countValue = mTrafficTypes.get(name); - if (countValue != null) { - count = countValue; - } - return count; - } - - private void addOrUpdateFlagSets(Split split) { - if (split.sets == null) { - return; - } - - for (String set : split.sets) { - Set splitsForSet = mFlagSets.get(set); - if (splitsForSet == null) { - splitsForSet = new HashSet<>(); - mFlagSets.put(set, splitsForSet); - } - splitsForSet.add(split.name); - } - - deleteFromFlagSetsIfNecessary(split); - } - - private void deleteFromFlagSetsIfNecessary(Split featureFlag) { - if (featureFlag.sets == null) { - return; - } - - for (String set : mFlagSets.keySet()) { - if (featureFlag.sets.contains(set)) { - continue; - } - - Set flagsForSet = mFlagSets.get(set); - if (flagsForSet != null) { - flagsForSet.remove(featureFlag.name); - } - } - } - - private void deleteFromFlagSets(Split featureFlag) { - for (String set : mFlagSets.keySet()) { - Set flagsForSet = mFlagSets.get(set); - if (flagsForSet != null) { - flagsForSet.remove(featureFlag.name); - } - } - } } diff --git a/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java index 14aa16835..5bd250a14 100644 --- a/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java @@ -1,5 +1,9 @@ package io.split.android.client.storage.splits; +import static io.split.android.client.storage.splits.MetadataHelper.addOrUpdateFlagSets; +import static io.split.android.client.storage.splits.MetadataHelper.decreaseTrafficTypeCount; +import static io.split.android.client.storage.splits.MetadataHelper.deleteFromFlagSetsIfNecessary; +import static io.split.android.client.storage.splits.MetadataHelper.increaseTrafficTypeCount; import static io.split.android.client.utils.Utils.checkNotNull; import static io.split.android.client.utils.Utils.partition; @@ -9,14 +13,24 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Set; +import io.split.android.client.SplitFactoryImpl; import io.split.android.client.dtos.Split; +import io.split.android.client.dtos.Status; import io.split.android.client.service.executor.parallel.SplitParallelTaskExecutorFactory; import io.split.android.client.service.executor.parallel.SplitParallelTaskExecutorFactoryImpl; import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.db.GeneralInfoEntity; import io.split.android.client.storage.db.SplitEntity; import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; + +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.concurrent.ConcurrentHashMap; public class SqLitePersistentSplitsStorage implements PersistentSplitsStorage { @@ -45,17 +59,18 @@ private SqLitePersistentSplitsStorage(@NonNull SplitRoomDatabase database, @NonNull SplitParallelTaskExecutorFactory executorFactory, @NonNull SplitCipher splitCipher) { this(database, - new SplitEntityToSplitTransformer(executorFactory.createForList(Split.class), splitCipher), + new SplitEntityToSplitTransformer(splitCipher), new SplitToSplitEntityTransformer(executorFactory.createForList(SplitEntity.class), splitCipher), splitCipher); } @Override - public boolean update(ProcessedSplitChange splitChange) { + public boolean update(ProcessedSplitChange splitChange, Map mTrafficTypes, Map> mFlagSets) { if (splitChange == null) { return false; } + List removedSplits = splitNameList(splitChange.getArchivedSplits()); List splitEntities = convertSplitListToEntities(splitChange.getActiveSplits()); @@ -64,8 +79,20 @@ public boolean update(ProcessedSplitChange splitChange) { public void run() { mDatabase.generalInfoDao().update( new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, splitChange.getChangeNumber())); - mDatabase.splitDao().insert(splitEntities); - mDatabase.splitDao().delete(removedSplits); + if (!splitEntities.isEmpty()) { + mDatabase.splitDao().insert(splitEntities); + } + if (!removedSplits.isEmpty()) { + mDatabase.splitDao().delete(removedSplits); + } + if (!mTrafficTypes.isEmpty()) { + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, + Json.toJson(mTrafficTypes))); + } + if (!mFlagSets.isEmpty()) { + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, + Json.toJson(mFlagSets))); + } mDatabase.generalInfoDao().update( new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, splitChange.getUpdateTimestamp())); } @@ -76,10 +103,11 @@ public void run() { @Override public SplitsSnapshot getSnapshot() { - SplitsSnapshotLoader loader = new SplitsSnapshotLoader(mDatabase); - mDatabase.runInTransaction(loader); - return new SplitsSnapshot(loadSplits(), loader.getChangeNumber(), - loader.getUpdateTimestamp(), loader.getSplitsFilterQueryString(), loader.getFlagsSpec()); + SplitsSnapshotLoader loader = new SplitsSnapshotLoader(mDatabase, loadSplits(), mCipher); + loader.run(); + return new SplitsSnapshot(loader.getSplits(), loader.getChangeNumber(), + loader.getUpdateTimestamp(), loader.getSplitsFilterQueryString(), loader.getFlagsSpec(), + loader.getTrafficTypes(), loader.getFlagSets()); } @Override @@ -130,6 +158,8 @@ public void clear() { @Override public void run() { mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, -1)); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, "")); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, "")); mDatabase.splitDao().deleteAll(); } }); @@ -148,10 +178,17 @@ public String getFilterQueryString() { } private List loadSplits() { - return mEntityToSplitTransformer.transform(mDatabase.splitDao().getAll()); + Map allNamesAndBodies = mDatabase.getSplitQueryDao().getAllAsMap(); + long transformStartTime = System.currentTimeMillis(); + List splits = mEntityToSplitTransformer.transform(allNamesAndBodies); + + return splits; } private List convertSplitListToEntities(List splits) { + if (splits == null) { + return new ArrayList<>(); + } return mSplitToEntityTransformer.transform(splits); } @@ -172,9 +209,15 @@ private static class SplitsSnapshotLoader implements Runnable { private Long mUpdateTimestamp = 0L; private String mSplitsFilterQueryString = ""; private String mFlagsSpec = ""; + private Map mTrafficTypes = new ConcurrentHashMap<>(); + private Map> mFlagSets = new ConcurrentHashMap<>(); + private final List mSplits; + private final SplitCipher mCipher; - public SplitsSnapshotLoader(SplitRoomDatabase database) { + public SplitsSnapshotLoader(SplitRoomDatabase database, List splits, SplitCipher cipher) { mDatabase = database; + mSplits = splits; + mCipher = cipher; } @Override @@ -183,6 +226,8 @@ public void run() { GeneralInfoEntity changeNumberEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO); GeneralInfoEntity filterQueryStringEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING); GeneralInfoEntity flagsSpecEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.FLAGS_SPEC); + GeneralInfoEntity trafficTypesEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.TRAFFIC_TYPES_MAP); + GeneralInfoEntity flagSetsEntity = mDatabase.generalInfoDao().getByName(GeneralInfoEntity.FLAG_SETS_MAP); if (changeNumberEntity != null) { mChangeNumber = changeNumberEntity.getLongValue(); @@ -199,6 +244,67 @@ public void run() { if (flagsSpecEntity != null) { mFlagsSpec = flagsSpecEntity.getStringValue(); } + + boolean splitsAreNotEmpty = !mSplits.isEmpty(); + boolean trafficTypesEntityIsEmpty = trafficTypesEntity == null || trafficTypesEntity.getStringValue().isEmpty(); + boolean flagSetsEntityIsEmpty = flagSetsEntity == null || flagSetsEntity.getStringValue().isEmpty(); + boolean trafficTypesAndSetsMigrationRequired = splitsAreNotEmpty && + (trafficTypesEntityIsEmpty || flagSetsEntityIsEmpty); + if (trafficTypesAndSetsMigrationRequired) { + migrateTrafficTypesAndSetsFromStoredData(); + } + + parseTrafficTypesAndSets(trafficTypesEntity, flagSetsEntity); + } + + private synchronized void parseTrafficTypesAndSets(@Nullable GeneralInfoEntity trafficTypesEntity, @Nullable GeneralInfoEntity flagSetsEntity) { + Logger.v("Parsing traffic types and sets"); + if (trafficTypesEntity != null) { + Type mapType = new TypeToken>(){}.getType(); + String encryptedTrafficTypes = trafficTypesEntity.getStringValue(); + String decryptedTrafficTypes = mCipher.decrypt(encryptedTrafficTypes); + mTrafficTypes = Json.fromJson(decryptedTrafficTypes, mapType); + } + + if (flagSetsEntity != null) { + Type flagsMapType = new TypeToken>>(){}.getType(); + String encryptedFlagSets = flagSetsEntity.getStringValue(); + String decryptedFlagSets = mCipher.decrypt(encryptedFlagSets); + mFlagSets = Json.fromJson(decryptedFlagSets, flagsMapType); + } + } + + private void migrateTrafficTypesAndSetsFromStoredData() { + Logger.i("Migration required for cached traffic types and flag sets. Migrating now."); + try { + for (Split split : mSplits) { + Split parsedSplit = Json.fromJson(split.json, Split.class); + if (parsedSplit != null && parsedSplit.status == Status.ACTIVE) { + increaseTrafficTypeCount(parsedSplit.trafficTypeName, mTrafficTypes); + addOrUpdateFlagSets(parsedSplit, mFlagSets); + } else { + decreaseTrafficTypeCount(parsedSplit.trafficTypeName, mTrafficTypes); + deleteFromFlagSetsIfNecessary(parsedSplit, mFlagSets); + } + } + + // persist TTs + String decryptedTrafficTypes = Json.toJson(mTrafficTypes); + String encryptedTrafficTypes = mCipher.encrypt(decryptedTrafficTypes); + + // persist flag sets + String decryptedFlagSets = Json.toJson(mFlagSets); + String encryptedFlagSets = mCipher.encrypt(decryptedFlagSets); + + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, encryptedTrafficTypes)); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, encryptedFlagSets)); + } catch (Exception e) { + Logger.e("Failed to migrate traffic types and flag sets", e); + } + } + + public List getSplits() { + return mSplits; } public Long getChangeNumber() { @@ -216,5 +322,13 @@ public String getSplitsFilterQueryString() { public String getFlagsSpec() { return mFlagsSpec; } + + public Map getTrafficTypes() { + return mTrafficTypes; + } + + public Map> getFlagSets() { + return mFlagSets; + } } } 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 029c84fdd..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) { @@ -71,4 +79,5 @@ private static Gson getNonNullsGsonInstance() { return mNonNullJson; } + } diff --git a/src/main/java/io/split/android/client/utils/Zlib.java b/src/main/java/io/split/android/client/utils/Zlib.java index 049250c0d..efe50e914 100644 --- a/src/main/java/io/split/android/client/utils/Zlib.java +++ b/src/main/java/io/split/android/client/utils/Zlib.java @@ -4,6 +4,7 @@ import java.util.zip.Inflater; import io.split.android.client.service.ServiceConstants; +import io.split.android.client.utils.logger.Logger; public class Zlib implements CompressionUtil { @@ -20,9 +21,9 @@ public byte[] decompress(byte[] input) { inflater.end(); return Arrays.copyOfRange(result, 0, resultLength); } catch (java.util.zip.DataFormatException e) { - System.out.println("DataFormatException error: " + e.getLocalizedMessage()); + Logger.e("DataFormatException error: " + e.getLocalizedMessage()); } catch (Exception e) { - System.out.println("Error decompressing: " + e.getLocalizedMessage()); + Logger.e("Error decompressing: " + e.getLocalizedMessage()); } return null; } 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/SplitFactoryHelperTest.kt b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt index 335173c5d..04cc76c80 100644 --- a/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt +++ b/src/test/java/io/split/android/client/SplitFactoryHelperTest.kt @@ -5,6 +5,7 @@ import io.split.android.client.SplitFactoryHelper.Initializer.Listener import io.split.android.client.events.EventsManagerCoordinator import io.split.android.client.events.SplitInternalEvent import io.split.android.client.lifecycle.SplitLifecycleManager +import io.split.android.client.service.CleanUpDatabaseTask import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor import io.split.android.client.service.executor.SplitTaskExecutionInfo import io.split.android.client.service.executor.SplitTaskExecutionListener 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/UserConsentManagerTest.java b/src/test/java/io/split/android/client/UserConsentManagerTest.java index b3d1f689f..8dc3a2194 100644 --- a/src/test/java/io/split/android/client/UserConsentManagerTest.java +++ b/src/test/java/io/split/android/client/UserConsentManagerTest.java @@ -2,6 +2,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import org.junit.Assert; import org.junit.Before; @@ -31,6 +32,8 @@ public class UserConsentManagerTest { @Mock private EventsTracker mEventsTracker; @Mock + private SplitFactoryImpl.EventsTrackerProvider mEventsTrackerProvider; + @Mock private ImpressionManager mImpressionManager; private SplitTaskExecutor mTaskExecutor; @@ -41,6 +44,7 @@ public class UserConsentManagerTest { public void setup() { mTaskExecutor = new SplitTaskExecutorStub(); MockitoAnnotations.openMocks(this); + when(mEventsTrackerProvider.getEventsTracker()).thenReturn(mEventsTracker); } @Test @@ -137,7 +141,7 @@ private void createUserConsentManager(UserConsent status) { mManager = new UserConsentManagerImpl(mSplitConfig, mImpressionsStorage, mEventsStorage, mSyncManager, - mEventsTracker, + mEventsTrackerProvider, mImpressionManager, mTaskExecutor); } 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/DebugStrategyTest.kt b/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt index 25c9919d9..333a16b8d 100644 --- a/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt +++ b/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt @@ -157,4 +157,15 @@ class DebugStrategyTest { eq(impressionsSyncHelper) ) } + + @Test + fun `impressions observer is not called when impression has properties`() { + val impression = createUniqueImpression(time = 1000000000L, propertiesJson = "{\"key\":\"value\"}") + val impression2 = createUniqueImpression(time = 1000000000L, propertiesJson = "{\"key\":\"value\"}") + + strategy.apply(impression) + strategy.apply(impression2) + + verify(impressionsObserver, times(0)).testAndSet(impression) + } } 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..c26932954 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 @@ -83,7 +83,8 @@ class NoneStrategyTest { fun createUniqueImpression( split: String = UUID.randomUUID().toString(), - time: Long = 100L + time: Long = 100L, + propertiesJson: String? = null ): Impression = Impression( "key", @@ -93,5 +94,6 @@ fun createUniqueImpression( time, "default rule", 999L, - null + null, + propertiesJson ) diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt b/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt index 7e05fae92..198e4d43b 100644 --- a/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt +++ b/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt @@ -202,4 +202,15 @@ class OptimizedStrategyTest { eq(impressionsSyncHelper) ) } + + @Test + fun `impressions observer is not called when impression has properties`() { + val impression = createUniqueImpression(time = 1000000000L, propertiesJson = "{\"key\":\"value\"}") + val impression2 = createUniqueImpression(time = 1000000000L, propertiesJson = "{\"key\":\"value\"}") + + strategy.apply(impression) + strategy.apply(impression2) + + verify(impressionsObserver, times(0)).testAndSet(impression) + } } diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/UtilsTest.java b/src/test/java/io/split/android/client/service/impressions/strategy/UtilsTest.java new file mode 100644 index 000000000..f60c6fa0b --- /dev/null +++ b/src/test/java/io/split/android/client/service/impressions/strategy/UtilsTest.java @@ -0,0 +1,42 @@ +package io.split.android.client.service.impressions.strategy; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import androidx.annotation.NonNull; + +import org.junit.Test; + +import io.split.android.client.impressions.Impression; + +public class UtilsTest { + + @Test + public void hasPropertiesReturnsTrueWhenPropertiesAreNotNUll() { + assertTrue(Utils.hasProperties(getImpression("{\"key\":\"value\"}"))); + } + + @Test + public void hasPropertiesReturnsFalseWhenPropertiesAreNull() { + assertFalse(Utils.hasProperties(getImpression(null))); + } + + @Test + public void hasPropertiesReturnsFalseWhenPropertiesIsEmpty() { + assertFalse(Utils.hasProperties(getImpression(""))); + } + + @NonNull + private static Impression getImpression(String props) { + return new Impression( + "key", + "bkey", + "flag", + "on", + System.currentTimeMillis(), + "default rule", + 999L, + null, + props); + } +} diff --git a/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java b/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java index 03de536c7..0d8cbe339 100644 --- a/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java @@ -131,8 +131,7 @@ public void lowerChangeNumberThanStoredDoesNothing() { @Test public void nullPreviousChangeNumberDoesNothing() { when(mSplitsStorage.getTill()).thenReturn(1000L); - SplitsChangeNotification notification = getNewNotification(); - when(notification.getPreviousChangeNumber()).thenReturn(null); + SplitsChangeNotification notification = getNewNotification(null); mNotificationsQueue.offer(notification); verify(mSynchronizer, never()) @@ -142,8 +141,7 @@ public void nullPreviousChangeNumberDoesNothing() { @Test public void zeroPreviousChangeNumberDoesNothing() { when(mSplitsStorage.getTill()).thenReturn(1000L); - SplitsChangeNotification notification = getNewNotification(); - when(notification.getPreviousChangeNumber()).thenReturn(0L); + SplitsChangeNotification notification = getNewNotification(0L); mNotificationsQueue.offer(notification); verify(mSynchronizer, never()) @@ -167,7 +165,7 @@ public void newNotificationSubmitsTaskInExecutor() throws InterruptedException { long changeNumber = 1000L; byte[] bytes = TEST_SPLIT.getBytes(); SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); - SplitsChangeNotification notification = getNewNotification(); + SplitsChangeNotification notification = getNewNotification(2000L); CompressionUtil mockCompressor = mock(CompressionUtil.class); when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); @@ -193,7 +191,7 @@ public void synchronizeSplitsIsCalledOnSynchronizerWhenTaskFails() throws Interr long changeNumber = 1000L; SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); - SplitsChangeNotification notification = getNewNotification(); + SplitsChangeNotification notification = getNewNotification(2000L); CompressionUtil mockCompressor = mock(CompressionUtil.class); when(updateTask.execute()).thenAnswer(invocation -> SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC)); @@ -216,7 +214,7 @@ public void synchronizeSplitsIsCalledOnSynchronizerWhenParsingFails() throws Int long changeNumber = 1000L; SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); - SplitsChangeNotification notification = getNewNotification(); + SplitsChangeNotification notification = getNewNotification(2000L); CompressionUtil mockCompressor = mock(CompressionUtil.class); when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); @@ -236,7 +234,7 @@ public void synchronizeSplitsIsCalledOnSynchronizerWhenParsingFails() throws Int public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToException() throws InterruptedException { long changeNumber = 1000L; SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); - SplitsChangeNotification notification = getNewNotification(); + SplitsChangeNotification notification = getNewNotification(2000L); CompressionUtil mockCompressor = mock(CompressionUtil.class); when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); @@ -256,7 +254,7 @@ public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToEx public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToNullDecompressor() throws InterruptedException { long changeNumber = 1000L; SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); - SplitsChangeNotification notification = getNewNotification(); + SplitsChangeNotification notification = getNewNotification(2000L); when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); when(mSplitsStorage.getTill()).thenReturn(changeNumber); @@ -274,7 +272,7 @@ public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToNu public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToNullDecompressedBytes() throws InterruptedException { long changeNumber = 1000L; SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); - SplitsChangeNotification notification = getNewNotification(); + SplitsChangeNotification notification = getNewNotification(2000L); CompressionUtil mockCompressor = mock(CompressionUtil.class); when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); @@ -294,7 +292,7 @@ public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToNu public void synchronizeSplitsIsCalledOnSynchronizerWhenDecompressingFailsDueToFailedBase64Decoding() throws InterruptedException { long changeNumber = 1000L; SplitInPlaceUpdateTask updateTask = mock(SplitInPlaceUpdateTask.class); - SplitsChangeNotification notification = getNewNotification(); + SplitsChangeNotification notification = getNewNotification(2000L); CompressionUtil mockCompressor = mock(CompressionUtil.class); when(mSplitTaskFactory.createSplitsUpdateTask(any(), anyLong())).thenReturn(updateTask); @@ -323,18 +321,18 @@ private void initWorkerWithStubExecutor() { mWorker.start(); } - private static SplitsChangeNotification getLegacyNotification() { + private synchronized static SplitsChangeNotification getLegacyNotification() { SplitsChangeNotification mock = mock(SplitsChangeNotification.class); when(mock.getChangeNumber()).thenReturn(1000L); return mock; } - private static SplitsChangeNotification getNewNotification() { + private synchronized static SplitsChangeNotification getNewNotification(Long changeNumber) { SplitsChangeNotification mock = mock(SplitsChangeNotification.class); when(mock.getCompressionType()).thenReturn(CompressionType.ZLIB); when(mock.getData()).thenReturn(TEST_SPLIT); when(mock.getPreviousChangeNumber()).thenReturn(1000L); - when(mock.getChangeNumber()).thenReturn(2000L); + when(mock.getChangeNumber()).thenReturn(changeNumber == null ? 0 : changeNumber); return mock; } } diff --git a/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt b/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt index b89da1649..47faababc 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt +++ b/src/test/java/io/split/android/client/service/synchronizer/RolloutCacheManagerTest.kt @@ -134,15 +134,6 @@ class RolloutCacheManagerTest { verify(mGeneralInfoStorage, times(0)).setRolloutCacheLastClearTimestamp(anyLong()) } - @Test - fun `validateCache executes cleanUpDatabaseTask`() { - mRolloutCacheManager = getCacheManager(10, false) - - mRolloutCacheManager.validateCache(mock(SplitTaskExecutionListener::class.java)) - - verify(mCleanUpDatabaseTask).execute() - } - @Test fun `validateCache executes encryptionMigrationTask`() { mRolloutCacheManager = getCacheManager(10, false) @@ -177,7 +168,7 @@ class RolloutCacheManagerTest { } private fun getCacheManager(expiration: Int, clearOnInit: Boolean): RolloutCacheManager { - return RolloutCacheManagerImpl(mGeneralInfoStorage, RolloutCacheConfiguration.builder().expirationDays(expiration).clearOnInit(clearOnInit).build(), mCleanUpDatabaseTask, mEncryptionMigrationTask, mSplitsCache, mSegmentsCache) + return RolloutCacheManagerImpl(mGeneralInfoStorage, RolloutCacheConfiguration.builder().expirationDays(expiration).clearOnInit(clearOnInit).build(), mEncryptionMigrationTask, mSplitsCache, mSegmentsCache) } private fun createMockedTimestamp(period: Long): Long { diff --git a/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java b/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java index 20801d90b..7ee312106 100644 --- a/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java +++ b/src/test/java/io/split/android/client/shared/SplitClientContainerImplTest.java @@ -321,8 +321,7 @@ private SplitClientContainerImpl getSplitClientContainer(String mDefaultMatching mConfig, mSplitClientFactory, mClientComponentsRegister, - mWorkManagerWrapper, - mEventsTracker + mWorkManagerWrapper ); } diff --git a/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt b/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt index 6d5f2b77c..af0b3609a 100644 --- a/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt +++ b/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt @@ -4,6 +4,8 @@ import io.split.android.client.service.executor.SplitTaskExecutionStatus import io.split.android.client.service.executor.SplitTaskType import io.split.android.client.storage.db.EventDao import io.split.android.client.storage.db.EventEntity +import io.split.android.client.storage.db.GeneralInfoDao +import io.split.android.client.storage.db.GeneralInfoEntity import io.split.android.client.storage.db.ImpressionDao import io.split.android.client.storage.db.ImpressionEntity import io.split.android.client.storage.db.ImpressionsCountDao @@ -12,8 +14,6 @@ import io.split.android.client.storage.db.MyLargeSegmentDao import io.split.android.client.storage.db.MyLargeSegmentEntity import io.split.android.client.storage.db.MySegmentDao import io.split.android.client.storage.db.MySegmentEntity -import io.split.android.client.storage.db.SegmentDao -import io.split.android.client.storage.db.SegmentEntity import io.split.android.client.storage.db.SplitDao import io.split.android.client.storage.db.SplitEntity import io.split.android.client.storage.db.SplitRoomDatabase @@ -69,6 +69,9 @@ class ApplyCipherTaskTest { @Mock private lateinit var attributesDao: AttributesDao + @Mock + private lateinit var generalInfoDao: GeneralInfoDao + private lateinit var applyCipherTask: ApplyCipherTask @Before @@ -82,6 +85,7 @@ class ApplyCipherTaskTest { `when`(splitDatabase.impressionsCountDao()).thenReturn(impressionsCountDao) `when`(splitDatabase.uniqueKeysDao()).thenReturn(uniqueKeysDao) `when`(splitDatabase.attributesDao()).thenReturn(attributesDao) + `when`(splitDatabase.generalInfoDao()).thenReturn(generalInfoDao) `when`(fromCipher.decrypt(anyString())).thenAnswer { invocation -> "decrypted_${invocation.arguments[0]}" } `when`(toCipher.encrypt(anyString())).thenAnswer { invocation -> "encrypted_${invocation.arguments[0]}" } @@ -123,6 +127,38 @@ class ApplyCipherTaskTest { verify(splitDao).update("name2", "encrypted_decrypted_name2", "encrypted_decrypted_body2") } + @Test + fun `traffic types are migrated`() { + `when`(generalInfoDao.getByName(GeneralInfoEntity.TRAFFIC_TYPES_MAP)).thenReturn( + GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, "trafficTypesMap") + ) + + applyCipherTask.execute() + + verify(generalInfoDao).getByName(GeneralInfoEntity.TRAFFIC_TYPES_MAP) + verify(fromCipher).decrypt("trafficTypesMap") + verify(toCipher).encrypt("decrypted_trafficTypesMap") + verify(generalInfoDao).update(argThat { + it.stringValue.equals("encrypted_decrypted_trafficTypesMap") + }) + } + + @Test + fun `flag sets are migrated`() { + `when`(generalInfoDao.getByName(GeneralInfoEntity.FLAG_SETS_MAP)).thenReturn( + GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, "flagSetsMap") + ) + + applyCipherTask.execute() + + verify(generalInfoDao).getByName(GeneralInfoEntity.FLAG_SETS_MAP) + verify(fromCipher).decrypt("flagSetsMap") + verify(toCipher).encrypt("decrypted_flagSetsMap") + verify(generalInfoDao).update(argThat { + it.stringValue.equals("encrypted_decrypted_flagSetsMap") + }) + } + @Test fun `segments are migrated`() { `when`(mySegmentDao.all).thenReturn( diff --git a/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java b/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java index 9fc67ff9d..0d0e3270a 100644 --- a/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java +++ b/src/test/java/io/split/android/client/storage/splits/SplitEntityToSplitTransformerTest.java @@ -2,12 +2,10 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; @@ -15,10 +13,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import io.split.android.client.dtos.Split; -import io.split.android.client.service.executor.parallel.SplitDeferredTaskItem; import io.split.android.client.service.executor.parallel.SplitParallelTaskExecutor; import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.db.SplitEntity; @@ -37,30 +36,19 @@ public void setUp() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(2); when(mSplitCipher.decrypt(any())).then((Answer) invocation -> (String) invocation.getArguments()[0]); - mConverter = new SplitEntityToSplitTransformer(mSplitTaskExecutor, mSplitCipher); - } - - @Test - public void tasksAreCreatedAccordingToTheAmountOfThreadsAvailable() { - ArgumentCaptor>>> argumentCaptor = ArgumentCaptor.forClass(List.class); - - when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(4); - List mockEntities = getMockEntities(65); - - int expectedNumberOfLists = 5; - - mConverter.transform(mockEntities); - - verify(mSplitTaskExecutor).execute(argumentCaptor.capture()); - assertEquals(expectedNumberOfLists, argumentCaptor.getValue().size()); + mConverter = new SplitEntityToSplitTransformer(mSplitCipher); } @Test public void amountOfSplitsEqualsAmountOfEntities() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(4); List mockEntities = getMockEntities(3); + Map map = new HashMap<>(); + for (SplitEntity entity : mockEntities) { + map.put(entity.getName(), entity); + } - List splits = mConverter.transform(mockEntities); + List splits = mConverter.transform(map); assertEquals(3, splits.size()); } @@ -69,12 +57,16 @@ public void amountOfSplitsEqualsAmountOfEntities() { public void amountOfSplitsEqualsAmountOfEntitiesWhenParallel() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(2); List mockEntities = getMockEntities(3); + Map map = new HashMap<>(); + for (SplitEntity entity : mockEntities) { + map.put(entity.getName(), entity); + } when(mSplitTaskExecutor.execute(any())).thenReturn( Arrays.asList(Collections.singletonList(new Split()), Collections.singletonList(new Split()), Collections.singletonList(new Split()))); - List splits = mConverter.transform(mockEntities); + List splits = mConverter.transform(map); assertEquals(3, splits.size()); } @@ -83,7 +75,7 @@ public void amountOfSplitsEqualsAmountOfEntitiesWhenParallel() { public void transformingNullReturnsEmptyList() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(4); - List splits = mConverter.transform(null); + List splits = mConverter.transform((Map) null); assertEquals(0, splits.size()); } diff --git a/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java b/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java index c2a1903aa..3df044115 100644 --- a/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java +++ b/src/test/java/io/split/android/client/storage/splits/SplitToSplitEntityTransformerTest.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import io.split.android.client.dtos.Split; import io.split.android.client.service.executor.parallel.SplitDeferredTaskItem; @@ -85,7 +86,7 @@ public void amountOfSplitsEqualsAmountOfEntitiesWhenParallel() { public void transformingNullReturnsEmptyList() { when(mSplitTaskExecutor.getAvailableThreads()).thenReturn(4); - List splits = mConverter.transform(null); + List splits = mConverter.transform((Map) null); assertEquals(0, splits.size()); } diff --git a/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java b/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java index ea28aff03..38c07f3c0 100644 --- a/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java +++ b/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java @@ -20,7 +20,10 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import io.split.android.client.dtos.Split; import io.split.android.client.storage.cipher.SplitCipher; @@ -28,6 +31,7 @@ import io.split.android.client.storage.db.GeneralInfoEntity; import io.split.android.client.storage.db.SplitDao; import io.split.android.client.storage.db.SplitEntity; +import io.split.android.client.storage.db.SplitQueryDao; import io.split.android.client.storage.db.SplitRoomDatabase; public class SqLitePersistentSplitsStorageTest { @@ -44,6 +48,8 @@ public class SqLitePersistentSplitsStorageTest { private SplitCipher mCipher; private SqLitePersistentSplitsStorage mStorage; private AutoCloseable mAutoCloseable; + private final Map> mFlagSets = new HashMap<>(); + private final Map mTrafficTypes = new HashMap<>(); @Before public void setUp() { @@ -70,12 +76,20 @@ public void tearDown() throws Exception { @Test public void getAllUsesTransformer() { List mockEntities = getMockEntities(); + Map map = new HashMap<>(); + for (SplitEntity entity : mockEntities) { + map.put(entity.getName(), entity); + } + when(mSplitDao.getAll()).thenReturn(mockEntities); when(mDatabase.splitDao()).thenReturn(mSplitDao); + SplitQueryDao queryDao = mock(SplitQueryDao.class); + when(queryDao.getAllAsMap()).thenReturn(map); + when(mDatabase.getSplitQueryDao()).thenReturn(queryDao); mStorage.getAll(); - verify(mEntityToSplitTransformer).transform(mockEntities); + verify(mEntityToSplitTransformer).transform(map); } @Test @@ -129,7 +143,7 @@ public void updateRemovesEncryptedSplitNames() { ProcessedSplitChange change = new ProcessedSplitChange(activeSplits, archivedSplits, changeNumber, timestamp); when(mCipher.encrypt(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0) + "_encrypted"); - mStorage.update(change); + mStorage.update(change, mTrafficTypes, mFlagSets); verify(mSplitDao).delete(argThat(list -> list.contains("split-1_encrypted") && list.contains("split-2_encrypted") && list.contains("split-3_encrypted") && list.size() == 3)); } @@ -153,14 +167,14 @@ public void updateForSplitChangeUsesTransformer() { ProcessedSplitChange change = new ProcessedSplitChange(activeSplits, archivedSplits, changeNumber, timestamp); when(mCipher.encrypt(any())).thenAnswer((Answer) invocation -> invocation.getArgument(0) + "_encrypted"); - mStorage.update(change); + mStorage.update(change, mTrafficTypes, mFlagSets); verify(mSplitToSplitEntityTransformer).transform(activeSplits); } @Test public void updatingNullSplitChangeDoesNotInteractWithDatabase() { - mStorage.update((ProcessedSplitChange) null); + mStorage.update((ProcessedSplitChange) null, mTrafficTypes, mFlagSets); verifyNoInteractions(mSplitToSplitEntityTransformer); verifyNoInteractions(mCipher);