Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions experimentation/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}

repositories {
google()
maven {
url 'https://a8c-libs.s3.amazonaws.com/android'
content {
includeGroup "org.wordpress"
includeGroup "org.wordpress.fluxc"
includeGroup "org.wordpress.wellsql"
}
}
}

android {
compileSdkVersion 30

defaultConfig {
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
warningsAsErrors true
warning 'GradleDependency'
}
}

dependencies {
// Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2"

//FluxC
implementation "org.wordpress:fluxc:1.36.0"

//WordPress utils
implementation "org.wordpress:utils:2.3.0"

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2"
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.assertj:assertj-core:3.19.0'
testImplementation 'org.mockito.kotlin:mockito-kotlin:2.2.11'
testImplementation 'org.mockito:mockito-inline:3.8.0'
}
5 changes: 5 additions & 0 deletions experimentation/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.automattic.android.experimentation">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.automattic.android.experimentation

import com.automattic.android.experimentation.ExPlat.RefreshStrategy.ALWAYS
import com.automattic.android.experimentation.ExPlat.RefreshStrategy.IF_STALE
import com.automattic.android.experimentation.ExPlat.RefreshStrategy.NEVER
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.wordpress.android.fluxc.model.experiments.Assignments
import org.wordpress.android.fluxc.model.experiments.Variation
import org.wordpress.android.fluxc.model.experiments.Variation.Control
import org.wordpress.android.fluxc.store.ExperimentStore
import org.wordpress.android.fluxc.store.ExperimentStore.Platform
import org.wordpress.android.fluxc.utils.AppLogWrapper
import org.wordpress.android.util.AppLog.T

class ExPlat(
private val platform: Platform,
private val experiments: Set<Experiment>,
private val experimentStore: ExperimentStore,
private val appLogWrapper: AppLogWrapper,
private val coroutineScope: CoroutineScope,
private val isDebug: Boolean
) {
private val activeVariations = mutableMapOf<String, Variation>()
private val experimentNames: List<String> by lazy { experiments.map { it.name } }

fun refreshIfNeeded() {
refresh(refreshStrategy = IF_STALE)
}

fun forceRefresh() {
refresh(refreshStrategy = ALWAYS)
}

fun clear() {
appLogWrapper.d(T.API, "ExPlat: clearing cached assignments and active variations")
activeVariations.clear()
experimentStore.clearCachedAssignments()
}

/**
* This returns the current active [Variation] for the provided [Experiment].
*
* If no active [Variation] is found, we can assume this is the first time this method is being
* called for the provided [Experiment] during the current session. In this case, the [Variation]
* is returned from the cached [Assignments] and then set as active. If the cached [Assignments]
* is stale and [shouldRefreshIfStale] is `true`, then new [Assignments] are fetched and their
* variations are going to be returned by this method on the next session.
*
* If the provided [Experiment] was not included in [experiments], then [Control] is returned.
* If [isDebug] is `true`, an [IllegalArgumentException] is thrown instead.
*/
internal fun getVariation(experimentName: String, shouldRefreshIfStale: Boolean): Variation {
if (!experimentNames.contains(experimentName)) {
val message = "ExPlat: experiment not found: \"${experimentName}\"! " +
"Make sure to include it in the set provided via constructor."
appLogWrapper.e(T.API, message)
if (isDebug) throw IllegalArgumentException(message) else return Control
}
return activeVariations.getOrPut(experimentName) {
getAssignments(if (shouldRefreshIfStale) IF_STALE else NEVER)
.getVariationForExperiment(experimentName)
}
}

private fun refresh(refreshStrategy: RefreshStrategy) {
if (experimentNames.isNotEmpty()) {
getAssignments(refreshStrategy)
}
}

private fun getAssignments(refreshStrategy: RefreshStrategy): Assignments {
val cachedAssignments = experimentStore.getCachedAssignments() ?: Assignments()
if (refreshStrategy == ALWAYS || (refreshStrategy == IF_STALE && cachedAssignments.isStale())) {
coroutineScope.launch { fetchAssignments() }
}
return cachedAssignments
}

private suspend fun fetchAssignments() = experimentStore.fetchAssignments(platform, experimentNames).also {
if (it.isError) {
appLogWrapper.d(T.API, "ExPlat: fetching assignments failed with result: ${it.error}")
} else {
appLogWrapper.d(T.API, "ExPlat: fetching assignments successful with result: ${it.assignments}")
}
}

private enum class RefreshStrategy { ALWAYS, IF_STALE, NEVER }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.automattic.android.experimentation

import org.wordpress.android.fluxc.model.experiments.Variation

open class Experiment(
val name: String,
private val exPlat: ExPlat
) {
@Suppress("unused")
@JvmOverloads
fun getVariation(shouldRefreshIfStale: Boolean = false): Variation {
return exPlat.getVariation(
experimentName = name,
shouldRefreshIfStale = shouldRefreshIfStale
)
}
}
Loading