Skip to content

Commit

Permalink
Merge experiments fork to master.
Browse files Browse the repository at this point in the history
Normally we do not do merge commits. But this setup tries to preserve the history of
"fretboard" and our fork "experiments".
  • Loading branch information
pocmo committed Mar 8, 2019
2 parents f86504a + b3b249a commit b8b75d5
Show file tree
Hide file tree
Showing 43 changed files with 4,809 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .buildconfig.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ projects:
path: components/service/fretboard
description: 'An Android framework for segmenting users in order to run A/B tests and rollout features gradually.'
publish: true
service-experiments:
path: components/service/experiments
description: 'An Android SDK for running experiments on user segments in multiple branches.'
publish: true
service-glean:
path: components/service/glean
description: 'A client-side telemetry SDK for collecting metrics and sending them to the Mozilla telemetry service'
Expand Down
3 changes: 3 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
/components/service/glean/ @mozilla-mobile/telemetry
/samples/glean/ @mozilla-mobile/telemetry

# Experiments library
/components/service/experiments/ @mozilla-mobile/telemetry

# Release Engineering pipeline
/automation/ @mozilla-mobile/releng @mozilla-mobile/act
/CODEOWNERS @mozilla-mobile/releng @mozilla-mobile/act
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ _Components and libraries to interact with backend services._

* 🔴 [**Glean**](components/service/glean/README.md) - A client-side telemetry SDK for collecting metrics and sending them to Mozilla's telemetry service (eventually replacing [service-telemetry](components/service/telemetry/README.md)).

* 🔴 [**Experiments**](components/service/experiments/README.md) - An Android SDK for running experiments on user segments in multiple branches.

* 🔴 [**Pocket**](components/service/pocket/README.md) - A library for communicating with the Pocket API.

* 🔵 [**Telemetry**](components/service/telemetry/README.md) - A generic library for sending telemetry pings from Android applications to Mozilla's telemetry service.
Expand Down
1 change: 1 addition & 0 deletions components/service/experiments/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
254 changes: 254 additions & 0 deletions components/service/experiments/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# [Android Components](../../../README.md) > Service > Experiments

An Android SDK for running experiments on user segments in multiple branches.

## Usage

### Setting up the dependency

Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):

```Groovy
implementation "org.mozilla.components:service-experiments:{latest-version}"
```

### Creating an Experiments instance
In order to use the library, first you have to create a new `Experiments` instance. You do this once per app launch
(typically in your `Application` class `onCreate` method). You simply have to instantiate the `Experiments` class and
provide the `ExperimentStorage` and `ExperimentSource` implementations, like this:

```Kotlin
class SampleApp : Application() {
lateinit var experiments: Experiments

override fun onCreate() {
experiments = Experiments(
experimentSource,
experimentStorage
)
}
}
```

#### Using Kinto as experiment source
Experiments includes a default source implementation for a Kinto backend, which you can use like this:

```Kotlin
// Specify which HTTP (Fetch) client to use
val httpClient = GeckoViewFetchClient(context)

val experiments = Experiments(
KintoExperimentSource(baseUrl, bucketName, collectionName, httpClient),
experimentStorage
)
```

#### Using a JSON file as experiment storage
Experiments includes support for flat JSON files as storage mechanism out of the box:

```Kotlin
val experiments = Experiments(
experimentSource,
FlatFileExperimentStorage(File(context.filesDir, "experiments.json"))
)
```

### Fetching experiments from disk
After instantiating `Experiments`, in order to load the list of already downloaded
experiments from disk, you need to call the `loadExperiments` method (don't call it
on the UI thread, this example uses a coroutine):

```Kotlin
launch(CommonPool) {
experiments.loadExperiments()
}
```

### Updating experiment list
Experiments provides two ways of updating the downloaded experiment list from the server: the first one is to directly
call `updateExperiments` on a `Experiments` instance, which forces experiments to be updated immediately and synchronously
(do not call this on the main thread), like this:

```Kotlin
experiments.updateExperiments()
```

The second one is to use the provided `JobScheduler`-based scheduler, like this:
```Kotlin
val scheduler = JobSchedulerSyncScheduler(context)
scheduler.schedule(EXPERIMENTS_JOB_ID, ComponentName(this, ExperimentsSyncService::class.java))
```

Where `ExperimentsSyncService` is a subclass of `SyncJob` you create like this, providing the `Experiments` instance via the
`getExperiments` method:

```Kotlin
class ExperimentsSyncService : SyncJob() {
override fun getExperiments(): Experiments {
return experiments
}
}
```

And then you have to register it on the manifest, just like any other `JobService`:

```xml
<service android:name=".ExperimentsSyncService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE">
```

### Checking if a user is part of an experiment
In order to check if a user is part of a specific experiment, Experiments provides two APIs: a Kotlin-friendly
`withExperiment` API and a more Java-like `isInExperiment`. In both cases you pass an instance of `ExperimentDescriptor`
with the name of the experiment you want to check:

```Kotlin
val descriptor = ExperimentDescriptor("first-experiment-name")
experiments.withExperiment(descriptor) {
someButton.setBackgroundColor(Color.RED)
}

otherButton.isEnabled = experiments.isInExperiment(descriptor)
```

### Getting experiment metadata
Experiments allows experiments to carry associated metadata, which can be retrieved using the Kotlin-friendly
`withExperiment` API or the more Java-like `getExperiment` API, like this:

```Kotlin
val descriptor = ExperimentDescriptor("first-experiment-name")
experiments.withExperiment(descriptor) {
toolbar.setColor(Color.parseColor(it.payload?.get("color") as String))
}
textView.setText(experiments.getExperiment(descriptor)?.payload?.get("message"))
```

### Setting override values
Experiments allows you to force activate / deactivate a specific experiment via `setOverride`, you
simply have to pass true to activate it, false to deactivate:

```Kotlin
val descriptor = ExperimentDescriptor("first-experiment-name")
experiments.setOverride(context, descriptor, true)
```

You can also clear an override for an experiment or all overrides:

```Kotlin
val descriptor = ExperimentDescriptor("first-experiment-name")
experiments.clearOverride(context, descriptor)
experiments.clearAllOverrides(context)
```

### Filters
Experiments allows you to specify the following filters:
- Buckets: Every user is in one of 100 buckets (0-99). For every experiment you can set up a min and max value (0 <= min <= max <= 100). The bounds are [min, max).
- Both max and min are optional. For example, specifying only min = 0 or only max = 100 includes all users
- 0-100 includes all users (as opposed to 0-99)
- 0-0 includes no users (as opposed to just bucket 0)
- 0-1 includes just bucket 0
- Users will always stay in the same bucket. An experiment targeting 0-25 will always target the same 25% of users
- appId (regex): The app ID (package name)
- version (regex): The app version
- country (regex): country, pulled from the default locale
- lang (regex): language, pulled from the default locale
- device (regex): Android device name
- manufacturer (regex): Android device manufacturer
- region: custom region, different from the one from the default locale (like a GeoIP, or something similar).
- release channel: release channel of the app (alpha, beta, etc)

For region and release channel to work you must provide a `ValuesProvider` implementation when creating the `Experiments` instance, as detailed below

### Specifying custom values for filters
Additionally, Experiments allows you to specify a custom `ValuesProvider` object in order to return a custom region,
different from the one of the current locale (perhaps doing a GeoIP or something like that), or the app
relase channel (alpha, beta, etc). It also allows you to override the values for other experiment properties
(such as the appId, country, etc):

```Kotlin
val experiments = Experiments(
experimentSource,
experimentStorage,
object : ValuesProvider {
override fun getRegion() {
return custom_region
}

override fun getReleaseChannel() {
return app_channel
}
}
)
```

### Creating a custom experiment source
You can create a custom experiment source simply by implementing the `ExperimentSource` interface:

```Kotlin
class MyExperimentSource : ExperimentSource {
override fun getExperiments(snapshot: ExperimentsSnapshot): ExperimentsSnapshot {
// ...
return updatedSnapshot
}
}
```

The `getExperiments` method takes an `ExperimentsSnapshot` object, which contains the list of already downloaded experiments and
a last_modified date, and returns another `ExperimentsSnapshot` object with the updated list of experiments.

As the `getExperiments` receives the list of experiments from storage and a last_modified date, it allows you
to do diff requests, if your storage mechanism supports it (like Kinto does).

### Creating a custom experiment storage
You can create a custom experiment storage simply by implementing the `ExperimentStorage` interface, overriding
the save and retrieve methods, which use `ExperimentsSnapshot` objects with the list of experiments and a last_modified date:

```Kotlin
class MyExperimentStorage : ExperimentStorage {
override fun save(snapshot: ExperimentsSnapshot) {
// save snapshot to disk
}

override fun retrieve(): ExperimentsSnapshot {
// load snapshot from disk
return snapshot
}
}
```

### Experiments format for Kinto
The provided implementation for Kinto expects the experiments in the following JSON format:
```json
{
"data":[
{
"name": "",
"match":{
"lang":"",
"appId":"",
"regions":[],
"country":"",
"version":"",
"device":"",
"manufacturer":"",
"region":"",
"release_channel":""
},
"buckets": {
"min": "0",
"max": "100"
},
"description":"",
"id":"",
"last_modified":1523549895713
}
]
}
```

## License

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/
44 changes: 44 additions & 0 deletions components/service/experiments/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* 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/. */

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion config.compileSdkVersion

defaultConfig {
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation Dependencies.kotlin_stdlib
implementation Dependencies.kotlin_coroutines
implementation Dependencies.arch_workmanager

implementation project(':concept-fetch')
implementation project(':support-ktx')
implementation project(':support-base')

testImplementation Dependencies.testing_junit
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
testImplementation Dependencies.testing_mockwebserver
testImplementation Dependencies.kotlin_reflect

testImplementation project(':support-test')
testImplementation project(':lib-fetch-httpurlconnection')
}

apply from: '../../../publish.gradle'
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
21 changes: 21 additions & 0 deletions components/service/experiments/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
5 changes: 5 additions & 0 deletions components/service/experiments/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- 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/. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="mozilla.components.service.experiments" />
Loading

0 comments on commit b8b75d5

Please sign in to comment.