From 60e6098008a659a2f6e5bd01e19afc24259ca89c Mon Sep 17 00:00:00 2001 From: Isabel Martin Date: Fri, 14 May 2021 10:09:15 -0700 Subject: [PATCH] NT-1916: Feature flag comment threading (#1243) --- .../kickstarter/libs/ExperimentsClientType.kt | 4 +- .../libs/OptimizelyExperimentsClient.kt | 6 ++ .../libs/models/OptimizelyFeature.kt | 3 +- .../kickstarter/libs/utils/ExperimentUtils.kt | 8 ++- .../mock/MockExperimentsClientType.kt | 11 ++- .../ui/activities/ProjectActivity.kt | 9 +++ .../viewmodels/ProjectViewModel.kt | 16 +++++ .../libs/utils/ExperimentUtilsTest.kt | 16 +++-- .../viewmodels/ProjectViewModelTest.kt | 71 ++++++++++++++++++- 9 files changed, 132 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/kickstarter/libs/ExperimentsClientType.kt b/app/src/main/java/com/kickstarter/libs/ExperimentsClientType.kt index 253b5a185e..f2c4a9efd5 100644 --- a/app/src/main/java/com/kickstarter/libs/ExperimentsClientType.kt +++ b/app/src/main/java/com/kickstarter/libs/ExperimentsClientType.kt @@ -12,7 +12,7 @@ import org.json.JSONObject interface ExperimentsClientType { fun ExperimentsClientType.attributes(experimentData: ExperimentData, optimizelyEnvironment: OptimizelyEnvironment): Map { - return ExperimentUtils.attributes(experimentData, appVersion(), OSVersion(), optimizelyEnvironment) + return ExperimentUtils.attributes(experimentData, appVersion(), OSVersion(), versionCode(), optimizelyEnvironment) } /** @@ -42,9 +42,11 @@ interface ExperimentsClientType { return properties } + fun versionCode(): Int fun appVersion(): String fun enabledFeatures(user: User?): List fun isFeatureEnabled(feature: OptimizelyFeature.Key, experimentData: ExperimentData): Boolean + fun isFeatureEnabled(feature: OptimizelyFeature.Key): Boolean fun optimizelyEnvironment(): OptimizelyEnvironment fun OSVersion(): String fun track(eventKey: String, experimentData: ExperimentData) diff --git a/app/src/main/java/com/kickstarter/libs/OptimizelyExperimentsClient.kt b/app/src/main/java/com/kickstarter/libs/OptimizelyExperimentsClient.kt index 9b9fd31d04..4d2a6a73b6 100644 --- a/app/src/main/java/com/kickstarter/libs/OptimizelyExperimentsClient.kt +++ b/app/src/main/java/com/kickstarter/libs/OptimizelyExperimentsClient.kt @@ -14,6 +14,8 @@ import com.optimizely.ab.android.sdk.OptimizelyManager class OptimizelyExperimentsClient(private val optimizelyManager: OptimizelyManager, private val optimizelyEnvironment: OptimizelyEnvironment) : ExperimentsClientType { override fun appVersion(): String = BuildConfig.VERSION_NAME + override fun versionCode() = BuildConfig.VERSION_CODE + override fun OSVersion(): String = Build.VERSION.RELEASE override fun track(eventKey: String, experimentData: ExperimentData) { @@ -34,6 +36,10 @@ class OptimizelyExperimentsClient(private val optimizelyManager: OptimizelyManag return optimizelyClient().isFeatureEnabled(feature.key, userId(), attributes(experimentData, this.optimizelyEnvironment)) } + override fun isFeatureEnabled(feature: OptimizelyFeature.Key): Boolean { + return optimizelyClient().isFeatureEnabled(feature.key, userId(), attributes(ExperimentData(null, null, null), this.optimizelyEnvironment)) + } + override fun variant(experiment: OptimizelyExperiment.Key, experimentData: ExperimentData): OptimizelyExperiment.Variant { val user = experimentData.user val variationString: String? = if (user?.isAdmin == true) { diff --git a/app/src/main/java/com/kickstarter/libs/models/OptimizelyFeature.kt b/app/src/main/java/com/kickstarter/libs/models/OptimizelyFeature.kt index 68234048ba..de5a91387a 100644 --- a/app/src/main/java/com/kickstarter/libs/models/OptimizelyFeature.kt +++ b/app/src/main/java/com/kickstarter/libs/models/OptimizelyFeature.kt @@ -2,6 +2,7 @@ package com.kickstarter.libs.models class OptimizelyFeature { enum class Key(val key: String) { - LIGHTS_ON("android_lights_on") + LIGHTS_ON("android_lights_on"), + COMMENT_THREADING("android_comment_threading") } } diff --git a/app/src/main/java/com/kickstarter/libs/utils/ExperimentUtils.kt b/app/src/main/java/com/kickstarter/libs/utils/ExperimentUtils.kt index 22f58f6be4..84a4551218 100644 --- a/app/src/main/java/com/kickstarter/libs/utils/ExperimentUtils.kt +++ b/app/src/main/java/com/kickstarter/libs/utils/ExperimentUtils.kt @@ -10,10 +10,16 @@ import java.util.Locale object ExperimentUtils { - fun attributes(experimentData: ExperimentData, appVersion: String, OSVersion: String, optimizelyEnvironment: OptimizelyEnvironment): Map { + /** + * Attributes we need to send alongside with the Experiment Data or Feature Flag, + * Optimizely will use this attributes for setting up audiences. + */ + fun attributes(experimentData: ExperimentData, appVersion: String, OSVersion: String, versionCode: Int, optimizelyEnvironment: OptimizelyEnvironment): Map { return mapOf( Pair("distinct_id", getInstanceId(optimizelyEnvironment)), Pair("session_app_release_version", appVersion), + Pair("session_app_release_version_number", appVersion.replace(".", "").toInt()), + Pair("session_app_release_version_code", versionCode), Pair("session_os_version", String.format("Android %s", OSVersion)), Pair("session_ref_tag", experimentData.intentRefTag?.tag()), Pair("session_referrer_credit", experimentData.cookieRefTag?.tag()), diff --git a/app/src/main/java/com/kickstarter/mock/MockExperimentsClientType.kt b/app/src/main/java/com/kickstarter/mock/MockExperimentsClientType.kt index c9a84ba887..59a13648e8 100644 --- a/app/src/main/java/com/kickstarter/mock/MockExperimentsClientType.kt +++ b/app/src/main/java/com/kickstarter/mock/MockExperimentsClientType.kt @@ -11,21 +11,26 @@ import org.json.JSONObject import rx.Observable import rx.subjects.PublishSubject -open class MockExperimentsClientType(private val variant: OptimizelyExperiment.Variant, private val optimizelyEnvironment: OptimizelyEnvironment) : ExperimentsClientType { - constructor(variant: OptimizelyExperiment.Variant) : this(variant, OptimizelyEnvironment.DEVELOPMENT) - constructor() : this(OptimizelyExperiment.Variant.CONTROL, OptimizelyEnvironment.DEVELOPMENT) +open class MockExperimentsClientType(private val variant: OptimizelyExperiment.Variant, private val optimizelyEnvironment: OptimizelyEnvironment, private val enabledFlag: Boolean) : ExperimentsClientType { + constructor(variant: OptimizelyExperiment.Variant) : this(variant, OptimizelyEnvironment.DEVELOPMENT, false) + constructor() : this(OptimizelyExperiment.Variant.CONTROL, OptimizelyEnvironment.DEVELOPMENT, false) + constructor(enabled: Boolean) : this(OptimizelyExperiment.Variant.CONTROL, OptimizelyEnvironment.DEVELOPMENT, enabled) class ExperimentsEvent internal constructor(internal val eventKey: String, internal val attributes: Map, internal val tags: Map?) private val experimentEvents: PublishSubject = PublishSubject.create() val eventKeys: Observable = this.experimentEvents.map { e -> e.eventKey } + override fun versionCode(): Int = 999999999 + override fun appVersion(): String = "9.9.9" override fun enabledFeatures(user: User?): List = emptyList() override fun isFeatureEnabled(feature: OptimizelyFeature.Key, experimentData: ExperimentData): Boolean = false + override fun isFeatureEnabled(feature: OptimizelyFeature.Key): Boolean = this.enabledFlag + override fun optimizelyEnvironment(): OptimizelyEnvironment = this.optimizelyEnvironment override fun optimizelyProperties(experimentData: ExperimentData): Map { diff --git a/app/src/main/java/com/kickstarter/ui/activities/ProjectActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/ProjectActivity.kt index e0a177c6ae..8be8dcccb9 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/ProjectActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/ProjectActivity.kt @@ -256,6 +256,11 @@ class ProjectActivity : .observeOn(AndroidSchedulers.mainThread()) .subscribe { this.startCommentsActivity(it) } + this.viewModel.outputs.startRootCommentsActivity() + .compose(bindToLifecycle()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { this.startRootCommentsActivity(it) } + this.viewModel.outputs.startCreatorBioWebViewActivity() .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) @@ -665,6 +670,10 @@ class ProjectActivity : startActivityWithTransition(intent, R.anim.slide_in_right, R.anim.fade_out_slide_out_left) } + private fun startRootCommentsActivity(projectAndData: Pair) { + // TODO: Start the new activity defined in https://kickstarter.atlassian.net/browse/NT-1920 + } + private fun startShareIntent(projectNameAndShareUrl: Pair) { val name = projectNameAndShareUrl.first val shareMessage = this.ksString.format(getString(this.projectShareCopyString), "project_title", name) diff --git a/app/src/main/java/com/kickstarter/viewmodels/ProjectViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/ProjectViewModel.kt index 3c758e893f..b7ed2f5b9d 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/ProjectViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/ProjectViewModel.kt @@ -14,6 +14,7 @@ import com.kickstarter.libs.ExperimentsClientType import com.kickstarter.libs.KSCurrency import com.kickstarter.libs.RefTag import com.kickstarter.libs.models.OptimizelyExperiment +import com.kickstarter.libs.models.OptimizelyFeature import com.kickstarter.libs.rx.transformers.Transformers.combineLatestPair import com.kickstarter.libs.rx.transformers.Transformers.errors import com.kickstarter.libs.rx.transformers.Transformers.ignoreValues @@ -219,6 +220,9 @@ interface ProjectViewModel { /** Emits when we should start [com.kickstarter.ui.activities.CommentsActivity]. */ fun startCommentsActivity(): Observable> + /** Emits when we should start [com.kickstarter.ui.activities.RootCommentsActivity]. */ + fun startRootCommentsActivity(): Observable> + /** Emits when we should start the creator bio [com.kickstarter.ui.activities.CreatorBioActivity]. */ fun startCreatorBioWebViewActivity(): Observable @@ -308,6 +312,7 @@ interface ProjectViewModel { private val showUpdatePledgeSuccess = PublishSubject.create() private val startCampaignWebViewActivity = PublishSubject.create() private val startCommentsActivity = PublishSubject.create>() + private val startRootCommentsActivity = PublishSubject.create>() private val startCreatorBioWebViewActivity = PublishSubject.create() private val startCreatorDashboardActivity = PublishSubject.create() private val startLoginToutActivity = PublishSubject.create() @@ -482,11 +487,19 @@ interface ProjectViewModel { .subscribe(this.startCreatorBioWebViewActivity) currentProject + .filter { !optimizely.isFeatureEnabled(OptimizelyFeature.Key.COMMENT_THREADING) } .compose(takeWhen(this.commentsTextViewClicked)) .compose>(combineLatestPair(projectData)) .compose(bindToLifecycle()) .subscribe(this.startCommentsActivity) + currentProject + .filter { optimizely.isFeatureEnabled(OptimizelyFeature.Key.COMMENT_THREADING) } + .compose(takeWhen(this.commentsTextViewClicked)) + .compose>(combineLatestPair(projectData)) + .compose(bindToLifecycle()) + .subscribe(this.startRootCommentsActivity) + currentProject .compose(takeWhen(this.creatorDashboardButtonClicked)) .compose(bindToLifecycle()) @@ -1069,6 +1082,9 @@ interface ProjectViewModel { @NonNull override fun startCommentsActivity(): Observable> = this.startCommentsActivity + @NonNull + override fun startRootCommentsActivity(): Observable> = this.startRootCommentsActivity + @NonNull override fun startCreatorBioWebViewActivity(): Observable = this.startCreatorBioWebViewActivity diff --git a/app/src/test/java/com/kickstarter/libs/utils/ExperimentUtilsTest.kt b/app/src/test/java/com/kickstarter/libs/utils/ExperimentUtilsTest.kt index 7df7cc3364..be3983d1a2 100644 --- a/app/src/test/java/com/kickstarter/libs/utils/ExperimentUtilsTest.kt +++ b/app/src/test/java/com/kickstarter/libs/utils/ExperimentUtilsTest.kt @@ -15,9 +15,11 @@ class ExperimentUtilsTest : KSRobolectricTestCase() { .backedProjectsCount(10) .build() val experimentData = ExperimentData(user, RefTag.discovery(), RefTag.search()) - val attributes = ExperimentUtils.attributes(experimentData, "9.9.9", "10", OptimizelyEnvironment.DEVELOPMENT) + val attributes = ExperimentUtils.attributes(experimentData, "9.9.9", "10", 99999, OptimizelyEnvironment.DEVELOPMENT) assertNotNull(attributes["distinct_id"]) assertEquals("9.9.9", attributes["session_app_release_version"]) + assertEquals(999, attributes["session_app_release_version_number"]) + assertEquals(99999, attributes["session_app_release_version_code"]) assertEquals("Android 10", attributes["session_os_version"]) assertEquals("discovery", attributes["session_ref_tag"]) assertEquals("search", attributes["session_referrer_credit"]) @@ -33,9 +35,11 @@ class ExperimentUtilsTest : KSRobolectricTestCase() { .backedProjectsCount(10) .build() val experimentData = ExperimentData(user, RefTag.discovery(), RefTag.search()) - val attributes = ExperimentUtils.attributes(experimentData, "9.9.9", "10", OptimizelyEnvironment.PRODUCTION) + val attributes = ExperimentUtils.attributes(experimentData, "9.9.9", "10", 99999, OptimizelyEnvironment.PRODUCTION) assertNull(attributes["distinct_id"]) assertEquals("9.9.9", attributes["session_app_release_version"]) + assertEquals(999, attributes["session_app_release_version_number"]) + assertEquals(99999, attributes["session_app_release_version_code"]) assertEquals("Android 10", attributes["session_os_version"]) assertEquals("discovery", attributes["session_ref_tag"]) assertEquals("search", attributes["session_referrer_credit"]) @@ -47,9 +51,11 @@ class ExperimentUtilsTest : KSRobolectricTestCase() { @Test fun attributes_loggedOutUser_notProd() { val experimentData = ExperimentData(null, RefTag.discovery(), RefTag.search()) - val attributes = ExperimentUtils.attributes(experimentData, "9.9.9", "9", OptimizelyEnvironment.DEVELOPMENT) + val attributes = ExperimentUtils.attributes(experimentData, "9.9.9", "9", 99999, OptimizelyEnvironment.DEVELOPMENT) assertNotNull(attributes["distinct_id"]) assertEquals("9.9.9", attributes["session_app_release_version"]) + assertEquals(999, attributes["session_app_release_version_number"]) + assertEquals(99999, attributes["session_app_release_version_code"]) assertEquals("Android 9", attributes["session_os_version"]) assertEquals("discovery", attributes["session_ref_tag"]) assertEquals("search", attributes["session_referrer_credit"]) @@ -61,9 +67,11 @@ class ExperimentUtilsTest : KSRobolectricTestCase() { @Test fun attributes_loggedOutUser_prod() { val experimentData = ExperimentData(null, RefTag.discovery(), RefTag.search()) - val attributes = ExperimentUtils.attributes(experimentData, "9.9.9", "9", OptimizelyEnvironment.PRODUCTION) + val attributes = ExperimentUtils.attributes(experimentData, "9.9.9", "9", 99999, OptimizelyEnvironment.PRODUCTION) assertNull(attributes["distinct_id"]) assertEquals("9.9.9", attributes["session_app_release_version"]) + assertEquals(999, attributes["session_app_release_version_number"]) + assertEquals(99999, attributes["session_app_release_version_code"]) assertEquals("Android 9", attributes["session_os_version"]) assertEquals("discovery", attributes["session_ref_tag"]) assertEquals("search", attributes["session_referrer_credit"]) diff --git a/app/src/test/java/com/kickstarter/viewmodels/ProjectViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/ProjectViewModelTest.kt index b030c7edd0..aedee122ff 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/ProjectViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/ProjectViewModelTest.kt @@ -66,6 +66,7 @@ class ProjectViewModelTest : KSRobolectricTestCase() { private val showUpdatePledgeSuccess = TestSubscriber() private val startCampaignWebViewActivity = TestSubscriber() private val startCommentsActivity = TestSubscriber>() + private val startRootCommentsActivityivity = TestSubscriber>() private val startCreatorBioWebViewActivity = TestSubscriber() private val startCreatorDashboardActivity = TestSubscriber() private val startLoginToutActivity = TestSubscriber() @@ -107,6 +108,7 @@ class ProjectViewModelTest : KSRobolectricTestCase() { this.vm.outputs.projectData().map { pD -> pD.project().isStarred }.subscribe(this.savedTest) this.vm.outputs.startCampaignWebViewActivity().subscribe(this.startCampaignWebViewActivity) this.vm.outputs.startCommentsActivity().subscribe(this.startCommentsActivity) + this.vm.outputs.startRootCommentsActivity().subscribe(this.startRootCommentsActivityivity) this.vm.outputs.startCreatorBioWebViewActivity().subscribe(this.startCreatorBioWebViewActivity) this.vm.outputs.startCreatorDashboardActivity().subscribe(this.startCreatorDashboardActivity) this.vm.outputs.startMessagesActivity().subscribe(this.startMessagesActivity) @@ -576,17 +578,82 @@ class ProjectViewModelTest : KSRobolectricTestCase() { } @Test - fun testStartCommentsActivity() { + fun testStartCommentsActivity_FeatureFlagOff() { val project = ProjectFactory.project() val projectData = ProjectDataFactory.project(project) val projectAndData = Pair.create(project, projectData) - setUpEnvironment(environment()) + setUpEnvironment( + environment().toBuilder() + .optimizely(MockExperimentsClientType(false)) + .build() + ) + + // Start the view model with a project. + this.vm.intent(Intent().putExtra(IntentKey.PROJECT, project)) + + this.vm.inputs.commentsTextViewClicked() + this.startCommentsActivity.assertValues(projectAndData) + this.startRootCommentsActivityivity.assertNoValues() + } + + @Test + fun testStartCommentsActivity_FeatureFlagOn() { + val project = ProjectFactory.project() + val projectData = ProjectDataFactory.project(project) + val projectAndData = Pair.create(project, projectData) + + setUpEnvironment( + environment().toBuilder() + .optimizely(MockExperimentsClientType(true)) + .build() + ) + + // Start the view model with a project. + this.vm.intent(Intent().putExtra(IntentKey.PROJECT, project)) + + this.vm.inputs.commentsTextViewClicked() + this.startCommentsActivity.assertNoValues() + this.startRootCommentsActivityivity.assertValues(projectAndData) + } + + @Test + fun testStartCommentsThreadedActivity_FeatureFlagOn() { + val project = ProjectFactory.project() + val projectData = ProjectDataFactory.project(project) + val projectAndData = Pair.create(project, projectData) + + setUpEnvironment( + environment().toBuilder() + .optimizely(MockExperimentsClientType(true)) + .build() + ) + + // Start the view model with a project. + this.vm.intent(Intent().putExtra(IntentKey.PROJECT, project)) + + this.vm.inputs.commentsTextViewClicked() + this.startRootCommentsActivityivity.assertValues(projectAndData) + this.startCommentsActivity.assertNoValues() + } + + @Test + fun testStartCommentsThreadedActivity_FeatureFlagOff() { + val project = ProjectFactory.project() + val projectData = ProjectDataFactory.project(project) + val projectAndData = Pair.create(project, projectData) + + setUpEnvironment( + environment().toBuilder() + .optimizely(MockExperimentsClientType(false)) + .build() + ) // Start the view model with a project. this.vm.intent(Intent().putExtra(IntentKey.PROJECT, project)) this.vm.inputs.commentsTextViewClicked() + this.startRootCommentsActivityivity.assertNoValues() this.startCommentsActivity.assertValues(projectAndData) }