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.Apps { /// /// GET /api/global/apps/{appName}/analytics?days=30[&tenantId=GUID] /// Global Admin variant of . /// Authorization: GlobalAdminOnly. /// public class GetGlobalAppAnalyticsFunction { private readonly ILogger _logger; private readonly IMetricsRepository _metricsRepo; private readonly ISessionRepository _sessionRepo; private readonly IHardwareRejectionNotificationTracker _notificationTracker; public GetGlobalAppAnalyticsFunction( ILogger logger, IMetricsRepository metricsRepo, ISessionRepository sessionRepo, IHardwareRejectionNotificationTracker notificationTracker) { _logger = logger; _metricsRepo = metricsRepo; _sessionRepo = sessionRepo; _notificationTracker = notificationTracker; } [Function("GetGlobalAppAnalytics")] public async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "global/apps/{appName}/analytics")] HttpRequestData req, string appName) { try { var userEmail = TenantHelper.GetUserIdentifier(req); var decodedAppName = Uri.UnescapeDataString(appName ?? string.Empty); if (string.IsNullOrWhiteSpace(decodedAppName)) { return await req.BadRequestAsync("appName is required"); } var query = req.Query; var scopedTenantId = query["tenantId"]; if (!AppsAnalyticsHelper.IsValidOptionalTenantIdQueryParam(scopedTenantId)) { return await req.BadRequestAsync("tenantId must be a valid GUID"); } var days = QueryParams.Int(query["days"], @default: 30, min: 1, max: 365); if (!AppsAnalyticsHelper.TryParseSourceQueryParam(query["source"], out var source)) { return await req.BadRequestAsync($"source must be one of: {string.Join(", ", AppInstallSources.All)}"); } _logger.LogInformation( "Global apps/{App}/analytics requested (user: {User}, tenantId: {TenantId}, days: {Days})", decodedAppName, userEmail, scopedTenantId ?? "", days); var summaries = await AppsAnalyticsHelper.LoadSummariesAsync(_metricsRepo, scopedTenantId, days); // Episodes are per-tenant tracker rows; without a tenant scope the aggregated // cross-tenant view carries none (mirrors the rule-stats global route). var versionRegressions = string.IsNullOrEmpty(scopedTenantId) ? new List() : (await _notificationTracker.GetAppVersionRegressionsAsync(scopedTenantId!)) .Where(a => string.Equals(a.AppName, decodedAppName, StringComparison.OrdinalIgnoreCase) && AppInstallSources.Normalize(a.Source) == source) .ToList(); var body = await AppsAnalyticsHelper.BuildAnalyticsResponseAsync( summaries, _sessionRepo, decodedAppName, source, days, versionRegressions); var response = req.CreateResponse(HttpStatusCode.OK); await response.WriteAsJsonAsync(body); return response; } catch (UnauthorizedAccessException ex) { _logger.LogWarning(ex, "Unauthorized global/apps/analytics request"); return await req.UnauthorizedAsync("Unauthorized"); } catch (Exception ex) { return await req.InternalServerErrorAsync(_logger, ex, "GetGlobalAppAnalytics"); } } } }