FlexTrack π―
The Complete Analytics Routing Solution for Flutter Apps
Stop wrestling with multiple analytics services, GDPR compliance, and performance issues. FlexTrack gives you one simple API that intelligently handles everything.
π€ What Problem Does This Solve?
The Nightmare of Modern App Analytics
Imagine you're building an e-commerce Flutter app. You need:
- Firebase Analytics (free, basic metrics)
- Mixpanel (detailed user behavior)
- Amplitude (product analytics)
- Your own API (business intelligence)
- Console logging (debugging)
Without FlexTrack, your code looks like this mess:
// π± SCATTERED THROUGHOUT YOUR APP
void onUserPurchase(double amount) {
// Firebase
FirebaseAnalytics.instance.logEvent(
name: 'purchase',
parameters: {'amount': amount}
);
// Mixpanel - but only if user consented
if (userConsentedToTracking) {
MixpanelFlutter.getInstance().track('purchase', {
'amount': amount
});
}
// Amplitude - different format
Amplitude.getInstance().logEvent('purchase', {
'revenue': amount
});
// Your API - only in production
if (!kDebugMode) {
customAPI.sendEvent('purchase', {'amount': amount});
}
// Console - only in debug
if (kDebugMode) {
print('Purchase: $amount');
}
}
// π± REPEATED FOR EVERY EVENT TYPE
void onUserSignup() { /* same mess */ }
void onPageView() { /* same mess */ }
void onButtonClick() { /* same mess */ }
Problems with this approach:
- β Code duplicated everywhere
- β Hard to maintain
- β Easy to forget analytics calls
- β GDPR compliance is manual and error-prone
- β Performance issues (all services hit for every event)
- β Different formats for each service
- β Environment logic mixed with business logic
The FlexTrack Solution
With FlexTrack, the same functionality becomes:
// β
ONE SIMPLE CALL ANYWHERE
await FlexTrack.track(PurchaseEvent(amount: amount));
// FlexTrack automatically:
// β Sends to Firebase (always)
// β Sends to Mixpanel (only with consent)
// β Sends to Amplitude (only in production)
// β Sends to your API (only in production)
// β Prints to console (only in debug)
// β Handles all formatting differences
// β Respects GDPR consent
// β Applies performance optimizations
π How FlexTrack Works (Visual Guide)
Traditional Approach vs FlexTrack
TRADITIONAL APPROACH (Manual Management)
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Login β β Purchase β β Page View β
β Screen β β Screen β β Screen β
βββββββ¬ββββββββ βββββββ¬ββββββββ βββββββ¬ββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DUPLICATE CODE EVERYWHERE β
β βββββββββββ βββββββββββ βββββββββββ βββββββββββ β
β βFirebase β βMixpanel β βAmplitudeβ βCustom APIβ β
β βββββββββββ βββββββββββ βββββββββββ βββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
FLEXTRACK APPROACH (Centralized Management)
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Login β β Purchase β β Page View β
β Screen β β Screen β β Screen β
βββββββ¬ββββββββ βββββββ¬ββββββββ βββββββ¬ββββββββ
β β β
ββββββββββββββββββββΌβββββββββββββββββββ
βΌ
βββββββββββββββββββ
β FlexTrack βββββ ONE API
β (Smart Router) β
βββββββββββ¬ββββββββ
β
βββββββββββββββΌββββββββββββββ
βΌ βΌ βΌ
βββββββββββ βββββββββββ βββββββββββ
βFirebase β βMixpanel β βAmplitudeβ
βββββββββββ βββββββββββ βββββββββββ
FlexTrack Internal Architecture
EVENT FLOW THROUGH FLEXTRACK
βββββββββββββββββββ
β Your Event β βββ
β (PurchaseEvent) β β
βββββββββββββββββββ β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β FLEXTRACK CORE β
β β
β βββββββββββββββ βββββββββββββββ β
β β Event β β Routing β β
β β Processor ββββ Engine β β
β βββββββββββββββ βββββββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββ βββββββββββββββ β
β β Consent β β Sampling β β
β β Manager β β System β β
β βββββββββββββββ βββββββββββββββ β
βββββββββββββββββββ¬ββββββββββββββββββββ
β
βββββββββββββΌββββββββββββ
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ
βTracker 1 β βTracker 2 β βTracker 3 β
β(Firebase)β β(Mixpanel)β β(Console) β
ββββββββββββ ββββββββββββ ββββββββββββ
π Step-by-Step Setup Guide
Step 1: Installation
Add to your pubspec.yaml
:
dependencies:
flex_track: ^0.1.0
# If using Firebase
firebase_analytics: ^10.0.0
# If using Mixpanel
mixpanel_flutter: ^2.0.0
Step 2: Basic Setup (Beginner)
In your main.dart
:
import 'package:flex_track/flex_track.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// π― SETUP FLEXTRACK WITH ONE LINE
await FlexTrack.setup([
ConsoleTracker(), // Shows events in debug console
// Add your other trackers here later
]);
runApp(MyApp());
}
Step 3: Create Your First Event
Create a file lib/events/my_events.dart
:
import 'package:flex_track/flex_track.dart';
// π USER SIGNUP EVENT
class UserSignupEvent extends BaseEvent {
final String method; // 'email', 'google', 'apple'
final bool acceptedMarketing;
UserSignupEvent({
required this.method,
this.acceptedMarketing = false,
});
@override
String getName() => 'user_signup';
@override
Map<String, Object> getProperties() => {
'signup_method': method,
'accepted_marketing': acceptedMarketing,
'timestamp': DateTime.now().millisecondsSinceEpoch,
};
@override
EventCategory get category => EventCategory.business; // Important!
@override
bool get containsPII => false; // No personal data
}
// π° PURCHASE EVENT
class PurchaseEvent extends BaseEvent {
final String productId;
final double amount;
final String currency;
PurchaseEvent({
required this.productId,
required this.amount,
this.currency = 'USD',
});
@override
String getName() => 'purchase';
@override
Map<String, Object> getProperties() => {
'product_id': productId,
'amount': amount,
'currency': currency,
};
@override
EventCategory get category => EventCategory.business;
@override
bool get isEssential => true; // Never sample this!
}
// π±οΈ BUTTON CLICK EVENT
class ButtonClickEvent extends BaseEvent {
final String buttonId;
final String screenName;
ButtonClickEvent({
required this.buttonId,
required this.screenName,
});
@override
String getName() => 'button_click';
@override
Map<String, Object> getProperties() => {
'button_id': buttonId,
'screen_name': screenName,
};
@override
EventCategory get category => EventCategory.user;
@override
bool get isHighVolume => true; // Will be sampled
}
Step 4: Track Events in Your App
// In your signup screen
class SignupScreen extends StatelessWidget {
void _onSignupSuccess(String method) async {
// π― ONE LINE TO TRACK
await FlexTrack.track(UserSignupEvent(
method: method,
acceptedMarketing: true,
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
ElevatedButton(
onPressed: () => _onSignupSuccess('email'),
child: Text('Sign Up with Email'),
),
ElevatedButton(
onPressed: () => _onSignupSuccess('google'),
child: Text('Sign Up with Google'),
),
],
),
);
}
}
// In your purchase screen
class PurchaseScreen extends StatelessWidget {
void _onPurchaseComplete(String productId, double amount) async {
await FlexTrack.track(PurchaseEvent(
productId: productId,
amount: amount,
));
}
}
// Track button clicks automatically
class MyButton extends StatelessWidget {
final String id;
final VoidCallback onPressed;
final Widget child;
const MyButton({
required this.id,
required this.onPressed,
required this.child,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// Track the click
FlexTrack.track(ButtonClickEvent(
buttonId: id,
screenName: 'home', // You can make this dynamic
));
// Execute the original action
onPressed();
},
child: child,
);
}
}
Step 5: See Your Events in Action
Run your app in debug mode and watch the console:
π FlexTrack: user_signup (business)
Properties: {signup_method: email, accepted_marketing: true, timestamp: 1641234567890}
Flags: ESSENTIAL
π FlexTrack: button_click (user)
Properties: {button_id: signup_btn, screen_name: home}
Flags: HIGH_VOLUME
Congratulations! π You're now tracking events with FlexTrack!
π§ Adding Real Analytics Services
Adding Firebase Analytics
-
Setup Firebase (follow official Firebase setup guide)
-
Create Firebase Tracker:
// lib/trackers/firebase_tracker.dart
import 'package:flex_track/flex_track.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
class FirebaseTracker extends BaseTrackerStrategy {
late FirebaseAnalytics _analytics;
FirebaseTracker() : super(
id: 'firebase',
name: 'Firebase Analytics',
);
@override
bool get isGDPRCompliant => true;
@override
Future<void> doInitialize() async {
_analytics = FirebaseAnalytics.instance;
print('π₯ Firebase Analytics initialized');
}
@override
Future<void> doTrack(BaseEvent event) async {
await _analytics.logEvent(
name: event.getName(),
parameters: _convertProperties(event.getProperties()),
);
}
// Firebase has parameter name/value restrictions
Map<String, Object>? _convertProperties(Map<String, Object>? props) {
if (props == null) return null;
final converted = <String, Object>{};
props.forEach((key, value) {
// Firebase parameter names must be <= 40 chars, alphanumeric + underscore
final cleanKey = key.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
final shortKey = cleanKey.length > 40 ? cleanKey.substring(0, 40) : cleanKey;
converted[shortKey] = value;
});
return converted;
}
}
- Add to FlexTrack Setup:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlexTrack.setup([
ConsoleTracker(),
FirebaseTracker(), // π₯ Added Firebase!
]);
runApp(MyApp());
}
Now your events go to both Console (debug) and Firebase (production)!
Adding Mixpanel
- Add Mixpanel dependency:
dependencies:
mixpanel_flutter: ^2.0.0
- Create Mixpanel Tracker:
// lib/trackers/mixpanel_tracker.dart
import 'package:flex_track/flex_track.dart';
import 'package:mixpanel_flutter/mixpanel_flutter.dart';
class MixpanelTracker extends BaseTrackerStrategy {
late Mixpanel _mixpanel;
final String _token;
MixpanelTracker({required String token})
: _token = token,
super(
id: 'mixpanel',
name: 'Mixpanel Analytics',
);
@override
bool get isGDPRCompliant => true;
@override
Future<void> doInitialize() async {
_mixpanel = await Mixpanel.init(_token, trackAutomaticEvents: false);
print('π― Mixpanel initialized');
}
@override
Future<void> doTrack(BaseEvent event) async {
_mixpanel.track(event.getName(), properties: event.getProperties());
}
@override
Future<void> doSetUserProperties(Map<String, dynamic> properties) async {
_mixpanel.getPeople().set(properties);
}
@override
Future<void> doIdentifyUser(String userId, [Map<String, dynamic>? properties]) async {
_mixpanel.identify(userId);
if (properties != null) {
await doSetUserProperties(properties);
}
}
}
- Add to Setup:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlexTrack.setup([
ConsoleTracker(),
FirebaseTracker(),
MixpanelTracker(token: 'YOUR_MIXPANEL_TOKEN'), // π― Added Mixpanel!
]);
runApp(MyApp());
}
Adding Your Custom API
// lib/trackers/custom_api_tracker.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flex_track/flex_track.dart';
class CustomAPITracker extends BaseTrackerStrategy {
final String _baseUrl;
final String? _apiKey;
CustomAPITracker({
required String baseUrl,
String? apiKey,
}) : _baseUrl = baseUrl,
_apiKey = apiKey,
super(
id: 'custom_api',
name: 'Custom API Tracker',
);
@override
Future<void> doInitialize() async {
// Test API connection
try {
await http.get(Uri.parse('$_baseUrl/health'));
print('π Custom API tracker initialized');
} catch (e) {
throw Exception('Failed to connect to custom API: $e');
}
}
@override
Future<void> doTrack(BaseEvent event) async {
final payload = {
'event_name': event.getName(),
'properties': event.getProperties(),
'timestamp': DateTime.now().toIso8601String(),
'category': event.category?.name,
};
await http.post(
Uri.parse('$_baseUrl/events'),
headers: {
'Content-Type': 'application/json',
if (_apiKey != null) 'Authorization': 'Bearer $_apiKey',
},
body: jsonEncode(payload),
);
}
}
π― Smart Routing (The Real Power)
Now that you have multiple trackers, you want different events to go to different places. This is where FlexTrack really shines!
Basic Routing Examples
await FlexTrack.setupWithRouting([
ConsoleTracker(),
FirebaseTracker(),
MixpanelTracker(token: 'your-token'),
CustomAPITracker(baseUrl: 'https://your-api.com'),
], (routing) => routing
// π EXAMPLE 1: Route by event category
.routeCategory(EventCategory.business)
.toAll() // Send business events to ALL trackers
.and()
// π€ EXAMPLE 2: Route user behavior only to Mixpanel
.routeCategory(EventCategory.user)
.to(['mixpanel']) // Only to Mixpanel
.and()
// π EXAMPLE 3: Debug events only to console in development
.routeMatching(RegExp(r'debug_.*'))
.to(['console'])
.onlyInDebug()
.and()
// π― EXAMPLE 4: Everything else goes to Firebase and Console
.routeDefault()
.to(['firebase', 'console'])
);
Real-World Routing Scenarios
Scenario 1: E-commerce App
routing
// π° CRITICAL BUSINESS EVENTS - Go everywhere, never sample
.routeCategory(EventCategory.business)
.toAll()
.noSampling() // 100% of events
.withPriority(20) // High priority
.and()
// π SHOPPING BEHAVIOR - Only to detailed analytics
.routeMatching(RegExp(r'(cart|product|checkout)_.*'))
.to(['mixpanel', 'custom_api'])
.and()
// π±οΈ UI INTERACTIONS - High volume, sample heavily
.routeMatching(RegExp(r'(click|scroll|hover)_.*'))
.toAll()
.heavySampling() // Only 1% of events
.and()
// π PAGE VIEWS - Medium volume
.routeMatching(RegExp(r'page_view'))
.to(['firebase', 'mixpanel'])
.lightSampling() // 10% of events
.and()
// π EVERYTHING ELSE - Basic tracking
.routeDefault()
.to(['firebase', 'console'])
.mediumSampling() // 50% of events
Scenario 2: SaaS App with Privacy Requirements
routing
// Define tracker groups first
.defineGroup('privacy_safe', ['firebase', 'console'])
.defineGroup('full_analytics', ['firebase', 'mixpanel', 'amplitude'])
.defineGroup('internal_only', ['custom_api', 'console'])
// π SENSITIVE DATA - Only to privacy-safe trackers
.routeCategory(EventCategory.sensitive)
.toGroupNamed('privacy_safe')
.requirePIIConsent() // Must have PII consent
.noSampling()
.and()
// π€ USER DATA - Requires consent
.routePII() // Events marked as containing PII
.toGroupNamed('privacy_safe')
.requirePIIConsent()
.and()
// πΌ BUSINESS METRICS - Internal tracking only
.routeWithProperty('internal_metric')
.toGroupNamed('internal_only')
.skipConsent() // Legitimate business interest
.and()
// π GENERAL ANALYTICS - Full tracking with consent
.routeDefault()
.toGroupNamed('full_analytics')
.requireConsent()
Understanding Routing Priority
Events are matched against rules in priority order (highest first):
routing
// Priority 30 - Checked first
.routeEssential()
.toAll()
.withPriority(30)
.and()
// Priority 20 - Checked second
.routeCategory(EventCategory.business)
.toAll()
.withPriority(20)
.and()
// Priority 10 - Checked third
.routeHighVolume()
.to(['firebase'])
.withPriority(10)
.and()
// Priority 0 - Checked last (default)
.routeDefault()
.toAll()
.withPriority(0)
Visual Priority Flow:
Event: PurchaseEvent (business, essential)
β
Priority 30: routeEssential() β MATCHES! (Goes to all trackers)
β
Priority 20: routeCategory(business) β Would match but skipped
β
Priority 10: routeHighVolume() β Skipped
β
Priority 0: routeDefault() β Skipped
π Event Categories Explained
Event categories help FlexTrack automatically route events. Here's what each category means:
EventCategory.business
What: Revenue, conversions, subscriptions, critical business metrics
Characteristics: High priority, never sampled, goes to all analytics
Examples:
class PurchaseEvent extends BaseEvent {
@override
EventCategory get category => EventCategory.business;
}
class SubscriptionEvent extends BaseEvent {
@override
EventCategory get category => EventCategory.business;
}
EventCategory.user
What: User behavior, preferences, actions
Characteristics: Requires consent, may be sampled
Examples:
class ProfileUpdateEvent extends BaseEvent {
@override
EventCategory get category => EventCategory.user;
}
class SettingsChangeEvent extends BaseEvent {
@override
EventCategory get category => EventCategory.user;
}
EventCategory.technical
What: Errors, performance, debugging info
Characteristics: Often debug-only, may skip consent (legitimate interest)
Examples:
class ErrorEvent extends BaseEvent {
@override
EventCategory get category => EventCategory.technical;
}
class PerformanceEvent extends BaseEvent {
@override
EventCategory get category => EventCategory.technical;
}
EventCategory.sensitive
What: Events with personal data
Characteristics: Requires PII consent, privacy-safe trackers only
Examples:
class LocationEvent extends BaseEvent {
@override
EventCategory get category => EventCategory.sensitive;
@override
bool get containsPII => true;
}
EventCategory.marketing
What: Campaign tracking, attribution, ads
Characteristics: Requires marketing consent
Examples:
class AdClickEvent extends BaseEvent {
@override
EventCategory get category => EventCategory.marketing;
}
EventCategory.system
What: App lifecycle, health checks, system status
Characteristics: Usually essential, no consent required
Examples:
class AppStartEvent extends BaseEvent {
@override
EventCategory get category => EventCategory.system;
@override
bool get isEssential => true;
@override
bool get requiresConsent => false;
}
π‘οΈ GDPR Compliance Made Easy
FlexTrack handles GDPR automatically once you set it up correctly.
Understanding Consent Types
// Set consent status (usually from your privacy settings screen)
FlexTrack.setConsent(
general: true, // Can track general analytics
pii: false, // Cannot track personal information
);
// Check current consent
final consent = FlexTrack.getConsentStatus();
print('General consent: ${consent['general']}');
print('PII consent: ${consent['pii']}');
How Events Are Filtered by Consent
USER HAS GENERAL CONSENT = true, PII CONSENT = false
Event: LoginEvent (requires general consent)
β
Has general consent β Event is tracked
Event: ProfileUpdateEvent (requires PII consent)
β No PII consent β Event is blocked
Event: CrashReportEvent (essential, no consent required)
β
Essential event β Always tracked
Automatic GDPR Routing
await FlexTrack.setupWithRouting([
FirebaseTracker(),
MixpanelTracker(token: 'token'),
GDPRCompliantTracker(), // Your privacy-safe tracker
], (routing) {
// Apply GDPR defaults - this sets up intelligent consent handling
GDPRDefaults.apply(routing, compliantTrackers: ['gdpr_compliant']);
return routing;
});
What GDPRDefaults.apply()
does automatically:
// Equivalent manual setup:
routing
// PII events only to compliant trackers
.routePII()
.toGroupNamed('gdpr_compliant')
.requirePIIConsent()
.noSampling()
.and()
// Sensitive category requires PII consent
.routeCategory(EventCategory.sensitive)
.toGroupNamed('gdpr_compliant')
.requirePIIConsent()
.and()
// Essential events bypass consent
.routeEssential()
.toAll()
.skipConsent()
.and()
// Everything else requires general consent
.routeDefault()
.toAll()
.requireConsent()
Complete GDPR Setup Example
// 1. Create your events with proper PII flags
class UserProfileEvent extends BaseEvent {
final String email;
final String name;
@override
bool get containsPII => true; // π Mark as containing PII
@override
EventCategory get category => EventCategory.sensitive;
}
class CrashReportEvent extends BaseEvent {
@override
bool get isEssential => true; // β
Always track (legitimate interest)
@override
bool get requiresConsent => false;
}
// 2. Set up privacy-compliant routing
void main() async {
await FlexTrack.setupWithRouting([
ConsoleTracker(),
FirebaseTracker(), // GDPR compliant
MixpanelTracker(token: 'token'), // Check their privacy policy!
YourInternalAPI(), // Your compliant tracker
], (routing) {
// Apply GDPR rules automatically
GDPRDefaults.apply(routing, compliantTrackers: [
'firebase',
'your_internal_api'
]);
return routing;
});
}
// 3. Handle consent in your app
class PrivacySettingsScreen extends StatefulWidget {
@override
_PrivacySettingsScreenState createState() => _PrivacySettingsScreenState();
}
class _PrivacySettingsScreenState extends State<PrivacySettingsScreen> {
bool _generalConsent = false;
bool _piiConsent = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Privacy Settings')),
body: Column(
children: [
SwitchListTile(
title: Text('General Analytics'),
subtitle: Text('Help us improve the app'),
value: _generalConsent,
onChanged: (value) {
setState(() => _generalConsent = value);
_updateConsent();
},
),
SwitchListTile(
title: Text('Personalization'),
subtitle: Text('Personalized content and recommendations'),
value: _piiConsent,
onChanged: (value) {
setState(() => _piiConsent = value);
_updateConsent();
},
),
],
),
);
}
void _updateConsent() {
FlexTrack.setConsent(
general: _generalConsent,
pii: _piiConsent,
);
}
}
β‘ Performance Optimization
FlexTrack includes several performance features to keep your app fast.
Sampling (Reducing Event Volume)
Sampling reduces the number of events sent to prevent performance issues and reduce costs.
Sampling Rates:
noSampling()
= 100% of events (1.0)lightSampling()
= 10% of events (0.1)mediumSampling()
= 50% of events (0.5)heavySampling()
= 1% of events (0.01)sample(0.25)
= 25% of events (custom)
When to use each:
routing
// π° NEVER sample business events - too important
.routeCategory(EventCategory.business)
.toAll()
.noSampling() // 100%
.and()
// π PAGE VIEWS - Medium volume, light sampling
.routeMatching(RegExp(r'page_view'))
.toAll()
.lightSampling() // 10%
.and()
// π±οΈ BUTTON CLICKS - High volume, medium sampling
.routeMatching(RegExp(r'button_click'))
.toAll()
.mediumSampling() // 50%
.and()
// π SCROLL EVENTS - Very high volume, heavy sampling
.routeMatching(RegExp(r'scroll_.*'))
.toAll()
.heavySampling() // 1%
.and()
// π¨ MOUSE MOVES - Extremely high volume, minimal sampling
.routeMatching(RegExp(r'mouse_move'))
.toAll()
.sample(0.001) // 0.1%
Batch Processing
Send multiple events at once for better performance:
// Instead of multiple individual calls:
await FlexTrack.track(Event1());
await FlexTrack.track(Event2());
await FlexTrack.track(Event3());
// Batch them together:
await FlexTrack.trackAll([
Event1(),
Event2(),
Event3(),
]);
Performance Presets
FlexTrack includes performance presets for common scenarios:
await FlexTrack.setupWithRouting([
ConsoleTracker(),
FirebaseTracker(),
], (routing) {
// Apply performance optimizations automatically
PerformanceDefaults.apply(routing);
return routing;
});
What PerformanceDefaults.apply()
does:
// Equivalent manual setup:
routing
// High volume events get aggressive sampling
.routeHighVolume()
.toAll()
.heavySampling() // 1%
.and()
// UI interactions are high volume
.routeMatching(RegExp(r'(click|scroll|hover)_.*'))
.toAll()
.heavySampling()
.and()
// Critical events never sampled
.routeMatching(RegExp(r'(purchase|error|crash)_.*'))
.toAll()
.noSampling()
.and()
// Default moderate sampling
.routeDefault()
.toAll()
.mediumSampling() // 50%
Platform-Specific Performance
// Mobile apps need more aggressive optimization
PerformanceDefaults.applyMobileOptimized(routing);
// Web apps have different patterns
PerformanceDefaults.applyWebOptimized(routing);
// Server/backend optimization
PerformanceDefaults.applyServerOptimized(routing);
π Debugging and Development
Console Tracker Features
The console tracker is your best friend during development:
ConsoleTracker(
showProperties: true, // Show all event properties
showTimestamps: true, // Show when events happened
colorOutput: true, // Colored output for better readability
prefix: 'π― MyApp', // Custom prefix
)
Console Output Examples:
π― MyApp: [14:23:45.123] user_signup (business) [User: user123]
Properties: {
signup_method: email,
accepted_marketing: true,
timestamp: 1641234567890
}
Flags: ESSENTIAL
π― MyApp: [14:23:46.456] button_click (user)
Properties: {
button_id: header_logo,
screen_name: home
}
Flags: HIGH_VOLUME
Event Routing Debugger
See exactly how your events are being routed:
// Debug a specific event
final event = PurchaseEvent(productId: 'abc', amount: 99.99);
final debugInfo = FlexTrack.debugEvent(event);
print('Event: ${debugInfo.event.getName()}');
print('Target trackers: ${debugInfo.routingResult.targetTrackers}');
print('Applied rules: ${debugInfo.routingResult.appliedRules.length}');
print('Skipped rules: ${debugInfo.routingResult.skippedRules.length}');
// Print detailed information
for (final rule in debugInfo.routingResult.appliedRules) {
print('β
Applied: ${rule.description}');
}
for (final skipped in debugInfo.routingResult.skippedRules) {
print('β Skipped: ${skipped.rule.description} - ${skipped.reason}');
}
Example Debug Output:
Event: purchase
Target trackers: [firebase, mixpanel, custom_api]
Applied rules: 1
Skipped rules: 2
β
Applied: business events to all trackers (priority: 20)
β Skipped: high volume events to firebase only - Event not high volume
β Skipped: debug events to console - Event name doesn't match pattern
System Debug Information
Get comprehensive information about FlexTrack's status:
// Print debug info to console
FlexTrack.printDebugInfo();
// Or get as data
final debugInfo = FlexTrack.getDebugInfo();
print('Is setup: ${debugInfo['isSetUp']}');
print('Is enabled: ${debugInfo['isEnabled']}');
print('Tracker count: ${debugInfo['eventProcessor']['trackerRegistry']['trackerCount']}');
Example System Debug Output:
=== FlexTrack Debug Info ===
Setup: true
Initialized: true
Enabled: true
Trackers: 4 registered, 4 enabled
Consent: General=true, PII=false
Configuration Validation
Check for configuration issues:
final issues = FlexTrack.validate();
if (issues.isEmpty) {
print('β
Configuration is valid!');
} else {
print('β οΈ Configuration issues found:');
for (final issue in issues) {
print(' β’ $issue');
}
}
Example Validation Output:
β οΈ Configuration issues found:
β’ No default routing rule specified
β’ Tracker 'mixpanel' is not GDPR compliant but receives PII events
β’ Sample rate 1.5 is invalid (must be between 0.0 and 1.0)
Development vs Production
Set up different behavior for development and production:
await FlexTrack.setupWithRouting([
ConsoleTracker(),
FirebaseTracker(),
if (!kDebugMode) MixpanelTracker(token: 'prod-token'),
if (kDebugMode) MockTracker(),
], (routing) => routing
// Debug events only in development
.routeMatching(RegExp(r'debug_.*'))
.to(['console'])
.onlyInDebug()
.and()
// Production analytics only in production
.routeCategory(EventCategory.business)
.to(kDebugMode ? ['console'] : ['firebase', 'mixpanel'])
.and()
// Default routing
.routeDefault()
.to(['console', 'firebase'])
);
π§ͺ Testing Your Analytics
Mock Tracker for Testing
FlexTrack includes a mock tracker perfect for unit tests:
import 'package:flutter_test/flutter_test.dart';
import 'package:flex_track/flex_track.dart';
void main() {
group('Analytics Tests', () {
late MockTracker mockTracker;
setUp(() async {
// Setup FlexTrack with mock tracker
mockTracker = await setupFlexTrackForTesting();
});
testWidgets('should track user signup', (tester) async {
// Act
await FlexTrack.track(UserSignupEvent(method: 'email'));
// Assert
expect(mockTracker.capturedEvents, hasLength(1));
final event = mockTracker.capturedEvents.first;
expect(event.getName(), equals('user_signup'));
final properties = event.getProperties();
expect(properties?['signup_method'], equals('email'));
});
testWidgets('should track purchase with correct amount', (tester) async {
// Act
await FlexTrack.track(PurchaseEvent(
productId: 'test_product',
amount: 99.99,
));
// Assert
expect(mockTracker.capturedEvents, hasLength(1));
final event = mockTracker.capturedEvents.first;
expect(event.getName(), equals('purchase'));
expect(event.getProperties()?['amount'], equals(99.99));
expect(event.getProperties()?['product_id'], equals('test_product'));
});
testWidgets('should not track when disabled', (tester) async {
// Arrange
FlexTrack.disable();
// Act
await FlexTrack.track(UserSignupEvent(method: 'email'));
// Assert
expect(mockTracker.capturedEvents, isEmpty);
// Cleanup
FlexTrack.enable();
});
testWidgets('should respect consent settings', (tester) async {
// Arrange
FlexTrack.setConsent(general: false, pii: false);
// Act - event requires consent
await FlexTrack.track(UserSignupEvent(method: 'email'));
// Assert - should be blocked
expect(mockTracker.capturedEvents, isEmpty);
// Arrange - grant consent
FlexTrack.setConsent(general: true);
// Act
await FlexTrack.track(UserSignupEvent(method: 'email'));
// Assert - should be tracked
expect(mockTracker.capturedEvents, hasLength(1));
});
});
}
Testing Custom Trackers
void main() {
group('Custom Tracker Tests', () {
test('should initialize correctly', () async {
final tracker = MyCustomTracker();
expect(tracker.isEnabled, isTrue);
expect(tracker.id, equals('my_custom'));
await tracker.initialize();
// Add your specific initialization tests
});
test('should track events correctly', () async {
final tracker = MyCustomTracker();
await tracker.initialize();
final event = UserSignupEvent(method: 'email');
await tracker.track(event);
// Verify your tracker's behavior
// This depends on your implementation
});
});
}
Integration Testing
Test the complete flow in widget tests:
testWidgets('complete user signup flow', (tester) async {
// Setup
final mockTracker = await setupFlexTrackForTesting();
// Build the signup screen
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// Navigate to signup
await tester.tap(find.text('Sign Up'));
await tester.pumpAndSettle();
// Fill in form
await tester.enterText(find.byKey(Key('email_field')), 'test@example.com');
await tester.enterText(find.byKey(Key('password_field')), 'password123');
// Submit form
await tester.tap(find.text('Create Account'));
await tester.pumpAndSettle();
// Verify analytics were tracked
expect(mockTracker.capturedEvents.length, greaterThan(0));
final signupEvent = mockTracker.capturedEvents.firstWhere(
(event) => event.getName() == 'user_signup',
);
expect(signupEvent, isNotNull);
expect(signupEvent.getProperties()?['signup_method'], equals('email'));
});
π¨ Common Pitfalls and Solutions
Problem 1: Events Not Appearing
Symptoms:
- No events in console
- Analytics dashboard shows no data
Common Causes & Solutions:
// β WRONG: FlexTrack not initialized
void main() {
runApp(MyApp()); // No FlexTrack setup!
}
// β
CORRECT: Initialize FlexTrack
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlexTrack.setup([ConsoleTracker()]);
runApp(MyApp());
}
// β WRONG: Tracking before initialization
void main() async {
FlexTrack.track(AppStartEvent()); // Too early!
await FlexTrack.setup([ConsoleTracker()]);
runApp(MyApp());
}
// β
CORRECT: Track after initialization
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlexTrack.setup([ConsoleTracker()]);
await FlexTrack.track(AppStartEvent()); // Now it works!
runApp(MyApp());
}
Debug Steps:
- Check console for FlexTrack initialization messages
- Verify trackers are enabled:
FlexTrack.printDebugInfo()
- Test with ConsoleTracker first
- Check if FlexTrack is disabled:
FlexTrack.isEnabled
Problem 2: GDPR Consent Issues
Symptoms:
- Events blocked unexpectedly
- Some events work, others don't
Common Causes & Solutions:
// β WRONG: Not setting consent
FlexTrack.track(UserProfileEvent()); // Blocked! Requires consent
// β
CORRECT: Set consent first
FlexTrack.setConsent(general: true, pii: true);
await FlexTrack.track(UserProfileEvent()); // Now works!
// β WRONG: PII event without PII consent
class EmailEvent extends BaseEvent {
@override
bool get containsPII => true; // Requires PII consent
}
FlexTrack.setConsent(general: true); // No PII consent!
await FlexTrack.track(EmailEvent()); // Blocked!
// β
CORRECT: Grant PII consent
FlexTrack.setConsent(general: true, pii: true);
await FlexTrack.track(EmailEvent()); // Works!
Debug Steps:
- Check consent status:
FlexTrack.getConsentStatus()
- Debug event routing:
FlexTrack.debugEvent(yourEvent)
- Look for "consent" in skipped rules
- Mark essential events:
@override bool get isEssential => true;
Problem 3: Performance Issues
Symptoms:
- App feels slow
- High network usage
- Analytics costs too high
Solutions:
// β WRONG: No sampling on high-volume events
class ScrollEvent extends BaseEvent {
@override
bool get isHighVolume => true; // But no sampling configured!
}
// β
CORRECT: Configure sampling
routing
.routeHighVolume()
.toAll()
.heavySampling() // Only 1% of scroll events
.and()
// β WRONG: Individual tracking of many events
for (final item in items) {
await FlexTrack.track(ItemViewEvent(itemId: item.id));
}
// β
CORRECT: Batch tracking
final events = items.map((item) => ItemViewEvent(itemId: item.id)).toList();
await FlexTrack.trackAll(events);
Problem 4: Routing Not Working
Symptoms:
- Events going to wrong trackers
- Debug events appearing in production
Common Issues:
// β WRONG: Rules in wrong order (priority issue)
routing
.routeDefault().toAll().and() // Priority 0 - matches everything first!
.routeCategory(EventCategory.business).to(['firebase']).and() // Never reached!
// β
CORRECT: Specific rules first, default last
routing
.routeCategory(EventCategory.business).to(['firebase']).withPriority(10).and()
.routeDefault().toAll().withPriority(0).and() // Default has lowest priority
// β WRONG: Environment conditions backwards
routing
.routeMatching(RegExp(r'debug_.*'))
.toAll()
.onlyInProduction() // Debug events in PRODUCTION?!
.and()
// β
CORRECT: Debug events in debug mode
routing
.routeMatching(RegExp(r'debug_.*'))
.to(['console'])
.onlyInDebug()
.and()
Problem 5: Custom Tracker Issues
Common Implementation Mistakes:
// β WRONG: Not calling super.doInitialize()
class MyTracker extends BaseTrackerStrategy {
@override
Future<void> doInitialize() async {
// Missing super call!
await mySDK.initialize();
}
}
// β
CORRECT: Always call parent methods when overriding
class MyTracker extends BaseTrackerStrategy {
@override
Future<void> doInitialize() async {
await super.doInitialize(); // Don't forget this!
await mySDK.initialize();
}
}
// β WRONG: Not handling errors
@override
Future<void> doTrack(BaseEvent event) async {
await myAPI.send(event.getName()); // What if this fails?
}
// β
CORRECT: Handle errors gracefully
@override
Future<void> doTrack(BaseEvent event) async {
try {
await myAPI.send(event.getName());
} catch (e) {
// Log error but don't crash the app
print('Failed to track ${event.getName()}: $e');
// FlexTrack will handle the TrackerException
}
}
π Real-World Complete Examples
Example 1: E-commerce Flutter App
Complete setup for a shopping app with multiple analytics needs:
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await setupECommerceAnalytics();
runApp(ECommerceApp());
}
Future<void> setupECommerceAnalytics() async {
await FlexTrack.setupWithRouting([
// Development
ConsoleTracker(showProperties: true, colorOutput: true),
// Free analytics
FirebaseTracker(),
// Paid analytics for detailed insights
MixpanelTracker(token: 'YOUR_MIXPANEL_TOKEN'),
// Revenue analytics
AmplitudeTracker(apiKey: 'YOUR_AMPLITUDE_KEY'),
// Internal business intelligence
CustomAPITracker(
baseUrl: 'https://analytics.yourstore.com',
apiKey: 'your-api-key',
),
], (routing) => routing
// Define tracker groups
.defineGroup('free_analytics', ['console', 'firebase'])
.defineGroup('paid_analytics', ['mixpanel', 'amplitude'])
.defineGroup('business_intel', ['custom_api'])
.defineGroup('all_external', ['firebase', 'mixpanel', 'amplitude'])
// π° REVENUE EVENTS - Highest priority, all trackers, never sample
.routeMatching(RegExp(r'(purchase|refund|subscription)_.*'))
.toAll()
.noSampling()
.withPriority(30)
.withDescription('Critical revenue events')
.and()
// π SHOPPING FUNNEL - Detailed analytics only
.routeMatching(RegExp(r'(product_view|add_to_cart|checkout_start|checkout_complete)'))
.toGroupNamed('paid_analytics')
.noSampling()
.withPriority(25)
.and()
// π SEARCH & DISCOVERY - User behavior insights
.routeMatching(RegExp(r'(search|filter|sort|category_view)'))
.toGroupNamed('paid_analytics')
.lightSampling()
.withPriority(20)
.and()
// π± UI INTERACTIONS - High volume, heavy sampling
.routeMatching(RegExp(r'(tap|swipe|scroll|zoom)'))
.toGroupNamed('free_analytics')
.heavySampling()
.withPriority(10)
.and()
// π DEBUG EVENTS - Development only
.routeMatching(RegExp(r'debug_.*'))
.to(['console'])
.onlyInDebug()
.withPriority(35)
.and()
// π BUSINESS METRICS - Internal tracking
.routeWithProperty('business_metric')
.toGroupNamed('business_intel')
.requireConsent()
.withPriority(15)
.and()
// π DEFAULT - All external analytics
.routeDefault()
.toGroupNamed('all_external')
.mediumSampling()
.withPriority(0)
);
// Set up initial consent (you'd get this from user preferences)
FlexTrack.setConsent(general: true, pii: false);
}
// events/ecommerce_events.dart
class ProductViewEvent extends BaseEvent {
final String productId;
final String productName;
final double price;
final String category;
ProductViewEvent({
required this.productId,
required this.productName,
required this.price,
required this.category,
});
@override
String getName() => 'product_view';
@override
Map<String, Object> getProperties() => {
'product_id': productId,
'product_name': productName,
'price': price,
'category': category,
};
@override
EventCategory get category => EventCategory.user;
}
class PurchaseCompleteEvent extends BaseEvent {
final String orderId;
final double totalAmount;
final String currency;
final List<Map<String, dynamic>> items;
final String paymentMethod;
PurchaseCompleteEvent({
required this.orderId,
required this.totalAmount,
this.currency = 'USD',
required this.items,
required this.paymentMethod,
});
@override
String getName() => 'purchase_complete';
@override
Map<String, Object> getProperties() => {
'order_id': orderId,
'total_amount': totalAmount,
'currency': currency,
'item_count': items.length,
'payment_method': paymentMethod,
'items': items,
};
@override
EventCategory get category => EventCategory.business;
@override
bool get isEssential => true; // Never sample revenue events
}
// Usage in your app
class ProductScreen extends StatelessWidget {
final Product product;
const ProductScreen({required this.product});
@override
void initState() {
super.initState();
// Track product view
FlexTrack.track(ProductViewEvent(
productId: product.id,
productName: product.name,
price: product.price,
category: product.category,
));
}
void _onAddToCart() {
// Track add to cart
FlexTrack.track(AddToCartEvent(
productId: product.id,
productName: product.name,
price: product.price,
));
// Your add to cart logic
CartService.addItem(product);
}
@override
Widget build(BuildContext context) {
return Scaffold(
// Your UI here
floatingActionButton: FloatingActionButton(
onPressed: _onAddToCart,
child: Icon(Icons.add_shopping_cart),
),
);
}
}
Example 2: SaaS App with Strict Privacy
Setup for a B2B SaaS app with GDPR requirements:
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await setupSaaSAnalytics();
runApp(SaaSApp());
}
Future<void> setupSaaSAnalytics() async {
await FlexTrack.setupWithRouting([
ConsoleTracker(),
FirebaseTracker(), // GDPR compliant
PostHogTracker(), // Privacy-focused analytics
InternalAPITracker(), // Your compliant tracker
], (routing) {
// Apply strict GDPR compliance
GDPRDefaults.applyStrict(routing, compliantTrackers: [
'firebase',
'posthog',
'internal_api'
]);
return routing
// π SENSITIVE BUSINESS DATA - Internal only
.routeWithProperty('revenue')
.to(['internal_api'])
.skipConsent() // Legitimate business interest
.withPriority(25)
.and()
// π€ USER BEHAVIOR - Privacy-safe analytics
.routeCategory(EventCategory.user)
.to(['posthog'])
.requireConsent()
.withPriority(15)
.and()
// β‘ PERFORMANCE MONITORING - System health
.routeCategory(EventCategory.technical)
.to(['internal_api'])
.skipConsent()
.lightSampling()
.withPriority(10)
.and();
});
}
// Custom privacy-focused events
class FeatureUsageEvent extends BaseEvent {
final String featureName;
final int timeSpentSeconds;
final bool isFirstTime;
FeatureUsageEvent({
required this.featureName,
required this.timeSpentSeconds,
this.isFirstTime = false,
});
@override
String getName() => 'feature_usage';
@override
Map<String, Object> getProperties() => {
'feature_name': featureName,
'time_spent_seconds': timeSpentSeconds,
'is_first_time': isFirstTime,
};
@override
EventCategory get category => EventCategory.user;
@override
bool get containsPII => false; // No personal data
}
class SubscriptionChangeEvent extends BaseEvent {
final String planFrom;
final String planTo;
final double priceChange;
final String changeReason;
SubscriptionChangeEvent({
required this.planFrom,
required this.planTo,
required this.priceChange,
required this.changeReason,
});
@override
String getName() => 'subscription_change';
@override
Map<String, Object> getProperties() => {
'plan_from': planFrom,
'plan_to': planTo,
'price_change': priceChange,
'change_reason': changeReason,
'business_metric': true, // Will route to internal API
};
@override
EventCategory get category => EventCategory.business;
}
π Migration Guide
From Firebase Analytics Only
Before:
// Scattered throughout your app
FirebaseAnalytics.instance.logEvent(
name: 'user_signup',
parameters: {'method': 'email'},
);
FirebaseAnalytics.instance.logEvent(
name: 'purchase',
parameters: {
'transaction_id': orderId,
'value': amount,
'currency': 'USD',
},
);
After (Step by Step):
- Add FlexTrack and keep existing code:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Add FlexTrack alongside existing Firebase
await FlexTrack.setup([
ConsoleTracker(), // For debugging
FirebaseTracker(), // Wrap your existing Firebase
]);
runApp(MyApp());
}
- Create events for new features:
// For new features, use FlexTrack events
await FlexTrack.track(UserSignupEvent(method: 'email'));
// Keep existing Firebase calls as-is
FirebaseAnalytics.instance.logEvent(name: 'legacy_event');
- Gradually migrate existing events:
// Replace this:
FirebaseAnalytics.instance.logEvent(
name: 'purchase',
parameters: {'value': amount},
);
// With this:
await FlexTrack.track(PurchaseEvent(
orderId: orderId,
amount: amount,
currency: 'USD',
));
From Multiple Manual Analytics
Before:
// Nightmare code scattered everywhere
void trackPurchase(String orderId, double amount) {
// Firebase
FirebaseAnalytics.instance.logEvent(
name: 'purchase',
parameters: {'value': amount},
);
// Mixpanel
if (userConsentedToTracking) {
Mixpanel.getInstance().track('Purchase', {
'Amount': amount,
'Order ID': orderId,
});
}
// Internal API
if (!kDebugMode) {
customAPI.track('purchase', {
'order_id': orderId,
'amount': amount,
});
}
}
After:
// One line replaces all of that
await FlexTrack.track(PurchaseEvent(
orderId: orderId,
amount: amount,
));
// FlexTrack handles:
// β
Routing to all trackers
// β
Consent checking
// β
Environment detection
// β
Format conversion
// β
Error handling
Migration Steps:
- Set up FlexTrack with all your existing trackers
- Create FlexTrack events for your existing tracking calls
- Replace manual tracking calls one by one
- Remove duplicate analytics code
- Enjoy cleaner, more maintainable code!
π Quick Reference
Essential Event Properties
class MyEvent extends BaseEvent {
@override
String getName() => 'my_event'; // Required
@override
Map<String, Object> getProperties() => {}; // Required
@override
EventCategory? get category => EventCategory.user; // Recommended
@override
bool get containsPII => false; // Important for GDPR
@override
bool get isEssential => false; // Bypasses consent/sampling
@override
bool get isHighVolume => false; // Triggers sampling
@override
bool get requiresConsent => true; // GDPR compliance
}
Routing Syntax Cheat Sheet
routing
// By event type
.route<MyEvent>().toAll().and()
// By name pattern
.routeNamed('purchase').toAll().and()
.routeMatching(RegExp(r'debug_.*')).to(['console']).and()
// By category
.routeCategory(EventCategory.business).toAll().and()
// By properties
.routeWithProperty('internal_metric').to(['internal']).and()
.routePII().to(['gdpr_compliant']).and()
// By flags
.routeEssential().toAll().and()
.routeHighVolume().toAll().heavySampling().and()
// Environment
.routeMatching(RegExp(r'debug_.*')).onlyInDebug().and()
.routeCategory(EventCategory.business).onlyInProduction().and()
// Consent requirements
.routeCategory(EventCategory.user).requireConsent().and()
.routePII().requirePIIConsent().and()
.routeEssential().skipConsent().and()
// Sampling
.routeHighVolume().heavySampling().and() // 1%
.routeCategory(EventCategory.user).lightSampling().and() // 10%
.routeDefault().mediumSampling().and() // 50%
// Priority (higher = more important)
.routeEssential().withPriority(30).and()
.routeCategory(EventCategory.business).withPriority(20).and()
.routeDefault().withPriority(0).and()
Sampling Rates Quick Reference
// Sampling methods
.noSampling() // 100% - All events
.lightSampling() // 10% - Low volume reduction
.mediumSampling() // 50% - Moderate reduction
.heavySampling() // 1% - Aggressive reduction
.sample(0.25) // 25% - Custom rate
// When to use each:
EventCategory.business β noSampling() // Never miss revenue
EventCategory.user β lightSampling() // Some user behavior
UI interactions β heavySampling() // Too many clicks
Default events β mediumSampling() // Balanced approach
Consent Management Quick Reference
// Set consent
FlexTrack.setConsent(general: true, pii: false);
// Check consent
final consent = FlexTrack.getConsentStatus();
bool hasGeneral = consent['general'] ?? false;
bool hasPII = consent['pii'] ?? false;
// Event consent requirements
@override bool get requiresConsent => true; // Needs general consent
@override bool get containsPII => true; // Needs PII consent
@override bool get isEssential => true; // Bypasses consent
Debugging Commands
// System info
FlexTrack.printDebugInfo();
final info = FlexTrack.getDebugInfo();
// Event routing
final debug = FlexTrack.debugEvent(myEvent);
print(debug.routingResult.targetTrackers);
// Configuration validation
final issues = FlexTrack.validate();
issues.forEach(print);
// Tracker status
print('Is enabled: ${FlexTrack.isEnabled}');
print('Trackers: ${FlexTrack.getTrackerIds()}');
π Advanced Use Cases
Multi-Tenant SaaS Application
For apps serving multiple organizations with different analytics needs:
class TenantAwareTracker extends BaseTrackerStrategy {
final Map<String, String> _tenantConfigs;
TenantAwareTracker(this._tenantConfigs) : super(
id: 'tenant_aware',
name: 'Tenant-Aware Analytics',
);
@override
Future<void> doTrack(BaseEvent event) async {
final tenantId = event.getProperties()?['tenant_id'] as String?;
if (tenantId == null) return;
final config = _tenantConfigs[tenantId];
if (config == null) return;
// Route to tenant-specific analytics endpoint
await _sendToTenantEndpoint(config, event);
}
}
// Setup with tenant-aware routing
await FlexTrack.setupWithRouting([
TenantAwareTracker({
'tenant_1': 'https://analytics.tenant1.com',
'tenant_2': 'https://analytics.tenant2.com',
}),
], (routing) => routing
.routeWithProperty('tenant_id')
.to(['tenant_aware'])
.and()
.routeDefault()
.to(['console'])
);
// Usage
await FlexTrack.track(TenantEvent(
tenantId: 'tenant_1',
eventName: 'feature_used',
));
A/B Test Integration
Track experiment participation and outcomes:
class ExperimentEvent extends BaseEvent {
final String experimentId;
final String variant;
final String outcome;
ExperimentEvent({
required this.experimentId,
required this.variant,
required this.outcome,
});
@override
String getName() => 'experiment_outcome';
@override
Map<String, Object> getProperties() => {
'experiment_id': experimentId,
'variant': variant,
'outcome': outcome,
};
@override
EventCategory get category => EventCategory.business;
@override
bool get isEssential => true; // Never sample A/B test data
}
// Routing for experiments
routing
.routeMatching(RegExp(r'experiment_.*'))
.toAll()
.noSampling() // Critical for statistical significance
.withPriority(25)
.and()
Real-Time Dashboard Integration
For apps that need real-time analytics dashboards:
class WebSocketTracker extends BaseTrackerStrategy {
WebSocketChannel? _channel;
WebSocketTracker() : super(
id: 'websocket',
name: 'Real-Time Dashboard',
);
@override
bool get supportsRealTime => true;
@override
Future<void> doInitialize() async {
_channel = WebSocketChannel.connect(
Uri.parse('wss://dashboard.yourapp.com/analytics'),
);
}
@override
Future<void> doTrack(BaseEvent event) async {
if (_channel == null) return;
final payload = {
'type': 'analytics_event',
'event': event.getName(),
'properties': event.getProperties(),
'timestamp': DateTime.now().toIso8601String(),
};
_channel!.sink.add(jsonEncode(payload));
}
}
// Setup for real-time events
routing
.routeCategory(EventCategory.business)
.to(['websocket', 'firebase']) // Real-time + persistent
.and()
π§ Custom Tracker Templates
REST API Tracker Template
class RESTAPITracker extends BaseTrackerStrategy {
final String baseUrl;
final String? apiKey;
final http.Client _client;
final List<Map<String, dynamic>> _eventBuffer = [];
RESTAPITracker({
required this.baseUrl,
this.apiKey,
}) : _client = http.Client(),
super(
id: 'rest_api',
name: 'REST API Tracker',
);
@override
bool get isGDPRCompliant => true; // Assuming your API is compliant
@override
bool supportsBatchTracking() => true;
@override
int get maxBatchSize => 100;
@override
Future<void> doInitialize() async {
// Test API connection
final response = await _client.get(
Uri.parse('$baseUrl/health'),
headers: _getHeaders(),
);
if (response.statusCode != 200) {
throw Exception('API health check failed: ${response.statusCode}');
}
}
@override
Future<void> doTrack(BaseEvent event) async {
final eventData = {
'name': event.getName(),
'properties': event.getProperties(),
'timestamp': event.timestamp.toIso8601String(),
'category': event.category?.name,
};
_eventBuffer.add(eventData);
// Auto-flush when buffer is full
if (_eventBuffer.length >= maxBatchSize) {
await doFlush();
}
}
@override
Future<void> doTrackBatch(List<BaseEvent> events) async {
for (final event in events) {
await doTrack(event);
}
}
@override
Future<void> doFlush() async {
if (_eventBuffer.isEmpty) return;
final eventsToSend = List<Map<String, dynamic>>.from(_eventBuffer);
_eventBuffer.clear();
try {
final response = await _client.post(
Uri.parse('$baseUrl/events/batch'),
headers: _getHeaders(),
body: jsonEncode({
'events': eventsToSend,
'batch_timestamp': DateTime.now().toIso8601String(),
}),
);
if (response.statusCode != 200 && response.statusCode != 201) {
throw Exception('API error: ${response.statusCode} ${response.body}');
}
} catch (e) {
// Re-add events on failure for retry
_eventBuffer.addAll(eventsToSend);
rethrow;
}
}
Map<String, String> _getHeaders() {
final headers = {
'Content-Type': 'application/json',
'User-Agent': 'FlexTrack-RESTTracker/1.0',
};
if (apiKey != null) {
headers['Authorization'] = 'Bearer $apiKey';
}
return headers;
}
@override
Future<void> doSetUserProperties(Map<String, dynamic> properties) async {
await _client.post(
Uri.parse('$baseUrl/users/properties'),
headers: _getHeaders(),
body: jsonEncode({
'properties': properties,
'timestamp': DateTime.now().toIso8601String(),
}),
);
}
@override
Future<void> doIdentifyUser(String userId, [Map<String, dynamic>? properties]) async {
await _client.post(
Uri.parse('$baseUrl/users/identify'),
headers: _getHeaders(),
body: jsonEncode({
'user_id': userId,
'properties': properties ?? {},
'timestamp': DateTime.now().toIso8601String(),
}),
);
}
}
Database Tracker Template
For storing analytics in your local database:
class DatabaseTracker extends BaseTrackerStrategy {
late Database _database;
DatabaseTracker() : super(
id: 'database',
name: 'Local Database Tracker',
);
@override
Future<void> doInitialize() async {
_database = await openDatabase(
'analytics.db',
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
properties TEXT,
category TEXT,
timestamp TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''');
},
);
}
@override
Future<void> doTrack(BaseEvent event) async {
await _database.insert('events', {
'name': event.getName(),
'properties': jsonEncode(event.getProperties()),
'category': event.category?.name,
'timestamp': event.timestamp.toIso8601String(),
});
}
// Method to retrieve stored events
Future<List<Map<String, dynamic>>> getStoredEvents({
int? limit,
String? category,
DateTime? since,
}) async {
String query = 'SELECT * FROM events';
List<dynamic> args = [];
List<String> conditions = [];
if (category != null) {
conditions.add('category = ?');
args.add(category);
}
if (since != null) {
conditions.add('timestamp >= ?');
args.add(since.toIso8601String());
}
if (conditions.isNotEmpty) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY created_at DESC';
if (limit != null) {
query += ' LIMIT ?';
args.add(limit);
}
return await _database.rawQuery(query, args);
}
}
π Performance Monitoring
Track FlexTrack's Own Performance
Monitor how FlexTrack affects your app:
class PerformanceMonitoringTracker extends BaseTrackerStrategy {
final Stopwatch _processingTime = Stopwatch();
int _eventsProcessed = 0;
int _eventsDropped = 0;
PerformanceMonitoringTracker() : super(
id: 'performance_monitor',
name: 'Performance Monitor',
);
@override
Future<void> doTrack(BaseEvent event) async {
_processingTime.start();
try {
_eventsProcessed++;
// Your actual tracking logic here
await _actualTracking(event);
} catch (e) {
_eventsDropped++;
rethrow;
} finally {
_processingTime.stop();
}
}
Map<String, dynamic> getPerformanceStats() {
return {
'events_processed': _eventsProcessed,
'events_dropped': _eventsDropped,
'total_processing_time_ms': _processingTime.elapsedMilliseconds,
'average_processing_time_ms': _eventsProcessed > 0
? _processingTime.elapsedMilliseconds / _eventsProcessed
: 0,
'success_rate': _eventsProcessed > 0
? (_eventsProcessed - _eventsDropped) / _eventsProcessed
: 0,
};
}
}
Memory Usage Monitoring
class MemoryAwareTracker extends BaseTrackerStrategy {
final List<BaseEvent> _eventBuffer = [];
static const int MAX_BUFFER_SIZE = 1000;
static const int MEMORY_CHECK_INTERVAL = 100;
int _eventCount = 0;
@override
Future<void> doTrack(BaseEvent event) async {
_eventCount++;
// Check memory usage periodically
if (_eventCount % MEMORY_CHECK_INTERVAL == 0) {
await _checkMemoryUsage();
}
_eventBuffer.add(event);
// Prevent memory leaks
if (_eventBuffer.length > MAX_BUFFER_SIZE) {
_eventBuffer.removeRange(0, _eventBuffer.length ~/ 2);
}
}
Future<void> _checkMemoryUsage() async {
// In a real implementation, you'd check actual memory usage
// For example, using dart:developer or platform-specific methods
if (_eventBuffer.length > MAX_BUFFER_SIZE * 0.8) {
print('β οΈ FlexTrack buffer approaching limit: ${_eventBuffer.length}');
await doFlush();
}
}
}
π§ͺ Advanced Testing Strategies
Integration Testing with Real Analytics
// Test with real analytics services in a controlled way
testWidgets('analytics integration test', (tester) async {
// Use test tokens/keys that don't affect production data
await FlexTrack.setup([
FirebaseTracker(), // Uses test Firebase project
TestMixpanelTracker(token: 'test_token'),
]);
// Build your app widget
await tester.pumpWidget(MyApp());
// Perform user actions
await tester.tap(find.text('Sign Up'));
await tester.enterText(find.byType(TextField), 'test@example.com');
await tester.tap(find.text('Submit'));
// Wait for analytics to be sent
await tester.pumpAndSettle();
// Verify with your test analytics dashboard
// (This would be specific to your testing setup)
});
Property-Based Testing
import 'package:test/test.dart';
void main() {
group('FlexTrack Property Tests', () {
test('all events should have valid names', () {
final testEvents = [
UserSignupEvent(method: 'email'),
PurchaseEvent(productId: 'abc', amount: 99.99),
ButtonClickEvent(buttonId: 'test', screenName: 'home'),
];
for (final event in testEvents) {
expect(event.getName(), isNotEmpty);
expect(event.getName(), matches(RegExp(r'^[a-z_]+)));
expect(event.getName().length, lessThan(50));
}
});
test('all events should have serializable properties', () {
final testEvents = [
UserSignupEvent(method: 'email'),
PurchaseEvent(productId: 'abc', amount: 99.99),
];
for (final event in testEvents) {
final properties = event.getProperties();
if (properties != null) {
// Should be JSON serializable
expect(() => jsonEncode(properties), returnsNormally);
// All values should be basic types
for (final value in properties.values) {
expect(
value is String || value is num || value is bool || value is List || value is Map,
isTrue,
reason: 'Property value $value is not serializable',
);
}
}
}
});
});
}
Load Testing
testWidgets('high load analytics test', (tester) async {
final mockTracker = await setupFlexTrackForTesting();
// Simulate high load
const eventCount = 10000;
final stopwatch = Stopwatch()..start();
final futures = <Future>[];
for (int i = 0; i < eventCount; i++) {
futures.add(FlexTrack.track(
ButtonClickEvent(buttonId: 'test_$i', screenName: 'test')
));
}
await Future.wait(futures);
stopwatch.stop();
// Verify performance
expect(stopwatch.elapsedMilliseconds, lessThan(5000)); // Less than 5 seconds
expect(mockTracker.capturedEvents.length, equals(eventCount));
print('Processed $eventCount events in ${stopwatch.elapsedMilliseconds}ms');
print('Average: ${stopwatch.elapsedMilliseconds / eventCount}ms per event');
});
π Security Best Practices
Secure API Key Management
class SecureAPITracker extends BaseTrackerStrategy {
final String _encryptedApiKey;
SecureAPITracker({required String encryptedApiKey})
: _encryptedApiKey = encryptedApiKey,
super(id: 'secure_api', name: 'Secure API Tracker');
@override
Future<void> doInitialize() async {
// Decrypt API key only when needed
final apiKey = await _decryptApiKey(_encryptedApiKey);
// Use the key for initialization
await _initializeWithKey(apiKey);
// Clear the key from memory
apiKey.replaceAll(RegExp(r'.'), '0'); // Basic key clearing
}
Future<String> _decryptApiKey(String encrypted) async {
// Implement your key decryption logic
// Consider using flutter_secure_storage or similar
return encrypted; // Placeholder
}
}
Data Sanitization
class SanitizingTracker extends BaseTrackerStrategy {
final Set<String> _piiFields = {
'email', 'phone', 'ssn', 'credit_card', 'password',
'first_name', 'last_name', 'address', 'ip_address'
};
@override
Future<void> doTrack(BaseEvent event) async {
final sanitizedProperties = _sanitizeProperties(event.getProperties());
// Create sanitized event
final sanitizedEvent = SanitizedEvent(
originalEvent: event,
sanitizedProperties: sanitizedProperties,
);
await _actualTracking(sanitizedEvent);
}
Map<String, Object>? _sanitizeProperties(Map<String, Object>? properties) {
if (properties == null) return null;
final sanitized = <String, Object>{};
properties.forEach((key, value) {
if (_piiFields.contains(key.toLowerCase())) {
// Replace PII with hashed or masked value
sanitized[key] = _hashValue(value.toString());
} else {
sanitized[key] = value;
}
});
return sanitized;
}
String _hashValue(String value) {
// Use a proper hashing algorithm
return 'hashed_${value.hashCode.abs()}';
}
}
π Business Intelligence Integration
Revenue Attribution Tracking
class RevenueAttributionEvent extends BaseEvent {
final double revenue;
final String currency;
final String source; // 'organic', 'paid_search', 'social', etc.
final String medium; // 'cpc', 'email', 'referral', etc.
final String campaign;
final String? couponCode;
RevenueAttributionEvent({
required this.revenue,
this.currency = 'USD',
required this.source,
required this.medium,
required this.campaign,
this.couponCode,
});
@override
String getName() => 'revenue_attribution';
@override
Map<String, Object> getProperties() => {
'revenue': revenue,
'currency': currency,
'source': source,
'medium': medium,
'campaign': campaign,
if (couponCode != null) 'coupon_code': couponCode!,
'attribution_timestamp': DateTime.now().toIso8601String(),
};
@override
EventCategory get category => EventCategory.business;
@override
bool get isEssential => true;
}
// Usage in your app
class PurchaseService {
static Future<void> completePurchase({
required double amount,
required String orderId,
String? couponCode,
}) async {
// Your purchase logic
await _processPurchase(orderId, amount);
// Track revenue with attribution
final attribution = await _getAttributionData();
await FlexTrack.track(RevenueAttributionEvent(
revenue: amount,
source: attribution.source,
medium: attribution.medium,
campaign: attribution.campaign,
couponCode: couponCode,
));
}
}
Customer Lifetime Value Tracking
class CLVUpdateEvent extends BaseEvent {
final String userId;
final double currentCLV;
final double previousCLV;
final String trigger; // 'purchase', 'subscription', 'churn'
CLVUpdateEvent({
required this.userId,
required this.currentCLV,
required this.previousCLV,
required this.trigger,
});
@override
String getName() => 'clv_update';
@override
Map<String, Object> getProperties() => {
'user_id': userId,
'current_clv': currentCLV,
'previous_clv': previousCLV,
'clv_change': currentCLV - previousCLV,
'trigger': trigger,
};
@override
EventCategory get category => EventCategory.business;
@override
bool get containsPII => true; // Contains user ID
}
π Final Tips and Best Practices
1. Start Simple, Scale Gradually
// β
GOOD: Start with basic setup
await FlexTrack.setup([
ConsoleTracker(),
FirebaseTracker(),
]);
// β AVOID: Complex setup from day one
await FlexTrack.setupWithRouting([...], (routing) => routing
.defineGroup(...)
.routeCategory(...)
.routeMatching(...)
// 50 more lines of complex routing
);
2. Always Use Console Tracker in Development
// β
ALWAYS include console tracker for debugging
await FlexTrack.setup([
ConsoleTracker(), // Essential for development
YourProductionTracker(),
]);
3. Test Your Events Early
void main() async {
await FlexTrack.setup([ConsoleTracker()]);
// Test your events immediately after setup
await FlexTrack.track(TestEvent());
runApp(MyApp());
}
4. Use Meaningful Event Names
// β
GOOD: Clear, descriptive names
class UserCompletedPurchaseEvent extends BaseEvent {
@override
String getName() => 'user_completed_purchase';
}
// β BAD: Vague or abbreviated names
class UCPEvent extends BaseEvent {
@override
String getName() => 'ucp';
}
5. Group Related Events
// β
GOOD: Consistent naming patterns
class UserRegistrationStartedEvent extends BaseEvent {
@override
String getName() => 'user_registration_started';
}
class UserRegistrationCompletedEvent extends BaseEvent {
@override
String getName() => 'user_registration_completed';
}
class UserRegistrationAbandonedEvent extends BaseEvent {
@override
String getName() => 'user_registration_abandoned';
}
6. Document Your Events
/// Tracks when a user completes a purchase
///
/// This event is critical for revenue tracking and should never be sampled.
/// It's sent to all analytics trackers and includes detailed product information.
///
/// Properties:
/// - product_id: Unique identifier for the purchased product
/// - amount: Purchase amount in the specified currency
/// - currency: 3-letter currency code (e.g., 'USD', 'EUR')
/// - payment_method: How the user paid ('credit_card', 'paypal', etc.)
class PurchaseCompletedEvent extends BaseEvent {
final String productId;
final double amount;
final String currency;
final String paymentMethod;
// ... implementation
}
7. Monitor Your Analytics
// Set up monitoring to catch issues early
class AnalyticsHealthMonitor {
static Timer? _healthCheckTimer;
static void startMonitoring() {
_healthCheckTimer = Timer.periodic(Duration(minutes: 5), (_) {
_checkAnalyticsHealth();
});
}
static void _checkAnalyticsHealth() {
final debugInfo = FlexTrack.getDebugInfo();
final issues = FlexTrack.validate();
if (issues.isNotEmpty) {
print('β οΈ Analytics issues detected: $issues');
// Send alert to your monitoring system
}
if (!debugInfo['isEnabled']) {
print('π¨ Analytics is disabled!');
// Send critical alert
}
}
}
π Congratulations!
You've now learned everything you need to know about FlexTrack! You can:
β
Set up FlexTrack with multiple analytics services
β
Create custom events that fit your app's needs
β
Configure intelligent routing based on your requirements
β
Handle GDPR compliance automatically
β
Optimize performance with sampling and batching
β
Debug issues when they arise
β
Test your analytics comprehensively
β
Scale your setup as your app grows
π Getting Help
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: Full Documentation
- Examples: Example Repository
π€ Contributing
We welcome contributions! Please see our Contributing Guide for details.
π License
This project is licensed under the MIT License - see the LICENSE file for details.
Happy Tracking! π―
Libraries
- flex_track
- FlexTrack - A flexible analytics tracking system for Flutter