From 585cea585486e6cbbc2c91d6e44fa32319107464 Mon Sep 17 00:00:00 2001 From: agardnerit Date: Wed, 8 Mar 2023 10:05:58 +1000 Subject: [PATCH 1/3] feat: Five Minutes to Feature Flags Tutorial Signed-off-by: agardnerit --- .../assets/01_vanilla.js | 14 +++ .../assets/02_basic_flags.js | 22 ++++ .../assets/03_openfeature.js | 23 ++++ .../assets/04_openfeature_with_provider.js | 31 +++++ .../assets/scripts/intro_foreground.sh | 52 +++++++++ five-minutes-to-feature-flags/finish.md | 11 ++ five-minutes-to-feature-flags/index.json | 37 ++++++ five-minutes-to-feature-flags/intro.md | 11 ++ five-minutes-to-feature-flags/step1.md | 90 ++++++++++++++ five-minutes-to-feature-flags/step2.md | 110 ++++++++++++++++++ five-minutes-to-feature-flags/step3.md | 42 +++++++ 11 files changed, 443 insertions(+) create mode 100644 five-minutes-to-feature-flags/assets/01_vanilla.js create mode 100644 five-minutes-to-feature-flags/assets/02_basic_flags.js create mode 100644 five-minutes-to-feature-flags/assets/03_openfeature.js create mode 100644 five-minutes-to-feature-flags/assets/04_openfeature_with_provider.js create mode 100644 five-minutes-to-feature-flags/assets/scripts/intro_foreground.sh create mode 100644 five-minutes-to-feature-flags/finish.md create mode 100644 five-minutes-to-feature-flags/index.json create mode 100644 five-minutes-to-feature-flags/intro.md create mode 100644 five-minutes-to-feature-flags/step1.md create mode 100644 five-minutes-to-feature-flags/step2.md create mode 100644 five-minutes-to-feature-flags/step3.md diff --git a/five-minutes-to-feature-flags/assets/01_vanilla.js b/five-minutes-to-feature-flags/assets/01_vanilla.js new file mode 100644 index 0000000..cb4f641 --- /dev/null +++ b/five-minutes-to-feature-flags/assets/01_vanilla.js @@ -0,0 +1,14 @@ +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!") +}) + +app.listen(3333) + +console.log("Server running on port 3333") \ No newline at end of file diff --git a/five-minutes-to-feature-flags/assets/02_basic_flags.js b/five-minutes-to-feature-flags/assets/02_basic_flags.js new file mode 100644 index 0000000..85bb52b --- /dev/null +++ b/five-minutes-to-feature-flags/assets/02_basic_flags.js @@ -0,0 +1,22 @@ +import express from 'express' +import Router from 'express-promise-router' +import cowsay from 'cowsay' + +const app = express() +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") \ No newline at end of file diff --git a/five-minutes-to-feature-flags/assets/03_openfeature.js b/five-minutes-to-feature-flags/assets/03_openfeature.js new file mode 100644 index 0000000..3c0be0d --- /dev/null +++ b/five-minutes-to-feature-flags/assets/03_openfeature.js @@ -0,0 +1,23 @@ +import express from 'express' +import Router from 'express-promise-router' +import cowsay from 'cowsay' +import { OpenFeature } from '@openfeature/js-sdk' + +const app = express() +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") \ No newline at end of file diff --git a/five-minutes-to-feature-flags/assets/04_openfeature_with_provider.js b/five-minutes-to-feature-flags/assets/04_openfeature_with_provider.js new file mode 100644 index 0000000..ef4bb65 --- /dev/null +++ b/five-minutes-to-feature-flags/assets/04_openfeature_with_provider.js @@ -0,0 +1,31 @@ +import express from 'express' +import Router from 'express-promise-router' +import cowsay from 'cowsay' +import { OpenFeature } from '@openfeature/js-sdk' +import { MinimalistProvider } from '@moredip/openfeature-minimalist-provider' + +const app = express() +const routes = Router(); +app.use(routes); + +const featureFlags = OpenFeature.getClient() +const FLAG_CONFIGURATION = { + 'with-cows': true +} + +const featureFlagProvider = new MinimalistProvider(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") \ No newline at end of file diff --git a/five-minutes-to-feature-flags/assets/scripts/intro_foreground.sh b/five-minutes-to-feature-flags/assets/scripts/intro_foreground.sh new file mode 100644 index 0000000..73d85ba --- /dev/null +++ b/five-minutes-to-feature-flags/assets/scripts/intro_foreground.sh @@ -0,0 +1,52 @@ +# ----------------------------------- +# Step 1/6: APT Update +# ----------------------------------- +apt update + +# ----------------------------------- +# Step 2/6: Installing bat +# ----------------------------------- +apt install -y bat < /dev/null +# Symlink: Make 'batcat' available as 'bat' command +ln -s /usr/bin/batcat /usr/sbin/bat +# Alias 'cat' to 'bat' so running: 'cat ~/myfile.txt' uses 'bat' instead +# Bonus points for using 'bat' to do this :) +bat <> ~/.bashrc +alias cat=bat +EOF +# Refresh source +source ~/.bashrc + +# ----------------------------------- +# Step 3/6: Installing Node +# ----------------------------------- +curl -fsSL https://deb.nodesource.com/setup_19.x | sudo -E bash - &&\ +apt install -y nodejs < /dev/null + +# ----------------------------------- +# Step 4/6: Installing jq +# ----------------------------------- +apt install -y jq < /dev/null + +# ----------------------------------- +# Step 5/6: Installing NPM packages +# ----------------------------------- +npm install express --save +npm install express-promise-router --save +npm install cowsay --save +npm install @openfeature/js-sdk --save +npm install --force @moredip/openfeature-minimalist-provider + +# ----------------------------------- +# Step 6/6: Initialising NPM package +# ----------------------------------- +cd app +npm init -y +mv package.json package.BAK.json +cat package.BAK.json | jq '. += { "type": "module" }' > package.json +rm package.BAK.json + +# ---------------------------------------------# +# 🎉 Installation Complete 🎉 # +# Please proceed now... # +# ---------------------------------------------# \ No newline at end of file diff --git a/five-minutes-to-feature-flags/finish.md b/five-minutes-to-feature-flags/finish.md new file mode 100644 index 0000000..8eaefc2 --- /dev/null +++ b/five-minutes-to-feature-flags/finish.md @@ -0,0 +1,11 @@ +## Congratulations + +We can get started with feature flags with low investment and low risk, and once we're ready, it's just a few lines of code to transition to a full-featured, scalable backend. + +## Next Steps + +To learn more about OpenFeature, check out their documentation [here](https://docs.openfeature.dev). Specifically, you can read more about how the [evaluation API works](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api), what [tech stacks are supported](https://docs.openfeature.dev/docs/reference/technologies/), or read [more tutorials](https://docs.openfeature.dev/docs/category/getting-started) about using OpenFeature in a variety of tech stacks. + +We strive to provide a welcoming, open community. If you have any questions - or just want to nerd out about feature flags - the [#OpenFeature](https://cloud-native.slack.com/archives/C0344AANLA1) channel in the CNCF slack is the place for you. + +This tutorial was originally created as a blog post [Five Minutes to Feature Flags by Pete Hodgson](https://blog.thepete.net/blog/2023/03/02/five-minutes-to-feature-flags/) and graciously donated to the OpenFeature project. diff --git a/five-minutes-to-feature-flags/index.json b/five-minutes-to-feature-flags/index.json new file mode 100644 index 0000000..bbb4ce0 --- /dev/null +++ b/five-minutes-to-feature-flags/index.json @@ -0,0 +1,37 @@ +{ + "title": "Five Minutes to Feature Flags", + "description": "Add feature flags to a NodeJS app with OpenFeature.", + "details": { + "intro": { + "title": "Feature Flagging in 5 Minutes", + "text": "intro.md", + "foreground": "assets/scripts/intro_foreground.sh" + }, + "steps": [{ + "title": "Hello, world", + "text": "step1.md" + }, { + "title": "Introducing OpenFeature", + "text": "step2.md" + }, { + "title": "Moving to a Full Feature Flagging System", + "text": "step3.md" + }], + "finish": { + "title": "Next Steps", + "text": "finish.md" + }, + "assets": { + "host01": [ + {"file": "01_vanilla.js", "target": "~/app" }, + {"file": "02_basic_flags.js", "target": "~/app" }, + {"file": "03_openfeature.js", "target": "~/app" }, + {"file": "04_openfeature_with_provider.js", "target": "~/app" } + ] + } + }, + "backend": { + "imageid": "ubuntu" + } +} + \ No newline at end of file diff --git a/five-minutes-to-feature-flags/intro.md b/five-minutes-to-feature-flags/intro.md new file mode 100644 index 0000000..a7e23bc --- /dev/null +++ b/five-minutes-to-feature-flags/intro.md @@ -0,0 +1,11 @@ +# Five Minutes to Feature Flags + +We're going to add feature flagging to a node service in under five minutes using OpenFeature, the open, vendor-agnostic feature flagging SDK. + +We'll be working with a simple [express](https://expressjs.com/) server, but if you have any basic familiarity with JavaScript and node you should be able to follow along. + +## Please Be Patient... + +As you can see on the right, we're busy installing everything you'll need. + +When you see 🎉 Installation Complete 🎉, click Start. \ No newline at end of file diff --git a/five-minutes-to-feature-flags/step1.md b/five-minutes-to-feature-flags/step1.md new file mode 100644 index 0000000..7af02b9 --- /dev/null +++ b/five-minutes-to-feature-flags/step1.md @@ -0,0 +1,90 @@ +Here's the service we'll be working on:js + +``` +cat ~/app/01_vanilla.js +```{{exec}} + +Pretty much the most basic express server you can imagine - a single endpoint at `/`{{}} that returns a plaintext `"Hello, world!"`{{}} response. + +Start the server: +``` +node ~/app/01_vanilla.js +```{{exec}} + +[Open the page in a browser]({{TRAFFIC_HOST1_3333}}) and / or open a new terminal Tab (click `+`{{}} next to `Tab 1`{{}}). + +We can test that is works: + +``` +curl http://localhost:3333 +```{{exec}} + +## With cows, please +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! + +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: + +``` +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!") + } +}) +```{{}} + +See the entire code: + +``` +cat ~/app/02_basic_flags.js +```{{exec interrupt}} + +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: + +``` +curl http://localhost:3333 +```{{exec}} + +No difference? Good. By default, our service continues to work exactly as it did before, but if we change `withCow`{{}} to `true`{{}} then our response comes in an exciting new format: + +``` +$> curl http://localhost:3333 + _______________ +< Hello, world! > + --------------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || +```{{}} + +Open the editor and inside the `app`{{}} folder, open `02_basic_flags.js`{{}}. + +Change `false`{{}} on line `12`{{}} to `true` + +Flick over again to tab 1 and restart the server: + +``` +node ~/app/02_basic_flags.js +```{{exec interrupt}} + +Flick back to tab 2 and curl the endpoint. You should see the new `cowsay`{{}} output: +``` +curl http://localhost:3333 +```{{exec}} + +# The Crudest Flag +That `withCow`{{}} boolean and its accompanying conditional check are a very basic feature flag - they let us hide an experimental or unfinished feature, but also easily switch the feature on while we're building and testing it. \ No newline at end of file diff --git a/five-minutes-to-feature-flags/step2.md b/five-minutes-to-feature-flags/step2.md new file mode 100644 index 0000000..f0953dc --- /dev/null +++ b/five-minutes-to-feature-flags/step2.md @@ -0,0 +1,110 @@ +Managing these flags by changing hardcoded constants gets old fast though. A team that uses feature flags in any significant way soon reaches for a feature flagging framework. Let's move in that direction by setting up the [OpenFeature](https://openfeature.dev) SDK: + +The code looks like this: + +``` +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!") + } +}) +``` + +Or show the entire server: + +``` +cat ~/app/03_openfeature.js +```{{exec}} + +We've installed and imported the `@openfeature/js-sdk`{{}} npm module, and used it to create an OpenFeature client called `featureFlags`{{}}. We then call `getBooleanValue`{{}} on that client to find out if the `with-cows` feature flag is `true` or `false`. Depending on what we get back we either show the new cow-based output, or the traditional plaintext format. + +Head back to Tab 1 and run the new server: + +``` +node ~/app/03_openfeature.js +```{{exec interrupt}} + +Note that when we call `getBooleanValue`{{}} we also provide a default value of `false`{{}}. Since we haven't configured the OpenFeature SDK with a feature flag provider yet, it will always return that default value: + +``` +$> curl http://localhost:3333 +Hello, world! +```{{}} + +Flick over to tab 2 and try it: + +``` +curl http://localhost:3333 +```{{exec}} + +## Configuring OpenFeature + +Without a feature flagging provider, [OpenFeature](https://openfeature.dev) is pretty pointless - it'll just return default values. Instead we want to connect our 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 that we have an actual flagging framework set up. For now, just to get started, let's just configure a really, really simple provider that doesn't need a backend. It looks like this: + +``` +import { MinimalistProvider } from '@moredip/openfeature-minimalist-provider' + +const FLAG_CONFIGURATION = { + 'with-cows': true +} + +const featureFlagProvider = new MinimalistProvider(FLAG_CONFIGURATION) + +OpenFeature.setProvider(featureFlagProvider) +const featureFlags = OpenFeature.getClient() +```{{}} + +This minimalist provider is exactly that - you give it a 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`{{}}, which means that conditional predicate in our express app will now evaluate to true, which means that our service will now start providing bovine output: + +``` +$> curl http://localhost:3333 + _______________ +< Hello, world! > + --------------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || +```{{}} + +In Tab 1, run this latest app version: +``` +node ~/app/04_openfeature_with_provider.js +```{{exec interrupt}} + +Try it (in tab 2): + +``` +curl http://localhost:3333 +```{{exec}} + +Open the editor again and edit `~/app/04_openfeature_with_provider.js`{{}}. On line `13`{{}}, change `true`{{}} to `false`. + +Restart the server and you'll see the more boring response: + +``` +$> curl http://localhost:3333 +Hello, world! +```{{}} + +Try it now. In Tab 1, relaunch `04_openfeature_with_provider.js`{{}}: +``` +node ~/app/04_openfeature_with_provider.js +```{{exec interrupt}} + +Then `curl`{{}} once again on tab 2: +``` +curl http://localhost:3333 +```{{exec}} \ No newline at end of file diff --git a/five-minutes-to-feature-flags/step3.md b/five-minutes-to-feature-flags/step3.md new file mode 100644 index 0000000..75487f4 --- /dev/null +++ b/five-minutes-to-feature-flags/step3.md @@ -0,0 +1,42 @@ +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. + +It's as simple as configuring a different provider. + +For example: + +### 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 +``` +OpenFeature.setProvider(new FlagdProvider({ + 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)); +``` \ No newline at end of file From 42be3a57c5bcfe17fc2a42ae48278f563a0d4f76 Mon Sep 17 00:00:00 2001 From: agardnerit Date: Wed, 15 Mar 2023 08:14:27 +1000 Subject: [PATCH 2/3] use Pete's tutorial Signed-off-by: agardnerit --- five-minutes-to-feature-flags/01-vanilla.md | 29 + five-minutes-to-feature-flags/02-cowsay.md | 82 ++ five-minutes-to-feature-flags/03-intro-of.md | 48 + .../04-minimal-provider.md | 76 ++ .../{step3.md => 05-more-providers.md} | 6 +- five-minutes-to-feature-flags/assets/.curlrc | 1 + .../assets/.gitignore | 1 + .../assets/01_vanilla.js | 9 +- .../assets/02_basic_flags.js | 13 +- .../assets/03_openfeature.js | 10 +- .../assets/04_openfeature_with_provider.js | 15 +- .../assets/package-lock.json | 1047 +++++++++++++++++ .../assets/package.json | 16 + .../assets/scripts/intro_foreground.sh | 28 +- five-minutes-to-feature-flags/finish.md | 2 - five-minutes-to-feature-flags/index.json | 25 +- five-minutes-to-feature-flags/intro.md | 6 +- five-minutes-to-feature-flags/step1.md | 90 -- five-minutes-to-feature-flags/step2.md | 110 -- 19 files changed, 1362 insertions(+), 252 deletions(-) create mode 100644 five-minutes-to-feature-flags/01-vanilla.md create mode 100644 five-minutes-to-feature-flags/02-cowsay.md create mode 100644 five-minutes-to-feature-flags/03-intro-of.md create mode 100644 five-minutes-to-feature-flags/04-minimal-provider.md rename five-minutes-to-feature-flags/{step3.md => 05-more-providers.md} (73%) create mode 100644 five-minutes-to-feature-flags/assets/.curlrc create mode 100644 five-minutes-to-feature-flags/assets/.gitignore create mode 100644 five-minutes-to-feature-flags/assets/package-lock.json create mode 100644 five-minutes-to-feature-flags/assets/package.json delete mode 100644 five-minutes-to-feature-flags/step1.md delete mode 100644 five-minutes-to-feature-flags/step2.md diff --git a/five-minutes-to-feature-flags/01-vanilla.md b/five-minutes-to-feature-flags/01-vanilla.md new file mode 100644 index 0000000..9182ab4 --- /dev/null +++ b/five-minutes-to-feature-flags/01-vanilla.md @@ -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... diff --git a/five-minutes-to-feature-flags/02-cowsay.md b/five-minutes-to-feature-flags/02-cowsay.md new file mode 100644 index 0000000..0979d28 --- /dev/null +++ b/five-minutes-to-feature-flags/02-cowsay.md @@ -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... diff --git a/five-minutes-to-feature-flags/03-intro-of.md b/five-minutes-to-feature-flags/03-intro-of.md new file mode 100644 index 0000000..b0b726d --- /dev/null +++ b/five-minutes-to-feature-flags/03-intro-of.md @@ -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. diff --git a/five-minutes-to-feature-flags/04-minimal-provider.md b/five-minutes-to-feature-flags/04-minimal-provider.md new file mode 100644 index 0000000..22fe97d --- /dev/null +++ b/five-minutes-to-feature-flags/04-minimal-provider.md @@ -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... diff --git a/five-minutes-to-feature-flags/step3.md b/five-minutes-to-feature-flags/05-more-providers.md similarity index 73% rename from five-minutes-to-feature-flags/step3.md rename to five-minutes-to-feature-flags/05-more-providers.md index 75487f4..5ebd06c 100644 --- a/five-minutes-to-feature-flags/step3.md +++ b/five-minutes-to-feature-flags/05-more-providers.md @@ -2,9 +2,9 @@ We've gotten started with OpenFeature using a very simple but extremely limited 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. -It's as simple as configuring a different provider. +Once you have the flagging system up and runnig, integrating it into this service is as simple as configuring a different provider. -For example: +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 ``` @@ -39,4 +39,4 @@ import {CloudbeesProvider} from 'cloudbees-openfeature-provider-node' const appKey = '[YOUR_APP_KEY]' OpenFeature.setProvider(await CloudbeesProvider.build(appKey)); -``` \ No newline at end of file +``` diff --git a/five-minutes-to-feature-flags/assets/.curlrc b/five-minutes-to-feature-flags/assets/.curlrc new file mode 100644 index 0000000..534bacd --- /dev/null +++ b/five-minutes-to-feature-flags/assets/.curlrc @@ -0,0 +1 @@ +-w "\n" diff --git a/five-minutes-to-feature-flags/assets/.gitignore b/five-minutes-to-feature-flags/assets/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/five-minutes-to-feature-flags/assets/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/five-minutes-to-feature-flags/assets/01_vanilla.js b/five-minutes-to-feature-flags/assets/01_vanilla.js index cb4f641..d2b41ba 100644 --- a/five-minutes-to-feature-flags/assets/01_vanilla.js +++ b/five-minutes-to-feature-flags/assets/01_vanilla.js @@ -2,13 +2,14 @@ import express from 'express' import Router from 'express-promise-router' const app = express() -const routes = Router(); +const routes = Router() app.use(routes); routes.get('/', async (req, res) => { - res.send("Hello, world!") + res.send("Hello, world!\n") }) -app.listen(3333) +app.listen(3333, () => { + console.log("Server running on port 3333") +}) -console.log("Server running on port 3333") \ No newline at end of file diff --git a/five-minutes-to-feature-flags/assets/02_basic_flags.js b/five-minutes-to-feature-flags/assets/02_basic_flags.js index 85bb52b..472a552 100644 --- a/five-minutes-to-feature-flags/assets/02_basic_flags.js +++ b/five-minutes-to-feature-flags/assets/02_basic_flags.js @@ -3,7 +3,12 @@ import Router from 'express-promise-router' import cowsay from 'cowsay' const app = express() -const routes = Router(); +app.use(function (req, res, next) { + res.setHeader('content-type', 'text/plain') + next() +}); + +const routes = Router() app.use(routes); routes.get('/', async (req, res) => { @@ -17,6 +22,6 @@ routes.get('/', async (req, res) => { } }) -app.listen(3333) - -console.log("Server running on port 3333") \ No newline at end of file +app.listen(3333, () => { + console.log("Server running on port 3333") +}) diff --git a/five-minutes-to-feature-flags/assets/03_openfeature.js b/five-minutes-to-feature-flags/assets/03_openfeature.js index 3c0be0d..da40815 100644 --- a/five-minutes-to-feature-flags/assets/03_openfeature.js +++ b/five-minutes-to-feature-flags/assets/03_openfeature.js @@ -4,6 +4,10 @@ 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); @@ -18,6 +22,6 @@ routes.get('/', async (req, res) => { } }) -app.listen(3333) - -console.log("Server running on port 3333") \ No newline at end of file +app.listen(3333, () => { + console.log("Server running on port 3333") +}) diff --git a/five-minutes-to-feature-flags/assets/04_openfeature_with_provider.js b/five-minutes-to-feature-flags/assets/04_openfeature_with_provider.js index ef4bb65..e6992dd 100644 --- a/five-minutes-to-feature-flags/assets/04_openfeature_with_provider.js +++ b/five-minutes-to-feature-flags/assets/04_openfeature_with_provider.js @@ -2,9 +2,14 @@ import express from 'express' import Router from 'express-promise-router' import cowsay from 'cowsay' import { OpenFeature } from '@openfeature/js-sdk' -import { MinimalistProvider } from '@moredip/openfeature-minimalist-provider' +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); @@ -13,7 +18,7 @@ const FLAG_CONFIGURATION = { 'with-cows': true } -const featureFlagProvider = new MinimalistProvider(FLAG_CONFIGURATION) +const featureFlagProvider = new InMemoryProvider(FLAG_CONFIGURATION) OpenFeature.setProvider(featureFlagProvider) @@ -26,6 +31,6 @@ routes.get('/', async (req, res) => { } }) -app.listen(3333) - -console.log("Server running on port 3333") \ No newline at end of file +app.listen(3333, () => { + console.log("Server running on port 3333") +}) diff --git a/five-minutes-to-feature-flags/assets/package-lock.json b/five-minutes-to-feature-flags/assets/package-lock.json new file mode 100644 index 0000000..ae002a6 --- /dev/null +++ b/five-minutes-to-feature-flags/assets/package-lock.json @@ -0,0 +1,1047 @@ +{ + "name": "five-minutes-to-feature-flags-tutorial", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "five-minutes-to-feature-flags-tutorial", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@openfeature/in-memory-provider": "^0.1.1", + "@openfeature/js-sdk": "^1.1.0", + "cowsay": "^1.5.0", + "express": "^4.18.2", + "express-promise-router": "^4.1.1" + } + }, + "node_modules/@openfeature/in-memory-provider": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@openfeature/in-memory-provider/-/in-memory-provider-0.1.1.tgz", + "integrity": "sha512-WCluAYTY42/r0BV4UprWN0gzXy0uIGR0x+iGBrdCAuWaDtBp2u3+/oVIDNSOCxnKcw2ARbr6oTCcjmcXZo2VgQ==", + "peerDependencies": { + "@openfeature/js-sdk": "^1.0.0" + } + }, + "node_modules/@openfeature/js-sdk": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@openfeature/js-sdk/-/js-sdk-1.1.0.tgz", + "integrity": "sha512-XvzmLjhlgI9/scKSSS8rA+/1kBwOo3F8uvWKDvT9FIJovuxmKrc6jTJSjyAPd+OQaPyw9wK9OXYcjIUhwXxKwA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cowsay": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.5.0.tgz", + "integrity": "sha512-8Ipzr54Z8zROr/62C8f0PdhQcDusS05gKTS87xxdji8VbWefWly0k8BwGK7+VqamOrkv3eGsCkPtvlHzrhWsCA==", + "dependencies": { + "get-stdin": "8.0.0", + "string-width": "~2.1.1", + "strip-final-newline": "2.0.0", + "yargs": "15.4.1" + }, + "bin": { + "cowsay": "cli.js", + "cowthink": "cli.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-promise-router": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/express-promise-router/-/express-promise-router-4.1.1.tgz", + "integrity": "sha512-Lkvcy/ZGrBhzkl3y7uYBHLMtLI4D6XQ2kiFg9dq7fbktBch5gjqJ0+KovX0cvCAvTJw92raWunRLM/OM+5l4fA==", + "dependencies": { + "is-promise": "^4.0.0", + "lodash.flattendeep": "^4.0.0", + "methods": "^1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/express": "^4.0.0", + "express": "^4.0.0" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/five-minutes-to-feature-flags/assets/package.json b/five-minutes-to-feature-flags/assets/package.json new file mode 100644 index 0000000..f983d05 --- /dev/null +++ b/five-minutes-to-feature-flags/assets/package.json @@ -0,0 +1,16 @@ +{ + "name": "five-minutes-to-feature-flags-tutorial", + "version": "1.0.0", + "description": "A short tutorial demonstrating how to add OpenFeature to an express server", + "type": "module", + "scripts": {}, + "author": "Pete Hodgson", + "license": "MIT", + "dependencies": { + "@openfeature/in-memory-provider": "^0.1.1", + "@openfeature/js-sdk": "^1.1.0", + "cowsay": "^1.5.0", + "express": "^4.18.2", + "express-promise-router": "^4.1.1" + } +} diff --git a/five-minutes-to-feature-flags/assets/scripts/intro_foreground.sh b/five-minutes-to-feature-flags/assets/scripts/intro_foreground.sh index 73d85ba..dcb990e 100644 --- a/five-minutes-to-feature-flags/assets/scripts/intro_foreground.sh +++ b/five-minutes-to-feature-flags/assets/scripts/intro_foreground.sh @@ -1,10 +1,10 @@ # ----------------------------------- -# Step 1/6: APT Update +# APT Update # ----------------------------------- apt update # ----------------------------------- -# Step 2/6: Installing bat +# Installing bat # ----------------------------------- apt install -y bat < /dev/null # Symlink: Make 'batcat' available as 'bat' command @@ -18,35 +18,19 @@ EOF source ~/.bashrc # ----------------------------------- -# Step 3/6: Installing Node +# Installing Node # ----------------------------------- curl -fsSL https://deb.nodesource.com/setup_19.x | sudo -E bash - &&\ apt install -y nodejs < /dev/null -# ----------------------------------- -# Step 4/6: Installing jq -# ----------------------------------- -apt install -y jq < /dev/null - -# ----------------------------------- -# Step 5/6: Installing NPM packages -# ----------------------------------- -npm install express --save -npm install express-promise-router --save -npm install cowsay --save -npm install @openfeature/js-sdk --save -npm install --force @moredip/openfeature-minimalist-provider # ----------------------------------- -# Step 6/6: Initialising NPM package +# npm install # ----------------------------------- cd app -npm init -y -mv package.json package.BAK.json -cat package.BAK.json | jq '. += { "type": "module" }' > package.json -rm package.BAK.json +npm clean-install --force # ---------------------------------------------# # 🎉 Installation Complete 🎉 # # Please proceed now... # -# ---------------------------------------------# \ No newline at end of file +# ---------------------------------------------# diff --git a/five-minutes-to-feature-flags/finish.md b/five-minutes-to-feature-flags/finish.md index 8eaefc2..a24a635 100644 --- a/five-minutes-to-feature-flags/finish.md +++ b/five-minutes-to-feature-flags/finish.md @@ -7,5 +7,3 @@ We can get started with feature flags with low investment and low risk, and once To learn more about OpenFeature, check out their documentation [here](https://docs.openfeature.dev). Specifically, you can read more about how the [evaluation API works](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api), what [tech stacks are supported](https://docs.openfeature.dev/docs/reference/technologies/), or read [more tutorials](https://docs.openfeature.dev/docs/category/getting-started) about using OpenFeature in a variety of tech stacks. We strive to provide a welcoming, open community. If you have any questions - or just want to nerd out about feature flags - the [#OpenFeature](https://cloud-native.slack.com/archives/C0344AANLA1) channel in the CNCF slack is the place for you. - -This tutorial was originally created as a blog post [Five Minutes to Feature Flags by Pete Hodgson](https://blog.thepete.net/blog/2023/03/02/five-minutes-to-feature-flags/) and graciously donated to the OpenFeature project. diff --git a/five-minutes-to-feature-flags/index.json b/five-minutes-to-feature-flags/index.json index bbb4ce0..aa225bd 100644 --- a/five-minutes-to-feature-flags/index.json +++ b/five-minutes-to-feature-flags/index.json @@ -7,22 +7,33 @@ "text": "intro.md", "foreground": "assets/scripts/intro_foreground.sh" }, - "steps": [{ + "steps": [ + { "title": "Hello, world", - "text": "step1.md" + "text": "01-vanilla.md" + }, { + "title": "Cows, please!", + "text": "02-cowsay.md" }, { "title": "Introducing OpenFeature", - "text": "step2.md" - }, { - "title": "Moving to a Full Feature Flagging System", - "text": "step3.md" - }], + "text": "03-intro-of.md" + }, { + "title": "Starting with a simple flag provider", + "text": "04-minimal-provider.md" + }, { + "title": "Moving to a Full Feature Flagging System", + "text": "05-more-providers.md" + } + ], "finish": { "title": "Next Steps", "text": "finish.md" }, "assets": { "host01": [ + {"file": ".curlrc", "target": "~/" }, + {"file": "package.json", "target": "~/app" }, + {"file": "package-lock.json", "target": "~/app" }, {"file": "01_vanilla.js", "target": "~/app" }, {"file": "02_basic_flags.js", "target": "~/app" }, {"file": "03_openfeature.js", "target": "~/app" }, diff --git a/five-minutes-to-feature-flags/intro.md b/five-minutes-to-feature-flags/intro.md index a7e23bc..9ca687a 100644 --- a/five-minutes-to-feature-flags/intro.md +++ b/five-minutes-to-feature-flags/intro.md @@ -2,10 +2,12 @@ We're going to add feature flagging to a node service in under five minutes using OpenFeature, the open, vendor-agnostic feature flagging SDK. -We'll be working with a simple [express](https://expressjs.com/) server, but if you have any basic familiarity with JavaScript and node you should be able to follow along. +In this hands-on tutorial we'll be working with a simple [express](https://expressjs.com/) server. If you have some basic familiarity with JavaScript and node you should be able to follow along. + +*This tutorial is based on [this blog post](https://blog.thepete.net/blog/2023/03/02/five-minutes-to-feature-flags/). You're welcome to give the post a look, but it's not required reading - we'll be explaining everything as we go along.* ## Please Be Patient... As you can see on the right, we're busy installing everything you'll need. -When you see 🎉 Installation Complete 🎉, click Start. \ No newline at end of file +When you see 🎉 Installation Complete 🎉, click Start. diff --git a/five-minutes-to-feature-flags/step1.md b/five-minutes-to-feature-flags/step1.md deleted file mode 100644 index 7af02b9..0000000 --- a/five-minutes-to-feature-flags/step1.md +++ /dev/null @@ -1,90 +0,0 @@ -Here's the service we'll be working on:js - -``` -cat ~/app/01_vanilla.js -```{{exec}} - -Pretty much the most basic express server you can imagine - a single endpoint at `/`{{}} that returns a plaintext `"Hello, world!"`{{}} response. - -Start the server: -``` -node ~/app/01_vanilla.js -```{{exec}} - -[Open the page in a browser]({{TRAFFIC_HOST1_3333}}) and / or open a new terminal Tab (click `+`{{}} next to `Tab 1`{{}}). - -We can test that is works: - -``` -curl http://localhost:3333 -```{{exec}} - -## With cows, please -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! - -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: - -``` -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!") - } -}) -```{{}} - -See the entire code: - -``` -cat ~/app/02_basic_flags.js -```{{exec interrupt}} - -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: - -``` -curl http://localhost:3333 -```{{exec}} - -No difference? Good. By default, our service continues to work exactly as it did before, but if we change `withCow`{{}} to `true`{{}} then our response comes in an exciting new format: - -``` -$> curl http://localhost:3333 - _______________ -< Hello, world! > - --------------- - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || || -```{{}} - -Open the editor and inside the `app`{{}} folder, open `02_basic_flags.js`{{}}. - -Change `false`{{}} on line `12`{{}} to `true` - -Flick over again to tab 1 and restart the server: - -``` -node ~/app/02_basic_flags.js -```{{exec interrupt}} - -Flick back to tab 2 and curl the endpoint. You should see the new `cowsay`{{}} output: -``` -curl http://localhost:3333 -```{{exec}} - -# The Crudest Flag -That `withCow`{{}} boolean and its accompanying conditional check are a very basic feature flag - they let us hide an experimental or unfinished feature, but also easily switch the feature on while we're building and testing it. \ No newline at end of file diff --git a/five-minutes-to-feature-flags/step2.md b/five-minutes-to-feature-flags/step2.md deleted file mode 100644 index f0953dc..0000000 --- a/five-minutes-to-feature-flags/step2.md +++ /dev/null @@ -1,110 +0,0 @@ -Managing these flags by changing hardcoded constants gets old fast though. A team that uses feature flags in any significant way soon reaches for a feature flagging framework. Let's move in that direction by setting up the [OpenFeature](https://openfeature.dev) SDK: - -The code looks like this: - -``` -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!") - } -}) -``` - -Or show the entire server: - -``` -cat ~/app/03_openfeature.js -```{{exec}} - -We've installed and imported the `@openfeature/js-sdk`{{}} npm module, and used it to create an OpenFeature client called `featureFlags`{{}}. We then call `getBooleanValue`{{}} on that client to find out if the `with-cows` feature flag is `true` or `false`. Depending on what we get back we either show the new cow-based output, or the traditional plaintext format. - -Head back to Tab 1 and run the new server: - -``` -node ~/app/03_openfeature.js -```{{exec interrupt}} - -Note that when we call `getBooleanValue`{{}} we also provide a default value of `false`{{}}. Since we haven't configured the OpenFeature SDK with a feature flag provider yet, it will always return that default value: - -``` -$> curl http://localhost:3333 -Hello, world! -```{{}} - -Flick over to tab 2 and try it: - -``` -curl http://localhost:3333 -```{{exec}} - -## Configuring OpenFeature - -Without a feature flagging provider, [OpenFeature](https://openfeature.dev) is pretty pointless - it'll just return default values. Instead we want to connect our 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 that we have an actual flagging framework set up. For now, just to get started, let's just configure a really, really simple provider that doesn't need a backend. It looks like this: - -``` -import { MinimalistProvider } from '@moredip/openfeature-minimalist-provider' - -const FLAG_CONFIGURATION = { - 'with-cows': true -} - -const featureFlagProvider = new MinimalistProvider(FLAG_CONFIGURATION) - -OpenFeature.setProvider(featureFlagProvider) -const featureFlags = OpenFeature.getClient() -```{{}} - -This minimalist provider is exactly that - you give it a 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`{{}}, which means that conditional predicate in our express app will now evaluate to true, which means that our service will now start providing bovine output: - -``` -$> curl http://localhost:3333 - _______________ -< Hello, world! > - --------------- - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || || -```{{}} - -In Tab 1, run this latest app version: -``` -node ~/app/04_openfeature_with_provider.js -```{{exec interrupt}} - -Try it (in tab 2): - -``` -curl http://localhost:3333 -```{{exec}} - -Open the editor again and edit `~/app/04_openfeature_with_provider.js`{{}}. On line `13`{{}}, change `true`{{}} to `false`. - -Restart the server and you'll see the more boring response: - -``` -$> curl http://localhost:3333 -Hello, world! -```{{}} - -Try it now. In Tab 1, relaunch `04_openfeature_with_provider.js`{{}}: -``` -node ~/app/04_openfeature_with_provider.js -```{{exec interrupt}} - -Then `curl`{{}} once again on tab 2: -``` -curl http://localhost:3333 -```{{exec}} \ No newline at end of file From 6d14a0016711864edac760ffee79fb99702353f7 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Wed, 15 Mar 2023 15:45:01 -0400 Subject: [PATCH 3/3] Update five-minutes-to-feature-flags/05-more-providers.md Signed-off-by: Michael Beemer --- five-minutes-to-feature-flags/05-more-providers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/five-minutes-to-feature-flags/05-more-providers.md b/five-minutes-to-feature-flags/05-more-providers.md index 5ebd06c..7addb8a 100644 --- a/five-minutes-to-feature-flags/05-more-providers.md +++ b/five-minutes-to-feature-flags/05-more-providers.md @@ -18,6 +18,8 @@ OpenFeature.setProvider(new LaunchDarklyProvider(ldClient)); ### flagd ``` +import { FlagdProvider } from '@openfeature/flagd-provider'; + OpenFeature.setProvider(new FlagdProvider({ host: '[FLAGD_HOST]', port: 8013,