feat(backend,web): geographic-sessions default-lean (?full=1 opt-out)

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>
This commit is contained in:
Oliver Kieselbach
2026-05-07 16:31:39 +02:00
parent 84a1aa14cb
commit 3116a0ecc9
3 changed files with 69 additions and 10 deletions
@@ -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;
}
/// <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,
};
}
}
@@ -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)
+5 -2
View File
@@ -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(),