25Dec
Using Feature Flags for A/B Testing & Optionally Enable/Disable Features in Your NodeJS Project
Using Feature Flags for A/B Testing & Optionally Enable/Disable Features in Your NodeJS Project

When rolling out a new feature in production with real users, it’s often useful to be able to turn it on and off on-the-fly. Consider, for example, being part of a team that has just finished working on a new feature, submitted it to QA and has just been pushed to production.

A few days later, a user reports a bug that somehow managed to make its way past QA. It would be nice to be able to turn that new feature off so the devs can work on an emergency fix, while users continue to enjoy the old functional experience. But since no such feature exists in your project, the only way to deal with it is to rollback an old version, fix the bug, send it back to QA and push it to production.

One interesting way of making the process less cumbersome would be to roll out the new feature slowly to your users, rather than all to all of them at once. This way, only a small portion of your users is affected by any bugs that may have accidentally made it to production. Depending on how advanced your system is, the feature could even be rolled out to users depending on their country or any other metric, instead of distributing it completely randomly.

All of these can be achieved using feature flags.

What are the feature flags?

Feature flags are conditions in your code that inform other parts of the app whether a feature should be enabled or not. The kind of features that can be implemented using feature flags are only limited by your imagination. However, common use cases include:

  • A/B testing
  • Enabling and disabling features on the fly
  • Slowly rolling out new features to specific segments of your user base
  • Limiting the blast radius of uncaught bugs
  • Gradually deprecating old features

Implementing feature flags

Using environment variables

The simplest way to detect whether a specific feature has been rolled out to users is with environment variables. These are usually stored in a .env file, located at the root of your project.

Say it contains the following information:

FEATURE_NEW_LANDING_PAGE = 1

We could use that in our frontend code as follows:

If (process.env.FEATURE_NEW_LANDING_PAGE === 1){   
    <NewLandingPage/>
}else {  
    <OldLandingPage/>
}

Now, if you ever needed to stop showing the landing page, all you have to do is change the FEATURE_NEW_LANDING_PAGE environment variable to anything other than 1.

The downside to using environment variables is that you have to restart your project every time any of them changes. If a bug crept its way into your production environment, taking it down and deploying the old change would be a tedious process and beats the whole point of being able to change values on-the-fly.

Additionally, working with environment variables doesn’t scale well for a system testing out multiple features at once, in addition to using environment variables in other parts of the project.

Using Unleash

Unleash is a relatively simple CRUD interface that encapsulates the creation and management of feature flags in your project. The server portion is a simple app that needs to connect to a Postgres database underneath.

First, we need to create a database that unleash will use:

createdb unleash

Next, we need to initialize it in our project:

npm install unleash-server

Then we can create a helper to start the unleash server.

const unleash = require('unleash-server');

module.exports.start = ()=> {
    return unleash.start({
        databaseUrl: 'postgres://brad:123456789@localhost:5432/unleash',
        port: 4242,
    }).then(unleash => {
        console.log(
            `Unleash started on http://localhost:${unleash.app.get('port')}`,
        );
    });
}

The server’s job is to communicate with the database and provide usage metrics. It comes with an interface that we can use to create, archive, and monitor feature flags. To interact with it, visit http://localhost:4242. It looks like this:

Unleash server dashboard showing newly created landing page feature
Unleash server dashboard showing newly created landing page feature

Before we create our first feature toggle, we should first install and initialize the unleash client. Its job is to make regular requests to the server in order to tell us if a feature is enabled and to report usage metrics.To install it, run

npm install unleash-client --save
//or
yarn add install unleash-client

Then we can start it together with the server:

const {initialize: initializeUnleashClient} = require('unleash-client');
const {start: startUnleashServer} = require('./helpers/unleash');

startUnleashServer()
    .then(()=> {
        const client = initializeUnleashClient({
            appName: 'my-project-feature-flags',
            instanceId: 'randomInt',
            url: 'http://localhost:4242/api/',
        });
        client.on('error', (e)=> console.error(e))
    })
    .catch(err => console.error(err));

Next, we need to create a new route that browser clients will call to query whether a specific feature is enabled or not.

const express = require('express');
const router = express.Router();
const {isEnabled} = require('unleash-client');

/* GET home page. */
router.get(
    '/features/:id/:name',
    function (req, res, next) {
      const flagName = req.params.name;
      const userId = req.params.id;

      return res.send({
          flagName,
          isEnabled: isEnabled(flagName, {
              userId,
          }),
      })
    });

module.exports = router;

Normally, you would query the user’s id from their access token or cookie, but for the sake of simplicity, it’s passed as a path parameter in our example.

Let’s create a simple toggle to demonstrate how the server works.

Unleash server dashboard showing flexible rollout to 50% of users
Unleash server dashboard showing flexible roll out to 50% of users

The toggle is named ‘new-landing-page’ and its type is ‘gradualRolloutUserId’. Since we’ve selected ‘50%’, the feature will be enabled for 50% of all users. This is meant to be used incrementally – i.e. – first, it should be activated for, say, 10% of your users, then 20% and so forth, until all users have experienced it, then it can be retired.

A simple A/B experiment with Unleash and Nodejs

With all that knowledge, let’s set up a javascript app to demonstrate how a simple A/B experiment would work. The purpose of this app is to have two different pages that report their usage metrics to a third-party server. We want to know which landing page has better engagement.

Depending on the kind of data you have, the feature can be activated randomly using flexibleRollout, depending on the user’s IP address using remoteAddress or for specific users using userWithId.

We’ll pick flexibleRollout and serve the landing page to 50% of our users using userId as the sticky value. In order to simulate unique users, we’ll generate a random string on every page load and provide it to Unleash.

// generate a random Id
        function randomId(length) {
            let result           = '';
            const characters       = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
            const charactersLength = characters.length;
            for ( let i = 0; i < length; i++ ) {
                result += characters.charAt(Math.floor(Math.random() * charactersLength));
            }
            return result;
        }

        //
        function shouldShowNewLandingPage(){
            return fetch(`http://localhost:3000/features/${randomId(4)}/new-landing-page`)
                .then(response => response.json());

        }

        shouldShowNewLandingPage().then(data=> {
            console.log(data);
            if (data.isEnabled) {
                const newLandingPage = document.querySelector('#new');
                newLandingPage.style.display = "block";
            } else {
                const oldLandingPage = document.querySelector('#old');
                if (oldLandingPage) {
                    oldLandingPage.style.display = "block";
                }
            }
        })

Here’s what it looks like loading it ten consecutive times.

Feature showing random distribution on new and old landing pages to users
Feature showing random distribution on new and old landing pages to users

Note: If you don’t want to have to worry about which users you’re targeting, the gradualRolloutRandom toggle type might be better for you.

How it works
If you are curious how it works under the hood, let’s take a deeper dive. When you pass a unique session or user identifier to Unleash, it is hashed and normalized to a value between 1 and 100 using the MurmurHash3 algorithm. That number is then stored in-memory and used by the server to categorize the user into different buckets. At the end of the day, our users are going to be split evenly – 50% of  visitors will see the new landing page, and 50% of users won’t.

Should you use feature flags?

There are a few issues you should be aware of regarding feature flags before you adopt them into your project. First, adding feature flags to your project adds its own level of complexity. Where once you had to serve just one feature to all your users, now you have to serve two, three, or more. That means more code to maintain, more metrics to keep your eyes on and more bugs to track down and fix.

Secondly, your app is potentially going to be slower because there has to be an additional request made to the server before users can see the actual functionality. This and the first reason combined are the reasons why feature flags are normally used sparingly within projects – normally for experiments and new features, not to manage all the features in an application. If you want to restrict access to your app, use role-based access instead.

Additionally, feature flags also have to be monitored and retired. Unleash flags expire automatically after 40 days, for instance, but even then, you still have to edit your code in order to get rid of the flag completely.

Different teams working on the same project will need to coordinate their git branch flows in order to safely clean up flag indicators and find a way to indicate the health of different flags.

If you’re able to look past or work with these issues, feature flags provide an excellent way to manage new features, distribute them to users, minimize the blast radius of potential bugs and give us room to perform interesting experiments that otherwise wouldn’t be possible. With a bit of creativity, it’s possible to collect data and optimize features depending on any number of metrics.

Find the project on Github: https://github.com/Bradleykingz/feature-flags-nodejs/tree/master

Conclusion

We have covered a brief overview of what feature flags are, how they are commonly used, and how to implement them in a NodeJS project. We also went through a brief overview of how to use Unleash to implement a simple A/B testing flow for our project and dived a little deeper with regards to how the data is anonymized and normalized in order to ensure complete and consistent results.

Leave a Reply