Rolling out a feature with a flag

Our recommended way to release a feature behind a flag. Ship it at 0% and check it with a query parameter, raise the share step by step, then remove the flag from your code.

4 min read

This is how we release features on our own sites, and the flow we recommend for any feature flag. It has three steps:

  1. Release at 0%. The code is live, nobody sees the feature, and a few people check it with a query parameter.
  2. Roll out in steps. Raise the share from 0% to 100%, watching your events and errors after each step.
  3. Remove the flag. Once the feature is at 100%, take the flag out of your code and archive it in Improve.

Every step can be undone without a deploy, until the last one.

Set up the flag

Create a flag with two options. The first option is the one people get when nothing else applies: a visitor outside the flag's audience, or a visitor the SDK can't identify. Make that the current behaviour.

OptionSlugSplit
Offoff100
Onon0

Set the flag to Active. A draft flag isn't in the datafile, so your code reads it as missing.

In your code, show the feature only for on, and treat anything else (off, a missing flag, null) as off. A misspelled slug or an unreachable datafile then hides the feature instead of showing it.

const showNewCart = improve.getFlagValue('new-cart') === 'on'

1. Release at 0% and check it with a query parameter

At 0% the code ships without anyone seeing the feature. To check it in production, read a query parameter named after the flag before you read the flag: ?new-cart=on.

With the client SDK, the simplest way is to write the parameter into the flag's cookie. The SDK keeps a visitor's option in a cookie named after the flag, and it trusts any cookie that holds one of the flag's options. The person keeps seeing the feature while they click through the site.

const FLAG = 'new-cart'

// Before the first getFlagValue: a valid option in the URL becomes this
// visitor's option.
const forced = new URLSearchParams(window.location.search).get(FLAG)
const options = improve.config?.flags[FLAG]?.options ?? []
if (forced && options.some((option) => option.slug === forced)) {
	document.cookie = `${FLAG}=${forced}; path=/; max-age=${60 * 60 * 24 * 30}; samesite=lax; secure`
}

const showNewCart = improve.getFlagValue(FLAG) === 'on'

?new-cart=off puts the same person back. On the server, check the parameter before calling getFlagValue and use its value instead.

A few things to keep in mind:

  • Only accept the flag's own options. Ignore any other value instead of passing it on.
  • It is not access control. Anyone who has the link sees the feature. Don't put anything behind a flag that must stay secret.
  • Keep previews out of search engines. When a whole page only exists behind the flag, add <meta name="robots" content="noindex"> when it is shown through the parameter.
  • Previews count. A forced visitor's exposure and events are recorded like anyone else's, so a handful of people checking the feature show up in its numbers.

2. Roll out in steps

Raise the split of on in steps, for example 5%, 25%, 50% and 100%. After each step, look at the events and errors you care about before taking the next one. A change reaches your site within about a minute and a half (the datafile's edge cache), without a deploy.

If something is wrong, set the flag back to Draft. It leaves the datafile, your code reads it as missing, and everyone gets the current behaviour again, including the people who already had on.

Watch out

A visitor keeps the option they were given. The SDK stores it in the flag's cookie and trusts it on every later visit, so raising the share only changes what new visitors get. Someone who got off at 5% still gets off at 50%, and setting on back to 0% doesn't take the feature away from people who already got it. To change it for everyone at once, remove the option from the flag (a cookie that holds an option the flag no longer has is ignored) or set the flag to Draft.

At 100%, remove the off option, so returning visitors who got off earlier switch over as well.

3. Remove the flag

A flag at 100% is finished work. Take it out of your code so the next person doesn't have to wonder whether off still matters:

  1. Find every place that reads the flag. Mark them when you add the flag, so this is one search. We put a comment with the flag's slug next to each one, like // FLAG(new-cart): …, and say in it what the code becomes without the flag.
  2. Keep the on path and delete the rest: the check, the off path and the query parameter.
  3. Deploy, then set the flag to Archived in Improve.

Archive the flag only after the code that reads it is gone. Code that still reads an archived flag gets null, which is the off path again.

On this page

Marks in the margin

  • Note
  • Watch out
  • Updated

Scribes marked doubtful lines in the margin. That practice is called obelism.