Skip to content
Open
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: 1 addition & 1 deletion src/client/src/pages/Home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function Home() {
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ UserId: currentUser ?? "" }),
body: JSON.stringify({ userId: currentUser ?? "" }),
});

if (response.ok) {
Expand Down
8 changes: 8 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

module.exports = {
appConfigEndpoint: process.env.APPCONFIG_ENDPOINT,
appInsightsConnectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
port: process.env.PORT || "8080"
};
49 changes: 49 additions & 0 deletions src/featureManagement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.


const { DefaultAzureCredential } = require("@azure/identity");
const { load } = require("@azure/app-configuration-provider");
const { FeatureManager, ConfigurationMapFeatureFlagProvider } = require("@microsoft/feature-management");
const { createTelemetryPublisher } = require("@microsoft/feature-management-applicationinsights-node");
const config = require("./config");

// Variables to hold the App Configuration provider and feature manager instances
let appConfig;
let featureManager;

// Initialize App Configuration provider and feature management
async function initializeFeatureManagement(appInsightsClient, targetingContextAccessor) {
console.log("Loading feature flags from Azure App Configuration...");
appConfig = await load(config.appConfigEndpoint, new DefaultAzureCredential(), {
featureFlagOptions: {
enabled: true,
refresh: {
enabled: true
}
}
});
const featureFlagProvider = new ConfigurationMapFeatureFlagProvider(appConfig);

const publishTelemetry = createTelemetryPublisher(appInsightsClient);
featureManager = new FeatureManager(featureFlagProvider, {
onFeatureEvaluated: publishTelemetry,
targetingContextAccessor: targetingContextAccessor
});

return { featureManager, appConfig };
}

// Middleware to refresh configuration before each request
const featureFlagRefreshMiddleware = (req, res, next) => {
// The configuration refresh happens asynchronously to the processing of your app's incoming requests.
// It will not block or slow down the incoming request that triggered the refresh.
// The request that triggered the refresh may not get the updated configuration values, but later requests will get new configuration values.
appConfig?.refresh(); // intended to not await the refresh
next();
};

module.exports = {
initializeFeatureManagement,
featureFlagRefreshMiddleware
};
4 changes: 2 additions & 2 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
},
"dependencies": {
"@azure/app-configuration-provider": "latest",
"@microsoft/feature-management": "^2.0.0",
"@microsoft/feature-management-applicationinsights-node": "^2.0.0",
"@microsoft/feature-management": "2.1.0",
"@microsoft/feature-management-applicationinsights-node": "2.1.0",
"applicationinsights": "^2.9.6",
"express": "^4.19.2"
}
Expand Down
30 changes: 30 additions & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

const express = require("express");
const router = express.Router();

// Initialize routes with dependencies
function initializeRoutes(featureManager, appInsightsClient) {
// API route to get greeting message with feature variants
router.get("/api/getGreetingMessage", async (req, res) => {
const variant = await featureManager.getVariant("Greeting");
res.status(200).send({
message: variant?.configuration
});
});

// API route to track like events
router.post("/api/like", (req, res) => {
const { userId } = req.body;
if (userId === undefined) {
return res.status(400).send({ error: "userId is required" });
}
appInsightsClient.trackEvent({ name: "Like" });
res.status(200).send({ message: "Like event logged successfully" });
});

return router;
}

module.exports = { initializeRoutes };
113 changes: 39 additions & 74 deletions src/server.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

const appConfigEndpoint = process.env.APPCONFIG_ENDPOINT;
const appInsightsConnectionString = process.env.APPLICATIONINSIGHTS_CONNECTION_STRING;

const applicationInsights = require("applicationinsights");
applicationInsights.setup(appInsightsConnectionString).start();
const config = require("./config");

const express = require("express");
const { targetingContextAccessor, requestStorageMiddleware } = require("./targetingContextAccessor");
const { initializeAppInsights } = require("./telemetry");
const { initializeFeatureManagement, featureFlagRefreshMiddleware } = require("./featureManagement");
const { initializeRoutes } = require("./routes");

// Initialize Express server
const server = express();

const { DefaultAzureCredential } = require("@azure/identity");
const { load } = require("@azure/app-configuration-provider");
const { FeatureManager, ConfigurationMapFeatureFlagProvider } = require("@microsoft/feature-management");
const { createTelemetryPublisher, trackEvent } = require("@microsoft/feature-management-applicationinsights-node");
let appConfig;
let featureManager;
async function initializeConfig() {
console.log("Loading configuration...");
appConfig = await load(appConfigEndpoint, new DefaultAzureCredential(), {
featureFlagOptions: {
enabled: true,
selectors: [
{
keyFilter: "*"
}
],
refresh: {
enabled: true,
refreshIntervalInMs: 10_000
}
}
});
// Initialize Application Insights
const appInsights = initializeAppInsights(targetingContextAccessor);

const featureFlagProvider = new ConfigurationMapFeatureFlagProvider(appConfig);
const publishTelemetry = createTelemetryPublisher(applicationInsights.defaultClient);
featureManager = new FeatureManager(featureFlagProvider, {
onFeatureEvaluated: publishTelemetry
});
}
// Global variables to store feature manager
let featureManager;

// Initialize the configuration and start the server
initializeConfig()
.then(() => {
async function startApp() {
try {
const result = await initializeFeatureManagement(
appInsights.defaultClient,
targetingContextAccessor
);
featureManager = result.featureManager;

console.log("Configuration loaded. Starting server...");
startServer();
})
.catch((error) => {

// Set up middleware
server.use(requestStorageMiddleware);
server.use(featureFlagRefreshMiddleware);
server.use(express.json());
server.use(express.static("public"));

// Set up routes
const routes = initializeRoutes(featureManager, appInsights.defaultClient);
server.use(routes);

// Start the server
server.listen(config.port, () => {
console.log(`Server is running at http://localhost:${config.port}`);
});
} catch (error) {
console.error("Failed to load configuration:", error);
process.exit(1);
});

function startServer() {
// Use a middleware to refresh the configuration before each request
// The configuration refresh is triggered by the incoming requests to your web app. No refresh will occur if your app is idle.
server.use((req, res, next) => {
// The configuration refresh happens asynchronously to the processing of your app's incoming requests.
// It will not block or slow down the incoming request that triggered the refresh.
// The request that triggered the refresh may not get the updated configuration values, but later requests will get new configuration values.
appConfig.refresh(); // intended to not await the refresh
next();
});
server.use(express.json());
server.use(express.static("public"));

server.get("/api/getGreetingMessage", async (req, res) => {
const { userId, groups } = req.query;
const variant = await featureManager.getVariant("Greeting", { userId: userId, groups: groups ? groups.split(",") : [] });
res.status(200).send({
message: variant?.configuration
});
});

server.post("/api/like", (req, res) => {
const { UserId } = req.body;
if (UserId === undefined) {
return res.status(400).send({ error: "UserId is required" });
}
trackEvent(applicationInsights.defaultClient, UserId, { name: "Like" });
res.status(200).send({ message: "Like event logged successfully" });
});

const port = process.env.PORT || "8080";
server.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
}
}

// Start the application
startApp();
32 changes: 32 additions & 0 deletions src/targetingContextAccessor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

const { AsyncLocalStorage } = require("async_hooks");

// Create AsyncLocalStorage for request access across async operations
const requestAccessor = new AsyncLocalStorage();

// Create targeting context accessor to get user information for feature targeting
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(",") : [] };
}
};

// A middleware to store request in AsyncLocalStorage
const requestStorageMiddleware = (req, res, next) => {
requestAccessor.run(req, next);
};

module.exports = {
targetingContextAccessor,
requestStorageMiddleware
};
22 changes: 22 additions & 0 deletions src/telemetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

const config = require('./config');
const applicationInsights = require("applicationinsights");
const { createTargetingTelemetryProcessor } = require("@microsoft/feature-management-applicationinsights-node");

// Initialize Application Insights
const initializeAppInsights = (targetingContextAccessor) => {
applicationInsights.setup(config.appInsightsConnectionString).start();

// Use the targeting telemetry processor to attach targeting id to the telemetry data sent to Application Insights.
applicationInsights.defaultClient.addTelemetryProcessor(
createTargetingTelemetryProcessor(targetingContextAccessor)
);

return applicationInsights;
};

module.exports = {
initializeAppInsights
};