diff --git a/backend/Program.cs b/backend/Program.cs index f9a3741..e1578db 100644 --- a/backend/Program.cs +++ b/backend/Program.cs @@ -169,10 +169,180 @@ app.MapGet("/api/metrics/general", async ( 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 diff --git a/lib/blocs/auth/auth_bloc.dart b/lib/blocs/auth/auth_bloc.dart index 2a3a291..a65f00e 100644 --- a/lib/blocs/auth/auth_bloc.dart +++ b/lib/blocs/auth/auth_bloc.dart @@ -26,12 +26,14 @@ import '../../repositories/auth_repository.dart'; import 'auth_event.dart'; import 'auth_state.dart'; import '../../services/notification_service.dart'; +import '../../services/analytics_service.dart'; /// BLoC for managing authentication state and operations. class AuthBloc extends Bloc { /// Repository for authentication operations. final AuthRepository _authRepository; final NotificationService _notificationService; + final AnalyticsService _analyticsService; /// Creates an [AuthBloc] with the provided [authRepository]. /// @@ -40,8 +42,10 @@ class AuthBloc extends Bloc { AuthBloc({ required AuthRepository authRepository, NotificationService? notificationService, + AnalyticsService? analyticsService, }) : _authRepository = authRepository, _notificationService = notificationService ?? NotificationService(), + _analyticsService = analyticsService ?? AnalyticsService(), super(AuthInitial()) { on(_onAuthCheckRequested); on(_onSignInRequested); @@ -76,6 +80,7 @@ class AuthBloc extends Bloc { if (user != null) { // Save FCM Token on auto-login await _notificationService.saveTokenToFirestore(user.id!); + await _analyticsService.setUserId(user.id); emit(AuthAuthenticated(user: user)); } else { emit(AuthUnauthenticated()); @@ -107,6 +112,11 @@ class AuthBloc extends Bloc { if (user != null) { // Save FCM Token await NotificationService().saveTokenToFirestore(user.id!); + await _analyticsService.setUserId(user.id); + await _analyticsService.logEvent( + name: 'login', + parameters: {'method': 'email'}, + ); emit(AuthAuthenticated(user: user)); } else { emit(const AuthError(message: 'Invalid email or password')); @@ -138,6 +148,11 @@ class AuthBloc extends Bloc { if (user != null) { // Save FCM Token await NotificationService().saveTokenToFirestore(user.id!); + await _analyticsService.setUserId(user.id); + await _analyticsService.logEvent( + name: 'sign_up', + parameters: {'method': 'email'}, + ); emit(AuthAuthenticated(user: user)); } else { emit(const AuthError(message: 'Failed to create account')); @@ -163,6 +178,11 @@ class AuthBloc extends Bloc { if (user != null) { // Save FCM Token await NotificationService().saveTokenToFirestore(user.id!); + await _analyticsService.setUserId(user.id); + await _analyticsService.logEvent( + name: 'login', + parameters: {'method': 'google'}, + ); emit(AuthAuthenticated(user: user)); } else { emit( @@ -191,6 +211,11 @@ class AuthBloc extends Bloc { ); if (user != null) { + await _analyticsService.setUserId(user.id); + await _analyticsService.logEvent( + name: 'sign_up', + parameters: {'method': 'google'}, + ); emit(AuthAuthenticated(user: user)); } else { emit(const AuthError(message: 'Failed to create account with Google')); @@ -214,6 +239,11 @@ class AuthBloc extends Bloc { ); if (user != null) { + await _analyticsService.setUserId(user.id); + await _analyticsService.logEvent( + name: 'sign_up', + parameters: {'method': 'apple'}, + ); emit(AuthAuthenticated(user: user)); } else { emit(const AuthError(message: 'Failed to create account with Apple')); @@ -239,6 +269,11 @@ class AuthBloc extends Bloc { if (user != null) { // Save FCM Token await NotificationService().saveTokenToFirestore(user.id!); + await _analyticsService.setUserId(user.id); + await _analyticsService.logEvent( + name: 'login', + parameters: {'method': 'apple'}, + ); emit(AuthAuthenticated(user: user)); } else { emit( @@ -261,6 +296,7 @@ class AuthBloc extends Bloc { Emitter emit, ) async { await _authRepository.signOut(); + await _analyticsService.setUserId(null); // Clear user ID emit(AuthUnauthenticated()); } diff --git a/lib/main.dart b/lib/main.dart index 22639b1..d323221 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ import 'package:travel_mate/services/expense_service.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:travel_mate/services/notification_service.dart'; import 'package:travel_mate/services/map_navigation_service.dart'; +import 'package:travel_mate/services/analytics_service.dart'; import 'blocs/auth/auth_bloc.dart'; import 'blocs/auth/auth_event.dart'; import 'blocs/theme/theme_bloc.dart'; @@ -146,6 +147,10 @@ class MyApp extends StatelessWidget { RepositoryProvider( create: (context) => MapNavigationService(), ), + // Analysis service + RepositoryProvider( + create: (context) => AnalyticsService(), + ), ], child: MultiBlocProvider( providers: [ @@ -206,6 +211,9 @@ class MyApp extends StatelessWidget { title: 'Travel Mate', navigatorKey: ErrorService.navigatorKey, themeMode: themeState.themeMode, + navigatorObservers: [ + context.read().getAnalyticsObserver(), + ], // Light theme configuration theme: ThemeData( colorScheme: ColorScheme.fromSeed( diff --git a/lib/services/analytics_service.dart b/lib/services/analytics_service.dart new file mode 100644 index 0000000..5fc642c --- /dev/null +++ b/lib/services/analytics_service.dart @@ -0,0 +1,54 @@ +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:flutter/foundation.dart'; + +/// Service wrapper for Google Analytics +class AnalyticsService { + final FirebaseAnalytics _analytics = FirebaseAnalytics.instance; + + FirebaseAnalyticsObserver getAnalyticsObserver() => + FirebaseAnalyticsObserver(analytics: _analytics); + + Future logEvent({ + required String name, + Map? parameters, + }) async { + try { + await _analytics.logEvent(name: name, parameters: parameters); + } catch (e) { + debugPrint('Error logging analytics event: $e'); + } + } + + Future setUserProperty({ + required String name, + required String? value, + }) async { + try { + await _analytics.setUserProperty(name: name, value: value); + } catch (e) { + debugPrint('Error setting user property: $e'); + } + } + + Future setUserId(String? id) async { + try { + await _analytics.setUserId(id: id); + } catch (e) { + debugPrint('Error setting user ID: $e'); + } + } + + Future logScreenView({ + required String screenName, + String? screenClass, + }) async { + try { + await _analytics.logScreenView( + screenName: screenName, + screenClass: screenClass, + ); + } catch (e) { + debugPrint('Error logging screen view: $e'); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 9600d52..af41ef3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "8a1f5f3020ef2a74fb93f7ab3ef127a8feea33a7a2276279113660784ee7516a" + sha256: e4a1b612fd2955908e26116075b3a4baf10c353418ca645b4deae231c82bf144 url: "https://pub.dev" source: hosted - version: "1.3.64" + version: "1.3.65" analyzer: dependency: transitive description: @@ -401,6 +401,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+4" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + sha256: "8ca4832c7a6d145ce987fd07d6dfbb8c91d9058178342f20de6305fb77b1b40d" + url: "https://pub.dev" + source: hosted + version: "12.1.0" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + sha256: d00234716f415f89eb5c2cefb1238d7fd2f3120275d71414b84ae434dcdb7a19 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + sha256: e42b294e51aedb4bd4b761a886c8d6b473c44b44aa4c0b47cab06b2c66ac3fba + url: "https://pub.dev" + source: hosted + version: "0.6.1+1" firebase_auth: dependency: "direct main" description: @@ -429,10 +453,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "1f2dfd9f535d81f8b06d7a50ecda6eac1e6922191ed42e09ca2c84bd2288927c" + sha256: "29cfa93c771d8105484acac340b5ea0835be371672c91405a300303986f4eba9" url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.3.0" firebase_core_platform_interface: dependency: transitive description: @@ -445,10 +469,10 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: ff18fabb0ad0ed3595d2f2c85007ecc794aadecdff5b3bb1460b7ee47cded398 + sha256: a631bbfbfa26963d68046aed949df80b228964020e9155b086eff94f462bbf1f url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" firebase_messaging: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6aff066..de2f6df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,12 @@ dependencies: firebase_messaging: ^16.0.4 flutter_local_notifications: ^19.5.0 package_info_plus: ^8.3.1 +<<<<<<< Updated upstream +======= + google_maps_flutter_platform_interface: ^2.14.1 + google_maps_flutter_android: ^2.18.6 + firebase_analytics: ^12.1.0 +>>>>>>> Stashed changes dev_dependencies: flutter_launcher_icons: ^0.13.1