Skip to content

Commit 301d618

Browse files
Merge pull request #114 from microsoft/preview
Merge preview to release/v2
2 parents cbd6f90 + d93defe commit 301d618

File tree

26 files changed

+389
-61
lines changed

26 files changed

+389
-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: 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+
});

src/feature-management-applicationinsights-browser/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@microsoft/feature-management-applicationinsights-browser",
3-
"version": "2.0.2",
3+
"version": "2.1.0-preview.1",
44
"description": "Feature Management Application Insights Plugin for Browser provides a solution for sending feature flag evaluation events produced by the Feature Management library.",
55
"main": "./dist/esm/index.js",
66
"module": "./dist/esm/index.js",
@@ -26,7 +26,7 @@
2626
},
2727
"license": "MIT",
2828
"publishConfig": {
29-
"tag": "latest"
29+
"tag": "preview"
3030
},
3131
"bugs": {
3232
"url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues"
@@ -46,7 +46,7 @@
4646
},
4747
"dependencies": {
4848
"@microsoft/applicationinsights-web": "^3.3.2",
49-
"@microsoft/feature-management": "2.0.2"
49+
"@microsoft/feature-management": "2.1.0-preview.1"
5050
}
5151
}
5252

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export { createTelemetryPublisher, trackEvent } from "./telemetry.js";
4+
export { createTargetingTelemetryInitializer, createTelemetryPublisher, trackEvent } from "./telemetry.js";
55
export { VERSION } from "./version.js";

src/feature-management-applicationinsights-browser/src/telemetry.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { EvaluationResult, createFeatureEvaluationEventProperties } from "@microsoft/feature-management";
5-
import { ApplicationInsights, IEventTelemetry } from "@microsoft/applicationinsights-web";
4+
import { EvaluationResult, createFeatureEvaluationEventProperties, ITargetingContextAccessor } from "@microsoft/feature-management";
5+
import { ApplicationInsights, IEventTelemetry, ITelemetryItem } from "@microsoft/applicationinsights-web";
66

77
const TARGETING_ID = "TargetingId";
88
const FEATURE_EVALUATION_EVENT_NAME = "FeatureEvaluation";
@@ -39,3 +39,20 @@ export function trackEvent(client: ApplicationInsights, targetingId: string, eve
3939
properties[TARGETING_ID] = targetingId ? targetingId.toString() : "";
4040
client.trackEvent(event, properties);
4141
}
42+
43+
/**
44+
* Creates a telemetry initializer that adds targeting id to telemetry item's custom properties.
45+
* @param targetingContextAccessor The accessor function to get the targeting context.
46+
* @returns A telemetry initializer that attaches targeting id to telemetry items.
47+
*/
48+
export function createTargetingTelemetryInitializer(targetingContextAccessor: ITargetingContextAccessor): (item: ITelemetryItem) => void {
49+
return (item: ITelemetryItem) => {
50+
const targetingContext = targetingContextAccessor.getTargetingContext();
51+
if (targetingContext !== undefined) {
52+
if (targetingContext?.userId === undefined) {
53+
console.warn("Targeting id is undefined.");
54+
}
55+
item.data = {...item.data, [TARGETING_ID]: targetingContext?.userId || ""};
56+
}
57+
};
58+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export const VERSION = "2.0.2";
4+
export const VERSION = "2.1.0-preview.1";

src/feature-management-applicationinsights-node/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@microsoft/feature-management-applicationinsights-node",
3-
"version": "2.0.2",
3+
"version": "2.1.0-preview.1",
44
"description": "Feature Management Application Insights Plugin for Node.js provides a solution for sending feature flag evaluation events produced by the Feature Management library.",
55
"main": "./dist/commonjs/index.js",
66
"module": "./dist/esm/index.js",
@@ -25,7 +25,7 @@
2525
},
2626
"license": "MIT",
2727
"publishConfig": {
28-
"tag": "latest"
28+
"tag": "preview"
2929
},
3030
"bugs": {
3131
"url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues"
@@ -45,7 +45,7 @@
4545
},
4646
"dependencies": {
4747
"applicationinsights": "^2.9.6",
48-
"@microsoft/feature-management": "2.0.2"
48+
"@microsoft/feature-management": "2.1.0-preview.1"
4949
}
5050
}
5151

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export { createTelemetryPublisher, trackEvent } from "./telemetry.js";
4+
export { createTargetingTelemetryProcessor, createTelemetryPublisher, trackEvent } from "./telemetry.js";
55
export { VERSION } from "./version.js";

src/feature-management-applicationinsights-node/src/telemetry.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { EvaluationResult, createFeatureEvaluationEventProperties } from "@microsoft/feature-management";
4+
import { EvaluationResult, createFeatureEvaluationEventProperties, ITargetingContextAccessor } from "@microsoft/feature-management";
55
import { TelemetryClient, Contracts } from "applicationinsights";
66

77
const TARGETING_ID = "TargetingId";
@@ -39,3 +39,19 @@ export function trackEvent(client: TelemetryClient, targetingId: string, event:
3939
};
4040
client.trackEvent(event);
4141
}
42+
43+
/**
44+
* Creates a telemetry processor that adds targeting id to telemetry envelope's custom properties.
45+
* @param targetingContextAccessor The accessor function to get the targeting context.
46+
* @returns A telemetry processor that attaches targeting id to telemetry envelopes.
47+
*/
48+
export function createTargetingTelemetryProcessor(targetingContextAccessor: ITargetingContextAccessor): (envelope: Contracts.EnvelopeTelemetry) => boolean {
49+
return (envelope: Contracts.EnvelopeTelemetry) => {
50+
const targetingContext = targetingContextAccessor.getTargetingContext();
51+
if (targetingContext?.userId !== undefined) {
52+
envelope.data.baseData = envelope.data.baseData || {};
53+
envelope.data.baseData.properties = {...envelope.data.baseData.properties, [TARGETING_ID]: targetingContext?.userId || ""};
54+
}
55+
return true;
56+
};
57+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export const VERSION = "2.0.2";
4+
export const VERSION = "2.1.0-preview.1";

src/feature-management/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/feature-management/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@microsoft/feature-management",
3-
"version": "2.0.2",
3+
"version": "2.1.0-preview.1",
44
"description": "Feature Management is a library for enabling/disabling features at runtime. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes.",
55
"main": "./dist/commonjs/index.js",
66
"module": "./dist/esm/index.js",
@@ -27,7 +27,7 @@
2727
},
2828
"license": "MIT",
2929
"publishConfig": {
30-
"tag": "latest"
30+
"tag": "preview"
3131
},
3232
"bugs": {
3333
"url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues"

src/feature-management/src/IFeatureManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { ITargetingContext } from "./common/ITargetingContext";
4+
import { ITargetingContext } from "./common/targetingContext";
55
import { Variant } from "./variant/Variant";
66

77
export interface IFeatureManager {

src/feature-management/src/common/ITargetingContext.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)