Implementing feature flags in React with Unleash
We implemented feature flags with Unleash, combined with Mixpanel, and were able to test more and faster, driving the MoM growth of our conversion rate.
ClaimCompass' Growth Team is working hard to deliver the best product possible to our users.
The implementation of feature flags was key in allowing us to set up a process allowing rapid experimentation.
By combining Unleash and Mixpanel, the team was able to test multiple experiments in little time and increase the conversion rate of our product.
What are Feature flags?
Generally a Feature Flag is a toggle that gives the ability to turn on/off features easily.
Feature Flags can be used to release unfinished features by hiding them from the users in order to maintain the same code in Development and Live servers. They can be also used for running experiments and A/B tests by releasing the features only to a selected part of users.
Together with Mixpanel for analytics, the Unleash feature flags have become the perfect combination for us here at the ClaimCompass Growth Team.
The Challenge
Researching what was out there, we couldn’t find a tool that is suitable for enabling feature flags in the Front End. So what we decided to do is get one of the existing Node JS tools and extend it in order to use it for our React application. We settled on Unleash.
What is Unleash?
In their own words, Unleash
"is an enterprise ready feature toggles service. It provides different strategies for controlling roll-out of feature toggles"
as well as the tool we chose to use for our Feature Flags implementation.
We found Unleash quite suitable for our needs: it has a simple UI, provides different activation strategies and different SDKs, including Node JS, which we use.
Unleash UI
The Unleash UI is quite simple and very easy to use. New feature toggles can be created, while existing ones can be turned on and off, updated and modified depending on our needs.
Having such an interface is quite useful, as it enables feature flags to be controlled not only by software developers, but by everyone on our Growth Team.
Activation Strategies
The different activation strategies in Unleash make it possible to switch features based on SessionId, Application Hostname, User ID, etc. For our purposes, the strategies we use the most are applicationHostname and gradualRolloutSessionId.
applicationHostname
applicationHostname: With this strategy features can be turned ON/OFF for lists of hostnames. The strategy has one parameter - a list of hostnames. We mostly use it when we want to test a feature in our UAT environment before releasing it to LIVE.
gradualRolloutSessionId
gradualRolloutSessionId: This session provides a gradual rollout of a feature for a desired percentage of users. We use the gradualRolloutSessionId mainly for A/B testing and usually turn the feature ON for a predetermined percentage of users based on the description of the test. What’s great about this activation strategy is that a given user will see the feature in the same state - enabled or disabled - each time they enter the app. This ensures a smooth UX and eliminates friction.
Unleash Context
The Unleash context contains the set of fields that the SDK needs in order to evaluate the feature toggle activation strategies rules. All of them are optional, but without them, some of the strategies could not be implemented:
- userId: String,
- sessionId: String,
- remoteAddress: String,
- properties: Map<String, String>
For instance, out of the above we have implemented only sessionId, which we use for the gradualRolloutSessionId activation strategy. I will go into greater detail on the implementation strategy later.
The server
Unleash has a client-server architecture. In order to implement feature flags, the client (your web app) needs to make requests to the Unleash server, in order to obtain information regarding the status of a feature flag (enabled/disabled) for each user.
There are many different options to host the Unleash server. The only requirement is that the client applications need to be able to make requests to the server. In other words, the clients and the server should either be on the same internal network or the server should be exposed to the Internet.
One option, which is great for enterprise users, is subscribing to Unleash Feature Management Service. For a monthly fee, Unleash provides a hosted server and full support.
We decided to run the Unleash server as a Docker container, since most of our other applications are containerised. There is an official image, but it doesn't come with authentication. This means that if your server is accessible over the Internet, anyone will be able to view and edit your feature flags from the dashboard.
Fortunately, adding authentication to the server is pretty straightforward. We are not going to go into much detail here because the official documentation already describes how it can be implemented. What we ended up doing was fork the official Docker image for Unleash and host it in our own private repository.
The only changes we had to make were in this file in order to implement OAuth. Now, when we run a container with our custom image, only users with a valid ClaimCompass email have access to the dashboard.
The client
For our implementation we use the official Node JS Client. Our backend is written in Node, so installation is just one line of code as follows:
$ npm install unleash-client --save
Then the Unleash client is initialized in the backend:
/* Initialize the unleash client */
const { initialize } = require('unleash-client')
const instance = initialize({
url: config.get('unleash').url,
appName: config.get('unleash').app_name
})
/* Optional events */
instance.on('error', console.error)
instance.on('warn', console.warn)
instance.on('ready', console.log)
console.log('hostname', process.env.HOSTNAME)
/* Metric Hooks */
instance.on('registered', clientData => { console.log('registered', clientData) })
instance.on('sent', payload => console.log('metrics bucket/payload sent', payload))
instance.on('count', (name, enabled) => console.log(`isEnabled(${name}) returned ${enabled}`))
Within the initialization the Unleash client sets up an in-memory repository and triggers the feature flag states from the server on regular intervals. After the client is initialized, the states of the feature flags can be retrieved with the methods:
isEnabled,
getVariant,
getFeatureToggleDefinition,
getFeatureToggleDefinitions
At the time we were doing our implementation of Unleash only isEnabled was available and this is the method we used. We extended it in order to be able to get all feature flags at the same time in a get_feature_toggles.js file:
'use strict'
const config = require('config')
const { isEnabled } = require('unleash-client')
function getFeatureToggle (context) {
return (obj, feature) => {
obj[feature] = {'isEnabled': isEnabled(feature, context)}
return obj
}
}
module.exports = function getAllFeatureToggles (context) {
const allFeatureToggles = config.get('feature_toggles')
return allFeatureToggles.reduce(getFeatureToggle(context), {})
}
Then we set up an end point from where we can retrieve the feature flags from the Front End:
app.get('/features', (req, res) => {
const sessionCookieName = config.get('unleash').session_cookie_name
let sessionId = req.cookies[sessionCookieName]
if (!sessionId) {
sessionId = uuidv1()
res.cookie(sessionCookieName, sessionId)
}
const context = setUnleashContext(sessionId)
res.send(getAllFeatureToggles(context))
})
Here we set the context for each user and then we call getAllFeatureToggles(context).
module.exports = function setUnleashContext (sessionId) {
const context = {
sessionId: sessionId
}
return context
}
The way we set the sessionId is interesting - we use a cookie that we send via the HTTP request. The cookie is set once for each user and then it is retrieved every time.
As we mentioned earlier this ensures that the same use will see the same feature flags every time when they are defined with the gradualRolloutSessionId strategy.
The Front End
Our application is written in React so naturally, we created a reusable component to handle each feature content based on a Feature Flag value. But first things first. We have a getFeatures() function that makes a call to the server and gets all the feature flags and their values.
/** Make a server call to get all registered feature flags. */
export async function getFeatures () {
try {
const response = await fetch('/features')
return response.json()
} catch (error) {
return {}
}
}
To make our implementation more generic and scalable we used React Context to handle our data. This way, adding a new feature flag requires minor development and is clear and straight-forward. We define FeatureFlagContext:
import { createContext } from 'react'
const FeatureFlagContext = createContext({})
export default FeatureFlagContext
Then we pass all the features to a FeatureFlagProvider component.
import { Component } from 'react'
import FeatureFlagContext from './FeatureFlagContext'
import { getFeatures } from '../util'
class FeatureFlagProvider extends Component {
constructor () {
super()
this.state = { features: {} }
}
async componentDidMount () {
const features = await getFeatures()
this.setState({ features })
}
render () {
return (
<FeatureFlagContext.Provider value={this.state.features}>
{this.props.children}
</FeatureFlagContext.Provider>
)
}
}
export default FeatureFlagProvider
... and we call it in our main App component ...
const App = () => (
<FeatureFlagProvider>
...
</FeatureFlagProvider>
)
export default App
In the end we define our FeatureFlag component which we use for each new feature.
import FeatureFlagContext from './FeatureFlagContext'
const FeatureFlag = ({ name, children, defaultChildren }) => (
<FeatureFlagContext.Consumer>
{features => {
if (features[name] && features[name].isEnabled) {
return children
}
return defaultChildren
}}
</FeatureFlagContext.Consumer>
)
FeatureFlag.defaultProps = {
defaultChildren: null
}
export default FeatureFlag
The Feature Flag component has the following parameters:
- name: The exact name of the feature flag as set in Unleash
- children: The new feature content
- defaultChildren: defaultChildren is an optional props and we use it when we have two versions of a feature and with defaultChildren we set the default(old) version, or the version for which the feature flag is disabled. This is essential for our growth experiments and A/B testing. To give an example - imagine that we have a feature that contains adding more text to a page in our application. In this case the defaultChildren is the original page with less text and the children is the same page, but with more text. When we use the gradualRolloutSesssionId in Unleash set on 50%, the feature flag is enabled to half of the users and they see the content of children and for the other 50% the flag is disabled and they see defaultChildren.
Trying it in action
To illustrate how we make use of the entire implementation as described thus far, let’s have a look at a real example.
Say we wanted to test if adding more text to a step in our funnel would improve the conversion rate of the app. We came up with the following two versions and made them both available to different cohorts of users by using a feature flag:
The first thing to do is create the feature flag in Unleash:
The version with less text is the original and we aleady had it in a component - <ProcessInfoMassage /> so we only needed to add a new React component for the new version (with more text) - <ProcessInfoMessageMoreText />.
<FeatureFlag name="claimcompass-app.eligibility-copy-more-text" defaultChildren={<ProcessInfoMessage />}>
<ProcessInfoMessageMoreText />
</FeatureFlag>
```
Having the two components and the name of the feature flag, we pass them as parameters to our <FeatureFlag> component and it is ready to use!
Conclusion
Our Growth Team’s main objective is to drive impact via rapid experimentation. Figuring out the best way to implement feature flags took some time, but combining Unleash with Mixpanel turned out to be a powerful and easy to use solution for rapid experimentation and A/B testing.