Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/1 revamp #2

Merged
merged 4 commits into from
Jul 21, 2022
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
138 changes: 77 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
# firely

Firely is an A/B Testing overlay based on Firebase Remote Config.
It's a work in progress to simplify the integration and make the management of A/B testing XPs safer.
Firely is an A/B Testing overlay based on Firebase Remote Config. It's a work in progress to
simplify the integration and make the management of A/B testing XPs safer.

## How does it work

This library, integrated in your gradle project, only requires:

- A `firely-config.json` file that will contains the type of items, the keys, and the default value
- A call to `Firely.setup(Context context)` from the `Application.onCreate()` method
- One proguard rule
- Set `apply plugin: 'com.google.gms.google-services'` in your build.gradle file
- Make sure you have a `google-services.json` provided by Firebase in your project

`firely-config.json` file is organized in 3 main sections (for us, but it can have the "names" you
want):

`firely-config.json` file is organized in 3 main sections (for us, but it can have the "names" you want):
- Feature Flags
- Config
- Experiments (or A/B Tests)


Here is an example of `firely-config.json`:

```json
{
"config": [
Expand Down Expand Up @@ -44,77 +49,83 @@ Here is an example of `firely-config.json`:
}
```

Firely is an Android library that come with a gradle plugin, `firely-plugin`. It will generate a `FirelyConfig.java` file based on the `firely-config.json`, like the `R.java` android creates. The `FirelyConfig.java` will contain Enums that match the configuration. You can then use these enums on Firely to get `LiveVariable`, `CodeBlock`, `OrderedArrayBlock`.

Firely is an Android library that come with a gradle plugin, `firely-plugin`. It will generate
a `FirelyConfig.kt` file based on the `firely-config.json`, like the `R.java` android creates.
The `FirelyConfig.kt` will contain Enums that match the configuration. You can then use these enums
on Firely to get `LiveVariable`, `CodeBlock`, `OrderedArrayBlock`.

### LiveVariable

Let's imagine I am using Remote Config to restrict my user to a minimum Android version on which they can run (otherwise they have to update the app). With Firely, I can instantiate a LiveVariable that will use this setting:
Let's imagine I am using Remote Config to restrict my user to a minimum Android version on which
they can run (otherwise they have to update the app). With Firely, I can instantiate a LiveVariable
that will use this setting:

```java
LiveVariable<Integer> minAndroidRemoteVersion = Firely.integerVariable(FirelyConfig.Config.ANDROID_VERSION_CODE_MIN);
```kotlin
val minAndroidRemoteVersion = Firely.integerVariable(FirelyConfig.Config.ANDROID_VERSION_CODE_MIN)
```

`FirelyConfig.Config.ANDROID_VERSION_CODE_MIN` is generated by the plugin and the default value is 0.
`FirelyConfig.Config.ANDROID_VERSION_CODE_MIN` is generated by the plugin and the default value is

0.

Now, anytime I need to get the last version that has been fetched, I just call:

```java
Integer lastVersion = minAndroidRemoteVersion.get();
```kotlin
val lastVersion = minAndroidRemoteVersion.get()
```

Here is another example with a feature flag:

```java
```kotlin
if (Firely.booleanVariable(FirelyConfig.FeatureFlag.REFER_A_FRIEND).get()) {
// Add the view
// Add the view
}
```


### CodeBlock

Now I need to build out an XP that will change the text of a button.

```java
```kotlin
Firely.codeBlock(Remote.Experiment.XP_BUTTON)
.withVariant("billed_currency", "no_price")
.execute(
() -> advance.setText(getString(R.string.bb_payment_cta)), // control
() -> advance.setText(.getString(R.string.bb_payment_cta_2)), // billed_currency
() -> advance.setText(getString(R.string.bb_payment_cta_3))); // no_price
.withVariant("billed_currency", "no_price")
.execute(
{ advance.setText(getString(R.string.bb_payment_cta)) },// control
{ advance.setText(getString(R.string.bb_payment_cta_2)) },// billed_currency
{ advance.setText(getString(R.string.bb_payment_cta_3)) }) // no_price
```

> NOTE: we are always using "control" as the default value and as the control group for A/B Tests.


### OrderedArrayBlock

In the [Busbud Android App](https://play.google.com/store/apps/details?id=com.busbud.android), we use a lot of blocks and lists. Let's imagine you have *N* blocks of data in a page.
You want to A/B test which one should go first and the order for all the others.
A basic approach could be to have *N!* variants.
In the [Busbud Android App](https://play.google.com/store/apps/details?id=com.busbud.android), we
use a lot of blocks and lists. Let's imagine you have *N* blocks of data in a page. You want to A/B
test which one should go first and the order for all the others. A basic approach could be to have *
N!* variants.

If we have three items: 1-2-3, 2-1-3, 2-3-1, 1-3-2, 3-2-1, 3-1-2

And while using CodeBlocks:

```java
```kotlin
Firely.codeBlock(Remote.Experiment.XP_BUTTON)
.withVariant("2-1-3", "2-3-1", "1-3-2", "3-2-1", "3-1-2")
.execute(
() -> {
addOne();
addTwo();
addThree();
}, // control
() -> {
addTwo();
addOne();
addThree();
},
... etc
.withVariant("2-1-3", "2-3-1", "1-3-2", "3-2-1", "3-1-2")
.execute(
{
addOne()
addTwo()
addThree()
}, // control
{
addTwo()
addOne()
addThree()
},
... etc

```

Really inefficient.

Another approach is to use OrderedArrayBlock. You will use one Firebase entry:
Expand All @@ -131,52 +142,57 @@ Another approach is to use OrderedArrayBlock. You will use one Firebase entry:
}
```

```java
OrderedArrayBlock mCheckoutXp =
Firely.orderedArrayBlock(FirelyConfig.Experiment.XP_CHECKOUT_ORDER)
.addStep("one", () -> addOne())
.addStep("two", () -> addTwo())
.addStep("three", () -> addThree());
```kotlin
val mCheckoutXp =
Firely.orderedArrayBlock(FirelyConfig.Experiment.XP_CHECKOUT_ORDER)
.addStep("one") { addOne() }
.addStep("two") { addTwo() }
.addStep("three") { addThree() }
```

And you can control your A/B Tests from the Firebase Remote Config dashboard by changing the `xp_mypage_order` key.
And you can control your A/B Tests from the Firebase Remote Config dashboard by changing
the `xp_mypage_order` key.

`three,one,two` will then call `addThree()`, `addOne()`, `addTwo()`. You can use this to remotely control the order of lists.
`three,one,two` will then call `addThree()`, `addOne()`, `addTwo()`. You can use this to remotely
control the order of lists.

## Analytics

One of the highlights of Firebase is that everything is working together. In the [documentation](https://firebase.google.com/docs/remote-config/config-analytics), Firebase proposes putting the values, manually, as a User Property:
One of the highlights of Firebase is that everything is working together. In
the [documentation](https://firebase.google.com/docs/remote-config/config-analytics), Firebase
proposes putting the values, manually, as a User Property:

```java
String experiment1_variant = FirebaseRemoteConfig.getInstance().getString("experiment1");
AppMeasurement.getInstance(context).setUserProperty("MyExperiment",experiment1_variant);
(TODO update to match the proper usage of the latest version firebase remoteconfig )
```kotlin
val experiment1_variant = FirebaseRemoteConfig.getInstance().getString("experiment1")
AppMeasurement.getInstance(context).setUserProperty("MyExperiment", experiment1_variant)
```

That's nice, but it does not fit our needs. Putting the property at the user level means it will be erased over time and we will lose the information. Instead, we prefer to tag all the events with all the experiments that have been applied at the time the event is triggered.
That's nice, but it does not fit our needs. Putting the property at the user level means it will be
erased over time and we will lose the information. Instead, we prefer to tag all the events with all
the experiments that have been applied at the time the event is triggered.

We added a method on Firely to help with this:
We added a property on Firely to help with this:

```java
Firely.getAllPropsWithCurrentValue()
```kotlin
InternalFirely.allPropsWithCurrentValue
```
And this method is called each time we send an event and merged into the property list.
Therefore we can track the configuration changes over time.

And this property is updated each time we send an event and merged into the property list. Therefore we
can track the configuration changes over time.

## Use in the project


You need to apply the plugin (TODO integrate in maven repo):

```
buildscript {
repositories {
mavenLocal()
jcenter()
}

dependencies {
classpath group: 'com.busbud.android', name: 'firely-plugin', version: '0.1.0'
classpath group: 'com.busbud.android', name: 'firely-plugin', version: '0.2.0'
}
}

Expand All @@ -186,6 +202,6 @@ apply plugin: 'com.busbud.android.firely'

You also need to import the aar (TODO integrate in maven repo):

`compile project(':firely-0.1.0')`
`compile project(':firely-0.2.0')`


17 changes: 13 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,27 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
ext.kotlin_version = '1.6.21'
ext.gradle_version = '7.2.1'
ext.buildtools_version = '33.0.0-rc4'
ext.corektx_version = '1.7.0'
ext.compilesdk_version = 32

repositories {
jcenter()
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.google.gms:google-services:3.0.0'
classpath "com.android.tools.build:gradle:$gradle_version"
classpath 'com.google.gms:google-services:4.3.10'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}

allprojects {
repositories {
jcenter()
google()
mavenCentral()
}
}
Expand Down
21 changes: 12 additions & 9 deletions firely-lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,37 @@
*/

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

android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
compileSdk compilesdk_version
buildToolsVersion = buildtools_version

defaultConfig {
minSdkVersion 16
targetSdkVersion 25
targetSdkVersion compilesdk_version
versionCode 1
versionName "1.0"

}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
targetCompatibility 1.7
sourceCompatibility 1.7
targetCompatibility 1.8
sourceCompatibility 1.8
}
}

dependencies {
compile 'com.google.firebase:firebase-config:10.0.1'
compile 'com.github.smaspe:iterables:0.3.0'
implementation 'com.google.firebase:firebase-config:21.1.0'
implementation "androidx.core:core-ktx:$corektx_version"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

task sourceJar(type: Jar) {
Expand All @@ -59,7 +62,7 @@ publishing {
firely(MavenPublication) {
groupId 'com.busbud.android'
artifactId 'firely'
version '0.1.0'
version '0.2.0'
artifact(sourceJar)
artifact("$buildDir/outputs/aar/firely-lib-release.aar")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,50 +18,43 @@
* THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.busbud.android.firely;
package com.busbud.android.firely

import android.util.Log;
import android.util.Log

import java.util.Arrays;
import java.util.List;
class CodeBlock(name: String, internalFirely: InternalFirely) : Operation(name, internalFirely) {

public class CodeBlock extends Operation {
private var variant = listOf<String>()

private List<String> mVariant;

CodeBlock(String name, InternalFirely internalConfig) {
super(name, internalConfig);
fun withVariant(vararg keys: String): CodeBlock {
variant = keys.toList()
return this
}

public CodeBlock withVariant(String... keys) {
mVariant = Arrays.asList(keys);
return this;
}
fun execute(defaultCodeBranch: IDefaultCodeBranch, vararg branches: ICodeBranch) {

public void execute(IDefaultCodeBranch defaultCodeBranch, ICodeBranch... branches) {
if (defaultCodeBranch == null) {
return;
}
if (branches == null) {
defaultCodeBranch.execute();
return;
if (branches.isEmpty()) {
defaultCodeBranch.execute()
return
}
if (branches.length != (mVariant != null ? mVariant.size() : 0)) {

if (branches.size != variant.size) {
if (Firely.logLevel().errorLogEnabled()) {
Log.e(CodeBlock.class.getSimpleName(), "Variants does not match CodeBranches");
Log.e(CodeBlock::class.simpleName, "Variants does not match CodeBranches")
}
// But continue.
}
String phoneVariant = getInternalFirely().getString(getName());
if (mVariant != null && mVariant.contains(phoneVariant)) {
int index = mVariant.indexOf(phoneVariant);
if (branches.length > index) {
branches[index].execute();

val phoneVariant: String = internalFirely.getString(name)
if (variant.contains(phoneVariant)) {
val index = variant.indexOf(phoneVariant)
if (branches.size > index) {
branches[index].execute()
} else {
defaultCodeBranch.execute();
defaultCodeBranch.execute()
}
} else {
defaultCodeBranch.execute();
defaultCodeBranch.execute()
}
}
}
Loading