mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
perf(backend): push app-install-summary time window server-side + parallelize session join
The app dashboard and metrics endpoints loaded entire AppInstallSummaries partitions (tenant scope) or the full table (global scope) and filtered StartedAt >= cutoff in memory, even for days=30. The device-model join then issued one serial GetSessionAsync point-read per distinct session. - Add optional DateTime? sinceUtc to GetAppInstallSummariesByTenantAsync / GetAllAppInstallSummariesAsync: when set, a server-side StartedAt ge filter scopes the scan so only in-window rows are deserialized/transferred. Default null preserves the legacy full scan for callers that need all history. - AppsAnalyticsHelper.LoadSummariesAsync derives sinceUtc from days; all six /api/apps/* functions pass it. The four metrics/app + metrics/geographic functions pass their existing cutoff. The in-memory Where stays as the exact sub-second trim (the OData filter is second-granular). MCP get_app_install_metrics benefits automatically (days already flows through; response shape unchanged). - Replace the serial session point-read loop with a bounded-concurrency (SemaphoreSlim=10) LoadSessionLookupAsync, shared by the analytics and sessions builders. AppInstallSummaries is already bounded by the session-retention cascade (DeletionManifestBuilder) + tenant offboarding, so no separate table retention is added. Full Backend suite: 2406 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ public class SlaMetricsServiceTests
|
||||
.ReturnsAsync(sessions);
|
||||
|
||||
var metricsRepo = new Mock<IMetricsRepository>();
|
||||
metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(tenantId))
|
||||
metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(tenantId, It.IsAny<DateTime?>()))
|
||||
.ReturnsAsync(appInstalls ?? new List<AppInstallSummary>());
|
||||
|
||||
var configService = new Mock<TenantConfigurationService>(
|
||||
|
||||
@@ -196,13 +196,13 @@ public class SlaTenantStatusLifecycleTests
|
||||
_sessionRepo.Setup(r => r.GetSessionsPageAsync(It.IsAny<string>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<string?>()))
|
||||
.ReturnsAsync(new RawPage<SessionSummary>(new List<SessionSummary>(), null));
|
||||
// Default: no app installs
|
||||
_metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(It.IsAny<string>()))
|
||||
_metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(It.IsAny<string>(), It.IsAny<DateTime?>()))
|
||||
.ReturnsAsync(new List<AppInstallSummary>());
|
||||
}
|
||||
|
||||
public void SetAppInstalls(string tenantId, List<AppInstallSummary> apps)
|
||||
{
|
||||
_metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(tenantId)).ReturnsAsync(apps);
|
||||
_metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(tenantId, It.IsAny<DateTime?>())).ReturnsAsync(apps);
|
||||
}
|
||||
|
||||
public void SetTenants(params TenantConfiguration[] tenants)
|
||||
@@ -427,7 +427,7 @@ public class SlaTenantStatusLifecycleTests
|
||||
var opsSvc = new OpsEventService(opsRepo.Object, NullLogger<OpsEventService>.Instance, alertDispatch);
|
||||
|
||||
var metricsRepo = new Mock<IMetricsRepository>();
|
||||
metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(It.IsAny<string>()))
|
||||
metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(It.IsAny<string>(), It.IsAny<DateTime?>()))
|
||||
.ReturnsAsync(new List<AppInstallSummary>());
|
||||
var svc = new SlaBreachEvaluationService(
|
||||
tenantConfigSvc, configRepoMock.Object, maintenanceRepo.Object, sessionRepo.Object,
|
||||
@@ -862,7 +862,7 @@ public class SlaTenantStatusLifecycleTests
|
||||
|
||||
_sessionRepo.Setup(r => r.GetSessionsPageAsync(It.IsAny<string>(), It.IsAny<int?>(), It.IsAny<int>(), It.IsAny<string?>()))
|
||||
.ReturnsAsync(new RawPage<SessionSummary>(new List<SessionSummary>(), null));
|
||||
_metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(It.IsAny<string>()))
|
||||
_metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(It.IsAny<string>(), It.IsAny<DateTime?>()))
|
||||
.ReturnsAsync(new List<AppInstallSummary>());
|
||||
}
|
||||
|
||||
@@ -873,7 +873,7 @@ public class SlaTenantStatusLifecycleTests
|
||||
=> _maintenanceRepo.Setup(r => r.GetSessionsByDateRangeAsync(It.IsAny<DateTime>(), It.IsAny<DateTime>(), tenantId)).ReturnsAsync(sessions);
|
||||
|
||||
public void SetAppInstalls(string tenantId, List<AppInstallSummary> apps)
|
||||
=> _metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(tenantId)).ReturnsAsync(apps);
|
||||
=> _metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(tenantId, It.IsAny<DateTime?>())).ReturnsAsync(apps);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,9 +24,9 @@ public class UsageMetricsServiceWindowTests
|
||||
.ReturnsAsync(new UserActivityMetrics());
|
||||
metricsRepo.Setup(r => r.GetAllUserActivityMetricsAsync())
|
||||
.ReturnsAsync(new UserActivityMetrics());
|
||||
metricsRepo.Setup(r => r.GetAllAppInstallSummariesAsync())
|
||||
metricsRepo.Setup(r => r.GetAllAppInstallSummariesAsync(It.IsAny<DateTime?>()))
|
||||
.ReturnsAsync(new List<AppInstallSummary>());
|
||||
metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(It.IsAny<string>()))
|
||||
metricsRepo.Setup(r => r.GetAppInstallSummariesByTenantAsync(It.IsAny<string>(), It.IsAny<DateTime?>()))
|
||||
.ReturnsAsync(new List<AppInstallSummary>());
|
||||
metricsRepo.Setup(r => r.GetPlatformStatsAsync())
|
||||
.ReturnsAsync((PlatformStats?)null);
|
||||
|
||||
+4
-4
@@ -35,11 +35,11 @@ namespace AutopilotMonitor.Functions.DataAccess.TableStorage
|
||||
public Task<bool> StoreAppInstallSummaryAsync(AppInstallSummary summary)
|
||||
=> _storage.StoreAppInstallSummaryAsync(summary);
|
||||
|
||||
public Task<List<AppInstallSummary>> GetAppInstallSummariesByTenantAsync(string tenantId)
|
||||
=> _storage.GetAppInstallSummariesByTenantAsync(tenantId);
|
||||
public Task<List<AppInstallSummary>> GetAppInstallSummariesByTenantAsync(string tenantId, DateTime? sinceUtc = null)
|
||||
=> _storage.GetAppInstallSummariesByTenantAsync(tenantId, sinceUtc);
|
||||
|
||||
public Task<List<AppInstallSummary>> GetAllAppInstallSummariesAsync()
|
||||
=> _storage.GetAllAppInstallSummariesAsync();
|
||||
public Task<List<AppInstallSummary>> GetAllAppInstallSummariesAsync(DateTime? sinceUtc = null)
|
||||
=> _storage.GetAllAppInstallSummariesAsync(sinceUtc);
|
||||
|
||||
public Task<PlatformStats?> GetPlatformStatsAsync()
|
||||
=> _storage.GetPlatformStatsAsync();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Threading;
|
||||
using AutopilotMonitor.Shared.DataAccess;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
|
||||
@@ -66,17 +67,55 @@ namespace AutopilotMonitor.Functions.Functions.Apps
|
||||
|
||||
// ── Data loaders ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Caps concurrent session point-reads in the device-model join so a wide app doesn't fan out hundreds of simultaneous Table reads.</summary>
|
||||
private const int SessionJoinConcurrency = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Loads app install summaries for the given scope.
|
||||
/// Loads app install summaries for the given scope, scoped server-side to the last
|
||||
/// <paramref name="days"/> days via a <c>StartedAt ge</c> filter (so a days=30 view does not
|
||||
/// dematerialize the full StartedAt history). The cutoff is derived just before the in-memory
|
||||
/// cutoff the Build* methods re-apply, so the server filter is never narrower than the in-memory
|
||||
/// one — at worst it returns a few extra boundary rows that the in-memory filter trims.
|
||||
/// - tenantId != null → tenant-scoped (per-tenant endpoint or global admin viewing one tenant)
|
||||
/// - tenantId == null → all tenants (global admin aggregated view)
|
||||
/// </summary>
|
||||
public static async Task<List<AppInstallSummary>> LoadSummariesAsync(
|
||||
IMetricsRepository repo, string? tenantId)
|
||||
IMetricsRepository repo, string? tenantId, int days)
|
||||
{
|
||||
var sinceUtc = DateTime.UtcNow.AddDays(-days);
|
||||
return string.IsNullOrEmpty(tenantId)
|
||||
? await repo.GetAllAppInstallSummariesAsync()
|
||||
: await repo.GetAppInstallSummariesByTenantAsync(tenantId);
|
||||
? await repo.GetAllAppInstallSummariesAsync(sinceUtc)
|
||||
: await repo.GetAppInstallSummariesByTenantAsync(tenantId, sinceUtc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the (TenantId, SessionId) → SessionSummary lookup used by the device-model join.
|
||||
/// Point-reads run with bounded concurrency (<see cref="SessionJoinConcurrency"/>) rather than the
|
||||
/// previous serial await-in-loop, which cost one sequential round-trip per distinct session.
|
||||
/// Keys with an empty tenant or session id are skipped; misses (deleted session) are simply absent.
|
||||
/// </summary>
|
||||
private static async Task<Dictionary<string, SessionSummary>> LoadSessionLookupAsync(
|
||||
ISessionRepository sessionRepo, IEnumerable<(string TenantId, string SessionId)> keys)
|
||||
{
|
||||
var distinct = keys
|
||||
.Where(k => !string.IsNullOrEmpty(k.TenantId) && !string.IsNullOrEmpty(k.SessionId))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
using var gate = new SemaphoreSlim(SessionJoinConcurrency);
|
||||
var tasks = distinct.Select(async key =>
|
||||
{
|
||||
await gate.WaitAsync();
|
||||
try { return (key, sess: await sessionRepo.GetSessionAsync(key.TenantId, key.SessionId)); }
|
||||
finally { gate.Release(); }
|
||||
});
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
var lookup = new Dictionary<string, SessionSummary>();
|
||||
foreach (var (key, sess) in results)
|
||||
if (sess != null) lookup[$"{key.TenantId}|{key.SessionId}"] = sess;
|
||||
return lookup;
|
||||
}
|
||||
|
||||
// ── /apps/list ──────────────────────────────────────────────────────
|
||||
@@ -308,14 +347,8 @@ namespace AutopilotMonitor.Functions.Functions.Apps
|
||||
// Device-model correlation: join via session lookup.
|
||||
// Global-admin (no tenant filter) summaries may span multiple tenants, so we use
|
||||
// each summary's own TenantId for the lookup instead of a single passed-in tenantId.
|
||||
var distinctKeys = summaries.Select(s => (s.TenantId, s.SessionId)).Distinct().ToList();
|
||||
var sessionLookup = new Dictionary<string, SessionSummary>();
|
||||
foreach (var (tid, sid) in distinctKeys)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tid) || string.IsNullOrEmpty(sid)) continue;
|
||||
var sess = await sessionRepo.GetSessionAsync(tid, sid);
|
||||
if (sess != null) sessionLookup[$"{tid}|{sid}"] = sess;
|
||||
}
|
||||
var sessionLookup = await LoadSessionLookupAsync(
|
||||
sessionRepo, summaries.Select(s => (s.TenantId, s.SessionId)));
|
||||
|
||||
var deviceModelBreakdown = summaries
|
||||
.Where(s => sessionLookup.ContainsKey($"{s.TenantId}|{s.SessionId}"))
|
||||
@@ -411,14 +444,8 @@ namespace AutopilotMonitor.Functions.Functions.Apps
|
||||
|
||||
// Batch-fetch sessions for device info. Uses each summary's TenantId so global-admin
|
||||
// aggregated view works correctly across tenants.
|
||||
var distinctKeys = summaries.Select(s => (s.TenantId, s.SessionId)).Distinct().ToList();
|
||||
var sessionLookup = new Dictionary<string, SessionSummary>();
|
||||
foreach (var (tid, sid) in distinctKeys)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tid) || string.IsNullOrEmpty(sid)) continue;
|
||||
var sess = await sessionRepo.GetSessionAsync(tid, sid);
|
||||
if (sess != null) sessionLookup[$"{tid}|{sid}"] = sess;
|
||||
}
|
||||
var sessionLookup = await LoadSessionLookupAsync(
|
||||
sessionRepo, summaries.Select(s => (s.TenantId, s.SessionId)));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(modelFilter))
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace AutopilotMonitor.Functions.Functions.Apps
|
||||
if (int.TryParse(query["days"], out var parsedDays) && parsedDays > 0 && parsedDays <= 365)
|
||||
days = parsedDays;
|
||||
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, tenantId);
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, tenantId, days);
|
||||
var body = await AppsAnalyticsHelper.BuildAnalyticsResponseAsync(
|
||||
summaries, _sessionRepo, decodedAppName, days);
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace AutopilotMonitor.Functions.Functions.Apps
|
||||
if (int.TryParse(query["limit"], out var parsedLimit) && parsedLimit > 0)
|
||||
limit = Math.Min(parsedLimit, MaxLimit);
|
||||
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, tenantId);
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, tenantId, days);
|
||||
var body = await AppsAnalyticsHelper.BuildSessionsResponseAsync(
|
||||
summaries, _sessionRepo, decodedAppName, days,
|
||||
statusFilter, modelFilter, versionFilter, offset, limit);
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace AutopilotMonitor.Functions.Functions.Apps
|
||||
return bad;
|
||||
}
|
||||
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, tenantId);
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, tenantId, days);
|
||||
var body = AppsAnalyticsHelper.BuildAppsListResponse(
|
||||
summaries, days, paging.PageSize, paging.Skip,
|
||||
nextOffset => $"/api/apps/list?days={days}&pageSize={paging.PageSize}&skip={nextOffset}");
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ namespace AutopilotMonitor.Functions.Functions.Apps
|
||||
"Global apps/{App}/analytics requested (user: {User}, tenantId: {TenantId}, days: {Days})",
|
||||
decodedAppName, userEmail, scopedTenantId ?? "<all>", days);
|
||||
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, scopedTenantId);
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, scopedTenantId, days);
|
||||
var body = await AppsAnalyticsHelper.BuildAnalyticsResponseAsync(
|
||||
summaries, _sessionRepo, decodedAppName, days);
|
||||
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ namespace AutopilotMonitor.Functions.Functions.Apps
|
||||
"Global apps/{App}/sessions requested (user: {User}, tenantId: {TenantId})",
|
||||
decodedAppName, userEmail, scopedTenantId ?? "<all>");
|
||||
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, scopedTenantId);
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, scopedTenantId, days);
|
||||
var body = await AppsAnalyticsHelper.BuildSessionsResponseAsync(
|
||||
summaries, _sessionRepo, decodedAppName, days,
|
||||
statusFilter, modelFilter, versionFilter, offset, limit);
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace AutopilotMonitor.Functions.Functions.Apps
|
||||
return bad;
|
||||
}
|
||||
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, scopedTenantId);
|
||||
var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, scopedTenantId, days);
|
||||
var tenantQs = string.IsNullOrEmpty(scopedTenantId) ? string.Empty : $"&tenantId={scopedTenantId}";
|
||||
var body = AppsAnalyticsHelper.BuildAppsListResponse(
|
||||
summaries, days, paging.PageSize, paging.Skip,
|
||||
|
||||
@@ -40,7 +40,9 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
|
||||
var cutoff = DateTime.UtcNow.AddDays(-days);
|
||||
|
||||
var allSummaries = await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantId);
|
||||
// Push the window server-side so only in-window rows are deserialized. The in-memory
|
||||
// Where below stays as the exact trim (the OData StartedAt filter is second-granular).
|
||||
var allSummaries = await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantId, cutoff);
|
||||
var summaries = allSummaries.Where(s => s.StartedAt >= cutoff).ToList();
|
||||
|
||||
// Aggregation (slowest/failing ranking + Delivery Optimization rollup) is shared
|
||||
|
||||
+3
-1
@@ -50,7 +50,9 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
|
||||
var cutoff = DateTime.UtcNow.AddDays(-days);
|
||||
var sessions = await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1), tenantId);
|
||||
var allSummaries = await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantId);
|
||||
// Push the window server-side so only in-window rows are deserialized. The in-memory
|
||||
// Where below stays as the exact trim (the OData StartedAt filter is second-granular).
|
||||
var allSummaries = await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantId, cutoff);
|
||||
var summaries = allSummaries.Where(s => s.StartedAt >= cutoff).ToList();
|
||||
|
||||
var result = ComputeGeographicMetrics(sessions, summaries, groupBy);
|
||||
|
||||
+4
-2
@@ -41,9 +41,11 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
|
||||
var cutoff = DateTime.UtcNow.AddDays(-days);
|
||||
|
||||
// Push the window server-side so only in-window rows are deserialized. The in-memory
|
||||
// Where below stays as the exact trim (the OData StartedAt filter is second-granular).
|
||||
var allSummaries = !string.IsNullOrWhiteSpace(tenantIdFilter)
|
||||
? await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantIdFilter)
|
||||
: await _metricsRepo.GetAllAppInstallSummariesAsync();
|
||||
? await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantIdFilter, cutoff)
|
||||
: await _metricsRepo.GetAllAppInstallSummariesAsync(cutoff);
|
||||
var summaries = allSummaries.Where(s => s.StartedAt >= cutoff).ToList();
|
||||
|
||||
// Aggregation (slowest/failing ranking + Delivery Optimization rollup) is shared
|
||||
|
||||
+4
-2
@@ -47,9 +47,11 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
var sessions = !string.IsNullOrWhiteSpace(tenantIdFilter)
|
||||
? await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1), tenantIdFilter)
|
||||
: await _maintenanceRepo.GetSessionsByDateRangeAsync(cutoff, DateTime.UtcNow.AddDays(1));
|
||||
// Push the window server-side so only in-window rows are deserialized. The in-memory
|
||||
// Where below stays as the exact trim (the OData StartedAt filter is second-granular).
|
||||
var allSummaries = !string.IsNullOrWhiteSpace(tenantIdFilter)
|
||||
? await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantIdFilter)
|
||||
: await _metricsRepo.GetAllAppInstallSummariesAsync();
|
||||
? await _metricsRepo.GetAppInstallSummariesByTenantAsync(tenantIdFilter, cutoff)
|
||||
: await _metricsRepo.GetAllAppInstallSummariesAsync(cutoff);
|
||||
var summaries = allSummaries.Where(s => s.StartedAt >= cutoff).ToList();
|
||||
|
||||
var result = GetGeographicMetricsFunction.ComputeGeographicMetrics(sessions, summaries, groupBy);
|
||||
|
||||
@@ -328,16 +328,24 @@ namespace AutopilotMonitor.Functions.Services
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all app install summaries for a tenant (fleet-level metrics)
|
||||
/// Gets app install summaries for a tenant (fleet-level metrics). When <paramref name="sinceUtc"/>
|
||||
/// is supplied, a server-side <c>StartedAt ge</c> filter is applied so a windowed view (e.g. the
|
||||
/// app dashboard's days=30) does not dematerialize the tenant's entire StartedAt history. The
|
||||
/// filter still scans the partition (no secondary index on StartedAt), but only the in-window rows
|
||||
/// are deserialized and returned over the wire. <paramref name="sinceUtc"/> is a server-derived
|
||||
/// DateTime (never caller-supplied text), so interpolating it into the OData filter is injection-safe.
|
||||
/// </summary>
|
||||
public async Task<List<AppInstallSummary>> GetAppInstallSummariesByTenantAsync(string tenantId)
|
||||
public async Task<List<AppInstallSummary>> GetAppInstallSummariesByTenantAsync(string tenantId, DateTime? sinceUtc = null)
|
||||
{
|
||||
SecurityValidator.EnsureValidGuid(tenantId, nameof(tenantId));
|
||||
|
||||
try
|
||||
{
|
||||
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.AppInstallSummaries);
|
||||
var query = tableClient.QueryAsync<TableEntity>(filter: $"PartitionKey eq '{tenantId}'");
|
||||
var filter = $"PartitionKey eq '{tenantId}'";
|
||||
if (sinceUtc.HasValue)
|
||||
filter += $" and StartedAt ge datetime'{sinceUtc.Value:yyyy-MM-ddTHH:mm:ss}Z'";
|
||||
var query = tableClient.QueryAsync<TableEntity>(filter: filter);
|
||||
|
||||
var summaries = new List<AppInstallSummary>();
|
||||
await foreach (var entity in query)
|
||||
@@ -355,14 +363,20 @@ namespace AutopilotMonitor.Functions.Services
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all app install summaries across all tenants (for global admin mode)
|
||||
/// Gets all app install summaries across all tenants (for global admin mode). When
|
||||
/// <paramref name="sinceUtc"/> is supplied, a server-side <c>StartedAt ge</c> filter scopes the
|
||||
/// (otherwise full-table) scan to the window so only in-window rows are deserialized.
|
||||
/// <paramref name="sinceUtc"/> is server-derived, so interpolating it is injection-safe.
|
||||
/// </summary>
|
||||
public async Task<List<AppInstallSummary>> GetAllAppInstallSummariesAsync()
|
||||
public async Task<List<AppInstallSummary>> GetAllAppInstallSummariesAsync(DateTime? sinceUtc = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tableClient = _tableServiceClient.GetTableClient(Constants.TableNames.AppInstallSummaries);
|
||||
var query = tableClient.QueryAsync<TableEntity>();
|
||||
var filter = sinceUtc.HasValue
|
||||
? $"StartedAt ge datetime'{sinceUtc.Value:yyyy-MM-ddTHH:mm:ss}Z'"
|
||||
: null;
|
||||
var query = tableClient.QueryAsync<TableEntity>(filter: filter);
|
||||
|
||||
var summaries = new List<AppInstallSummary>();
|
||||
await foreach (var entity in query)
|
||||
|
||||
@@ -21,8 +21,10 @@ namespace AutopilotMonitor.Shared.DataAccess
|
||||
|
||||
// --- App Install Summaries ---
|
||||
Task<bool> StoreAppInstallSummaryAsync(AppInstallSummary summary);
|
||||
Task<List<AppInstallSummary>> GetAppInstallSummariesByTenantAsync(string tenantId);
|
||||
Task<List<AppInstallSummary>> GetAllAppInstallSummariesAsync();
|
||||
/// <summary>Gets a tenant's app install summaries. Pass <paramref name="sinceUtc"/> to push the window's <c>StartedAt ge</c> filter server-side; null returns the full partition (legacy behaviour).</summary>
|
||||
Task<List<AppInstallSummary>> GetAppInstallSummariesByTenantAsync(string tenantId, DateTime? sinceUtc = null);
|
||||
/// <summary>Gets all tenants' app install summaries. Pass <paramref name="sinceUtc"/> to scope the (otherwise full-table) scan to the window; null returns everything (legacy behaviour).</summary>
|
||||
Task<List<AppInstallSummary>> GetAllAppInstallSummariesAsync(DateTime? sinceUtc = null);
|
||||
|
||||
// --- Platform Stats ---
|
||||
Task<PlatformStats?> GetPlatformStatsAsync();
|
||||
|
||||
Reference in New Issue
Block a user