using DotNetEnv; using Google.Apis.AnalyticsData.v1beta; using Google.Apis.AnalyticsData.v1beta.Data; using Google.Apis.Auth.OAuth2; using Google.Apis.Services; using Microsoft.AspNetCore.Mvc; Env.Load(); var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Register Google Analytics Data Client builder.Services.AddSingleton(sp => { GoogleCredential credential; try { // Tries to find credentials from GOOGLE_APPLICATION_CREDENTIALS environment variable // or default cloud environment credentials. credential = GoogleCredential.GetApplicationDefault(); } catch (Exception) { // Fallback for local development if env var not set (User might need to run 'gcloud auth application-default login') // Or we warn them. For now, we assume it's set up or will fail at runtime. throw new InvalidOperationException("Could not load Google Credentials. Set GOOGLE_APPLICATION_CREDENTIALS or run 'gcloud auth application-default login'."); } if (credential.IsCreateScopedRequired) { credential = credential.CreateScoped(AnalyticsDataService.Scope.AnalyticsReadonly); } var service = new AnalyticsDataService(new BaseClientService.Initializer { HttpClientInitializer = credential, ApplicationName = "TravelMate Backend", }); return service; }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); string? ga4PropertyId = Environment.GetEnvironmentVariable("GA4_PROPERTY_ID"); if (string.IsNullOrEmpty(ga4PropertyId)) { Console.WriteLine("Warning: GA4_PROPERTY_ID not set in .env"); // Default or error? Let's just hold it and fail in the endpoint if needed, or fail startup. // For now, let's allow it to start but endpoint will likely fail. } app.MapGet("/api/metrics/daily", async ( [FromServices] AnalyticsDataService service, [FromQuery] string from, [FromQuery] string to) => { // Basic validation // Basic validation: Check if it's a valid date OR a valid GA4 keyword bool IsValidDate(string date) { return DateTime.TryParse(date, out _) || date == "today" || date == "yesterday" || date.EndsWith("daysAgo"); } if (!IsValidDate(from) || !IsValidDate(to)) { return Results.BadRequest(new { error = "Invalid date format. Use YYYY-MM-DD, 'today', 'yesterday', or 'NdaysAgo'." }); } var request = new RunReportRequest { Property = $"properties/{ga4PropertyId}", DateRanges = new List { new DateRange { StartDate = from, EndDate = to } }, Dimensions = new List { new Dimension { Name = "date" } }, Metrics = new List { new Metric { Name = "activeUsers" } // equivalent to Daily Active Users in this context often // Or strictly "activeUsers" } }; try { var response = await service.Properties.RunReport(request, $"properties/{ga4PropertyId}").ExecuteAsync(); var result = response.Rows?.Select(row => new { date = FormatDate(row.DimensionValues[0].Value), // GA4 returns date as YYYYMMDD string usually dailyActiveUsers = int.Parse(row.MetricValues[0].Value) }) ?? Enumerable.Empty(); return Results.Ok(result); } catch (Exception ex) { Console.WriteLine($"Error: {ex}"); return Results.Problem($"Analytics API Error: {ex.Message}"); } }) .WithName("GetDailyMetrics") .WithOpenApi(); app.MapGet("/api/metrics/general", async ( [FromServices] AnalyticsDataService service) => { // "Global" means all time ideally, or a very long range. // GA4 retention might limit this for user-level data, but aggregated metrics usually go back further. var startDate = "2023-01-01"; // Arbitrary start date for the app var endDate = "today"; var request = new RunReportRequest { Property = $"properties/{ga4PropertyId}", DateRanges = new List { new DateRange { StartDate = startDate, EndDate = endDate } }, Metrics = new List { new Metric { Name = "totalUsers" }, // Proxy for "nb de comptes" new Metric { Name = "eventCount" } // Proxy for "nb de requètes" } }; try { var response = await service.Properties.RunReport(request, $"properties/{ga4PropertyId}").ExecuteAsync(); var totalUsers = 0; var eventCount = 0; if (response.Rows != null && response.Rows.Count > 0) { var row = response.Rows[0]; totalUsers = int.Parse(row.MetricValues[0].Value); eventCount = int.Parse(row.MetricValues[1].Value); } return Results.Ok(new { totalAccounts = totalUsers, totalRequests = eventCount }); } catch (Exception ex) { Console.WriteLine($"Error: {ex}"); return Results.Problem($"Analytics API Error: {ex.Message}"); } }) .WithName("GetGeneralMetrics") .WithOpenApi(); // 3. Tech Overview (OS & App Version) // GET /api/metrics/tech?from=x&to=y app.MapGet("/api/metrics/tech", async ( [FromServices] AnalyticsDataService service, [FromQuery] string? from, [FromQuery] string? to) => { var startDate = from ?? "30daysAgo"; var endDate = to ?? "today"; // Request for OS var requestOS = new RunReportRequest { Property = $"properties/{ga4PropertyId}", DateRanges = new List { new DateRange { StartDate = startDate, EndDate = endDate } }, Dimensions = new List { new Dimension { Name = "operatingSystem" } }, Metrics = new List { new Metric { Name = "activeUsers" } } }; // Request for App Version var requestVersion = new RunReportRequest { Property = $"properties/{ga4PropertyId}", DateRanges = new List { new DateRange { StartDate = startDate, EndDate = endDate } }, Dimensions = new List { new Dimension { Name = "appVersion" } }, Metrics = new List { new Metric { Name = "activeUsers" } } // use active users to see version adoption }; try { var responseOS = await service.Properties.RunReport(requestOS, $"properties/{ga4PropertyId}").ExecuteAsync(); var responseVersion = await service.Properties.RunReport(requestVersion, $"properties/{ga4PropertyId}").ExecuteAsync(); var osData = responseOS.Rows?.Select(row => new { os = row.DimensionValues[0].Value, users = int.Parse(row.MetricValues[0].Value) }) ?? Enumerable.Empty(); var versionData = responseVersion.Rows?.Select(row => new { version = row.DimensionValues[0].Value, users = int.Parse(row.MetricValues[0].Value) }) ?? Enumerable.Empty(); return Results.Ok(new { operatingSystems = osData, appVersions = versionData }); } catch (Exception ex) { return Results.Problem($"Analytics API Error: {ex.Message}"); } }) .WithName("GetTechOverview") .WithOpenApi(); // 4. Top Events // GET /api/metrics/events?from=x&to=y app.MapGet("/api/metrics/events", async ( [FromServices] AnalyticsDataService service, [FromQuery] string? from, [FromQuery] string? to) => { var startDate = from ?? "30daysAgo"; var endDate = to ?? "today"; var request = new RunReportRequest { Property = $"properties/{ga4PropertyId}", DateRanges = new List { new DateRange { StartDate = startDate, EndDate = endDate } }, Dimensions = new List { new Dimension { Name = "eventName" } }, Metrics = new List { new Metric { Name = "eventCount" }, new Metric { Name = "activeUsers" } }, OrderBys = new List { new OrderBy { Desc = true, Metric = new MetricOrderBy { MetricName = "eventCount" } } }, Limit = 20 }; try { var response = await service.Properties.RunReport(request, $"properties/{ga4PropertyId}").ExecuteAsync(); var result = response.Rows?.Select(row => new { eventName = row.DimensionValues[0].Value, count = int.Parse(row.MetricValues[0].Value), users = int.Parse(row.MetricValues[1].Value) }) ?? Enumerable.Empty(); return Results.Ok(result); } catch (Exception ex) { return Results.Problem($"Analytics API Error: {ex.Message}"); } }) .WithName("GetTopEvents") .WithOpenApi(); // 5. Top Screens // GET /api/metrics/screens?from=x&to=y app.MapGet("/api/metrics/screens", async ( [FromServices] AnalyticsDataService service, [FromQuery] string? from, [FromQuery] string? to) => { var startDate = from ?? "30daysAgo"; var endDate = to ?? "today"; var request = new RunReportRequest { Property = $"properties/{ga4PropertyId}", DateRanges = new List { new DateRange { StartDate = startDate, EndDate = endDate } }, // GA4 uses 'unifiedScreenName' or 'pagePath' depending on setup, usually 'unifiedScreenName' for apps Dimensions = new List { new Dimension { Name = "unifiedScreenName" } }, Metrics = new List { new Metric { Name = "screenPageViews" }, new Metric { Name = "activeUsers" } }, OrderBys = new List { new OrderBy { Desc = true, Metric = new MetricOrderBy { MetricName = "screenPageViews" } } }, Limit = 20 }; try { var response = await service.Properties.RunReport(request, $"properties/{ga4PropertyId}").ExecuteAsync(); var result = response.Rows?.Select(row => new { screenName = row.DimensionValues[0].Value, views = int.Parse(row.MetricValues[0].Value), users = int.Parse(row.MetricValues[1].Value) }) ?? Enumerable.Empty(); return Results.Ok(result); } catch (Exception ex) { return Results.Problem($"Analytics API Error: {ex.Message}"); } }) .WithName("GetTopScreens") .WithOpenApi(); // 6. User Retention (New vs Returning) // GET /api/metrics/retention?from=x&to=y app.MapGet("/api/metrics/retention", async ( [FromServices] AnalyticsDataService service, [FromQuery] string? from, [FromQuery] string? to) => { var startDate = from ?? "30daysAgo"; var endDate = to ?? "today"; var request = new RunReportRequest { Property = $"properties/{ga4PropertyId}", DateRanges = new List { new DateRange { StartDate = startDate, EndDate = endDate } }, Dimensions = new List { new Dimension { Name = "newVsReturning" } }, Metrics = new List { new Metric { Name = "activeUsers" }, new Metric { Name = "sessions" } } }; try { var response = await service.Properties.RunReport(request, $"properties/{ga4PropertyId}").ExecuteAsync(); var result = response.Rows?.Select(row => new { type = row.DimensionValues[0].Value, users = int.Parse(row.MetricValues[0].Value), sessions = int.Parse(row.MetricValues[1].Value) }) ?? Enumerable.Empty(); return Results.Ok(result); } catch (Exception ex) { return Results.Problem($"Analytics API Error: {ex.Message}"); } }) .WithName("GetUserRetention") .WithOpenApi(); app.Run(); // Helper to format GA4 YYYYMMDD to YYYY-MM-DD static string FormatDate(string gaDate) { if (gaDate.Length == 8) { return $"{gaDate.Substring(0, 4)}-{gaDate.Substring(4, 2)}-{gaDate.Substring(6, 2)}"; } return gaDate; }