Infrastructure

Firely: Make Firebase Remote Config a bit more comfy

The new Firebase has been released during the Google I/O 2016, and one of the key features is called Firebase Remote Config. It allows you to experiment within your app and to ship faster with less risk. It can be used as an asynchronous remote control in your apps. And we’ve open-sourced our Firely Facade to make using it easier.

Firebase Remote Config

The Remote Config is a key-value pair configuration store that can be synced with the devices. And you can apply conditions on those configurations to distribute different values depending on many criteria: countries, languages, package names, audiences, random percentage of users…

Remote Config is great for :

  • A/B testing
  • Remote configuration
  • Feature flags

Integration of Firebase Remote Config

This is really easy and not the scope of this blog post, but you can find complete tutorials and sample app walkthrough on Firebase Remote Config Android Sample App Walkthrough

At Busbud, we use Firebase Remote Config a lot and especially for A/B testing. We initially integrated Remote Config very simply in the application but after a few months of use, we saw patterns emerge from using Firebase Remote Config: the footprint in the code was not that light. And it was definitely not DRY.

Let’s imagine that we want to integrate a feature flag (on/off) using Firebase Remote Config, from scratch.

Configuring Firebase Remote Config is quite simple:

  • First you need to integrate the SDK (one line of code in the gradle file)
  • Then you need to put your defaults in a XML file
  • Then you need to setup your instance in a place that makes sense and that will not make Firebase crash (we ran into some issue with the classic “FirebaseApp is not initialized”)
  • Then you need to call the asynchronous fetch method in a place that makes sense, at a pace that makes sense
  • Then you need to create a key for each entry you got in the remote config
  • Then you need to plug that to your Tracking because you are using something else than or in addition to Firebase Analytics
  • etc…

In fact, you end up with quite a bit more code than you had bargained for, when all you really wanted to write was:

if (FirebaseRemoteConfig.isMyFeatureFlagOn(MyFeatureFlag)) {
	// Let's do that
} else {
	// Let's do this
}

What we needed

After few iterations, we wanted something that:

  • Is simpler to configure, defining the keys and their default value once
  • Is safe while removing A/B Tests: the application should not compile if I remove a key from my initial config (which helps clean up A/B Tests very easily)
  • Provided reasonable behavior for fetching of the config and the time it’s applied with our application
  • Has static methods anywhere (or integrated with Dagger+2)
  • Is independent of tracking

We ended-up with a small and light library called Firely.
Nothing revolutionary here, but just a nicer Facade, our comfy version of Firebase Remote Config.

How the Firely Facade works

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

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:

{
  "config": [
    {
      "key": "android_version_code_min",
      "default": 0
    }
  ],
  "feature_flag": [
    {
      "key": "refer_a_friend",
      "default": true
    },
    {
      "key": "promotion_url",
      "default": ""
    }
  ],
  "experiment": [
    {
      "key": "xp_button_pay",
      "default": "control"
    }
  ]
}

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.

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:

LiveVariable<Integer> 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.

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

Integer lastVersion = minAndroidRemoteVersion.get();

Here is another example with a feature flag:

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

CodeBlock

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

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

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, 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:

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

Really inefficient.

Another approach is to use OrderedArrayBlock. You will use one Firebase entry:

{
  ...
  "experiment": [
    {
      "key": "xp_mypage_order",
      "default": "one,two,three"
    }
  ]
}
OrderedArrayBlock 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.

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, Firebase proposes putting the values, manually, as a User Property:

String 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.

We added a method on Firely to help with this:

Firely.getAllPropsWithCurrentValue()

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.

Firely in depth

Firely is really simple and contains:

  • The groovy gradle plugin which is really basic
  • The Firely Java library

One of the key issues we currently have is determining the best time to FirebaseRemoteConfig.fetch() and follow with FirebaseRemoteConfig.activateFetched() on the local Firebase Remote Config. And also decide on the cacheExpiration.

For your convenience, here’s the way Firebase Remote Config works:

Firebase Remote Config Fetch

Firely is very simple. It’s plugged into the registerActivityLifecycleCallbacks and will only fetch the configuration while your user is using the app actively (i.e. opening / closing screens – if you have an app that have no Activity component, we’re accepting contributions 😉 ).
FirebaseRemoteConfig.fetch() is called in onResume() and, in the case of production, the cache expiration is set at 0 until we actually get a configuration and then it’s set to 1 hour. For the record, Firebase allow you to make 5 requests per hour for a given app. The INITIAL_CHECK with the cache expiration at 0 is reset anytime your app is updated, to force the last config to be used as fast as possible.

FirebaseRemoteConfig.activateFetched() is a bit more complex because we wanted to misrepresent for analytics (you don’t want to send properties that do not reflect what is used in the app) and that make sense as a user. You don’t want your users to navigate forward through your app and then discover as they try to go back that they’re now in another variant of the screen they were expecting to find. To provide a more stable experience for users, we decided to FirebaseRemoteConfig.activateFetched() once the root task is destroyed, so the config will be applied next launch. The reason why is that our app is based on a single Task of Activities. This may not fit all projects.

But when developing, you want the fetch to be activated as soon as possible. In debugMode, the FirebaseRemoteConfig.activateFetched() is called in onPause().

And that’s pretty much it!

Firely – what is next?

This is a very first rough cut, and the idea behind the Firely library is to keep it light and open.

Some ideas here:

  • Improving the FirebaseRemoteConfig.activateFetched() and the way the consistency can be guaranteed between the variable you access and what is sent to the Analytics APIs.
  • FirelyTextView to automatically change Text Content ?
  • FirelyImageView : same with Picasso-like integration to be able to load remote images
  • Having the notion of scope on a LiveVariable to be sure that the config will not change during the lifetime of the Activity / Presenter / Fragment / Application…

One last word

It took roughly 2 or 3 days of dev work to end up with a solution that better fits our needs.
There are a lot of complex solutions that exist on the market, but Firebase Remote Config is one that is almost free.
And while Firebase Remote Config is nice, you may need something comfier and DRYer for your daya to day work. There is nothing wrong in making your life easier, so give Firely a try.

You can find the open-source code on github.

Source: Busbud engineering