Skip to content

Build your own Posthog - PART 4

Published: at 03:22 PMSuggest Changes

Welcome to Part 4 of the SmolHog series! Today we’re building the Flutter SDK - the client-side library that makes analytics tracking effortless for Flutter developers.

In the previous parts, we built the backend infrastructure, API Gateway, Now it’s time to create the SDK that developers will actually use in their apps.

Project Structure

Start by creating a new Flutter Package using the command:

flutter create --template=package smolhog_flutter

Then create an Example App inside the package (this will be helpful for testing our package)

flutter create example

Our Flutter SDK will live in smolhog_flutter/ with this structure:

smolhog_flutter/
├── lib/
│   ├── smolhog/
│   │   └── smolhog.dart          # Main SDK implementation
│   └── smolhog_flutter.dart      # Public API export
├── example/
│   └── lib/
│       └── main.dart             # Example usage
└── pubspec.yaml                  # Dependencies

Core SDK Implementation

Let’s start building the main SDK class in smolhog_flutter/lib/smolhog/smolhog.dart:

Imports and Setup

import 'dart:convert';
import 'dart:developer' as dev;
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

Main SmolHog Class

class SmolHog {
  static SmolHog? _instance;
  static SmolHog get instance => _instance!;

  final String _apiKey;
  final String _host;
  String? _userId;
  String? _sessionId;
  final List<Map<String, dynamic>> _eventQueue = [];

  SmolHog._internal(this._apiKey, this._host);

Key Design Decisions:

Initialization Method

static Future<void> initialize({
  required String apiKey,
  required String host,
}) async {
  _instance = SmolHog._internal(apiKey, host);
  await _instance!._setup();
}

Future<void> _setup() async {
  final prefs = await SharedPreferences.getInstance();

  // Get or generate persistent user ID
  _userId = prefs.getString('smolhog_user_id');
  if (_userId == null) {
    _userId = _generateId();
    await prefs.setString('smolhog_user_id', _userId!);
  }
  
  // Generate new session ID for each app launch
  _sessionId = _generateId();
}

User ID Management:

ID Generation

String _generateId() {
  final random = Random();
  return '${DateTime.now().millisecondsSinceEpoch}-${random.nextInt(99999)}';
}

This generates unique IDs using timestamp + random number, ensuring global uniqueness while being human-readable.

Event Tracking

The core functionality - tracking user events:

Future<void> track(
  String eventName, {
  Map<String, dynamic>? properties,
}) async {
  if (_userId == null) return;
  
  final event = {
    'event_id': _generateId(),
    'event_name': eventName,
    'user_id': _userId!,
    'properties': properties ?? {},
    'timestamp': DateTime.now().toUtc().toIso8601String(),
    'session_id': _sessionId,
  };

  _eventQueue.add(event);
  await _sendEvents();
}

Event Structure:

Network Communication

Future<void> _sendEvents() async {
  if (_eventQueue.isEmpty) return;
  
  // Create copy and clear queue immediately
  final events = List<Map<String, dynamic>>.from(_eventQueue);
  _eventQueue.clear();

  try {
    final response = await http.post(
      Uri.parse('$_host/events'),
      headers: {
        'Content-Type': 'application/json',
        'smolhog-api-key': _apiKey
      },
      body: jsonEncode({'events': events}),
    );

    if (response.statusCode != 200) {
      // Re-queue events on failure
      _eventQueue.addAll(events);
      dev.log('Failed to send events: ${response.statusCode}');
    }
  } catch (e) {
    // Re-queue events on network error
    _eventQueue.addAll(events);
    dev.log('Error sending events: $e');
  }
}

Robust Error Handling:

Screen Tracking Widget

For automatic screen view tracking, we’ll create a wrapper widget:

class AnalyticsScreen extends StatefulWidget {
  final Widget child;
  final String screenName;

  const AnalyticsScreen({
    super.key,
    required this.child,
    required this.screenName,
  });

  @override
  _AnalyticsScreenState createState() => _AnalyticsScreenState();
}

class _AnalyticsScreenState extends State<AnalyticsScreen> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      SmolHog.instance.track(
        'screen_view',
        properties: {'screen_name': widget.screenName},
      );
    });
  }

  @override
  Widget build(BuildContext context) => widget.child;
}

Automatic Screen Tracking:

Public API Export

Create smolhog_flutter/lib/smolhog_flutter.dart:

export 'package:smolhog_flutter/smolhog/smolhog.dart';

This creates a clean public API surface for package consumers.

Package Configuration

Define dependencies in smolhog_flutter/pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  http: ^1.5.0
  shared_preferences: ^2.5.3

Example Implementation

Let’s create a complete example in smolhog_flutter/example/lib/main.dart:

import 'package:flutter/material.dart';
import 'package:smolhog_flutter/smolhog_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize SmolHog SDK
  await SmolHog.initialize(
    apiKey: 'smolhog-ding-dong',
    host: 'http://localhost:8000',
  );

  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SmolHog Demo',
      home: AnalyticsScreen(
        screenName: 'home',
        child: HomePage(),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('SmolHog Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                SmolHog.instance.track(
                  'button_clicked',
                  properties: {
                    'button_type': 'cta',
                    'screen': 'home',
                    'timestamp': DateTime.now().toIso8601String(),
                  },
                );
              },
              child: Text('Track Button Click'),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                SmolHog.instance.track(
                  'user_action',
                  properties: {
                    'action': 'navigation',
                    'destination': 'settings',
                  },
                );
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => AnalyticsScreen(
                      screenName: 'settings',
                      child: SettingsPage(),
                    ),
                  ),
                );
              },
              child: Text('Go to Settings'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Settings')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            SmolHog.instance.track('settings_interaction');
            Navigator.pop(context);
          },
          child: Text('Go Back'),
        ),
      ),
    );
  }
}

Advanced SDK Features

Batch Event Sending

For production use, you might want to implement batching:

class SmolHog {
  Timer? _batchTimer;
  static const int _batchSize = 10;
  static const Duration _batchTimeout = Duration(seconds: 30);

  Future<void> track(String eventName, {Map<String, dynamic>? properties}) async {
    // ... event creation logic ...
    
    _eventQueue.add(event);
    
    // Send immediately if batch is full
    if (_eventQueue.length >= _batchSize) {
      await _sendEvents();
    } else {
      // Otherwise, start/reset batch timer
      _batchTimer?.cancel();
      _batchTimer = Timer(_batchTimeout, () => _sendEvents());
    }
  }
}

User Identification

For logged-in users, add identification:

Future<void> identify(String userId, {Map<String, dynamic>? traits}) async {
  _userId = userId;
  
  // Persist the identified user
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('smolhog_user_id', userId);
  
  // Track identification event
  await track('user_identified', properties: traits);
}

Future<void> reset() async {
  // Clear user data and generate new anonymous ID
  final prefs = await SharedPreferences.getInstance();
  await prefs.remove('smolhog_user_id');
  _userId = _generateId();
  _sessionId = _generateId();
  await prefs.setString('smolhog_user_id', _userId!);
}

Testing the SDK

Run the example app:

cd smolhog_flutter/example
flutter pub get
flutter run

The app will:

  1. Initialize the SDK with your backend URL
  2. Track screen views automatically
  3. Send custom events when buttons are pressed
  4. Handle network failures gracefully

Common Event Types

// User engagement
SmolHog.instance.track('button_clicked', properties: {'button_id': 'signup'});
SmolHog.instance.track('page_viewed', properties: {'page': 'pricing'});
SmolHog.instance.track('feature_used', properties: {'feature': 'dark_mode'});

// Business metrics
SmolHog.instance.track('purchase_completed', properties: {'amount': 29.99, 'currency': 'USD'});
SmolHog.instance.track('subscription_started', properties: {'plan': 'premium'});

// User journey
SmolHog.instance.track('onboarding_completed', properties: {'steps': 5});
SmolHog.instance.track('tutorial_skipped', properties: {'step': 'permissions'});

Custom Properties

Use consistent property naming:

SmolHog.instance.track('video_played', properties: {
  'video_id': 'intro_tutorial',
  'duration': 120,
  'quality': '720p',
  'timestamp': DateTime.now().toIso8601String(),
});

In Part 5, we’ll build the analytics dashboard that visualizes all this beautiful data! The dashboard will include real-time charts, user insights, and powerful filtering capabilities.

Till then, Happy Coding! 📱✨


Next Post
Build your own Posthog - PART 3