Flutter RUM
Middleware’s Flutter RUM SDK (middleware_flutter_opentelemetry) instruments your Flutter app using OpenTelemetry, so you can see how real users experience your app. You can track which screens they visit, where performance drops, and what errors they hit, with optional session replay for visual debugging.
Because it’s built on OpenTelemetry, the data is standards-compliant and can be exported via OTLP, while still fitting cleanly into Middleware’s RUM experience.
Before you begin
Make sure you have:
- A Flutter app running on Android, iOS, Web, or Desktop (the SDK supports all, with platform-specific transport notes).
- Flutter 3.7.0+ and Dart 3.7.0+.
- Your Middleware Account Key (the SDK notes this comes from your RUM Flutter installation page).
- Your Middleware endpoint in the form
https://<account>.middleware.io.
Install and initialise the SDK
1 Add the Dependency
Add the package in pubspec.yaml:
dependencies:
middleware_flutter_opentelemetry: ^1.0.0(Use the latest version you’ve validated in your app.)
2 Initialise early in main()
RUM works best when it starts as early as possible because it can capture first navigation events, early lifecycle transitions, and crashes that happen near app startup.
The SDK’s recommended pattern is:
- Hook
FlutterError.onErrorso Flutter framework errors get reported. - Wrap the app in
runZonedGuardedso uncaught async errors are also captured.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:middleware_flutter_opentelemetry/flutterrific_otel.dart';
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
FlutterOTel.reportError(
'FlutterError.onError',
details.exception,
details.stack,
);
};
runZonedGuarded(() {
FlutterOTel.initialize(
serviceName: 'my-flutter-app',
serviceVersion: '1.0.0',
tracerName: 'main',
middlewareAccountKey: "*****", // Obtain from RUM Flutter installation page
endpoint: 'https://<account>.middleware.io',
resourceAttributes: {
'env': 'production',
'service.namespace': 'mobile-apps',
},
);
runApp(const MyApp());
}, (error, stack) {
FlutterOTel.reportError('Zone Error', error, stack);
});
}This setup is doing two important things:
- It ensures errors don’t escape your telemetry pipeline.
- It gives the SDK a consistent place to attach context (service name, version, environment) to every event.
What the SDK tracks automatically:
After initialization, the SDK automatically instruments common RUM fundamentals so you get useful data without custom code. That includes:
- Navigation: Route changes and user flows, so you can reason about what screen led to an issue.
- App lifecycle: Foreground and background transitions, useful for crashes after resume or stalls.
- Performance: Frame rate and rendering metrics, useful for “jank” investigations.
- Errors: Captured exceptions with context so you can tie errors back to sessions.
Enable Session Replay
Session replay is useful when an error report isn’t enough and you want to see what the user saw. The Flutter SDK supports session recording, and the expected integration has two parts.
- Start and stop recording:
FlutterOTel.startSessionRecording()FlutterOTel.stopSessionRecording()
- Wrap your UI with a
RepaintBoundaryusing the SDK key:
FlutterOTel.repaintBoundaryKey
Example:
@override
void initState() {
super.initState();
// Start after a short delay so the widget tree is stable.
Timer(const Duration(milliseconds: 500), () {
FlutterOTel.startSessionRecording();
});
}
@override
void dispose() {
try {
FlutterOTel.stopSessionRecording();
} catch (_) {}
super.dispose();
}
@override
Widget build(BuildContext context) {
return RepaintBoundary(
key: FlutterOTel.repaintBoundaryKey,
child: /* your app */,
);
}Add Lightweight UI Interaction Tracking
When you want more clarity around what the user did (for example, Submit button pressed, then API call, then error), widget-level tracking helps.
The SDK provides widget extensions for common patterns like buttons, text fields, and error boundaries.
ElevatedButton(
onPressed: handleSubmit,
child: const Text('Submit'),
).withOTelButtonTracking('submit_form');
TextField(
decoration: const InputDecoration(
labelText: 'Enter something',
border: OutlineInputBorder(),
),
).withOTelTextFieldTracking('demo_text_field');
RiskyWidget().withOTelErrorBoundary('risky_operation');This gives you structured interaction breadcrumbs without forcing you to manually instrument every click and input.
Instrument Important Business Operations with Custom Spans
Automatic RUM is great for baseline visibility, but custom spans are how you make RUM diagnostic. Typical examples include login, checkout, sync data, and fetch profile.
The SDK exposes a tracer (FlutterOTel.tracer) so you can create spans around key flows and attach useful attributes.
final tracer = FlutterOTel.tracer;
final span = tracer.startSpan('fetch_user_data', attributes: {
'user.id': userId,
'api.endpoint': '/users',
});
try {
final result = await apiClient.getUser(userId);
span.setStatus(SpanStatusCode.Ok);
return result;
} catch (e, stackTrace) {
span.recordException(e, stackTrace: stackTrace);
span.setStatus(SpanStatusCode.Error, e.toString());
rethrow;
} finally {
span.end();
}Configure via --dart-define
For production apps, you’ll often want different service names, endpoints, or headers per environment (dev, staging, prod) without changing code.
The SDK supports standard OpenTelemetry environment variables, and shows Flutter usage using --dart-define.
flutter run \
--dart-define=OTEL_SERVICE_NAME=my-flutter-app \
--dart-define=OTEL_SERVICE_VERSION=1.0.0 \
--dart-define=OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector:4317 \
--dart-define=OTEL_EXPORTER_OTLP_PROTOCOL=grpc \
--dart-define=OTEL_EXPORTER_OTLP_HEADERS=Authorization=your-api-keyThis is especially useful in CI/CD pipelines where you want one build artifact and environment-specific configuration.
HTTP Client Instrumentation
This section includes only the HTTP client instrumentation you asked for, sourced from middleware_dart_opentelemetry.
If your Flutter app makes outbound requests using dart:io’s HttpClient, you can wrap it with OTelHttpClient to automatically:
- create an HTTP span for each request
- inject W3C Trace Context headers (
traceparent,tracestate) - capture request and response metadata (method, URL, status, timings, errors)
- correlate client spans with downstream services
1. Wrap HttpClient
import 'dart:io';
import 'package:middleware_dart_opentelemetry/middleware_dart_opentelemetry.dart';
void main() async {
await OTel.initialize(serviceName: 'http-client-demo');
final client = OTelHttpClient(HttpClient());
final request = await client.getUrl(Uri.parse('https://api.example.com/data'));
final response = await request.close();
print('Status: ${response.statusCode}');
}At this point, spans are generated automatically and exported using your configured OTLP exporter.
2. Recommended: make HTTP calls a child of a parent span
If you want network calls to appear under a meaningful business span (for example, checkout or load_home_screen), create a parent span and run the request inside its context:
final tracer = OTel.tracer();
final client = OTelHttpClient(HttpClient());
final span = tracer.startSpan('demo-operation');
await Context.withSpan(span, () async {
final request = await client.getUrl(Uri.parse('https://middleware.io'));
final response = await request.close();
print('Status: ${response.statusCode}');
});
span.end();This produces:
- a parent span (
demo-operation) - plus a child HTTP client span for the request
- with correct W3C context propagation
Verify your data in Middleware
After shipping the build:
- Open RUM and confirm sessions and events are flowing. You should see navigation and performance data when the app is being used.
- Trigger an intentional error in a dev build and confirm it appears with session context.
- If session replay is enabled, open a session and confirm replay data is attached.
Need assistance or want to learn more about Middleware? Contact our support team at [email protected] or join our Slack channel.