Skip to content
Last9
Book demo

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)

CDN artifacts

ArtifactURL
Tarballhttps://cdn.last9.io/rum-sdk/flutter/builds/0.7.0/last9_rum_flutter-0.7.0.tar.gz
Checksumhttps://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

  1. Download, verify, and extract the SDK

    VERSION=0.7.0
    curl -L -o last9_rum_flutter.tar.gz \
    "https://cdn.last9.io/rum-sdk/flutter/builds/${VERSION}/last9_rum_flutter-${VERSION}.tar.gz"
    # Verify checksum
    EXPECTED=$(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 vendor
    tar xzf last9_rum_flutter.tar.gz -C vendor/
    rm last9_rum_flutter.tar.gz
  2. Add as a path dependency

    In pubspec.yaml:

    dependencies:
    last9_rum_flutter:
    path: vendor/flutter

    Then:

    flutter pub get
  3. Android — add the CDN Maven repository

    dependencyResolutionManagement {
    repositories {
    google()
    mavenCentral()
    maven { url = uri("https://cdn.last9.io/rum-sdk/android/maven/") }
    }
    }
  4. iOS — add the Last9RUM podspec

    In ios/Podfile, inside the target 'Runner' block:

    target 'Runner' do
    use_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__))
    end

    Then:

    cd ios && pod install
  5. 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 spanWhat it measures
dnsDNS lookup duration
tcp_connectTCP connection establishment
tls_handshakeTLS negotiation
ttfbTime 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',
});
// Clear
await 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.