Skip to content

Commit 9201d32

Browse files
Merge pull request #119 from microsoft/preview
Merge preview to main
2 parents 5fc8c82 + fa707d8 commit 9201d32

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1415
-61
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,5 @@ examples/**/**/package-lock.json
413413

414414
# playwright test result
415415
test-results
416+
417+
**/public

examples/express-app/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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. Install the dependencies using `npm`:
12+
13+
```bash
14+
npm install
15+
```
16+
17+
1. Run the examples:
18+
19+
```bash
20+
node server.mjs
21+
```
22+
23+
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).
24+
25+
- If you are not targeted, you will get the message "Page not found".
26+
27+
- If you are targeted, you will get the message "Welcome to the Beta page!".
28+
29+
## Targeting
30+
31+
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.
32+
33+
```javascript
34+
const exampleTargetingContextAccessor = {
35+
getTargetingContext: () => {
36+
const req = requestAccessor.getStore();
37+
if (req === undefined) {
38+
return undefined;
39+
}
40+
// read user and groups from request query data
41+
const { userId, groups } = req.query;
42+
// return aa ITargetingContext with the appropriate user info
43+
return { userId: userId, groups: groups ? groups.split(",") : [] };
44+
}
45+
};
46+
```
47+
48+
The `FeatureManager` is configured with this targeting context accessor:
49+
50+
```javascript
51+
const featureManager = new FeatureManager(
52+
featureProvider,
53+
{
54+
targetingContextAccessor: exampleTargetingContextAccessor
55+
}
56+
);
57+
```
58+
59+
This allows you to get ambient targeting context while doing feature flag evaluation.
60+
61+
### Request Accessor
62+
63+
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
64+
65+
```javascript
66+
import { AsyncLocalStorage } from "async_hooks";
67+
const requestAccessor = new AsyncLocalStorage();
68+
```
69+
70+
Middleware is used to store the request object in the AsyncLocalStorage:
71+
72+
```javascript
73+
server.use((req, res, next) => {
74+
requestAccessor.run(req, next);
75+
});
76+
```

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": "2.1.0-preview.1",
7+
"express": "^4.21.2"
8+
}
9+
}

examples/express-app/server.mjs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
if (req === undefined) {
19+
return undefined;
20+
}
21+
// read user and groups from request query data
22+
const { userId, groups } = req.query;
23+
// return an ITargetingContext with the appropriate user info
24+
return { userId: userId, groups: groups ? groups.split(",") : [] };
25+
}
26+
};
27+
28+
const featureManager = new FeatureManager(
29+
featureProvider,
30+
{
31+
targetingContextAccessor: exampleTargetingContextAccessor
32+
}
33+
);
34+
35+
import express from "express";
36+
const server = express();
37+
const PORT = 3000;
38+
39+
// Use a middleware to store the request object in async local storage.
40+
// The async local storage allows the targeting context accessor to access the current request throughout its lifetime.
41+
// Middleware 1 (request object is stored in async local storage here and it will be available across the following chained async operations)
42+
// Middleware 2
43+
// Request Handler (feature flag evaluation happens here)
44+
server.use((req, res, next) => {
45+
requestAccessor.run(req, next);
46+
});
47+
48+
server.get("/", (req, res) => {
49+
res.send("Hello World!");
50+
});
51+
52+
server.get("/Beta", async (req, res) => {
53+
if (await featureManager.isEnabled("Beta")) {
54+
res.send("Welcome to the Beta page!");
55+
} else {
56+
res.status(404).send("Page not found");
57+
}
58+
});
59+
60+
// Start the server
61+
server.listen(PORT, () => {
62+
console.log(`Server is running at http://localhost:${PORT}`);
63+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# You can define environment variables in .env file and load them with 'dotenv' package.
2+
# This is a template of related environment variables in examples.
3+
# To use this file directly, please rename it to .env
4+
APPCONFIG_CONNECTION_STRING=<app-configuration-connection-string>
5+
APPLICATIONINSIGHTS_CONNECTION_STRING=<application-insights-connection-string>
6+
USE_APP_CONFIG=true

examples/quote-of-the-day/README.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Quote of the day - JavaScript
2+
3+
These examples show how to use the Microsoft Feature Management in an express application.
4+
5+
## Setup & Run
6+
7+
1. Build the project.
8+
9+
```cmd
10+
npm run build
11+
```
12+
13+
1. Start the application.
14+
15+
```cmd
16+
npm run start
17+
```
18+
19+
## Telemetry
20+
21+
The Quote of the Day example implements telemetry using Azure Application Insights to track feature flag evaluations. This helps monitor and analyze how feature flags are being used in your application.
22+
23+
### Application Insights Integration
24+
25+
The application uses the `@microsoft/feature-management-applicationinsights-node` package to integrate Feature Management with Application Insights:
26+
27+
```javascript
28+
const { createTelemetryPublisher } = require("@microsoft/feature-management-applicationinsights-node");
29+
30+
// When initializing Feature Management
31+
const publishTelemetry = createTelemetryPublisher(appInsightsClient);
32+
featureManager = new FeatureManager(featureFlagProvider, {
33+
onFeatureEvaluated: publishTelemetry,
34+
targetingContextAccessor: targetingContextAccessor
35+
});
36+
```
37+
38+
The `onFeatureEvaluated` option registers a callback that automatically sends telemetry events to Application Insights whenever a feature flag is evaluated.
39+
40+
### Targeting Context in Telemetry
41+
42+
`createTargetingTelemetryProcessor` method creates a built-in Application Insights telemetry processor which gets targeting context from the targeting context accessor and attaches the targeting id to telemetry.
43+
44+
```javascript
45+
// Initialize Application Insights with targeting context
46+
applicationInsights.defaultClient.addTelemetryProcessor(
47+
createTargetingTelemetryProcessor(targetingContextAccessor)
48+
);
49+
```
50+
51+
This ensures that every telemetry sent to Application Insights includes the targeting id information, allowing you to correlate feature flag usage with specific users or groups in your analytics.
52+
53+
### Experimentation and A/B Testing
54+
55+
Telemetry is particularly valuable for running experiments like A/B tests. Here's how you can use telemetry to track whether different variants of a feature influence user behavior.
56+
57+
In this example, a variant feature flag is used to track the like button click rate of a web application:
58+
59+
```json
60+
{
61+
"id": "Greeting",
62+
"enabled": true,
63+
"variants": [
64+
{
65+
"name": "Default"
66+
},
67+
{
68+
"name": "Simple",
69+
"configuration_value": "Hello!"
70+
},
71+
{
72+
"name": "Long",
73+
"configuration_value": "I hope this makes your day!"
74+
}
75+
],
76+
"allocation": {
77+
"percentile": [
78+
{
79+
"variant": "Default",
80+
"from": 0,
81+
"to": 50
82+
},
83+
{
84+
"variant": "Simple",
85+
"from": 50,
86+
"to": 75
87+
},
88+
{
89+
"variant": "Long",
90+
"from": 75,
91+
"to": 100
92+
}
93+
],
94+
"default_when_enabled": "Default",
95+
"default_when_disabled": "Default"
96+
},
97+
"telemetry": {
98+
"enabled": true
99+
}
100+
}
101+
```
102+
103+
## Targeting
104+
105+
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.
106+
107+
```javascript
108+
const targetingContextAccessor = {
109+
getTargetingContext: () => {
110+
const req = requestAccessor.getStore();
111+
if (req === undefined) {
112+
return undefined;
113+
}
114+
// read user and groups from request
115+
const userId = req.query.userId ?? req.body.userId;
116+
const groups = req.query.groups ?? req.body.groups;
117+
// return an ITargetingContext with the appropriate user info
118+
return { userId: userId, groups: groups ? groups.split(",") : [] };
119+
}
120+
};
121+
```
122+
123+
The `FeatureManager` is configured with this targeting context accessor:
124+
125+
```javascript
126+
const featureManager = new FeatureManager(
127+
featureProvider,
128+
{
129+
targetingContextAccessor: exampleTargetingContextAccessor
130+
}
131+
);
132+
```
133+
134+
This allows you to get ambient targeting context while doing feature flag evaluation and variant allocation.
135+
136+
### Request Accessor
137+
138+
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
139+
140+
```javascript
141+
import { AsyncLocalStorage } from "async_hooks";
142+
const requestAccessor = new AsyncLocalStorage();
143+
```
144+
145+
Middleware is used to store the request object in the AsyncLocalStorage:
146+
147+
```javascript
148+
const requestStorageMiddleware = (req, res, next) => {
149+
requestAccessor.run(req, next);
150+
};
151+
152+
...
153+
154+
server.use(requestStorageMiddleware);
155+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Quote of the Day</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/index.jsx"></script>
12+
</body>
13+
</html>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "quoteoftheday",
3+
"type": "module",
4+
"scripts": {
5+
"build": "vite build --emptyOutDir"
6+
},
7+
"dependencies": {
8+
"react": "^18.3.1",
9+
"react-dom": "^18.3.1",
10+
"react-router-dom": "^6.27.0",
11+
"react-icons": "5.3.0"
12+
},
13+
"devDependencies": {
14+
"@vitejs/plugin-react": "^4.3.1",
15+
"vite": "^5.4.1"
16+
}
17+
}

0 commit comments

Comments
 (0)