From d3094a0b64a42cc55f14f23b8d3d8af4b0d17350 Mon Sep 17 00:00:00 2001 From: Amaury Date: Tue, 22 Jul 2025 15:22:30 -0300 Subject: [PATCH 1/3] add analytics layer --- .../analytics/abstract/analytics_client.dart | 32 ++++++++++ .../concrete/firebase_analytics.dart | 58 +++++++++++++++++++ .../common/lib/analytics/setup_analytics.dart | 7 +++ 3 files changed, 97 insertions(+) create mode 100644 modules/common/lib/analytics/abstract/analytics_client.dart create mode 100644 modules/common/lib/analytics/concrete/firebase_analytics.dart create mode 100644 modules/common/lib/analytics/setup_analytics.dart diff --git a/modules/common/lib/analytics/abstract/analytics_client.dart b/modules/common/lib/analytics/abstract/analytics_client.dart new file mode 100644 index 0000000..3352d86 --- /dev/null +++ b/modules/common/lib/analytics/abstract/analytics_client.dart @@ -0,0 +1,32 @@ +/// Additional method to track custom events with a specific type. +/// Follow the naming convention for event types. +/// Future trackInitLoginFlow() => trackEvent('init_login', properties: {...}); +/// Future trackErrorLogin() => trackEvent('error_login', properties: {...}); + +abstract class AnalyticsClient { + /// Tracks an event with a function call and a name. + /// This is useful for tracking events that are triggered by specific actions. + /// Example usage: + /// trackFunction(() => loginWithEmailPassword(email, password), 'login_triggered', properties: {email: email}); + Future trackFunction( + Function fn, + String name, { + Map? properties, + }); + + Future trackEvent(String name, {Map? properties}); + + Future setUserId(String? userId); + + Future setUserProperties(Map properties); + + Future setUserProperty(String name, String value); + + Future reset(); + + Future trackAppCreated(); + + Future trackAppUpdated(); + + Future trackAppDeleted(); +} diff --git a/modules/common/lib/analytics/concrete/firebase_analytics.dart b/modules/common/lib/analytics/concrete/firebase_analytics.dart new file mode 100644 index 0000000..47727dc --- /dev/null +++ b/modules/common/lib/analytics/concrete/firebase_analytics.dart @@ -0,0 +1,58 @@ +import 'package:common/analytics/abstract/analytics_client.dart'; + +class FirebaseAnalytics implements AnalyticsClient { + @override + Future reset() { + // TODO: implement reset + throw UnimplementedError(); + } + + @override + Future setUserId(String? userId) { + // TODO: implement setUserId + throw UnimplementedError(); + } + + @override + Future setUserProperties(Map properties) { + // TODO: implement setUserProperties + throw UnimplementedError(); + } + + @override + Future setUserProperty(String name, String value) { + // TODO: implement setUserProperty + throw UnimplementedError(); + } + + @override + Future trackAppCreated() { + // TODO: implement trackAppCreated + throw UnimplementedError(); + } + + @override + Future trackAppDeleted() { + // TODO: implement trackAppDeleted + throw UnimplementedError(); + } + + @override + Future trackAppUpdated() { + // TODO: implement trackAppUpdated + throw UnimplementedError(); + } + + @override + Future trackEvent(String name, {Map? properties}) { + // TODO: implement trackEvent + throw UnimplementedError(); + } + + @override + Future trackFunction( + Function fn, + String name, { + Map? properties, + }) => fn().then((_) => trackEvent(name, properties: properties)); +} diff --git a/modules/common/lib/analytics/setup_analytics.dart b/modules/common/lib/analytics/setup_analytics.dart new file mode 100644 index 0000000..1689793 --- /dev/null +++ b/modules/common/lib/analytics/setup_analytics.dart @@ -0,0 +1,7 @@ + +class SetupAnalytics { + static void initialize() { + // Initialize analytics services here + print("Analytics services initialized."); + } +} From 3caeaec3674b1694d8d645daeedc6e765c8acf2f Mon Sep 17 00:00:00 2001 From: Amaury Date: Tue, 22 Jul 2025 15:31:25 -0300 Subject: [PATCH 2/3] fix copilot review --- .github/instructions/copilot-agent.instructions.md.new | 0 .github/instructions/flutter.instructions.md.new | 0 .../common/lib/analytics/abstract/analytics_client.dart | 4 +++- .../common/lib/analytics/concrete/firebase_analytics.dart | 7 +++++-- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .github/instructions/copilot-agent.instructions.md.new create mode 100644 .github/instructions/flutter.instructions.md.new diff --git a/.github/instructions/copilot-agent.instructions.md.new b/.github/instructions/copilot-agent.instructions.md.new new file mode 100644 index 0000000..e69de29 diff --git a/.github/instructions/flutter.instructions.md.new b/.github/instructions/flutter.instructions.md.new new file mode 100644 index 0000000..e69de29 diff --git a/modules/common/lib/analytics/abstract/analytics_client.dart b/modules/common/lib/analytics/abstract/analytics_client.dart index 3352d86..878a9d8 100644 --- a/modules/common/lib/analytics/abstract/analytics_client.dart +++ b/modules/common/lib/analytics/abstract/analytics_client.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + /// Additional method to track custom events with a specific type. /// Follow the naming convention for event types. /// Future trackInitLoginFlow() => trackEvent('init_login', properties: {...}); @@ -9,7 +11,7 @@ abstract class AnalyticsClient { /// Example usage: /// trackFunction(() => loginWithEmailPassword(email, password), 'login_triggered', properties: {email: email}); Future trackFunction( - Function fn, + FutureOr Function() fn, String name, { Map? properties, }); diff --git a/modules/common/lib/analytics/concrete/firebase_analytics.dart b/modules/common/lib/analytics/concrete/firebase_analytics.dart index 47727dc..eda689a 100644 --- a/modules/common/lib/analytics/concrete/firebase_analytics.dart +++ b/modules/common/lib/analytics/concrete/firebase_analytics.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:common/analytics/abstract/analytics_client.dart'; class FirebaseAnalytics implements AnalyticsClient { @@ -51,8 +53,9 @@ class FirebaseAnalytics implements AnalyticsClient { @override Future trackFunction( - Function fn, + FutureOr Function() fn, String name, { Map? properties, - }) => fn().then((_) => trackEvent(name, properties: properties)); + }) => + Future.value(fn()).then((_) => trackEvent(name, properties: properties)); } From fa6ca2f684ffc31444a0abd5c8c8416792f792fa Mon Sep 17 00:00:00 2001 From: Amaury Date: Wed, 23 Jul 2025 10:22:38 -0300 Subject: [PATCH 3/3] add automatic tracking page --- .../presentation/ui/base/route_observer.dart | 3 + .../presentation/ui/base/tracked_page.dart | 109 ++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 app/lib/presentation/ui/base/route_observer.dart create mode 100644 app/lib/presentation/ui/base/tracked_page.dart diff --git a/app/lib/presentation/ui/base/route_observer.dart b/app/lib/presentation/ui/base/route_observer.dart new file mode 100644 index 0000000..a453e93 --- /dev/null +++ b/app/lib/presentation/ui/base/route_observer.dart @@ -0,0 +1,3 @@ +import 'package:flutter/material.dart'; + +final RouteObserver routeObserver = RouteObserver(); diff --git a/app/lib/presentation/ui/base/tracked_page.dart b/app/lib/presentation/ui/base/tracked_page.dart new file mode 100644 index 0000000..e65a000 --- /dev/null +++ b/app/lib/presentation/ui/base/tracked_page.dart @@ -0,0 +1,109 @@ +import 'package:app/main/init.dart'; +import 'package:app/presentation/ui/base/route_observer.dart'; +import 'package:common/analytics/abstract/analytics_client.dart'; +import 'package:flutter/material.dart'; + +/// A base class for pages that are tracked for analytics purposes. +/// This class can be extended to implement specific tracking logic +/// for different pages in the application. +/// class HomePage extends TrackedPage { +/// const HomePage({super.key}); +/// @override +/// String get trackingName => "home_page"; +/// @override +/// Map? get trackingProperties => { +/// 'userType': 'guest', +/// 'origin': 'splash_screen', +/// }; +/// @override +/// Widget buildPage(BuildContext context) { +/// return Scaffold( +/// appBar: AppBar(title: const Text("Home")), +/// body: const Center(child: Text("Welcome")), +/// ); +/// } +///} +abstract class TrackedPage extends StatefulWidget { + const TrackedPage({super.key}); + + /// The name used for tracking this page. + String get trackingName; + + /// Automatic Events + /// Override when needed + bool get trackOnCreate => true; + bool get trackOnEnter => true; + bool get trackOnExit => true; + bool get trackOnDispose => false; + + /// Extra properties for tracking. + Map? get trackingProperties => null; + + /// Build normal + Widget buildPage(BuildContext context); + + @override + State createState() => _TrackedPageState(); +} + +class _TrackedPageState extends State with RouteAware { + ModalRoute? _route; + + AnalyticsClient get analytics => getIt(); + + void _track(String phase) { + analytics.trackEvent('${widget.trackingName}_$phase', + properties: widget.trackingProperties); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.trackOnCreate) _track("create"); + }); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _route = ModalRoute.of(context); + if (_route is PageRoute) { + routeObserver.subscribe(this, _route as PageRoute); + } + } + + @override + void dispose() { + if (widget.trackOnDispose) _track("dispose"); + if (_route is PageRoute) { + routeObserver.unsubscribe(this); + } + super.dispose(); + } + + @override + void didPush() { + if (widget.trackOnEnter) _track("enter"); + } + + @override + void didPopNext() { + if (widget.trackOnEnter) _track("enter"); + } + + @override + void didPushNext() { + if (widget.trackOnExit) _track("exit"); + } + + @override + void didPop() { + if (widget.trackOnExit) _track("exit"); + } + + @override + Widget build(BuildContext context) { + return widget.buildPage(context); + } +}