Lesson 55 of 77 intermediate

Logging, Observability, Analytics & Feature Flags

Monitoring production health, understanding user behaviour, and shipping safely with flags

Open interactive version (quiz + challenge)

Real-world analogy

Observability is like a pilot's cockpit — analytics tells you where the plane is going (user journeys), logging tells you what the engines are doing (system events), crash reporting tells you when something breaks (incidents), and feature flags let you flick switches mid-flight without landing the plane (deployments).

What is it?

Observability in Flutter apps combines crash reporting, analytics, logging, and feature flags to understand production health, user behaviour, and ship new features safely with controlled rollouts.

Real-world relevance

A SaaS collaboration app tracks workspace_created, message_sent, and file_uploaded events in Amplitude for funnel analysis. Feature flags in Remote Config gate the new AI summarization feature to 5% of Pro users. Crashlytics custom keys (workspaceId, userRole) help reproduce workspace-specific crashes in minutes.

Key points

Code example

// Comprehensive observability setup
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:talker_flutter/talker_flutter.dart';

final talker = TalkerFlutter.init();

Future<void> setupObservability() async {
  // Crashlytics — catch all Flutter errors
  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
  };
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };
}

// Analytics service with event taxonomy
class Analytics {
  static final _analytics = FirebaseAnalytics.instance;

  static Future<void> workspaceCreated({
    required String plan,
    required int memberCount,
  }) async {
    await _analytics.logEvent(
      name: 'workspace_created',
      parameters: {'plan': plan, 'member_count': memberCount},
    );
    talker.info('workspace_created plan=$plan members=$memberCount');
  }

  static Future<void> paymentFailed({
    required String reason,
    required String plan,
  }) async {
    await _analytics.logEvent(
      name: 'payment_failed',
      parameters: {'reason': reason, 'plan': plan},
    );
    // Also record as non-fatal for investigation
    FirebaseCrashlytics.instance.recordError(
      'PaymentFailed: $reason',
      null,
      fatal: false,
      information: ['plan: $plan'],
    );
  }
}

// Feature flags with Remote Config
class FeatureFlags {
  static final _config = FirebaseRemoteConfig.instance;

  static Future<void> initialize() async {
    await _config.setConfigSettings(RemoteConfigSettings(
      fetchTimeout: const Duration(seconds: 10),
      minimumFetchInterval: const Duration(hours: 12),
    ));
    // Safe defaults — app works without network
    await _config.setDefaults({
      'ai_summary_enabled': false,
      'max_file_upload_mb': 25,
      'paywall_variant': 'control',
    });
    await _config.fetchAndActivate();
  }

  static bool get aiSummaryEnabled => _config.getBool('ai_summary_enabled');
  static int get maxFileUploadMb => _config.getInt('max_file_upload_mb');
  static String get paywallVariant => _config.getString('paywall_variant');
}

Line-by-line walkthrough

  1. 1. FlutterError.onError catches errors inside the Flutter framework — widget build failures, assertion errors, rendering exceptions.
  2. 2. PlatformDispatcher.instance.onError catches errors in async code and platform-level errors not caught by Flutter — the two together give full coverage.
  3. 3. recordFlutterFatalError vs recordError: fatal errors crashed the app; non-fatal were caught but worth tracking. Crashlytics groups these separately in the dashboard.
  4. 4. Analytics.workspaceCreated logs to both Firebase Analytics and the local talker logger — production log for analysis, local log for debugging.
  5. 5. payment_failed is also recorded as a non-fatal Crashlytics error with context — this surfaces payment issues in the Crashlytics dashboard alongside crashes for prioritisation.
  6. 6. RemoteConfigSettings sets fetchTimeout (network call timeout) and minimumFetchInterval (how often to hit the server — avoid quota exhaustion).
  7. 7. setDefaults ensures the app behaves correctly even on first launch before Remote Config fetches — critical for flags that gate core features.
  8. 8. fetchAndActivate() fetches new values AND activates them atomically — fetching without activating leaves stale values until the next app restart.

Spot the bug

class FeatureFlags {
  static final _config = FirebaseRemoteConfig.instance;

  static Future<void> initialize() async {
    await _config.fetchAndActivate();
  }

  static bool get newDashboard => _config.getBool('new_dashboard');
}

// In main():
await FeatureFlags.initialize();
if (FeatureFlags.newDashboard) {
  runApp(const NewDashboardApp());
} else {
  runApp(const LegacyApp());
}
Need a hint?
This crashes on first install and sometimes fails silently. Two things are wrong.
Show answer
Bug 1: No setDefaults call — on first install, before any fetch completes, getBool('new_dashboard') returns false (the SDK default for missing bool keys) but this is accidental behaviour, not intentional. If the default should be true, the app incorrectly shows the legacy UI. Fix: call setDefaults({'new_dashboard': false}) with explicit intent before fetchAndActivate. Bug 2: No setConfigSettings call — without minimumFetchInterval configuration, the SDK uses its default (12h in production, unlimited in debug) but more importantly there is no fetchTimeout set. If the network is slow, fetchAndActivate can hang indefinitely. Fix: add RemoteConfigSettings with a fetchTimeout of 10 seconds so the app startup isn't blocked on a slow Remote Config server — it falls back to cached/default values gracefully.

Explain like I'm 5

Imagine your app is a spaceship. Logging is the flight recorder that writes down everything that happens. Crashlytics is the emergency beacon that screams if something breaks. Analytics is the mission report that tells you how the journey went. Feature flags are the control switches that let ground control turn things on and off while you're flying.

Fun fact

Spotify uses feature flags to deploy code to 100% of users but enable features for 0% initially — every new feature ships dark (deployed but inactive) and is turned on gradually. This decouples deployment from release, dramatically reducing incident risk.

Hands-on challenge

Set up a complete observability stack for a Flutter app: (1) Configure Crashlytics to catch both Flutter framework errors and platform errors. (2) Create an Analytics class with at least 3 semantically named events (with parameters). (3) Set up Remote Config with 3 feature flags and safe defaults. (4) Implement a crash triage checklist for a hypothetical NullPointerException in a payment screen. (5) Define what a healthy crash-free user rate target should be.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery