Skip to content

Commit 6adbb93

Browse files
Add an express example to demostrate ambient targeting usage (#105)
* support targeting context accessor * add test * fix lint * update * update * export targeting context * add comments * update * add express example * update * update * fix lint * update * update example in README * update script * update
1 parent a7694d6 commit 6adbb93

File tree

4 files changed

+180
-0
lines changed

4 files changed

+180
-0
lines changed

examples/express-app/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Examples for Microsoft Feature Management for JavaScript
2+
3+
These examples show how to use the Microsoft Feature Management in an express application.
4+
5+
## Prerequisites
6+
7+
The examples are compatible with [LTS versions of Node.js](https://github.com/nodejs/release#release-schedule).
8+
9+
## Setup & Run
10+
11+
1. Go to `src/feature-management` under the root folder and run:
12+
13+
```bash
14+
npm run install
15+
npm run build
16+
```
17+
18+
1. Go back to `examples/express-app` and install the dependencies using `npm`:
19+
20+
```bash
21+
npm install
22+
```
23+
24+
1. Run the examples:
25+
26+
```bash
27+
node server.mjs
28+
```
29+
30+
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).
31+
32+
- If you are not targeted, you will get the message "Page not found".
33+
34+
- If you are targeted, you will get the message "Welcome to the Beta page!".
35+
36+
## Targeting
37+
38+
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.
39+
40+
```javascript
41+
const exampleTargetingContextAccessor = {
42+
getTargetingContext: () => {
43+
const req = requestAccessor.getStore();
44+
// read user and groups from request query data
45+
const { userId, groups } = req.query;
46+
// return aa ITargetingContext with the appropriate user info
47+
return { userId: userId, groups: groups ? groups.split(",") : [] };
48+
}
49+
};
50+
```
51+
52+
The `FeatureManager` is configured with this targeting context accessor:
53+
54+
```javascript
55+
const featureManager = new FeatureManager(
56+
featureProvider,
57+
{
58+
targetingContextAccessor: exampleTargetingContextAccessor
59+
}
60+
);
61+
```
62+
63+
This allows you to get ambient targeting context while doing feature flag evaluation.
64+
65+
### Request Accessor
66+
67+
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
68+
69+
```javascript
70+
import { AsyncLocalStorage } from "async_hooks";
71+
const requestAccessor = new AsyncLocalStorage();
72+
```
73+
74+
Middleware is used to store the request object in the AsyncLocalStorage:
75+
76+
```javascript
77+
server.use((req, res, next) => {
78+
requestAccessor.run(req, next);
79+
});
80+
```

examples/express-app/config.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"feature_management": {
3+
"feature_flags": [
4+
{
5+
"id": "Beta",
6+
"enabled": true,
7+
"conditions": {
8+
"client_filters": [
9+
{
10+
"name": "Microsoft.Targeting",
11+
"parameters": {
12+
"Audience": {
13+
"Users": [
14+
"Jeff"
15+
],
16+
"Groups": [
17+
{
18+
"Name": "Admin",
19+
"RolloutPercentage": 100
20+
}
21+
],
22+
"DefaultRolloutPercentage": 40
23+
}
24+
}
25+
}
26+
]
27+
}
28+
}
29+
]
30+
}
31+
}

examples/express-app/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"scripts": {
3+
"start": "node server.mjs"
4+
},
5+
"dependencies": {
6+
"@microsoft/feature-management": "../../src/feature-management",
7+
"express": "^4.21.2"
8+
}
9+
}

examples/express-app/server.mjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import fs from "fs/promises";
5+
import { ConfigurationObjectFeatureFlagProvider, FeatureManager } from "@microsoft/feature-management";
6+
// You can also use Azure App Configuration as the source of feature flags.
7+
// For more information, please go to quickstart: https://learn.microsoft.com/azure/azure-app-configuration/quickstart-feature-flag-javascript
8+
9+
const config = JSON.parse(await fs.readFile("config.json"));
10+
const featureProvider = new ConfigurationObjectFeatureFlagProvider(config);
11+
12+
// https://nodejs.org/api/async_context.html
13+
import { AsyncLocalStorage } from "async_hooks";
14+
const requestAccessor = new AsyncLocalStorage();
15+
const exampleTargetingContextAccessor = {
16+
getTargetingContext: () => {
17+
const req = requestAccessor.getStore();
18+
// read user and groups from request query data
19+
const { userId, groups } = req.query;
20+
// return an ITargetingContext with the appropriate user info
21+
return { userId: userId, groups: groups ? groups.split(",") : [] };
22+
}
23+
};
24+
25+
const featureManager = new FeatureManager(
26+
featureProvider,
27+
{
28+
targetingContextAccessor: exampleTargetingContextAccessor
29+
}
30+
);
31+
32+
import express from "express";
33+
const server = express();
34+
const PORT = 3000;
35+
36+
// Use a middleware to store the request object in async local storage.
37+
// The async local storage allows the targeting context accessor to access the current request throughout its lifetime.
38+
// Middleware 1 (request object is stored in async local storage here and it will be available across the following chained async operations)
39+
// Middleware 2
40+
// Request Handler (feature flag evaluation happens here)
41+
server.use((req, res, next) => {
42+
requestAccessor.run(req, next);
43+
});
44+
45+
server.get("/", (req, res) => {
46+
res.send("Hello World!");
47+
});
48+
49+
server.get("/Beta", async (req, res) => {
50+
if (await featureManager.isEnabled("Beta")) {
51+
res.send("Welcome to the Beta page!");
52+
} else {
53+
res.status(404).send("Page not found");
54+
}
55+
});
56+
57+
// Start the server
58+
server.listen(PORT, () => {
59+
console.log(`Server is running at http://localhost:${PORT}`);
60+
});

0 commit comments

Comments
 (0)