From dbe1906941ad783e10b56226b327a6dd6c335b4b Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Wed, 31 Jul 2024 20:03:45 -0700 Subject: [PATCH 1/6] fix: update consumer proguard rules for kotlinx.serialization --- sdk/consumer-rules.pro | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/sdk/consumer-rules.pro b/sdk/consumer-rules.pro index e69de29..701ba63 100644 --- a/sdk/consumer-rules.pro +++ b/sdk/consumer-rules.pro @@ -0,0 +1,35 @@ +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} + +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} + +# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault + +# Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes +# See also https://github.com/Kotlin/kotlinx.serialization/issues/1900 +-dontnote kotlinx.serialization.** + +# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes. +# If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning. +# However, since in this case they will not be used, we can disable these warnings +-dontwarn kotlinx.serialization.internal.ClassValueReferences \ No newline at end of file From 5d3fb2ff0346e2b04afed6493b1ad3fa5df05d4a Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 16 Aug 2024 09:31:59 -0700 Subject: [PATCH 2/6] fix: add consumer proguard rules for serialization; update gradle --- .gitignore | 1 + analytics-connector/build.gradle | 4 ++++ .../src/main/AndroidManifest.xml | 3 +-- build.gradle | 4 ++-- example/build.gradle | 23 +++++++++++-------- example/src/main/AndroidManifest.xml | 6 ++--- .../exampleapp/ExampleApplication.java | 4 ---- .../amplitude/exampleapp/MainActivity.java | 1 - gradle.properties | 3 +++ gradle/wrapper/gradle-wrapper.properties | 2 +- sdk/build.gradle | 4 ++++ sdk/src/main/AndroidManifest.xml | 3 +-- 12 files changed, 33 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 4f7b373..78d201b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /captures .externalNativeBuild .cxx +.kotlin diff --git a/analytics-connector/build.gradle b/analytics-connector/build.gradle index aaf2ede..4986e6c 100644 --- a/analytics-connector/build.gradle +++ b/analytics-connector/build.gradle @@ -33,6 +33,10 @@ android { sourceCompatibility 1.8 targetCompatibility 1.8 } + kotlinOptions { + jvmTarget = '1.8' + } + namespace 'com.amplitude.analytics.connector' } dependencies { diff --git a/analytics-connector/src/main/AndroidManifest.xml b/analytics-connector/src/main/AndroidManifest.xml index e937d4c..8bdb7e1 100644 --- a/analytics-connector/src/main/AndroidManifest.xml +++ b/analytics-connector/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/build.gradle b/build.gradle index 23c3cac..fa76bcb 100644 --- a/build.gradle +++ b/build.gradle @@ -15,8 +15,8 @@ buildscript { } dependencies { - classpath "com.android.tools.build:gradle:7.0.4" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.android.tools.build:gradle:8.5.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.10" classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" classpath "io.github.gradle-nexus:publish-plugin:1.1.0" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.1.0" diff --git a/example/build.gradle b/example/build.gradle index 924ae72..9de1399 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' +apply plugin: 'org.jetbrains.kotlin.android' android { - compileSdkVersion 30 - buildToolsVersion "30.0.1" + compileSdkVersion 34 defaultConfig { applicationId "com.amplitude.exampleapp" - minSdkVersion 17 - targetSdkVersion 30 + minSdkVersion 19 + targetSdkVersion 34 versionCode 1 versionName "1.0" @@ -21,13 +21,14 @@ android { } } compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } -} -configurations.all { - resolutionStrategy.cacheChangingModulesFor 0, 'seconds' + kotlinOptions { + jvmTarget = '17' + } + namespace 'com.amplitude.exampleapp' } dependencies { @@ -39,10 +40,12 @@ dependencies { implementation 'androidx.navigation:navigation-fragment:2.1.0' implementation 'androidx.navigation:navigation-ui:2.1.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'com.amplitude:android-sdk:2.30.0' + implementation 'com.amplitude:android-sdk:2.39.8' // implementation("com.amplitude:experiment-android-client:0.0.4") implementation project(path: ':sdk') implementation 'com.squareup.okhttp3:okhttp:4.2.2' + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 0184d3d..66a2b79 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + + android:label="@string/app_name" + android:exported="true"> diff --git a/example/src/main/java/com/amplitude/exampleapp/ExampleApplication.java b/example/src/main/java/com/amplitude/exampleapp/ExampleApplication.java index c324139..91d6b93 100644 --- a/example/src/main/java/com/amplitude/exampleapp/ExampleApplication.java +++ b/example/src/main/java/com/amplitude/exampleapp/ExampleApplication.java @@ -2,10 +2,6 @@ import android.app.Application; -import com.amplitude.api.Amplitude; -import com.amplitude.api.AmplitudeAnalyticsProvider; -import com.amplitude.api.AmplitudeClient; -import com.amplitude.api.AmplitudeUserProvider; import com.amplitude.experiment.Experiment; import com.amplitude.experiment.ExperimentClient; import com.amplitude.experiment.ExperimentConfig; diff --git a/example/src/main/java/com/amplitude/exampleapp/MainActivity.java b/example/src/main/java/com/amplitude/exampleapp/MainActivity.java index 80e456d..0fc9779 100644 --- a/example/src/main/java/com/amplitude/exampleapp/MainActivity.java +++ b/example/src/main/java/com/amplitude/exampleapp/MainActivity.java @@ -3,7 +3,6 @@ import android.os.Bundle; import android.widget.TextView; -import com.amplitude.experiment.Experiment; import com.amplitude.experiment.ExperimentClient; import com.amplitude.experiment.Variant; import com.google.android.material.bottomnavigation.BottomNavigationView; diff --git a/gradle.properties b/gradle.properties index c964866..67d28cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,3 +26,6 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_ID=amplitude POM_DEVELOPER_NAME=Amplitude POM_DEVELOPER_EMAIL=dev@amplitude.com +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3b0f97a..0315014 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/sdk/build.gradle b/sdk/build.gradle index 58733d4..e1ad0fd 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -37,6 +37,10 @@ android { sourceCompatibility 1.8 targetCompatibility 1.8 } + kotlinOptions { + jvmTarget = '1.8' + } + namespace 'com.amplitude.experiment' } dependencies { diff --git a/sdk/src/main/AndroidManifest.xml b/sdk/src/main/AndroidManifest.xml index fd52e66..b07e428 100644 --- a/sdk/src/main/AndroidManifest.xml +++ b/sdk/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + From 840d8c2d6e9242e9cdd268ddd75cd62d897ed385 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 16 Aug 2024 13:09:10 -0700 Subject: [PATCH 3/6] fix: update build gradle --- build.gradle | 6 ++--- example/build.gradle | 3 --- .../exampleapp/ExampleInstrumentedTest.java | 26 ------------------- sdk/build.gradle | 2 +- 4 files changed, 4 insertions(+), 33 deletions(-) delete mode 100644 example/src/androidTest/java/com/amplitude/exampleapp/ExampleInstrumentedTest.java diff --git a/build.gradle b/build.gradle index fa76bcb..d7009c3 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ apply plugin: 'org.jetbrains.dokka' apply plugin: 'org.jlleitschuh.gradle.ktlint' buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.8.22' ext.dokka_version = '1.4.32' repositories { @@ -16,10 +16,10 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.5.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.10" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" classpath "io.github.gradle-nexus:publish-plugin:1.1.0" - classpath "org.jlleitschuh.gradle:ktlint-gradle:10.1.0" + classpath "org.jlleitschuh.gradle:ktlint-gradle:12.1.1" } } diff --git a/example/build.gradle b/example/build.gradle index 9de1399..5438729 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -41,12 +41,9 @@ dependencies { implementation 'androidx.navigation:navigation-ui:2.1.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'com.amplitude:android-sdk:2.39.8' -// implementation("com.amplitude:experiment-android-client:0.0.4") implementation project(path: ':sdk') implementation 'com.squareup.okhttp3:okhttp:4.2.2' implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } diff --git a/example/src/androidTest/java/com/amplitude/exampleapp/ExampleInstrumentedTest.java b/example/src/androidTest/java/com/amplitude/exampleapp/ExampleInstrumentedTest.java deleted file mode 100644 index 33c4d6c..0000000 --- a/example/src/androidTest/java/com/amplitude/exampleapp/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.amplitude.exampleapp; - -import android.content.Context; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("com.amplitude.exampleapp", appContext.getPackageName()); - } -} diff --git a/sdk/build.gradle b/sdk/build.gradle index e1ad0fd..da0c70d 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -51,7 +51,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3") testImplementation 'junit:junit:4.13.2' - testImplementation 'org.json:json:20201115' + testImplementation 'org.json:json:20240303' testImplementation project(path: ':sdk') testImplementation "io.mockk:mockk:1.12.0" } From 5844afb49c0da7e00cfaad47b6a87d1682341a17 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 16 Aug 2024 13:12:52 -0700 Subject: [PATCH 4/6] fix: full lint --- .../api/AmplitudeAnalyticsProvider.kt | 3 +- .../amplitude/api/AmplitudeUserProvider.kt | 19 +- .../amplitude/evaluation/EvaluationBucket.kt | 2 - .../evaluation/EvaluationCondition.kt | 4 +- .../amplitude/evaluation/EvaluationContext.kt | 1 - .../evaluation/EvaluationDistribution.kt | 1 - .../amplitude/evaluation/EvaluationEngine.kt | 98 +- .../amplitude/evaluation/EvaluationFlag.kt | 6 +- .../amplitude/evaluation/EvaluationSegment.kt | 5 +- .../evaluation/EvaluationSerialization.kt | 48 +- .../java/com/amplitude/evaluation/Logger.kt | 29 +- .../java/com/amplitude/evaluation/Murmur3.kt | 16 +- .../com/amplitude/evaluation/Selectable.kt | 11 +- .../amplitude/evaluation/SemanticVersion.kt | 4 +- .../amplitude/evaluation/TopologicalSort.kt | 11 +- .../ConnectorExposureTrackingProvider.kt | 18 +- .../experiment/ConnectorUserProvider.kt | 28 +- .../experiment/DefaultExperimentClient.kt | 377 +++--- .../experiment/DefaultUserProvider.kt | 1 - .../com/amplitude/experiment/Experiment.kt | 123 +- .../amplitude/experiment/ExperimentClient.kt | 10 +- .../amplitude/experiment/ExperimentConfig.kt | 161 ++- .../amplitude/experiment/ExperimentUser.kt | 83 +- .../java/com/amplitude/experiment/Exposure.kt | 2 +- .../com/amplitude/experiment/FetchOptions.kt | 8 +- .../java/com/amplitude/experiment/FlagApi.kt | 77 +- .../java/com/amplitude/experiment/Variant.kt | 94 +- .../analytics/ExperimentAnalyticsProvider.kt | 1 - .../experiment/analytics/ExposureEvent.kt | 23 +- .../com/amplitude/experiment/storage/Cache.kt | 57 +- .../experiment/storage/SharedPrefsStorage.kt | 43 +- .../amplitude/experiment/storage/Storage.kt | 7 +- .../amplitude/experiment/util/AsyncFuture.kt | 10 +- .../com/amplitude/experiment/util/Backoff.kt | 69 +- .../experiment/util/FetchException.kt | 2 +- .../com/amplitude/experiment/util/Lock.kt | 4 +- .../com/amplitude/experiment/util/Logger.kt | 36 +- .../com/amplitude/experiment/util/Poller.kt | 9 +- .../util/SessionAnalyticsProvider.kt | 1 - .../com/amplitude/experiment/util/User.kt | 31 +- .../util/UserSessionExposureTracker.kt | 18 +- .../com/amplitude/experiment/util/Variant.kt | 133 +- .../ConnectorExposureTrackingProviderTest.kt | 3 +- .../experiment/ExperimentClientTest.kt | 1177 +++++++++-------- .../experiment/ExperimentUserTest.kt | 461 ++++--- .../com/amplitude/experiment/StorageTest.kt | 113 +- .../UserSessionExposureTrackerTest.kt | 1 - .../com/amplitude/experiment/VariantTest.kt | 1 - .../amplitude/experiment/util/MockStorage.kt | 25 +- .../amplitude/experiment/util/SystemLogger.kt | 11 +- .../util/TestExposureTrackingProvider.kt | 1 + 51 files changed, 1915 insertions(+), 1562 deletions(-) diff --git a/sdk/src/main/java/com/amplitude/api/AmplitudeAnalyticsProvider.kt b/sdk/src/main/java/com/amplitude/api/AmplitudeAnalyticsProvider.kt index 8259510..499965c 100644 --- a/sdk/src/main/java/com/amplitude/api/AmplitudeAnalyticsProvider.kt +++ b/sdk/src/main/java/com/amplitude/api/AmplitudeAnalyticsProvider.kt @@ -10,12 +10,11 @@ import org.json.JSONObject */ @Deprecated( "Update your version of the amplitude analytics SDK to 2.36.0+ and for seamless " + - "integration with the amplitude analytics SDK" + "integration with the amplitude analytics SDK", ) class AmplitudeAnalyticsProvider( private val amplitudeClient: AmplitudeClient, ) : ExperimentAnalyticsProvider { - override fun track(event: ExperimentAnalyticsEvent) { amplitudeClient.logEvent(event.name, JSONObject(event.properties)) } diff --git a/sdk/src/main/java/com/amplitude/api/AmplitudeUserProvider.kt b/sdk/src/main/java/com/amplitude/api/AmplitudeUserProvider.kt index 8e0ceb9..3cd0b9d 100644 --- a/sdk/src/main/java/com/amplitude/api/AmplitudeUserProvider.kt +++ b/sdk/src/main/java/com/amplitude/api/AmplitudeUserProvider.kt @@ -12,10 +12,9 @@ import java.util.Locale @Deprecated( "Update your version of the amplitude analytics SDK to 2.36.0+ and for seamless " + - "integration with the amplitude analytics SDK" + "integration with the amplitude analytics SDK", ) class AmplitudeUserProvider(private val amplitudeClient: AmplitudeClient) : ExperimentUserProvider { - private var initialized = false private var version: String? = null private var carrier: String? = null @@ -39,8 +38,8 @@ class AmplitudeUserProvider(private val amplitudeClient: AmplitudeClient) : Expe Logger.d( String.format( "Waited %.3f ms for Amplitude SDK initialization", - (end - start) / 1000000.0 - ) + (end - start) / 1000000.0, + ), ) } @@ -48,9 +47,10 @@ class AmplitudeUserProvider(private val amplitudeClient: AmplitudeClient) : Expe if (amplitudeClient.context != null) { val packageInfo: PackageInfo try { - packageInfo = amplitudeClient.context.packageManager.getPackageInfo( - amplitudeClient.context.packageName, 0 - ) + packageInfo = + amplitudeClient.context.packageManager.getPackageInfo( + amplitudeClient.context.packageName, 0, + ) version = packageInfo.versionName } catch (ignored: PackageManager.NameNotFoundException) { } catch (ignored: Exception) { @@ -61,8 +61,9 @@ class AmplitudeUserProvider(private val amplitudeClient: AmplitudeClient) : Expe private fun cacheCarrier() { if (amplitudeClient.context != null) { try { - val manager = amplitudeClient.context - .getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + val manager = + amplitudeClient.context + .getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager carrier = manager.networkOperatorName } catch (e: Exception) { // Failed to get network operator name from network diff --git a/sdk/src/main/java/com/amplitude/evaluation/EvaluationBucket.kt b/sdk/src/main/java/com/amplitude/evaluation/EvaluationBucket.kt index e8cc31e..0556f55 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/EvaluationBucket.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/EvaluationBucket.kt @@ -6,10 +6,8 @@ import kotlinx.serialization.Serializable internal data class EvaluationBucket( // How to select the prop from the context. val selector: List, - // A random string used to salt the bucketing value prior to hashing. val salt: String, - // Determines which variant, if any, should be returned based on the // result of the hash functions. val allocations: List, diff --git a/sdk/src/main/java/com/amplitude/evaluation/EvaluationCondition.kt b/sdk/src/main/java/com/amplitude/evaluation/EvaluationCondition.kt index 2a6102e..c32384b 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/EvaluationCondition.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/EvaluationCondition.kt @@ -6,10 +6,8 @@ import kotlinx.serialization.Serializable internal data class EvaluationCondition( // How to select the property from the evaluation state. val selector: List, - // The operator. val op: String, - // The values to compare to. - val values: Set + val values: Set, ) diff --git a/sdk/src/main/java/com/amplitude/evaluation/EvaluationContext.kt b/sdk/src/main/java/com/amplitude/evaluation/EvaluationContext.kt index e8d8120..4baa47a 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/EvaluationContext.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/EvaluationContext.kt @@ -4,6 +4,5 @@ import kotlinx.serialization.Serializable @Serializable internal class EvaluationContext : MutableMap by LinkedHashMap(), Selectable { - override fun select(selector: String): Any? = this[selector] } diff --git a/sdk/src/main/java/com/amplitude/evaluation/EvaluationDistribution.kt b/sdk/src/main/java/com/amplitude/evaluation/EvaluationDistribution.kt index af70556..85dee8d 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/EvaluationDistribution.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/EvaluationDistribution.kt @@ -6,7 +6,6 @@ import kotlinx.serialization.Serializable internal data class EvaluationDistribution( // The key of the variant to deliver if this range matches. val variant: String, - // The distribution range [start, end), where the max value is 42949672. // E.g. [0, 42949673] = [0%, 100%] val range: List, diff --git a/sdk/src/main/java/com/amplitude/evaluation/EvaluationEngine.kt b/sdk/src/main/java/com/amplitude/evaluation/EvaluationEngine.kt index 860e93e..3102226 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/EvaluationEngine.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/EvaluationEngine.kt @@ -8,15 +8,14 @@ import kotlinx.serialization.json.JsonArray internal interface EvaluationEngine { fun evaluate( context: EvaluationContext, - flags: List + flags: List, ): Map } internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) : EvaluationEngine { - data class EvaluationTarget( val context: EvaluationContext, - val result: MutableMap + val result: MutableMap, ) : Selectable { override fun select(selector: String): Any? { return when (selector) { @@ -29,7 +28,7 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) override fun evaluate( context: EvaluationContext, - flags: List + flags: List, ): Map { log?.debug { "Evaluating flags ${flags.map { it.key }} with context $context." } val results: MutableMap = mutableMapOf() @@ -47,7 +46,10 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) return results } - private fun evaluateFlag(target: EvaluationTarget, flag: EvaluationFlag): EvaluationVariant? { + private fun evaluateFlag( + target: EvaluationTarget, + flag: EvaluationFlag, + ): EvaluationVariant? { log?.verbose { "Evaluating flag $flag with target $target." } var result: EvaluationVariant? = null for (segment in flag.segments) { @@ -66,7 +68,7 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) private fun evaluateSegment( target: EvaluationTarget, flag: EvaluationFlag, - segment: EvaluationSegment + segment: EvaluationSegment, ): EvaluationVariant? { log?.verbose { "Evaluating segment $segment with target $target." } if (segment.conditions == null) { @@ -98,7 +100,10 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) return null } - private fun matchCondition(target: EvaluationTarget, condition: EvaluationCondition): Boolean { + private fun matchCondition( + target: EvaluationTarget, + condition: EvaluationCondition, + ): Boolean { val propValue = target.select(condition.selector) // We need special matching for null properties and set type prop values // and operators. All other values are matched as strings, since the @@ -122,7 +127,10 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) return value.toLong() and 0xffffffffL } - private fun bucket(target: EvaluationTarget, segment: EvaluationSegment): String? { + private fun bucket( + target: EvaluationTarget, + segment: EvaluationSegment, + ): String? { log?.verbose { "Bucketing segment $segment with target $target" } if (segment.bucket == null) { // A null bucket means the segment is fully rolled out. Select the default variant. @@ -175,7 +183,10 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) } } - private fun matchNull(op: String, filterValues: Set): Boolean { + private fun matchNull( + op: String, + filterValues: Set, + ): Boolean { val containsNone = containsNone(filterValues) return when (op) { EvaluationOperator.IS, EvaluationOperator.CONTAINS, EvaluationOperator.LESS_THAN, @@ -183,10 +194,12 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) EvaluationOperator.GREATER_THAN_EQUALS, EvaluationOperator.VERSION_LESS_THAN, EvaluationOperator.VERSION_LESS_THAN_EQUALS, EvaluationOperator.VERSION_GREATER_THAN, EvaluationOperator.VERSION_GREATER_THAN_EQUALS, EvaluationOperator.SET_IS, - EvaluationOperator.SET_CONTAINS, EvaluationOperator.SET_CONTAINS_ANY -> containsNone + EvaluationOperator.SET_CONTAINS, EvaluationOperator.SET_CONTAINS_ANY, + -> containsNone EvaluationOperator.IS_NOT, EvaluationOperator.DOES_NOT_CONTAIN, - EvaluationOperator.SET_DOES_NOT_CONTAIN, EvaluationOperator.SET_DOES_NOT_CONTAIN_ANY -> !containsNone + EvaluationOperator.SET_DOES_NOT_CONTAIN, EvaluationOperator.SET_DOES_NOT_CONTAIN_ANY, + -> !containsNone EvaluationOperator.REGEX_MATCH -> false EvaluationOperator.REGEX_DOES_NOT_MATCH, EvaluationOperator.SET_IS_NOT -> true @@ -194,7 +207,11 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) } } - private fun matchSet(propValues: Set, op: String, filterValues: Set): Boolean { + private fun matchSet( + propValues: Set, + op: String, + filterValues: Set, + ): Boolean { return when (op) { EvaluationOperator.SET_IS -> propValues == filterValues EvaluationOperator.SET_IS_NOT -> propValues != filterValues @@ -206,18 +223,24 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) } } - private fun matchString(propValue: String, op: String, filterValues: Set): Boolean { + private fun matchString( + propValue: String, + op: String, + filterValues: Set, + ): Boolean { return when (op) { EvaluationOperator.IS -> matchesIs(propValue, filterValues) EvaluationOperator.IS_NOT -> !matchesIs(propValue, filterValues) EvaluationOperator.CONTAINS -> matchesContains(propValue, filterValues) EvaluationOperator.DOES_NOT_CONTAIN -> !matchesContains(propValue, filterValues) EvaluationOperator.LESS_THAN, EvaluationOperator.LESS_THAN_EQUALS, - EvaluationOperator.GREATER_THAN, EvaluationOperator.GREATER_THAN_EQUALS -> + EvaluationOperator.GREATER_THAN, EvaluationOperator.GREATER_THAN_EQUALS, + -> matchesComparable(propValue, op, filterValues) { value -> parseDouble(value) } EvaluationOperator.VERSION_LESS_THAN, EvaluationOperator.VERSION_LESS_THAN_EQUALS, - EvaluationOperator.VERSION_GREATER_THAN, EvaluationOperator.VERSION_GREATER_THAN_EQUALS -> + EvaluationOperator.VERSION_GREATER_THAN, EvaluationOperator.VERSION_GREATER_THAN_EQUALS, + -> matchesComparable(propValue, op, filterValues) { value -> SemanticVersion.parse(value) } EvaluationOperator.REGEX_MATCH -> matchesRegex(propValue, filterValues) @@ -226,7 +249,10 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) } } - private fun matchesIs(propValue: String, filterValues: Set): Boolean { + private fun matchesIs( + propValue: String, + filterValues: Set, + ): Boolean { if (containsBooleans(filterValues)) { val lower: String = propValue.lowercase() if (lower == "true" || lower == "false") { @@ -236,7 +262,10 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) return filterValues.contains(propValue) } - private fun matchesContains(propValue: String, filterValues: Set): Boolean { + private fun matchesContains( + propValue: String, + filterValues: Set, + ): Boolean { for (filterValue in filterValues) { if (propValue.lowercase().contains(filterValue.lowercase())) { return true @@ -245,7 +274,10 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) return false } - private fun matchesSetContainsAll(propValues: Set, filterValues: Set): Boolean { + private fun matchesSetContainsAll( + propValues: Set, + filterValues: Set, + ): Boolean { if (propValues.size < filterValues.size) { return false } @@ -257,7 +289,10 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) return true } - private fun matchesSetContainsAny(propValues: Set, filterValues: Set): Boolean { + private fun matchesSetContainsAny( + propValues: Set, + filterValues: Set, + ): Boolean { for (filterValue in filterValues) { if (matchesIs(filterValue, propValues)) { return true @@ -288,7 +323,11 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) } } - private fun matchesComparable(propValue: Comparable, op: String, filterValue: T): Boolean { + private fun matchesComparable( + propValue: Comparable, + op: String, + filterValue: T, + ): Boolean { val compareTo = propValue.compareTo(filterValue) return when (op) { EvaluationOperator.LESS_THAN, EvaluationOperator.VERSION_LESS_THAN -> compareTo < 0 @@ -299,7 +338,10 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) } } - private fun matchesRegex(propValue: String, filterValues: Set): Boolean { + private fun matchesRegex( + propValue: String, + filterValues: Set, + ): Boolean { return filterValues.any { filterValue -> Regex(filterValue).matches(propValue) } } @@ -341,11 +383,12 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) // Parse a string as json array and convert to list of strings, or // return null if the string could not be parsed as a json array. val stringValue = value.toString() - val jsonArray = try { - json.decodeFromString(stringValue) - } catch (e: SerializationException) { - return null - } + val jsonArray = + try { + json.decodeFromString(stringValue) + } catch (e: SerializationException) { + return null + } return jsonArray.toList().mapNotNull { coerceString(it) }.toSet() } @@ -356,7 +399,8 @@ internal class EvaluationEngineImpl(private val log: Logger? = DefaultLogger()) EvaluationOperator.SET_CONTAINS, EvaluationOperator.SET_DOES_NOT_CONTAIN, EvaluationOperator.SET_CONTAINS_ANY, - EvaluationOperator.SET_DOES_NOT_CONTAIN_ANY -> true + EvaluationOperator.SET_DOES_NOT_CONTAIN_ANY, + -> true else -> false } diff --git a/sdk/src/main/java/com/amplitude/evaluation/EvaluationFlag.kt b/sdk/src/main/java/com/amplitude/evaluation/EvaluationFlag.kt index a567ad2..711710f 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/EvaluationFlag.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/EvaluationFlag.kt @@ -9,19 +9,15 @@ import kotlinx.serialization.UseSerializers internal data class EvaluationFlag( // The flag key. Must be unique for deployment. val key: String, - // The flag's variants. The result of a flag evaluation is exactly one // variant. val variants: Map, - // The targeting segments. val segments: List, - // The flag's dependencies, used to order the flags prior to evaluation. val dependencies: Set? = null, - // An object of metadata for this flag. Contains information useful // outside evaluation. The bucketing segment's metadata is merged with // the flag metadata and returned within the evaluation result. - val metadata: Map? = null + val metadata: Map? = null, ) diff --git a/sdk/src/main/java/com/amplitude/evaluation/EvaluationSegment.kt b/sdk/src/main/java/com/amplitude/evaluation/EvaluationSegment.kt index f5b4417..7f88263 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/EvaluationSegment.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/EvaluationSegment.kt @@ -9,18 +9,15 @@ import kotlinx.serialization.UseSerializers internal data class EvaluationSegment( // How to bucket the user given a matching condition. val bucket: EvaluationBucket? = null, - // The targeting conditions. On match, bucket the user. The outer list // is operated with "OR" and the inner list is operated with "AND". val conditions: List>? = null, - // The default variant if the conditions match but either no bucket is set, // or the bucket does not produce a variant. val variant: String? = null, - // An object of metadata for this segment. For example, contains the // segment name and may contain the experiment key associated with this // segment. The bucketing segment's metadata is passed back in the // evaluation result along with the flag metadata. - val metadata: Map? = null + val metadata: Map? = null, ) diff --git a/sdk/src/main/java/com/amplitude/evaluation/EvaluationSerialization.kt b/sdk/src/main/java/com/amplitude/evaluation/EvaluationSerialization.kt index a3988c4..a572ef4 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/EvaluationSerialization.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/EvaluationSerialization.kt @@ -20,33 +20,36 @@ import kotlin.jvm.JvmSynthetic @JvmSynthetic @JvmField -internal val json = Json { - ignoreUnknownKeys = true - isLenient = true - coerceInputValues = true - explicitNulls = false -} +internal val json = + Json { + ignoreUnknownKeys = true + isLenient = true + coerceInputValues = true + explicitNulls = false + } @JvmSynthetic -internal fun Any?.toJsonElement(): JsonElement = when (this) { - null -> JsonNull - is Map<*, *> -> toJsonObject() - is Collection<*> -> toJsonArray() - is Boolean -> JsonPrimitive(this) - is Number -> JsonPrimitive(this) - is String -> JsonPrimitive(this) - else -> JsonPrimitive(toString()) -} +internal fun Any?.toJsonElement(): JsonElement = + when (this) { + null -> JsonNull + is Map<*, *> -> toJsonObject() + is Collection<*> -> toJsonArray() + is Boolean -> JsonPrimitive(this) + is Number -> JsonPrimitive(this) + is String -> JsonPrimitive(this) + else -> JsonPrimitive(toString()) + } @JvmSynthetic internal fun Collection<*>.toJsonArray(): JsonArray = JsonArray(map { it.toJsonElement() }) @JvmSynthetic -internal fun Map<*, *>.toJsonObject(): JsonObject = JsonObject( - mapNotNull { - (it.key as? String ?: return@mapNotNull null) to it.value.toJsonElement() - }.toMap(), -) +internal fun Map<*, *>.toJsonObject(): JsonObject = + JsonObject( + mapNotNull { + (it.key as? String ?: return@mapNotNull null) to it.value.toJsonElement() + }.toMap(), + ) @JvmSynthetic internal fun JsonElement.toAny(): Any? { @@ -77,7 +80,10 @@ internal object AnySerializer : KSerializer { override val descriptor: SerialDescriptor get() = SerialDescriptor("Any", delegate.descriptor) - override fun serialize(encoder: Encoder, value: Any?) { + override fun serialize( + encoder: Encoder, + value: Any?, + ) { val jsonElement = value.toJsonElement() encoder.encodeSerializableValue(delegate, jsonElement) } diff --git a/sdk/src/main/java/com/amplitude/evaluation/Logger.kt b/sdk/src/main/java/com/amplitude/evaluation/Logger.kt index 6afbd8d..bcff093 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/Logger.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/Logger.kt @@ -5,22 +5,31 @@ internal enum class Level { DEBUG, INFO, WARN, - ERROR + ERROR, } internal interface Logger { fun verbose(log: () -> String) + fun debug(log: () -> String) + fun info(log: () -> String) - fun warn(e: Throwable? = null, log: () -> String) - fun error(e: Throwable? = null, log: () -> String) + + fun warn( + e: Throwable? = null, + log: () -> String, + ) + + fun error( + e: Throwable? = null, + log: () -> String, + ) } internal class DefaultLogger( private val level: Level = Level.ERROR, - private val tag: String = "Experiment" + private val tag: String = "Experiment", ) : Logger { - override fun verbose(log: () -> String) { if (level <= Level.VERBOSE) { println("VERBOSE [$tag] ${log.invoke()}") @@ -39,7 +48,10 @@ internal class DefaultLogger( } } - override fun warn(e: Throwable?, log: () -> String) { + override fun warn( + e: Throwable?, + log: () -> String, + ) { if (level <= Level.WARN) { if (e == null) { println("WARN [$tag] ${log.invoke()}") @@ -49,7 +61,10 @@ internal class DefaultLogger( } } - override fun error(e: Throwable?, log: () -> String) { + override fun error( + e: Throwable?, + log: () -> String, + ) { if (level <= Level.ERROR) { if (e == null) { println("ERROR [$tag] ${log.invoke()}") diff --git a/sdk/src/main/java/com/amplitude/evaluation/Murmur3.kt b/sdk/src/main/java/com/amplitude/evaluation/Murmur3.kt index d85d111..6a0fc34 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/Murmur3.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/Murmur3.kt @@ -1,7 +1,6 @@ package com.amplitude.experiment.evaluation internal object Murmur3 { - private const val C1_32 = -0x3361d2af private const val C2_32 = 0x1b873593 private const val R1_32 = 15 @@ -9,7 +8,11 @@ internal object Murmur3 { private const val M_32 = 5 private const val N_32 = -0x19ab949c - internal fun hash32x86(data: ByteArray, length: Int, seed: Int): Int { + internal fun hash32x86( + data: ByteArray, + length: Int, + seed: Int, + ): Int { var hash = seed val nblocks = length shr 2 @@ -56,7 +59,10 @@ internal object Murmur3 { return fmix32(hash) } - private fun mix32(k: Int, hash: Int): Int { + private fun mix32( + k: Int, + hash: Int, + ): Int { var kResult = k var hashResult = hash kResult *= C1_32 @@ -64,7 +70,7 @@ internal object Murmur3 { kResult *= C2_32 hashResult = hashResult xor kResult return hashResult.rotateLeft( - R2_32 + R2_32, ) * M_32 + N_32 } @@ -91,6 +97,6 @@ internal object Murmur3 { or (this[index + 1].toInt() and 0xff shl 16) or (this[index + 2].toInt() and 0xff shl 8) or (this[index + 3].toInt() and 0xff) - ).reverseBytes() + ).reverseBytes() } } diff --git a/sdk/src/main/java/com/amplitude/evaluation/Selectable.kt b/sdk/src/main/java/com/amplitude/evaluation/Selectable.kt index bf9629b..33c21e5 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/Selectable.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/Selectable.kt @@ -15,11 +15,12 @@ internal interface Selectable { for (i in 0 until selector.size - 1) { val selectorElement = selector[i] ?: return null val value = selectable.select(selectorElement) - selectable = when (value) { - is Selectable -> value - is Map<*, *> -> SelectableMap(value) - else -> return null - } + selectable = + when (value) { + is Selectable -> value + is Map<*, *> -> SelectableMap(value) + else -> return null + } } val lastSelector = selector[selector.size - 1] ?: return null return selectable.select(lastSelector) diff --git a/sdk/src/main/java/com/amplitude/evaluation/SemanticVersion.kt b/sdk/src/main/java/com/amplitude/evaluation/SemanticVersion.kt index befb8c4..975a46d 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/SemanticVersion.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/SemanticVersion.kt @@ -28,11 +28,9 @@ internal data class SemanticVersion( val major: Int = 0, val minor: Int = 0, val patch: Int = 0, - val preRelease: String? = null + val preRelease: String? = null, ) : Comparable { - companion object { - fun parse(version: String?): SemanticVersion? { if (version == null) { return null diff --git a/sdk/src/main/java/com/amplitude/evaluation/TopologicalSort.kt b/sdk/src/main/java/com/amplitude/evaluation/TopologicalSort.kt index 078fe9a..1d0f542 100644 --- a/sdk/src/main/java/com/amplitude/evaluation/TopologicalSort.kt +++ b/sdk/src/main/java/com/amplitude/evaluation/TopologicalSort.kt @@ -8,7 +8,7 @@ internal class CycleException(val cycle: Set) : RuntimeException() { @Throws(CycleException::class) internal fun topologicalSort( flagConfigs: List, - flagKeys: Set = setOf() + flagKeys: Set = setOf(), ): List { return topologicalSort(flagConfigs.associateBy { it.key }, flagKeys) } @@ -16,13 +16,14 @@ internal fun topologicalSort( @Throws(CycleException::class) internal fun topologicalSort( flagConfigs: Map, - flagKeys: Set = setOf() + flagKeys: Set = setOf(), ): List { val available = flagConfigs.toMutableMap() val result = mutableListOf() - val startingKeys = flagKeys.ifEmpty { - available.keys.toSet() - } + val startingKeys = + flagKeys.ifEmpty { + available.keys.toSet() + } for (flagKey in startingKeys) { val traversal = parentTraversal(flagKey, available) ?: continue result.addAll(traversal) diff --git a/sdk/src/main/java/com/amplitude/experiment/ConnectorExposureTrackingProvider.kt b/sdk/src/main/java/com/amplitude/experiment/ConnectorExposureTrackingProvider.kt index d14d61d..caebe6a 100644 --- a/sdk/src/main/java/com/amplitude/experiment/ConnectorExposureTrackingProvider.kt +++ b/sdk/src/main/java/com/amplitude/experiment/ConnectorExposureTrackingProvider.kt @@ -4,20 +4,20 @@ import com.amplitude.analytics.connector.AnalyticsEvent import com.amplitude.analytics.connector.EventBridge internal class ConnectorExposureTrackingProvider( - private val eventBridge: EventBridge + private val eventBridge: EventBridge, ) : ExposureTrackingProvider { - override fun track(exposure: Exposure) { eventBridge.logEvent( AnalyticsEvent( eventType = "\$exposure", - eventProperties = mapOf( - "flag_key" to exposure.flagKey, - "variant" to exposure.variant, - "experiment_key" to exposure.experimentKey, - "metadata" to exposure.metadata, - ).filterNull() - ) + eventProperties = + mapOf( + "flag_key" to exposure.flagKey, + "variant" to exposure.variant, + "experiment_key" to exposure.experimentKey, + "metadata" to exposure.metadata, + ).filterNull(), + ), ) } } diff --git a/sdk/src/main/java/com/amplitude/experiment/ConnectorUserProvider.kt b/sdk/src/main/java/com/amplitude/experiment/ConnectorUserProvider.kt index 964ce52..e95e32c 100644 --- a/sdk/src/main/java/com/amplitude/experiment/ConnectorUserProvider.kt +++ b/sdk/src/main/java/com/amplitude/experiment/ConnectorUserProvider.kt @@ -12,7 +12,6 @@ internal class ConnectorUserProvider( context: Context, private val identityStore: IdentityStore, ) : ExperimentUserProvider { - private val base = DefaultUserProvider(context) override fun getUser(): ExperimentUser { @@ -49,22 +48,23 @@ private fun IdentityStore.getIdentityOrWait(ms: Long): Identity { } addIdentityListener(callback) val immediateIdentity = getIdentity() - val result = if (immediateIdentity.isUnidentified()) { - when (val result = lock.wait(ms)) { - is LockResult.Success -> result.value - is LockResult.Error -> { - if (result.error is TimeoutException) { - throw TimeoutException( - "Timed out waiting for Amplitude Analytics SDK to initialize. " + - "You should ensure that the analytics SDK is initialized prior to calling fetch()." - ) + val result = + if (immediateIdentity.isUnidentified()) { + when (val result = lock.wait(ms)) { + is LockResult.Success -> result.value + is LockResult.Error -> { + if (result.error is TimeoutException) { + throw TimeoutException( + "Timed out waiting for Amplitude Analytics SDK to initialize. " + + "You should ensure that the analytics SDK is initialized prior to calling fetch().", + ) + } + Identity() } - Identity() } + } else { + immediateIdentity } - } else { - immediateIdentity - } removeIdentityListener(callback) return result } diff --git a/sdk/src/main/java/com/amplitude/experiment/DefaultExperimentClient.kt b/sdk/src/main/java/com/amplitude/experiment/DefaultExperimentClient.kt index 7a4c450..54b8cd6 100644 --- a/sdk/src/main/java/com/amplitude/experiment/DefaultExperimentClient.kt +++ b/sdk/src/main/java/com/amplitude/experiment/DefaultExperimentClient.kt @@ -56,20 +56,21 @@ internal class DefaultExperimentClient internal constructor( storage: Storage, private val executorService: ScheduledExecutorService, ) : ExperimentClient { - private var user: ExperimentUser? = null private val engine = EvaluationEngineImpl() - private val variants: LoadStoreCache = getVariantStorage( - this.apiKey, - this.config.instanceName, - storage, - ) - private val flags: LoadStoreCache = getFlagStorage( - this.apiKey, - this.config.instanceName, - storage, - ) + private val variants: LoadStoreCache = + getVariantStorage( + this.apiKey, + this.config.instanceName, + storage, + ) + private val flags: LoadStoreCache = + getFlagStorage( + this.apiKey, + this.config.instanceName, + storage, + ) init { this.variants.load() @@ -80,24 +81,31 @@ internal class DefaultExperimentClient internal constructor( private val backoffLock = Any() private var backoff: Backoff? = null private val fetchBackoffTimeoutMillis = 10000L - private val backoffConfig = BackoffConfig( - attempts = 8, - min = 500, - max = 10000, - scalar = 1.5f, - ) + private val backoffConfig = + BackoffConfig( + attempts = 8, + min = 500, + max = 10000, + scalar = 1.5f, + ) private val poller: Poller = Poller(this.executorService, ::doFlags, FLAG_POLLER_INTERVAL_MILLIS) internal val serverUrl: HttpUrl = - if (config.serverUrl == ExperimentConfig.Defaults.SERVER_URL && config.flagsServerUrl == ExperimentConfig.Defaults.FLAGS_SERVER_URL && config.serverZone == ServerZone.EU) { + if (config.serverUrl == ExperimentConfig.Defaults.SERVER_URL && + config.flagsServerUrl == ExperimentConfig.Defaults.FLAGS_SERVER_URL && + config.serverZone == ServerZone.EU + ) { EU_SERVER_URL.toHttpUrl() } else { config.serverUrl.toHttpUrl() } internal val flagsServerUrl: HttpUrl = - if (config.serverUrl == ExperimentConfig.Defaults.SERVER_URL && config.flagsServerUrl == ExperimentConfig.Defaults.FLAGS_SERVER_URL && config.serverZone == ServerZone.EU) { + if (config.serverUrl == ExperimentConfig.Defaults.SERVER_URL && + config.flagsServerUrl == ExperimentConfig.Defaults.FLAGS_SERVER_URL && + config.serverZone == ServerZone.EU + ) { EU_FLAGS_SERVER_URL.toHttpUrl() } else { config.flagsServerUrl.toHttpUrl() @@ -108,9 +116,10 @@ internal class DefaultExperimentClient internal constructor( @Deprecated("moved to experiment config") private var userProvider: ExperimentUserProvider? = config.userProvider - private val analyticsProvider: SessionAnalyticsProvider? = config.analyticsProvider?.let { - SessionAnalyticsProvider(it) - } + private val analyticsProvider: SessionAnalyticsProvider? = + config.analyticsProvider?.let { + SessionAnalyticsProvider(it) + } private val userSessionExposureTracker: UserSessionExposureTracker? = config.exposureTrackingProvider?.let { UserSessionExposureTracker(it) @@ -141,14 +150,14 @@ internal class DefaultExperimentClient internal constructor( getUserMergedWithProviderOrWait(10000), config.fetchTimeoutMillis, config.retryFetchOnFailure, - null + null, ) flagsFuture.get() } else { flagsFuture.get() } this - } + }, ) } @@ -169,14 +178,17 @@ internal class DefaultExperimentClient internal constructor( return fetch(user, null) } - override fun fetch(user: ExperimentUser?, options: FetchOptions?): Future { + override fun fetch( + user: ExperimentUser?, + options: FetchOptions?, + ): Future { this.user = user ?: this.user return executorService.submit( Callable { val fetchUser = getUserMergedWithProviderOrWait(10000) fetchInternal(fetchUser, config.fetchTimeoutMillis, config.retryFetchOnFailure, options) this - } + }, ) } @@ -184,7 +196,10 @@ internal class DefaultExperimentClient internal constructor( return variant(key, null) } - override fun variant(key: String, fallback: Variant?): Variant { + override fun variant( + key: String, + fallback: Variant?, + ): Variant { val variantAndSource = resolveVariantAndSource(key, fallback) if (config.automaticExposureTracking) { exposureInternal(key, variantAndSource) @@ -199,11 +214,12 @@ internal class DefaultExperimentClient internal constructor( override fun all(): Map { val evaluationResults = this.evaluate() - val evaluatedVariants = synchronized(flags) { - evaluationResults.filter { entry -> - this.flags.get(entry.key).isLocalEvaluationMode() + val evaluatedVariants = + synchronized(flags) { + evaluationResults.filter { entry -> + this.flags.get(entry.key).isLocalEvaluationMode() + } } - } return secondaryVariants() + sourceVariants() + evaluatedVariants } @@ -235,7 +251,10 @@ internal class DefaultExperimentClient internal constructor( return synchronized(flags) { this.flags.getAll() } } - private fun exposureInternal(key: String, variantAndSource: VariantAndSource) { + private fun exposureInternal( + key: String, + variantAndSource: VariantAndSource, + ) { legacyExposureInternal(key, variantAndSource.variant, variantAndSource.source) // Do not track exposure for fallback variants that are not associated with a default variant. @@ -246,18 +265,23 @@ internal class DefaultExperimentClient internal constructor( val experimentKey = variantAndSource.variant.expKey val metadata = variantAndSource.variant.metadata - val variant = if (!fallback && !variantAndSource.variant.isDefaultVariant()) { - variantAndSource.variant.key ?: variantAndSource.variant.value - } else { - null - } + val variant = + if (!fallback && !variantAndSource.variant.isDefaultVariant()) { + variantAndSource.variant.key ?: variantAndSource.variant.value + } else { + null + } val exposure = Exposure(key, variant, experimentKey, metadata) userSessionExposureTracker?.track(exposure) } - private fun legacyExposureInternal(key: String, variant: Variant, source: VariantSource) { + private fun legacyExposureInternal( + key: String, + variant: Variant, + source: VariantSource, + ) { val exposedUser = getUserMergedWithProvider() val event = OldExposureEvent(exposedUser, key, variant, source) // Track the exposure event if an analytics provider is set @@ -273,12 +297,16 @@ internal class DefaultExperimentClient internal constructor( return source == null || source.isFallback() } - private fun resolveVariantAndSource(key: String, fallback: Variant? = null): VariantAndSource { + private fun resolveVariantAndSource( + key: String, + fallback: Variant? = null, + ): VariantAndSource { var variantAndSource: VariantAndSource - variantAndSource = when (config.source) { - Source.LOCAL_STORAGE -> localStorageVariantAndSource(key, fallback) - Source.INITIAL_VARIANTS -> initialVariantsVariantAndSource(key, fallback) - } + variantAndSource = + when (config.source) { + Source.LOCAL_STORAGE -> localStorageVariantAndSource(key, fallback) + Source.INITIAL_VARIANTS -> initialVariantsVariantAndSource(key, fallback) + } val flag = synchronized(flags) { this.flags.get(key) } if (flag != null && (flag.isLocalEvaluationMode() || variantAndSource.variant.isNullOrEmpty())) { variantAndSource = this.localEvaluationVariantAndSource(key, flag, fallback) @@ -287,7 +315,12 @@ internal class DefaultExperimentClient internal constructor( } @Throws - internal fun fetchInternal(user: ExperimentUser, timeoutMillis: Long, retry: Boolean, options: FetchOptions?) { + internal fun fetchInternal( + user: ExperimentUser, + timeoutMillis: Long, + retry: Boolean, + options: FetchOptions?, + ) { if (retry) { stopRetries() } @@ -305,31 +338,35 @@ internal class DefaultExperimentClient internal constructor( private fun doFetch( user: ExperimentUser, timeoutMillis: Long, - options: FetchOptions? + options: FetchOptions?, ): Future> { if (user.userId == null && user.deviceId == null) { Logger.w("user id and device id are null; amplitude may not resolve identity") } Logger.d("Fetch variants for user: $user") // Build request to fetch variants for the user - val userBase64 = user.toJson() - .toByteArray(Charsets.UTF_8) - .toByteString() - .base64Url() - val url = serverUrl.newBuilder() - .addPathSegments("sdk/v2/vardata") - .build() - val builder = Request.Builder() - .get() - .url(url) - .addHeader("Authorization", "Api-Key $apiKey") - .addHeader("X-Amp-Exp-User", userBase64) - if (!options?.flagKeys.isNullOrEmpty()) { - val flagKeysBase64 = JSONArray(options?.flagKeys) - .toString() + val userBase64 = + user.toJson() .toByteArray(Charsets.UTF_8) .toByteString() - .base64() + .base64Url() + val url = + serverUrl.newBuilder() + .addPathSegments("sdk/v2/vardata") + .build() + val builder = + Request.Builder() + .get() + .url(url) + .addHeader("Authorization", "Api-Key $apiKey") + .addHeader("X-Amp-Exp-User", userBase64) + if (!options?.flagKeys.isNullOrEmpty()) { + val flagKeysBase64 = + JSONArray(options?.flagKeys) + .toString() + .toByteArray(Charsets.UTF_8) + .toByteString() + .base64() builder.addHeader("X-Amp-Exp-Flag-Keys", flagKeysBase64) } val request = builder.build() @@ -337,24 +374,32 @@ internal class DefaultExperimentClient internal constructor( call.timeout().timeout(timeoutMillis, TimeUnit.MILLISECONDS) val future = AsyncFuture>(call) // Execute request and handle response - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - try { - Logger.d("Received fetch variants response: $response") - if (!response.isSuccessful) { - throw FetchException(response.code, "fetch error response: $response") + call.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + try { + Logger.d("Received fetch variants response: $response") + if (!response.isSuccessful) { + throw FetchException(response.code, "fetch error response: $response") + } + val variants = parseResponse(response) + future.complete(variants) + } catch (e: Exception) { + future.completeExceptionally(e) } - val variants = parseResponse(response) - future.complete(variants) - } catch (e: Exception) { - future.completeExceptionally(e) } - } - override fun onFailure(call: Call, e: IOException) { - future.completeExceptionally(e) - } - }) + override fun onFailure( + call: Call, + e: IOException, + ) { + future.completeExceptionally(e) + } + }, + ) return future } @@ -363,8 +408,8 @@ internal class DefaultExperimentClient internal constructor( GetFlagsOptions( libraryName = "experiment-android-client", libraryVersion = BuildConfig.VERSION_NAME, - timeoutMillis = config.fetchTimeoutMillis - ) + timeoutMillis = config.fetchTimeoutMillis, + ), ) { flags -> synchronized(this.flags) { this.flags.clear() @@ -375,35 +420,44 @@ internal class DefaultExperimentClient internal constructor( } } - private fun startRetries(user: ExperimentUser, options: FetchOptions?) = synchronized(backoffLock) { + private fun startRetries( + user: ExperimentUser, + options: FetchOptions?, + ) = synchronized(backoffLock) { backoff?.cancel() - backoff = executorService.backoff(backoffConfig) { - fetchInternal(user, fetchBackoffTimeoutMillis, false, options) - } + backoff = + executorService.backoff(backoffConfig) { + fetchInternal(user, fetchBackoffTimeoutMillis, false, options) + } } - private fun stopRetries() = synchronized(backoffLock) { - backoff?.cancel() - } + private fun stopRetries() = + synchronized(backoffLock) { + backoff?.cancel() + } @Throws(IOException::class) - private fun parseResponse(response: Response): Map = response.use { - if (!response.isSuccessful) { - throw IOException("fetch error response: $response") - } - val body = response.body?.string() ?: "" - val json = JSONObject(body) - val variants = mutableMapOf() - json.keys().forEach { key -> - val variant = json.getJSONObject(key).toVariant() - if (variant != null) { - variants[key] = variant + private fun parseResponse(response: Response): Map = + response.use { + if (!response.isSuccessful) { + throw IOException("fetch error response: $response") } + val body = response.body?.string() ?: "" + val json = JSONObject(body) + val variants = mutableMapOf() + json.keys().forEach { key -> + val variant = json.getJSONObject(key).toVariant() + if (variant != null) { + variants[key] = variant + } + } + return variants } - return variants - } - private fun storeVariants(variants: Map, options: FetchOptions?) { + private fun storeVariants( + variants: Map, + options: FetchOptions?, + ) { val failedFlagKeys = options?.flagKeys?.toMutableList() ?: mutableListOf() synchronized(this.variants) { if (options?.flagKeys == null) { @@ -445,15 +499,16 @@ internal class DefaultExperimentClient internal constructor( @Throws(IllegalStateException::class) private fun getUserMergedWithProviderOrWait(ms: Long): ExperimentUser { val safeUserProvider = userProvider - val providedUser = if (safeUserProvider is ConnectorUserProvider) { - try { - safeUserProvider.getUserOrWait(ms) - } catch (e: TimeoutException) { - throw IllegalStateException(e) + val providedUser = + if (safeUserProvider is ConnectorUserProvider) { + try { + safeUserProvider.getUserOrWait(ms) + } catch (e: TimeoutException) { + throw IllegalStateException(e) + } + } else { + safeUserProvider?.getUser() } - } else { - safeUserProvider?.getUser() - } val user = this.user ?: ExperimentUser() return user.copyToBuilder() .library("experiment-android-client/${BuildConfig.VERSION_NAME}") @@ -462,12 +517,13 @@ internal class DefaultExperimentClient internal constructor( private fun evaluate(flagKeys: Set = emptySet()): Map { val user = getUserMergedWithProvider() - val flags = try { - topologicalSort(synchronized(flags) { this.flags.getAll() }, flagKeys) - } catch (e: Exception) { - Logger.w("Error during topological sort of flags", e) - return emptyMap() - } + val flags = + try { + topologicalSort(synchronized(flags) { this.flags.getAll() }, flagKeys) + } catch (e: Exception) { + Logger.w("Error during topological sort of flags", e) + return emptyMap() + } val context = user.toEvaluationContext() val evaluationVariants = this.engine.evaluate(context, flags) return evaluationVariants.mapValues { it.value.convertToVariant() } @@ -487,7 +543,7 @@ internal class DefaultExperimentClient internal constructor( private fun localEvaluationVariantAndSource( key: String, flag: EvaluationFlag, - fallback: Variant? = null + fallback: Variant? = null, ): VariantAndSource { var defaultVariantAndSource = VariantAndSource() // Local evaluation @@ -498,21 +554,22 @@ internal class DefaultExperimentClient internal constructor( return VariantAndSource( variant = variant, source = source, - hasDefaultVariant = false + hasDefaultVariant = false, ) } else if (isLocalEvaluationDefault == true) { - defaultVariantAndSource = VariantAndSource( - variant = variant, - source = source, - hasDefaultVariant = true - ) + defaultVariantAndSource = + VariantAndSource( + variant = variant, + source = source, + hasDefaultVariant = true, + ) } // Inline fallback if (fallback != null) { return VariantAndSource( variant = fallback, source = VariantSource.FALLBACK_INLINE, - hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant + hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant, ) } // Initial variants @@ -521,16 +578,17 @@ internal class DefaultExperimentClient internal constructor( return VariantAndSource( variant = initialVariant, source = VariantSource.SECONDARY_INITIAL_VARIANTS, - hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant + hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant, ) } // Configured fallback, or default variant val fallbackVariant = config.fallbackVariant - val fallbackVariantAndSource = VariantAndSource( - variant = fallbackVariant, - source = VariantSource.FALLBACK_CONFIG, - hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant - ) + val fallbackVariantAndSource = + VariantAndSource( + variant = fallbackVariant, + source = VariantSource.FALLBACK_CONFIG, + hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant, + ) if (!fallbackVariant.isNullOrEmpty()) { return fallbackVariantAndSource } @@ -549,7 +607,7 @@ internal class DefaultExperimentClient internal constructor( */ private fun localStorageVariantAndSource( key: String, - fallback: Variant? + fallback: Variant?, ): VariantAndSource { var defaultVariantAndSource = VariantAndSource() // Local storage @@ -559,21 +617,22 @@ internal class DefaultExperimentClient internal constructor( return VariantAndSource( variant = localStorageVariant, source = VariantSource.LOCAL_STORAGE, - hasDefaultVariant = false + hasDefaultVariant = false, ) } else if (isLocalStorageDefault == true) { - defaultVariantAndSource = VariantAndSource( - variant = localStorageVariant, - source = VariantSource.LOCAL_STORAGE, - hasDefaultVariant = true - ) + defaultVariantAndSource = + VariantAndSource( + variant = localStorageVariant, + source = VariantSource.LOCAL_STORAGE, + hasDefaultVariant = true, + ) } // Inline fallback if (fallback != null) { return VariantAndSource( variant = fallback, source = VariantSource.FALLBACK_INLINE, - hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant + hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant, ) } // Initial variants @@ -582,16 +641,17 @@ internal class DefaultExperimentClient internal constructor( return VariantAndSource( variant = initialVariant, source = VariantSource.SECONDARY_INITIAL_VARIANTS, - hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant + hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant, ) } // Configured fallback, or default variant val fallbackVariant = config.fallbackVariant - val fallbackVariantAndSource = VariantAndSource( - variant = fallbackVariant, - source = VariantSource.FALLBACK_CONFIG, - hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant - ) + val fallbackVariantAndSource = + VariantAndSource( + variant = fallbackVariant, + source = VariantSource.FALLBACK_CONFIG, + hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant, + ) if (!fallbackVariant.isNullOrEmpty()) { return fallbackVariantAndSource } @@ -610,7 +670,7 @@ internal class DefaultExperimentClient internal constructor( */ private fun initialVariantsVariantAndSource( key: String, - fallback: Variant? = null + fallback: Variant? = null, ): VariantAndSource { var defaultVariantAndSource = VariantAndSource() // Initial variants @@ -619,7 +679,7 @@ internal class DefaultExperimentClient internal constructor( return VariantAndSource( variant = initialVariantsVariant, source = VariantSource.INITIAL_VARIANTS, - hasDefaultVariant = false + hasDefaultVariant = false, ) } // Local storage @@ -629,30 +689,32 @@ internal class DefaultExperimentClient internal constructor( return VariantAndSource( variant = localStorageVariant, source = VariantSource.LOCAL_STORAGE, - hasDefaultVariant = false + hasDefaultVariant = false, ) } else if (isLocalStorageDefault == true) { - defaultVariantAndSource = VariantAndSource( - variant = localStorageVariant, - source = VariantSource.LOCAL_STORAGE, - hasDefaultVariant = true - ) + defaultVariantAndSource = + VariantAndSource( + variant = localStorageVariant, + source = VariantSource.LOCAL_STORAGE, + hasDefaultVariant = true, + ) } // Inline fallback if (fallback != null) { return VariantAndSource( variant = fallback, source = VariantSource.FALLBACK_INLINE, - hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant + hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant, ) } // Configured fallback, or default variant val fallbackVariant = config.fallbackVariant - val fallbackVariantAndSource = VariantAndSource( - variant = fallbackVariant, - source = VariantSource.FALLBACK_CONFIG, - hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant - ) + val fallbackVariantAndSource = + VariantAndSource( + variant = fallbackVariant, + source = VariantSource.FALLBACK_CONFIG, + hasDefaultVariant = defaultVariantAndSource.hasDefaultVariant, + ) if (!fallbackVariant.isNullOrEmpty()) { return fallbackVariantAndSource } @@ -682,7 +744,7 @@ internal class DefaultExperimentClient internal constructor( data class VariantAndSource( val variant: Variant = Variant(), val source: VariantSource = VariantSource.FALLBACK_CONFIG, - val hasDefaultVariant: Boolean = false + val hasDefaultVariant: Boolean = false, ) enum class VariantSource(val type: String) { @@ -692,7 +754,8 @@ enum class VariantSource(val type: String) { SECONDARY_INITIAL_VARIANTS("secondary-initial"), FALLBACK_INLINE("fallback-inline"), FALLBACK_CONFIG("fallback-config"), - LOCAL_EVALUATION("local-evaluation"); + LOCAL_EVALUATION("local-evaluation"), + ; override fun toString(): String { return type diff --git a/sdk/src/main/java/com/amplitude/experiment/DefaultUserProvider.kt b/sdk/src/main/java/com/amplitude/experiment/DefaultUserProvider.kt index 2dea1dd..e3598e5 100644 --- a/sdk/src/main/java/com/amplitude/experiment/DefaultUserProvider.kt +++ b/sdk/src/main/java/com/amplitude/experiment/DefaultUserProvider.kt @@ -12,7 +12,6 @@ class DefaultUserProvider( private val userId: String? = null, private val deviceId: String? = null, ) : ExperimentUserProvider { - constructor(context: Context) : this (context, null, null) private val version = context.getAppVersion() diff --git a/sdk/src/main/java/com/amplitude/experiment/Experiment.kt b/sdk/src/main/java/com/amplitude/experiment/Experiment.kt index 50409a5..04d689f 100644 --- a/sdk/src/main/java/com/amplitude/experiment/Experiment.kt +++ b/sdk/src/main/java/com/amplitude/experiment/Experiment.kt @@ -11,7 +11,6 @@ import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.ThreadFactory object Experiment { - private val daemonThreadFactory = ThreadFactory { r -> Executors.defaultThreadFactory().newThread(r).apply { @@ -36,32 +35,35 @@ object Experiment { fun initialize( application: Application, apiKey: String, - config: ExperimentConfig - ): ExperimentClient = synchronized(instances) { - val instanceName = config.instanceName - val instanceKey = "$instanceName.$apiKey" - return when (val instance = instances[instanceKey]) { - null -> { - Logger.implementation = AndroidLogger(config.debug) - var mergedConfig = config - if (config.userProvider == null) { - mergedConfig = config.copyToBuilder() - .userProvider(DefaultUserProvider(application)) - .build() + config: ExperimentConfig, + ): ExperimentClient = + synchronized(instances) { + val instanceName = config.instanceName + val instanceKey = "$instanceName.$apiKey" + return when (val instance = instances[instanceKey]) { + null -> { + Logger.implementation = AndroidLogger(config.debug) + var mergedConfig = config + if (config.userProvider == null) { + mergedConfig = + config.copyToBuilder() + .userProvider(DefaultUserProvider(application)) + .build() + } + val newInstance = + DefaultExperimentClient( + apiKey, + mergedConfig, + httpClient, + SharedPrefsStorage(application), + executorService, + ) + instances[instanceKey] = newInstance + newInstance } - val newInstance = DefaultExperimentClient( - apiKey, - mergedConfig, - httpClient, - SharedPrefsStorage(application), - executorService, - ) - instances[instanceKey] = newInstance - newInstance + else -> instance } - else -> instance } - } /** * Initialize a singleton [ExperimentClient] which automatically @@ -80,42 +82,45 @@ object Experiment { fun initializeWithAmplitudeAnalytics( application: Application, apiKey: String, - config: ExperimentConfig - ): ExperimentClient = synchronized(instances) { - val instanceName = config.instanceName - val instanceKey = "$instanceName.$apiKey" - val connector = AnalyticsConnector.getInstance(instanceName) - val instance = when (val instance = instances[instanceKey]) { - null -> { - Logger.implementation = AndroidLogger(config.debug) - val configBuilder = config.copyToBuilder() - if (config.userProvider == null) { - configBuilder.userProvider( - ConnectorUserProvider(application, connector.identityStore) - ) - } - if (config.exposureTrackingProvider == null) { - configBuilder.exposureTrackingProvider( - ConnectorExposureTrackingProvider(connector.eventBridge) - ) - } - val newInstance = DefaultExperimentClient( - apiKey, - configBuilder.build(), - httpClient, - SharedPrefsStorage(application), - executorService, - ) - instances[instanceKey] = newInstance - if (config.automaticFetchOnAmplitudeIdentityChange) { - connector.identityStore.addIdentityListener { - newInstance.fetch() + config: ExperimentConfig, + ): ExperimentClient = + synchronized(instances) { + val instanceName = config.instanceName + val instanceKey = "$instanceName.$apiKey" + val connector = AnalyticsConnector.getInstance(instanceName) + val instance = + when (val instance = instances[instanceKey]) { + null -> { + Logger.implementation = AndroidLogger(config.debug) + val configBuilder = config.copyToBuilder() + if (config.userProvider == null) { + configBuilder.userProvider( + ConnectorUserProvider(application, connector.identityStore), + ) + } + if (config.exposureTrackingProvider == null) { + configBuilder.exposureTrackingProvider( + ConnectorExposureTrackingProvider(connector.eventBridge), + ) + } + val newInstance = + DefaultExperimentClient( + apiKey, + configBuilder.build(), + httpClient, + SharedPrefsStorage(application), + executorService, + ) + instances[instanceKey] = newInstance + if (config.automaticFetchOnAmplitudeIdentityChange) { + connector.identityStore.addIdentityListener { + newInstance.fetch() + } + } + newInstance } + else -> instance } - newInstance - } - else -> instance + return instance } - return instance - } } diff --git a/sdk/src/main/java/com/amplitude/experiment/ExperimentClient.kt b/sdk/src/main/java/com/amplitude/experiment/ExperimentClient.kt index 8678313..0754096 100644 --- a/sdk/src/main/java/com/amplitude/experiment/ExperimentClient.kt +++ b/sdk/src/main/java/com/amplitude/experiment/ExperimentClient.kt @@ -68,7 +68,10 @@ interface ExperimentClient { * @see ExperimentUser * @see ExperimentUserProvider */ - fun fetch(user: ExperimentUser? = null, options: FetchOptions? = null): Future + fun fetch( + user: ExperimentUser? = null, + options: FetchOptions? = null, + ): Future /** * Returns the stored variant for the provided key. @@ -95,7 +98,10 @@ interface ExperimentClient { * @see Variant * @see ExperimentConfig */ - fun variant(key: String, fallback: Variant? = null): Variant + fun variant( + key: String, + fallback: Variant? = null, + ): Variant /** * Returns all variants for the user. diff --git a/sdk/src/main/java/com/amplitude/experiment/ExperimentConfig.kt b/sdk/src/main/java/com/amplitude/experiment/ExperimentConfig.kt index 089f536..c64e458 100644 --- a/sdk/src/main/java/com/amplitude/experiment/ExperimentConfig.kt +++ b/sdk/src/main/java/com/amplitude/experiment/ExperimentConfig.kt @@ -57,7 +57,6 @@ class ExperimentConfig internal constructor( @JvmField val exposureTrackingProvider: ExposureTrackingProvider? = Defaults.EXPOSURE_TRACKING_PROVIDER, ) { - /** * Construct the default [ExperimentConfig]. */ @@ -67,7 +66,6 @@ class ExperimentConfig internal constructor( * Defaults for [ExperimentConfig] */ object Defaults { - /** * false */ @@ -168,7 +166,6 @@ class ExperimentConfig internal constructor( } class Builder { - private var debug = Defaults.DEBUG private var instanceName = Defaults.INSTANCE_NAME private var fallbackVariant = Defaults.FALLBACK_VARIANT @@ -188,78 +185,96 @@ class ExperimentConfig internal constructor( private var analyticsProvider = Defaults.ANALYTICS_PROVIDER private var exposureTrackingProvider = Defaults.EXPOSURE_TRACKING_PROVIDER - fun debug(debug: Boolean) = apply { - this.debug = debug - } - - fun instanceName(instanceName: String) = apply { - this.instanceName = instanceName - } - - fun fallbackVariant(fallbackVariant: Variant) = apply { - this.fallbackVariant = fallbackVariant - } - - fun initialFlags(initialFlags: String?) = apply { - this.initialFlags = initialFlags - } - - fun initialVariants(initialVariants: Map) = apply { - this.initialVariants = initialVariants - } - - fun source(source: Source) = apply { - this.source = source - } - - fun serverUrl(serverUrl: String) = apply { - this.serverUrl = serverUrl - } - - fun flagsServerUrl(flagsServerUrl: String) = apply { - this.flagsServerUrl = flagsServerUrl - } - - fun serverZone(serverZone: ServerZone) = apply { - this.serverZone = serverZone - } - - fun fetchTimeoutMillis(fetchTimeoutMillis: Long) = apply { - this.fetchTimeoutMillis = fetchTimeoutMillis - } - - fun retryFetchOnFailure(retryFetchOnFailure: Boolean) = apply { - this.retryFetchOnFailure = retryFetchOnFailure - } - - fun automaticExposureTracking(automaticExposureTracking: Boolean) = apply { - this.automaticExposureTracking = automaticExposureTracking - } - - fun pollOnStart(pollOnStart: Boolean) = apply { - this.pollOnStart = pollOnStart - } - - fun fetchOnStart(fetchOnStart: Boolean?) = apply { - this.fetchOnStart = fetchOnStart ?: true - } - - fun automaticFetchOnAmplitudeIdentityChange(automaticFetchOnAmplitudeIdentityChange: Boolean) = apply { - this.automaticFetchOnAmplitudeIdentityChange = automaticFetchOnAmplitudeIdentityChange - } - - fun userProvider(userProvider: ExperimentUserProvider?) = apply { - this.userProvider = userProvider - } + fun debug(debug: Boolean) = + apply { + this.debug = debug + } + + fun instanceName(instanceName: String) = + apply { + this.instanceName = instanceName + } + + fun fallbackVariant(fallbackVariant: Variant) = + apply { + this.fallbackVariant = fallbackVariant + } + + fun initialFlags(initialFlags: String?) = + apply { + this.initialFlags = initialFlags + } + + fun initialVariants(initialVariants: Map) = + apply { + this.initialVariants = initialVariants + } + + fun source(source: Source) = + apply { + this.source = source + } + + fun serverUrl(serverUrl: String) = + apply { + this.serverUrl = serverUrl + } + + fun flagsServerUrl(flagsServerUrl: String) = + apply { + this.flagsServerUrl = flagsServerUrl + } + + fun serverZone(serverZone: ServerZone) = + apply { + this.serverZone = serverZone + } + + fun fetchTimeoutMillis(fetchTimeoutMillis: Long) = + apply { + this.fetchTimeoutMillis = fetchTimeoutMillis + } + + fun retryFetchOnFailure(retryFetchOnFailure: Boolean) = + apply { + this.retryFetchOnFailure = retryFetchOnFailure + } + + fun automaticExposureTracking(automaticExposureTracking: Boolean) = + apply { + this.automaticExposureTracking = automaticExposureTracking + } + + fun pollOnStart(pollOnStart: Boolean) = + apply { + this.pollOnStart = pollOnStart + } + + fun fetchOnStart(fetchOnStart: Boolean?) = + apply { + this.fetchOnStart = fetchOnStart ?: true + } + + fun automaticFetchOnAmplitudeIdentityChange(automaticFetchOnAmplitudeIdentityChange: Boolean) = + apply { + this.automaticFetchOnAmplitudeIdentityChange = automaticFetchOnAmplitudeIdentityChange + } + + fun userProvider(userProvider: ExperimentUserProvider?) = + apply { + this.userProvider = userProvider + } @Deprecated("Use the exposureTrackingProvider instead") - fun analyticsProvider(analyticsProvider: ExperimentAnalyticsProvider?) = apply { - this.analyticsProvider = analyticsProvider - } - - fun exposureTrackingProvider(exposureTrackingProvider: ExposureTrackingProvider?) = apply { - this.exposureTrackingProvider = exposureTrackingProvider - } + fun analyticsProvider(analyticsProvider: ExperimentAnalyticsProvider?) = + apply { + this.analyticsProvider = analyticsProvider + } + + fun exposureTrackingProvider(exposureTrackingProvider: ExposureTrackingProvider?) = + apply { + this.exposureTrackingProvider = exposureTrackingProvider + } fun build(): ExperimentConfig { return ExperimentConfig( diff --git a/sdk/src/main/java/com/amplitude/experiment/ExperimentUser.kt b/sdk/src/main/java/com/amplitude/experiment/ExperimentUser.kt index f65764e..4fdc247 100644 --- a/sdk/src/main/java/com/amplitude/experiment/ExperimentUser.kt +++ b/sdk/src/main/java/com/amplitude/experiment/ExperimentUser.kt @@ -39,7 +39,6 @@ class ExperimentUser internal constructor( @JvmField val groups: Map>? = null, @JvmField val groupProperties: Map>>? = null, ) { - /** * Construct an empty [ExperimentUser]. */ @@ -154,54 +153,86 @@ class ExperimentUser internal constructor( private var groupProperties: MutableMap>>? = null fun userId(userId: String?) = apply { this.userId = userId } + fun deviceId(deviceId: String?) = apply { this.deviceId = deviceId } + fun country(country: String?) = apply { this.country = country } + fun region(region: String?) = apply { this.region = region } + fun dma(dma: String?) = apply { this.dma = dma } + fun city(city: String?) = apply { this.city = city } + fun language(language: String?) = apply { this.language = language } + fun platform(platform: String?) = apply { this.platform = platform } + fun version(version: String?) = apply { this.version = version } + fun os(os: String?) = apply { this.os = os } - fun deviceManufacturer(deviceManufacturer: String?) = apply { - this.deviceManufacturer = deviceManufacturer - } + + fun deviceManufacturer(deviceManufacturer: String?) = + apply { + this.deviceManufacturer = deviceManufacturer + } fun deviceBrand(deviceBrand: String?) = apply { this.deviceBrand = deviceBrand } + fun deviceModel(deviceModel: String?) = apply { this.deviceModel = deviceModel } + fun carrier(carrier: String?) = apply { this.carrier = carrier } + fun library(library: String?) = apply { this.library = library } - fun userProperties(userProperties: Map?) = apply { - this.userProperties = userProperties?.toMutableMap() - } - fun userProperty(key: String, value: Any?) = apply { - userProperties = (userProperties ?: mutableMapOf()).apply { - this[key] = value + fun userProperties(userProperties: Map?) = + apply { + this.userProperties = userProperties?.toMutableMap() } - } - fun groups(groups: Map>?) = apply { - this.groups = groups?.toMutableMap() + fun userProperty( + key: String, + value: Any?, + ) = apply { + userProperties = + (userProperties ?: mutableMapOf()).apply { + this[key] = value + } } - fun group(groupType: String, groupName: String) = apply { - this.groups = (this.groups ?: mutableMapOf()).apply { put(groupType, setOf(groupName)) } - } + fun groups(groups: Map>?) = + apply { + this.groups = groups?.toMutableMap() + } - fun groupProperties(groupProperties: Map>>?) = apply { - this.groupProperties = groupProperties?.mapValues { groupTypes -> - groupTypes.value.toMutableMap().mapValues { groupNames -> - groupNames.value.toMutableMap() - }.toMutableMap() - }?.toMutableMap() + fun group( + groupType: String, + groupName: String, + ) = apply { + this.groups = (this.groups ?: mutableMapOf()).apply { put(groupType, setOf(groupName)) } } - fun groupProperty(groupType: String, groupName: String, key: String, value: Any?) = apply { - this.groupProperties = (this.groupProperties ?: mutableMapOf()).apply { - getOrPut(groupType) { mutableMapOf(groupName to mutableMapOf()) } - .getOrPut(groupName) { mutableMapOf(key to value) }[key] = value + fun groupProperties(groupProperties: Map>>?) = + apply { + this.groupProperties = + groupProperties?.mapValues { groupTypes -> + groupTypes.value.toMutableMap().mapValues { groupNames -> + groupNames.value.toMutableMap() + }.toMutableMap() + }?.toMutableMap() } + + fun groupProperty( + groupType: String, + groupName: String, + key: String, + value: Any?, + ) = apply { + this.groupProperties = + (this.groupProperties ?: mutableMapOf()).apply { + getOrPut(groupType) { mutableMapOf(groupName to mutableMapOf()) } + .getOrPut(groupName) { mutableMapOf(key to value) }[key] = value + } } fun build(): ExperimentUser { diff --git a/sdk/src/main/java/com/amplitude/experiment/Exposure.kt b/sdk/src/main/java/com/amplitude/experiment/Exposure.kt index 4cb3e54..2b705f2 100644 --- a/sdk/src/main/java/com/amplitude/experiment/Exposure.kt +++ b/sdk/src/main/java/com/amplitude/experiment/Exposure.kt @@ -34,5 +34,5 @@ data class Exposure internal constructor( val flagKey: String, val variant: String?, val experimentKey: String?, - val metadata: Map? = null + val metadata: Map? = null, ) diff --git a/sdk/src/main/java/com/amplitude/experiment/FetchOptions.kt b/sdk/src/main/java/com/amplitude/experiment/FetchOptions.kt index cf0892b..77172d5 100644 --- a/sdk/src/main/java/com/amplitude/experiment/FetchOptions.kt +++ b/sdk/src/main/java/com/amplitude/experiment/FetchOptions.kt @@ -1,5 +1,7 @@ package com.amplitude.experiment -data class FetchOptions @JvmOverloads constructor( - @JvmField val flagKeys: List? = null -) +data class FetchOptions + @JvmOverloads + constructor( + @JvmField val flagKeys: List? = null, + ) diff --git a/sdk/src/main/java/com/amplitude/experiment/FlagApi.kt b/sdk/src/main/java/com/amplitude/experiment/FlagApi.kt index b40067d..9269f50 100644 --- a/sdk/src/main/java/com/amplitude/experiment/FlagApi.kt +++ b/sdk/src/main/java/com/amplitude/experiment/FlagApi.kt @@ -20,33 +20,35 @@ internal data class GetFlagsOptions( val libraryName: String, val libraryVersion: String, val evaluationMode: String? = null, - val timeoutMillis: Long = ExperimentConfig.Defaults.FETCH_TIMEOUT_MILLIS + val timeoutMillis: Long = ExperimentConfig.Defaults.FETCH_TIMEOUT_MILLIS, ) internal interface FlagApi { fun getFlags( options: GetFlagsOptions? = null, - callback: ((Map) -> Unit)? = null + callback: ((Map) -> Unit)? = null, ): Future> } internal class SdkFlagApi( private val deploymentKey: String, private val serverUrl: HttpUrl, - private val httpClient: OkHttpClient + private val httpClient: OkHttpClient, ) : FlagApi { override fun getFlags( options: GetFlagsOptions?, - callback: ((Map) -> Unit)? + callback: ((Map) -> Unit)?, ): Future> { - val url = serverUrl.newBuilder() - .addPathSegments("sdk/v2/flags") - .addQueryParameter("v", "0") - .build() + val url = + serverUrl.newBuilder() + .addPathSegments("sdk/v2/flags") + .addQueryParameter("v", "0") + .build() - val builder = Request.Builder() - .get() - .url(url).addHeader("Authorization", "Api-Key $deploymentKey") + val builder = + Request.Builder() + .get() + .url(url).addHeader("Authorization", "Api-Key $deploymentKey") options?.let { if (it.libraryName.isNotEmpty() && it.libraryVersion.isNotEmpty()) { @@ -60,31 +62,40 @@ internal class SdkFlagApi( call.timeout().timeout(options.timeoutMillis, TimeUnit.MILLISECONDS) } val future = AsyncFuture(call, callback) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - try { - Logger.d("Received fetch flags response: $response") - if (response.isSuccessful) { - val body = response.body?.string() ?: "" - val flags = json.decodeFromString>(body) - .associateBy { it.key } - future.complete(flags) - } else { - Logger.e("Non-successful response: ${response.code}") - future.completeExceptionally(IOException("Non-successful response: ${response.code}")) + call.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + try { + Logger.d("Received fetch flags response: $response") + if (response.isSuccessful) { + val body = response.body?.string() ?: "" + val flags = + json.decodeFromString>(body) + .associateBy { it.key } + future.complete(flags) + } else { + Logger.e("Non-successful response: ${response.code}") + future.completeExceptionally(IOException("Non-successful response: ${response.code}")) + } + } catch (e: IOException) { + onFailure(call, e) + } catch (e: SerializationException) { + Logger.e("Error decoding JSON: ${e.message}") + future.completeExceptionally(e) } - } catch (e: IOException) { - onFailure(call, e) - } catch (e: SerializationException) { - Logger.e("Error decoding JSON: ${e.message}") - future.completeExceptionally(e) } - } - override fun onFailure(call: Call, e: IOException) { - future.completeExceptionally(e) - } - }) + override fun onFailure( + call: Call, + e: IOException, + ) { + future.completeExceptionally(e) + } + }, + ) return future } } diff --git a/sdk/src/main/java/com/amplitude/experiment/Variant.kt b/sdk/src/main/java/com/amplitude/experiment/Variant.kt index de9a02e..c148f1e 100644 --- a/sdk/src/main/java/com/amplitude/experiment/Variant.kt +++ b/sdk/src/main/java/com/amplitude/experiment/Variant.kt @@ -1,51 +1,53 @@ package com.amplitude.experiment -data class Variant @JvmOverloads constructor( - /** - * The value of the variant. - */ - @JvmField val value: String? = null, - /** - * The attached payload, if any. - */ - @JvmField val payload: Any? = null, - /** - * The experiment key. Used to distinguish two experiments associated with the same flag. - */ - @JvmField val expKey: String? = null, - /** - * The key of the variant. - */ - @JvmField val key: String? = null, - /** - * Flag, segment, and variant metadata produced as a result of - * evaluation for the user. Used for system purposes. - */ - @JvmField val metadata: Map? = null -) { +data class Variant + @JvmOverloads + constructor( + /** + * The value of the variant. + */ + @JvmField val value: String? = null, + /** + * The attached payload, if any. + */ + @JvmField val payload: Any? = null, + /** + * The experiment key. Used to distinguish two experiments associated with the same flag. + */ + @JvmField val expKey: String? = null, + /** + * The key of the variant. + */ + @JvmField val key: String? = null, + /** + * Flag, segment, and variant metadata produced as a result of + * evaluation for the user. Used for system purposes. + */ + @JvmField val metadata: Map? = null, + ) { + /** + * Useful for comparing a variant's key to a string in java. + * + * ``` + * variant.is("on"); + * ``` + * + * is equivalent to + * + * ``` + * "on".equals(variant.key); + * ``` + * + * @param value The value to compare with the key of this variant. + */ + @Suppress("ktlint") + fun `is`(value: String): Boolean = this.key == value - /** - * Useful for comparing a variant's key to a string in java. - * - * ``` - * variant.is("on"); - * ``` - * - * is equivalent to - * - * ``` - * "on".equals(variant.key); - * ``` - * - * @param value The value to compare with the key of this variant. - */ - fun `is`(value: String): Boolean = this.key == value + fun isNullOrEmpty(): Boolean = + this.key == null && this.value == null && this.payload == null && this.expKey == null && this.metadata == null - fun isNullOrEmpty(): Boolean = - this.key == null && this.value == null && this.payload == null && this.expKey == null && this.metadata == null - - fun isDefaultVariant(): Boolean { - val isDefault = metadata?.get("default") as? Boolean - return isDefault ?: false + fun isDefaultVariant(): Boolean { + val isDefault = metadata?.get("default") as? Boolean + return isDefault ?: false + } } -} diff --git a/sdk/src/main/java/com/amplitude/experiment/analytics/ExperimentAnalyticsProvider.kt b/sdk/src/main/java/com/amplitude/experiment/analytics/ExperimentAnalyticsProvider.kt index e097439..21c8404 100644 --- a/sdk/src/main/java/com/amplitude/experiment/analytics/ExperimentAnalyticsProvider.kt +++ b/sdk/src/main/java/com/amplitude/experiment/analytics/ExperimentAnalyticsProvider.kt @@ -6,7 +6,6 @@ package com.amplitude.experiment.analytics */ @Deprecated("Use ExposureTrackingProvider instead") interface ExperimentAnalyticsProvider { - /** * Wraps an analytics event track call. This is typically called by the * experiment client after setting user properties to track an diff --git a/sdk/src/main/java/com/amplitude/experiment/analytics/ExposureEvent.kt b/sdk/src/main/java/com/amplitude/experiment/analytics/ExposureEvent.kt index eda3f6c..c32b9ff 100644 --- a/sdk/src/main/java/com/amplitude/experiment/analytics/ExposureEvent.kt +++ b/sdk/src/main/java/com/amplitude/experiment/analytics/ExposureEvent.kt @@ -14,30 +14,29 @@ class ExposureEvent( * The user exposed to the flag/experiment variant. */ override val user: ExperimentUser, - /** * The key of the flag/experiment that the user has been exposed to. */ override val key: String, - /** * The variant of the flag/experiment that the user has been exposed to. */ override val variant: Variant, - /** * The source of the determination of the variant. */ - val source: VariantSource + val source: VariantSource, ) : ExperimentAnalyticsEvent { override val name: String = "[Experiment] Exposure" - override val properties: Map = mapOf( - "key" to key, - "variant" to variant.key, - "source" to source.toString(), - ) - override val userProperties: Map = mapOf( - "[Experiment] $key" to variant.key - ) + override val properties: Map = + mapOf( + "key" to key, + "variant" to variant.key, + "source" to source.toString(), + ) + override val userProperties: Map = + mapOf( + "[Experiment] $key" to variant.key, + ) override val userProperty: String = "[Experiment] $key" } diff --git a/sdk/src/main/java/com/amplitude/experiment/storage/Cache.kt b/sdk/src/main/java/com/amplitude/experiment/storage/Cache.kt index b0f06c6..e4fa3b6 100644 --- a/sdk/src/main/java/com/amplitude/experiment/storage/Cache.kt +++ b/sdk/src/main/java/com/amplitude/experiment/storage/Cache.kt @@ -12,7 +12,7 @@ internal class LoadStoreCache( private val namespace: String, private val storage: Storage, private val decoder: ((value: String) -> V?), - private val encoder: ((value: V) -> String) + private val encoder: ((value: V) -> String), ) { private val cache: MutableMap = mutableMapOf() @@ -24,7 +24,10 @@ internal class LoadStoreCache( return HashMap(cache) } - fun put(key: String, value: V) { + fun put( + key: String, + value: V, + ) { cache[key] = value } @@ -42,40 +45,46 @@ internal class LoadStoreCache( fun load() { val rawValues = storage.get(namespace) - val values = rawValues.mapNotNull { entry -> - try { - val value = decoder.invoke(entry.value) - if (value != null) { - entry.key to value - } else { + val values = + rawValues.mapNotNull { entry -> + try { + val value = decoder.invoke(entry.value) + if (value != null) { + entry.key to value + } else { + null + } + } catch (e: Exception) { null } - } catch (e: Exception) { - null - } - }.toMap() + }.toMap() clear() putAll(values) } fun store(values: MutableMap = cache) { - val stringValues = values.mapNotNull { entry -> - try { - val value = encoder(entry.value) - if (value != null) { - entry.key to value - } else { + val stringValues = + values.mapNotNull { entry -> + try { + val value = encoder(entry.value) + if (value != null) { + entry.key to value + } else { + null + } + } catch (e: Exception) { null } - } catch (e: Exception) { - null - } - }.toMap() + }.toMap() storage.put(namespace, stringValues) } } -internal fun getVariantStorage(deploymentKey: String, instanceName: String, storage: Storage): LoadStoreCache { +internal fun getVariantStorage( + deploymentKey: String, + instanceName: String, + storage: Storage, +): LoadStoreCache { val truncatedDeployment = deploymentKey.takeLast(6) val namespace = "amp-exp-$instanceName-$truncatedDeployment" return LoadStoreCache(namespace, storage, ::decodeVariantFromStorage, ::encodeVariantToStorage) @@ -84,7 +93,7 @@ internal fun getVariantStorage(deploymentKey: String, instanceName: String, stor internal fun getFlagStorage( deploymentKey: String, instanceName: String, - storage: Storage + storage: Storage, ): LoadStoreCache { val truncatedDeployment = deploymentKey.takeLast(6) val namespace = "amp-exp-$instanceName-$truncatedDeployment-flags" diff --git a/sdk/src/main/java/com/amplitude/experiment/storage/SharedPrefsStorage.kt b/sdk/src/main/java/com/amplitude/experiment/storage/SharedPrefsStorage.kt index 2ec7b77..73ce1f5 100644 --- a/sdk/src/main/java/com/amplitude/experiment/storage/SharedPrefsStorage.kt +++ b/sdk/src/main/java/com/amplitude/experiment/storage/SharedPrefsStorage.kt @@ -10,34 +10,39 @@ import android.content.Context internal class SharedPrefsStorage( appContext: Context, ) : Storage { - private val appContext: Context init { this.appContext = appContext } - override fun get(key: String): Map = synchronized(this) { - val sharedPrefs = appContext.getSharedPreferences(key, Context.MODE_PRIVATE) - val result = mutableMapOf() - for ((spKey, spValue) in sharedPrefs.all) { - if (spValue is String) { - result[spKey] = spValue + override fun get(key: String): Map = + synchronized(this) { + val sharedPrefs = appContext.getSharedPreferences(key, Context.MODE_PRIVATE) + val result = mutableMapOf() + for ((spKey, spValue) in sharedPrefs.all) { + if (spValue is String) { + result[spKey] = spValue + } } + return result } - return result - } - override fun put(key: String, value: Map): Unit = synchronized(this) { - val editor = appContext.getSharedPreferences(key, Context.MODE_PRIVATE).edit() - editor.clear() - for ((k, v) in value) { - editor.putString(k, v) + override fun put( + key: String, + value: Map, + ): Unit = + synchronized(this) { + val editor = appContext.getSharedPreferences(key, Context.MODE_PRIVATE).edit() + editor.clear() + for ((k, v) in value) { + editor.putString(k, v) + } + editor.commit() } - editor.commit() - } - override fun delete(key: String): Unit = synchronized(this) { - appContext.getSharedPreferences(key, Context.MODE_PRIVATE).edit().remove(key).commit() - } + override fun delete(key: String): Unit = + synchronized(this) { + appContext.getSharedPreferences(key, Context.MODE_PRIVATE).edit().remove(key).commit() + } } diff --git a/sdk/src/main/java/com/amplitude/experiment/storage/Storage.kt b/sdk/src/main/java/com/amplitude/experiment/storage/Storage.kt index 26bb094..9961abc 100644 --- a/sdk/src/main/java/com/amplitude/experiment/storage/Storage.kt +++ b/sdk/src/main/java/com/amplitude/experiment/storage/Storage.kt @@ -2,6 +2,11 @@ package com.amplitude.experiment.storage internal interface Storage { fun get(key: String): Map - fun put(key: String, value: Map) + + fun put( + key: String, + value: Map, + ) + fun delete(key: String) } diff --git a/sdk/src/main/java/com/amplitude/experiment/util/AsyncFuture.kt b/sdk/src/main/java/com/amplitude/experiment/util/AsyncFuture.kt index 5881b1c..1829067 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/AsyncFuture.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/AsyncFuture.kt @@ -9,11 +9,12 @@ import java.util.concurrent.TimeoutException internal class AsyncFuture( private val call: Call? = null, - private val callback: ((T) -> Unit)? = null + private val callback: ((T) -> Unit)? = null, ) : Future { - @Volatile private var value: T? = null + @Volatile private var completed = false + @Volatile private var throwable: Throwable? = null private val lock = Object() @@ -45,7 +46,10 @@ internal class AsyncFuture( } @Throws(InterruptedException::class, ExecutionException::class, TimeoutException::class) - override fun get(timeout: Long, unit: TimeUnit): T { + override fun get( + timeout: Long, + unit: TimeUnit, + ): T { var nanosRemaining = unit.toNanos(timeout) val end = System.nanoTime() + nanosRemaining synchronized(lock) { diff --git a/sdk/src/main/java/com/amplitude/experiment/util/Backoff.kt b/sdk/src/main/java/com/amplitude/experiment/util/Backoff.kt index 2857d48..5166703 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/Backoff.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/Backoff.kt @@ -23,49 +23,52 @@ internal class Backoff constructor( private val config: BackoffConfig, private val executorService: ScheduledExecutorService, ) { - private val lock = Any() private var started = false private var cancelled = false private var future: Future<*>? = null - fun start(function: () -> Unit) = synchronized(lock) { - if (started) { - return + fun start(function: () -> Unit) = + synchronized(lock) { + if (started) { + return + } + started = true + backoff(0, config.min, function) } - started = true - backoff(0, config.min, function) - } - fun cancel() = synchronized(lock) { - if (!cancelled) { - cancelled = true - future?.cancel(true) + fun cancel() = + synchronized(lock) { + if (!cancelled) { + cancelled = true + future?.cancel(true) + } } - } private fun backoff( attempt: Int, delay: Long, - function: () -> Unit - ): Unit = synchronized(lock) { - future = executorService.schedule( - { - if (cancelled) { - return@schedule - } - try { - function.invoke() - } catch (e: Exception) { - // Retry the request function - val nextAttempt = attempt + 1 - if (nextAttempt < config.attempts) { - val nextDelay = min(delay * config.scalar, config.max.toFloat()).toLong() - backoff(nextAttempt, nextDelay, function) - } - } - }, - delay, TimeUnit.MILLISECONDS - ) - } + function: () -> Unit, + ): Unit = + synchronized(lock) { + future = + executorService.schedule( + { + if (cancelled) { + return@schedule + } + try { + function.invoke() + } catch (e: Exception) { + // Retry the request function + val nextAttempt = attempt + 1 + if (nextAttempt < config.attempts) { + val nextDelay = min(delay * config.scalar, config.max.toFloat()).toLong() + backoff(nextAttempt, nextDelay, function) + } + } + }, + delay, TimeUnit.MILLISECONDS, + ) + } } diff --git a/sdk/src/main/java/com/amplitude/experiment/util/FetchException.kt b/sdk/src/main/java/com/amplitude/experiment/util/FetchException.kt index c776744..0950bcf 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/FetchException.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/FetchException.kt @@ -4,5 +4,5 @@ import okio.IOException internal class FetchException( val statusCode: Int, - message: String + message: String, ) : IOException(message) diff --git a/sdk/src/main/java/com/amplitude/experiment/util/Lock.kt b/sdk/src/main/java/com/amplitude/experiment/util/Lock.kt index 2669d0c..34d213a 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/Lock.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/Lock.kt @@ -4,11 +4,11 @@ import java.util.concurrent.TimeoutException internal sealed class LockResult { data class Success(val value: T) : LockResult() + data class Error(val error: Exception) : LockResult() } internal class Lock { - private val lock = Object() private var result: LockResult? = null @@ -25,7 +25,7 @@ internal class Lock { } } result ?: LockResult.Error( - TimeoutException("Lock timed out waiting $ms ms for notify.") + TimeoutException("Lock timed out waiting $ms ms for notify."), ) } } diff --git a/sdk/src/main/java/com/amplitude/experiment/util/Logger.kt b/sdk/src/main/java/com/amplitude/experiment/util/Logger.kt index 8cf41db..7ba2a2f 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/Logger.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/Logger.kt @@ -4,14 +4,23 @@ import android.util.Log internal interface ILogger { fun v(msg: String) + fun d(msg: String) + fun i(msg: String) - fun w(msg: String, e: Throwable? = null) - fun e(msg: String, e: Throwable? = null) + + fun w( + msg: String, + e: Throwable? = null, + ) + + fun e( + msg: String, + e: Throwable? = null, + ) } internal object Logger : ILogger { - internal var implementation: ILogger? = null override fun v(msg: String) { @@ -26,17 +35,22 @@ internal object Logger : ILogger { implementation?.i(msg) } - override fun w(msg: String, e: Throwable?) { + override fun w( + msg: String, + e: Throwable?, + ) { implementation?.w(msg) } - override fun e(msg: String, e: Throwable?) { + override fun e( + msg: String, + e: Throwable?, + ) { implementation?.e(msg, e) } } internal class AndroidLogger(private val debug: Boolean) : ILogger { - private val tag = "Experiment" override fun v(msg: String) { @@ -57,11 +71,17 @@ internal class AndroidLogger(private val debug: Boolean) : ILogger { } } - override fun w(msg: String, e: Throwable?) { + override fun w( + msg: String, + e: Throwable?, + ) { Log.w(tag, msg) } - override fun e(msg: String, e: Throwable?) { + override fun e( + msg: String, + e: Throwable?, + ) { Log.e(tag, msg, e) } } diff --git a/sdk/src/main/java/com/amplitude/experiment/util/Poller.kt b/sdk/src/main/java/com/amplitude/experiment/util/Poller.kt index 210ac3e..7f9b760 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/Poller.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/Poller.kt @@ -7,14 +7,15 @@ import java.util.concurrent.TimeUnit internal class Poller( private val executorService: ScheduledExecutorService, private val action: () -> Unit, - private val ms: Long + private val ms: Long, ) { private var future: ScheduledFuture<*>? = null internal fun start() { - future = this.executorService.scheduleAtFixedRate( - action, ms, ms, TimeUnit.MILLISECONDS - ) + future = + this.executorService.scheduleAtFixedRate( + action, ms, ms, TimeUnit.MILLISECONDS, + ) } internal fun stop() { diff --git a/sdk/src/main/java/com/amplitude/experiment/util/SessionAnalyticsProvider.kt b/sdk/src/main/java/com/amplitude/experiment/util/SessionAnalyticsProvider.kt index f11fb76..65f39f4 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/SessionAnalyticsProvider.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/SessionAnalyticsProvider.kt @@ -6,7 +6,6 @@ import com.amplitude.experiment.analytics.ExperimentAnalyticsProvider internal class SessionAnalyticsProvider( private val analyticsProvider: ExperimentAnalyticsProvider, ) : ExperimentAnalyticsProvider { - private val lock = Any() private val setProperties = mutableMapOf() private val unsetProperties = mutableSetOf() diff --git a/sdk/src/main/java/com/amplitude/experiment/util/User.kt b/sdk/src/main/java/com/amplitude/experiment/util/User.kt index 05235b4..be629f6 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/User.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/User.kt @@ -25,15 +25,15 @@ internal fun ExperimentUser.toJson(): String { json.put("library", library) json.put( "user_properties", - JSONObject(userProperties?.toMutableMap() ?: mutableMapOf()) + JSONObject(userProperties?.toMutableMap() ?: mutableMapOf()), ) json.put( "groups", - groups?.toJSONObject() + groups?.toJSONObject(), ) json.put( "group_properties", - groupProperties?.toJSONObject() + groupProperties?.toJSONObject(), ) } catch (e: JSONException) { Logger.w("Error converting SkylabUser to JSONObject", e) @@ -86,7 +86,7 @@ internal fun ExperimentUser.toMap(): Map { "library" to library, "user_properties" to userProperties, "groups" to groups, - "group_properties" to groupProperties + "group_properties" to groupProperties, ).filterValues { it != null } } @@ -113,7 +113,7 @@ internal fun ExperimentUser?.merge(other: ExperimentUser?): ExperimentUser { .version(user.version.merge(other?.version)) .os(user.os.merge(other?.os)) .deviceManufacturer( - user.deviceManufacturer.merge(other?.deviceManufacturer) + user.deviceManufacturer.merge(other?.deviceManufacturer), ) .deviceBrand(user.deviceBrand.merge(other?.deviceBrand)) .deviceModel(user.deviceModel.merge(other?.deviceModel)) @@ -127,7 +127,10 @@ internal fun ExperimentUser?.merge(other: ExperimentUser?): ExperimentUser { // Private Helpers -private fun Map?.mergeMapValues(other: Map?, merger: (T, T) -> T?): Map? { +private fun Map?.mergeMapValues( + other: Map?, + merger: (T, T) -> T?, +): Map? { return when { this == null -> other other == null -> this @@ -135,11 +138,12 @@ private fun Map?.mergeMapValues(other: Map?, merger: ( val result = mutableMapOf() for ((thisKey, thisValue) in this.entries) { val otherValue = other[thisKey] - val value = if (otherValue != null) { - merger(thisValue, otherValue) - } else { - thisValue - } + val value = + if (otherValue != null) { + merger(thisValue, otherValue) + } else { + thisValue + } if (value != null) { result[thisKey] = value } @@ -154,7 +158,10 @@ private fun Map?.mergeMapValues(other: Map?, merger: ( } } -private fun T?.merge(other: T?, merger: (T, T) -> T = { t, o -> t }): T? { +private fun T?.merge( + other: T?, + merger: (T, T) -> T = { t, o -> t }, +): T? { return when { this == null -> other other == null -> this diff --git a/sdk/src/main/java/com/amplitude/experiment/util/UserSessionExposureTracker.kt b/sdk/src/main/java/com/amplitude/experiment/util/UserSessionExposureTracker.kt index 76bfe1f..0e37280 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/UserSessionExposureTracker.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/UserSessionExposureTracker.kt @@ -8,12 +8,14 @@ import com.amplitude.experiment.ExposureTrackingProvider internal class UserSessionExposureTracker( private val trackingProvider: ExposureTrackingProvider, ) { - private val lock = Any() private val tracked = mutableSetOf() private var identity = Identity() - fun track(exposure: Exposure, user: ExperimentUser? = null) { + fun track( + exposure: Exposure, + user: ExperimentUser? = null, + ) { synchronized(lock) { val newIdentity = user.toIdentity() if (!identity.identityEquals(newIdentity)) { @@ -30,10 +32,10 @@ internal class UserSessionExposureTracker( } } -private fun ExperimentUser?.toIdentity() = Identity( - userId = this?.userId, - deviceId = this?.deviceId -) +private fun ExperimentUser?.toIdentity() = + Identity( + userId = this?.userId, + deviceId = this?.deviceId, + ) -private fun Identity.identityEquals(other: Identity): Boolean = - this.userId == other.userId && this.deviceId == other.deviceId +private fun Identity.identityEquals(other: Identity): Boolean = this.userId == other.userId && this.deviceId == other.deviceId diff --git a/sdk/src/main/java/com/amplitude/experiment/util/Variant.kt b/sdk/src/main/java/com/amplitude/experiment/util/Variant.kt index d1def9b..f267234 100644 --- a/sdk/src/main/java/com/amplitude/experiment/util/Variant.kt +++ b/sdk/src/main/java/com/amplitude/experiment/util/Variant.kt @@ -38,78 +38,89 @@ internal fun String?.toVariant(): Variant? { internal fun JSONObject?.toVariant(): Variant? { return if (this == null) { null - } else try { - var key = when { - has("key") -> getString("key") - else -> null - } - val value = when { - has("value") -> getString("value") - else -> null - } + } else { + try { + var key = + when { + has("key") -> getString("key") + else -> null + } + val value = + when { + has("value") -> getString("value") + else -> null + } - if (key == null && value == null) { - return null - } - if (key == null && value != null) { - key = value - } + if (key == null && value == null) { + return null + } + if (key == null && value != null) { + key = value + } + + val payload = + when { + has("payload") -> { + get("payload") + } + else -> null + } - val payload = when { - has("payload") -> { - get("payload") + var expKey = + when { + has("expKey") -> getString("expKey") + else -> null + } + var metadata = + when { + has("metadata") -> getJSONObject("metadata").toMap() + else -> null + }?.toMutableMap() + if (metadata != null && metadata["experimentKey"] != null) { + expKey = metadata["experimentKey"] as? String + } else if (expKey != null) { + metadata = metadata ?: HashMap() + metadata["experimentKey"] = expKey } - else -> null - } - var expKey = when { - has("expKey") -> getString("expKey") - else -> null - } - var metadata = when { - has("metadata") -> getJSONObject("metadata").toMap() - else -> null - }?.toMutableMap() - if (metadata != null && metadata["experimentKey"] != null) { - expKey = metadata["experimentKey"] as? String - } else if (expKey != null) { - metadata = metadata ?: HashMap() - metadata["experimentKey"] = expKey + Variant(value, payload, expKey, key, metadata) + } catch (e: JSONException) { + e.printStackTrace() + Logger.w("Error parsing Variant from json string $this, $e") + null } - - Variant(value, payload, expKey, key, metadata) - } catch (e: JSONException) { - e.printStackTrace() - Logger.w("Error parsing Variant from json string $this, $e") - null } } internal fun EvaluationVariant.convertToVariant(): Variant { val experimentKey = this.metadata?.get("experimentKey")?.toString() - val value = when { - this.value != null -> this.value.toString() - else -> null - } - val expKey = when { - experimentKey != null -> experimentKey - else -> null - } - val payload = when { - this.payload != null -> { - if (this.payload is Map<*, *>) { - this.payload.toJSONObject() - } else if (this.payload is Collection<*>) { - this.payload.toJSONArray() - } else { - this.payload + val value = + when { + this.value != null -> this.value.toString() + else -> null + } + val expKey = + when { + experimentKey != null -> experimentKey + else -> null + } + val payload = + when { + this.payload != null -> { + if (this.payload is Map<*, *>) { + this.payload.toJSONObject() + } else if (this.payload is Collection<*>) { + this.payload.toJSONArray() + } else { + this.payload + } } + else -> null + } + val metadata = + when { + this.metadata != null -> this.metadata + else -> null } - else -> null - } - val metadata = when { - this.metadata != null -> this.metadata - else -> null - } return Variant(value, payload, expKey, this.key, metadata) } diff --git a/sdk/src/test/java/com/amplitude/experiment/ConnectorExposureTrackingProviderTest.kt b/sdk/src/test/java/com/amplitude/experiment/ConnectorExposureTrackingProviderTest.kt index ae62ba1..df5bb0c 100644 --- a/sdk/src/test/java/com/amplitude/experiment/ConnectorExposureTrackingProviderTest.kt +++ b/sdk/src/test/java/com/amplitude/experiment/ConnectorExposureTrackingProviderTest.kt @@ -10,15 +10,16 @@ import org.junit.Test class TestEventBridge : EventBridge { var logEventCount = 0 var recentEvent: AnalyticsEvent? = null + override fun logEvent(event: AnalyticsEvent) { recentEvent = event logEventCount++ } + override fun setEventReceiver(receiver: AnalyticsEventReceiver?) {} } class ConnectorExposureTrackingProviderTest { - @Test fun `track called once each per variant for different flag keys`() { val eventBridge = TestEventBridge() diff --git a/sdk/src/test/java/com/amplitude/experiment/ExperimentClientTest.kt b/sdk/src/test/java/com/amplitude/experiment/ExperimentClientTest.kt index 24056ff..906ea4e 100644 --- a/sdk/src/test/java/com/amplitude/experiment/ExperimentClientTest.kt +++ b/sdk/src/test/java/com/amplitude/experiment/ExperimentClientTest.kt @@ -30,7 +30,6 @@ private const val KEY = "sdk-ci-test" private const val INITIAL_KEY = "initial-key" class ExperimentClientTest { - init { Logger.implementation = SystemLogger(true) } @@ -43,57 +42,62 @@ class ExperimentClientTest { private val initialVariant = Variant(key = "initial", value = "initial") private val inlineVariant = Variant(key = "inline", value = "inline") - private val initialVariants = mapOf( - INITIAL_KEY to initialVariant, - KEY to Variant(key = "off"), - ) - - private val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - fallbackVariant = fallbackVariant, - initialVariants = initialVariants, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) - - private val timeoutClient = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - fallbackVariant = fallbackVariant, - initialVariants = initialVariants, - fetchTimeoutMillis = 1, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) - - private val initialVariantSourceClient = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - source = Source.INITIAL_VARIANTS, - initialVariants = initialVariants, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) - - private val generalClient = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + private val initialVariants = + mapOf( + INITIAL_KEY to initialVariant, + KEY to Variant(key = "off"), + ) + + private val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + fallbackVariant = fallbackVariant, + initialVariants = initialVariants, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) + + private val timeoutClient = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + fallbackVariant = fallbackVariant, + initialVariants = initialVariants, + fetchTimeoutMillis = 1, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) + + private val initialVariantSourceClient = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + source = Source.INITIAL_VARIANTS, + initialVariants = initialVariants, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) + + private val generalClient = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) @Before fun init() { @@ -236,43 +240,45 @@ class ExperimentClientTest { fun `test exposure event through analytics provider when variant called`() { var didExposureGetTracked = false var didUserPropertyGetSet = false - val analyticsProvider = object : ExperimentAnalyticsProvider { - override fun track(event: ExperimentAnalyticsEvent) { - Assert.assertEquals("[Experiment] Exposure", event.name) - Assert.assertEquals( - mapOf( - "key" to KEY, - "variant" to serverVariant.key, - "source" to VariantSource.LOCAL_STORAGE.toString() - ), - event.properties - ) - - Assert.assertEquals(KEY, event.key) - Assert.assertEquals(serverVariant, event.variant) - didExposureGetTracked = true - } + val analyticsProvider = + object : ExperimentAnalyticsProvider { + override fun track(event: ExperimentAnalyticsEvent) { + Assert.assertEquals("[Experiment] Exposure", event.name) + Assert.assertEquals( + mapOf( + "key" to KEY, + "variant" to serverVariant.key, + "source" to VariantSource.LOCAL_STORAGE.toString(), + ), + event.properties, + ) + + Assert.assertEquals(KEY, event.key) + Assert.assertEquals(serverVariant, event.variant) + didExposureGetTracked = true + } - override fun setUserProperty(event: ExperimentAnalyticsEvent) { - Assert.assertEquals("[Experiment] $KEY", event.userProperty) - Assert.assertEquals(serverVariant, event.variant) - didUserPropertyGetSet = true - } + override fun setUserProperty(event: ExperimentAnalyticsEvent) { + Assert.assertEquals("[Experiment] $KEY", event.userProperty) + Assert.assertEquals(serverVariant, event.variant) + didUserPropertyGetSet = true + } - override fun unsetUserProperty(event: ExperimentAnalyticsEvent) { - Assert.fail("analytics provider unset() should not be called") + override fun unsetUserProperty(event: ExperimentAnalyticsEvent) { + Assert.fail("analytics provider unset() should not be called") + } } - } - val analyticsProviderClient = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - analyticsProvider = analyticsProvider, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val analyticsProviderClient = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + analyticsProvider = analyticsProvider, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) analyticsProviderClient.fetch(testUser).get() analyticsProviderClient.variant(KEY) Assert.assertTrue(didExposureGetTracked) @@ -282,40 +288,42 @@ class ExperimentClientTest { @Test fun `test exposure event not tracked on fallback variant and unset called`() { var didExposureGetUnset = false - val analyticsProvider = object : ExperimentAnalyticsProvider { - override fun track(event: ExperimentAnalyticsEvent) { - Assert.fail("analytics provider track() should not be called.") - } + val analyticsProvider = + object : ExperimentAnalyticsProvider { + override fun track(event: ExperimentAnalyticsEvent) { + Assert.fail("analytics provider track() should not be called.") + } - override fun setUserProperty(event: ExperimentAnalyticsEvent) { - Assert.fail("analytics provider setUserProperty() should not be called") - } + override fun setUserProperty(event: ExperimentAnalyticsEvent) { + Assert.fail("analytics provider setUserProperty() should not be called") + } - override fun unsetUserProperty(event: ExperimentAnalyticsEvent) { - Assert.assertEquals( - event.userProperty, - "[Experiment] asdf" - ) - Assert.assertEquals( - event.variant, - fallbackVariant - ) - Assert.assertEquals(event.properties["source"], "fallback-config") - didExposureGetUnset = true + override fun unsetUserProperty(event: ExperimentAnalyticsEvent) { + Assert.assertEquals( + event.userProperty, + "[Experiment] asdf", + ) + Assert.assertEquals( + event.variant, + fallbackVariant, + ) + Assert.assertEquals(event.properties["source"], "fallback-config") + didExposureGetUnset = true + } } - } - val analyticsProviderClient = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - fallbackVariant = fallbackVariant, - initialVariants = initialVariants, - analyticsProvider = analyticsProvider, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val analyticsProviderClient = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + fallbackVariant = fallbackVariant, + initialVariants = initialVariants, + analyticsProvider = analyticsProvider, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) analyticsProviderClient.fetch(testUser).get() analyticsProviderClient.variant("asdf") Assert.assertTrue(didExposureGetUnset) @@ -324,40 +332,42 @@ class ExperimentClientTest { @Test fun `test exposure event not tracked on secondary variant and unset not called`() { var didExposureGetUnset = false - val analyticsProvider = object : ExperimentAnalyticsProvider { - override fun track(event: ExperimentAnalyticsEvent) { - Assert.assertEquals( - event.userProperty, - "[Experiment] $INITIAL_KEY" - ) - Assert.assertEquals( - event.variant, - initialVariants[INITIAL_KEY] - ) - Assert.assertEquals(event.properties["source"], "secondary-initial") - didExposureGetUnset = true - } + val analyticsProvider = + object : ExperimentAnalyticsProvider { + override fun track(event: ExperimentAnalyticsEvent) { + Assert.assertEquals( + event.userProperty, + "[Experiment] $INITIAL_KEY", + ) + Assert.assertEquals( + event.variant, + initialVariants[INITIAL_KEY], + ) + Assert.assertEquals(event.properties["source"], "secondary-initial") + didExposureGetUnset = true + } - override fun setUserProperty(event: ExperimentAnalyticsEvent) { - Assert.fail("analytics provider set() should not be called.") - } + override fun setUserProperty(event: ExperimentAnalyticsEvent) { + Assert.fail("analytics provider set() should not be called.") + } - override fun unsetUserProperty(event: ExperimentAnalyticsEvent) { - Assert.assertEquals("[Experiment] $INITIAL_KEY", event.userProperty) + override fun unsetUserProperty(event: ExperimentAnalyticsEvent) { + Assert.assertEquals("[Experiment] $INITIAL_KEY", event.userProperty) + } } - } - val analyticsProviderClient = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - fallbackVariant = fallbackVariant, - initialVariants = initialVariants, - analyticsProvider = analyticsProvider, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val analyticsProviderClient = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + fallbackVariant = fallbackVariant, + initialVariants = initialVariants, + analyticsProvider = analyticsProvider, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) analyticsProviderClient.fetch(testUser).get() analyticsProviderClient.variant(INITIAL_KEY) Assert.assertFalse(didExposureGetUnset) @@ -367,37 +377,39 @@ class ExperimentClientTest { fun `test exposure event through analytics provider with user properties`() { var didExposureGetTracked = false var didUserPropertyGetSet = false - val analyticsProvider = object : ExperimentAnalyticsProvider { - override fun track(event: ExperimentAnalyticsEvent) { - Assert.assertEquals( - event.userProperties, - mapOf("[Experiment] $KEY" to serverVariant.key) - ) - didExposureGetTracked = true - } + val analyticsProvider = + object : ExperimentAnalyticsProvider { + override fun track(event: ExperimentAnalyticsEvent) { + Assert.assertEquals( + event.userProperties, + mapOf("[Experiment] $KEY" to serverVariant.key), + ) + didExposureGetTracked = true + } - override fun setUserProperty(event: ExperimentAnalyticsEvent) { - Assert.assertEquals( - "[Experiment] $KEY", - event.userProperty - ) - didUserPropertyGetSet = true - } + override fun setUserProperty(event: ExperimentAnalyticsEvent) { + Assert.assertEquals( + "[Experiment] $KEY", + event.userProperty, + ) + didUserPropertyGetSet = true + } - override fun unsetUserProperty(event: ExperimentAnalyticsEvent) { - Assert.fail("analytics provider unset() should not be called") + override fun unsetUserProperty(event: ExperimentAnalyticsEvent) { + Assert.fail("analytics provider unset() should not be called") + } } - } - val analyticsProviderClient = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - analyticsProvider = analyticsProvider, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val analyticsProviderClient = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + analyticsProvider = analyticsProvider, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) analyticsProviderClient.fetch(testUser).get() analyticsProviderClient.variant(KEY) Assert.assertTrue(didExposureGetTracked) @@ -407,112 +419,120 @@ class ExperimentClientTest { @Test fun `test exposure through exposure tracking provider has experiment key from variant`() { var didTrack = false - val exposureTrackingProvider = object : ExposureTrackingProvider { - override fun track(exposure: Exposure) { - Assert.assertEquals("flagKey", exposure.flagKey) - Assert.assertEquals("variant", exposure.variant) - Assert.assertEquals("experimentKey", exposure.experimentKey) - didTrack = true + val exposureTrackingProvider = + object : ExposureTrackingProvider { + override fun track(exposure: Exposure) { + Assert.assertEquals("flagKey", exposure.flagKey) + Assert.assertEquals("variant", exposure.variant) + Assert.assertEquals("experimentKey", exposure.experimentKey) + didTrack = true + } } - } - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - exposureTrackingProvider = exposureTrackingProvider, - source = Source.INITIAL_VARIANTS, - initialVariants = mapOf("flagKey" to Variant(key = "variant", expKey = "experimentKey")) - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + exposureTrackingProvider = exposureTrackingProvider, + source = Source.INITIAL_VARIANTS, + initialVariants = mapOf("flagKey" to Variant(key = "variant", expKey = "experimentKey")), + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.variant("flagKey") Assert.assertTrue(didTrack) } @Test fun `ServerZone - test no config uses defaults`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig(), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig(), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) Assert.assertEquals("https://api.lab.amplitude.com/".toHttpUrl(), client.serverUrl) Assert.assertEquals("https://flag.lab.amplitude.com/".toHttpUrl(), client.flagsServerUrl) } @Test fun `ServerZone - test us server zone config uses defaults`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig(serverZone = ServerZone.US), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig(serverZone = ServerZone.US), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) Assert.assertEquals("https://api.lab.amplitude.com/".toHttpUrl(), client.serverUrl) Assert.assertEquals("https://flag.lab.amplitude.com/".toHttpUrl(), client.flagsServerUrl) } @Test fun `ServerZone - test us server zone config with explicit config uses explicit config`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - serverZone = ServerZone.US, - serverUrl = "https://experiment.company.com", - flagsServerUrl = "https://flags.company.com" - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + serverZone = ServerZone.US, + serverUrl = "https://experiment.company.com", + flagsServerUrl = "https://flags.company.com", + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) Assert.assertEquals("https://experiment.company.com".toHttpUrl(), client.serverUrl) Assert.assertEquals("https://flags.company.com".toHttpUrl(), client.flagsServerUrl) } @Test fun `ServerZone - test eu server zone uses eu defaults`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig(serverZone = ServerZone.EU), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig(serverZone = ServerZone.EU), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) Assert.assertEquals("https://api.lab.eu.amplitude.com/".toHttpUrl(), client.serverUrl) Assert.assertEquals("https://flag.lab.eu.amplitude.com/".toHttpUrl(), client.flagsServerUrl) } @Test fun `ServerZone - test eu server zone with explicit config uses explicit config`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - serverZone = ServerZone.EU, - serverUrl = "https://experiment.company.com", - flagsServerUrl = "https://flags.company.com" - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + serverZone = ServerZone.EU, + serverUrl = "https://experiment.company.com", + flagsServerUrl = "https://flags.company.com", + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) Assert.assertEquals("https://experiment.company.com".toHttpUrl(), client.serverUrl) Assert.assertEquals("https://flags.company.com".toHttpUrl(), client.flagsServerUrl) } @Test fun `LocalEvaluation - test start loads flags into local storage`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig(fetchOnStart = true), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig(fetchOnStart = true), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(ExperimentUser(deviceId = "test_device")).get() Assert.assertEquals("sdk-ci-test-local", client.allFlags()["sdk-ci-test-local"]?.key) client.stop() @@ -520,13 +540,14 @@ class ExperimentClientTest { @Test fun `LocalEvaluation - test variant after start returns expected locally evaluated variant`() { - val client = DefaultExperimentClient( - SERVER_API_KEY, - ExperimentConfig(fetchOnStart = true), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + SERVER_API_KEY, + ExperimentConfig(fetchOnStart = true), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(ExperimentUser(deviceId = "test_device")).get() var variant = client.variant("sdk-ci-test-local") Assert.assertEquals("on", variant.key) @@ -540,13 +561,14 @@ class ExperimentClientTest { @Test fun `LocalEvaluation - remote evaluation variant preferred over local evaluation variant`() { - val client = DefaultExperimentClient( - SERVER_API_KEY, - ExperimentConfig(fetchOnStart = false), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + SERVER_API_KEY, + ExperimentConfig(fetchOnStart = false), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) val user = ExperimentUser(userId = "test_user", deviceId = "test_device") client.start(user).get() var variant = client.variant("sdk-ci-test") @@ -565,17 +587,18 @@ class ExperimentClientTest { val user = ExperimentUser(userId = "test_user") val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test") Assert.assertEquals("on", variant.key) @@ -585,7 +608,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == "on" - } + }, ) } } @@ -595,19 +618,20 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - initialVariants = mapOf("sdk-ci-test" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + initialVariants = mapOf("sdk-ci-test" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test", inlineVariant) Assert.assertEquals(inlineVariant, variant) @@ -615,7 +639,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == null - } + }, ) } } @@ -625,19 +649,20 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - initialVariants = mapOf("sdk-ci-test" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + initialVariants = mapOf("sdk-ci-test" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test") Assert.assertEquals(initialVariant, variant) @@ -645,7 +670,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == null - } + }, ) } } @@ -655,19 +680,20 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test") Assert.assertEquals(Variant(key = "fallback", value = "fallback"), variant) @@ -675,7 +701,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == null - } + }, ) } } @@ -685,18 +711,19 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test") Assert.assertEquals(variant.key, "off") @@ -706,7 +733,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == null - } + }, ) } } @@ -716,19 +743,20 @@ class ExperimentClientTest { val user = ExperimentUser(userId = "test_user") val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.INITIAL_VARIANTS, - initialVariants = mapOf("sdk-ci-test" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.INITIAL_VARIANTS, + initialVariants = mapOf("sdk-ci-test" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test") Assert.assertEquals(initialVariant, variant) @@ -736,7 +764,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == "initial" - } + }, ) } } @@ -746,19 +774,20 @@ class ExperimentClientTest { val user = ExperimentUser(userId = "test_user") val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.INITIAL_VARIANTS, - initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.INITIAL_VARIANTS, + initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test", inlineVariant) Assert.assertEquals("on", variant.key) @@ -768,7 +797,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == "on" - } + }, ) } } @@ -778,19 +807,20 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.INITIAL_VARIANTS, - initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.INITIAL_VARIANTS, + initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test", inlineVariant) Assert.assertEquals(inlineVariant, variant) @@ -798,7 +828,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == null - } + }, ) } } @@ -808,19 +838,20 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.INITIAL_VARIANTS, - initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.INITIAL_VARIANTS, + initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test") Assert.assertEquals(fallbackVariant, variant) @@ -828,7 +859,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == null - } + }, ) } } @@ -838,18 +869,19 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.INITIAL_VARIANTS, - initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.INITIAL_VARIANTS, + initialVariants = mapOf("sdk-ci-test-not-selected" to initialVariant), + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test") Assert.assertEquals(Variant(key = "off", metadata = mapOf("default" to true)), variant) @@ -857,7 +889,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test" && it.variant == null - } + }, ) } } @@ -867,19 +899,20 @@ class ExperimentClientTest { val user = ExperimentUser(deviceId = "0123456789") val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - initialVariants = mapOf("sdk-ci-test-local" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + initialVariants = mapOf("sdk-ci-test-local" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test-local", inlineVariant) Assert.assertEquals("on", variant.key) @@ -889,7 +922,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test-local" && it.variant == "on" - } + }, ) } } @@ -899,19 +932,20 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - initialVariants = mapOf("sdk-ci-test-local" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + initialVariants = mapOf("sdk-ci-test-local" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test-local", inlineVariant) Assert.assertEquals(inlineVariant, variant) @@ -919,7 +953,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test-local" && it.variant == null - } + }, ) } } @@ -929,19 +963,20 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - initialVariants = mapOf("sdk-ci-test-local" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + initialVariants = mapOf("sdk-ci-test-local" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test-local") Assert.assertEquals(initialVariant, variant) @@ -949,7 +984,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test-local" && it.variant == null - } + }, ) } } @@ -959,19 +994,20 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - initialVariants = mapOf("sdk-ci-test-local-not-selected" to initialVariant), - fallbackVariant = fallbackVariant - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + initialVariants = mapOf("sdk-ci-test-local-not-selected" to initialVariant), + fallbackVariant = fallbackVariant, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test-local") Assert.assertEquals(fallbackVariant, variant) @@ -979,7 +1015,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test-local" && it.variant == null - } + }, ) } } @@ -989,17 +1025,18 @@ class ExperimentClientTest { val user = ExperimentUser() val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val variant = client.variant("sdk-ci-test-local") Assert.assertEquals("off", variant.key) @@ -1008,7 +1045,7 @@ class ExperimentClientTest { exposureTrackingProvider.track( match { it.flagKey == "sdk-ci-test-local" && it.variant == null - } + }, ) } } @@ -1017,18 +1054,19 @@ class ExperimentClientTest { fun `LocalEvaluationFlags - test all returns local evaluation variant over remote or initialVariants with local storage source`() { val user = ExperimentUser(userId = "test_user", deviceId = "0123456789") val exposureTrackingProvider = TestExposureTrackingProvider() - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.LOCAL_STORAGE, - initialVariants = mapOf("sdk-ci-test" to initialVariant, "sdk-ci-test-local" to initialVariant) - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.LOCAL_STORAGE, + initialVariants = mapOf("sdk-ci-test" to initialVariant, "sdk-ci-test-local" to initialVariant), + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val allVariants = client.all() val localVariant = allVariants["sdk-ci-test-local"] @@ -1044,18 +1082,19 @@ class ExperimentClientTest { fun `LocalEvaluationFlags - test all returns local evaluation variant over remote or initialVariants with initial variants source`() { val user = ExperimentUser(userId = "test_user", deviceId = "0123456789") val exposureTrackingProvider = TestExposureTrackingProvider() - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = true, - source = Source.INITIAL_VARIANTS, - initialVariants = mapOf("sdk-ci-test" to initialVariant, "sdk-ci-test-local" to initialVariant) - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = true, + source = Source.INITIAL_VARIANTS, + initialVariants = mapOf("sdk-ci-test" to initialVariant, "sdk-ci-test-local" to initialVariant), + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() val allVariants = client.all() val localVariant = allVariants["sdk-ci-test-local"] @@ -1069,13 +1108,14 @@ class ExperimentClientTest { @Test fun `start - test with local and remote evaluation, calls fetchInternal`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig(), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig(), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) val spyClient = spyk(client) spyClient.start(null).get() verify(exactly = 1) { spyClient.fetchInternal(any(), any(), any(), any()) } @@ -1083,13 +1123,14 @@ class ExperimentClientTest { @Test fun `start - with local evaluation only, calls fetchInternal`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig(), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig(), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) val spyClient = spyk(client) every { spyClient.allFlags() } returns emptyMap() spyClient.start(null).get() @@ -1098,13 +1139,14 @@ class ExperimentClientTest { @Test fun `start - test with local evaluation only, fetchOnStart enabled, calls fetchInternal`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig(fetchOnStart = true), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig(fetchOnStart = true), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) val spyClient = spyk(client) every { spyClient.allFlags() } returns emptyMap() spyClient.start(null).get() @@ -1113,13 +1155,14 @@ class ExperimentClientTest { @Test fun `start - test with local and remote evaluation, fetchOnStart disabled, does not call fetchInternal`() { - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig(fetchOnStart = false), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig(fetchOnStart = false), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) val spyClient = spyk(client) spyClient.start(null).get() verify(exactly = 0) { spyClient.fetchInternal(any(), any(), any(), any()) } @@ -1130,18 +1173,19 @@ class ExperimentClientTest { val user = ExperimentUser(userId = "test_user") val exposureTrackingProvider = mockk() every { exposureTrackingProvider.track(any()) } just Runs - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - debug = true, - exposureTrackingProvider = exposureTrackingProvider, - fetchOnStart = false, - source = Source.LOCAL_STORAGE, - ), - OkHttpClient(), - mockStorage, - Experiment.executorService, - ) + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + debug = true, + exposureTrackingProvider = exposureTrackingProvider, + fetchOnStart = false, + source = Source.LOCAL_STORAGE, + ), + OkHttpClient(), + mockStorage, + Experiment.executorService, + ) client.start(user).get() var variant = client.variant("sdk-payload-ci-test") val obj = JSONObject().put("key1", "val1").put("key2", "val2") @@ -1159,21 +1203,23 @@ class ExperimentClientTest { fun `initial flags`() { val storage = MockStorage() // Flag, sdk-ci-test-local is modified to always return off - val initialFlags = """ + val initialFlags = + """ [ {"key":"sdk-ci-test-local","metadata":{"deployed":true,"evaluationMode":"local","flagType":"release","flagVersion":1},"segments":[{"metadata":{"segmentName":"All Other Users"},"variant":"off"}],"variants":{"off":{"key":"off","metadata":{"default":true}},"on":{"key":"on","value":"on"}}}, {"key":"sdk-ci-test-local-2","metadata":{"deployed":true,"evaluationMode":"local","flagType":"release","flagVersion":1},"segments":[{"metadata":{"segmentName":"All Other Users"},"variant":"on"}],"variants":{"off":{"key":"off","metadata":{"default":true}},"on":{"key":"on","value":"on"}}} ] - """.trimIndent() - val client = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - initialFlags = initialFlags, - ), - OkHttpClient(), - storage, - Experiment.executorService, - ) + """.trimIndent() + val client = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + initialFlags = initialFlags, + ), + OkHttpClient(), + storage, + Experiment.executorService, + ) val user = ExperimentUser(userId = "user_id", deviceId = "device_id") client.setUser(user) var variant = client.variant("sdk-ci-test-local") @@ -1187,15 +1233,16 @@ class ExperimentClientTest { variant2 = client.variant("sdk-ci-test-local-2") Assert.assertEquals("on", variant2.key) // Initialize a second client with the same storage to simulate an app restart - val client2 = DefaultExperimentClient( - API_KEY, - ExperimentConfig( - initialFlags = initialFlags, - ), - OkHttpClient(), - storage, - Experiment.executorService, - ) + val client2 = + DefaultExperimentClient( + API_KEY, + ExperimentConfig( + initialFlags = initialFlags, + ), + OkHttpClient(), + storage, + Experiment.executorService, + ) // Storage flag should take precedent over initial flag variant = client.variant("sdk-ci-test-local") Assert.assertEquals("on", variant.key) @@ -1206,26 +1253,28 @@ class ExperimentClientTest { @Test fun `fetch retry with different response codes`() { // Response code, error message, and whether retry should be called - val testData = listOf( - Triple(300, "Fetch Exception 300", 1), - Triple(400, "Fetch Exception 400", 0), - Triple(429, "Fetch Exception 429", 1), - Triple(500, "Fetch Exception 500", 1), - Triple(0, "Other Exception", 1) - ) + val testData = + listOf( + Triple(300, "Fetch Exception 300", 1), + Triple(400, "Fetch Exception 400", 0), + Triple(429, "Fetch Exception 429", 1), + Triple(500, "Fetch Exception 500", 1), + Triple(0, "Other Exception", 1), + ) testData.forEach { (responseCode, errorMessage, retryCalled) -> val storage = MockStorage() - val client = spyk( - DefaultExperimentClient( - API_KEY, - ExperimentConfig(retryFetchOnFailure = true), - OkHttpClient(), - storage, - Experiment.executorService, - ), - recordPrivateCalls = true - ) + val client = + spyk( + DefaultExperimentClient( + API_KEY, + ExperimentConfig(retryFetchOnFailure = true), + OkHttpClient(), + storage, + Experiment.executorService, + ), + recordPrivateCalls = true, + ) // Mock the private method to throw FetchException or other exceptions every { client["doFetch"](any(), any(), any()) } answers { val future = CompletableFuture>() diff --git a/sdk/src/test/java/com/amplitude/experiment/ExperimentUserTest.kt b/sdk/src/test/java/com/amplitude/experiment/ExperimentUserTest.kt index e0a7783..f7b0778 100644 --- a/sdk/src/test/java/com/amplitude/experiment/ExperimentUserTest.kt +++ b/sdk/src/test/java/com/amplitude/experiment/ExperimentUserTest.kt @@ -12,33 +12,33 @@ import org.junit.Assert import org.junit.Test class ExperimentUserTest { - init { Logger.implementation = SystemLogger(true) } @Test fun `user to json`() { - val user = builder() - .userId("user_id") - .deviceId("deviceId") - .country("country") - .city("city") - .region("region") - .dma("dma") - .language("language") - .platform("platform") - .version("version") - .os("os") - .library("library") - .deviceBrand("deviceBrand") - .deviceManufacturer("deviceManufacturer") - .deviceModel("deviceModel") - .carrier("carrier") - .userProperty("userPropertyKey", "value") - .groups(mapOf("groupType" to setOf("groupName"))) - .groupProperties(mapOf("groupType" to mapOf("groupName" to mapOf("k" to "v")))) - .build() + val user = + builder() + .userId("user_id") + .deviceId("deviceId") + .country("country") + .city("city") + .region("region") + .dma("dma") + .language("language") + .platform("platform") + .version("version") + .os("os") + .library("library") + .deviceBrand("deviceBrand") + .deviceManufacturer("deviceManufacturer") + .deviceModel("deviceModel") + .carrier("carrier") + .userProperty("userPropertyKey", "value") + .groups(mapOf("groupType" to setOf("groupName"))) + .groupProperties(mapOf("groupType" to mapOf("groupName" to mapOf("k" to "v")))) + .build() // Ordering matters here, based on toJson() extension function val expected = JSONObject() @@ -64,7 +64,7 @@ class ExperimentUserTest { "groups", JSONObject().apply { put("groupType", JSONArray().apply { put("groupName") }) - } + }, ) expected.put( "group_properties", @@ -76,140 +76,156 @@ class ExperimentUserTest { "groupName", JSONObject().apply { put("k", "v") - } + }, ) - } + }, ) - } + }, ) Assert.assertEquals(expected.toString(), user.toJson()) } @Test fun `user merge`() { - val user1 = builder() - .userId("user_id") - .deviceId("device_id") - .country("country") - .city("city") - .region("region") - .dma("dma") - .language("language") - .platform("platform") - .os("os") - .library("library") - .deviceBrand("deviceBrand") - .deviceManufacturer("deviceManufacturer") - .deviceModel("deviceModel") - .carrier("carrier") - .userProperty("userPropertyKey", "value") - .groups( - mapOf( - "gt2" to setOf("gn2"), - "gt3" to setOf("gn3"), - "gt4" to setOf("gn4"), + val user1 = + builder() + .userId("user_id") + .deviceId("device_id") + .country("country") + .city("city") + .region("region") + .dma("dma") + .language("language") + .platform("platform") + .os("os") + .library("library") + .deviceBrand("deviceBrand") + .deviceManufacturer("deviceManufacturer") + .deviceModel("deviceModel") + .carrier("carrier") + .userProperty("userPropertyKey", "value") + .groups( + mapOf( + "gt2" to setOf("gn2"), + "gt3" to setOf("gn3"), + "gt4" to setOf("gn4"), + ), ) - ) - .groupProperties( - mapOf( - "gt1" to mapOf( - "gn1" to mapOf( - "gp1" to "v2", - "gp3" to "v1" - ), - "gn3" to mapOf( - "gp1" to "v1", - ), + .groupProperties( + mapOf( + "gt1" to + mapOf( + "gn1" to + mapOf( + "gp1" to "v2", + "gp3" to "v1", + ), + "gn3" to + mapOf( + "gp1" to "v1", + ), + ), ), ) - ) - .build() + .build() - val user2 = builder() - .country("newCountry") - .version("newVersion") - .userProperty("userPropertyKey2", "value2") - .userProperty("userPropertyKey", "value2") - .groups( - mapOf( - "gt1" to setOf("gn1"), - "gt2" to setOf("difference"), - "gt4" to setOf("gn4"), - ) - ) - .groupProperties( - mapOf( - "gt1" to mapOf( - "gn1" to mapOf( - "gp1" to "v1", - "gp2" to "v1" - ), - "gn2" to mapOf( - "gp1" to "v1", - ), + val user2 = + builder() + .country("newCountry") + .version("newVersion") + .userProperty("userPropertyKey2", "value2") + .userProperty("userPropertyKey", "value2") + .groups( + mapOf( + "gt1" to setOf("gn1"), + "gt2" to setOf("difference"), + "gt4" to setOf("gn4"), ), - "gt2" to mapOf( - "gn1" to mapOf( - "gp1" to "v1", - ), + ) + .groupProperties( + mapOf( + "gt1" to + mapOf( + "gn1" to + mapOf( + "gp1" to "v1", + "gp2" to "v1", + ), + "gn2" to + mapOf( + "gp1" to "v1", + ), + ), + "gt2" to + mapOf( + "gn1" to + mapOf( + "gp1" to "v1", + ), + ), ), ) - ) - .build() + .build() val user = user2.merge(user1) - val expected = builder() - .userId("user_id") - .deviceId("device_id") - .country("newCountry") // overwrites value - .version("newVersion") // overwrites null - .language("language") - .city("city") - .region("region") - .dma("dma") - .language("language") - .platform("platform") - .os("os") - .library("library") - .deviceBrand("deviceBrand") - .deviceManufacturer("deviceManufacturer") - .deviceModel("deviceModel") - .carrier("carrier") - .userProperty("userPropertyKey", "value2") - .userProperty("userPropertyKey2", "value2") - .groups( - mapOf( - "gt1" to setOf("gn1"), - "gt2" to setOf("difference"), - "gt3" to setOf("gn3"), - "gt4" to setOf("gn4"), - ) - ) - .groupProperties( - mapOf( - "gt1" to mapOf( - "gn1" to mapOf( - "gp1" to "v1", - "gp2" to "v1", - "gp3" to "v1" - ), - "gn2" to mapOf( - "gp1" to "v1", - ), - "gn3" to mapOf( - "gp1" to "v1", - ), + val expected = + builder() + .userId("user_id") + .deviceId("device_id") + .country("newCountry") // overwrites value + .version("newVersion") // overwrites null + .language("language") + .city("city") + .region("region") + .dma("dma") + .language("language") + .platform("platform") + .os("os") + .library("library") + .deviceBrand("deviceBrand") + .deviceManufacturer("deviceManufacturer") + .deviceModel("deviceModel") + .carrier("carrier") + .userProperty("userPropertyKey", "value2") + .userProperty("userPropertyKey2", "value2") + .groups( + mapOf( + "gt1" to setOf("gn1"), + "gt2" to setOf("difference"), + "gt3" to setOf("gn3"), + "gt4" to setOf("gn4"), ), - "gt2" to mapOf( - "gn1" to mapOf( - "gp1" to "v1", - ), + ) + .groupProperties( + mapOf( + "gt1" to + mapOf( + "gn1" to + mapOf( + "gp1" to "v1", + "gp2" to "v1", + "gp3" to "v1", + ), + "gn2" to + mapOf( + "gp1" to "v1", + ), + "gn3" to + mapOf( + "gp1" to "v1", + ), + ), + "gt2" to + mapOf( + "gn1" to + mapOf( + "gp1" to "v1", + ), + ), ), ) - ) - - .build() + .build() Assert.assertEquals(expected, user) @@ -219,36 +235,43 @@ class ExperimentUserTest { @Test fun `test group and group property builder`() { - val user = builder().apply { - group("gt1", "gn1") - group("gt2", "gn2") - groupProperty("gt1", "gn1", "k", "v") - groupProperty("gt1", "gn1", "k2", "v2") - groupProperty("gt1", "gn2", "k", "v") - groupProperty("gt2", "gn1", "k", "v") - }.build() + val user = + builder().apply { + group("gt1", "gn1") + group("gt2", "gn2") + groupProperty("gt1", "gn1", "k", "v") + groupProperty("gt1", "gn1", "k2", "v2") + groupProperty("gt1", "gn2", "k", "v") + groupProperty("gt2", "gn1", "k", "v") + }.build() - val expected = builder().apply { - groups(mapOf("gt1" to setOf("gn1"), "gt2" to setOf("gn2"))) - groupProperties( - mapOf( - "gt1" to mapOf( - "gn1" to mapOf( - "k" to "v", - "k2" to "v2", - ), - "gn2" to mapOf( - "k" to "v" - ) + val expected = + builder().apply { + groups(mapOf("gt1" to setOf("gn1"), "gt2" to setOf("gn2"))) + groupProperties( + mapOf( + "gt1" to + mapOf( + "gn1" to + mapOf( + "k" to "v", + "k2" to "v2", + ), + "gn2" to + mapOf( + "k" to "v", + ), + ), + "gt2" to + mapOf( + "gn1" to + mapOf( + "k" to "v", + ), + ), ), - "gt2" to mapOf( - "gn1" to mapOf( - "k" to "v" - ) - ) ) - ) - }.build() + }.build() Assert.assertEquals(expected, user) } @@ -277,113 +300,123 @@ class ExperimentUserTest { @Test fun `toEvaluationContext - test user groups, undefined group properties, moved under context groups`() { - val user: ExperimentUser = ExperimentUser.Builder() - .userId("user_id") - .group("gt1", "gn1") - .build() + val user: ExperimentUser = + ExperimentUser.Builder() + .userId("user_id") + .group("gt1", "gn1") + .build() val context = user.toEvaluationContext() Assert.assertEquals( mapOf( "user" to mapOf("user_id" to "user_id"), - "groups" to mapOf("gt1" to mapOf("group_name" to "gn1")) + "groups" to mapOf("gt1" to mapOf("group_name" to "gn1")), ), - context.toMap() + context.toMap(), ) } @Test fun `toEvaluationContext - test user groups, empty group properties, moved under context groups`() { - val user: ExperimentUser = ExperimentUser.Builder() - .userId("user_id") - .group("gt1", "gn1") - .groupProperties(emptyMap()) - .build() + val user: ExperimentUser = + ExperimentUser.Builder() + .userId("user_id") + .group("gt1", "gn1") + .groupProperties(emptyMap()) + .build() val context = user.toEvaluationContext() Assert.assertEquals( mapOf( "user" to mapOf("user_id" to "user_id"), - "groups" to mapOf("gt1" to mapOf("group_name" to "gn1")) + "groups" to mapOf("gt1" to mapOf("group_name" to "gn1")), ), - context.toMap() + context.toMap(), ) } @Test fun `toEvaluationContext - test user groups, group properties empty group type object, moved under context groups`() { - val user: ExperimentUser = ExperimentUser.Builder() - .userId("user_id") - .group("gt1", "gn1") - .groupProperties(mapOf("gt1" to emptyMap())) - .build() + val user: ExperimentUser = + ExperimentUser.Builder() + .userId("user_id") + .group("gt1", "gn1") + .groupProperties(mapOf("gt1" to emptyMap())) + .build() val context = user.toEvaluationContext() Assert.assertEquals( mapOf( "user" to mapOf("user_id" to "user_id"), - "groups" to mapOf("gt1" to mapOf("group_name" to "gn1")) + "groups" to mapOf("gt1" to mapOf("group_name" to "gn1")), ), - context.toMap() + context.toMap(), ) } @Test fun `toEvaluationContext - test user groups, group properties empty group name object, moved under context groups`() { - val user: ExperimentUser = ExperimentUser.Builder() - .userId("user_id") - .group("gt1", "gn1") - .groupProperties(mapOf("gt1" to mapOf("gn1" to emptyMap()))) - .build() + val user: ExperimentUser = + ExperimentUser.Builder() + .userId("user_id") + .group("gt1", "gn1") + .groupProperties(mapOf("gt1" to mapOf("gn1" to emptyMap()))) + .build() val context = user.toEvaluationContext() Assert.assertEquals( mapOf( "user" to mapOf("user_id" to "user_id"), - "groups" to mapOf("gt1" to mapOf("group_name" to "gn1")) + "groups" to mapOf("gt1" to mapOf("group_name" to "gn1")), ), - context.toMap() + context.toMap(), ) } @Test fun `toEvaluationContext - test user groups, with group properties, moved under context groups`() { - val user: ExperimentUser = ExperimentUser.Builder() - .userId("user_id") - .group("gt1", "gn1") - .groupProperty("gt1", "gn1", "gp1", "gp1") - .build() + val user: ExperimentUser = + ExperimentUser.Builder() + .userId("user_id") + .group("gt1", "gn1") + .groupProperty("gt1", "gn1", "gp1", "gp1") + .build() val context = user.toEvaluationContext() Assert.assertEquals( mapOf( "user" to mapOf("user_id" to "user_id"), - "groups" to mapOf( - "gt1" to mapOf( - "group_name" to "gn1", - "group_properties" to mapOf("gp1" to "gp1") - ) - ) + "groups" to + mapOf( + "gt1" to + mapOf( + "group_name" to "gn1", + "group_properties" to mapOf("gp1" to "gp1"), + ), + ), ), - context.toMap() + context.toMap(), ) } @Test fun `toEvaluationContext - test user groups and group properties, with multiple group names, takes first`() { - val user: ExperimentUser = ExperimentUser.Builder() - .userId("user_id") - .groups(mapOf("gt1" to setOf("gn1", "gn2"))) - .groupProperty("gt1", "gn1", "gp1", "gp1") - .groupProperty("gt1", "gn2", "gp2", "gp2") - .build() + val user: ExperimentUser = + ExperimentUser.Builder() + .userId("user_id") + .groups(mapOf("gt1" to setOf("gn1", "gn2"))) + .groupProperty("gt1", "gn1", "gp1", "gp1") + .groupProperty("gt1", "gn2", "gp2", "gp2") + .build() val context = user.toEvaluationContext() Assert.assertEquals( mapOf( "user" to mapOf("user_id" to "user_id"), - "groups" to mapOf( - "gt1" to mapOf( - "group_name" to "gn1", - "group_properties" to mapOf("gp1" to "gp1") - ) - ) + "groups" to + mapOf( + "gt1" to + mapOf( + "group_name" to "gn1", + "group_properties" to mapOf("gp1" to "gp1"), + ), + ), ), - context.toMap() + context.toMap(), ) } } diff --git a/sdk/src/test/java/com/amplitude/experiment/StorageTest.kt b/sdk/src/test/java/com/amplitude/experiment/StorageTest.kt index c019ae6..ec3d7fe 100644 --- a/sdk/src/test/java/com/amplitude/experiment/StorageTest.kt +++ b/sdk/src/test/java/com/amplitude/experiment/StorageTest.kt @@ -6,125 +6,130 @@ import org.junit.Assert import org.junit.Test class StorageTest { - @Test fun `v1 variant transformation`() { val storedVariant = JSONObject(mapOf("value" to "on")).toString() Assert.assertEquals( Variant(key = "on", value = "on"), - decodeVariantFromStorage(storedVariant) + decodeVariantFromStorage(storedVariant), ) } @Test fun `v1 variant transformation with payload`() { - val storedVariant = JSONObject( - mapOf( - "value" to "on", - "payload" to mapOf("k" to "v") - ) - ).toString() + val storedVariant = + JSONObject( + mapOf( + "value" to "on", + "payload" to mapOf("k" to "v"), + ), + ).toString() Assert.assertEquals( Variant(key = "on", value = "on", payload = JSONObject(mapOf("k" to "v"))).toString(), - decodeVariantFromStorage(storedVariant).toString() + decodeVariantFromStorage(storedVariant).toString(), ) } @Test fun `v1 variant transformation with payload and experiment key`() { - val storedVariant = JSONObject( - mapOf( - "value" to "on", - "payload" to mapOf("k" to "v"), - "expKey" to "exp-1" - ) - ).toString() + val storedVariant = + JSONObject( + mapOf( + "value" to "on", + "payload" to mapOf("k" to "v"), + "expKey" to "exp-1", + ), + ).toString() Assert.assertEquals( Variant( key = "on", value = "on", payload = JSONObject(mapOf("k" to "v")), expKey = "exp-1", - metadata = mapOf("experimentKey" to "exp-1") + metadata = mapOf("experimentKey" to "exp-1"), ).toString(), - decodeVariantFromStorage(storedVariant).toString() + decodeVariantFromStorage(storedVariant).toString(), ) } @Test fun `v2 variant transformation`() { - val storedVariant = JSONObject( - mapOf( - "key" to "treatment", - "value" to "on" - ) - ).toString() + val storedVariant = + JSONObject( + mapOf( + "key" to "treatment", + "value" to "on", + ), + ).toString() Assert.assertEquals( Variant(key = "treatment", value = "on"), - decodeVariantFromStorage(storedVariant) + decodeVariantFromStorage(storedVariant), ) } @Test fun `v2 variant transformation with payload`() { - val storedVariant = JSONObject( - mapOf( - "key" to "treatment", - "value" to "on", - "payload" to mapOf("k" to "v") - ) - ).toString() + val storedVariant = + JSONObject( + mapOf( + "key" to "treatment", + "value" to "on", + "payload" to mapOf("k" to "v"), + ), + ).toString() Assert.assertEquals( Variant( key = "treatment", value = "on", - payload = JSONObject(mapOf("k" to "v")) + payload = JSONObject(mapOf("k" to "v")), ).toString(), - decodeVariantFromStorage(storedVariant).toString() + decodeVariantFromStorage(storedVariant).toString(), ) } @Test fun `v2 variant transformation with payload and experiment key`() { - val storedVariant = JSONObject( - mapOf( - "key" to "treatment", - "value" to "on", - "payload" to mapOf("k" to "v"), - "expKey" to "exp-1" - ) - ).toString() + val storedVariant = + JSONObject( + mapOf( + "key" to "treatment", + "value" to "on", + "payload" to mapOf("k" to "v"), + "expKey" to "exp-1", + ), + ).toString() Assert.assertEquals( Variant( key = "treatment", value = "on", payload = JSONObject(mapOf("k" to "v")), expKey = "exp-1", - metadata = mapOf("experimentKey" to "exp-1") + metadata = mapOf("experimentKey" to "exp-1"), ).toString(), - decodeVariantFromStorage(storedVariant).toString() + decodeVariantFromStorage(storedVariant).toString(), ) } @Test fun `v2 variant transformation with payload and experiment key metadata`() { - val storedVariant = JSONObject( - mapOf( - "key" to "treatment", - "value" to "on", - "payload" to mapOf("k" to "v"), - "metadata" to mapOf("experimentKey" to "exp-1") - ) - ).toString() + val storedVariant = + JSONObject( + mapOf( + "key" to "treatment", + "value" to "on", + "payload" to mapOf("k" to "v"), + "metadata" to mapOf("experimentKey" to "exp-1"), + ), + ).toString() Assert.assertEquals( Variant( key = "treatment", value = "on", payload = JSONObject(mapOf("k" to "v")), expKey = "exp-1", - metadata = mapOf("experimentKey" to "exp-1") + metadata = mapOf("experimentKey" to "exp-1"), ).toString(), - decodeVariantFromStorage(storedVariant).toString() + decodeVariantFromStorage(storedVariant).toString(), ) } } diff --git a/sdk/src/test/java/com/amplitude/experiment/UserSessionExposureTrackerTest.kt b/sdk/src/test/java/com/amplitude/experiment/UserSessionExposureTrackerTest.kt index c82e964..7e1c8cd 100644 --- a/sdk/src/test/java/com/amplitude/experiment/UserSessionExposureTrackerTest.kt +++ b/sdk/src/test/java/com/amplitude/experiment/UserSessionExposureTrackerTest.kt @@ -6,7 +6,6 @@ import org.junit.Assert import org.junit.Test class UserSessionExposureTrackerTest { - @Test fun `test track called once per flag`() { val provider = TestExposureTrackingProvider() diff --git a/sdk/src/test/java/com/amplitude/experiment/VariantTest.kt b/sdk/src/test/java/com/amplitude/experiment/VariantTest.kt index 737a58c..062495b 100644 --- a/sdk/src/test/java/com/amplitude/experiment/VariantTest.kt +++ b/sdk/src/test/java/com/amplitude/experiment/VariantTest.kt @@ -9,7 +9,6 @@ import org.junit.Assert import org.junit.Test class VariantTest { - init { Logger.implementation = SystemLogger(true) } diff --git a/sdk/src/test/java/com/amplitude/experiment/util/MockStorage.kt b/sdk/src/test/java/com/amplitude/experiment/util/MockStorage.kt index 1ffea47..4bbb8f9 100644 --- a/sdk/src/test/java/com/amplitude/experiment/util/MockStorage.kt +++ b/sdk/src/test/java/com/amplitude/experiment/util/MockStorage.kt @@ -3,18 +3,23 @@ package com.amplitude.experiment.util import com.amplitude.experiment.storage.Storage internal class MockStorage() : Storage { - private val store: MutableMap> = mutableMapOf() - override fun get(key: String): Map = synchronized(this) { - return store[key] ?: mutableMapOf() - } + override fun get(key: String): Map = + synchronized(this) { + return store[key] ?: mutableMapOf() + } - override fun put(key: String, value: Map): Unit = synchronized(this) { - store[key] = value - } + override fun put( + key: String, + value: Map, + ): Unit = + synchronized(this) { + store[key] = value + } - override fun delete(key: String): Unit = synchronized(this) { - store.remove(key) - } + override fun delete(key: String): Unit = + synchronized(this) { + store.remove(key) + } } diff --git a/sdk/src/test/java/com/amplitude/experiment/util/SystemLogger.kt b/sdk/src/test/java/com/amplitude/experiment/util/SystemLogger.kt index 9660fc0..bfa7965 100644 --- a/sdk/src/test/java/com/amplitude/experiment/util/SystemLogger.kt +++ b/sdk/src/test/java/com/amplitude/experiment/util/SystemLogger.kt @@ -12,7 +12,6 @@ internal fun timestamp(): String { // For Testing internal class SystemLogger(private val debug: Boolean) : ILogger { - override fun v(msg: String) { if (debug) { println("[${timestamp()}] VERBOSE: $msg") @@ -31,7 +30,10 @@ internal class SystemLogger(private val debug: Boolean) : ILogger { } } - override fun w(msg: String, e: Throwable?) { + override fun w( + msg: String, + e: Throwable?, + ) { if (e == null) { println("[${timestamp()}] WARN: $msg") } else { @@ -39,7 +41,10 @@ internal class SystemLogger(private val debug: Boolean) : ILogger { } } - override fun e(msg: String, e: Throwable?) { + override fun e( + msg: String, + e: Throwable?, + ) { if (e == null) { println("[${timestamp()}] ERROR: $msg") } else { diff --git a/sdk/src/test/java/com/amplitude/experiment/util/TestExposureTrackingProvider.kt b/sdk/src/test/java/com/amplitude/experiment/util/TestExposureTrackingProvider.kt index ddb7c60..5abda6f 100644 --- a/sdk/src/test/java/com/amplitude/experiment/util/TestExposureTrackingProvider.kt +++ b/sdk/src/test/java/com/amplitude/experiment/util/TestExposureTrackingProvider.kt @@ -6,6 +6,7 @@ import com.amplitude.experiment.ExposureTrackingProvider class TestExposureTrackingProvider : ExposureTrackingProvider { var trackCount = 0 var lastExposure: Exposure? = null + override fun track(exposure: Exposure) { trackCount++ lastExposure = exposure From 91939d2e88fefadaf0535862281a5e666152c468 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 16 Aug 2024 13:13:50 -0700 Subject: [PATCH 5/6] chore: use java 17 in build actions --- .github/workflows/build.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6dc44b..ad02c8d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,10 +13,10 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: 'zulu' - name: Cache Gradle Dependencies diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7f56f8a..16798b6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,10 +12,10 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: 'zulu' - name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 42a8912..aefad03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,10 +29,10 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: 'zulu' - name: Set up Node diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b11794..d475471 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: 'zulu' - name: Cache Gradle Dependencies From 84c90deba955c89d1aff6ecae8135c6b05bc05e6 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 16 Aug 2024 13:20:57 -0700 Subject: [PATCH 6/6] fix: target sdk 33 --- analytics-connector/build.gradle | 2 +- sdk/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/analytics-connector/build.gradle b/analytics-connector/build.gradle index 4986e6c..a6fada6 100644 --- a/analytics-connector/build.gradle +++ b/analytics-connector/build.gradle @@ -18,7 +18,7 @@ android { defaultConfig { minSdkVersion 14 - targetSdkVersion 30 + targetSdkVersion 33 versionName PUBLISH_VERSION } diff --git a/sdk/build.gradle b/sdk/build.gradle index da0c70d..24a08df 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -20,7 +20,7 @@ android { defaultConfig { minSdkVersion 14 - targetSdkVersion 30 + targetSdkVersion 33 versionName PUBLISH_VERSION buildConfigField "String", "VERSION_NAME", "\"$PUBLISH_VERSION\"" consumerProguardFiles "consumer-rules.pro"