diff --git a/examples/express-app/README.md b/examples/express-app/README.md new file mode 100644 index 0000000..5cd07b0 --- /dev/null +++ b/examples/express-app/README.md @@ -0,0 +1,80 @@ +# Examples for Microsoft Feature Management for JavaScript + +These examples show how to use the Microsoft Feature Management in an express application. + +## Prerequisites + +The examples are compatible with [LTS versions of Node.js](https://github.com/nodejs/release#release-schedule). + +## Setup & Run + +1. Go to `src/feature-management` under the root folder and run: + + ```bash + npm run install + npm run build + ``` + +1. Go back to `examples/express-app` and install the dependencies using `npm`: + + ```bash + npm install + ``` + +1. Run the examples: + + ```bash + node server.mjs + ``` + +1. Visit `http://localhost:3000/Beta` and use `userId` and `groups` query to specify the targeting context (e.g. /Beta?userId=Jeff or /Beta?groups=Admin). + + - If you are not targeted, you will get the message "Page not found". + + - If you are targeted, you will get the message "Welcome to the Beta page!". + +## Targeting + +The targeting mechanism uses the `exampleTargetingContextAccessor` to extract the targeting context from the request. This function retrieves the userId and groups from the query parameters of the request. + +```javascript +const exampleTargetingContextAccessor = { + getTargetingContext: () => { + const req = requestAccessor.getStore(); + // read user and groups from request query data + const { userId, groups } = req.query; + // return aa ITargetingContext with the appropriate user info + return { userId: userId, groups: groups ? groups.split(",") : [] }; + } +}; +``` + +The `FeatureManager` is configured with this targeting context accessor: + +```javascript +const featureManager = new FeatureManager( + featureProvider, + { + targetingContextAccessor: exampleTargetingContextAccessor + } +); +``` + +This allows you to get ambient targeting context while doing feature flag evaluation. + +### Request Accessor + +The `requestAccessor` is an instance of `AsyncLocalStorage` from the `async_hooks` module. It is used to store the request object in asynchronous local storage, allowing it to be accessed throughout the lifetime of the request. This is particularly useful for accessing request-specific data in asynchronous operations. For more information, please go to https://nodejs.org/api/async_context.html + +```javascript +import { AsyncLocalStorage } from "async_hooks"; +const requestAccessor = new AsyncLocalStorage(); +``` + +Middleware is used to store the request object in the AsyncLocalStorage: + +```javascript +server.use((req, res, next) => { + requestAccessor.run(req, next); +}); +``` \ No newline at end of file diff --git a/examples/express-app/config.json b/examples/express-app/config.json new file mode 100644 index 0000000..f085efe --- /dev/null +++ b/examples/express-app/config.json @@ -0,0 +1,31 @@ +{ + "feature_management": { + "feature_flags": [ + { + "id": "Beta", + "enabled": true, + "conditions": { + "client_filters": [ + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ + "Jeff" + ], + "Groups": [ + { + "Name": "Admin", + "RolloutPercentage": 100 + } + ], + "DefaultRolloutPercentage": 40 + } + } + } + ] + } + } + ] + } +} diff --git a/examples/express-app/package.json b/examples/express-app/package.json new file mode 100644 index 0000000..6833583 --- /dev/null +++ b/examples/express-app/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "start": "node server.mjs" + }, + "dependencies": { + "@microsoft/feature-management": "../../src/feature-management", + "express": "^4.21.2" + } +} \ No newline at end of file diff --git a/examples/express-app/server.mjs b/examples/express-app/server.mjs new file mode 100644 index 0000000..2b8ea53 --- /dev/null +++ b/examples/express-app/server.mjs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import fs from "fs/promises"; +import { ConfigurationObjectFeatureFlagProvider, FeatureManager } from "@microsoft/feature-management"; +// You can also use Azure App Configuration as the source of feature flags. +// For more information, please go to quickstart: https://learn.microsoft.com/azure/azure-app-configuration/quickstart-feature-flag-javascript + +const config = JSON.parse(await fs.readFile("config.json")); +const featureProvider = new ConfigurationObjectFeatureFlagProvider(config); + +// https://nodejs.org/api/async_context.html +import { AsyncLocalStorage } from "async_hooks"; +const requestAccessor = new AsyncLocalStorage(); +const exampleTargetingContextAccessor = { + getTargetingContext: () => { + const req = requestAccessor.getStore(); + // read user and groups from request query data + const { userId, groups } = req.query; + // return an ITargetingContext with the appropriate user info + return { userId: userId, groups: groups ? groups.split(",") : [] }; + } +}; + +const featureManager = new FeatureManager( + featureProvider, + { + targetingContextAccessor: exampleTargetingContextAccessor + } +); + +import express from "express"; +const server = express(); +const PORT = 3000; + +// Use a middleware to store the request object in async local storage. +// The async local storage allows the targeting context accessor to access the current request throughout its lifetime. +// Middleware 1 (request object is stored in async local storage here and it will be available across the following chained async operations) +// Middleware 2 +// Request Handler (feature flag evaluation happens here) +server.use((req, res, next) => { + requestAccessor.run(req, next); +}); + +server.get("/", (req, res) => { + res.send("Hello World!"); +}); + +server.get("/Beta", async (req, res) => { + if (await featureManager.isEnabled("Beta")) { + res.send("Welcome to the Beta page!"); + } else { + res.status(404).send("Page not found"); + } +}); + +// Start the server +server.listen(PORT, () => { + console.log(`Server is running at http://localhost:${PORT}`); +}); \ No newline at end of file