Skip to main content
The LeanCode analytics packages provide a unified interface for tracking analytics events across multiple providers. This guide covers setting up analytics with Firebase, PostHog, or custom implementations.

Overview

The analytics system is split into three packages:
  • leancode_analytics_base - Core abstractions and interfaces
  • leancode_analytics_firebase - Firebase Analytics implementation
  • leancode_analytics_posthog - PostHog Analytics implementation
This modular approach allows you to:
  • Use multiple analytics providers simultaneously
  • Switch providers without changing your app code
  • Create custom analytics implementations

Installation

1

Add the base package

All implementations require the base package:
flutter pub add leancode_analytics_base
2

Add your analytics provider

Choose one or more analytics providers:
flutter pub add leancode_analytics_firebase

Firebase Analytics Setup

Configure Firebase

1

Set up Firebase project

Follow the Firebase setup guide to add Firebase to your Flutter app.
2

Initialize Firebase and analytics

import 'package:firebase_core/firebase_core.dart';
import 'package:leancode_analytics_firebase/leancode_analytics_firebase.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize Firebase (Firebase class is reexported from the package)
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  
  // Create analytics instance
  final analytics = FirebaseLeanAnalytics();
  
  runApp(MyApp(analytics: analytics));
}
3

Add navigation observers

Add analytics observers to your app’s navigation for automatic screen tracking:
class MyApp extends StatelessWidget {
  const MyApp({required this.analytics, super.key});
  
  final FirebaseLeanAnalytics analytics;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      navigatorObservers: analytics.navigatorObservers,
      home: const HomePage(),
    );
  }
}

Track Events

import 'package:leancode_analytics_base/leancode_analytics_base.dart';

// Track a button tap
analytics.register(
  TapAnalyticsEvent(
    key: 'submit_button',
    label: 'Submit Form',
  ),
);

// Track user login
analytics.register(
  LoginAnalyticsEvent(userId: 'user_123'),
);

// Track custom events
analytics.register(
  AnalyticsEvent(
    name: 'purchase_completed',
    params: {
      'item_id': 'prod_123',
      'price': 29.99,
      'currency': 'USD',
    },
  ),
);

PostHog Analytics Setup

Configure PostHog

1

Get PostHog credentials

Sign up at PostHog and get your API key and host URL.
2

Initialize PostHog and analytics

import 'package:leancode_analytics_posthog/leancode_analytics_posthog.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize PostHog (Posthog class is reexported from the package)
  await Posthog().setup(
    apiKey: 'phc_your_api_key',
    host: 'https://app.posthog.com', // or your self-hosted instance
    captureApplicationLifecycleEvents: true,
  );
  
  // Create analytics instance
  final analytics = PostHogLeanAnalytics();
  
  runApp(MyApp(analytics: analytics));
}
3

Add navigation observers

class MyApp extends StatelessWidget {
  const MyApp({required this.analytics, super.key});
  
  final PostHogLeanAnalytics analytics;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      navigatorObservers: analytics.navigatorObservers,
      home: const HomePage(),
    );
  }
}
PostHog uses PosthogObserver from the posthog_flutter package for automatic screen tracking.

Track Events

PostHog uses the same event API as Firebase:
// Track user interactions
analytics.register(
  TapAnalyticsEvent(
    key: 'checkout_button',
    label: 'Proceed to Checkout',
  ),
);

// Track user login
analytics.register(
  LoginAnalyticsEvent(userId: 'user_456'),
);

// Custom events with properties
analytics.register(
  AnalyticsEvent(
    name: 'feature_used',
    params: {
      'feature_name': 'dark_mode',
      'enabled': true,
    },
  ),
);

Using Multiple Analytics Providers

You can send events to multiple analytics providers simultaneously:
import 'package:leancode_analytics_base/leancode_analytics_base.dart';
import 'package:leancode_analytics_firebase/leancode_analytics_firebase.dart';
import 'package:leancode_analytics_posthog/leancode_analytics_posthog.dart';

class MultiAnalytics implements LeanAnalytics {
  MultiAnalytics({
    required this.firebase,
    required this.postHog,
  });

  final FirebaseLeanAnalytics firebase;
  final PostHogLeanAnalytics postHog;

  @override
  void register(AnalyticsEvent event) {
    firebase.register(event);
    postHog.register(event);
  }

  @override
  List<NavigatorObserver> get navigatorObservers => [
    ...firebase.navigatorObservers,
    ...postHog.navigatorObservers,
  ];
}

// Usage
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize both providers
  await Firebase.initializeApp(options: firebaseOptions);
  await Posthog().setup(apiKey: 'your_key', host: 'https://app.posthog.com');
  
  final analytics = MultiAnalytics(
    firebase: FirebaseLeanAnalytics(),
    postHog: PostHogLeanAnalytics(),
  );
  
  runApp(MyApp(analytics: analytics));
}
Events are now tracked in both Firebase and PostHog automatically!

Event Types

Built-in Events

The base package provides common event types:
Track user taps and button clicks:
analytics.register(
  TapAnalyticsEvent(
    key: 'add_to_cart_button',
    label: 'Add to Cart', // Optional
  ),
);
Parameters automatically added:
  • key: Unique identifier for the tapped element
  • label: Optional descriptive label

Custom Event Classes

Create reusable custom event classes:
class PurchaseEvent extends AnalyticsEvent {
  PurchaseEvent({
    required String itemId,
    required double price,
    required String currency,
  }) : super(
    name: 'purchase',
    params: {
      'item_id': itemId,
      'price': price,
      'currency': currency,
      'timestamp': DateTime.now().toIso8601String(),
    },
  );
}

class ScreenViewEvent extends AnalyticsEvent {
  ScreenViewEvent(String screenName) : super(
    name: 'screen_view',
    params: {'screen_name': screenName},
  );
}

// Usage
analytics.register(
  PurchaseEvent(
    itemId: 'prod_456',
    price: 49.99,
    currency: 'USD',
  ),
);

analytics.register(
  ScreenViewEvent('product_details'),
);

Automatic Screen Tracking

With MaterialApp

MaterialApp(
  navigatorObservers: analytics.navigatorObservers,
  routes: {
    '/': (context) => const HomePage(),
    '/profile': (context) => const ProfilePage(),
    '/settings': (context) => const SettingsPage(),
  },
)

With GoRouter

import 'package:go_router/go_router.dart';

final router = GoRouter(
  observers: analytics.navigatorObservers,
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
    ),
    GoRoute(
      path: '/profile',
      builder: (context, state) => const ProfilePage(),
    ),
  ],
);

MaterialApp.router(
  routerConfig: router,
)

Custom Route Tracking

Implement LeanAnalyticsRoute for custom screen names:
import 'package:leancode_analytics_base/leancode_analytics_base.dart';

class CustomRoute<T> extends MaterialPageRoute<T> implements LeanAnalyticsRoute {
  CustomRoute({
    required super.builder,
    required this.screenName,
    super.settings,
  });

  final String screenName;

  @override
  String get analyticsScreenName => screenName;
}

// Usage
Navigator.of(context).push(
  CustomRoute(
    builder: (context) => const ProductDetailsPage(),
    screenName: 'product_details_premium',
  ),
);

Common Use Cases

Track User Interactions

class ProductCard extends StatelessWidget {
  const ProductCard({required this.product, super.key});

  final Product product;

  @override
  Widget build(BuildContext context) {
    final analytics = context.read<LeanAnalytics>();

    return GestureDetector(
      onTap: () {
        // Track the tap
        analytics.register(
          TapAnalyticsEvent(
            key: 'product_card_${product.id}',
            label: product.name,
          ),
        );
        
        // Navigate to details
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => ProductDetailsPage(product),
          ),
        );
      },
      child: Card(
        child: Column(
          children: [
            Image.network(product.imageUrl),
            Text(product.name),
            Text('\$${product.price}'),
          ],
        ),
      ),
    );
  }
}

Track Form Submissions

Future<void> submitForm() async {
  final analytics = context.read<LeanAnalytics>();
  
  // Track form submission start
  analytics.register(
    AnalyticsEvent(
      name: 'form_submit_started',
      params: {'form_type': 'contact'},
    ),
  );
  
  try {
    await apiService.submitContact(formData);
    
    // Track success
    analytics.register(
      AnalyticsEvent(
        name: 'form_submit_success',
        params: {'form_type': 'contact'},
      ),
    );
  } catch (e) {
    // Track error
    analytics.register(
      AnalyticsEvent(
        name: 'form_submit_error',
        params: {
          'form_type': 'contact',
          'error': e.toString(),
        },
      ),
    );
  }
}

Track Feature Usage

class SettingsPage extends StatelessWidget {
  const SettingsPage({super.key});

  @override
  Widget build(BuildContext context) {
    final analytics = context.read<LeanAnalytics>();

    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: ListView(
        children: [
          SwitchListTile(
            title: const Text('Dark Mode'),
            value: isDarkMode,
            onChanged: (value) {
              // Track feature toggle
              analytics.register(
                AnalyticsEvent(
                  name: 'feature_toggled',
                  params: {
                    'feature': 'dark_mode',
                    'enabled': value,
                  },
                ),
              );
              
              setState(() => isDarkMode = value);
            },
          ),
        ],
      ),
    );
  }
}

Track Search Queries

class SearchBar extends StatefulWidget {
  const SearchBar({super.key});

  @override
  State<SearchBar> createState() => _SearchBarState();
}

class _SearchBarState extends State<SearchBar> {
  final _controller = TextEditingController();

  void _onSearch() {
    final query = _controller.text;
    final analytics = context.read<LeanAnalytics>();
    
    analytics.register(
      AnalyticsEvent(
        name: 'search',
        params: {
          'search_term': query,
          'query_length': query.length,
        },
      ),
    );
    
    // Perform search
    searchService.search(query);
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      onSubmitted: (_) => _onSearch(),
      decoration: InputDecoration(
        hintText: 'Search...',
        suffixIcon: IconButton(
          icon: const Icon(Icons.search),
          onPressed: _onSearch,
        ),
      ),
    );
  }
}

Dependency Injection

With Provider

import 'package:provider/provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  await Firebase.initializeApp(options: firebaseOptions);
  final analytics = FirebaseLeanAnalytics();
  
  runApp(
    Provider<LeanAnalytics>.value(
      value: analytics,
      child: MyApp(analytics: analytics),
    ),
  );
}

// Usage in widgets
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final analytics = context.read<LeanAnalytics>();
    
    return ElevatedButton(
      onPressed: () {
        analytics.register(
          TapAnalyticsEvent(key: 'my_button', label: 'Click'),
        );
      },
      child: const Text('Track Me'),
    );
  }
}

With GetIt

import 'package:get_it/get_it.dart';

final getIt = GetIt.instance;

void setupDependencies() async {
  await Firebase.initializeApp(options: firebaseOptions);
  
  getIt.registerSingleton<LeanAnalytics>(
    FirebaseLeanAnalytics(),
  );
}

// Usage
final analytics = getIt<LeanAnalytics>();
analytics.register(TapAnalyticsEvent(key: 'button', label: 'Tap'));

Custom Analytics Implementation

Create your own analytics backend:
import 'package:flutter/widgets.dart';
import 'package:leancode_analytics_base/leancode_analytics_base.dart';

class CustomAnalytics implements LeanAnalytics {
  final ApiClient apiClient;
  
  CustomAnalytics(this.apiClient);

  @override
  void register(AnalyticsEvent event) {
    // Send to your custom backend
    apiClient.post('/analytics/events', {
      'name': event.name,
      'params': event.params,
      'timestamp': DateTime.now().toIso8601String(),
    });
  }

  @override
  List<NavigatorObserver> get navigatorObservers => [
    CustomNavigatorObserver(this),
  ];
}

class CustomNavigatorObserver extends NavigatorObserver {
  final CustomAnalytics analytics;
  
  CustomNavigatorObserver(this.analytics);

  @override
  void didPush(Route route, Route? previousRoute) {
    super.didPush(route, previousRoute);
    
    if (route is LeanAnalyticsRoute) {
      analytics.register(
        AnalyticsEvent(
          name: 'screen_view',
          params: {'screen_name': route.analyticsScreenName},
        ),
      );
    }
  }
}

Best Practices

Follow a naming convention for your events:
// Good: snake_case, descriptive
'button_clicked'
'purchase_completed'
'video_started'

// Avoid: inconsistent casing, vague names
'ButtonClick'
'event1'
'test'
Never include personally identifiable information (PII) in event parameters:
// Bad: Contains PII
analytics.register(
  AnalyticsEvent(
    name: 'form_submitted',
    params: {
      'email': '[email protected]', // Don't do this!
      'password': 'secret123',      // Never do this!
    },
  ),
);

// Good: Uses anonymized identifiers
analytics.register(
  AnalyticsEvent(
    name: 'form_submitted',
    params: {
      'user_id': 'user_123',
      'form_type': 'contact',
    },
  ),
);
Focus on events that help understand user behavior:
// Onboarding funnel
'onboarding_started'
'onboarding_step_completed'
'onboarding_finished'

// Purchase funnel
'product_viewed'
'add_to_cart'
'checkout_started'
'purchase_completed'
Make analytics injectable for easier testing:
class MyService {
  final LeanAnalytics analytics;
  
  MyService({required this.analytics});
  
  void performAction() {
    // Service logic
    analytics.register(
      AnalyticsEvent(name: 'action_performed', params: {}),
    );
  }
}

// Easy to mock in tests
final mockAnalytics = MockLeanAnalytics();
final service = MyService(analytics: mockAnalytics);

Migration from Deprecated Package

If you’re using the deprecated leancode_analytics package, here’s how to migrate:
1

Update dependencies

Replace the old package with the new ones:
# Remove
# leancode_analytics: ^x.x.x

# Add
dependencies:
  leancode_analytics_base: ^1.0.0
  leancode_analytics_firebase: ^1.0.0  # if using Firebase
2

Update imports

// Old
import 'package:leancode_analytics/leancode_analytics.dart';

// New
import 'package:leancode_analytics_base/leancode_analytics_base.dart';
import 'package:leancode_analytics_firebase/leancode_analytics_firebase.dart';
3

Update initialization

// Old
final analytics = FirebaseLeanAnalytics();

// New (same!)
final analytics = FirebaseLeanAnalytics();
The API remains largely the same, so most code should work without changes!

Next Steps

Build docs developers (and LLMs) love