Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions/Functions/Apps/GetGlobalAppAnalyticsFunction.cs
T
Oliver Kieselbach 5163d7d6a0 refactor(backend): read the request query through req.Query only
73 sites in 68 files built the query collection with
HttpUtility.ParseQueryString(req.Url.Query), 15 files used req.Query and five mixed
both. HttpRequestData.Query is that same call, cached per request, and the ASP.NET
Core request type does not override it, so the change is behaviour-neutral.

- QueryParamGuardTests: ParseQueryString in the Functions project is limited to a
  named baseline of three sites that parse something other than the request query.
- A fact pins req.Query against HttpUtility.ParseQueryString (repeated keys, encoded
  values, caching) so a worker update that changes the implementation fails a test.
- Test mocks that set Url answer Query through CallBase; a loose mock returns null.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 22:28:11 +02:00

88 lines
3.9 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.Apps
{
/// <summary>
/// GET /api/global/apps/{appName}/analytics?days=30[&amp;tenantId=GUID]
/// Global Admin variant of <see cref="GetAppAnalyticsFunction"/>.
/// Authorization: GlobalAdminOnly.
/// </summary>
public class GetGlobalAppAnalyticsFunction
{
private readonly ILogger<GetGlobalAppAnalyticsFunction> _logger;
private readonly IMetricsRepository _metricsRepo;
private readonly ISessionRepository _sessionRepo;
private readonly IHardwareRejectionNotificationTracker _notificationTracker;
public GetGlobalAppAnalyticsFunction(
ILogger<GetGlobalAppAnalyticsFunction> logger,
IMetricsRepository metricsRepo,
ISessionRepository sessionRepo,
IHardwareRejectionNotificationTracker notificationTracker)
{
_logger = logger;
_metricsRepo = metricsRepo;
_sessionRepo = sessionRepo;
_notificationTracker = notificationTracker;
}
[Function("GetGlobalAppAnalytics")]
public async Task<HttpResponseData> 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);
_logger.LogInformation(
"Global apps/{App}/analytics requested (user: {User}, tenantId: {TenantId}, days: {Days})",
decodedAppName, userEmail, scopedTenantId ?? "<all>", 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<AppVersionRegressionAlert>()
: (await _notificationTracker.GetAppVersionRegressionsAsync(scopedTenantId!))
.Where(a => string.Equals(a.AppName, decodedAppName, StringComparison.OrdinalIgnoreCase))
.ToList();
var body = await AppsAnalyticsHelper.BuildAnalyticsResponseAsync(
summaries, _sessionRepo, decodedAppName, 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");
}
}
}
}