Skip to content

Merge preview to release/v2 #114

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 13 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 80 additions & 0 deletions examples/express-app/README.md
Original file line number Diff line number Diff line change
@@ -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);
});
```
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": "../../src/feature-management",
"express": "^4.21.2"
}
}
60 changes: 60 additions & 0 deletions examples/express-app/server.mjs
Original file line number Diff line number Diff line change
@@ -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}`);
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@microsoft/feature-management-applicationinsights-browser",
"version": "2.0.2",
"version": "2.1.0-preview.1",
"description": "Feature Management Application Insights Plugin for Browser provides a solution for sending feature flag evaluation events produced by the Feature Management library.",
"main": "./dist/esm/index.js",
"module": "./dist/esm/index.js",
Expand All @@ -26,7 +26,7 @@
},
"license": "MIT",
"publishConfig": {
"tag": "latest"
"tag": "preview"
},
"bugs": {
"url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues"
Expand All @@ -46,7 +46,7 @@
},
"dependencies": {
"@microsoft/applicationinsights-web": "^3.3.2",
"@microsoft/feature-management": "2.0.2"
"@microsoft/feature-management": "2.1.0-preview.1"
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export { createTelemetryPublisher, trackEvent } from "./telemetry.js";
export { createTargetingTelemetryInitializer, createTelemetryPublisher, trackEvent } from "./telemetry.js";
export { VERSION } from "./version.js";
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

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

const TARGETING_ID = "TargetingId";
const FEATURE_EVALUATION_EVENT_NAME = "FeatureEvaluation";
Expand Down Expand Up @@ -39,3 +39,20 @@ export function trackEvent(client: ApplicationInsights, targetingId: string, eve
properties[TARGETING_ID] = targetingId ? targetingId.toString() : "";
client.trackEvent(event, properties);
}

/**
* Creates a telemetry initializer that adds targeting id to telemetry item's custom properties.
* @param targetingContextAccessor The accessor function to get the targeting context.
* @returns A telemetry initializer that attaches targeting id to telemetry items.
*/
export function createTargetingTelemetryInitializer(targetingContextAccessor: ITargetingContextAccessor): (item: ITelemetryItem) => void {
return (item: ITelemetryItem) => {
const targetingContext = targetingContextAccessor.getTargetingContext();
if (targetingContext !== undefined) {
if (targetingContext?.userId === undefined) {
console.warn("Targeting id is undefined.");
}
item.data = {...item.data, [TARGETING_ID]: targetingContext?.userId || ""};
}
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export const VERSION = "2.0.2";
export const VERSION = "2.1.0-preview.1";
6 changes: 3 additions & 3 deletions src/feature-management-applicationinsights-node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@microsoft/feature-management-applicationinsights-node",
"version": "2.0.2",
"version": "2.1.0-preview.1",
"description": "Feature Management Application Insights Plugin for Node.js provides a solution for sending feature flag evaluation events produced by the Feature Management library.",
"main": "./dist/commonjs/index.js",
"module": "./dist/esm/index.js",
Expand All @@ -25,7 +25,7 @@
},
"license": "MIT",
"publishConfig": {
"tag": "latest"
"tag": "preview"
},
"bugs": {
"url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues"
Expand All @@ -45,7 +45,7 @@
},
"dependencies": {
"applicationinsights": "^2.9.6",
"@microsoft/feature-management": "2.0.2"
"@microsoft/feature-management": "2.1.0-preview.1"
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export { createTelemetryPublisher, trackEvent } from "./telemetry.js";
export { createTargetingTelemetryProcessor, createTelemetryPublisher, trackEvent } from "./telemetry.js";
export { VERSION } from "./version.js";
18 changes: 17 additions & 1 deletion src/feature-management-applicationinsights-node/src/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

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

const TARGETING_ID = "TargetingId";
Expand Down Expand Up @@ -39,3 +39,19 @@ export function trackEvent(client: TelemetryClient, targetingId: string, event:
};
client.trackEvent(event);
}

/**
* Creates a telemetry processor that adds targeting id to telemetry envelope's custom properties.
* @param targetingContextAccessor The accessor function to get the targeting context.
* @returns A telemetry processor that attaches targeting id to telemetry envelopes.
*/
export function createTargetingTelemetryProcessor(targetingContextAccessor: ITargetingContextAccessor): (envelope: Contracts.EnvelopeTelemetry) => boolean {
return (envelope: Contracts.EnvelopeTelemetry) => {
const targetingContext = targetingContextAccessor.getTargetingContext();
if (targetingContext?.userId !== undefined) {
envelope.data.baseData = envelope.data.baseData || {};
envelope.data.baseData.properties = {...envelope.data.baseData.properties, [TARGETING_ID]: targetingContext?.userId || ""};
}
return true;
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export const VERSION = "2.0.2";
export const VERSION = "2.1.0-preview.1";
4 changes: 2 additions & 2 deletions src/feature-management/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/feature-management/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@microsoft/feature-management",
"version": "2.0.2",
"version": "2.1.0-preview.1",
"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.",
"main": "./dist/commonjs/index.js",
"module": "./dist/esm/index.js",
Expand All @@ -27,7 +27,7 @@
},
"license": "MIT",
"publishConfig": {
"tag": "latest"
"tag": "preview"
},
"bugs": {
"url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues"
Expand Down
2 changes: 1 addition & 1 deletion src/feature-management/src/IFeatureManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { ITargetingContext } from "./common/ITargetingContext";
import { ITargetingContext } from "./common/targetingContext";
import { Variant } from "./variant/Variant";

export interface IFeatureManager {
Expand Down
8 changes: 0 additions & 8 deletions src/feature-management/src/common/ITargetingContext.ts

This file was deleted.

Loading