Bond Analytics¶
Bond Analytics provides type-safe event tracking with provider adapters, system events, and structured parameters. Track user behavior consistently across Firebase, custom analytics, and multiple providers simultaneously.
Why Bond Analytics?¶
Traditional Flutter analytics is fragmented and error-prone:
// ❌ Traditional approach - inconsistent and scattered
// Firebase Analytics
FirebaseAnalytics.instance.logEvent(
name: 'user_login',
parameters: {'method': 'email', 'user_id': userId},
);
// Custom Analytics
CustomAnalytics.track('user_login', {
'method': 'email',
'user_id': userId,
'timestamp': DateTime.now().toIso8601String(),
});
// Different parameter names across providers
MixpanelAnalytics.track('User Logged In', {
'login_method': 'email', // Different key name
'user_identifier': userId, // Different key name
});
Bond Analytics unifies all providers with type-safe events:
// ✅ Bond Analytics approach - unified and type-safe
class LoginEvent extends AnalyticsEvent with UserLoggedIn {
LoginEvent({required this.method, required this.userId});
final String method;
final String userId;
@override
String get key => 'user_logged_in';
@override
Map<String, dynamic> get params => {
'method': method,
'user_id': userId,
};
}
// Fire to all configured providers automatically
AppAnalytics.fire(LoginEvent(method: 'email', userId: user.id));
Quick Start¶
1. Setup in Service Provider¶
// lib/providers/analytics_service_provider.dart
class AnalyticsServiceProvider extends ServiceProvider {
@override
Future<void> register(GetIt it) async {
// Configure analytics providers
await AppAnalytics.configure(
providers: [
FirebaseAnalyticsProvider(),
MixpanelAnalyticsProvider(token: 'your_mixpanel_token'),
CustomAnalyticsProvider(),
],
enableDebugMode: AppConfig.isDebug,
);
}
}
2. Define Events¶
// lib/core/analytics/events.dart
class PurchaseEvent extends AnalyticsEvent {
PurchaseEvent({
required this.productId,
required this.amount,
required this.currency,
});
final String productId;
final double amount;
final String currency;
@override
String get key => 'purchase_completed';
@override
Map<String, dynamic> get params => {
'product_id': productId,
'amount': amount,
'currency': currency,
'timestamp': DateTime.now().toIso8601String(),
};
}
class ScreenViewEvent extends AnalyticsEvent {
ScreenViewEvent({required this.screenName, this.screenClass});
final String screenName;
final String? screenClass;
@override
String get key => 'screen_view';
@override
Map<String, dynamic> get params => {
'screen_name': screenName,
if (screenClass != null) 'screen_class': screenClass,
};
}
3. Fire Events¶
// In your widgets/controllers
class ProductPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Track screen view
AppAnalytics.fire(ScreenViewEvent(
screenName: 'product_page',
screenClass: 'ProductPage',
));
return Scaffold(
body: Column(
children: [
ProductDetails(),
ElevatedButton(
onPressed: () {
// Track purchase
AppAnalytics.fire(PurchaseEvent(
productId: product.id,
amount: product.price,
currency: 'USD',
));
// Process purchase
processPurchase();
},
child: Text('Buy Now'),
),
],
),
);
}
}
Event Types¶
Basic Events¶
// Simple event with parameters
class ButtonClickEvent extends AnalyticsEvent {
ButtonClickEvent({required this.buttonName, required this.location});
final String buttonName;
final String location;
@override
String get key => 'button_clicked';
@override
Map<String, dynamic> get params => {
'button_name': buttonName,
'location': location,
};
}
// Event without parameters
class AppOpenedEvent extends AnalyticsEvent {
@override
String get key => 'app_opened';
@override
Map<String, dynamic> get params => {};
}
System Events with Mixins¶
Bond Analytics provides pre-built mixins for common events:
// User authentication events
class LoginEvent extends AnalyticsEvent with UserLoggedIn {
LoginEvent({required this.method});
final String method;
@override
String get key => 'user_logged_in';
@override
Map<String, dynamic> get params => {'method': method};
}
class LogoutEvent extends AnalyticsEvent with UserLoggedOut {
@override
String get key => 'user_logged_out';
@override
Map<String, dynamic> get params => {};
}
class SignUpEvent extends AnalyticsEvent with UserSignedUp {
SignUpEvent({required this.method, required this.plan});
final String method;
final String plan;
@override
String get key => 'user_signed_up';
@override
Map<String, dynamic> get params => {
'method': method,
'plan': plan,
};
}
// E-commerce events
class PurchaseEvent extends AnalyticsEvent with PurchaseCompleted {
PurchaseEvent({
required this.transactionId,
required this.amount,
required this.currency,
required this.items,
});
final String transactionId;
final double amount;
final String currency;
final List<PurchaseItem> items;
@override
String get key => 'purchase_completed';
@override
Map<String, dynamic> get params => {
'transaction_id': transactionId,
'amount': amount,
'currency': currency,
'items': items.map((item) => item.toJson()).toList(),
};
}
class AddToCartEvent extends AnalyticsEvent with ItemAddedToCart {
AddToCartEvent({required this.item, required this.quantity});
final Product item;
final int quantity;
@override
String get key => 'add_to_cart';
@override
Map<String, dynamic> get params => {
'item_id': item.id,
'item_name': item.name,
'item_category': item.category,
'quantity': quantity,
'price': item.price,
};
}
// Content interaction events
class ContentViewEvent extends AnalyticsEvent with ContentViewed {
ContentViewEvent({required this.contentId, required this.contentType});
final String contentId;
final String contentType;
@override
String get key => 'content_viewed';
@override
Map<String, dynamic> get params => {
'content_id': contentId,
'content_type': contentType,
};
}
class ShareEvent extends AnalyticsEvent with ContentShared {
ShareEvent({required this.contentId, required this.method});
final String contentId;
final String method;
@override
String get key => 'content_shared';
@override
Map<String, dynamic> get params => {
'content_id': contentId,
'method': method,
};
}
Custom Events¶
// Feature-specific events
class SearchEvent extends AnalyticsEvent {
SearchEvent({
required this.query,
required this.category,
this.resultsCount,
});
final String query;
final String category;
final int? resultsCount;
@override
String get key => 'search_performed';
@override
Map<String, dynamic> get params => {
'query': query,
'category': category,
if (resultsCount != null) 'results_count': resultsCount,
};
}
class VideoEvent extends AnalyticsEvent {
VideoEvent({
required this.action,
required this.videoId,
required this.progress,
});
final String action; // 'play', 'pause', 'complete'
final String videoId;
final double progress; // 0.0 to 1.0
@override
String get key => 'video_${action}';
@override
Map<String, dynamic> get params => {
'video_id': videoId,
'progress': progress,
};
}
User Context¶
Set User Properties¶
// Set user ID (important for cross-device tracking)
AppAnalytics.setUserId(user.id);
// Set user properties
AppAnalytics.setUserProperties({
'plan': 'premium',
'registration_date': user.createdAt.toIso8601String(),
'age_group': user.ageGroup,
'country': user.country,
});
// Set individual property
AppAnalytics.setUserProperty('subscription_status', 'active');
// Clear user data (on logout)
AppAnalytics.clearUser();
User Lifecycle Tracking¶
// Track user lifecycle automatically
class AuthService {
Future<void> login(String email, String password) async {
try {
final user = await api.login(email, password);
// Set user context
AppAnalytics.setUserId(user.id);
AppAnalytics.setUserProperties({
'email': user.email,
'plan': user.plan,
'registration_date': user.createdAt.toIso8601String(),
});
// Track login event
AppAnalytics.fire(LoginEvent(method: 'email'));
} catch (e) {
AppAnalytics.fire(LoginFailedEvent(
method: 'email',
error: e.toString(),
));
}
}
Future<void> logout() async {
AppAnalytics.fire(LogoutEvent());
await api.logout();
AppAnalytics.clearUser();
}
}
Analytics Providers¶
Firebase Analytics Provider¶
class FirebaseAnalyticsProvider extends AnalyticsProvider {
final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
@override
Future<void> initialize() async {
await _analytics.setAnalyticsCollectionEnabled(true);
}
@override
Future<void> trackEvent(String eventName, Map<String, dynamic> parameters) async {
await _analytics.logEvent(
name: eventName,
parameters: parameters,
);
}
@override
Future<void> setUserId(String userId) async {
await _analytics.setUserId(id: userId);
}
@override
Future<void> setUserProperties(Map<String, dynamic> properties) async {
for (final entry in properties.entries) {
await _analytics.setUserProperty(
name: entry.key,
value: entry.value?.toString(),
);
}
}
@override
Future<void> clearUser() async {
await _analytics.setUserId(id: null);
}
}
Custom Analytics Provider¶
class CustomAnalyticsProvider extends AnalyticsProvider {
final Dio _dio = Dio();
final String _endpoint = 'https://analytics.yourapp.com/events';
@override
Future<void> initialize() async {
_dio.options.headers = {
'Authorization': 'Bearer ${AppConfig.analyticsToken}',
'Content-Type': 'application/json',
};
}
@override
Future<void> trackEvent(String eventName, Map<String, dynamic> parameters) async {
try {
await _dio.post(_endpoint, data: {
'event': eventName,
'properties': parameters,
'timestamp': DateTime.now().toIso8601String(),
'session_id': SessionManager.currentSessionId,
});
} catch (e) {
print('Failed to send analytics event: $e');
}
}
@override
Future<void> setUserId(String userId) async {
await _dio.post('$_endpoint/user', data: {
'user_id': userId,
'timestamp': DateTime.now().toIso8601String(),
});
}
@override
Future<void> setUserProperties(Map<String, dynamic> properties) async {
await _dio.put('$_endpoint/user/properties', data: properties);
}
@override
Future<void> clearUser() async {
await _dio.delete('$_endpoint/user');
}
}
Multiple Providers¶
// Configure multiple providers simultaneously
await AppAnalytics.configure(
providers: [
FirebaseAnalyticsProvider(),
MixpanelAnalyticsProvider(token: 'mixpanel_token'),
AmplitudeAnalyticsProvider(apiKey: 'amplitude_key'),
CustomAnalyticsProvider(),
],
);
// Events automatically go to all providers
AppAnalytics.fire(PurchaseEvent(...)); // Tracked in Firebase, Mixpanel, Amplitude, and Custom
Advanced Features¶
Conditional Events¶
// Only track in production
class ProductionOnlyEvent extends AnalyticsEvent {
@override
bool get shouldTrack => !AppConfig.isDebug;
@override
String get key => 'production_event';
@override
Map<String, dynamic> get params => {};
}
// Track based on user properties
class PremiumUserEvent extends AnalyticsEvent {
@override
bool get shouldTrack => UserService.currentUser?.isPremium ?? false;
@override
String get key => 'premium_feature_used';
@override
Map<String, dynamic> get params => {};
}
Event Batching¶
// Configure batching for performance
await AppAnalytics.configure(
providers: [...],
batchConfig: BatchConfig(
maxBatchSize: 50,
maxWaitTime: Duration(seconds: 30),
enableBatching: true,
),
);
// Events are automatically batched and sent together
AppAnalytics.fire(Event1());
AppAnalytics.fire(Event2());
AppAnalytics.fire(Event3());
// All three events sent in single batch after 30 seconds or 50 events
Event Validation¶
// Validate events before sending
class ValidatedEvent extends AnalyticsEvent {
ValidatedEvent({required this.userId, required this.action});
final String userId;
final String action;
@override
String get key => 'user_action';
@override
Map<String, dynamic> get params => {
'user_id': userId,
'action': action,
};
@override
bool validate() {
if (userId.isEmpty) {
print('Warning: user_id is empty for user_action event');
return false;
}
if (!['click', 'view', 'share'].contains(action)) {
print('Warning: invalid action "$action" for user_action event');
return false;
}
return true;
}
}
Event Transformation¶
// Transform events for specific providers
class TransformingAnalyticsProvider extends AnalyticsProvider {
final AnalyticsProvider _baseProvider;
TransformingAnalyticsProvider(this._baseProvider);
@override
Future<void> trackEvent(String eventName, Map<String, dynamic> parameters) async {
// Transform event name
final transformedName = eventName.replaceAll('_', ' ').toTitleCase();
// Transform parameters
final transformedParams = <String, dynamic>{};
for (final entry in parameters.entries) {
final key = entry.key.replaceAll('_', ' ').toTitleCase();
transformedParams[key] = entry.value;
}
await _baseProvider.trackEvent(transformedName, transformedParams);
}
}
Debugging and Testing¶
Debug Mode¶
// Enable debug mode to see all events in console
await AppAnalytics.configure(
providers: [...],
enableDebugMode: true,
);
// Debug output:
// [ANALYTICS] Event: user_logged_in
// [ANALYTICS] Params: {method: email, user_id: 123}
// [ANALYTICS] Providers: [Firebase, Mixpanel, Custom]
Firebase DebugView¶
// Enable Firebase DebugView for real-time event monitoring
class FirebaseAnalyticsProvider extends AnalyticsProvider {
@override
Future<void> initialize() async {
if (AppConfig.isDebug) {
// Enable debug mode for Firebase DebugView
await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(true);
}
}
}
// View events in Firebase Console > Analytics > DebugView
Analytics Testing¶
void main() {
group('Analytics Events', () {
late MockAnalyticsProvider mockProvider;
setUp(() {
mockProvider = MockAnalyticsProvider();
AppAnalytics.configure(providers: [mockProvider]);
});
test('should track login event with correct parameters', () {
final event = LoginEvent(method: 'email');
AppAnalytics.fire(event);
verify(mockProvider.trackEvent(
'user_logged_in',
{'method': 'email'},
)).called(1);
});
test('should set user properties on login', () {
AppAnalytics.setUserProperties({
'plan': 'premium',
'country': 'US',
});
verify(mockProvider.setUserProperties({
'plan': 'premium',
'country': 'US',
})).called(1);
});
});
}
class MockAnalyticsProvider extends Mock implements AnalyticsProvider {}
Integration Examples¶
With Bond Authentication¶
class AuthAnalytics {
static void trackLoginAttempt(String method) {
AppAnalytics.fire(LoginAttemptEvent(method: method));
}
static void trackLoginSuccess(User user, String method) {
AppAnalytics.setUserId(user.id);
AppAnalytics.setUserProperties({
'plan': user.plan,
'registration_date': user.createdAt.toIso8601String(),
'email_verified': user.emailVerified,
});
AppAnalytics.fire(LoginEvent(method: method));
}
static void trackLoginFailure(String method, String error) {
AppAnalytics.fire(LoginFailedEvent(
method: method,
error: error,
));
}
static void trackSignUp(User user, String method) {
AppAnalytics.setUserId(user.id);
AppAnalytics.fire(SignUpEvent(
method: method,
plan: user.plan,
));
}
}
With Bond Forms¶
class FormAnalytics {
static void trackFormStarted(String formName) {
AppAnalytics.fire(FormStartedEvent(formName: formName));
}
static void trackFormFieldChanged(String formName, String fieldName) {
AppAnalytics.fire(FormFieldChangedEvent(
formName: formName,
fieldName: fieldName,
));
}
static void trackFormSubmitted(String formName, bool success, String? error) {
AppAnalytics.fire(FormSubmittedEvent(
formName: formName,
success: success,
error: error,
));
}
static void trackFormAbandoned(String formName, double completionRate) {
AppAnalytics.fire(FormAbandonedEvent(
formName: formName,
completionRate: completionRate,
));
}
}
// Use in form controllers
class LoginFormController extends FormController {
@override
void onFormInitialized() {
FormAnalytics.trackFormStarted('login_form');
}
@override
void onFieldChanged(String fieldName, dynamic value) {
FormAnalytics.trackFormFieldChanged('login_form', fieldName);
}
@override
void onSubmissionSuccess(result) {
FormAnalytics.trackFormSubmitted('login_form', true, null);
}
@override
void onSubmissionError(error) {
FormAnalytics.trackFormSubmitted('login_form', false, error.toString());
}
}
With BondFire¶
// Track API performance and errors
class ApiAnalytics {
static void trackApiCall(String endpoint, String method) {
AppAnalytics.fire(ApiCallEvent(
endpoint: endpoint,
method: method,
));
}
static void trackApiSuccess(String endpoint, int statusCode, int responseTime) {
AppAnalytics.fire(ApiSuccessEvent(
endpoint: endpoint,
statusCode: statusCode,
responseTime: responseTime,
));
}
static void trackApiError(String endpoint, int? statusCode, String error) {
AppAnalytics.fire(ApiErrorEvent(
endpoint: endpoint,
statusCode: statusCode,
error: error,
));
}
}
// Custom BondFire interceptor
class AnalyticsInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
ApiAnalytics.trackApiCall(options.path, options.method);
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final responseTime = response.extra['response_time'] as int? ?? 0;
ApiAnalytics.trackApiSuccess(
response.requestOptions.path,
response.statusCode ?? 0,
responseTime,
);
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
ApiAnalytics.trackApiError(
err.requestOptions.path,
err.response?.statusCode,
err.message ?? 'Unknown error',
);
handler.next(err);
}
}
Best Practices¶
✅ Do's¶
// Use descriptive event names
class UserProfileUpdatedEvent extends AnalyticsEvent {
@override
String get key => 'user_profile_updated'; // Clear and descriptive
}
// Include relevant context
class PurchaseEvent extends AnalyticsEvent {
@override
Map<String, dynamic> get params => {
'product_id': productId,
'amount': amount,
'currency': currency,
'payment_method': paymentMethod,
'user_plan': userPlan,
'is_first_purchase': isFirstPurchase,
};
}
// Set user context early
void onUserLogin(User user) {
AppAnalytics.setUserId(user.id);
AppAnalytics.setUserProperties({
'plan': user.plan,
'registration_date': user.createdAt.toIso8601String(),
});
}
// Use system event mixins when available
class LoginEvent extends AnalyticsEvent with UserLoggedIn {
// Ensures consistent event structure across providers
}
❌ Don'ts¶
// Don't use generic event names
class Event extends AnalyticsEvent {
@override
String get key => 'event'; // ❌ Too generic
}
// Don't track sensitive information
class LoginEvent extends AnalyticsEvent {
@override
Map<String, dynamic> get params => {
'email': userEmail, // ❌ PII data
'password': password, // ❌ Sensitive data
};
}
// Don't forget to set user ID
AppAnalytics.fire(UserEvent()); // ❌ No user context
// Don't track everything
AppAnalytics.fire(MouseMoveEvent(x: x, y: y)); // ❌ Too granular
Troubleshooting¶
Common Issues¶
Issue: Events not appearing in Firebase
// ❌ Problem: Debug mode not enabled
await AppAnalytics.configure(
providers: [FirebaseAnalyticsProvider()],
);
// ✅ Solution: Enable debug mode and check DebugView
await AppAnalytics.configure(
providers: [FirebaseAnalyticsProvider()],
enableDebugMode: true, // See events in console
);
Issue: User properties not updating
// ❌ Problem: Setting properties before user ID
AppAnalytics.setUserProperties({'plan': 'premium'});
AppAnalytics.setUserId(user.id);
// ✅ Solution: Set user ID first
AppAnalytics.setUserId(user.id);
AppAnalytics.setUserProperties({'plan': 'premium'});
Issue: Events with invalid parameters
// ❌ Problem: Parameter values too long or invalid types
AppAnalytics.fire(SearchEvent(
query: veryLongString, // Firebase has 100 char limit
results: complexObject, // Should be primitive types
));
// ✅ Solution: Validate and truncate parameters
AppAnalytics.fire(SearchEvent(
query: query.length > 100 ? query.substring(0, 100) : query,
results: results.length, // Use count instead of object
));
Next Steps¶
- Bond Authentication - Track auth events and user lifecycle
- Bond Forms - Track form interactions and conversions
- BondFire Networking - Track API performance and errors
- Service Providers - Register analytics providers
Bond Analytics provides unified, type-safe event tracking across all your analytics providers. Start with basic events and gradually add more detailed tracking as your app grows! 🚀