Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions five-minutes-to-feature-flags/01-vanilla.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
We're starting off with a "Hello, World" server, stored in `01_vanilla.js`. You can use the Editor tab (up at the top left of the terminal on the right) to view that file, or just click on the command below
to print the file out to the terminal:

```
cat ~/app/01_vanilla.js
```{{exec}}

*(yes, you can literally click that command above to have it run live in the terminal on the right!)*

This is pretty much the most basic express server you can imagine - a single endpoint at `/`{{}} that returns a plaintext `"Hello, world!"`{{}} response.

Start the server by clicking the command below:
```
node ~/app/01_vanilla.js
```{{exec}}

You should be informed of a `Server running on port 3333`.

Now you can [visit this actual running server in a browser]({{TRAFFIC_HOST1_3333}}) to see its output.

Alternatively you can be old-school and test its responses from the command line. Open a new terminal Tab (click `+`{{}} next to `Tab 1`{{}}) then click the following command:

```
curl http://localhost:3333
```{{exec}}

Either way, you should see a very vanilla `Hello, World!` response.

Let's see if we can make that response a bit more exciting...
82 changes: 82 additions & 0 deletions five-minutes-to-feature-flags/02-cowsay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
Let's imagine that we're adding a new, experimental feature to this hello world service. We're going to upgrade the format of the server's response, using [cowsay](https://www.npmjs.com/package/cowsay)!

However, we're not 100% sure that this cowsay formatting is going to work out, so for now we'll protect it behind a conditional. We've made this change in a new copy of the server, `app/02_basic_flags.js`:

```javascript
import 'cowsay'
...
routes.get('/', async (req, res) => {
// set this to true to test our new
// cow-based greeting system
const withCow = false
if(withCow){
res.send(cowsay.say({text:'Hello, world!'}))
}else{
res.send("Hello, world!")
}
})
```{{}}

By default our service continues to work exactly as it did before, but if we change `withCow` to `true`, our new formatting will come to life.

Let's run this new version of the server. Flick back to tab 1 and try out the new code:

```
node ~/app/02_basic_flags.js
```{{exec interrupt}}

Back to tab 2 to re-curl the server (or [load it in the browser]({{TRAFFIC_HOST1_3333}})):

```
curl http://localhost:3333
```{{exec}}

Don't be surprised if the output looks the same as before - `withCow` is still set to `false` in that file, so it should be returning the same response as before.
However, if we now update it to `true` then the format should change.

Give it a try! Open the server code (`app/02_basic_flags.js`) in the IDE (that `Editor` tab at the top left of the terminal to the right) and set `withCow` to true:

```javascript{4}
routes.get('/', async (req, res) => {
// set this to true to test our new
// cow-based greeting system
const withCow = true
if(withCow){
res.send(cowsay.say({text:'Hello, world!'}))
}else{
res.send("Hello, world!")
}
})
```

Now flip back to `Tab 1` and restart the server:
```
node ~/app/02_basic_flags.js
```{{exec interrupt}}


Finally, check [our server's response]({{TRAFFIC_HOST1_3333}})

```
curl http://localhost:3333
```{{exec}}

it should look a bit more exciting:

```
_______________
< Hello, world! >
---------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
```{{}}

Beautiful.

# The Crudest Flag
That `withCow`{{}} boolean and its accompanying conditional check is a very basic implementation of a *Feature Flag*. It lets us hide an experimental or unfinished feature, but also easily switch the feature on while we're building and testing it.

But managing these flags by changing hardcoded constants gets old pretty fast. Teams that uses feature flags in any significant way soon reach for a feature flagging framework. We'll take a confident step in that direction next, by setting up the [OpenFeature](https://openfeature.dev) SDK...
48 changes: 48 additions & 0 deletions five-minutes-to-feature-flags/03-intro-of.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[OpenFeature](https://openfeature.dev/) is an open standard that lets us add feature flagging capabilities to our service in a vendor-neutral way. You can read more about the benefits of this approach [here](https://docs.openfeature.dev/blog/openfeature-a-standard-for-feature-flagging), but for now let's get our Hello, World service set up with OpenFeature - it shouldn't take more than a minute or two.

We'll create a new version of our server (`app/03_openfeature.js`) which looks something like this:

```javascript{6}
import { OpenFeature } from '@openfeature/js-sdk'
// ...
const featureFlags = OpenFeature.getClient()
// ...
routes.get('/', async (req, res) => {
const withCows = await featureFlags.getBooleanValue('with-cows', false)
if(withCows){
res.send(cowsay.say({text:'Hello, world!'}))
}else{
res.send("Hello, world!")
}
})
```

We're importing the `@openfeature/js-sdk`{{}} npm module, and using it to create an OpenFeature client called `featureFlags`{{}}. We then call `getBooleanValue`{{}} to find out if the `with-cows` feature flag is `true` or `false`. Finally, we show either the new cow-based output or the traditional plaintext format depending on whether `withCows` is true or false, just as we did before.

The big difference is that rather than using a hard-coded conditional we're now asking for the state of the flag dynamically, at runtime, using `getBooleanValue()`.

Let's try this new server out. Head back to Tab 1 and run it:

```
node ~/app/03_openfeature.js
```{{exec interrupt}}

Over in tab 2 we can re-curl the server (or [load it in the browser]({{TRAFFIC_HOST1_3333}})):

```
curl http://localhost:3333
```{{exec}}

and we should be back to getting a vanilla `Hello, World!` response. Why is that?

Well, the OpenFeature SDK doesn't provide feature flagging capabilities by itself. We have to configure it with a "[provider](https://docs.openfeature.dev/docs/specification/glossary/#provider)" which connects the SDK to a feature flagging implementation which can actually make the flagging decisions we need. (You can read more about OpenFeature's architecture [here](https://docs.openfeature.dev/docs/reference/intro#what-is-openfeature).)

Since we haven't configured the SDK with a provider it has no way of making feature flagging decisions and will just return default values. In this case, `with-cows` is defaulting to `false`, so now we don't see any cows in our output.

Let's fix that by configuring the SDK with a feature flag provider!

## Configuring OpenFeature

If this was a fancy production-grade system we'd probably want to connect the OpenFeature SDK to a full-fledged feature flagging system - a commercial product such as LaunchDarkly or Split, an open-source system like [FlagD](https://github.com/open-feature/flagd), or perhaps a custom internal system - so that it can provide flagging decisions from that system.

Connecting OpenFeature to one of these backends is very straightforward, but it does require us to get a backend set up and ready to go. Instead, just to get us started, we'll use a super-simple flag provider that doesn't need a backend.
76 changes: 76 additions & 0 deletions five-minutes-to-feature-flags/04-minimal-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
A new version of our server, `04_openfeature_with_provider.js`, shows how to plug the OpenFeature SDK into a really simple flag provider. Here's the relevant part of that file:

```
import { InMemoryProvider } from '@openfeature/in-memory-provider'


const FLAG_CONFIGURATION = {
'with-cows': true
}

const featureFlagProvider = new InMemoryProvider(FLAG_CONFIGURATION)

OpenFeature.setProvider(featureFlagProvider)
const featureFlags = OpenFeature.getClient()
```{{}}

This [in-memory provider](https://www.npmjs.com/package/@openfeature/in-memory-provider) is the most basic feature flagging provider you could imagine. You configure it with hard-coded set of feature flag values, and it provides those values via the OpenFeature SDK.

In our `FLAG_CONFIGURATION`{{}} above we've hard-coded that `with-cows`{{}} feature flag to `true`{{}}, causing that conditional predicate in our express app to now evaluate to true, which in turn means that our service should now start providing bovine output. Let's check!

First, we'll start up this version of the server. Head back to Tab 1 and launch it:

```
node ~/app/04_openfeature_with_provider.js
```{{exec interrupt}}

Now in tab 2 we can re-curl the server (or [load it in the browser]({{TRAFFIC_HOST1_3333}})):

```
curl http://localhost:3333
```{{exec}}

The output should look like this:

```
_______________
< Hello, world! >
---------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
```{{}}

Our feature flagging system is working! Our server asks OpenFeature for the state of the `with-cows` flag. The OpenFeature SDK passes this request on to the configured provider which returns a value of `true`, enabling the flag in the server and producing the new and improved formatting.

# Flipping a Flag

We can double-check that our feature flag is having an effect by updating the provider's flag configuration. Open up `04_openfeature_with_provider.js` in the Editor tab, and update the configuration to turn off the `with-cows` feature:

```javascript{3}
const featureFlags = OpenFeature.getClient()
const FLAG_CONFIGURATION = {
'with-cows': false
}

const featureFlagProvider = new InMemoryProvider(FLAG_CONFIGURATION)
```

Now hop over to tab 1 and re-start the server:

```
node ~/app/04_openfeature_with_provider.js
```{{exec interrupt}}

and over in tab 2 reload the response (or [load it in the browser]({{TRAFFIC_HOST1_3333}})):

```
curl http://localhost:3333
```{{exec}}

You should see that we're back to the cow-less, vanilla `Hello, World!` response. Feature flags in action!

This is all very exciting, but we're still having to make changes in source code to control that feature flag, due to the limitation of
this extremely basic in-memory provider. Next we'll look at upgrading to a *real* feature flagging backend...
44 changes: 44 additions & 0 deletions five-minutes-to-feature-flags/05-more-providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
We've gotten started with OpenFeature using a very simple but extremely limited provider.

The beauty of OpenFeature is that we can transition to a real feature-flagging system when we're ready, without any change to how we evaluate flags.

Once you have the flagging system up and runnig, integrating it into this service is as simple as configuring a different provider.

That next step is left as an exercise to you, dear reader. Documentation on the OpenFeature providers available to you is [here](https://docs.openfeature.dev/docs/reference/technologies/server/javascript), but here's a cheat sheet to illustrate how straightforward it is to switch over.

### LaunchDarkly
```
import { init } from 'launchdarkly-node-server-sdk';
import { LaunchDarklyProvider } from '@launchdarkly/openfeature-node-server';

const ldClient = init('[YOUR-SDK-KEY]');
await ldClient.waitForInitialization();
OpenFeature.setProvider(new LaunchDarklyProvider(ldClient));
```

### flagd
```
import { FlagdProvider } from '@openfeature/flagd-provider';

OpenFeature.setProvider(new FlagdProvider({
Comment thread
beeme1mr marked this conversation as resolved.
host: '[FLAGD_HOST]',
port: 8013,
}))
```

### Split
```
import { SplitFactory } from '@splitsoftware/splitio';
import { OpenFeatureSplitProvider } from '@splitsoftware/openfeature-js-split-provider';

const splitClient = SplitFactory({core: {authorizationKey:'[YOUR_AUTH_KEY]'}}).client();
OpenFeature.setProvider(new OpenFeatureSplitProvider({splitClient}));
```

### CloudBees
```
import {CloudbeesProvider} from 'cloudbees-openfeature-provider-node'

const appKey = '[YOUR_APP_KEY]'
OpenFeature.setProvider(await CloudbeesProvider.build(appKey));
```
1 change: 1 addition & 0 deletions five-minutes-to-feature-flags/assets/.curlrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-w "\n"
1 change: 1 addition & 0 deletions five-minutes-to-feature-flags/assets/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
15 changes: 15 additions & 0 deletions five-minutes-to-feature-flags/assets/01_vanilla.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import express from 'express'
import Router from 'express-promise-router'

const app = express()
const routes = Router()
app.use(routes);

routes.get('/', async (req, res) => {
res.send("Hello, world!\n")
})

app.listen(3333, () => {
console.log("Server running on port 3333")
})

27 changes: 27 additions & 0 deletions five-minutes-to-feature-flags/assets/02_basic_flags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import express from 'express'
import Router from 'express-promise-router'
import cowsay from 'cowsay'

const app = express()
app.use(function (req, res, next) {
res.setHeader('content-type', 'text/plain')
next()
});

const routes = Router()
app.use(routes);

routes.get('/', async (req, res) => {
// set this to true to test our new
// cow-based greeting system
const withCow = false
if(withCow){
res.send(cowsay.say({text:'Hello, world!'}))
}else{
res.send("Hello, world!")
}
})

app.listen(3333, () => {
console.log("Server running on port 3333")
})
27 changes: 27 additions & 0 deletions five-minutes-to-feature-flags/assets/03_openfeature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import express from 'express'
import Router from 'express-promise-router'
import cowsay from 'cowsay'
import { OpenFeature } from '@openfeature/js-sdk'

const app = express()
app.use(function (req, res, next) {
res.setHeader('content-type', 'text/plain')
next()
});
const routes = Router();
app.use(routes);

const featureFlags = OpenFeature.getClient()

routes.get('/', async (req, res) => {
const withCows = await featureFlags.getBooleanValue('with-cows', false)
if(withCows){
res.send(cowsay.say({text:'Hello, world!'}))
}else{
res.send("Hello, world!")
}
})

app.listen(3333, () => {
console.log("Server running on port 3333")
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import express from 'express'
import Router from 'express-promise-router'
import cowsay from 'cowsay'
import { OpenFeature } from '@openfeature/js-sdk'
import { InMemoryProvider } from '@openfeature/in-memory-provider'


const app = express()
app.use(function (req, res, next) {
res.setHeader('content-type', 'text/plain')
next()
});
const routes = Router();
app.use(routes);

const featureFlags = OpenFeature.getClient()
const FLAG_CONFIGURATION = {
'with-cows': true
}

const featureFlagProvider = new InMemoryProvider(FLAG_CONFIGURATION)

OpenFeature.setProvider(featureFlagProvider)

routes.get('/', async (req, res) => {
const withCows = await featureFlags.getBooleanValue('with-cows', false)
if(withCows){
res.send(cowsay.say({text:'Hello, world!'}))
}else{
res.send("Hello, world!")
}
})

app.listen(3333, () => {
console.log("Server running on port 3333")
})
Loading