Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Issue #6: Implement code for evaluating experiment configuration and …
Browse files Browse the repository at this point in the history
…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 b4d9e41
Show file tree
Hide file tree
Showing 8 changed files with 575 additions and 2 deletions.
@@ -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"
}
}
Expand Up @@ -20,7 +20,11 @@ data class Experiment(
data class Matcher( data class Matcher(
val language: String? = null, val language: String? = null,
val appId: 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( data class Bucket(
Expand Down
@@ -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
}
}
Expand Up @@ -16,6 +16,7 @@ class Fretboard(
) { ) {
private var experiments: List<Experiment> = listOf() private var experiments: List<Experiment> = listOf()
private var experimentsLoaded: Boolean = false private var experimentsLoaded: Boolean = false
private val evaluator = ExperimentEvaluator()


/** /**
* Loads experiments from local storage * Loads experiments from local storage
Expand Down
Expand Up @@ -22,7 +22,14 @@ class JSONExperimentParser {
val matchObject: JSONObject? = jsonObject.optJSONObject(MATCH_KEY) val matchObject: JSONObject? = jsonObject.optJSONObject(MATCH_KEY)
val regions: List<String>? = matchObject?.optJSONArray(REGIONS_KEY)?.toList() val regions: List<String>? = matchObject?.optJSONArray(REGIONS_KEY)?.toList()
val matcher = if (matchObject != null) { 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 } else null
val bucket = if (bucketsObject != null) { val bucket = if (bucketsObject != null) {
Experiment.Bucket(bucketsObject.tryGetInt(MAX_KEY), bucketsObject.tryGetInt(MIN_KEY)) Experiment.Bucket(bucketsObject.tryGetInt(MAX_KEY), bucketsObject.tryGetInt(MIN_KEY))
Expand Down Expand Up @@ -107,6 +114,10 @@ class JSONExperimentParser {
private const val REGIONS_KEY = "regions" private const val REGIONS_KEY = "regions"
private const val LANG_KEY = "lang" private const val LANG_KEY = "lang"
private const val APP_ID_KEY = "appId" 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 MAX_KEY = "max"
private const val MIN_KEY = "min" private const val MIN_KEY = "min"
private const val ID_KEY = "id" private const val ID_KEY = "id"
Expand Down
@@ -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())
}
}

0 comments on commit b4d9e41

Please sign in to comment.