Skip to content

Merge preview to main #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ddee610
add publish tag (#77)
zhiyuanliang-ms Dec 19, 2024
538e680
Merge branch 'main' of https://github.com/microsoft/FeatureManagement…
zhiyuanliang-ms Jan 8, 2025
712387f
Revert "remove exp telemetry & stable v2 (#76)"
zhiyuanliang-ms Jan 8, 2025
99e86e7
Merge pull request #79 from microsoft/merge-main-to-preview
zhiyuanliang-ms Jan 9, 2025
3cfa8ea
version bump v3 preview (#82)
zhiyuanliang-ms Jan 9, 2025
8cda65a
Merge branch 'main' of https://github.com/microsoft/FeatureManagement…
zhiyuanliang-ms Jan 16, 2025
42e48b5
Merge pull request #90 from microsoft/merge-main-to-preview
zhiyuanliang-ms Jan 16, 2025
b65da98
Merge pull request #103 from microsoft/main
zhiyuanliang-ms Mar 6, 2025
a7694d6
Support targeting context accessor (#93)
zhiyuanliang-ms Mar 26, 2025
6adbb93
Add an express example to demostrate ambient targeting usage (#105)
zhiyuanliang-ms Apr 10, 2025
dfbe35e
Support telemetry processor & initializer (#98)
zhiyuanliang-ms Apr 21, 2025
17fc778
not log warning for node (#111)
zhiyuanliang-ms Apr 21, 2025
d93defe
version bump 2.1.0-preview.1 (#113)
zhiyuanliang-ms Apr 22, 2025
57ed1b8
Merge pull request #112 from microsoft/zhiyuanliang/quote-of-the-day
zhiyuanliang-ms Apr 23, 2025
7604979
use latest preview package (#115)
zhiyuanliang-ms Apr 23, 2025
acb7674
Update README.md (#116)
zhiyuanliang-ms Apr 24, 2025
d38d36d
Merge branch 'main' of https://github.com/microsoft/FeatureManagement…
zhiyuanliang-ms May 13, 2025
fa707d8
Merge pull request #118 from microsoft/merge-main-to-preview
zhiyuanliang-ms May 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,5 @@ examples/**/**/package-lock.json

# playwright test result
test-results

**/public
76 changes: 76 additions & 0 deletions examples/express-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# 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. 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();
if (req === undefined) {
return undefined;
}
// 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);
});
```
31 changes: 31 additions & 0 deletions examples/express-app/config.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
]
}
}
]
}
}
9 changes: 9 additions & 0 deletions examples/express-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"scripts": {
"start": "node server.mjs"
},
"dependencies": {
"@microsoft/feature-management": "2.1.0-preview.1",
"express": "^4.21.2"
}
}
63 changes: 63 additions & 0 deletions examples/express-app/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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();
if (req === undefined) {
return undefined;
}
// 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}`);
});
6 changes: 6 additions & 0 deletions examples/quote-of-the-day/.env.temlate
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# You can define environment variables in .env file and load them with 'dotenv' package.
# This is a template of related environment variables in examples.
# To use this file directly, please rename it to .env
APPCONFIG_CONNECTION_STRING=<app-configuration-connection-string>
APPLICATIONINSIGHTS_CONNECTION_STRING=<application-insights-connection-string>
USE_APP_CONFIG=true
155 changes: 155 additions & 0 deletions examples/quote-of-the-day/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Quote of the day - JavaScript

These examples show how to use the Microsoft Feature Management in an express application.

## Setup & Run

1. Build the project.

```cmd
npm run build
```

1. Start the application.

```cmd
npm run start
```

## Telemetry

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.

### Application Insights Integration

The application uses the `@microsoft/feature-management-applicationinsights-node` package to integrate Feature Management with Application Insights:

```javascript
const { createTelemetryPublisher } = require("@microsoft/feature-management-applicationinsights-node");

// When initializing Feature Management
const publishTelemetry = createTelemetryPublisher(appInsightsClient);
featureManager = new FeatureManager(featureFlagProvider, {
onFeatureEvaluated: publishTelemetry,
targetingContextAccessor: targetingContextAccessor
});
```

The `onFeatureEvaluated` option registers a callback that automatically sends telemetry events to Application Insights whenever a feature flag is evaluated.

### Targeting Context in Telemetry

`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.

```javascript
// Initialize Application Insights with targeting context
applicationInsights.defaultClient.addTelemetryProcessor(
createTargetingTelemetryProcessor(targetingContextAccessor)
);
```

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.

### Experimentation and A/B Testing

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.

In this example, a variant feature flag is used to track the like button click rate of a web application:

```json
{
"id": "Greeting",
"enabled": true,
"variants": [
{
"name": "Default"
},
{
"name": "Simple",
"configuration_value": "Hello!"
},
{
"name": "Long",
"configuration_value": "I hope this makes your day!"
}
],
"allocation": {
"percentile": [
{
"variant": "Default",
"from": 0,
"to": 50
},
{
"variant": "Simple",
"from": 50,
"to": 75
},
{
"variant": "Long",
"from": 75,
"to": 100
}
],
"default_when_enabled": "Default",
"default_when_disabled": "Default"
},
"telemetry": {
"enabled": true
}
}
```

## 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 targetingContextAccessor = {
getTargetingContext: () => {
const req = requestAccessor.getStore();
if (req === undefined) {
return undefined;
}
// read user and groups from request
const userId = req.query.userId ?? req.body.userId;
const groups = req.query.groups ?? req.body.groups;
// return an 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 and variant allocation.

### 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
const requestStorageMiddleware = (req, res, next) => {
requestAccessor.run(req, next);
};

...

server.use(requestStorageMiddleware);
```
13 changes: 13 additions & 0 deletions examples/quote-of-the-day/client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quote of the Day</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
17 changes: 17 additions & 0 deletions examples/quote-of-the-day/client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "quoteoftheday",
"type": "module",
"scripts": {
"build": "vite build --emptyOutDir"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0",
"react-icons": "5.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.1",
"vite": "^5.4.1"
}
}
Loading