Analytics & Crash Reporting

Medium PriorityAsked in ~55% of senior interviews

4 min read

Operations

LayerPurposeTools
Crash reportingCapture every unhandled exception with stack + contextCrashlytics, Sentry, Bugsnag
Analytics eventsUnderstand what users doFirebase Analytics, Mixpanel, Amplitude
Breadcrumbs / logsAnnotate crash reports with contextCrashlytics log, Sentry breadcrumbs
Performance tracesCold-start, network, custom durationsFirebase Performance, Sentry Performance

Pair the funnels: when a crash happens, the report shows the last N events the user did → instant debugging.


Code in action

class Observability {
  static Future<void> init() async {
    // Crash funnels (both!)
    FlutterError.onError = (details) {
      FlutterError.presentError(details);                           // console
      FirebaseCrashlytics.instance.recordFlutterFatalError(details);
    };

    PlatformDispatcher.instance.onError = (error, stack) {
      FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
      return true;
    };
  }

  // Identity & user properties
  static Future<void> identify({required String userId, String? plan}) async {
    await FirebaseCrashlytics.instance.setUserIdentifier(userId);
    await FirebaseAnalytics.instance.setUserId(id: userId);
    if (plan != null) {
      await FirebaseAnalytics.instance.setUserProperty(name: 'plan', value: plan);
    }
  }

  // Structured events
  static Future<void> track(String event, {Map<String, Object>? props}) =>
      FirebaseAnalytics.instance.logEvent(name: event, parameters: props);

  static Future<void> screen(String name) =>
      FirebaseAnalytics.instance.logScreenView(screenName: name);

  // Breadcrumbs — shown in crash reports
  static Future<void> log(String msg) =>
      FirebaseCrashlytics.instance.log(msg);
}

// Auto screen tracking via go_router
final router = GoRouter(
  observers: [
    FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance),
  ],
  routes: [...],
);

Abstract the SDK (you'll want to swap providers)

abstract class AnalyticsClient {
  Future<void> track(String event, {Map<String, Object>? props});
  Future<void> screen(String name);
  Future<void> identify({required String userId, Map<String, Object>? traits});
}

class FirebaseAnalyticsClient implements AnalyticsClient { ... }
class MixpanelAnalyticsClient implements AnalyticsClient { ... }
class NoopAnalyticsClient implements AnalyticsClient { ... }   // for tests / opt-out

Inject AnalyticsClient via DI. Now switching providers is a one-line change.


Event taxonomy — make tracking actually useful

PracticeWhy
Standard naming (object_action: checkout_started, video_played)Searchable, consistent
Required + optional properties documented in a specCross-team alignment
Distinguish screen views from user actionsDifferent funnels
Include source/origin on actionsKnow where users came from
Track failures explicitly (payment_failed with reason)Debug funnels, not just successes
Cap cardinality (don't put userId in properties)Avoids analytics quota issues
Never log PII / passwords / tokensPrivacy + compliance

Privacy and consent

RegionRequirement
EU (GDPR)Explicit consent before tracking; offer opt-out
iOS 14.5+App Tracking Transparency prompt for cross-app tracking (IDFA)
Android 13+Per-app notification settings, scoped storage
CCPA / others"Do not sell my data" opt-out

Implementation: gate AnalyticsClient behind a ConsentService. Pre-consent: NoopAnalyticsClient. Post-consent: real client. Persist the choice; respect changes.


Common mistakes to avoid

❌ Only wiring FlutterError.onError, not PlatformDispatcher.onError
   You'll miss every uncaught async error in production.

❌ Logging PII in analytics events
   "User Alice (alice@x.com) bought premium" → privacy nightmare.
   ✅ Track user IDs only; resolve PII via your server.

❌ Adding tracking everywhere without a spec
   Engineering ships events; product can't find them; data team can't join them.
   ✅ Maintain a tracking plan / dictionary.

❌ Calling debug + production analytics the same
   Inflates real metrics with dev noise.
   ✅ Disable in debug, or route to a separate dev project.

❌ Forgetting to set user identifier for crash reports
   Crashes come in faceless; you can't reach out to affected users.
   ✅ setUserIdentifier(userId) on login.

❌ Triggering analytics on every build()
   Tracking event on rebuild → thousands of "screen_viewed" per session.
   ✅ Route observer for screens; events from explicit user actions.

❌ Hard-coding "Firebase" everywhere
   Switching to Sentry / Mixpanel requires touching every screen.
   ✅ Abstract behind an interface.

Interview follow-ups

  1. Why wire BOTH FlutterError.onError and PlatformDispatcher.onError? FlutterError.onError catches synchronous framework errors (build/layout/paint/gesture). PlatformDispatcher.onError catches uncaught async errors at the Dart VM level. Production apps need both — async errors (failed network, dropped futures) are the most common source of "silent" crashes that need to be reported.

  2. How do you balance analytics granularity vs cost? Track actions and outcomes, not every UI event. Aggregate on the server. Use sampling for high-volume events (e.g., 10% of scroll events). For Firebase Analytics, watch the per-event-name cardinality limit (500). For paid services (Mixpanel), monitor event volume against your plan.

  3. How do you debug "this crash report is useless — no info"? Add breadcrumbs before risky operations (Observability.log('Loading user $id')), set user properties (plan tier, app version), enable native symbolication (Crashlytics auto-symbolicates if you upload dSYM/mapping files), and add structured metadata to errors (recordError(e, s, information: [...])).

  4. How do you handle consent / opt-out cleanly? Wrap your analytics client in a ConsentGate that returns a real or no-op client based on stored consent. On consent change, swap implementations. Combine with FirebaseAnalytics.setAnalyticsCollectionEnabled(false) to fully disable at the SDK level. Test both states.


How helpful was this content?

Please sign in to rate this article.