Skip to content

Add analytics layer #104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file.
3 changes: 3 additions & 0 deletions app/lib/presentation/ui/base/route_observer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import 'package:flutter/material.dart';

final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
109 changes: 109 additions & 0 deletions app/lib/presentation/ui/base/tracked_page.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic>? 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<String, dynamic>? get trackingProperties => null;

/// Build normal
Widget buildPage(BuildContext context);

@override
State<TrackedPage> createState() => _TrackedPageState();
}

class _TrackedPageState extends State<TrackedPage> with RouteAware {
ModalRoute? _route;

AnalyticsClient get analytics => getIt<AnalyticsClient>();

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);
}
}
34 changes: 34 additions & 0 deletions modules/common/lib/analytics/abstract/analytics_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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: {...});
/// 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});
Copy link
Preview

Copilot AI Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example shows tracking email addresses in analytics properties, which could be sensitive user data. Consider using hashed identifiers or removing PII from analytics examples to avoid accidental data exposure.

Suggested change
/// trackFunction(() => loginWithEmailPassword(email, password), 'login_triggered', properties: {email: email});
/// trackFunction(() => loginWithEmailPassword(email, password), 'login_triggered', properties: {user_id: hashedUserId});

Copilot uses AI. Check for mistakes.

Future trackFunction(
Copy link
Preview

Copilot AI Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Function type is too generic and doesn't provide type safety. Consider using FutureOr Function() or separate methods for sync/async functions to ensure proper type checking and runtime behavior.

Copilot uses AI. Check for mistakes.

FutureOr<void> Function() fn,
String name, {
Map<String, dynamic>? properties,
});

Future trackEvent(String name, {Map<String, dynamic>? properties});

Future setUserId(String? userId);

Future setUserProperties(Map<String, dynamic> properties);

Future setUserProperty(String name, String value);

Future reset();

Future trackAppCreated();

Future trackAppUpdated();

Future trackAppDeleted();
}
61 changes: 61 additions & 0 deletions modules/common/lib/analytics/concrete/firebase_analytics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'dart:async';

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<String, dynamic> 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<String, dynamic>? properties}) {
// TODO: implement trackEvent
throw UnimplementedError();
}

@override
Future trackFunction(
FutureOr<void> Function() fn,
String name, {
Map<String, dynamic>? properties,
}) =>
Future.value(fn()).then((_) => trackEvent(name, properties: properties));
}
7 changes: 7 additions & 0 deletions modules/common/lib/analytics/setup_analytics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

class SetupAnalytics {
static void initialize() {
// Initialize analytics services here
print("Analytics services initialized.");
Copy link
Preview

Copilot AI Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using print() statements for logging is not recommended in production code. Consider using a proper logging framework or removing debug prints before production deployment.

Copilot uses AI. Check for mistakes.

}
}