mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
3116a0ecc9
The full LocationSessionRow shape is ~40 fields per session — for locations with dozens of sessions the response easily blew past 100KB and tripped MCP token caps. Most fields aren't needed for the typical "which sessions enrolled from here" triage flow. Default is now a 17-field lean projection (sessionId, device basics, status/duration, geo, app/DO summary). UI keeps the full shape via ?full=1 — single-line addition in lib/api.ts. Also adds an optional tenantId filter on /api/global/metrics/geographic/ sessions so a GA can scope a single tenant's sessions for a location without leaving the global endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
6.4 KiB
C#
144 lines
6.4 KiB
C#
using System.Net;
|
|
using AutopilotMonitor.Functions.Helpers;
|
|
using AutopilotMonitor.Shared.DataAccess;
|
|
using AutopilotMonitor.Shared.Models;
|
|
using Microsoft.Azure.Functions.Worker;
|
|
using Microsoft.Azure.Functions.Worker.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace AutopilotMonitor.Functions.Functions.Metrics
|
|
{
|
|
public class GetGeographicLocationSessionsFunction
|
|
{
|
|
private readonly ILogger<GetGeographicLocationSessionsFunction> _logger;
|
|
private readonly IMaintenanceRepository _maintenanceRepo;
|
|
private readonly IMetricsRepository _metricsRepo;
|
|
|
|
public GetGeographicLocationSessionsFunction(
|
|
ILogger<GetGeographicLocationSessionsFunction> logger,
|
|
IMaintenanceRepository maintenanceRepo,
|
|
IMetricsRepository metricsRepo)
|
|
{
|
|
_logger = logger;
|
|
_maintenanceRepo = maintenanceRepo;
|
|
_metricsRepo = metricsRepo;
|
|
}
|
|
|
|
[Function("GetGeographicLocationSessions")]
|
|
public async Task<HttpResponseData> Run(
|
|
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "metrics/geographic/sessions")] HttpRequestData req)
|
|
{
|
|
try
|
|
{
|
|
// Authentication + MemberRead authorization enforced by PolicyEnforcementMiddleware
|
|
var tenantId = TenantHelper.GetTenantId(req);
|
|
|
|
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
|
|
var locationKey = query["locationKey"];
|
|
if (string.IsNullOrEmpty(locationKey))
|
|
{
|
|
var badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
|
|
await badRequest.WriteAsJsonAsync(new { success = false, message = "locationKey parameter is required" });
|
|
return badRequest;
|
|
}
|
|
|
|
var daysParam = query["days"];
|
|
int days = 30;
|
|
if (!string.IsNullOrEmpty(daysParam) && int.TryParse(daysParam, out var parsedDays) && parsedDays > 0)
|
|
days = parsedDays;
|
|
if (days < 1) days = 1;
|
|
if (days > 365) days = 365;
|
|
|
|
var groupBy = query["groupBy"] ?? "city";
|
|
// Default response shape is lean (~15 fields/row) so MCP-driven callers fit
|
|
// many sessions in a single response. UI sets ?full=1 to opt back into the
|
|
// full LocationSessionRow shape.
|
|
var full = string.Equals(query["full"], "1", StringComparison.Ordinal);
|
|
|
|
_logger.LogInformation("Fetching sessions for location '{LocationKey}' tenant {TenantId} ({Days}d, groupBy={GroupBy}, full={Full})",
|
|
locationKey, tenantId, days, groupBy, full);
|
|
|
|
var cutoff = DateTime.UtcNow.AddDays(-days);
|
|
var sessions = await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1), tenantId);
|
|
var filtered = FilterSessionsByLocation(sessions, locationKey, groupBy);
|
|
|
|
var appSummaries = await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantId);
|
|
var rows = BuildRows(filtered, appSummaries);
|
|
|
|
var response = req.CreateResponse(HttpStatusCode.OK);
|
|
if (full)
|
|
{
|
|
await response.WriteAsJsonAsync(new { success = true, sessions = rows, totalCount = rows.Count });
|
|
}
|
|
else
|
|
{
|
|
var lean = rows.Select(ToLeanRow).ToList();
|
|
await response.WriteAsJsonAsync(new { success = true, sessions = lean, totalCount = lean.Count });
|
|
}
|
|
return response;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error fetching geographic location sessions");
|
|
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
|
|
await errorResponse.WriteAsJsonAsync(new { success = false, message = "Internal server error" });
|
|
return errorResponse;
|
|
}
|
|
}
|
|
|
|
internal static List<SessionSummary> FilterSessionsByLocation(List<SessionSummary> sessions, string locationKey, string groupBy)
|
|
{
|
|
return sessions
|
|
.Where(s => !string.IsNullOrEmpty(s.GeoCountry))
|
|
.Where(s => GetGeographicMetricsFunction.GetLocationKey(s, groupBy) == locationKey)
|
|
.OrderByDescending(s => s.StartedAt)
|
|
.ToList();
|
|
}
|
|
|
|
internal static List<LocationSessionRow> BuildRows(
|
|
List<SessionSummary> sessions, List<AppInstallSummary> appSummaries)
|
|
{
|
|
var appsBySession = appSummaries
|
|
.GroupBy(a => a.SessionId)
|
|
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
|
|
|
|
var rows = new List<LocationSessionRow>(sessions.Count);
|
|
foreach (var s in sessions)
|
|
{
|
|
var apps = appsBySession.TryGetValue(s.SessionId, out var list) ? list : new List<AppInstallSummary>();
|
|
var agg = DoAggregator.Compute(apps);
|
|
rows.Add(LocationSessionRow.From(s, agg, apps.Count));
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lean projection used as the default response shape. Fields chosen to support
|
|
/// the typical MCP triage flow (which session, when, where, how big a deal, was
|
|
/// DO active) while keeping per-row payload an order of magnitude smaller than
|
|
/// the full <see cref="LocationSessionRow"/>. Callers needing the full shape
|
|
/// pass <c>?full=1</c>.
|
|
/// </summary>
|
|
internal static object ToLeanRow(LocationSessionRow r) => new
|
|
{
|
|
sessionId = r.SessionId,
|
|
tenantId = r.TenantId,
|
|
serialNumber = r.SerialNumber,
|
|
deviceName = r.DeviceName,
|
|
manufacturer = r.Manufacturer,
|
|
model = r.Model,
|
|
startedAt = r.StartedAt,
|
|
completedAt = r.CompletedAt,
|
|
status = r.Status,
|
|
failureReason = r.FailureReason,
|
|
durationSeconds = r.DurationSeconds,
|
|
enrollmentType = r.EnrollmentType,
|
|
geoCountry = r.GeoCountry,
|
|
geoCity = r.GeoCity,
|
|
totalAppCount = r.TotalAppCount,
|
|
hasDoTelemetry = r.HasDoTelemetry,
|
|
doPercentPeerCaching = r.DoPercentPeerCaching,
|
|
};
|
|
}
|
|
}
|