From 3116a0ecc9635b302d1f81f66df536f99d1aad13 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Thu, 7 May 2026 16:31:39 +0200 Subject: [PATCH] feat(backend,web): geographic-sessions default-lean (?full=1 opt-out) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../GetGeographicLocationSessionsFunction.cs | 46 +++++++++++++++++-- ...lobalGeographicLocationSessionsFunction.cs | 26 +++++++++-- src/Web/autopilot-monitor-web/lib/api.ts | 7 ++- 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGeographicLocationSessionsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGeographicLocationSessionsFunction.cs index 7e071625..e68c0685 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGeographicLocationSessionsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGeographicLocationSessionsFunction.cs @@ -50,9 +50,13 @@ namespace AutopilotMonitor.Functions.Functions.Metrics 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})", - locationKey, tenantId, days, groupBy); + _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); @@ -62,7 +66,15 @@ namespace AutopilotMonitor.Functions.Functions.Metrics var rows = BuildRows(filtered, appSummaries); var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(new { success = true, sessions = rows, totalCount = rows.Count }); + 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) @@ -99,5 +111,33 @@ namespace AutopilotMonitor.Functions.Functions.Metrics } return rows; } + + /// + /// 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 . Callers needing the full shape + /// pass ?full=1. + /// + 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, + }; } } diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalGeographicLocationSessionsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalGeographicLocationSessionsFunction.cs index 74c1cbcb..b447bfcd 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalGeographicLocationSessionsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Metrics/GetGlobalGeographicLocationSessionsFunction.cs @@ -47,19 +47,35 @@ namespace AutopilotMonitor.Functions.Functions.Metrics days = parsedDays; var groupBy = query["groupBy"] ?? "city"; + var full = string.Equals(query["full"], "1", StringComparison.Ordinal); + // Optional tenantId filter so a GA can scope a single tenant's sessions + // for a location without leaving the cross-tenant endpoint. + var filterTenantId = query["tenantId"]; - _logger.LogInformation("Fetching global sessions for location '{LocationKey}' ({Days}d, groupBy={GroupBy}) (User: {UserEmail})", - locationKey, days, groupBy, userEmail); + _logger.LogInformation("Fetching global sessions for location '{LocationKey}' ({Days}d, groupBy={GroupBy}, full={Full}, tenantFilter={Tenant}) (User: {UserEmail})", + locationKey, days, groupBy, full, filterTenantId ?? "(none)", userEmail); var cutoff = DateTime.UtcNow.AddDays(-days); - var sessions = await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1)); + var sessions = string.IsNullOrEmpty(filterTenantId) + ? await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1)) + : await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1), filterTenantId); var filtered = GetGeographicLocationSessionsFunction.FilterSessionsByLocation(sessions, locationKey, groupBy); - var appSummaries = await _metricsRepo.GetAllAppInstallSummariesAsync(); + var appSummaries = string.IsNullOrEmpty(filterTenantId) + ? await _metricsRepo.GetAllAppInstallSummariesAsync() + : await _metricsRepo.GetAppInstallSummariesByTenantAsync(filterTenantId); var rows = GetGeographicLocationSessionsFunction.BuildRows(filtered, appSummaries); var response = req.CreateResponse(HttpStatusCode.OK); - await response.WriteAsJsonAsync(new { success = true, sessions = rows, totalCount = rows.Count }); + if (full) + { + await response.WriteAsJsonAsync(new { success = true, sessions = rows, totalCount = rows.Count }); + } + else + { + var lean = rows.Select(GetGeographicLocationSessionsFunction.ToLeanRow).ToList(); + await response.WriteAsJsonAsync(new { success = true, sessions = lean, totalCount = lean.Count }); + } return response; } catch (Exception ex) diff --git a/src/Web/autopilot-monitor-web/lib/api.ts b/src/Web/autopilot-monitor-web/lib/api.ts index dcbca23d..cfb90665 100644 --- a/src/Web/autopilot-monitor-web/lib/api.ts +++ b/src/Web/autopilot-monitor-web/lib/api.ts @@ -176,10 +176,13 @@ export const api = { `${API_BASE_URL}/api/metrics/geographic${qs({ tenantId, days: String(days), groupBy })}`, globalGeographic: (days: number, groupBy: string, tenantId?: string) => `${API_BASE_URL}/api/global/metrics/geographic${qs({ days: String(days), groupBy, tenantId })}`, + // ?full=1 — UI needs the full LocationSessionRow shape for the page table; default + // is now lean (~15 fields/row) so MCP-driven callers fit many sessions in a single + // response. geographicSessions: (tenantId: string, days: number, groupBy: string, locationKey: string) => - `${API_BASE_URL}/api/metrics/geographic/sessions${qs({ tenantId, days: String(days), groupBy, locationKey })}`, + `${API_BASE_URL}/api/metrics/geographic/sessions${qs({ tenantId, days: String(days), groupBy, locationKey, full: '1' })}`, globalGeographicSessions: (days: number, groupBy: string, locationKey: string) => - `${API_BASE_URL}/api/global/metrics/geographic/sessions${qs({ days: String(days), groupBy, locationKey })}`, + `${API_BASE_URL}/api/global/metrics/geographic/sessions${qs({ days: String(days), groupBy, locationKey, full: '1' })}`, platform: (opts?: { limit?: number; days?: number }) => `${API_BASE_URL}/api/global/metrics/platform${qs({ limit: opts?.limit?.toString(),