Skip to content
Permalink
Browse files

Issue #6: Implement code for evaluating experiment configuration and …

…bucketing users

Closes #6: Implment code for evaluating experiment configuration and bucketing users
  • Loading branch information...
fercarcedo authored and pocmo committed Jun 14, 2018
1 parent fcf13f7 commit b4d9e41b749f34ac75cdb01e6c5150368fcdcba3
@@ -0,0 +1,29 @@
/* 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 java.util.UUID

internal class DeviceUuidFactory(context: Context) {
val uuid by lazy {
val preferences = context
.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
val prefUuid = preferences.getString(PREF_UUID_KEY, null)

if (prefUuid != null) {
UUID.fromString(prefUuid)
} else {
val uuid = UUID.randomUUID()
preferences.edit().putString(PREF_UUID_KEY, uuid.toString()).apply()
uuid
}
}

companion object {
private const val PREFS_FILE = "mozilla.components.service.fretboard"
private const val PREF_UUID_KEY = "device_uuid"
}
}
@@ -20,7 +20,11 @@ data class Experiment(
data class Matcher(
val language: String? = null,
val appId: String? = null,
val regions: List<String>? = null
val regions: List<String>? = null,
val version: String? = null,
val manufacturer: String? = null,
val device: String? = null,
val country: String? = null
)

data class Bucket(
@@ -0,0 +1,7 @@
/* 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

data class ExperimentDescriptor(val id: String)
@@ -0,0 +1,66 @@
/* 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 android.text.TextUtils
import java.util.Locale
import java.util.zip.CRC32

internal class ExperimentEvaluator {
fun evaluate(
context: Context,
experimentDescriptor: ExperimentDescriptor,
experiments: List<Experiment>,
userBucket: Int = getUserBucket(context)
): Boolean {
val experiment = experiments.firstOrNull { it.id == experimentDescriptor.id } ?: return false
return isInBucket(userBucket, experiment) && matches(context, experiment)
}

private fun matches(context: Context, experiment: Experiment): Boolean {
if (experiment.match != null) {
val region = Locale.getDefault().isO3Country
val matchesRegion = !(experiment.match.regions != null &&
experiment.match.regions.isNotEmpty() &&
experiment.match.regions.none { it == region })
val appVersion = context.packageManager.getPackageInfo(context.packageName, 0).versionName
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)
}
return true
}

private fun matchesExperiment(experimentValue: String?, deviceValue: String): Boolean {
return !(experimentValue != null &&
!TextUtils.isEmpty(experimentValue) &&
!deviceValue.matches(experimentValue.toRegex()))
}

private fun isInBucket(userBucket: Int, experiment: Experiment): Boolean {
return !(experiment.bucket?.min == null ||
userBucket < experiment.bucket.min ||
experiment.bucket.max == null ||
userBucket >= experiment.bucket.max)
}

private fun getUserBucket(context: Context): Int {
val uuid = DeviceUuidFactory(context).uuid.toString()
val crc = CRC32()
crc.update(uuid.toByteArray())
val checksum = crc.value
return (checksum % MAX_BUCKET).toInt()
}

companion object {
private const val MAX_BUCKET = 100L
}
}
@@ -16,6 +16,7 @@ class Fretboard(
) {
private var experiments: List<Experiment> = listOf()
private var experimentsLoaded: Boolean = false
private val evaluator = ExperimentEvaluator()

/**
* Loads experiments from local storage
@@ -22,7 +22,14 @@ class JSONExperimentParser {
val matchObject: JSONObject? = jsonObject.optJSONObject(MATCH_KEY)
val regions: List<String>? = matchObject?.optJSONArray(REGIONS_KEY)?.toList()
val matcher = if (matchObject != null) {
Experiment.Matcher(matchObject.tryGetString(LANG_KEY), matchObject.tryGetString(APP_ID_KEY), regions)
Experiment.Matcher(
matchObject.tryGetString(LANG_KEY),
matchObject.tryGetString(APP_ID_KEY),
regions,
matchObject.tryGetString(VERSION_KEY),
matchObject.tryGetString(MANUFACTURER_KEY),
matchObject.tryGetString(DEVICE_KEY),
matchObject.tryGetString(COUNTRY_KEY))
} else null
val bucket = if (bucketsObject != null) {
Experiment.Bucket(bucketsObject.tryGetInt(MAX_KEY), bucketsObject.tryGetInt(MIN_KEY))
@@ -107,6 +114,10 @@ class JSONExperimentParser {
private const val REGIONS_KEY = "regions"
private const val LANG_KEY = "lang"
private const val APP_ID_KEY = "appId"
private const val VERSION_KEY = "version"
private const val MANUFACTURER_KEY = "manufacturer"
private const val DEVICE_KEY = "device"
private const val COUNTRY_KEY = "country"
private const val MAX_KEY = "max"
private const val MIN_KEY = "min"
private const val ID_KEY = "id"
@@ -0,0 +1,49 @@
/* 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.content.SharedPreferences
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.robolectric.RobolectricTestRunner
import java.util.UUID

@RunWith(RobolectricTestRunner::class)
class DeviceUuidFactoryTest {
@Test
fun testUuidNoPreference() {
val context = mock(Context::class.java)
val sharedPreferences = mock(SharedPreferences::class.java)
val editor = mock(SharedPreferences.Editor::class.java)
`when`(editor.putString(anyString(), any())).thenReturn(editor)
`when`(sharedPreferences.edit()).thenReturn(editor)
`when`(sharedPreferences.getString(eq("device_uuid"), ArgumentMatchers.any())).thenReturn(null)
`when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPreferences)
val uuid = DeviceUuidFactory(context).uuid
assertTrue(uuid is UUID)
verify(editor).putString("device_uuid", uuid.toString())
}

@Test
fun testUuidSavedInPreferences() {
val savedUuid = "99111a0f-ca5d-4de1-913a-daba905c53b2"
val context = mock(Context::class.java)
val sharedPreferences = mock(SharedPreferences::class.java)
`when`(sharedPreferences.getString(eq("device_uuid"), any())).thenReturn(savedUuid)
`when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPreferences)
assertEquals(savedUuid, DeviceUuidFactory(context).uuid.toString())
}
}
Oops, something went wrong.

0 comments on commit b4d9e41

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