Skip to main content

Flutter Mobile Observability with OpenTelemetry

ยท 5 min read
Nimisha G J
Engineer at base14

Most teams have solid observability on their backend. Structured logs, distributed traces, SLOs, alerting. The mobile app, which is often the first thing a user touches, gets crash reports at best.

A user taps a button and nothing happens. Was it the network? A janky frame that swallowed the tap? A backend timeout? A state management bug? Without telemetry on the device, you are guessing.

This post explains a couple of approaches we have used to help our customers instrument their Flutter apps and when to use each approach.

Why Mobile Observability Is Differentโ€‹

With a backend service, you can SSH in, read logs, attach a debugger, and deploy a fix in minutes. On mobile, your code runs on hardware you have never seen, over networks you do not manage, inside an OS that will kill your process to save battery.

A few things make mobile uniquely hard:

  • Battery constraints. Telemetry export burns power. You need batching, compression, and sampling strategies that respect the device.
  • Unreliable connectivity. Spans need to be buffered and retried. You cannot assume the network is there when you need it.
  • Background kills. The OS can terminate your app at any time. If you haven't flushed your telemetry buffer, those spans are gone.
  • Release cycles. You can't hot-fix a mobile app. A bad instrumentation build ships to the App Store and stays there until the next review cycle.

These constraints mean you can't just bolt your backend tracing library onto a Flutter app and call it done. You need instrumentation designed for mobile.

Two Approaches to Flutter Instrumentationโ€‹

We documented two paths, each built on OpenTelemetry.

Flutterific RUM gives you automatic session-level monitoring. Drop in the package, add a route observer, and you get session tracking, screen load times, jank detection, ANR monitoring, cold start measurement, and navigation spans. No per-signal tracing code required.

Direct OpenTelemetry SDK gives you full control. You manage span creation, configure W3C trace context propagation to correlate mobile spans with backend traces, add battery-aware sampling, and build custom conversion funnels.

Here's how they compare:

Flutterific RUMDirect SDK
Session trackingAutomaticManual
Device/app/network contextAutomatic on every spanManual
Navigation spansAutomaticManual
Screen load/dwell timesAutomaticNot included
Cold start measurementAutomaticManual
Jank/ANR detectionAutomaticNot included
HTTP tracingVia RumHttpClient wrapperVia HttpService wrapper
W3C trace context propagationAutomatic (traceparent header)Automatic
Battery-aware samplingAutomatic (4-tier adaptive)Automatic
Breadcrumb trailAutomatic (last 20 actions on error spans)Not included
Error boundary widgetIncludedNot included
Flush on backgroundAutomatic (AppLifecycleListener)Manual
Conversion funnel trackingNot includedVia FunnelTrackingService
Custom spans and eventsSupportedSupported
Best forRUM dashboards, UX monitoringBackend correlation, fine-grained control

The Flutterific setup is minimal. Here's the entry point:

lib/main_otel.dart
import 'dart:ui';

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

import 'main.dart';
import 'otel/otel_config.dart';
import 'otel/rum_cold_start.dart';
import 'otel/rum_session.dart';

Future<void> main() async {
RumColdStart.markMainStart();

FlutterError.onError = (details) {
FlutterError.presentError(details);
RumSession.instance.forceNextSample();
RumSession.instance.recordBreadcrumb(
'error',
'flutter_error: ${details.exceptionAsString()}',
);
FlutterOTel.reportError(
details.exceptionAsString(),
details.exception,
details.stack,
attributes: {
'app.screen.name': RumSession.instance.currentScreen,
'session.id': RumSession.instance.sessionId,
'error.breadcrumbs': RumSession.instance.getBreadcrumbString(),
},
);
OTelConfig.flush();
};

PlatformDispatcher.instance.onError = (error, stack) {
RumSession.instance.forceNextSample();
RumSession.instance.recordBreadcrumb(
'error',
'uncaught_error: ${error.runtimeType}',
);
FlutterOTel.reportError(
'Uncaught error',
error,
stack,
attributes: {
'app.screen.name': RumSession.instance.currentScreen,
'session.id': RumSession.instance.sessionId,
'error.breadcrumbs': RumSession.instance.getBreadcrumbString(),
},
);
OTelConfig.flush();
return true;
};

await OTelConfig.initialize();
WidgetsBinding.instance.addObserver(OTelConfig.lifecycleObserver);

AppLifecycleListener(
onPause: () {
OTelConfig.flush();
OTelConfig.pauseJankDetection();
},
onResume: () {
OTelConfig.resumeJankDetection();
RumSession.instance.refreshBatteryState();
},
onExitRequested: () async {
await OTelConfig.shutdown();
return AppExitResponse.exit;
},
);

runApp(const MyApp());
RumColdStart.measureFirstFrame();
}

The lib/otel/ directory holds session management, route observation, jank detection, and span export. The Flutterific RUM reference covers every file.

What You Get Out of the Boxโ€‹

With Flutterific RUM, the following signals are collected automatically once you wire up the route observer and lifecycle observer:

SignalWhat's Captured
Sessionsession.id, session.start, session.duration_ms on every span
Devicedevice.model.identifier, device.model.name, device.manufacturer, os.type, os.version
Appservice.version, app.build_id, app.installation.id
Networknetwork.type (wifi / cellular / none), live updates
Batterydevice.battery.level, device.battery.state with 4-tier adaptive sampling
Cold Startapp.cold_start span with duration histogram
Screen Loadscreen.load span with timing histogram
Screen Dwellscreen.dwell span with duration histogram
Navigationnavigation.push / pop / replace / remove spans
Jankjank.frame spans for frames exceeding 16ms
ANRanr.detected spans when the main thread blocks for 5+ seconds
Lifecycleapp_lifecycle.changed spans (active, inactive, paused)
BreadcrumbsLast 20 user actions attached to error spans as error.breadcrumbs
W3C Propagationtraceparent header injected on outgoing HTTP requests
Flush on BackgroundPending spans flushed via AppLifecycleListener on pause/exit
Error BoundaryErrorBoundaryWidget catches render errors with fallback UI and retry

Additional signals like user identity, button clicks, rage click detection, HTTP requests, and custom business events are available with a few lines of code each.

Getting Startedโ€‹

Start with the Flutter Mobile Observability guide. It walks through choosing an approach, installing dependencies, initializing telemetry, and verifying that spans reach your collector. Takes about 15-20 minutes.

The decision framework is straightforward:

  • Use Flutterific RUM if you want session-level UX monitoring (jank, screen times, navigation, breadcrumbs, battery-aware sampling) with minimal boilerplate.
  • Use the Direct SDK if you need conversion funnel tracking or full control over span creation and batching.

The full reference docs cover everything from directory structure to production deployment:

Closingโ€‹

Every production service gets traces and metrics. Mobile apps should too. OpenTelemetry makes it possible without locking into a vendor, and Flutter's single-codebase model means you instrument once and cover both platforms.