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.8.0(resolved from CDN Maven) - iOS:
Last9RUM 0.8.0(resolved from CDN podspec)
- Android:
Create Client Monitoring Tokens
Create one Client token per platform — do not combine Android and iOS origins on a single token. Each build uses the native SDK for that platform, so separate tokens keep origin scoping, rotation, and access control aligned with Android and iOS setup.
- Open Last9 → Settings → Ingestion Tokens
- Click Create Token → choose type Client → create an Android token with allowed origin
android://com.yourcompany.yourapp(your app’s exact package name). Copy the token. - Create a second Client token for iOS with allowed origin
ios://com.yourcompany.yourapp(your app’s exact bundle ID). Copy the token. - Copy the OTLP endpoint URL (the same URL is used for both platforms)
CDN artifacts
| Artifact | Stable URL | Versioned URL |
|---|---|---|
| Tarball | https://cdn.last9.io/rum-sdk/flutter/builds/stable/v0/last9_rum_flutter.tar.gz | https://cdn.last9.io/rum-sdk/flutter/builds/0.8.0/last9_rum_flutter-0.8.0.tar.gz |
| Checksum | https://cdn.last9.io/rum-sdk/flutter/builds/stable/v0/last9_rum_flutter.tar.gz.sha256 | https://cdn.last9.io/rum-sdk/flutter/builds/0.8.0/last9_rum_flutter-0.8.0.tar.gz.sha256 |
The stable CDN channel is major-pinned at stable/v0. Staging builds use the -alpha.<run_number> suffix and explicit versioned URLs.
Installation
-
Download, verify, and extract the SDK
curl -L -o last9_rum_flutter.tar.gz \"https://cdn.last9.io/rum-sdk/flutter/builds/stable/v0/last9_rum_flutter.tar.gz"# Verify checksumEXPECTED=$(curl -sL "https://cdn.last9.io/rum-sdk/flutter/builds/stable/v0/last9_rum_flutter.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/stable/v0/Last9RUM.podspec'flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))endThen:
cd ios && pod install -
Initialize the SDK
import 'dart:io' show Platform;import 'package:flutter/widgets.dart';import 'package:last9_rum_flutter/last9_rum_flutter.dart';void main() async {WidgetsFlutterBinding.ensureInitialized();final isIos = Platform.isIOS;await L9Rum.initialize(L9RumConfig(baseUrl: 'https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>',// Use the platform-matching token and origin pair — see// "Create Client Monitoring Tokens" above.origin: isIos? 'ios://com.yourcompany.yourapp': 'android://com.yourcompany.yourapp',clientToken: isIos ? 'your-ios-client-token' : 'your-android-client-token',serviceName: 'my-flutter-app',serviceVersion: '1.0.0',deploymentEnvironment: 'production',));runApp(const MyApp());}
Configuration
final L9RumConfig config = L9RumConfig( // --- Required --------------------------------------------------------- baseUrl: 'https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>', clientToken: Platform.isIOS ? 'your-ios-client-token' : 'your-android-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. Must match the origin on the platform's Client token — // android://<package> on Android, ios://<bundle-id> on iOS. Pass the // matching clientToken for the same platform. Needs `import 'dart:io' show // Platform` as shown in the Installation section above. origin: Platform.isIOS ? 'ios://com.yourcompany.yourapp' : 'android://com.yourcompany.yourapp',
// Specific build identifier (maps to app.build_id) appBuildId: '1.0.0-build-42',
// Optional override for the app.installation.id resource attribute. // The Client-ID header always uses the native SDK-generated per-install UUID. 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, ),);Stable release notes
Flutter 0.8.0 uses native Android and iOS SDK 0.8.0. The native layers now send an SDK-generated per-install UUID as the ingestion Client-ID header instead of serviceName, so devices no longer share one rate-limit bucket. No Flutter app-code change is required.
appInstallationId only overrides the app.installation.id resource attribute. It does not override the Client-ID header, so the header and the resource attribute can differ when you set appInstallationId manually.
The SDK also normalizes non-Error Dart payloads and bridged native fallback errors so exception.type stays populated.
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.