diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
index d4d7ff29..a3888f7a 100644
--- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
@@ -1 +1 @@
-685
+687
diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs
index 4c051c3c..042bcfec 100644
--- a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs
@@ -10,6 +10,7 @@ using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Http;
using System.Net;
using System.Security.Claims;
+using AutopilotMonitor.Functions.Extensions;
using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Shared.DataAccess;
@@ -232,10 +233,9 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware
authenticated = true;
// Track per-user usage (fire-and-forget via Task.Run to ensure execution in isolated worker)
- var oid = principal.FindFirst("oid")?.Value;
- var upn = principal.FindFirst("upn")?.Value
- ?? principal.FindFirst("preferred_username")?.Value;
- var tid = principal.FindFirst("tid")?.Value;
+ var oid = principal.GetObjectId();
+ var upn = principal.GetUserPrincipalName();
+ var tid = principal.GetTenantId();
if (!string.IsNullOrEmpty(oid))
{
var normalizedEndpoint = EndpointNormalizer.Normalize(requestPath);
diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/RequestTelemetryMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/RequestTelemetryMiddleware.cs
new file mode 100644
index 00000000..de1304f0
--- /dev/null
+++ b/src/Backend/AutopilotMonitor.Functions/Middleware/RequestTelemetryMiddleware.cs
@@ -0,0 +1,98 @@
+using System.Diagnostics;
+using Microsoft.ApplicationInsights;
+using Microsoft.ApplicationInsights.DataContracts;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Middleware;
+using AutopilotMonitor.Functions.Helpers;
+
+namespace AutopilotMonitor.Functions.Middleware;
+
+///
+/// Emits worker-side with business context (TenantId, UserId,
+/// CorrelationId, UserRole) so the Application Insights requests table is queryable
+/// by tenant, user, and correlation ID. Runs first in the pipeline to capture accurate
+/// duration including auth and policy evaluation. Non-HTTP triggers are skipped.
+///
+public class RequestTelemetryMiddleware : IFunctionsWorkerMiddleware
+{
+ private readonly TelemetryClient _telemetryClient;
+
+ public RequestTelemetryMiddleware(TelemetryClient telemetryClient)
+ {
+ _telemetryClient = telemetryClient;
+ }
+
+ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
+ {
+ var httpContext = context.GetHttpContext();
+ if (httpContext == null)
+ {
+ // Non-HTTP trigger (timer, queue) — nothing to track
+ await next(context);
+ return;
+ }
+
+ var sw = Stopwatch.StartNew();
+ var startTime = DateTimeOffset.UtcNow;
+ Exception? caughtException = null;
+
+ try
+ {
+ await next(context);
+ }
+ catch (Exception ex)
+ {
+ caughtException = ex;
+ throw;
+ }
+ finally
+ {
+ sw.Stop();
+
+ var statusCode = caughtException != null ? 500 : httpContext.Response.StatusCode;
+ var functionName = context.FunctionDefinition.Name;
+ var method = httpContext.Request.Method;
+ var url = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}{httpContext.Request.Path}{httpContext.Request.QueryString}";
+
+ var requestTelemetry = new RequestTelemetry
+ {
+ Name = $"{method} {functionName}",
+ Timestamp = startTime,
+ Duration = sw.Elapsed,
+ ResponseCode = statusCode.ToString(),
+ Success = statusCode < 500,
+ Url = new Uri(url),
+ };
+
+ // Distributed trace correlation
+ var activity = Activity.Current;
+ if (activity != null)
+ {
+ requestTelemetry.Context.Operation.Id = activity.RootId;
+ requestTelemetry.Context.Operation.ParentId = activity.Id;
+ }
+
+ // Business context from downstream middleware
+ requestTelemetry.Properties["Source"] = "WorkerMiddleware";
+ requestTelemetry.Properties["FunctionName"] = functionName;
+ requestTelemetry.Properties["HttpMethod"] = method;
+ requestTelemetry.Properties["HttpPath"] = httpContext.Request.Path.Value ?? "";
+
+ if (context.Items.TryGetValue("CorrelationId", out var corrId) && corrId is string correlationId)
+ requestTelemetry.Properties["CorrelationId"] = correlationId;
+
+ var reqCtx = context.GetRequestContext();
+ if (!string.IsNullOrEmpty(reqCtx.TenantId))
+ requestTelemetry.Properties["TenantId"] = reqCtx.TenantId;
+ if (!string.IsNullOrEmpty(reqCtx.UserPrincipalName))
+ requestTelemetry.Properties["UserId"] = reqCtx.UserPrincipalName;
+ if (!string.IsNullOrEmpty(reqCtx.UserRole))
+ requestTelemetry.Properties["UserRole"] = reqCtx.UserRole;
+
+ if (caughtException != null)
+ requestTelemetry.Properties["ExceptionType"] = caughtException.GetType().Name;
+
+ _telemetryClient.TrackRequest(requestTelemetry);
+ }
+ }
+}
diff --git a/src/Backend/AutopilotMonitor.Functions/Program.cs b/src/Backend/AutopilotMonitor.Functions/Program.cs
index 356a19fd..1c9062a7 100644
--- a/src/Backend/AutopilotMonitor.Functions/Program.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Program.cs
@@ -21,7 +21,8 @@ var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();
// Register middleware pipeline (Azure Functions .NET 8 isolated worker pattern)
-// Order matters: correlation ID (all requests) → JWT authentication (401) → policy enforcement (403)
+// Order matters: request telemetry (wraps all) → correlation ID → JWT authentication (401) → policy enforcement (403)
+builder.UseMiddleware();
builder.UseMiddleware();
builder.UseMiddleware();
builder.UseMiddleware();