Flutter RUM SDK
Install and configure the Last9 Flutter RUM SDK. Dart plugin over native Android and iOS SDKs, CDN-hosted tar.gz, auto-instruments HttpClient, NavigatorObserver, errors, ANRs.
Real User Monitoring for Flutter apps. Automatic instrumentation for sessions, views, network requests, errors, and resource metrics via OpenTelemetry. ANR detection is available on Android only.
The Flutter SDK is a Dart plugin over the Android and iOS native SDKs. Both platform CDN repos must be configured so native dependencies resolve.
Prerequisites
- Flutter >= 3.10.0
- Dart SDK >= 3.0.0
- iOS 15.1+
- Android minSdk 21 (Android 5.0+)
- Native dependencies:
- Android:
io.last9:rum-android:0.7.0(resolved from CDN Maven) - iOS:
Last9RUM 0.7.0(resolved from CDN podspec)
- Android:
CDN artifacts
| Artifact | URL |
|---|---|
| Tarball | https://cdn.last9.io/rum-sdk/flutter/builds/0.7.0/last9_rum_flutter-0.7.0.tar.gz |
| Checksum | https://cdn.last9.io/rum-sdk/flutter/builds/0.7.0/last9_rum_flutter-0.7.0.tar.gz.sha256 |
Staging builds use the -alpha.<run_number> suffix.
Installation
-
Download, verify, and extract the SDK
VERSION=0.7.0curl -L -o last9_rum_flutter.tar.gz \"https://cdn.last9.io/rum-sdk/flutter/builds/${VERSION}/last9_rum_flutter-${VERSION}.tar.gz"# Verify checksumEXPECTED=$(curl -sL "https://cdn.last9.io/rum-sdk/flutter/builds/${VERSION}/last9_rum_flutter-${VERSION}.tar.gz.sha256")ACTUAL=$(shasum -a 256 last9_rum_flutter.tar.gz | awk '{print $1}')[ "$EXPECTED" = "$ACTUAL" ] && echo "OK" || echo "CHECKSUM MISMATCH"# Extract into vendor/mkdir -p vendortar xzf last9_rum_flutter.tar.gz -C vendor/rm last9_rum_flutter.tar.gz -
Add as a path dependency
In
pubspec.yaml:dependencies:last9_rum_flutter:path: vendor/flutterThen:
flutter pub get -
Android — add the CDN Maven repository
dependencyResolutionManagement {repositories {google()mavenCentral()maven { url = uri("https://cdn.last9.io/rum-sdk/android/maven/") }}}dependencyResolutionManagement {repositories {google()mavenCentral()maven { url = uri("https://cdn.last9.io/rum-sdk/android/maven/") }}} -
iOS — add the Last9RUM podspec
In
ios/Podfile, inside thetarget 'Runner'block:target 'Runner' douse_frameworks!pod 'Last9RUM', :podspec => 'https://cdn.last9.io/rum-sdk/ios/builds/0.7.0/Last9RUM.podspec'flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))endThen:
cd ios && pod install -
Initialize the SDK
import 'package:flutter/widgets.dart';import 'package:last9_rum_flutter/last9_rum_flutter.dart';void main() async {WidgetsFlutterBinding.ensureInitialized();await L9Rum.initialize(const L9RumConfig(baseUrl: 'https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>',origin: 'https://app.last9.io',clientToken: 'your-client-token',serviceName: 'my-flutter-app',serviceVersion: '1.0.0',deploymentEnvironment: 'production',));runApp(const MyApp());}
Configuration
const L9RumConfig config = L9RumConfig( // --- Required --------------------------------------------------------- baseUrl: 'https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>', clientToken: 'your-client-token', serviceName: 'my-flutter-app', serviceVersion: '1.0.0', deploymentEnvironment: 'production',
// --- Optional ---------------------------------------------------------
// Origin sent as X-LAST9-ORIGIN header. // Required for client_monitoring tokens. origin: 'https://app.last9.io',
// Specific build identifier (maps to app.build_id) appBuildId: '1.0.0-build-42',
// Unique per-install ID (NOT a hardware ID). Persists across launches. appInstallationId: null,
// Session sampling rate: 0-100 (percentage). 100 = sample everything. sampleRate: 100,
// Print debug logs to native console debugLogs: false,
// Automatically instrument HTTP requests networkInstrumentation: true,
// Automatically capture unhandled Dart errors errorInstrumentation: true,
// Max spans per export batch maxExportBatchSize: 100,
// Export timeout in milliseconds exportTimeoutMs: 30000,
// ANR detection (Android only) anrDetectionEnabled: true, anrThresholdMs: 5000,
// Periodically sample memory and CPU resourceMonitoringEnabled: true, resourceSamplingIntervalMs: 30000,
// Setting this to true will hide network requests (and their // DNS/TCP/TLS/TTFB phase child spans) from the Last9 dashboard's // Sessions → APIs tab. Each request would get its own traceId // instead of sharing the current view's traceId, and that tab // only fetches spans that share the View's traceId. Keep this // false unless you specifically need per-request trace isolation. isolateTracePerRequest: false,
// Custom resource attributes added to every span resourceAttributes: <String, String>{ 'app.platform': 'flutter', },
// Fine-grained network ignore rules. contains() uses substring matching; // regex() uses regex search semantics. Matched URLs are dropped before // span creation. ignorePatterns: L9NetworkIgnorePatterns( fullUrl: <L9UrlPattern>[ L9UrlPattern.contains('https://cdn.example.com'), L9UrlPattern.regex(RegExp(r'^https://.*\.example\.com', caseSensitive: false)), ], pathname: <L9UrlPattern>[ L9UrlPattern.contains('.pdf'), L9UrlPattern.contains('.jpg'), L9UrlPattern.regex(RegExp(r'^/internal/metrics')), ], hostname: <L9UrlPattern>[ L9UrlPattern.contains('cdn.example.com'), L9UrlPattern.regex(RegExp(r'(^|\.)assets\.example\.com$', caseSensitive: false)), ], ),
// 'preserve' (default): keep traceparent on ignored requests. // 'strip': remove traceparent from ignored requests. propagationMode: L9PropagationMode.preserve,
// W3C Baggage propagation on outgoing requests baggage: L9BaggageConfig( enabled: false, allowedKeys: <String>['session.id', 'user.id'], maxTotalBytes: 8192, warnAtPercentage: 80, ),);API reference
Automatic view tracking
Attach L9NavigationObserver to your MaterialApp or CupertinoApp:
MaterialApp( navigatorObservers: <NavigatorObserver>[L9NavigationObserver()], // ...);Network ignore patterns
Skip noisy URLs before span creation by matching against full URL, pathname, or hostname. L9UrlPattern.contains() uses substring matching; L9UrlPattern.regex() uses regex search semantics.
await L9Rum.initialize(L9RumConfig( // ...required config ignorePatterns: L9NetworkIgnorePatterns( fullUrl: <L9UrlPattern>[ L9UrlPattern.contains('https://cdn.example.com'), L9UrlPattern.regex(RegExp(r'^https://.*\.example\.com', caseSensitive: false)), ], pathname: <L9UrlPattern>[ L9UrlPattern.contains('.pdf'), L9UrlPattern.contains('.jpg'), L9UrlPattern.regex(RegExp(r'^/internal/metrics')), ], hostname: <L9UrlPattern>[ L9UrlPattern.contains('cdn.example.com'), L9UrlPattern.regex(RegExp(r'(^|\.)assets\.example\.com$', caseSensitive: false)), ], ), // preserve (default): keep traceparent on ignored requests. // strip: remove traceparent from ignored requests. propagationMode: L9PropagationMode.strip,));Network phase child spans
Network instrumentation emits child spans for individual HTTP phases where native timing hooks are available:
| Child span | What it measures |
|---|---|
dns | DNS lookup duration |
tcp_connect | TCP connection establishment |
tls_handshake | TLS negotiation |
ttfb | Time from request sent to first response byte |
No SDK config change is required. The native Android and iOS layers use OkHttp EventListener and URLSession task metrics respectively.
Reused connections skip DNS, TCP, and TLS work. For those requests the SDK emits zero-duration child spans with l9rum.network.phase.skipped=true so the waterfall shape stays consistent.
HTTP client instrumentation
When networkInstrumentation is true, the SDK installs a dart:io HttpOverrides that wraps the default HttpClient. Requests made through the http package and dio go through HttpClient under the hood and are traced automatically — no extra setup needed.
Identify a user
await L9Rum.identify(const L9UserInfo( id: 'user-123', name: 'Jane', email: 'jane@example.com', fullName: 'Jane Doe', roles: <String>['admin'],));Clear user on sign-out
await L9Rum.clearUser();Capture errors
try { // risky operation} catch (error, stackTrace) { await L9Rum.captureError( error, stackTrace: stackTrace, context: <String, dynamic>{'screen': 'checkout'}, );}Unhandled error forwarding
Flutter’s error surfaces are separate — framework, platform dispatcher, and async zones each need wiring:
void main() async { WidgetsFlutterBinding.ensureInitialized(); await L9Rum.initialize(config);
// Flutter framework errors FlutterError.onError = (FlutterErrorDetails details) { FlutterError.presentError(details); L9Rum.captureError(details.exception, stackTrace: details.stack); };
// Platform dispatcher errors PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { L9Rum.captureError(error, stackTrace: stack); return true; };
// Async errors via zone runZonedGuarded( () => runApp(const MyApp()), (Object error, StackTrace stack) { L9Rum.captureError(error, stackTrace: stack); }, );}Track views manually
await L9Rum.startView('ProductDetailsScreen');await L9Rum.setViewName('Product #42');Custom events
await L9Rum.addEvent('purchase_completed', attributes: <String, dynamic>{ 'product_id': '12345', 'amount': 29.99,});Global span attributes
await L9Rum.spanAttributes(<String, dynamic>{ 'experiment': 'checkout_v2', 'feature_flag': 'new_cart',});
// Clearawait L9Rum.spanAttributes(null);Session ID
final String? sessionId = await L9Rum.getSessionId();Flush pending data
await L9Rum.flush();WebView correlation
Instrument a webview_flutter controller to share the native session ID with Browser RUM spans running inside it:
import 'package:webview_flutter/webview_flutter.dart';import 'package:last9_rum_flutter/last9_rum_flutter.dart';
final controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..loadRequest(Uri.parse('https://app.example.com'));
L9Rum.instrumentWebView(controller);The SDK injects session context at navigation time via the native iOS and Android SDKs. No manual re-injection is needed.
See the WebView Session Correlation guide for auto-load options and verification steps.
Next steps
Once data is flowing, explore it in Discover > Applications — performance, errors, and sessions.
Troubleshooting
Please get in touch with us on Discord or Email if you have any questions.