diff --git a/src/client/src/pages/Home.jsx b/src/client/src/pages/Home.jsx index 3128616..28b09ec 100644 --- a/src/client/src/pages/Home.jsx +++ b/src/client/src/pages/Home.jsx @@ -39,7 +39,7 @@ function Home() { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ UserId: currentUser ?? "" }), + body: JSON.stringify({ userId: currentUser ?? "" }), }); if (response.ok) { diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..9d194d5 --- /dev/null +++ b/src/config.js @@ -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" +}; diff --git a/src/featureManagement.js b/src/featureManagement.js new file mode 100644 index 0000000..3d59a38 --- /dev/null +++ b/src/featureManagement.js @@ -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 +}; \ No newline at end of file diff --git a/src/package.json b/src/package.json index 15a6984..7c97968 100644 --- a/src/package.json +++ b/src/package.json @@ -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" } diff --git a/src/routes.js b/src/routes.js new file mode 100644 index 0000000..12c1aeb --- /dev/null +++ b/src/routes.js @@ -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 }; diff --git a/src/server.js b/src/server.js index 87c0df9..74dccc3 100644 --- a/src/server.js +++ b/src/server.js @@ -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(); diff --git a/src/targetingContextAccessor.js b/src/targetingContextAccessor.js new file mode 100644 index 0000000..9275d74 --- /dev/null +++ b/src/targetingContextAccessor.js @@ -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 +}; diff --git a/src/telemetry.js b/src/telemetry.js new file mode 100644 index 0000000..2ba9714 --- /dev/null +++ b/src/telemetry.js @@ -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 +};