Skip to content
Permalink
Browse files

Issue #456: Fretboard: Allow filtering by release channel

Closes #456: Fretboard: Allow filtering by release channel
  • Loading branch information...
fercarcedo authored and pocmo committed Jul 20, 2018
1 parent 7ae0eef commit 43b3938ee9386a33c34f658017f5ae289d2a3e97
@@ -25,7 +25,8 @@ data class Experiment(
val version: String? = null,
val manufacturer: String? = null,
val device: String? = null,
val country: String? = null
val country: String? = null,
val releaseChannel: String? = null
)

data class Bucket(
@@ -5,12 +5,10 @@
package mozilla.components.service.fretboard

import android.content.Context
import android.os.Build
import android.text.TextUtils
import java.util.Locale
import java.util.zip.CRC32

internal class ExperimentEvaluator(private val regionProvider: RegionProvider? = null) {
internal class ExperimentEvaluator(private val valuesProvider: ValuesProvider = ValuesProvider()) {
fun evaluate(
context: Context,
experimentDescriptor: ExperimentDescriptor,
@@ -30,19 +28,23 @@ internal class ExperimentEvaluator(private val regionProvider: RegionProvider? =

private fun matches(context: Context, experiment: Experiment): Boolean {
if (experiment.match != null) {
val region = regionProvider?.getRegion()
val region = valuesProvider.getRegion(context)
val matchesRegion = !(region != null &&
experiment.match.regions != null &&
experiment.match.regions.isNotEmpty() &&
experiment.match.regions.none { it == region })
val appVersion = context.packageManager.getPackageInfo(context.packageName, 0).versionName
val releaseChannel = valuesProvider.getReleaseChannel(context)
val matchesReleaseChannel = releaseChannel == null ||
experiment.match.releaseChannel == null ||
releaseChannel == experiment.match.releaseChannel
return matchesRegion &&
matchesExperiment(experiment.match.appId, context.packageName) &&
matchesExperiment(experiment.match.language, Locale.getDefault().isO3Language) &&
matchesExperiment(experiment.match.country, Locale.getDefault().isO3Country) &&
matchesExperiment(experiment.match.version, appVersion) &&
matchesExperiment(experiment.match.manufacturer, Build.MANUFACTURER) &&
matchesExperiment(experiment.match.device, Build.DEVICE)
matchesReleaseChannel &&
matchesExperiment(experiment.match.appId, valuesProvider.getAppId(context)) &&
matchesExperiment(experiment.match.language, valuesProvider.getLanguage(context)) &&
matchesExperiment(experiment.match.country, valuesProvider.getCountry(context)) &&
matchesExperiment(experiment.match.version, valuesProvider.getVersion(context)) &&
matchesExperiment(experiment.match.manufacturer, valuesProvider.getManufacturer(context)) &&
matchesExperiment(experiment.match.device, valuesProvider.getDevice(context))
}
return true
}
@@ -15,11 +15,11 @@ import android.content.Context
class Fretboard(
private val source: ExperimentSource,
private val storage: ExperimentStorage,
regionProvider: RegionProvider? = null
valuesProvider: ValuesProvider = ValuesProvider()
) {
private var experimentsResult: ExperimentsSnapshot = ExperimentsSnapshot(listOf(), null)
private var experimentsLoaded: Boolean = false
private val evaluator = ExperimentEvaluator(regionProvider)
private val evaluator = ExperimentEvaluator(valuesProvider)

/**
* Loads experiments from local storage
@@ -29,7 +29,8 @@ class JSONExperimentParser {
matchObject.tryGetString(VERSION_KEY),
matchObject.tryGetString(MANUFACTURER_KEY),
matchObject.tryGetString(DEVICE_KEY),
matchObject.tryGetString(COUNTRY_KEY))
matchObject.tryGetString(COUNTRY_KEY),
matchObject.tryGetString(RELEASE_CHANNEL_KEY))
} else null
val bucket = if (bucketsObject != null) {
Experiment.Bucket(bucketsObject.tryGetInt(MAX_KEY), bucketsObject.tryGetInt(MIN_KEY))
@@ -53,23 +54,33 @@ class JSONExperimentParser {
*/
fun toJson(experiment: Experiment): JSONObject {
val jsonObject = JSONObject()
jsonObject.putIfNotNull(NAME_KEY, experiment.name)
val matchObject = JSONObject()
matchObject.putIfNotNull(LANG_KEY, experiment.match?.language)
matchObject.putIfNotNull(APP_ID_KEY, experiment.match?.appId)
matchObject.putIfNotNull(REGIONS_KEY, experiment.match?.regions?.toJsonArray())
jsonObject.put(MATCH_KEY, matchObject)
val matchObject = matchersToJson(experiment)
val bucketsObject = JSONObject()
bucketsObject.putIfNotNull(MAX_KEY, experiment.bucket?.max)
bucketsObject.putIfNotNull(MIN_KEY, experiment.bucket?.min)
bucketsObject.putIfNotNull(MAX_KEY, experiment.bucket?.max?.toString())
bucketsObject.putIfNotNull(MIN_KEY, experiment.bucket?.min?.toString())
jsonObject.put(BUCKETS_KEY, bucketsObject)
jsonObject.putIfNotNull(DESCRIPTION_KEY, experiment.description)
jsonObject.put(ID_KEY, experiment.id)
jsonObject.putIfNotNull(LAST_MODIFIED_KEY, experiment.lastModified)
jsonObject.put(MATCH_KEY, matchObject)
jsonObject.putIfNotNull(NAME_KEY, experiment.name)
jsonObject.putIfNotNull(PAYLOAD_KEY, payloadToJson(experiment.payload))
return jsonObject
}

private fun matchersToJson(experiment: Experiment): JSONObject {
val matchObject = JSONObject()
matchObject.putIfNotNull(APP_ID_KEY, experiment.match?.appId)
matchObject.putIfNotNull(COUNTRY_KEY, experiment.match?.country)
matchObject.putIfNotNull(DEVICE_KEY, experiment.match?.device)
matchObject.putIfNotNull(LANG_KEY, experiment.match?.language)
matchObject.putIfNotNull(MANUFACTURER_KEY, experiment.match?.manufacturer)
matchObject.putIfNotNull(REGIONS_KEY, experiment.match?.regions?.toJsonArray())
matchObject.putIfNotNull(RELEASE_CHANNEL_KEY, experiment.match?.releaseChannel)
matchObject.putIfNotNull(VERSION_KEY, experiment.match?.version)
return matchObject
}

private fun jsonToPayload(jsonObject: JSONObject): ExperimentPayload {
// For now we decided to support primitive types only
val payload = ExperimentPayload()
@@ -100,48 +111,6 @@ class JSONExperimentParser {
return jsonObject
}

private fun <T> List<T>.toJsonArray(): JSONArray {
return fold(JSONArray()) { jsonArray, element -> jsonArray.put(element) }
}

@Suppress("UNCHECKED_CAST")
private fun <T> JSONArray?.toList(): List<T> {
if (this != null) {
val result = ArrayList<T>()
for (i in 0 until length())
result.add(get(i) as T)
return result
}
return listOf()
}

private fun JSONObject.tryGetString(key: String): String? {
if (!isNull(key)) {
return getString(key)
}
return null
}

private fun JSONObject.tryGetInt(key: String): Int? {
if (!isNull(key)) {
return getInt(key)
}
return null
}

private fun JSONObject.tryGetLong(key: String): Long? {
if (!isNull(key)) {
return getLong(key)
}
return null
}

private fun JSONObject.putIfNotNull(key: String, value: Any?) {
if (value != null) {
put(key, value)
}
}

companion object {
private const val BUCKETS_KEY = "buckets"
private const val MATCH_KEY = "match"
@@ -152,6 +121,7 @@ class JSONExperimentParser {
private const val MANUFACTURER_KEY = "manufacturer"
private const val DEVICE_KEY = "device"
private const val COUNTRY_KEY = "country"
private const val RELEASE_CHANNEL_KEY = "release_channel"
private const val MAX_KEY = "max"
private const val MIN_KEY = "min"
private const val ID_KEY = "id"
@@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.service.fretboard

import org.json.JSONArray
import org.json.JSONObject

fun <T> List<T>.toJsonArray(): JSONArray {
return fold(JSONArray()) { jsonArray, element -> jsonArray.put(element) }
}

@Suppress("UNCHECKED_CAST")
fun <T> JSONArray?.toList(): List<T> {
if (this != null) {
val result = ArrayList<T>()
for (i in 0 until length())
result.add(get(i) as T)
return result
}
return listOf()
}

fun JSONObject.tryGetString(key: String): String? {
if (!isNull(key)) {
return getString(key)
}
return null
}

fun JSONObject.tryGetInt(key: String): Int? {
if (!isNull(key)) {
return getInt(key)
}
return null
}

fun JSONObject.tryGetLong(key: String): Long? {
if (!isNull(key)) {
return getLong(key)
}
return null
}

fun JSONObject.putIfNotNull(key: String, value: Any?) {
if (value != null) {
put(key, value)
}
}

This file was deleted.

Oops, something went wrong.
@@ -0,0 +1,87 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.service.fretboard

import android.content.Context
import android.os.Build
import java.util.Locale

/**
* Class used to provide
* custom filter values
*/
open class ValuesProvider {
/**
* Provides the user's language
*
* @return user's language as a three-letter abbreviation
*/
open fun getLanguage(context: Context): String {
return Locale.getDefault().isO3Language
}

/**
* Provides the app id (package name)
*
* @return app id (package name)
*/
open fun getAppId(context: Context): String {
return context.packageName
}

/**
* Provides the user's region
*
* @return user's region as a three-letter abbreviation
*/
open fun getRegion(context: Context): String? {
return null
}

/**
* Provides the app version
*
* @return app version name
*/
open fun getVersion(context: Context): String {
return context.packageManager.getPackageInfo(context.packageName, 0).versionName
}

/**
* Provides the device manufacturer
*
* @return device manufacturer
*/
open fun getManufacturer(context: Context): String {
return Build.MANUFACTURER
}

/**
* Provides the device model
*
* @return device model
*/
open fun getDevice(context: Context): String {
return Build.DEVICE
}

/**
* Provides the user's country
*
* @return user's country, as a three-letter abbreviation
*/
open fun getCountry(context: Context): String {
return Locale.getDefault().isO3Country
}

/**
* Provides the app's release channel (alpha, beta, ...)
*
* @return release channel of the app
*/
open fun getReleaseChannel(context: Context): String? {
return null
}
}
@@ -399,16 +399,16 @@ class ExperimentEvaluatorTest {
`when`(packageManager.getPackageInfo(anyString(), anyInt())).thenReturn(packageInfo)
`when`(context.packageManager).thenReturn(packageManager)

var evaluator = ExperimentEvaluator(object : RegionProvider {
override fun getRegion(): String {
var evaluator = ExperimentEvaluator(object : ValuesProvider() {
override fun getRegion(context: Context): String? {
return "USA"
}
})

assertNotNull(evaluator.evaluate(context, ExperimentDescriptor("testid"), listOf(experiment), 20))

evaluator = ExperimentEvaluator(object : RegionProvider {
override fun getRegion(): String {
evaluator = ExperimentEvaluator(object : ValuesProvider() {
override fun getRegion(context: Context): String? {
return "ESP"
}
})
@@ -17,7 +17,14 @@ class JSONExperimentParserTest {
val experiment = Experiment("sample-id",
"sample-name",
"sample-description",
Experiment.Matcher("es|en", "sample-appId", listOf("US")),
Experiment.Matcher("es|en",
"sample-appId",
listOf("US"),
"1.0",
"manufacturer",
"device",
"country",
"release_channel"),
Experiment.Bucket(20, 0),
1526991669)
val jsonObject = JSONExperimentParser().toJson(experiment)
@@ -31,6 +38,11 @@ class JSONExperimentParserTest {
assertEquals("US", regions.get(0))
assertEquals("sample-appId", match.getString("appId"))
assertEquals("es|en", match.getString("lang"))
assertEquals("1.0", match.getString("version"))
assertEquals("manufacturer", match.getString("manufacturer"))
assertEquals("device", match.getString("device"))
assertEquals("country", match.getString("country"))
assertEquals("release_channel", match.getString("release_channel"))
assertEquals("sample-description", jsonObject.getString("description"))
assertEquals("sample-id", jsonObject.getString("id"))
assertEquals(1526991669, jsonObject.getLong("last_modified"))

0 comments on commit 43b3938

Please sign in to comment.
You can’t perform that action at this time.