Feature Flags & Server Side Rendering With AfterJS + fflip

A feature flag is a true/false value that allows you to deliver different content on a per user basis. Pairing fflip and AfterJS offers powerful new tools for your application, and reduces workload by providing greater reliability.

Brian Dinga
Apr 9, 2020
·
10 min read

Pairing fflip and AfterJS offers powerful new tools for your application, and reduces workload by providing greater reliability.

A feature flag is a true/false value that allows you to deliver different content on a per user basis. Some of the top software companies in the world leverage feature flags. It’s an open secret that Google, Facebook, and Amazon have taken advantage of feature flags within their applications. The specific implementations vary as feature flags provide an architectural layer where you can build other tools.

Combining feature flags with NodeJS and server-side rendering, you can have complete control of your content discoverability while taking advantage of the React rendering library.

TL;DR: You can leverage feature flags within isomorphic JavaScript frameworks in a variety of ways to satisfy many department goals and reduce your workload. Jump to the conclusion below for the provided example repo on GitHub.


Recent Emergence of Feature Flags

Employees of major software companies have discussed their implementation of feature flags on podcasts and in publications. Most often these implementations are through A/B split testing, but a few cases stand out.

I coded the fix, had it reviewed, and pushed it live to hundreds of millions of people, all on my first day. It was unbelievable.

- Facebook Blog Post

There are many sources that echo Facebook employee’s experiences where they push code on day one thanks to branchless development. This development process has been made possible through their implementation of Gatekeeper.

Facebook, Google, and Flickr have embraced branchless development which some people consider rather bold.

Flickr is somewhat unique in that it uses a code repository with no branches

Flipping Out - Flickr Blog Post

Flickr’s implementation of Flippers follows this inventive branching methodology, but if you want to get serious about dogfooding, beta testing or testing in production it is an innovation you must adopt.

Analytics and data drives many modern companies—this makes feature flags especially desirable. Most organization-driven goals related to feature flags are complex integrations with income and marketing goals—specifically, split testing.

If there is an identified need for split testing, this could be an opportunity for holistic integration of engineering (front-to-back) and company stakeholder goals, providing a valuable tool for everyone involved.

There is a great primer at featureflags.io. Give their best practices and use cases a read.  There are feature flag service providers RollOut and LaunchDarkly (affiliated with this guide) that have built multi-platform SDKs, which are great if you have a multi-platform product. If you’re not an isomorphic JavaScript stack I encourage you to research their products.


More is More Risk

Adding logic and conditional statements to your application adds more risk. There are other types of risks, though, that are often overlooked because the costs are more human and less tangible.

Rollbacks alone introduce risks that feature flags can help manage:

  • Overtime or late night support calls are often stressful or rushed

  • Perfect context is required on very large codebases

  • Rollbacks can be harmful especially when there is no rollback plan

  • Bus factor interferes, the change in availability of a key player can be disastrous

Martin Fowler’s blog post on feature flags (which some refer to as “feature toggles”) gives a great overview on risk trade-offs and concepts important to integrating feature flags. Feature flags can be an engineering tool like CI/CD, which leads to more reliable releases. While code linting and CI/CD also add risk, we don’t consider them a burden as their benefits offset these drawbacks.

Feature flags can reduce your overall workload:

  • Calendar-driven launches remove the need to push code at midnight

  • Dogfooding allows you to test in production thereby reducing last minute surprises

  • Temporary features/campaigns can be removed, archived, and/or scaled with minimum effort

The documentation and management aspect of feature flags is important. Documenting the feature flags enables teams to collaborate, determine dependencies and reduce bug report diagnoses times. These are best practices throughout application development.


Why fflip

The fflip npm package is great!

  • Simple functionality with synchronous APIs while allowing for asynchronous integration

  • Agent agnostic runs on the frontend and backend

  • Context and criteria is managed through configuration

  • No subscription fee so it's free, whereas feature flag providers do not offer free tiers

You could easily write your own feature flag library with a collection of boolean flags, but you’ll miss out on the rich functionality that the concepts of criteria and context bring to the table, and its function signatures.

Implementing logic for a flag only requires access to user context data and importing the library. The feature flag complexity is managed through configuration, but you can easily expand feature suite dependencies through your own logic.


Why Razzle & AfterJS

What stands out when working with Razzle/AfterJS:

  • More performant (anecdotally) on small to medium-sized projects than other SSRs I’ve worked with

  • Decoupled

  • Less opinionated, which allows it to be more universal

  • Leverages Webpack more, which makes it like an ejected Create React App

  • Routing configuration is shared

  • Pure client-side applications can still work with libraries like React Router DOM

Overall, I also like how coding with Razzle is really easy to troubleshoot as most issues come down to troubleshooting Webpack (through razzle.config.js), ExpressJS, and React.  The layer between React and ExpressJS is very transparent. Your app will only be limited by your Webpack configuration.

As you can see, the advantage of fflip being agnostic and the layer between Webpack and Razzle being so thin is that you can easily associate the two, which gives you a high level of control over your application.

There are many great resources on SSRs and dynamic rendering. I recommend Trello’s breakdown of their case study and these two resources from Google’s Search with their considerations around rendering as a great primer.

Hell, there are a lot of NextJS articles out there!


Let’s get started!

Be sure to follow the example provided at the Razzle Github Repo. At the time of writing, it’s the following:

curl https://codeload.github.com/jaredpalmer/razzle/tar.gz/master | tar -xz --strip=2 razzle-master/examples/with-afterjs

cd with-afterjs

npm install

Add After.js and the Razzle plugin for SASS:

npm install --save @jaredpalmer/after razzle-plugin-scss

Then add a razzle.config.js to enable the SASS Razzle plugin:

module.exports = {
  plugins: ['scss']
};

I’ve restructured my src/ directory as follows (using a simplified tree src/):

src/
├── client
│   ├── hydrate.js
│   └── index.js
├── server
│   ├── Document.js
│   ├── api
│   ├── models
│   └── utils
└── shared
    ├── assets
    ├── components
    │   ├── Footer
    │   ├── Header
    │   ├── Nav
    │   └── SiteMain
    ├── hoc
    ├── sections
    │   ├── About
    │   └── Home
    ├── staticData
    └── utils

Install any of your preferred npm packages.  

We’re ready to test our hello world. Start Razzle through npm:

npm run start

You should be able to see your code running at http://localhost:3000 now!


Custom Document

Like NextJS, you have access to getInitialProps(). It must be a static method that returns a helmet and HTML properties (provided by renderPage()), or AfterJS will not be able to render. This returned object provides required props helmet and html into the render method.  Omitting assets or data won’t lead to fatal crashes but will lead to bugs at build and render time.

// src/server/Document.js
static async getInitialProps(ctx) {
const { assets, preJS, data, renderPage } = ctx;
const { html, helmet } = await renderPage();

  return {
    assets,
    preJS,
    data,
    // these props must be present
    helmet,
    html
  };
}

Unlike NextJS you must setup your own custom document to embed the React application. The template in the setup step does provide you with an example to build from:

// src/server/Document.js
render() {
  const { helmet, assets, preJS, data } = this.props;
  // get attributes from React Helmet
  const htmlAttrs = helmet.htmlAttributes.toComponent();
  const bodyAttrs = helmet.bodyAttributes.toComponent();

  return (
    <html lang="en-us" { ...htmlAttrs }>
      <head>
        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        { helmet.meta.toComponent() }
        { helmet.title.toComponent() }
        { assets.client.css && <link rel="stylesheet" href={assets.client.css} /> }
        { helmet.link.toComponent() }
      </head>
      <body {...bodyAttrs}>
        <AfterRoot />
        <AfterData data={data}/>
        <script type="text/javascript" src={assets.client.js} defer crossOrigin="anonymous" />
      </body>
    </html>
  );
}

Only page-level components will have getInitalProps available to their lifecycle. This lifecycle method is called client-side and backend for components embedded from the routes configuration (shared client and server-side routes)

// src/routes.js
import { asyncComponent } from '@jaredpalmer/after';

export default [
  // ...
  {
    path: '/about',
    exact: true,
    component: asyncComponent({
      // import is required -> returns promise
      loader: () => import('./shared/sections/About)
    })
  },
  // ...];

Generally, it’s best to use the route configuration to handle all navigation.

Follow the link in the repo (below) if you want to follow these changes in detail.


Set Up fflip

First install the package:

npm install --save fflip

A baseline configuration is needed. Specifically a features and criteria property that contains a list of objects:

{
  criteria: [],
  features: []
}

A features object has two important properties: id and enabled.  The enabled key is a boolean (true/false) value that indicates whether the flag is usable. This is a global on/off switch for that specific feature.

{
  criteria: [],
  features: [
    { id: 'flagNameA', enabled: true }
  ]
}

This portion of the configuration could be managed through a JSON file (which we’ll do later), but the criteria portion will require functions that cannot be serialized with JSON.

If you only wanted basic toggle flags you could stop here. For a more sophisticated implementation you would want to leverage user or session data which will require a criteria property into the configuration which is otherwise optional.

The property check will provide a user context (discussed below) and the configured expectations from features configuration property.

{
  criteria: [
    { id: 'barCriteria', check: (user, barCriteria) => {} }
  ],
  features: [ ... ]
}

However, if enabled is absent and the criteria property is added, the additional property will be used to determine whether or not the flag is enabled.

{
  criteria: [{
    id: 'fooCriteriaOver',
    check: ({ foo }, fooCriteriaOver) => foo > fooCriteriaOver
  }],
  features: [
    { id: 'flagNameB', criteria: { fooCriteriaOver: 9000 }}
  ]
}

Refer to the documentation for the specifics of the criteria property as it can be an Array or an Object. Briefly: an Array requires a single criteria to be true to enable a flag, whereas an Object requires all to be true.

Once you have built your configuration object you can pass it into the fflip.config:

import fflip from 'fflip';

fflip.config({
  criteria: [ ... ],
  features: [ ... ]});

Now you are ready to use feature flags in your JavaScript application!


Integrating a Basic Feature Flag

To simply add a feature you can use either isFeatureEnabledForUser or getFeaturesForUser. I generally prefer to run through isFeatureEnabledForUser but it's best to be consistent whatever the approach.

In an instance where the flag flagNameA is enabled it can be resolved in the following manner

let ContentItem = null;
if (fflip.isFeatureEnabledForUser('flagNameA', userFlags)) {
  ContentItem = <HiddenComponent />;
}

Or, if you wish to use a contextual-driven approach, the getFeaturesForUser method might be preferred;

const { flagNameA } = fflip.getFeaturesForUser(userFlags);
const ContentItem = flagNameA ? <NewComponent /> : <CurrentComponent />;

Those functions require that you provide an object that represents users’ context, however, you can pass an empty object.


Introducing User Context

The user context should be derived from persistent storage such as a session. Somewhere in your implementation you’ll need to set up a provider for the user context. This will need to be one of your last app-level Express middlewares.

app.use((req, res, next) => {
  const userFlags = {
    flagNameA: req.session.isReferalCookie,
    flagNameB: req.session.powerLevel
  };

  res.locals.userFlags = userFlags;
  next();
});

Late binded route handlers and middlewares will use the Express local variables. Then in the portions where your retrieve data you can introduce logic where the feature flag can be integrated into your application code.

const { userFlags } = res.locals;
if (fflip.isFeatureEnabledForUser('flagNameA', userFlags)) {
  // ...
}

Be security minded: keep your feature flags entirely off the client/browser. While flags can be driven by cookies or tracking codes, the user context should be driven by your session store or otherwise securely managed on a backend.

That's the core concepts of a user contextual fflip implementation!


DIY Split Test

In most cases any logic for feature flags should be kept on the backend. However, in some cases you can’t avoid exposing feature flag logic on the client-side such as implementing a split test with a frontend only analytics platform. Since we want the split test to appear onscreen smoothly we’ll introduce our user flags at render time.

// src/server/Document.js
import serialize from 'serialize-javascript';

<head>
   <script type="text/javascript" dangerouslySetInnerHTML={{
     __html: `window.env = ${serialize(preJS)};`
   }} />
<head>

This isn't best practice but it suits our simplified use case.

The most important aspect of a split test is the collection of data and tracking the users’ behaviour. For our example we’ll use Google Analytics with a custom dimension.

First you’ll need to add your analytics code on the client side (following instructions provided by Google Analytics):

// src/server/Document.js
<body>
  <script async src="https://www.googletagmanager.com/gtag/js?id={process.env.GA_UA_ID}"></script>
  <script dangerouslySetInnerHTML={{ __html: `
    window.dataLayer = window.dataLayer || [];
    function gtag(){
      window.dataLayer.push(arguments);
    };
    gtag('js', new Date(${new Date().getTime()}));

    gtag('config', '${process.env.GA_UA_ID}', {
        dimension3: 'promoA',
        dimension4: 'promoB'
    });
  `}}>
  </script>
</body>

Earlier we configured our analytics to use a custom dimension tethered to the analytics ‘Session’ but you can track within the ‘Hit’ or ‘User’ scopes depending on your campaign’s requirement.

Once our analytics are set up and the feature flags are accessible for our test we can set up a component to contain our split test.  We’ll assume at some point we’ll call our feature flags promoSplitA and promoSplitB which will appear in our features portion of our configuration.

// src/shared/components/RibbonSplitTest/index.js
const RibbonSplitTest = () => {
  // ...
  const isRibbonA = fflip.isFeatureEnabledForUser('promoSplitA', userFlags);
  const isRibbonB = fflip.isFeatureEnabledForUser('promoSplitB', userFlags);
  
  // safely handle an unexpected event
  if (!isRibbonA && !isRibbonB) {
    return null;
  } else if (isRibbonA) {
    gtag('event', 'page_view', { promoA: 'VIEWED' });
    className = 'corner-ribbon--promo-style-a';
    headingText = 'New Inventory';
  } else {
    gtag('event', 'page_view', { promoB: 'VIEWED' });
    className = 'corner-ribbon--promo-style-b';
    headingText = 'Limited Time Offer';
  }

  return (<Ribbon className={className}>
    <Link to="/store" onClick={onClickHandler}>
      <h2>{headingText}</h2>
    </Link>
  </Ribbon>);
};

Add tracking to onClickHandler to determine the ratio of views to clicks:

const onClickHandler = () => {
  if (typeof window !== 'undefined' && typeof gtag === 'function') {
    const { userFlags } = window.env;
    if (fflip.isFeatureEnabledForUser('promoSplitA', userFlags)) {
      gtag('event', 'page_view', { promoA: 'CLICKED' });
      return;
    }
    gtag('event', 'page_view', { promoB: 'CLICKED' });
  }
};

Now alter the features portion of the configuration to include promoSplitA and promoSplitB flags:

fflip.config({
  criteria: [ ... ],
  features: [
    ...
    {
      id: 'promoSplitA',
      criteria: { splitVersion: 1 }
    },
    {
      id: 'promoSplitB',
      criteria: { splitVersion: 2 }
    }
  ]};

As you can see we’ve introduced a new criteria which will need to be reflected in the fflip.config initialization:

fflip.config({
  criteria: [
    ...
    {
      id: 'splitVersion',
      check: (user, splitVersion) => user.splitVersion === splitVersion
    }
  ],
  features: [ ... ]};

Lastly expand the handler to determine which portion of the test to show—either ‘A’ or ‘B’:

app.use((req, res, next) => {
  const splitVersion = req.session.splitVersion;
  req.session.splitVersion = typeof splitVersion !== 'number'
    ? getRandomNumber(1, 2)
    : splitVersion;
    
  const userFlags = {
    ...
    splitVersion: req.session.splitVersion
  };

  res.locals.userFlags = userFlags;
  next();
});

Now you are able to run a split A/B test on your website.

If you are setting up the example repo provided below be sure to visit http://localhost:3000/referral/offer/9f427f50-45f9-11ea-a85e-a34337e74359 to activate the split test.


Console.warn

Any new layer built into software architecture introduces risk. This includes the chance of configuration not loading, which should be a particular consideration for split tests because a broken configuration or a type error can lead to skewed numbers.

There are a few things you can do to lessen any conflicts within the code itself:

  • Prefer if-else paradigm as even returning a null will make it clear that if a feature flag is to be deprecated it can be safely removed

  • Validate the feature flag from an API if feature flag logic must be exposed on the frontend

  • Maintenance is important so remove unused flags, migrate permanent features into the codebase, and simplify co-dependant features over time. Dead code, unused configuration and assets don’t benefit anyone

  • Consider feature flags fully integrated into your application and not an addon—include them in your tests, especially integration tests

Ideally the feature flag logic should be removed through compilation, be sure to look into your Webpack configuration.

You may need to have logic on the frontend: it’s possible to trick the browser or native app to reveal portions of your app that weren’t intended for that user.

Be sure that any flags on the frontend are immutable and sparingly used. Within a React context, keep feature sets off the component lifecycle (props, useState, setState, context, Redux stores) as they can be tampered with.


Conclusion

After.js/Razzle gives you a lot of flexibility in project setup and fflip can be a powerful tool in managing content controls on your website or application. While this article focused on a fflip, React, AfterJS SSR stack, I hope that you consider any implementation of these technologies that suits your technology needs.

Feature flags bring a new toolset to your technical team. While this guide focused on A/B split testing you should consider a feature flag suite as an ingredient to build out other tools. These tools can make managing releases much easier or reduce your workload by automating calendar-driven releases.

Like CI/CD, consider how this can reduce your own workload alongside other tools available to your team.

A complete example is available on GitHub so feel free to check it out!

written by
Logo of QuantumMob

We are a Toronto-based end-to-end digital innovation firm with a passion for building beautiful & functional products that deliver results.

The image of hire us
You might also like