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