diff --git a/src/androidTest/assets/split_changes_imp_toggle.json b/src/androidTest/assets/split_changes_imp_toggle.json new file mode 100644 index 000000000..d1f7196ce --- /dev/null +++ b/src/androidTest/assets/split_changes_imp_toggle.json @@ -0,0 +1,124 @@ +{ + "splits": [ + { + "trafficTypeName": "user", + "name": "tracked", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "trackImpressions": true, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "free", + "size": 100 + }, + { + "treatment": "conta", + "size": 0 + } + ], + "label": "in segment new_segment" + } + ] + }, + { + "trafficTypeName": "user", + "name": "not_tracked", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "trackImpressions": false, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "free", + "size": 100 + }, + { + "treatment": "conta", + "size": 0 + } + ], + "label": "in segment new_segment" + } + ] + } + ], + "since": 1506703262916, + "till": 1506703262916 +} diff --git a/src/androidTest/java/fake/SyncManagerStub.java b/src/androidTest/java/fake/SyncManagerStub.java index de24ad9cb..179a8461f 100644 --- a/src/androidTest/java/fake/SyncManagerStub.java +++ b/src/androidTest/java/fake/SyncManagerStub.java @@ -1,7 +1,7 @@ package fake; import io.split.android.client.dtos.Event; -import io.split.android.client.impressions.Impression; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.service.synchronizer.SyncManager; import io.split.android.client.shared.UserConsent; @@ -42,6 +42,6 @@ public void pushEvent(Event event) { } @Override - public void pushImpression(Impression impression) { + public void pushImpression(DecoratedImpression impression) { } } diff --git a/src/androidTest/java/fake/SynchronizerSpyImpl.java b/src/androidTest/java/fake/SynchronizerSpyImpl.java index 2203f3913..81a1c8fdd 100644 --- a/src/androidTest/java/fake/SynchronizerSpyImpl.java +++ b/src/androidTest/java/fake/SynchronizerSpyImpl.java @@ -5,7 +5,7 @@ import io.split.android.client.api.Key; import io.split.android.client.dtos.Event; -import io.split.android.client.impressions.Impression; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.service.synchronizer.Synchronizer; import io.split.android.client.service.synchronizer.SynchronizerSpy; import io.split.android.client.service.synchronizer.attributes.AttributesSynchronizer; @@ -88,7 +88,7 @@ public void pushEvent(Event event) { } @Override - public void pushImpression(Impression impression) { + public void pushImpression(DecoratedImpression impression) { mSynchronizer.pushImpression(impression); } diff --git a/src/androidTest/java/helper/DatabaseHelper.java b/src/androidTest/java/helper/DatabaseHelper.java index e71be3479..57646fcd2 100644 --- a/src/androidTest/java/helper/DatabaseHelper.java +++ b/src/androidTest/java/helper/DatabaseHelper.java @@ -17,9 +17,12 @@ public static boolean removeDatabaseFile(String name) { } public static SplitRoomDatabase getTestDatabase(Context context) { - return Room.inMemoryDatabaseBuilder(context, SplitRoomDatabase.class) + SplitRoomDatabase database = Room.inMemoryDatabaseBuilder(context, SplitRoomDatabase.class) .fallbackToDestructiveMigration() .allowMainThreadQueries() .build(); + + database.clearAllTables(); + return database; } } diff --git a/src/androidTest/java/helper/IntegrationHelper.java b/src/androidTest/java/helper/IntegrationHelper.java index 528f730fa..6144c6a7b 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/src/androidTest/java/helper/IntegrationHelper.java @@ -442,5 +442,9 @@ public interface StreamingResponseClosure { public static class ServicePath { public static final String MEMBERSHIPS = "memberships"; public static final String SPLIT_CHANGES = "splitChanges"; + public static final String EVENTS = "events"; + public static final String UNIQUE_KEYS = "keys/cs"; + public static final String COUNT = "testImpressions/count"; + public static final String IMPRESSIONS = "testImpressions/bulk"; } } diff --git a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java index f7f9c6127..30b748d0d 100644 --- a/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java +++ b/src/androidTest/java/tests/integration/streaming/CleanUpDatabaseTest.java @@ -1,7 +1,5 @@ package tests.integration.streaming; -import static java.lang.Thread.sleep; - import android.content.Context; import androidx.core.util.Pair; @@ -140,7 +138,7 @@ public void testCleanUp() throws IOException, InterruptedException { .streamingEnabled(true) .impressionsRefreshRate(999999999) .eventFlushInterval(99999999) - .logLevel(SplitLogLevel.DEBUG) + .logLevel(SplitLogLevel.VERBOSE) .trafficType("account") .build(); @@ -155,11 +153,12 @@ public void testCleanUp() throws IOException, InterruptedException { mClient.on(SplitEvent.SDK_READY, readyTask); latch.await(40, TimeUnit.SECONDS); - - // wait to allow cleanup to run - sleep(5000); + Thread.sleep(1000); // Load all records again after cleanup + List remainingKeys = mUniqueKeysDao.getBy(0, StorageRecordStatus.ACTIVE, 10); + remainingKeys.addAll(mUniqueKeysDao.getBy(0, StorageRecordStatus.DELETED, 10)); + List remainingEvents = mEventDao.getBy(0, StorageRecordStatus.ACTIVE, 10); remainingEvents.addAll(mEventDao.getBy(0, StorageRecordStatus.DELETED, 10)); @@ -169,8 +168,6 @@ public void testCleanUp() throws IOException, InterruptedException { List remainingCounts = mImpressionsCountDao.getBy(0, StorageRecordStatus.ACTIVE, 10); remainingCounts.addAll(mImpressionsCountDao.getBy(0, StorageRecordStatus.DELETED, 10)); - List remainingKeys = mUniqueKeysDao.getBy(0, StorageRecordStatus.ACTIVE, 10); - remainingKeys.addAll(mUniqueKeysDao.getBy(0, StorageRecordStatus.DELETED, 10)); List remainingImpressionsObserverCacheEntities = mImpressionsObserverCacheDao.getAll(3); @@ -184,7 +181,6 @@ public void testCleanUp() throws IOException, InterruptedException { Assert.assertEquals(1, remainingCounts.size()); Assert.assertEquals(1, remainingKeys.size()); Assert.assertEquals(1, remainingImpressionsObserverCacheEntities.size()); - } private EventEntity createEventEntity(long createdAt, int status, String name) { @@ -258,7 +254,7 @@ public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { } else if (uri.getPath().contains("/bulk")) { return createResponse(500, ""); } else { - return new HttpResponseMock(200); + return new HttpResponseMock(404); } } diff --git a/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java new file mode 100644 index 000000000..8d3f4c352 --- /dev/null +++ b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java @@ -0,0 +1,287 @@ +package tests.integration.toggle; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static helper.IntegrationHelper.buildFactory; +import static helper.IntegrationHelper.emptyAllSegments; +import static helper.IntegrationHelper.getSinceFromUri; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.google.common.reflect.TypeToken; + +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import helper.DatabaseHelper; +import helper.FileHelper; +import helper.IntegrationHelper; +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.SplitManager; +import io.split.android.client.api.SplitView; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TestImpressions; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.service.impressions.ImpressionsCount; +import io.split.android.client.service.impressions.ImpressionsMode; +import io.split.android.client.service.impressions.unique.MTK; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; +import io.split.android.client.utils.logger.SplitLogLevel; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import tests.integration.shared.TestingHelper; + +public class ImpressionsToggleTest { + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private CountDownLatch mRequestCountdownLatch; + private MockWebServer mWebServer; + + private final AtomicReference mCountBody = new AtomicReference<>(null); + private final AtomicReference mImpressionsBody = new AtomicReference<>(null); + private final AtomicReference mUniqueKeysBody = new AtomicReference<>(null); + + private CountDownLatch mCountLatch; + private CountDownLatch mImpressionsLatch; + private CountDownLatch mUniqueKeysLatch; + + @Before + public void setUp() { + setupServer(); + mRequestCountdownLatch = new CountDownLatch(1); + mCountBody.set(null); + mImpressionsBody.set(null); + mUniqueKeysBody.set(null); + + mCountLatch = new CountDownLatch(1); + mImpressionsLatch = new CountDownLatch(1); + mUniqueKeysLatch = new CountDownLatch(1); + } + + @Test + public void managerContainsProperty() throws InterruptedException { + SplitFactory splitFactory = getReadyFactory(ImpressionsMode.OPTIMIZED); + + SplitManager manager = splitFactory.manager(); + List splits = manager.splits(); + + assertTrue(manager.split("tracked").trackImpressions); + assertFalse(manager.split("not_tracked").trackImpressions); + assertEquals(2, splits.size()); + } + + @Test + public void testNoneMode() throws InterruptedException { + // 1. Initialize SDK in impressions NONE mode + SplitFactory splitFactory = getReadyFactory(ImpressionsMode.NONE); + + // 2. Fetch splitChanges with both flags with trackImpressions true & false + SplitClient client = splitFactory.client(); + client.getTreatment("tracked"); + client.getTreatment("not_tracked"); + Thread.sleep(200); + + // 3. Flush + client.flush(); + + // 4. Wait for requests to be sent + mUniqueKeysLatch.await(5, TimeUnit.SECONDS); + mCountLatch.await(5, TimeUnit.SECONDS); + boolean impressionsAwait = mImpressionsLatch.await(3, TimeUnit.SECONDS); + + // 5. Verify request bodies + verifyOnlyNone(impressionsAwait); + } + + @Test + public void testDebugMode() throws InterruptedException { + // 1. Initialize SDK in impressions DEBUG mode + SplitFactory splitFactory = getReadyFactory(ImpressionsMode.DEBUG); + + // 2. Fetch splitChanges with both flags with trackImpressions true & false + SplitClient client = splitFactory.client(); + client.getTreatment("tracked"); + client.getTreatment("not_tracked"); + Thread.sleep(500); + + // 3. Flush + client.flush(); + + // 4. Wait for requests to be sent + mUniqueKeysLatch.await(5, TimeUnit.SECONDS); + mCountLatch.await(5, TimeUnit.SECONDS); + mImpressionsLatch.await(5, TimeUnit.SECONDS); + + // 5. Verify request bodies + verify(); + } + + @Test + public void testOptimizedMode() throws InterruptedException { + // 1. Initialize SDK in impressions OPTIMIZED mode + SplitFactory splitFactory = getReadyFactory(ImpressionsMode.OPTIMIZED); + + // 2. Fetch splitChanges with both flags with trackImpressions true & false + SplitClient client = splitFactory.client(); + client.getTreatment("tracked"); + client.getTreatment("not_tracked"); + Thread.sleep(200); + + // 3. Flush + client.flush(); + + // 4. Wait for requests to be sent + mUniqueKeysLatch.await(5, TimeUnit.SECONDS); + mCountLatch.await(5, TimeUnit.SECONDS); + mImpressionsLatch.await(5, TimeUnit.SECONDS); + + // 5. Verify request bodies + verify(); + } + + private void verifyOnlyNone(boolean impressionsAwait) { + // a. Counts + String countBody = mCountBody.get(); + ImpressionsCount impressionsCount = Json.fromJson(countBody, ImpressionsCount.class); + assertEquals(2, impressionsCount.perFeature.size()); + assertTrue(impressionsCount.perFeature.stream().anyMatch(c -> c.feature.equals("tracked") && c.count == 1)); + assertTrue(impressionsCount.perFeature.stream().anyMatch(c -> c.feature.equals("not_tracked") && c.count == 1)); + + // b. MTKs + String uniqueBody = mUniqueKeysBody.get(); + MTK mtk = Json.fromJson(uniqueBody, MTK.class); + assertEquals(1, mtk.getKeys().size()); + assertTrue(mtk.getKeys().get(0).getFeatures().containsAll(Arrays.asList("not_tracked", "tracked"))); + assertEquals("CUSTOMER_ID", mtk.getKeys().get(0).getKey()); + + // c. Impressions (no impressions) + assertNull(mImpressionsBody.get()); + assertFalse(impressionsAwait); + } + + private void verify() { + // a. Counts + String countBody = mCountBody.get(); + ImpressionsCount impressionsCount = Json.fromJson(countBody, ImpressionsCount.class); + assertEquals(1, impressionsCount.perFeature.size()); + assertEquals("not_tracked", impressionsCount.perFeature.get(0).feature); + + // b. MTKs + String uniqueBody = mUniqueKeysBody.get(); + MTK mtk = Json.fromJson(uniqueBody, MTK.class); + assertEquals(1, mtk.getKeys().size()); + assertTrue(mtk.getKeys().get(0).getFeatures().containsAll(Arrays.asList("not_tracked"))); + assertEquals(1, mtk.getKeys().get(0).getFeatures().size()); + assertNotNull(uniqueBody); + + // c. Impressions + String impressionsBody = mImpressionsBody.get(); + Type listTypeToken = new TypeToken>(){}.getType(); + List impressions = Json.fromJson(impressionsBody, listTypeToken); + assertEquals(1, impressions.size()); + assertEquals("tracked", impressions.get(0).testName); + } + + private SplitFactory getReadyFactory(ImpressionsMode impressionsMode) throws InterruptedException { + SplitFactory splitFactory = getSplitFactory(impressionsMode); + CountDownLatch latch = new CountDownLatch(1); + splitFactory.client().on(SplitEvent.SDK_READY, new TestingHelper.TestEventTask(latch)); + mRequestCountdownLatch.countDown(); + + boolean await = latch.await(5, TimeUnit.SECONDS); + + if (!await) { + fail("Client was not ready"); + } + + return splitFactory; + } + + private SplitFactory getSplitFactory(ImpressionsMode impressionsMode) { + final String url = mWebServer.url("/").url().toString(); + ServiceEndpoints endpoints = ServiceEndpoints.builder() + .apiEndpoint(url) + .eventsEndpoint(url) + .telemetryServiceEndpoint(url) + .build(); + SplitClientConfig config = new SplitClientConfig.Builder() + .serviceEndpoints(endpoints) + .impressionsMode(impressionsMode) + .streamingEnabled(false) + .featuresRefreshRate(9999) + .segmentsRefreshRate(9999) + .impressionsRefreshRate(9999) + .logLevel(SplitLogLevel.VERBOSE) + .streamingEnabled(false) + .build(); + + return buildFactory(IntegrationHelper.dummyApiKey(), IntegrationHelper.dummyUserKey(), config, + mContext, null, DatabaseHelper.getTestDatabase(mContext)); + } + + private void setupServer() { + mWebServer = new MockWebServer(); + + final Dispatcher dispatcher = new Dispatcher() { + + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + mRequestCountdownLatch.await(); + Logger.e("Path is: " + request.getPath()); + if (request.getPath().contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { + return new MockResponse().setResponseCode(200).setBody(emptyAllSegments()); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.SPLIT_CHANGES)) { + String sinceFromUri = getSinceFromUri(request.getRequestUrl().uri()); + if (sinceFromUri.equals("-1")) { + return new MockResponse().setResponseCode(200).setBody(loadSplitChanges()); + } else { + return new MockResponse().setResponseCode(200) + .setBody(IntegrationHelper.emptySplitChanges(1506703262916L, 1506703262916L)); + } + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.COUNT)) { + mCountBody.set(request.getBody().readUtf8()); + mCountLatch.countDown(); + return new MockResponse().setResponseCode(200); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.IMPRESSIONS)) { + mImpressionsBody.set(request.getBody().readUtf8()); + mImpressionsLatch.countDown(); + return new MockResponse().setResponseCode(200); + } else if (request.getPath().contains("/" + IntegrationHelper.ServicePath.UNIQUE_KEYS)) { + mUniqueKeysBody.set(request.getBody().readUtf8()); + mUniqueKeysLatch.countDown(); + return new MockResponse().setResponseCode(200); + } else { + return new MockResponse().setResponseCode(404); + } + } + }; + mWebServer.setDispatcher(dispatcher); + } + + private String loadSplitChanges() { + FileHelper fileHelper = new FileHelper(); + String change = fileHelper.loadFileContent(mContext, "split_changes_imp_toggle.json"); + SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + parsedChange.since = parsedChange.till; + return Json.toJson(parsedChange); + } +} diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt index c86df4698..16b224947 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt @@ -199,7 +199,8 @@ class UserConsentModeDebugTest { mEventDao = splitRoomDatabase.eventDao() val dispatcher: HttpResponseMockDispatcher = buildDispatcher() val config = TestableSplitConfigBuilder().ready(30000) - .trafficType("client") + .trafficType("account") + .enableDebug() .impressionsMode(ImpressionsMode.DEBUG) .impressionsRefreshRate(3) .eventFlushInterval(3) @@ -224,7 +225,7 @@ class UserConsentModeDebugTest { return HttpStreamResponseMock(200, null) } - override fun getResponse(uri: URI, method: HttpMethod, body: String): HttpResponseMock { + override fun getResponse(uri: URI, method: HttpMethod, body: String?): HttpResponseMock { println(uri.path) return if (uri.path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { HttpResponseMock(200, IntegrationHelper.emptyAllSegments()) diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt index eac6083a6..fd8e7b4cc 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt @@ -72,8 +72,8 @@ class UserConsentModeNoneTest { mKeysLatch?.await(10, TimeUnit.SECONDS) mCountLatch?.await(10, TimeUnit.SECONDS) - Assert.assertTrue(mKeysPosted) Assert.assertTrue(mCountPosted) + Assert.assertTrue(mKeysPosted) } @Test @@ -202,11 +202,12 @@ class UserConsentModeNoneTest { mCountDao = splitRoomDatabase.impressionsCountDao() val dispatcher: HttpResponseMockDispatcher = buildDispatcher() val config = TestableSplitConfigBuilder().ready(30000) - .trafficType("client") + .trafficType("account") .impressionsMode(ImpressionsMode.NONE) .impressionsRefreshRate(3) .eventFlushInterval(3) .mtkRefreshRate(3) + .enableDebug() .userConsent(userConsent) .build() @@ -228,7 +229,7 @@ class UserConsentModeNoneTest { return HttpStreamResponseMock(200, null) } - override fun getResponse(uri: URI, method: HttpMethod, body: String): HttpResponseMock { + override fun getResponse(uri: URI, method: HttpMethod, body: String?): HttpResponseMock { println(uri.path) return if (uri.path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { HttpResponseMock(200, IntegrationHelper.emptyAllSegments()) diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt index 686b3436e..ea7580626 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt @@ -209,11 +209,12 @@ class UserConsentModeOptimizedTest { mCountDao = splitRoomDatabase.impressionsCountDao() val dispatcher: HttpResponseMockDispatcher = buildDispatcher() val config = TestableSplitConfigBuilder().ready(30000) - .trafficType("client") + .trafficType("account") .impressionsMode(ImpressionsMode.OPTIMIZED) .impressionsRefreshRate(3) .impressionsCountersRefreshRate(3) .mtkRefreshRate(3) + .enableDebug() .eventFlushInterval(3) .userConsent(userConsent) .build() @@ -236,7 +237,7 @@ class UserConsentModeOptimizedTest { return HttpStreamResponseMock(200, null) } - override fun getResponse(uri: URI, method: HttpMethod, body: String): HttpResponseMock { + override fun getResponse(uri: URI, method: HttpMethod, body: String?): HttpResponseMock { println(uri.path) return if (uri.path.contains("/" + IntegrationHelper.ServicePath.MEMBERSHIPS)) { HttpResponseMock(200, IntegrationHelper.emptyAllSegments()) diff --git a/src/main/java/io/split/android/client/EvaluationResult.java b/src/main/java/io/split/android/client/EvaluationResult.java index f5ecebc80..d7f16ec4c 100644 --- a/src/main/java/io/split/android/client/EvaluationResult.java +++ b/src/main/java/io/split/android/client/EvaluationResult.java @@ -1,24 +1,33 @@ package io.split.android.client; +import androidx.annotation.VisibleForTesting; + public final class EvaluationResult { private final String mTreatment; private final String mLabel; private final Long mChangeNumber; private final String mConfigurations; + private final boolean mTrackImpression; + @VisibleForTesting public EvaluationResult(String treatment, String label) { - this(treatment, label, null); + this(treatment, label, null, null, true); + } + + public EvaluationResult(String treatment, String label, boolean trackImpression) { + this(treatment, label, null, null, trackImpression); } - EvaluationResult(String treatment, String label, Long changeNumber) { - this(treatment, label, changeNumber, null); + EvaluationResult(String treatment, String label, Long changeNumber, boolean trackImpression) { + this(treatment, label, changeNumber, null, trackImpression); } - public EvaluationResult(String treatment, String label, Long changeNumber, String configurations) { + public EvaluationResult(String treatment, String label, Long changeNumber, String configurations, boolean trackImpression) { mTreatment = treatment; mLabel = label; mChangeNumber = changeNumber; mConfigurations = configurations; + mTrackImpression = trackImpression; } public String getTreatment() { @@ -36,4 +45,8 @@ public Long getChangeNumber() { public String getConfigurations() { return mConfigurations; } + + public boolean getTrackImpression() { + return mTrackImpression; + } } diff --git a/src/main/java/io/split/android/client/EvaluatorImpl.java b/src/main/java/io/split/android/client/EvaluatorImpl.java index ea15ab796..33c0eac9b 100644 --- a/src/main/java/io/split/android/client/EvaluatorImpl.java +++ b/src/main/java/io/split/android/client/EvaluatorImpl.java @@ -28,16 +28,16 @@ public EvaluationResult getTreatment(String matchingKey, String bucketingKey, St try { ParsedSplit parsedSplit = mSplitParser.parse(mSplitsStorage.get(splitName), matchingKey); if (parsedSplit == null) { - return new EvaluationResult(Treatments.CONTROL, TreatmentLabels.DEFINITION_NOT_FOUND); + return new EvaluationResult(Treatments.CONTROL, TreatmentLabels.DEFINITION_NOT_FOUND, true); } return getTreatment(matchingKey, bucketingKey, parsedSplit, attributes); } catch (ChangeNumberExceptionWrapper ex) { Logger.e(ex, "Catch Change Number Exception"); - return new EvaluationResult(Treatments.CONTROL, TreatmentLabels.EXCEPTION, ex.changeNumber()); + return new EvaluationResult(Treatments.CONTROL, TreatmentLabels.EXCEPTION, ex.changeNumber(), true); } catch (Exception e) { Logger.e(e, "Catch All Exception"); - return new EvaluationResult(Treatments.CONTROL, TreatmentLabels.EXCEPTION); + return new EvaluationResult(Treatments.CONTROL, TreatmentLabels.EXCEPTION, true); } } @@ -52,7 +52,7 @@ public EvaluationResult getTreatment(String matchingKey, String bucketingKey, St private EvaluationResult getTreatment(String matchingKey, String bucketingKey, ParsedSplit parsedSplit, Map attributes) throws ChangeNumberExceptionWrapper { try { if (parsedSplit.killed()) { - return new EvaluationResult(parsedSplit.defaultTreatment(), TreatmentLabels.KILLED, parsedSplit.changeNumber(), configForTreatment(parsedSplit, parsedSplit.defaultTreatment())); + return new EvaluationResult(parsedSplit.defaultTreatment(), TreatmentLabels.KILLED, parsedSplit.changeNumber(), configForTreatment(parsedSplit, parsedSplit.defaultTreatment()), parsedSplit.trackImpressions()); } /* @@ -75,7 +75,7 @@ private EvaluationResult getTreatment(String matchingKey, String bucketingKey, P if (bucket > parsedSplit.trafficAllocation()) { // out of split - return new EvaluationResult(parsedSplit.defaultTreatment(), TreatmentLabels.NOT_IN_SPLIT, parsedSplit.changeNumber(), configForTreatment(parsedSplit, parsedSplit.defaultTreatment())); + return new EvaluationResult(parsedSplit.defaultTreatment(), TreatmentLabels.NOT_IN_SPLIT, parsedSplit.changeNumber(), configForTreatment(parsedSplit, parsedSplit.defaultTreatment()), parsedSplit.trackImpressions()); } } @@ -84,11 +84,11 @@ private EvaluationResult getTreatment(String matchingKey, String bucketingKey, P if (parsedCondition.matcher().match(matchingKey, bucketingKey, attributes, this)) { String treatment = Splitter.getTreatment(bk, parsedSplit.seed(), parsedCondition.partitions(), parsedSplit.algo()); - return new EvaluationResult(treatment, parsedCondition.label(), parsedSplit.changeNumber(), configForTreatment(parsedSplit, treatment)); + return new EvaluationResult(treatment, parsedCondition.label(), parsedSplit.changeNumber(), configForTreatment(parsedSplit, treatment), parsedSplit.trackImpressions()); } } - return new EvaluationResult(parsedSplit.defaultTreatment(), TreatmentLabels.DEFAULT_RULE, parsedSplit.changeNumber(), configForTreatment(parsedSplit, parsedSplit.defaultTreatment())); + return new EvaluationResult(parsedSplit.defaultTreatment(), TreatmentLabels.DEFAULT_RULE, parsedSplit.changeNumber(), configForTreatment(parsedSplit, parsedSplit.defaultTreatment()), parsedSplit.trackImpressions()); } catch (Exception e) { throw new ChangeNumberExceptionWrapper(e, parsedSplit.changeNumber()); } diff --git a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java index 0956c1788..544aab48b 100644 --- a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java @@ -17,9 +17,9 @@ import io.split.android.client.service.mysegments.MySegmentsTaskFactory; import io.split.android.client.service.synchronizer.SyncManager; import io.split.android.client.shared.SplitClientContainer; -import io.split.android.client.storage.common.SplitStorageContainer; import io.split.android.client.storage.attributes.AttributesStorage; import io.split.android.client.storage.attributes.PersistentAttributesStorage; +import io.split.android.client.storage.common.SplitStorageContainer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.TelemetrySynchronizer; import io.split.android.client.telemetry.storage.TelemetryInitProducer; @@ -43,7 +43,7 @@ public class SplitClientFactoryImpl implements SplitClientFactory { private final SplitParser mSplitParser; private final AttributesManagerFactory mAttributesManagerFactory; private final TreatmentManagerFactory mTreatmentManagerFactory; - private final ImpressionListener mCustomerImpressionListener; + private final ImpressionListener.FederatedImpressionListener mCustomerImpressionListener; private final SplitValidatorImpl mSplitValidator; private final EventsTracker mEventsTracker; @@ -57,7 +57,7 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, @NonNull ValidationMessageLogger validationLogger, @NonNull KeyValidator keyValidator, @NonNull EventsTracker eventsTracker, - @NonNull ImpressionListener customerImpressionListener, + @NonNull ImpressionListener.FederatedImpressionListener customerImpressionListener, @Nullable FlagSetsFilter flagSetsFilter) { mSplitFactory = checkNotNull(splitFactory); mClientContainer = checkNotNull(clientContainer); diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index bfb9b6bde..a0649aba1 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -39,7 +39,6 @@ import io.split.android.client.service.http.mysegments.MySegmentsFetcherFactoryImpl; import io.split.android.client.service.impressions.strategy.ImpressionStrategyConfig; import io.split.android.client.service.impressions.strategy.ImpressionStrategyProvider; -import io.split.android.client.service.impressions.strategy.ProcessStrategy; import io.split.android.client.service.mysegments.AllSegmentsResponseParser; import io.split.android.client.service.sseclient.EventStreamParser; import io.split.android.client.service.sseclient.ReconnectBackoffCounter; @@ -387,10 +386,10 @@ public StreamingComponents buildStreamingComponents(@NonNull SplitTaskExecutor s syncGuardian); } - public ProcessStrategy getImpressionStrategy(SplitTaskExecutor splitTaskExecutor, - SplitTaskFactory splitTaskFactory, - SplitStorageContainer splitStorageContainer, - SplitClientConfig config) { + public ImpressionStrategyProvider getImpressionStrategyProvider(SplitTaskExecutor splitTaskExecutor, + SplitTaskFactory splitTaskFactory, + SplitStorageContainer splitStorageContainer, + SplitClientConfig config) { return new ImpressionStrategyProvider(splitTaskExecutor, splitStorageContainer, splitTaskFactory, @@ -402,8 +401,7 @@ public ProcessStrategy getImpressionStrategy(SplitTaskExecutor splitTaskExecutor config.impressionsCounterRefreshRate(), config.mtkRefreshRate(), config.userConsent() == UserConsent.GRANTED, - config.impressionsDedupeTimeInterval())) - .getStrategy(config.impressionsMode()); + config.impressionsDedupeTimeInterval())); } @Nullable diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index 34d3abd63..32ea3963c 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -20,6 +20,7 @@ import io.split.android.client.events.EventsManagerCoordinator; import io.split.android.client.factory.FactoryMonitor; import io.split.android.client.factory.FactoryMonitorImpl; +import io.split.android.client.impressions.DecoratedImpressionListener; import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.impressions.SyncImpressionListener; import io.split.android.client.lifecycle.SplitLifecycleManager; @@ -32,8 +33,10 @@ import io.split.android.client.service.executor.SplitTaskExecutorImpl; import io.split.android.client.service.executor.SplitTaskFactory; import io.split.android.client.service.executor.SplitTaskFactoryImpl; -import io.split.android.client.service.impressions.ImpressionManager; import io.split.android.client.service.impressions.StrategyImpressionManager; +import io.split.android.client.service.impressions.strategy.ImpressionStrategyProvider; +import io.split.android.client.service.impressions.strategy.PeriodicTracker; +import io.split.android.client.service.impressions.strategy.ProcessStrategy; import io.split.android.client.service.sseclient.sseclient.StreamingComponents; import io.split.android.client.service.synchronizer.SyncManager; import io.split.android.client.service.synchronizer.Synchronizer; @@ -185,7 +188,9 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp SplitSingleThreadTaskExecutor splitSingleThreadTaskExecutor = new SplitSingleThreadTaskExecutor(); splitSingleThreadTaskExecutor.pause(); - ImpressionManager impressionManager = new StrategyImpressionManager(factoryHelper.getImpressionStrategy(splitTaskExecutor, splitTaskFactory, mStorageContainer, config)); + ImpressionStrategyProvider impressionStrategyProvider = factoryHelper.getImpressionStrategyProvider(splitTaskExecutor, splitTaskFactory, mStorageContainer, config); + Pair noneComponents = impressionStrategyProvider.getNoneComponents(); + StrategyImpressionManager impressionManager = new StrategyImpressionManager(noneComponents, impressionStrategyProvider.getStrategy(config.impressionsMode())); final RetryBackoffCounterTimerFactory retryBackoffCounterTimerFactory = new RetryBackoffCounterTimerFactory(); StreamingComponents streamingComponents = factoryHelper.buildStreamingComponents(splitTaskExecutor, @@ -239,17 +244,16 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp } ExecutorService impressionsLoggingTaskExecutor = factoryHelper.getImpressionsLoggingTaskExecutor(); - final ImpressionListener splitImpressionListener + final DecoratedImpressionListener splitImpressionListener = new SyncImpressionListener(mSyncManager, impressionsLoggingTaskExecutor); - final ImpressionListener customerImpressionListener; + final ImpressionListener.FederatedImpressionListener customerImpressionListener; + List impressionListeners = new ArrayList<>(); if (config.impressionListener() != null) { - List impressionListeners = new ArrayList<>(); - impressionListeners.add(splitImpressionListener); impressionListeners.add(config.impressionListener()); - customerImpressionListener = new ImpressionListener.FederatedImpressionListener(impressionListeners); + customerImpressionListener = new ImpressionListener.FederatedImpressionListener(splitImpressionListener, impressionListeners); } else { - customerImpressionListener = splitImpressionListener; + customerImpressionListener = new ImpressionListener.FederatedImpressionListener(splitImpressionListener, impressionListeners); } EventsTracker eventsTracker = buildEventsTracker(); mUserConsentManager = new UserConsentManagerImpl(config, @@ -409,11 +413,6 @@ private void setupValidations(SplitClientConfig splitClientConfig) { ValidationConfig.getInstance().setTrackEventNamePattern(splitClientConfig.trackEventNamePattern()); } - private void cleanUpDabase(SplitTaskExecutor splitTaskExecutor, - SplitTaskFactory splitTaskFactory) { - splitTaskExecutor.submit(splitTaskFactory.createCleanUpDatabaseTask(System.currentTimeMillis() / 1000), null); - } - private EventsTracker buildEventsTracker() { EventValidator eventsValidator = new EventValidatorImpl(new KeyValidatorImpl(), mStorageContainer.getSplitsStorage()); return new EventsTrackerImpl(eventsValidator, new ValidationMessageLoggerImpl(), mStorageContainer.getTelemetryStorage(), diff --git a/src/main/java/io/split/android/client/SplitManagerImpl.java b/src/main/java/io/split/android/client/SplitManagerImpl.java index 5fe90cd85..4f7bc30ae 100644 --- a/src/main/java/io/split/android/client/SplitManagerImpl.java +++ b/src/main/java/io/split/android/client/SplitManagerImpl.java @@ -1,5 +1,14 @@ package io.split.android.client; +import static io.split.android.client.utils.Utils.checkNotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + import io.split.android.client.api.SplitView; import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; @@ -13,15 +22,6 @@ import io.split.android.engine.experiments.ParsedSplit; import io.split.android.engine.experiments.SplitParser; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static io.split.android.client.utils.Utils.checkNotNull; - public class SplitManagerImpl implements SplitManager { private final SplitsStorage _splitsStorage; @@ -144,6 +144,7 @@ private SplitView toSplitView(ParsedSplit parsedSplit) { splitView.configs = parsedSplit.configurations(); splitView.sets = new ArrayList<>(parsedSplit.sets() == null ? new HashSet<>() : parsedSplit.sets()); splitView.defaultTreatment = parsedSplit.defaultTreatment(); + splitView.trackImpressions = parsedSplit.trackImpressions(); Set treatments = new HashSet<>(); for (ParsedCondition condition : parsedSplit.parsedConditions()) { diff --git a/src/main/java/io/split/android/client/api/SplitView.java b/src/main/java/io/split/android/client/api/SplitView.java index 9cbc7192e..a8a416037 100644 --- a/src/main/java/io/split/android/client/api/SplitView.java +++ b/src/main/java/io/split/android/client/api/SplitView.java @@ -21,4 +21,5 @@ public class SplitView { @NonNull public List sets = new ArrayList<>(); public String defaultTreatment; + public boolean trackImpressions; } 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 395a4c3a3..be1b235f1 100644 --- a/src/main/java/io/split/android/client/dtos/Split.java +++ b/src/main/java/io/split/android/client/dtos/Split.java @@ -49,4 +49,7 @@ public class Split { @Nullable @SerializedName("sets") public Set sets; + + @SerializedName("trackImpressions") + public boolean trackImpressions = true; } diff --git a/src/main/java/io/split/android/client/impressions/DecoratedImpression.java b/src/main/java/io/split/android/client/impressions/DecoratedImpression.java new file mode 100644 index 000000000..ecfb0e03f --- /dev/null +++ b/src/main/java/io/split/android/client/impressions/DecoratedImpression.java @@ -0,0 +1,20 @@ +package io.split.android.client.impressions; + +public class DecoratedImpression { + + private final Impression mImpression; + private final boolean mTrackImpressions; + + public DecoratedImpression(Impression impression, boolean trackImpressions) { + mImpression = impression; + mTrackImpressions = trackImpressions; + } + + public Impression getImpression() { + return mImpression; + } + + public boolean getTrackImpressions() { + return mTrackImpressions; + } +} diff --git a/src/main/java/io/split/android/client/impressions/DecoratedImpressionListener.java b/src/main/java/io/split/android/client/impressions/DecoratedImpressionListener.java new file mode 100644 index 000000000..e063128c0 --- /dev/null +++ b/src/main/java/io/split/android/client/impressions/DecoratedImpressionListener.java @@ -0,0 +1,6 @@ +package io.split.android.client.impressions; + +public interface DecoratedImpressionListener { + + void log(DecoratedImpression impression); +} diff --git a/src/main/java/io/split/android/client/impressions/ImpressionListener.java b/src/main/java/io/split/android/client/impressions/ImpressionListener.java index 7db48a401..7f87e8d95 100644 --- a/src/main/java/io/split/android/client/impressions/ImpressionListener.java +++ b/src/main/java/io/split/android/client/impressions/ImpressionListener.java @@ -19,22 +19,29 @@ public interface ImpressionListener { */ void close(); - final class NoopImpressionListener implements ImpressionListener { + final class NoopImpressionListener implements ImpressionListener, DecoratedImpressionListener { @Override public void log(Impression impression) { // noop } + @Override + public void log(DecoratedImpression impression) { + + } + @Override public void close() { // noop } } - final class FederatedImpressionListener implements ImpressionListener { - private List _delegates; + final class FederatedImpressionListener implements ImpressionListener, DecoratedImpressionListener { + private final DecoratedImpressionListener mDecoratedImpressionListener; + private final List _delegates; - public FederatedImpressionListener(List delegates) { + public FederatedImpressionListener(DecoratedImpressionListener decoratedImpressionListener, List delegates) { + mDecoratedImpressionListener = decoratedImpressionListener; _delegates = delegates; } @@ -45,6 +52,11 @@ public void log(Impression impression) { } } + @Override + public void log(DecoratedImpression impression) { + mDecoratedImpressionListener.log(impression); + } + @Override public void close() { for (ImpressionListener listener : _delegates) { diff --git a/src/main/java/io/split/android/client/impressions/ImpressionLoggingTask.java b/src/main/java/io/split/android/client/impressions/ImpressionLoggingTask.java index ff80c8530..8ca6bc3b4 100644 --- a/src/main/java/io/split/android/client/impressions/ImpressionLoggingTask.java +++ b/src/main/java/io/split/android/client/impressions/ImpressionLoggingTask.java @@ -10,10 +10,10 @@ class ImpressionLoggingTask implements Runnable { private final SyncManager mSyncManager; - private final Impression mImpression; + private final DecoratedImpression mImpression; ImpressionLoggingTask(@NonNull SyncManager syncManager, - Impression impression) { + DecoratedImpression impression) { mSyncManager = checkNotNull(syncManager); mImpression = impression; } diff --git a/src/main/java/io/split/android/client/impressions/SyncImpressionListener.java b/src/main/java/io/split/android/client/impressions/SyncImpressionListener.java index 6bdf4fa14..147e940d2 100644 --- a/src/main/java/io/split/android/client/impressions/SyncImpressionListener.java +++ b/src/main/java/io/split/android/client/impressions/SyncImpressionListener.java @@ -9,7 +9,7 @@ import io.split.android.client.service.synchronizer.SyncManager; import io.split.android.client.utils.logger.Logger; -public class SyncImpressionListener implements ImpressionListener { +public class SyncImpressionListener implements DecoratedImpressionListener { private final SyncManager mSyncManager; private final ExecutorService mExecutorService; @@ -21,15 +21,11 @@ public SyncImpressionListener(@NonNull SyncManager syncManager, } @Override - public void log(Impression impression) { + public void log(DecoratedImpression impression) { try { mExecutorService.submit(new ImpressionLoggingTask(mSyncManager, impression)); } catch (Exception ex) { Logger.w("Error submitting impression logging task: " + ex.getLocalizedMessage()); } } - - @Override - public void close() { - } } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostImpressionsListener.java b/src/main/java/io/split/android/client/localhost/LocalhostImpressionsListener.java index af7e053ca..846dd6a83 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostImpressionsListener.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostImpressionsListener.java @@ -1,13 +1,19 @@ package io.split.android.client.localhost; +import io.split.android.client.impressions.DecoratedImpression; +import io.split.android.client.impressions.DecoratedImpressionListener; import io.split.android.client.impressions.Impression; import io.split.android.client.impressions.ImpressionListener; -public class LocalhostImpressionsListener implements ImpressionListener { +public class LocalhostImpressionsListener implements ImpressionListener, DecoratedImpressionListener { @Override public void log(Impression impression) { } + @Override + public void log(DecoratedImpression impression) { + } + @Override public void close() { } 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 5c3d31e64..77c394308 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSplitClient.java @@ -263,11 +263,11 @@ public boolean track(String eventType, double value, Map propert return false; } - private ImpressionListener getImpressionsListener(SplitClientConfig config) { + private ImpressionListener.FederatedImpressionListener getImpressionsListener(SplitClientConfig config) { if (config.impressionListener() != null) { - return config.impressionListener(); + return new ImpressionListener.FederatedImpressionListener(new ImpressionListener.NoopImpressionListener(), Collections.singletonList(config.impressionListener())); } else { - return new LocalhostImpressionsListener(); + return new ImpressionListener.FederatedImpressionListener(new ImpressionListener.NoopImpressionListener(), Collections.emptyList()); } } diff --git a/src/main/java/io/split/android/client/service/impressions/ImpressionManager.java b/src/main/java/io/split/android/client/service/impressions/ImpressionManager.java index 11598f02f..511536d06 100644 --- a/src/main/java/io/split/android/client/service/impressions/ImpressionManager.java +++ b/src/main/java/io/split/android/client/service/impressions/ImpressionManager.java @@ -1,9 +1,10 @@ package io.split.android.client.service.impressions; -import io.split.android.client.impressions.Impression; -import io.split.android.client.service.impressions.strategy.PeriodicTracker; +import io.split.android.client.impressions.DecoratedImpression; -public interface ImpressionManager extends PeriodicTracker { +public interface ImpressionManager { - void pushImpression(Impression impression); + void enableTracking(boolean enable); + + void pushImpression(DecoratedImpression impression); } diff --git a/src/main/java/io/split/android/client/service/impressions/StrategyImpressionManager.java b/src/main/java/io/split/android/client/service/impressions/StrategyImpressionManager.java index 3512a2049..d291cc875 100644 --- a/src/main/java/io/split/android/client/service/impressions/StrategyImpressionManager.java +++ b/src/main/java/io/split/android/client/service/impressions/StrategyImpressionManager.java @@ -2,59 +2,73 @@ import static io.split.android.client.utils.Utils.checkNotNull; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; +import androidx.core.util.Pair; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; -import io.split.android.client.impressions.Impression; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.service.impressions.strategy.PeriodicTracker; import io.split.android.client.service.impressions.strategy.ProcessStrategy; import io.split.android.client.utils.logger.Logger; -public class StrategyImpressionManager implements ImpressionManager { +public class StrategyImpressionManager implements ImpressionManager, PeriodicTracker { private final AtomicBoolean isTrackingEnabled = new AtomicBoolean(true); private final ProcessStrategy mProcessStrategy; - private final PeriodicTracker mPeriodicTracker; + private final ProcessStrategy mNoneStrategy; + private final Set mPeriodicTrackers; - public StrategyImpressionManager(@NonNull ProcessStrategy processStrategy) { - this(processStrategy, processStrategy); + public StrategyImpressionManager(Pair noneComponents, Pair strategy) { + this(noneComponents.first, noneComponents.second, strategy.first, strategy.second); } - @VisibleForTesting - StrategyImpressionManager(@NonNull ProcessStrategy processStrategy, @NonNull PeriodicTracker periodicTracker) { - mProcessStrategy = checkNotNull(processStrategy); - mPeriodicTracker = checkNotNull(periodicTracker); + StrategyImpressionManager(ProcessStrategy noneStrategy, PeriodicTracker noneTracker, ProcessStrategy strategy, PeriodicTracker strategyTracker) { + mProcessStrategy = checkNotNull(strategy); + mNoneStrategy = checkNotNull(noneStrategy); + mPeriodicTrackers = new HashSet<>(); + mPeriodicTrackers.add(noneTracker); + mPeriodicTrackers.add(strategyTracker); } @Override - public void enableTracking(boolean enable) { - isTrackingEnabled.set(enable); - } - - @Override - public void pushImpression(Impression impression) { + public void pushImpression(DecoratedImpression impression) { if (!isTrackingEnabled.get()) { Logger.v("Impression not tracked because tracking is disabled"); return; } - mProcessStrategy.apply(impression); + if (impression.getTrackImpressions()) { + mProcessStrategy.apply(impression.getImpression()); + } else { + mNoneStrategy.apply(impression.getImpression()); + } + } + + @Override + public void enableTracking(boolean enable) { + isTrackingEnabled.set(enable); } @Override public void flush() { - mPeriodicTracker.flush(); + for (PeriodicTracker tracker : mPeriodicTrackers) { + tracker.flush(); + } } @Override public void startPeriodicRecording() { - mPeriodicTracker.startPeriodicRecording(); + for (PeriodicTracker tracker : mPeriodicTrackers) { + tracker.startPeriodicRecording(); + } } @Override public void stopPeriodicRecording() { - mPeriodicTracker.stopPeriodicRecording(); + for (PeriodicTracker tracker : mPeriodicTrackers) { + tracker.stopPeriodicRecording(); + } } } 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 ffeeaa60f..1a0c0855d 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 @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import java.util.concurrent.atomic.AtomicBoolean; @@ -16,7 +15,6 @@ import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.impressions.observer.ImpressionsObserver; -import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; import io.split.android.client.service.synchronizer.RecorderSyncHelper; import io.split.android.client.telemetry.model.ImpressionsDataType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -31,7 +29,6 @@ class DebugStrategy implements ProcessStrategy { private final SplitTaskExecutor mTaskExecutor; private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; private final ImpressionsTaskFactory mImpressionsTaskFactory; - private final PeriodicTracker mDebugTracker; private final AtomicBoolean mIsSynchronizing = new AtomicBoolean(true); /** @noinspection FieldCanBeLocal*/ private final SplitTaskExecutionListener mTaskExecutionListener = new SplitTaskExecutionListener() { @@ -41,7 +38,6 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { if (Boolean.TRUE.equals(taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY))) { mIsSynchronizing.compareAndSet(true, false); - mDebugTracker.stopPeriodicRecording(); } } } @@ -51,24 +47,7 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { @NonNull RecorderSyncHelper impressionsSyncHelper, @NonNull SplitTaskExecutor taskExecutor, @NonNull ImpressionsTaskFactory taskFactory, - @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, - @NonNull RetryBackoffCounterTimer retryTimer, - int impressionsRefreshRate) { - this(impressionsObserver, - impressionsSyncHelper, - taskExecutor, - taskFactory, - telemetryRuntimeProducer, - new DebugTracker(impressionsSyncHelper, taskExecutor, taskFactory, retryTimer, impressionsRefreshRate)); - } - - @VisibleForTesting - DebugStrategy(@NonNull ImpressionsObserver impressionsObserver, - @NonNull RecorderSyncHelper impressionsSyncHelper, - @NonNull SplitTaskExecutor taskExecutor, - @NonNull ImpressionsTaskFactory taskFactory, - @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, - @NonNull PeriodicTracker tracker) { + @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { mImpressionsObserver = checkNotNull(impressionsObserver); RecorderSyncHelper syncHelper = checkNotNull(impressionsSyncHelper); syncHelper.addListener(mTaskExecutionListener); @@ -76,7 +55,6 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { mTaskExecutor = checkNotNull(taskExecutor); mImpressionsTaskFactory = checkNotNull(taskFactory); mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); - mDebugTracker = checkNotNull(tracker); } @Override @@ -92,27 +70,4 @@ public void apply(@NonNull Impression impression) { mTelemetryRuntimeProducer.recordImpressionStats(ImpressionsDataType.IMPRESSIONS_QUEUED, 1); } - - @Override - public void flush() { - mDebugTracker.flush(); - } - - @Override - public void startPeriodicRecording() { - if (mIsSynchronizing.get()) { - mDebugTracker.startPeriodicRecording(); - } - } - - @Override - public void stopPeriodicRecording() { - mDebugTracker.stopPeriodicRecording(); - mImpressionsObserver.persist(); - } - - @Override - public void enableTracking(boolean enable) { - mDebugTracker.enableTracking(enable); - } } diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java b/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java index 05ac1c29f..36d86ff7b 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java @@ -4,10 +4,16 @@ import androidx.annotation.NonNull; +import java.util.concurrent.atomic.AtomicBoolean; + import io.split.android.client.dtos.KeyImpression; import io.split.android.client.service.ServiceConstants; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionListener; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.impressions.ImpressionsTaskFactory; +import io.split.android.client.service.impressions.observer.ImpressionsObserver; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; import io.split.android.client.service.synchronizer.RecorderSyncHelper; @@ -18,14 +24,32 @@ class DebugTracker implements PeriodicTracker { private final ImpressionsTaskFactory mImpressionsTaskFactory; private final RetryBackoffCounterTimer mRetryTimer; private final int mImpressionsRefreshRate; + private final AtomicBoolean mIsSynchronizing = new AtomicBoolean(true); + private final ImpressionsObserver mImpressionsObserver; + /** @noinspection FieldCanBeLocal*/ + private final SplitTaskExecutionListener mTaskExecutionListener = new SplitTaskExecutionListener() { + @Override + public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { + // this listener intercepts impressions recording task + if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { + if (Boolean.TRUE.equals(taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY))) { + mIsSynchronizing.compareAndSet(true, false); + stopPeriodicRecording(); + } + } + } + }; private String mImpressionsRecorderTaskId; - DebugTracker(@NonNull RecorderSyncHelper impressionsSyncHelper, + DebugTracker(@NonNull ImpressionsObserver impressionsObserver, + @NonNull RecorderSyncHelper impressionsSyncHelper, @NonNull SplitTaskExecutor taskExecutor, @NonNull ImpressionsTaskFactory taskFactory, @NonNull RetryBackoffCounterTimer retryTimer, int impressionsRefreshRate) { + mImpressionsObserver = checkNotNull(impressionsObserver); mImpressionsSyncHelper = checkNotNull(impressionsSyncHelper); + mImpressionsSyncHelper.addListener(mTaskExecutionListener); mTaskExecutor = checkNotNull(taskExecutor); mImpressionsTaskFactory = checkNotNull(taskFactory); mRetryTimer = retryTimer; @@ -46,7 +70,9 @@ private void flushImpressions() { @Override public void startPeriodicRecording() { - scheduleImpressionsRecorderTask(); + if (mIsSynchronizing.get()) { + scheduleImpressionsRecorderTask(); + } } private void scheduleImpressionsRecorderTask() { @@ -63,6 +89,7 @@ private void scheduleImpressionsRecorderTask() { @Override public void stopPeriodicRecording() { mTaskExecutor.stopTask(mImpressionsRecorderTaskId); + mImpressionsObserver.persist(); } @Override diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java b/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java index 787647587..c331ee61f 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java @@ -1,5 +1,8 @@ package io.split.android.client.service.impressions.strategy; +import androidx.core.util.Pair; + +import io.split.android.client.dtos.KeyImpression; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskType; @@ -8,6 +11,7 @@ import io.split.android.client.service.impressions.ImpressionsMode; import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.impressions.observer.ImpressionsObserverImpl; +import io.split.android.client.service.impressions.unique.UniqueKeysTracker; import io.split.android.client.service.impressions.unique.UniqueKeysTrackerImpl; import io.split.android.client.service.synchronizer.RecorderSyncHelperImpl; import io.split.android.client.storage.common.SplitStorageContainer; @@ -20,6 +24,10 @@ public class ImpressionStrategyProvider { private final ImpressionsTaskFactory mSplitTaskFactory; private final TelemetryRuntimeProducer mTelemetryStorage; private final ImpressionStrategyConfig mImpressionStrategyConfig; + private final ImpressionsCounter mImpressionsCounter; + private final ImpressionManagerRetryTimerProviderImpl mImpressionManagerRetryTimerProvider; + private final NoneStrategy mNoneStrategy; + private final NoneTracker mNoneTracker; public ImpressionStrategyProvider(SplitTaskExecutor splitTaskExecutor, SplitStorageContainer storageContainer, @@ -31,58 +39,77 @@ public ImpressionStrategyProvider(SplitTaskExecutor splitTaskExecutor, mSplitTaskFactory = splitTaskFactory; mTelemetryStorage = telemetryStorage; mImpressionStrategyConfig = config; + mImpressionsCounter = new ImpressionsCounter(mImpressionStrategyConfig.getDedupeTimeIntervalInMs()); + mImpressionManagerRetryTimerProvider = new ImpressionManagerRetryTimerProviderImpl(mSplitTaskExecutor); + + UniqueKeysTracker uniqueKeysTracker = new UniqueKeysTrackerImpl(); + mNoneStrategy = new NoneStrategy( + mSplitTaskExecutor, + mSplitTaskFactory, + mImpressionsCounter, + uniqueKeysTracker, + mImpressionStrategyConfig.isUserConsentGranted()); + mNoneTracker = new NoneTracker( + mSplitTaskExecutor, + mSplitTaskFactory, + mImpressionsCounter, + uniqueKeysTracker, + mImpressionManagerRetryTimerProvider.getImpressionsCountTimer(), + mImpressionManagerRetryTimerProvider.getUniqueKeysTimer(), + mImpressionStrategyConfig.getImpressionsCounterRefreshRate(), + mImpressionStrategyConfig.getUniqueKeysRefreshRate(), + mImpressionStrategyConfig.isUserConsentGranted()); } - public ProcessStrategy getStrategy(ImpressionsMode mode) { - ImpressionManagerRetryTimerProviderImpl impressionManagerRetryTimerProvider = new ImpressionManagerRetryTimerProviderImpl(mSplitTaskExecutor); + public Pair getStrategy(ImpressionsMode mode) { + ImpressionsObserverImpl impressionsObserver = new ImpressionsObserverImpl(mStorageContainer.getImpressionsObserverCachePersistentStorage(), ServiceConstants.LAST_SEEN_IMPRESSION_CACHE_SIZE); + RecorderSyncHelperImpl impressionsSyncHelper = new RecorderSyncHelperImpl<>( + SplitTaskType.IMPRESSIONS_RECORDER, + mStorageContainer.getImpressionsStorage(), + mImpressionStrategyConfig.getImpressionsQueueSize(), + mImpressionStrategyConfig.getImpressionsChunkSize(), + mSplitTaskExecutor); switch (mode) { case DEBUG: - return new DebugStrategy( - new ImpressionsObserverImpl(mStorageContainer.getImpressionsObserverCachePersistentStorage(), ServiceConstants.LAST_SEEN_IMPRESSION_CACHE_SIZE), - new RecorderSyncHelperImpl<>( - SplitTaskType.IMPRESSIONS_RECORDER, - mStorageContainer.getImpressionsStorage(), - mImpressionStrategyConfig.getImpressionsQueueSize(), - mImpressionStrategyConfig.getImpressionsChunkSize(), - mSplitTaskExecutor), + DebugTracker tracker = new DebugTracker( + impressionsObserver, + impressionsSyncHelper, mSplitTaskExecutor, mSplitTaskFactory, - mTelemetryStorage, - impressionManagerRetryTimerProvider.getImpressionsTimer(), - mImpressionStrategyConfig.getImpressionsRefreshRate() - ); - case NONE: - return new NoneStrategy( + mImpressionManagerRetryTimerProvider.getImpressionsTimer(), + mImpressionStrategyConfig.getImpressionsRefreshRate()); + DebugStrategy debugStrategy = new DebugStrategy( + impressionsObserver, + impressionsSyncHelper, mSplitTaskExecutor, mSplitTaskFactory, - new ImpressionsCounter(mImpressionStrategyConfig.getDedupeTimeIntervalInMs()), - new UniqueKeysTrackerImpl(), - impressionManagerRetryTimerProvider.getImpressionsCountTimer(), - impressionManagerRetryTimerProvider.getUniqueKeysTimer(), - mImpressionStrategyConfig.getImpressionsCounterRefreshRate(), - mImpressionStrategyConfig.getUniqueKeysRefreshRate(), - mImpressionStrategyConfig.isUserConsentGranted() - ); + mTelemetryStorage); + return new Pair<>(debugStrategy, tracker); + case NONE: + return getNoneComponents(); default: - return new OptimizedStrategy( - new ImpressionsObserverImpl(mStorageContainer.getImpressionsObserverCachePersistentStorage(), ServiceConstants.LAST_SEEN_IMPRESSION_CACHE_SIZE), - new ImpressionsCounter(mImpressionStrategyConfig.getDedupeTimeIntervalInMs()), - new RecorderSyncHelperImpl<>( - SplitTaskType.IMPRESSIONS_RECORDER, - mStorageContainer.getImpressionsStorage(), - mImpressionStrategyConfig.getImpressionsQueueSize(), - mImpressionStrategyConfig.getImpressionsChunkSize(), - mSplitTaskExecutor), + OptimizedStrategy optimizedStrategy = new OptimizedStrategy( + impressionsObserver, + mImpressionsCounter, + impressionsSyncHelper, mSplitTaskExecutor, mSplitTaskFactory, mTelemetryStorage, - impressionManagerRetryTimerProvider.getImpressionsTimer(), - impressionManagerRetryTimerProvider.getImpressionsCountTimer(), + mImpressionStrategyConfig.getDedupeTimeIntervalInMs()); + OptimizedTracker optimizedTracker = new OptimizedTracker( + impressionsObserver, + impressionsSyncHelper, + mSplitTaskExecutor, + mSplitTaskFactory, + mImpressionManagerRetryTimerProvider.getImpressionsTimer(), mImpressionStrategyConfig.getImpressionsRefreshRate(), - mImpressionStrategyConfig.getImpressionsCounterRefreshRate(), - mImpressionStrategyConfig.isUserConsentGranted(), - mImpressionStrategyConfig.getDedupeTimeIntervalInMs() + mImpressionStrategyConfig.isUserConsentGranted() ); + return new Pair<>(optimizedStrategy, optimizedTracker); } } + + public Pair getNoneComponents() { + return new Pair<>(mNoneStrategy, mNoneTracker); + } } diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/NoneStrategy.java b/src/main/java/io/split/android/client/service/impressions/strategy/NoneStrategy.java index 243477f06..064ef1dfa 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/NoneStrategy.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/NoneStrategy.java @@ -3,7 +3,6 @@ import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import java.util.concurrent.atomic.AtomicBoolean; @@ -12,7 +11,6 @@ import io.split.android.client.service.impressions.ImpressionsCounter; import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.impressions.unique.UniqueKeysTracker; -import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; /** * {@link ProcessStrategy} that corresponds to NONE Impressions mode. @@ -24,47 +22,17 @@ class NoneStrategy implements ProcessStrategy { private final ImpressionsCounter mImpressionsCounter; private final UniqueKeysTracker mUniqueKeysTracker; private final AtomicBoolean mTrackingIsEnabled; - private final PeriodicTracker mNoneTracker; NoneStrategy(@NonNull SplitTaskExecutor taskExecutor, @NonNull ImpressionsTaskFactory taskFactory, @NonNull ImpressionsCounter impressionsCounter, @NonNull UniqueKeysTracker uniqueKeysTracker, - @NonNull RetryBackoffCounterTimer impressionsCountRetryTimer, - @NonNull RetryBackoffCounterTimer uniqueKeysRetryTimer, - int impressionsCounterRefreshRate, - int uniqueKeysRefreshRate, boolean trackingIsEnabled) { - this(taskExecutor, - taskFactory, - impressionsCounter, - uniqueKeysTracker, - trackingIsEnabled, - new NoneTracker( - taskExecutor, - taskFactory, - impressionsCounter, - uniqueKeysTracker, - impressionsCountRetryTimer, - uniqueKeysRetryTimer, - impressionsCounterRefreshRate, - uniqueKeysRefreshRate, - trackingIsEnabled)); - } - - @VisibleForTesting - NoneStrategy(@NonNull SplitTaskExecutor taskExecutor, - @NonNull ImpressionsTaskFactory taskFactory, - @NonNull ImpressionsCounter impressionsCounter, - @NonNull UniqueKeysTracker uniqueKeysTracker, - boolean trackingIsEnabled, - @NonNull PeriodicTracker tracker) { mTaskExecutor = checkNotNull(taskExecutor); mTaskFactory = checkNotNull(taskFactory); mImpressionsCounter = checkNotNull(impressionsCounter); mUniqueKeysTracker = checkNotNull(uniqueKeysTracker); mTrackingIsEnabled = new AtomicBoolean(trackingIsEnabled); - mNoneTracker = checkNotNull(tracker); } @Override @@ -77,27 +45,6 @@ public void apply(@NonNull Impression impression) { } } - @Override - public void flush() { - mNoneTracker.flush(); - } - - @Override - public void startPeriodicRecording() { - mNoneTracker.startPeriodicRecording(); - } - - @Override - public void stopPeriodicRecording() { - mNoneTracker.stopPeriodicRecording(); - } - - @Override - public void enableTracking(boolean enable) { - mTrackingIsEnabled.set(enable); - mNoneTracker.enableTracking(enable); - } - private void saveUniqueKeys() { if (mTrackingIsEnabled.get()) { mTaskExecutor.submit( diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/NoneTracker.java b/src/main/java/io/split/android/client/service/impressions/strategy/NoneTracker.java index 566272c02..de9b5e06c 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/NoneTracker.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/NoneTracker.java @@ -7,13 +7,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import io.split.android.client.service.ServiceConstants; -import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; -import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskSerialWrapper; -import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.impressions.ImpressionsCounter; import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.impressions.unique.UniqueKeysTracker; 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 e232e5ff6..6adcd40bf 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 @@ -3,7 +3,6 @@ import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import java.util.concurrent.atomic.AtomicBoolean; @@ -17,7 +16,6 @@ import io.split.android.client.service.impressions.ImpressionsCounter; import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.impressions.observer.ImpressionsObserver; -import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; import io.split.android.client.service.synchronizer.RecorderSyncHelper; import io.split.android.client.telemetry.model.ImpressionsDataType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -33,11 +31,11 @@ class OptimizedStrategy implements ProcessStrategy { private final SplitTaskExecutor mTaskExecutor; private final ImpressionsTaskFactory mImpressionsTaskFactory; private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; - private final AtomicBoolean mTrackingIsEnabled; - private final PeriodicTracker mOptimizedTracker; private final AtomicBoolean mIsSynchronizing = new AtomicBoolean(true); private final long mImpressionsDedupeTimeInterval; - /** @noinspection FieldCanBeLocal*/ + /** + * @noinspection FieldCanBeLocal + */ private final SplitTaskExecutionListener mTaskExecutionListener = new SplitTaskExecutionListener() { @Override public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { @@ -45,7 +43,6 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { if (Boolean.TRUE.equals(taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY))) { mIsSynchronizing.compareAndSet(true, false); - mOptimizedTracker.stopPeriodicRecording(); } } } @@ -57,40 +54,6 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { @NonNull SplitTaskExecutor taskExecutor, @NonNull ImpressionsTaskFactory taskFactory, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, - @NonNull RetryBackoffCounterTimer impressionsRetryTimer, - @NonNull RetryBackoffCounterTimer impressionsCountRetryTimer, - int impressionsRefreshRate, - int impressionsCounterRefreshRate, - boolean isTrackingEnabled, - long impressionsDedupeTimeInterval) { - this(impressionsObserver, - impressionsCounter, - impressionsSyncHelper, - taskExecutor, - taskFactory, - telemetryRuntimeProducer, - isTrackingEnabled, - new OptimizedTracker(impressionsCounter, - impressionsSyncHelper, - taskExecutor, - taskFactory, - impressionsRetryTimer, - impressionsCountRetryTimer, - impressionsRefreshRate, - impressionsCounterRefreshRate, - isTrackingEnabled), - impressionsDedupeTimeInterval); - } - - @VisibleForTesting - OptimizedStrategy(@NonNull ImpressionsObserver impressionsObserver, - @NonNull ImpressionsCounter impressionsCounter, - @NonNull RecorderSyncHelper impressionsSyncHelper, - @NonNull SplitTaskExecutor taskExecutor, - @NonNull ImpressionsTaskFactory taskFactory, - @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, - boolean isTrackingEnabled, - @NonNull PeriodicTracker tracker, long impressionsDedupeTimeInterval) { mImpressionsObserver = checkNotNull(impressionsObserver); mImpressionsCounter = checkNotNull(impressionsCounter); @@ -100,8 +63,6 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { mTaskExecutor = checkNotNull(taskExecutor); mImpressionsTaskFactory = checkNotNull(taskFactory); mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); - mTrackingIsEnabled = new AtomicBoolean(isTrackingEnabled); - mOptimizedTracker = checkNotNull(tracker); mImpressionsDedupeTimeInterval = impressionsDedupeTimeInterval; } @@ -136,28 +97,4 @@ private boolean shouldPushImpression(KeyImpression impression) { private static boolean previousTimeIsValid(Long previousTime) { return previousTime != null && previousTime != 0; } - - @Override - public void flush() { - mOptimizedTracker.flush(); - } - - @Override - public void startPeriodicRecording() { - if (mIsSynchronizing.get()) { - mOptimizedTracker.startPeriodicRecording(); - } - } - - @Override - public void stopPeriodicRecording() { - mOptimizedTracker.stopPeriodicRecording(); - mImpressionsObserver.persist(); - } - - @Override - public void enableTracking(boolean enable) { - mTrackingIsEnabled.set(enable); - mOptimizedTracker.enableTracking(enable); - } } diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java b/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java index 05a42cf87..5f441dfaf 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java @@ -8,67 +8,78 @@ import io.split.android.client.dtos.KeyImpression; import io.split.android.client.service.ServiceConstants; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionListener; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.executor.SplitTaskExecutor; -import io.split.android.client.service.executor.SplitTaskSerialWrapper; -import io.split.android.client.service.impressions.ImpressionsCounter; import io.split.android.client.service.impressions.ImpressionsTaskFactory; +import io.split.android.client.service.impressions.observer.ImpressionsObserver; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; import io.split.android.client.service.synchronizer.RecorderSyncHelper; class OptimizedTracker implements PeriodicTracker { - private final ImpressionsCounter mImpressionsCounter; + private final ImpressionsObserver mImpressionsObserver; private final RecorderSyncHelper mImpressionsSyncHelper; private final SplitTaskExecutor mTaskExecutor; private final ImpressionsTaskFactory mImpressionsTaskFactory; private final RetryBackoffCounterTimer mRetryTimer; - private final RetryBackoffCounterTimer mImpressionsCountRetryTimer; private final int mImpressionsRefreshRate; - private final int mImpressionsCounterRefreshRate; private String mImpressionsRecorderTaskId; - private String mImpressionsRecorderCountTaskId; private final AtomicBoolean mTrackingIsEnabled; + private final AtomicBoolean mIsSynchronizing = new AtomicBoolean(true); + /** + * @noinspection FieldCanBeLocal + */ + private final SplitTaskExecutionListener mTaskExecutionListener = new SplitTaskExecutionListener() { + @Override + public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { + // this listener intercepts impressions recording task + if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { + if (Boolean.TRUE.equals(taskInfo.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY))) { + mIsSynchronizing.compareAndSet(true, false); + stopPeriodicRecording(); + } + } + } + }; - OptimizedTracker(@NonNull ImpressionsCounter impressionsCounter, + OptimizedTracker(@NonNull ImpressionsObserver impressionsObserver, @NonNull RecorderSyncHelper impressionsSyncHelper, @NonNull SplitTaskExecutor taskExecutor, @NonNull ImpressionsTaskFactory taskFactory, @NonNull RetryBackoffCounterTimer impressionsRetryTimer, - @NonNull RetryBackoffCounterTimer impressionsCountRetryTimer, int impressionsRefreshRate, - int impressionsCounterRefreshRate, boolean isTrackingEnabled) { - mImpressionsCounter = checkNotNull(impressionsCounter); + mImpressionsObserver = checkNotNull(impressionsObserver); mImpressionsSyncHelper = checkNotNull(impressionsSyncHelper); + mImpressionsSyncHelper.addListener(mTaskExecutionListener); mTaskExecutor = checkNotNull(taskExecutor); mImpressionsTaskFactory = checkNotNull(taskFactory); mRetryTimer = checkNotNull(impressionsRetryTimer); - mImpressionsCountRetryTimer = checkNotNull(impressionsCountRetryTimer); mImpressionsRefreshRate = impressionsRefreshRate; - mImpressionsCounterRefreshRate = impressionsCounterRefreshRate; mTrackingIsEnabled = new AtomicBoolean(isTrackingEnabled); } @Override public void flush() { flushImpressions(); - flushImpressionsCount(); } @Override public void startPeriodicRecording() { - scheduleImpressionsRecorderTask(); - scheduleImpressionsCountRecorderTask(); + if (mIsSynchronizing.get()) { + scheduleImpressionsRecorderTask(); + } } @Override public void stopPeriodicRecording() { - saveImpressionsCount(); mTaskExecutor.stopTask(mImpressionsRecorderTaskId); - mTaskExecutor.stopTask(mImpressionsRecorderCountTaskId); + mImpressionsObserver.persist(); } @Override @@ -83,13 +94,6 @@ private void flushImpressions() { mRetryTimer.start(); } - private void flushImpressionsCount() { - mImpressionsCountRetryTimer.setTask(new SplitTaskSerialWrapper( - mImpressionsTaskFactory.createSaveImpressionsCountTask(mImpressionsCounter.popAll()), - mImpressionsTaskFactory.createImpressionsCountRecorderTask())); - mImpressionsCountRetryTimer.start(); - } - private void scheduleImpressionsRecorderTask() { if (mImpressionsRecorderTaskId != null) { mTaskExecutor.stopTask(mImpressionsRecorderTaskId); @@ -100,22 +104,4 @@ private void scheduleImpressionsRecorderTask() { mImpressionsRefreshRate, mImpressionsSyncHelper); } - - private void scheduleImpressionsCountRecorderTask() { - if (mImpressionsRecorderCountTaskId != null) { - mTaskExecutor.stopTask(mImpressionsRecorderCountTaskId); - } - mImpressionsRecorderCountTaskId = mTaskExecutor.schedule( - mImpressionsTaskFactory.createImpressionsCountRecorderTask(), - ServiceConstants.NO_INITIAL_DELAY, - mImpressionsCounterRefreshRate, - null); - } - - private void saveImpressionsCount() { - if (mTrackingIsEnabled.get()) { - mTaskExecutor.submit( - mImpressionsTaskFactory.createSaveImpressionsCountTask(mImpressionsCounter.popAll()), null); - } - } } diff --git a/src/main/java/io/split/android/client/service/impressions/strategy/ProcessStrategy.java b/src/main/java/io/split/android/client/service/impressions/strategy/ProcessStrategy.java index d43786741..202a33835 100644 --- a/src/main/java/io/split/android/client/service/impressions/strategy/ProcessStrategy.java +++ b/src/main/java/io/split/android/client/service/impressions/strategy/ProcessStrategy.java @@ -4,7 +4,7 @@ import io.split.android.client.impressions.Impression; -public interface ProcessStrategy extends PeriodicTracker { +public interface ProcessStrategy { void apply(@NonNull Impression impression); } diff --git a/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java b/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java index a2582584a..4351930fb 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java @@ -5,6 +5,8 @@ import androidx.annotation.NonNull; import java.lang.ref.WeakReference; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -26,7 +28,7 @@ public class RecorderSyncHelperImpl implements Recorde private final int mMaxQueueSize; private final long mMaxQueueSizeInBytes; private final SplitTaskType mTaskType; - private WeakReference mTaskExecutionListener; + private final Set> mTaskExecutionListener; public RecorderSyncHelperImpl(SplitTaskType taskType, StoragePusher storage, @@ -40,7 +42,7 @@ public RecorderSyncHelperImpl(SplitTaskType taskType, mTotalPushedSizeInBytes = new AtomicLong(0); mMaxQueueSize = maxQueueSize; mMaxQueueSizeInBytes = maxQueueSizeInBytes; - mTaskExecutionListener = new WeakReference<>(null); + mTaskExecutionListener = new HashSet<>(); } @Override @@ -67,19 +69,28 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { SplitTaskExecutionInfo.NON_SENT_BYTES)); } - if (mTaskExecutionListener.get() != null) { - mTaskExecutionListener.get().taskExecuted(taskInfo); + for (WeakReference reference : mTaskExecutionListener) { + SplitTaskExecutionListener listener = reference.get(); + if (listener != null) { + listener.taskExecuted(taskInfo); + } } } @Override public void addListener(SplitTaskExecutionListener listener) { - mTaskExecutionListener = new WeakReference<>(listener); + mTaskExecutionListener.add(new WeakReference<>(listener)); } @Override public void removeListener(SplitTaskExecutionListener listener) { - mTaskExecutionListener = new WeakReference<>(null); + for (WeakReference reference : mTaskExecutionListener) { + SplitTaskExecutionListener listenerRef = reference.get(); + if (listenerRef != null && listenerRef.equals(listener)) { + mTaskExecutionListener.remove(reference); + break; + } + } } private void pushAsync(T entity) { diff --git a/src/main/java/io/split/android/client/service/synchronizer/SyncManager.java b/src/main/java/io/split/android/client/service/synchronizer/SyncManager.java index 200862b3f..bb5904220 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/SyncManager.java +++ b/src/main/java/io/split/android/client/service/synchronizer/SyncManager.java @@ -1,7 +1,7 @@ package io.split.android.client.service.synchronizer; import io.split.android.client.dtos.Event; -import io.split.android.client.impressions.Impression; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.lifecycle.SplitLifecycleAware; import io.split.android.client.shared.UserConsent; @@ -12,7 +12,7 @@ public interface SyncManager extends SplitLifecycleAware { void pushEvent(Event event); - void pushImpression(Impression impression); + void pushImpression(DecoratedImpression impression); void stop(); diff --git a/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java index 1a96ebff2..e96693539 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/SyncManagerImpl.java @@ -9,6 +9,7 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.dtos.Event; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.impressions.Impression; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -163,7 +164,7 @@ public void pushEvent(Event event) { } @Override - public void pushImpression(Impression impression) { + public void pushImpression(DecoratedImpression impression) { mSynchronizer.pushImpression(impression); } diff --git a/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java b/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java index 27adf8d38..b16331b83 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java +++ b/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java @@ -1,7 +1,7 @@ package io.split.android.client.service.synchronizer; import io.split.android.client.dtos.Event; -import io.split.android.client.impressions.Impression; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.lifecycle.SplitLifecycleAware; public interface Synchronizer extends SplitLifecycleAware { @@ -28,7 +28,7 @@ public interface Synchronizer extends SplitLifecycleAware { void pushEvent(Event event); - void pushImpression(Impression impression); + void pushImpression(DecoratedImpression impression); void flush(); diff --git a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java index b18fd0b50..930f5bd9b 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java @@ -13,6 +13,7 @@ import io.split.android.client.api.Key; import io.split.android.client.dtos.Event; import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.impressions.Impression; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -20,7 +21,7 @@ import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskFactory; import io.split.android.client.service.executor.SplitTaskType; -import io.split.android.client.service.impressions.ImpressionManager; +import io.split.android.client.service.impressions.StrategyImpressionManager; import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; import io.split.android.client.service.synchronizer.attributes.AttributesSynchronizer; @@ -44,7 +45,7 @@ public class SynchronizerImpl implements Synchronizer, SplitTaskExecutionListene private final StoragePusher mEventsStorage; private final SplitClientConfig mSplitClientConfig; private final SplitTaskFactory mSplitTaskFactory; - private final ImpressionManager mImpressionManager; + private final StrategyImpressionManager mImpressionManager; private final FeatureFlagsSynchronizer mFeatureFlagsSynchronizer; private RecorderSyncHelper mEventsSyncHelper; @@ -66,7 +67,7 @@ public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, @NonNull AttributesSynchronizerRegistryImpl attributesSynchronizerRegistry, @NonNull MySegmentsSynchronizerRegistryImpl mySegmentsSynchronizerRegistry, - @NonNull ImpressionManager impressionManager, + @NonNull StrategyImpressionManager impressionManager, @NonNull StoragePusher eventsStorage, @NonNull ISplitEventsManager eventsManagerCoordinator, @Nullable PushManagerEventBroadcaster pushManagerEventBroadcaster) { @@ -100,7 +101,7 @@ public SynchronizerImpl(@NonNull SplitClientConfig splitClientConfig, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, @NonNull AttributesSynchronizerRegistryImpl attributesSynchronizerRegistry, @NonNull MySegmentsSynchronizerRegistryImpl mySegmentsSynchronizerRegistry, - @NonNull ImpressionManager impressionManager, + @NonNull StrategyImpressionManager impressionManager, @NonNull FeatureFlagsSynchronizer featureFlagsSynchronizer, @NonNull StoragePusher eventsStorage) { @@ -255,7 +256,7 @@ public void pushEvent(Event event) { } @Override - public void pushImpression(Impression impression) { + public void pushImpression(DecoratedImpression impression) { mImpressionManager.pushImpression(impression); } 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 9d50f645d..a49788bf5 100644 --- a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java @@ -67,7 +67,7 @@ public SplitClientContainerImpl(@NonNull String defaultMatchingKey, @NonNull SplitApiFacade splitApiFacade, @NonNull ValidationMessageLogger validationLogger, @NonNull KeyValidator keyValidator, - @NonNull ImpressionListener customerImpressionListener, + @NonNull ImpressionListener.FederatedImpressionListener customerImpressionListener, @Nullable PushNotificationManager pushNotificationManager, @NonNull ClientComponentsRegister clientComponentsRegister, @NonNull MySegmentsWorkManagerWrapper workManagerWrapper, 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 770461b09..3fce796d9 100644 --- a/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java +++ b/src/main/java/io/split/android/client/validators/TreatmentManagerFactoryImpl.java @@ -21,7 +21,7 @@ public class TreatmentManagerFactoryImpl implements TreatmentManagerFactory { private final KeyValidator mKeyValidator; private final SplitValidator mSplitValidator; - private final ImpressionListener mCustomerImpressionListener; + private final ImpressionListener.FederatedImpressionListener mCustomerImpressionListener; private final boolean mLabelsEnabled; private final AttributesMerger mAttributesMerger; private final TelemetryStorageProducer mTelemetryStorageProducer; @@ -33,7 +33,7 @@ public class TreatmentManagerFactoryImpl implements TreatmentManagerFactory { public TreatmentManagerFactoryImpl(@NonNull KeyValidator keyValidator, @NonNull SplitValidator splitValidator, - @NonNull ImpressionListener customerImpressionListener, + @NonNull ImpressionListener.FederatedImpressionListener customerImpressionListener, boolean labelsEnabled, @NonNull AttributesMerger attributesMerger, @NonNull TelemetryStorageProducer telemetryStorageProducer, 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 a88e81747..2adc8ace8 100644 --- a/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java +++ b/src/main/java/io/split/android/client/validators/TreatmentManagerImpl.java @@ -21,6 +21,7 @@ import io.split.android.client.attributes.AttributesMerger; import io.split.android.client.events.ListenableEventsManager; import io.split.android.client.events.SplitEvent; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.impressions.Impression; import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.storage.splits.SplitsStorage; @@ -34,7 +35,7 @@ public class TreatmentManagerImpl implements TreatmentManager { private final Evaluator mEvaluator; private final KeyValidator mKeyValidator; private final SplitValidator mSplitValidator; - private final ImpressionListener mImpressionListener; + private final ImpressionListener.FederatedImpressionListener mImpressionListener; private final String mMatchingKey; private final String mBucketingKey; private final boolean mLabelsEnabled; @@ -54,7 +55,7 @@ public TreatmentManagerImpl(String matchingKey, Evaluator evaluator, KeyValidator keyValidator, SplitValidator splitValidator, - ImpressionListener impressionListener, + ImpressionListener.FederatedImpressionListener impressionListener, boolean labelsEnabled, ListenableEventsManager eventsManager, @NonNull AttributesManager attributesManager, @@ -294,7 +295,8 @@ private TreatmentResult getTreatmentWithConfigWithoutMetrics(String split, Map attributes) { + private void logImpression(String matchingKey, String bucketingKey, String splitName, String result, String label, Long changeNumber, Map attributes, boolean trackImpression) { try { - mImpressionListener.log(new Impression(matchingKey, bucketingKey, splitName, result, System.currentTimeMillis(), label, changeNumber, attributes)); + Impression impression = new Impression(matchingKey, bucketingKey, splitName, result, System.currentTimeMillis(), label, changeNumber, attributes); + DecoratedImpression decoratedImpression = new DecoratedImpression(impression, trackImpression); + mImpressionListener.log(decoratedImpression); + mImpressionListener.log(impression); } catch (Throwable t) { Logger.e("An error occurred logging impression: " + t.getLocalizedMessage()); } @@ -339,7 +345,7 @@ private EvaluationResult evaluateIfReady(String featureFlagName, mValidationLogger.w("the SDK is not ready, results may be incorrect for feature flag " + featureFlagName + ". Make sure to wait for SDK readiness before using this method", validationTag); mTelemetryStorageProducer.recordNonReadyUsage(); - return new EvaluationResult(Treatments.CONTROL, TreatmentLabels.NOT_READY, null, null); + return new EvaluationResult(Treatments.CONTROL, TreatmentLabels.NOT_READY, null, null, true); } return mEvaluator.getTreatment(mMatchingKey, mBucketingKey, featureFlagName, attributes); } diff --git a/src/main/java/io/split/android/engine/experiments/ParsedSplit.java b/src/main/java/io/split/android/engine/experiments/ParsedSplit.java index c00b17f8b..eec6ed718 100644 --- a/src/main/java/io/split/android/engine/experiments/ParsedSplit.java +++ b/src/main/java/io/split/android/engine/experiments/ParsedSplit.java @@ -23,6 +23,7 @@ public class ParsedSplit { private final int mAlgo; private final Map mConfigurations; private final Set mSets; + private final boolean mTrackImpressions; public ParsedSplit( String feature, @@ -36,7 +37,8 @@ public ParsedSplit( int trafficAllocationSeed, int algo, Map configurations, - Set sets + Set sets, + boolean trackImpressions ) { mSplit = feature; mSeed = seed; @@ -47,6 +49,7 @@ public ParsedSplit( mChangeNumber = changeNumber; mAlgo = algo; mConfigurations = configurations; + mTrackImpressions = trackImpressions; if (mDefaultTreatment == null) { throw new IllegalArgumentException("DefaultTreatment is null"); @@ -104,6 +107,10 @@ public Set sets() { return mSets; } + public boolean trackImpressions() { + return mTrackImpressions; + } + @Override public int hashCode() { int result = 17; @@ -116,6 +123,7 @@ public int hashCode() { result = 31 * result + (int) (mChangeNumber ^ (mChangeNumber >>> 32)); result = 31 * result + (mAlgo ^ (mAlgo >>> 32)); result = 31 * result + ((mSets != null) ? mSets.hashCode() : 0); + result = 31 * result + (mTrackImpressions ? 1 : 0); return result; } @@ -135,7 +143,8 @@ public boolean equals(Object obj) { && mChangeNumber == other.mChangeNumber && mAlgo == other.mAlgo && (Objects.equals(mConfigurations, other.mConfigurations)) - && (Objects.equals(mSets, other.mSets)); + && (Objects.equals(mSets, other.mSets) + && mTrackImpressions == other.mTrackImpressions); } @@ -146,7 +155,7 @@ public String toString() { ", default treatment:" + mDefaultTreatment + ", parsedConditions:" + mParsedCondition + ", trafficTypeName:" + mTrafficTypeName + ", changeNumber:" + mChangeNumber + - ", algo:" + mAlgo + ", config:" + mConfigurations + ", sets:" + mSets; + ", algo:" + mAlgo + ", config:" + mConfigurations + ", sets:" + mSets + ", trackImpressions:" + mTrackImpressions; } } diff --git a/src/main/java/io/split/android/engine/experiments/SplitParser.java b/src/main/java/io/split/android/engine/experiments/SplitParser.java index 2763ecca7..5684afd88 100644 --- a/src/main/java/io/split/android/engine/experiments/SplitParser.java +++ b/src/main/java/io/split/android/engine/experiments/SplitParser.java @@ -124,7 +124,8 @@ private ParsedSplit parseWithoutExceptionHandling(Split split, String matchingKe split.trafficAllocationSeed, split.algo, split.configurations, - split.sets); + split.sets, + split.trackImpressions); } private CombiningMatcher toMatcher(MatcherGroup matcherGroup, String matchingKey) throws UnsupportedMatcherException { diff --git a/src/test/java/io/split/android/client/SplitManagerImplTest.java b/src/test/java/io/split/android/client/SplitManagerImplTest.java index 07adcfc84..02ff0b7f3 100644 --- a/src/test/java/io/split/android/client/SplitManagerImplTest.java +++ b/src/test/java/io/split/android/client/SplitManagerImplTest.java @@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.when; @@ -186,6 +187,19 @@ public void defaultTreatmentIsPresentWhenFetchingMultipleSplits() { assertEquals("some_treatment", splitNames.get(0).defaultTreatment); } + @Test + public void trackImpressionsIsPresent() { + Split split = SplitHelper.createSplit("FeatureName", 123, true, + "some_treatment", Arrays.asList(getTestCondition()), + "traffic", 456L, 1, null); + split.trackImpressions = false; + when(mSplitsStorage.get("FeatureName")).thenReturn(split); + + SplitView featureFlag = mSplitManager.split("FeatureName"); + + assertFalse(featureFlag.trackImpressions); + } + private Condition getTestCondition() { return SplitHelper.createCondition(CombiningMatcher.of(new AllKeysMatcher()), Arrays.asList(ConditionsTestUtil.partition("off", 10))); } diff --git a/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java b/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java index 74593a436..e130f233c 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerExceptionsTest.java @@ -45,7 +45,7 @@ public class TreatmentManagerExceptionsTest { @Mock SplitValidator splitValidator; @Mock - ImpressionListener impressionListener; + ImpressionListener.FederatedImpressionListener impressionListener; @Mock ListenableEventsManager eventsManager; @Mock diff --git a/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java b/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java index dfe84c02e..d4b917084 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerTelemetryTest.java @@ -41,7 +41,7 @@ public class TreatmentManagerTelemetryTest { @Mock SplitValidator splitValidator; @Mock - ImpressionListener impressionListener; + ImpressionListener.FederatedImpressionListener impressionListener; @Mock ListenableEventsManager eventsManager; @Mock diff --git a/src/test/java/io/split/android/client/TreatmentManagerTest.java b/src/test/java/io/split/android/client/TreatmentManagerTest.java index a13f3e287..1fb4d5307 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerTest.java @@ -2,6 +2,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -27,6 +28,7 @@ import io.split.android.client.dtos.Split; import io.split.android.client.events.ListenableEventsManager; import io.split.android.client.events.SplitEvent; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; @@ -43,7 +45,6 @@ import io.split.android.client.validators.ValidationMessageLogger; import io.split.android.client.validators.ValidationMessageLoggerImpl; import io.split.android.engine.experiments.SplitParser; -import io.split.android.fake.ImpressionListenerMock; import io.split.android.fake.SplitEventsManagerStub; import io.split.android.grammar.Treatments; import io.split.android.helpers.FileHelper; @@ -52,7 +53,7 @@ public class TreatmentManagerTest { Evaluator evaluator; - ImpressionListener impressionListener; + ImpressionListener.FederatedImpressionListener impressionListener; ListenableEventsManager eventsManagerStub; AttributesManager attributesManager = mock(AttributesManager.class); TelemetryStorageProducer telemetryStorageProducer = mock(TelemetryStorageProducer.class); @@ -89,7 +90,7 @@ public void loadSplitsFromFile() { evaluator = new EvaluatorImpl(splitsStorage, splitParser); } - impressionListener = mock(ImpressionListener.class); + impressionListener = mock(ImpressionListener.FederatedImpressionListener.class); eventsManagerStub = new SplitEventsManagerStub(); } @@ -316,6 +317,18 @@ public void evaluationWhenNotReadyLogsCorrectMessage() { 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()); } + @Test + public void trackValueFromEvaluationResultGetsPassedInToImpression() { + Evaluator evaluatorMock = mock(Evaluator.class); + when(evaluatorMock.getTreatment(eq("matching_key"), eq("bucketing_key"), eq("test_split"), anyMap())) + .thenReturn(new EvaluationResult("test", "test", true)); + TreatmentManagerImpl tManager = initializeTreatmentManager(evaluatorMock); + + tManager.getTreatment("test_split", null, false); + + verify(impressionListener).log(argThat(DecoratedImpression::getTrackImpressions)); + } + private void assertControl(List splitList, String treatment, Map treatmentList, SplitResult splitResult, Map splitResultList) { Assert.assertNotNull(treatment); Assert.assertEquals(Treatments.CONTROL, treatment); @@ -350,7 +363,7 @@ private TreatmentManager createTreatmentManager(String matchingKey, String bucke return new TreatmentManagerImpl( matchingKey, bucketingKey, evaluator, new KeyValidatorImpl(), splitValidator, - new ImpressionListenerMock(), config.labelsEnabled(), eventsManager, + mock(ImpressionListener.FederatedImpressionListener.class), config.labelsEnabled(), eventsManager, mock(AttributesManager.class), mock(AttributesMerger.class), mock(TelemetryStorageProducer.class), mFlagSetsFilter, mSplitsStorage, validationLogger, new FlagSetsValidatorImpl()); } @@ -374,7 +387,7 @@ private TreatmentManagerImpl initializeTreatmentManager(Evaluator evaluator) { evaluator, mock(KeyValidator.class), mock(SplitValidator.class), - mock(ImpressionListener.class), + impressionListener, SplitClientConfig.builder().build().labelsEnabled(), eventsManager, attributesManager, diff --git a/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java b/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java index adb7d46fd..bdcbec7db 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerWithFlagSetsTest.java @@ -45,7 +45,7 @@ public class TreatmentManagerWithFlagSetsTest { @Mock private SplitValidator mSplitValidator; @Mock - private ImpressionListener mImpressionListener; + private ImpressionListener.FederatedImpressionListener mImpressionListener; @Mock private ListenableEventsManager mEventsManager; @Mock diff --git a/src/test/java/io/split/android/client/dtos/SplitDeserializationTest.java b/src/test/java/io/split/android/client/dtos/SplitDeserializationTest.java new file mode 100644 index 000000000..0fa373f9d --- /dev/null +++ b/src/test/java/io/split/android/client/dtos/SplitDeserializationTest.java @@ -0,0 +1,127 @@ +package io.split.android.client.dtos; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import io.split.android.client.utils.Json; + +public class SplitDeserializationTest { + + @Test + public void trackImpressionsDefaultsToTrueWhenNotPresentInSplit() { + assertTrue(Json.fromJson(getTestSplit(null), Split.class).trackImpressions); + } + + @Test + public void trackImpressionsValueIsParsedCorrectly() { + assertTrue(Json.fromJson(getTestSplit(true), Split.class).trackImpressions); + assertFalse(Json.fromJson(getTestSplit(false), Split.class).trackImpressions); + } + + private String getTestSplit(Boolean trackImpressions) { + return "{\n" + + ((trackImpressions != null) ? "\"trackImpressions\": " + trackImpressions + ",\n" : "") + + " \"trafficTypeName\": \"client\",\n" + + " \"name\": \"workm\",\n" + + " \"trafficAllocation\": 100,\n" + + " \"trafficAllocationSeed\": 147392224,\n" + + " \"seed\": 524417105,\n" + + " \"status\": \"ACTIVE\",\n" + + " \"killed\": false,\n" + + " \"defaultTreatment\": \"on\",\n" + + " \"changeNumber\": 1602796638344,\n" + + " \"algo\": 2,\n" + + " \"configurations\": {},\n" + + " \"conditions\": [\n" + + " {\n" + + " \"conditionType\": \"ROLLOUT\",\n" + + " \"matcherGroup\": {\n" + + " \"combiner\": \"AND\",\n" + + " \"matchers\": [\n" + + " {\n" + + " \"keySelector\": {\n" + + " \"trafficType\": \"client\",\n" + + " \"attribute\": null\n" + + " },\n" + + " \"matcherType\": \"IN_SEGMENT\",\n" + + " \"negate\": false,\n" + + " \"userDefinedSegmentMatcherData\": {\n" + + " \"segmentName\": \"new_segment\"\n" + + " },\n" + + " \"whitelistMatcherData\": null,\n" + + " \"unaryNumericMatcherData\": null,\n" + + " \"betweenMatcherData\": null,\n" + + " \"booleanMatcherData\": null,\n" + + " \"dependencyMatcherData\": null,\n" + + " \"stringMatcherData\": null\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"partitions\": [\n" + + " {\n" + + " \"treatment\": \"on\",\n" + + " \"size\": 0\n" + + " },\n" + + " {\n" + + " \"treatment\": \"off\",\n" + + " \"size\": 0\n" + + " },\n" + + " {\n" + + " \"treatment\": \"free\",\n" + + " \"size\": 100\n" + + " },\n" + + " {\n" + + " \"treatment\": \"conta\",\n" + + " \"size\": 0\n" + + " }\n" + + " ],\n" + + " \"label\": \"in segment new_segment\"\n" + + " },\n" + + " {\n" + + " \"conditionType\": \"ROLLOUT\",\n" + + " \"matcherGroup\": {\n" + + " \"combiner\": \"AND\",\n" + + " \"matchers\": [\n" + + " {\n" + + " \"keySelector\": {\n" + + " \"trafficType\": \"client\",\n" + + " \"attribute\": null\n" + + " },\n" + + " \"matcherType\": \"ALL_KEYS\",\n" + + " \"negate\": false,\n" + + " \"userDefinedSegmentMatcherData\": null,\n" + + " \"whitelistMatcherData\": null,\n" + + " \"unaryNumericMatcherData\": null,\n" + + " \"betweenMatcherData\": null,\n" + + " \"booleanMatcherData\": null,\n" + + " \"dependencyMatcherData\": null,\n" + + " \"stringMatcherData\": null\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"partitions\": [\n" + + " {\n" + + " \"treatment\": \"on\",\n" + + " \"size\": 100\n" + + " },\n" + + " {\n" + + " \"treatment\": \"off\",\n" + + " \"size\": 0\n" + + " },\n" + + " {\n" + + " \"treatment\": \"free\",\n" + + " \"size\": 0\n" + + " },\n" + + " {\n" + + " \"treatment\": \"conta\",\n" + + " \"size\": 0\n" + + " }\n" + + " ],\n" + + " \"label\": \"default rule\"\n" + + " }\n" + + " ]\n" + + " }"; + } +} 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 135d21544..713f38c13 100644 --- a/src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java +++ b/src/test/java/io/split/android/client/impressions/ImpressionLoggingTaskTest.java @@ -24,7 +24,7 @@ public void setUp() { @Test public void executeLogsImpressionInListener() { - Impression impression = createImpression(); + DecoratedImpression impression = createImpression(); mImpressionsLoggingTask = new ImpressionLoggingTask(mSyncManager, impression); mImpressionsLoggingTask.run(); @@ -34,7 +34,7 @@ public void executeLogsImpressionInListener() { @Test public void successfulExecutionReturnsSuccessInfo() { - Impression impression = createImpression(); + DecoratedImpression impression = createImpression(); mImpressionsLoggingTask = new ImpressionLoggingTask(mSyncManager, impression); mImpressionsLoggingTask.run(); @@ -42,14 +42,14 @@ public void successfulExecutionReturnsSuccessInfo() { @Test public void unsuccessfulExecutionDoesNotCrash() { - doThrow(new RuntimeException("test")).when(mSyncManager).pushImpression(any(Impression.class)); - Impression impression = createImpression(); + doThrow(new RuntimeException("test")).when(mSyncManager).pushImpression(any(DecoratedImpression.class)); + DecoratedImpression impression = createImpression(); mImpressionsLoggingTask = new ImpressionLoggingTask(mSyncManager, impression); mImpressionsLoggingTask.run(); } - private static Impression createImpression() { - return new Impression("key", "feature", "treatment", "on", 1402040204L, "label", 123123L, new HashMap<>()); + private static DecoratedImpression createImpression() { + return new DecoratedImpression(new Impression("key", "feature", "treatment", "on", 1402040204L, "label", 123123L, new HashMap<>()), 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 3c6632098..4131e445f 100644 --- a/src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java +++ b/src/test/java/io/split/android/client/impressions/SyncImpressionListenerTest.java @@ -27,7 +27,7 @@ public void setUp() { @Test public void logSubmitsImpressionLoggingTaskInExecutor() { SyncImpressionListener syncImpressionListener = new SyncImpressionListener(mSyncManager, mExecutorService); - Impression impression = createImpression(); + DecoratedImpression impression = createImpression(); syncImpressionListener.log(impression); @@ -37,13 +37,13 @@ public void logSubmitsImpressionLoggingTaskInExecutor() { @Test public void errorWhileSubmittingTaskIsHandled() { SyncImpressionListener syncImpressionListener = new SyncImpressionListener(mSyncManager, mExecutorService); - Impression impression = createImpression(); + DecoratedImpression impression = createImpression(); doThrow(new RuntimeException("test")).when(mExecutorService).submit(any(ImpressionLoggingTask.class), any()); syncImpressionListener.log(impression); } - private static Impression createImpression() { - return new Impression("key", "feature", "treatment", "on", 1402040204L, "label", 123123L, new HashMap<>()); + private static DecoratedImpression createImpression() { + return new DecoratedImpression(new Impression("key", "feature", "treatment", "on", 1402040204L, "label", 123123L, new HashMap<>()), true); } } 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 58b19db94..79347551f 100644 --- a/src/test/java/io/split/android/client/service/SynchronizerTest.java +++ b/src/test/java/io/split/android/client/service/SynchronizerTest.java @@ -44,6 +44,7 @@ import io.split.android.client.dtos.KeyImpression; import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEventsManager; +import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.impressions.Impression; import io.split.android.client.service.events.EventsRecorderTask; import io.split.android.client.service.executor.SplitTask; @@ -55,12 +56,12 @@ import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpRecorder; -import io.split.android.client.service.impressions.ImpressionManager; import io.split.android.client.service.impressions.ImpressionManagerConfig; import io.split.android.client.service.impressions.ImpressionsCountRecorderTask; import io.split.android.client.service.impressions.ImpressionsMode; import io.split.android.client.service.impressions.ImpressionsRecorderTask; import io.split.android.client.service.impressions.SaveImpressionsCountTask; +import io.split.android.client.service.impressions.StrategyImpressionManager; import io.split.android.client.service.mysegments.LoadMySegmentsTask; import io.split.android.client.service.mysegments.MySegmentsSyncTask; import io.split.android.client.service.mysegments.MySegmentsTaskFactory; @@ -141,7 +142,7 @@ public class SynchronizerTest { private AttributesSynchronizerRegistryImpl mAttributesSynchronizerRegistry; @Mock private FeatureFlagsSynchronizer mFeatureFlagsSynchronizer; - ImpressionManager mImpressionManager; + private StrategyImpressionManager mImpressionManager; private final String mUserKey = "user_key"; @@ -195,7 +196,7 @@ public void setup(SplitClientConfig splitClientConfig, ImpressionManagerConfig.M .thenReturn(mRetryTimerSplitsUpdate); when(mRetryBackoffFactory.createWithFixedInterval(any(), eq(1), eq(3))) .thenReturn(mRetryTimerEventsRecorder); - mImpressionManager = Mockito.mock(ImpressionManager.class); + mImpressionManager = Mockito.mock(StrategyImpressionManager.class); mSynchronizer = new SynchronizerImpl(splitClientConfig, mTaskExecutor, mSingleThreadedTaskExecutor, mTaskFactory, mWorkManagerWrapper, mRetryBackoffFactory, mTelemetryRuntimeProducer, mAttributesSynchronizerRegistry, mMySegmentsSynchronizerRegistry, mImpressionManager, mFeatureFlagsSynchronizer, mSplitStorageContainer.getEventsStorage()); @@ -433,8 +434,8 @@ public void pushImpression() throws InterruptedException { .impressionsQueueSize(3) .build(); setup(config); - Impression impression = createImpression(); - ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(Impression.class); + DecoratedImpression impression = createImpression(); + ArgumentCaptor impressionCaptor = ArgumentCaptor.forClass(DecoratedImpression.class); mSynchronizer.startPeriodicRecording(); mSynchronizer.pushImpression(impression); Thread.sleep(200); @@ -442,13 +443,14 @@ public void pushImpression() throws InterruptedException { any(ImpressionsRecorderTask.class), any(SplitTaskExecutionListener.class)); verify(mImpressionManager).pushImpression(impressionCaptor.capture()); - Assert.assertEquals("key", impressionCaptor.getValue().key()); - Assert.assertEquals("bkey", impressionCaptor.getValue().bucketingKey()); - Assert.assertEquals("split", impressionCaptor.getValue().split()); - Assert.assertEquals("on", impressionCaptor.getValue().treatment()); - Assert.assertEquals(100L, impressionCaptor.getValue().time()); - Assert.assertEquals("default rule", impressionCaptor.getValue().appliedRule()); - Assert.assertEquals(999, impressionCaptor.getValue().changeNumber().longValue()); + Impression capturedImpression = impressionCaptor.getValue().getImpression(); + Assert.assertEquals("key", capturedImpression.key()); + Assert.assertEquals("bkey", capturedImpression.bucketingKey()); + Assert.assertEquals("split", capturedImpression.split()); + Assert.assertEquals("on", capturedImpression.treatment()); + Assert.assertEquals(100L, capturedImpression.time()); + Assert.assertEquals("default rule", capturedImpression.appliedRule()); + Assert.assertEquals(999, capturedImpression.changeNumber().longValue()); } @Test @@ -465,7 +467,7 @@ public void pushImpressionReachQueueSizeImpDebug() throws InterruptedException { mSynchronizer.pushImpression(createImpression()); } Thread.sleep(200); - verify(mImpressionManager, times(8)).pushImpression(any(Impression.class)); + verify(mImpressionManager, times(8)).pushImpression(any(DecoratedImpression.class)); } @Test @@ -482,7 +484,7 @@ public void pushImpressionReachQueueSizeImpOptimized() throws InterruptedExcepti mSynchronizer.pushImpression(createUniqueImpression()); } Thread.sleep(200); - verify(mImpressionManager, times(8)).pushImpression(any(Impression.class)); + verify(mImpressionManager, times(8)).pushImpression(any(DecoratedImpression.class)); } @Test @@ -500,7 +502,7 @@ public void pushImpressionBytesLimitImpDebug() throws InterruptedException { mSynchronizer.pushImpression(createImpression()); } Thread.sleep(200); - verify(mImpressionManager, times(10)).pushImpression(any(Impression.class)); + verify(mImpressionManager, times(10)).pushImpression(any(DecoratedImpression.class)); } @Test @@ -518,7 +520,7 @@ public void pushImpressionBytesLimitImpOptimized() throws InterruptedException { mSynchronizer.pushImpression(createUniqueImpression()); } Thread.sleep(200); - verify(mImpressionManager, times(10)).pushImpression(any(Impression.class)); + verify(mImpressionManager, times(10)).pushImpression(any(DecoratedImpression.class)); } @Test @@ -587,7 +589,7 @@ public void destroy() { .synchronizeInBackground(false) .build(); setup(config); - ImpressionManager impressionManager = mock(ImpressionManager.class); + StrategyImpressionManager impressionManager = mock(StrategyImpressionManager.class); when(mRetryBackoffFactory.create(any(), anyInt())) .thenReturn(mRetryTimerSplitsSync) .thenReturn(mRetryTimerSplitsUpdate); @@ -794,14 +796,14 @@ public void synchronizeSplitsDelegatesToFeatureFlagsSynchronizer() { public void tearDown() { } - private Impression createImpression() { - return new Impression("key", "bkey", "split", "on", - 100L, "default rule", 999L, null); + private DecoratedImpression createImpression() { + return new DecoratedImpression(new Impression("key", "bkey", "split", "on", + 100L, "default rule", 999L, null), true); } - private Impression createUniqueImpression() { - return new Impression("key", "bkey", UUID.randomUUID().toString(), "on", - 100L, "default rule", 999L, null); + private DecoratedImpression createUniqueImpression() { + return new DecoratedImpression(new Impression("key", "bkey", UUID.randomUUID().toString(), "on", + 100L, "default rule", 999L, null), true); } private KeyImpression keyImpression(Impression impression) { diff --git a/src/test/java/io/split/android/client/service/impressions/StrategyImpressionManagerTest.kt b/src/test/java/io/split/android/client/service/impressions/StrategyImpressionManagerTest.kt index b9128505e..d72103443 100644 --- a/src/test/java/io/split/android/client/service/impressions/StrategyImpressionManagerTest.kt +++ b/src/test/java/io/split/android/client/service/impressions/StrategyImpressionManagerTest.kt @@ -1,5 +1,6 @@ package io.split.android.client.service.impressions +import io.split.android.client.impressions.DecoratedImpression import io.split.android.client.impressions.Impression import io.split.android.client.service.impressions.strategy.PeriodicTracker import io.split.android.client.service.impressions.strategy.ProcessStrategy @@ -8,6 +9,8 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoInteractions +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations class StrategyImpressionManagerTest { @@ -18,12 +21,18 @@ class StrategyImpressionManagerTest { @Mock private lateinit var strategy: ProcessStrategy + @Mock + private lateinit var noneStrategy: ProcessStrategy + + @Mock + private lateinit var noneTracker: PeriodicTracker + private lateinit var impressionManager: StrategyImpressionManager @Before fun setUp() { MockitoAnnotations.openMocks(this) - impressionManager = StrategyImpressionManager(strategy, tracker) + impressionManager = StrategyImpressionManager(noneStrategy, noneTracker, strategy, tracker) } @Test @@ -31,6 +40,7 @@ class StrategyImpressionManagerTest { impressionManager.flush() verify(tracker).flush() + verify(noneTracker).flush() } @Test @@ -38,6 +48,7 @@ class StrategyImpressionManagerTest { impressionManager.startPeriodicRecording() verify(tracker).startPeriodicRecording() + verify(noneTracker).startPeriodicRecording() } @Test @@ -45,13 +56,61 @@ class StrategyImpressionManagerTest { impressionManager.stopPeriodicRecording() verify(tracker).stopPeriodicRecording() + verify(noneTracker).stopPeriodicRecording() } @Test fun `pushImpression calls apply on strategy`() { val impression = mock(Impression::class.java) - impressionManager.pushImpression(impression) + val decoratedImpression = DecoratedImpression(impression, true) + impressionManager.pushImpression(decoratedImpression) verify(strategy).apply(impression) + verifyNoInteractions(noneStrategy) + } + + @Test + fun `pushImpression calls apply on noneStrategy when trackImpressions is false`() { + val impression = mock(Impression::class.java) + val decoratedImpression = DecoratedImpression(impression, false) + impressionManager.pushImpression(decoratedImpression) + + verify(noneStrategy).apply(impression) + verifyNoInteractions(strategy) + } + + @Test + fun `pushImpression when it is decorated uses value from trackImpression to track`() { + val impression = mock(Impression::class.java) + val impression2 = mock(Impression::class.java) + val decoratedImpression = DecoratedImpression(impression, false) + val decoratedImpression2 = DecoratedImpression(impression2, true) + impressionManager.pushImpression(decoratedImpression) + impressionManager.pushImpression(decoratedImpression2) + + verify(strategy).apply(impression2) + verify(noneStrategy).apply(impression) + } + + @Test + fun `enableTracking set to true causes impressions to be sent to strategy`() { + impressionManager.enableTracking(true) + val impression = mock(Impression::class.java) + val decoratedImpression = DecoratedImpression(impression, true) + impressionManager.pushImpression(decoratedImpression) + + verify(strategy).apply(impression) + verifyNoInteractions(noneStrategy) + } + + @Test + fun `enableTracking set to false causes impressions to not be tracked`() { + impressionManager.enableTracking(false) + val impression = mock(Impression::class.java) + val decoratedImpression = DecoratedImpression(impression, true) + impressionManager.pushImpression(decoratedImpression) + + verifyNoInteractions(noneStrategy) + verifyNoInteractions(strategy) } } 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 d7f4a8607..25c9919d9 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 @@ -1,7 +1,6 @@ package io.split.android.client.service.impressions.strategy import io.split.android.client.dtos.KeyImpression -import io.split.android.client.impressions.Impression import io.split.android.client.service.executor.SplitTaskExecutionInfo import io.split.android.client.service.executor.SplitTaskExecutionListener import io.split.android.client.service.executor.SplitTaskExecutor @@ -17,7 +16,14 @@ import org.junit.Test import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.Mock -import org.mockito.Mockito.* +import org.mockito.Mockito.argThat +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.invocation.InvocationOnMock @@ -35,9 +41,6 @@ class DebugStrategyTest { @Mock private lateinit var telemetryRuntimeProducer: TelemetryRuntimeProducer - @Mock - private lateinit var tracker: PeriodicTracker - @Mock private lateinit var impressionsObserver: ImpressionsObserver @@ -52,7 +55,6 @@ class DebugStrategyTest { taskExecutor, taskFactory, telemetryRuntimeProducer, - tracker ) } @@ -88,41 +90,6 @@ class DebugStrategyTest { verify(taskExecutor, never()).submit(impressionsRecorderTask, impressionsSyncHelper) } - @Test - fun `flush calls flush on tracker`() { - strategy.flush() - - verify(tracker).flush() - } - - @Test - fun `startPeriodicRecording calls startPeriodicRecording on tracker`() { - strategy.startPeriodicRecording() - - verify(tracker).startPeriodicRecording() - } - - @Test - fun `stopPeriodicRecording calls stopPeriodicRecording on tracker`() { - strategy.stopPeriodicRecording() - - verify(tracker).stopPeriodicRecording() - } - - @Test - fun `stopPeriodicRecording calls persist on observer`() { - strategy.stopPeriodicRecording() - - verify(impressionsObserver).persist() - } - - @Test - fun `enableTracking calls enableTracking on tracker`() { - strategy.enableTracking(true) - - verify(tracker).enableTracking(true) - } - @Test fun `apply calls testAndSet on observer`() { val impression = createUniqueImpression() @@ -140,48 +107,6 @@ class DebugStrategyTest { spy(impression).withPreviousTime(20421) } - @Test - fun `call stop periodic tracking when sync listener returns do not retry`() { - val listenerCaptor = ArgumentCaptor.forClass(SplitTaskExecutionListener::class.java) - - `when`(impressionsSyncHelper.addListener(listenerCaptor.capture())).thenAnswer { it } - `when`(impressionsSyncHelper.taskExecuted(argThat { - it.taskType == SplitTaskType.IMPRESSIONS_RECORDER - })).thenAnswer { - listenerCaptor.value.taskExecuted( - SplitTaskExecutionInfo.error( - SplitTaskType.IMPRESSIONS_RECORDER, - mapOf(SplitTaskExecutionInfo.DO_NOT_RETRY to true) - ) - ) - it - } - - strategy = DebugStrategy( - impressionsObserver, - impressionsSyncHelper, - taskExecutor, - taskFactory, - telemetryRuntimeProducer, - tracker - ) - - strategy.startPeriodicRecording() - // simulate sync helper trigger - impressionsSyncHelper.taskExecuted( - SplitTaskExecutionInfo.error( - SplitTaskType.IMPRESSIONS_RECORDER, - mapOf(SplitTaskExecutionInfo.DO_NOT_RETRY to true) - ) - ) - - // start periodic recording again to verify it is not working anymore - strategy.startPeriodicRecording() - - verify(tracker, times(1)).startPeriodicRecording() - verify(tracker).stopPeriodicRecording() - } - @Test fun `do not submit recording task when push fails with do not retry`() { val listenerCaptor = ArgumentCaptor.forClass(SplitTaskExecutionListener::class.java) @@ -221,7 +146,6 @@ class DebugStrategyTest { taskExecutor, taskFactory, telemetryRuntimeProducer, - tracker ) // call apply two times; first one will trigger the recording task and second one should not diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt b/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt index 70d2ffd4e..6ddca42f7 100644 --- a/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt +++ b/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt @@ -1,19 +1,26 @@ package io.split.android.client.service.impressions.strategy import io.split.android.client.dtos.KeyImpression +import io.split.android.client.service.executor.SplitTaskExecutionInfo +import io.split.android.client.service.executor.SplitTaskExecutionListener import io.split.android.client.service.executor.SplitTaskExecutor +import io.split.android.client.service.executor.SplitTaskType import io.split.android.client.service.impressions.ImpressionsRecorderTask import io.split.android.client.service.impressions.ImpressionsTaskFactory +import io.split.android.client.service.impressions.observer.ImpressionsObserver import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer import io.split.android.client.service.synchronizer.RecorderSyncHelper import org.junit.Before import org.junit.Test +import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.MockitoAnnotations class DebugTrackerTest { + @Mock + private lateinit var impressionsObserver: ImpressionsObserver @Mock private lateinit var syncHelper: RecorderSyncHelper @Mock @@ -29,6 +36,7 @@ class DebugTrackerTest { fun setUp() { MockitoAnnotations.openMocks(this) tracker = DebugTracker( + impressionsObserver, syncHelper, taskExecutor, taskFactory, @@ -89,4 +97,57 @@ class DebugTrackerTest { verify(taskExecutor).stopTask("250") verify(taskExecutor).schedule(task2, 0L, 30L, syncHelper) } + + @Test + fun `stopPeriodicRecording calls persist on observer`() { + tracker.stopPeriodicRecording() + + verify(impressionsObserver).persist() + } + + @Test + fun `call stop periodic tracking when sync listener returns do not retry`() { + val listenerCaptor = ArgumentCaptor.forClass(SplitTaskExecutionListener::class.java) + + val impressionsRecorderTask = mock(ImpressionsRecorderTask::class.java) + `when`(taskFactory.createImpressionsRecorderTask()).thenReturn(impressionsRecorderTask) + `when`(taskExecutor.schedule(eq(impressionsRecorderTask), eq(0L), eq(30L), any())).thenReturn("250") + `when`(syncHelper.addListener(listenerCaptor.capture())).thenAnswer { it } + `when`(syncHelper.taskExecuted(argThat { + it.taskType == SplitTaskType.IMPRESSIONS_RECORDER + })).thenAnswer { + listenerCaptor.value.taskExecuted( + SplitTaskExecutionInfo.error( + SplitTaskType.IMPRESSIONS_RECORDER, + mapOf(SplitTaskExecutionInfo.DO_NOT_RETRY to true) + ) + ) + it + } + + tracker = DebugTracker( + impressionsObserver, + syncHelper, + taskExecutor, + taskFactory, + retryBackoffCounterTimer, + 30 + ) + + tracker.startPeriodicRecording() + val spy = spy(tracker) + // simulate sync helper trigger + syncHelper.taskExecuted( + SplitTaskExecutionInfo.error( + SplitTaskType.IMPRESSIONS_RECORDER, + mapOf(SplitTaskExecutionInfo.DO_NOT_RETRY to true) + ) + ) + + // start periodic recording again to verify it is not working anymore + spy.startPeriodicRecording() + + verify(taskExecutor).stopTask("250") + verify(impressionsObserver).persist() + } } 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 316ced052..daaeb89aa 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 @@ -7,7 +7,6 @@ import io.split.android.client.service.impressions.ImpressionsCounter import io.split.android.client.service.impressions.ImpressionsTaskFactory import io.split.android.client.service.impressions.unique.SaveUniqueImpressionsTask import io.split.android.client.service.impressions.unique.UniqueKeysTracker -import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.any @@ -31,15 +30,6 @@ class NoneStrategyTest { @Mock private lateinit var uniqueKeysTracker: UniqueKeysTracker - @Mock - private lateinit var countRetryTimer: RetryBackoffCounterTimer - - @Mock - private lateinit var uniqueKeysRetryTimer: RetryBackoffCounterTimer - - @Mock - private lateinit var tracker: PeriodicTracker - private lateinit var strategy: NoneStrategy @Before @@ -50,8 +40,7 @@ class NoneStrategyTest { taskFactory, impressionsCounter, uniqueKeysTracker, - true, - tracker + true ) } @@ -90,34 +79,6 @@ class NoneStrategyTest { 1 ) } - - @Test - fun `flush calls flush on tracker`() { - strategy.flush() - - verify(tracker).flush() - } - - @Test - fun `startPeriodicRecording calls startPeriodicRecording on tracker`() { - strategy.startPeriodicRecording() - - verify(tracker).startPeriodicRecording() - } - - @Test - fun `stopPeriodicRecording calls stopPeriodicRecording on tracker`() { - strategy.stopPeriodicRecording() - - verify(tracker).stopPeriodicRecording() - } - - @Test - fun `enableTracking calls enableTracking on tracker`() { - strategy.enableTracking(true) - - verify(tracker).enableTracking(true) - } } fun createUniqueImpression( diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/NoneTrackerTest.kt b/src/test/java/io/split/android/client/service/impressions/strategy/NoneTrackerTest.kt index 00028caa0..8d1ebe1f9 100644 --- a/src/test/java/io/split/android/client/service/impressions/strategy/NoneTrackerTest.kt +++ b/src/test/java/io/split/android/client/service/impressions/strategy/NoneTrackerTest.kt @@ -186,7 +186,6 @@ class NoneTrackerTest { verify(taskExecutor, never()).submit(eq(countTask), any()) } - @Test fun `stop periodic recording does not save unique keys when tracking is disabled`() { val countTask = mock(SaveUniqueImpressionsTask::class.java) 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 f404d9296..7e05fae92 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 @@ -52,9 +52,6 @@ class OptimizedStrategyTest { @Mock private lateinit var telemetryRuntimeProducer: TelemetryRuntimeProducer - @Mock - private lateinit var tracker: PeriodicTracker - private lateinit var strategy: OptimizedStrategy private val dedupeTimeInterval = ServiceConstants.DEFAULT_IMPRESSIONS_DEDUPE_TIME_INTERVAL @@ -69,8 +66,6 @@ class OptimizedStrategyTest { taskExecutor, taskFactory, telemetryRuntimeProducer, - true, - tracker, dedupeTimeInterval ) } @@ -155,86 +150,6 @@ class OptimizedStrategyTest { ) } - @Test - fun `flush calls flush on tracker`() { - strategy.flush() - - verify(tracker).flush() - } - - @Test - fun `startPeriodicRecording calls startPeriodicRecording on tracker`() { - strategy.startPeriodicRecording() - - verify(tracker).startPeriodicRecording() - } - - @Test - fun `stopPeriodicRecording calls stopPeriodicRecording on tracker`() { - strategy.stopPeriodicRecording() - - verify(tracker).stopPeriodicRecording() - } - - @Test - fun `stopPeriodicRecording calls persist on ImpressionsObserver`() { - strategy.stopPeriodicRecording() - - verify(impressionsObserver).persist() - } - - @Test - fun `enableTracking calls enableTracking on tracker`() { - strategy.enableTracking(true) - - verify(tracker).enableTracking(true) - } - - @Test - fun `call stop periodic tracking when sync listener returns do not retry`() { - val listenerCaptor = ArgumentCaptor.forClass(SplitTaskExecutionListener::class.java) - - `when`(impressionsSyncHelper.addListener(listenerCaptor.capture())).thenAnswer { it } - `when`(impressionsSyncHelper.taskExecuted(argThat { - it.taskType == SplitTaskType.IMPRESSIONS_RECORDER - })).thenAnswer { - listenerCaptor.value.taskExecuted( - SplitTaskExecutionInfo.error( - SplitTaskType.IMPRESSIONS_RECORDER, - mapOf(DO_NOT_RETRY to true) - ) - ) - it - } - - strategy = OptimizedStrategy( - impressionsObserver, - impressionsCounter, - impressionsSyncHelper, - taskExecutor, - taskFactory, - telemetryRuntimeProducer, - true, - tracker, - dedupeTimeInterval - ) - - strategy.startPeriodicRecording() - // simulate sync helper trigger - impressionsSyncHelper.taskExecuted( - SplitTaskExecutionInfo.error( - SplitTaskType.IMPRESSIONS_RECORDER, - mapOf(DO_NOT_RETRY to true) - ) - ) - - // start periodic recording again to verify it is not working anymore - strategy.startPeriodicRecording() - - verify(tracker, times(1)).startPeriodicRecording() - verify(tracker).stopPeriodicRecording() - } - @Test fun `do not submit recording task when push fails with do not retry`() { val listenerCaptor = ArgumentCaptor.forClass(SplitTaskExecutionListener::class.java) @@ -275,8 +190,6 @@ class OptimizedStrategyTest { taskExecutor, taskFactory, telemetryRuntimeProducer, - true, - tracker, dedupeTimeInterval ) diff --git a/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt b/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt index 51f7d6a4b..71915b9ca 100644 --- a/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt +++ b/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt @@ -1,14 +1,19 @@ package io.split.android.client.service.impressions.strategy import io.split.android.client.dtos.KeyImpression +import io.split.android.client.service.executor.SplitTaskExecutionInfo +import io.split.android.client.service.executor.SplitTaskExecutionInfo.DO_NOT_RETRY import io.split.android.client.service.executor.SplitTaskExecutionListener import io.split.android.client.service.executor.SplitTaskExecutor import io.split.android.client.service.executor.SplitTaskSerialWrapper +import io.split.android.client.service.executor.SplitTaskType import io.split.android.client.service.impressions.* +import io.split.android.client.service.impressions.observer.ImpressionsObserver import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer import io.split.android.client.service.synchronizer.RecorderSyncHelper import org.junit.Before import org.junit.Test +import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.* import org.mockito.MockitoAnnotations @@ -25,13 +30,10 @@ class OptimizedTrackerTest { private lateinit var impressionTimer: RetryBackoffCounterTimer @Mock - private lateinit var impressionCountTimer: RetryBackoffCounterTimer + private lateinit var syncHelper: RecorderSyncHelper @Mock - private lateinit var impressionCounter: ImpressionsCounter - - @Mock - private lateinit var syncHelper: RecorderSyncHelper + private lateinit var impressionsObserver: ImpressionsObserver private lateinit var tracker: OptimizedTracker @@ -39,34 +41,24 @@ class OptimizedTrackerTest { fun setUp() { MockitoAnnotations.openMocks(this) tracker = OptimizedTracker( - impressionCounter, + impressionsObserver, syncHelper, taskExecutor, taskFactory, impressionTimer, - impressionCountTimer, 30, - 40, true ) } @Test - fun `flush flushes impression count`() { - val saveTask = mock(SaveImpressionsCountTask::class.java) - val recorderTask = mock(ImpressionsCountRecorderTask::class.java) - `when`(taskFactory.createSaveImpressionsCountTask(any())) - .thenReturn(saveTask) - `when`(taskFactory.createImpressionsCountRecorderTask()).thenReturn(recorderTask) + fun `start periodic recording schedules impression recorder task`() { + val task = mock(ImpressionsRecorderTask::class.java) + `when`(taskFactory.createImpressionsRecorderTask()).thenReturn(task) - tracker.flush() + tracker.startPeriodicRecording() - verify(impressionCountTimer) - .setTask(argThat { argument -> - val taskList = argument.taskList - taskList.size == 2 && taskList[0] == saveTask && taskList[1] == recorderTask - }) - verify(impressionCountTimer).start() + verify(taskExecutor).schedule(task, 0L, 30L, syncHelper) } @Test @@ -90,16 +82,6 @@ class OptimizedTrackerTest { verify(taskExecutor).schedule(task, 0L, 30L, syncHelper) } - @Test - fun `start periodic recording schedules impression count recorder task`() { - val task = mock(ImpressionsCountRecorderTask::class.java) - `when`(taskFactory.createImpressionsCountRecorderTask()).thenReturn(task) - - tracker.startPeriodicRecording() - - verify(taskExecutor).schedule(task, 0L, 40L, null) - } - @Test fun `stop periodic recording stops impression recording`() { val task = mock(ImpressionsRecorderTask::class.java) @@ -115,38 +97,56 @@ class OptimizedTrackerTest { } @Test - fun `stop periodic recording stops impression count recording`() { - val countTask = mock(ImpressionsCountRecorderTask::class.java) - `when`(taskFactory.createImpressionsCountRecorderTask()).thenReturn(countTask) - `when`( - taskExecutor.schedule( - any(ImpressionsCountRecorderTask::class.java), - eq(0L), - eq(40L), - eq(null) - ) - ).thenReturn("id_1") - tracker.startPeriodicRecording() + fun `stopPeriodicRecording calls persist on ImpressionsObserver`() { tracker.stopPeriodicRecording() - verify(taskExecutor).stopTask("id_1") + verify(impressionsObserver).persist() } @Test - fun `stop periodic recording does not save impression count when tracking is disabled`() { - val countTask = mock(SaveImpressionsCountTask::class.java) - `when`(taskFactory.createSaveImpressionsCountTask(any())).thenReturn(countTask) - `when`( - taskExecutor.schedule( - any(SaveImpressionsCountTask::class.java), - eq(0L), - eq(40L), - eq(null) + fun `call stop periodic tracking when sync listener returns do not retry`() { + val listenerCaptor = ArgumentCaptor.forClass(SplitTaskExecutionListener::class.java) + + val impressionsRecorderTask = mock(ImpressionsRecorderTask::class.java) + `when`(taskFactory.createImpressionsRecorderTask()).thenReturn(impressionsRecorderTask) + `when`(taskExecutor.schedule(eq(impressionsRecorderTask), eq(0L), eq(30L), any())).thenReturn("250") + `when`(syncHelper.addListener(listenerCaptor.capture())).thenAnswer { it } + `when`(syncHelper.taskExecuted(argThat { + it.taskType == SplitTaskType.IMPRESSIONS_RECORDER + })).thenAnswer { + listenerCaptor.value.taskExecuted( + SplitTaskExecutionInfo.error( + SplitTaskType.IMPRESSIONS_RECORDER, + mapOf(DO_NOT_RETRY to true) + ) ) - ).thenReturn("id_1") - tracker.enableTracking(false) - tracker.stopPeriodicRecording() + it + } - verify(taskExecutor, never()).submit(eq(countTask), any()) + tracker = OptimizedTracker( + impressionsObserver, + syncHelper, + taskExecutor, + taskFactory, + impressionTimer, + 30, + true + ) + + val spy = spy(tracker) + tracker.startPeriodicRecording() + // simulate sync helper trigger + syncHelper.taskExecuted( + SplitTaskExecutionInfo.error( + SplitTaskType.IMPRESSIONS_RECORDER, + mapOf(DO_NOT_RETRY to true) + ) + ) + + // start periodic recording again to verify it is not working anymore + spy.startPeriodicRecording() + + verify(taskExecutor).stopTask("250") + verify(impressionsObserver).persist() } } diff --git a/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java b/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java index dcd5a45ad..1888bf831 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java +++ b/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java @@ -52,13 +52,40 @@ public void listenerIsNotCalledOnceRemoved() { mTaskExecutor); SplitTaskExecutionListener listener = mock(SplitTaskExecutionListener.class); + SplitTaskExecutionListener listener2 = mock(SplitTaskExecutionListener.class); SplitTaskExecutionInfo executionInfo = SplitTaskExecutionInfo.success(SplitTaskType.IMPRESSIONS_RECORDER); syncHelper.addListener(listener); + syncHelper.addListener(listener2); syncHelper.taskExecuted(executionInfo); syncHelper.removeListener(listener); syncHelper.taskExecuted(executionInfo); verify(listener, times(1)).taskExecuted(executionInfo); + verify(listener2, times(2)).taskExecuted(executionInfo); + } + + @Test + public void multipleListenersCanBeAdded() { + mStoragePusher = mock(StoragePusher.class); + mTaskExecutor = mock(SplitTaskExecutor.class); + + RecorderSyncHelperImpl syncHelper = new RecorderSyncHelperImpl<>( + SplitTaskType.IMPRESSIONS_RECORDER, + mStoragePusher, + 1, + 1, + mTaskExecutor); + + SplitTaskExecutionListener listener1 = mock(SplitTaskExecutionListener.class); + SplitTaskExecutionListener listener2 = mock(SplitTaskExecutionListener.class); + SplitTaskExecutionInfo executionInfo = SplitTaskExecutionInfo.success(SplitTaskType.IMPRESSIONS_RECORDER); + + syncHelper.addListener(listener1); + syncHelper.addListener(listener2); + syncHelper.taskExecuted(executionInfo); + + verify(listener1).taskExecuted(executionInfo); + verify(listener2).taskExecuted(executionInfo); } } diff --git a/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java b/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java index 367282052..2c83e3ada 100644 --- a/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java +++ b/src/test/java/io/split/android/client/service/telemetry/SynchronizerImplTelemetryTest.java @@ -23,7 +23,7 @@ import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskFactory; import io.split.android.client.service.executor.SplitTaskType; -import io.split.android.client.service.impressions.ImpressionManager; +import io.split.android.client.service.impressions.StrategyImpressionManager; import io.split.android.client.service.splits.SplitsSyncTask; import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; @@ -55,7 +55,7 @@ public class SynchronizerImplTelemetryTest { @Mock MySegmentsSynchronizerRegistryImpl mMySegmentsSynchronizerRegistry; @Mock - ImpressionManager mImpressionManager; + StrategyImpressionManager mImpressionManager; @Mock PushManagerEventBroadcaster mPushManagerEventBroadcaster; diff --git a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java b/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java index 73f254230..36b8728a5 100644 --- a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java +++ b/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java @@ -14,6 +14,7 @@ import io.split.android.client.attributes.AttributesMergerImpl; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.impressions.DecoratedImpressionListener; import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.shared.SplitClientContainer; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; @@ -37,7 +38,7 @@ public static SplitClientImpl get(Key key, SplitsStorage splitsStorage) { SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class), mock(MySegmentsStorageContainer.class)); TelemetryStorage telemetryStorage = mock(TelemetryStorage.class); TreatmentManagerFactory treatmentManagerFactory = new TreatmentManagerFactoryImpl( - new KeyValidatorImpl(), new SplitValidatorImpl(), new ImpressionListener.NoopImpressionListener(), + new KeyValidatorImpl(), new SplitValidatorImpl(), new ImpressionListener.FederatedImpressionListener(mock(DecoratedImpressionListener.class), Collections.emptyList()), false, new AttributesMergerImpl(), telemetryStorage, splitParser, new FlagSetsFilterImpl(Collections.emptySet()), splitsStorage); diff --git a/src/test/java/io/split/android/engine/experiments/SplitParserTest.java b/src/test/java/io/split/android/engine/experiments/SplitParserTest.java index e6c1d6ca6..ae7024f8d 100644 --- a/src/test/java/io/split/android/engine/experiments/SplitParserTest.java +++ b/src/test/java/io/split/android/engine/experiments/SplitParserTest.java @@ -3,7 +3,9 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -495,6 +497,22 @@ public void inLargeSegmentMatcherParsingTest() { verify(mMySegmentsStorageContainer, never()).getStorageForKey("matching_key"); } + @Test + public void trackImpressionsParsingTest(){ + SplitParser parser = createParser(); + + Split split = makeSplit("splitName", Collections.emptyList()); + split.trackImpressions = false; + Split split2 = makeSplit("splitName", Collections.emptyList()); + split2.trackImpressions = true; + + ParsedSplit actual = parser.parse(split); + ParsedSplit actual2 = parser.parse(split2); + + assertFalse(actual.trackImpressions()); + assertTrue(actual2.trackImpressions()); + } + private void set_matcher_test(Condition c, io.split.android.engine.matchers.Matcher m) { SplitParser parser = createParser(); @@ -536,6 +554,7 @@ private Split makeSplit(String name, List conditions, long changeNumb split.algo = 1; split.configurations = configurations; split.sets = Collections.emptySet(); + split.trackImpressions = true; return split; } diff --git a/src/test/java/io/split/android/fake/ImpressionListenerMock.java b/src/test/java/io/split/android/fake/ImpressionListenerMock.java index ce2ff7bd1..e9d53773f 100644 --- a/src/test/java/io/split/android/fake/ImpressionListenerMock.java +++ b/src/test/java/io/split/android/fake/ImpressionListenerMock.java @@ -1,13 +1,19 @@ package io.split.android.fake; +import io.split.android.client.impressions.DecoratedImpression; +import io.split.android.client.impressions.DecoratedImpressionListener; import io.split.android.client.impressions.Impression; import io.split.android.client.impressions.ImpressionListener; -public class ImpressionListenerMock implements ImpressionListener { +public class ImpressionListenerMock implements ImpressionListener, DecoratedImpressionListener { @Override public void log(Impression impression) { } + @Override + public void log(DecoratedImpression impression) { + } + @Override public void close() { } diff --git a/src/test/java/io/split/android/helpers/SplitHelper.java b/src/test/java/io/split/android/helpers/SplitHelper.java index 0aec872e8..c0f4792c0 100644 --- a/src/test/java/io/split/android/helpers/SplitHelper.java +++ b/src/test/java/io/split/android/helpers/SplitHelper.java @@ -122,7 +122,8 @@ public static ParsedSplit createParsedSplit( seed, algo, configurations, - Collections.emptySet() + Collections.emptySet(), + true ); }