Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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"
};
55 changes: 55 additions & 0 deletions src/featureManagement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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 AppConfig and FeatureManager instances
let appConfig;
let featureManager;

// Initialize AppConfig and FeatureManager
async function initializeFeatureManagement(appInsightsClient, targetingContextAccessor) {
console.log("Loading configuration...");
appConfig = await load(config.appConfigEndpoint, new DefaultAzureCredential(), {
featureFlagOptions: {
enabled: true,
selectors: [
{
keyFilter: "*"
}
],
refresh: {
enabled: true,
refreshIntervalInMs: 10_000
}
}
});
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-preview.1",
"@microsoft/feature-management-applicationinsights-node": "2.1.0-preview.1",
"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 likes
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 };
114 changes: 40 additions & 74 deletions src/server.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,54 @@
// 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 and app config
let featureManager;

// Initialize the configuration and start the server
initializeConfig()
.then(() => {
async function startApp() {
try {
// Initialize AppConfig and FeatureManager
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(",") : [] };
}
};

// Create 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
};