Flutter SDK Integration
This guide covers every package of the Kubesense Flutter SDK: adding the packages, setting up Android, iOS and Web, initializing the SDK, and enabling RUM, Logs, crash reporting, distributed tracing, Session Replay, Feature Flags and WebView tracking, plus the network integrations for dart:io/package:http, Dio, GraphQL and gRPC.
| Field | Value |
|---|---|
| Flutter | 3.27 or newer (Dart 3.6 or newer) |
| Android | minSdk 23 |
| iOS | 15.0 or newer |
| Web | Supported, except Logs and Session Replay, which are not available on Web yet. |
| Distribution | pub.dev, packages kubesense_* |
| Native SDKs | Android SDK 2.1.0, iOS SDK 1.0.0, browser SDK (@kubesense/kubesense-browser-rum-slim 1.8.0) |
The Dart layer is thin: on each platform the plugin drives the native Kubesense SDK, so events from Flutter and from native code land in the same RUM session.
Prerequisites
Before integrating, obtain the following from your Kubesense workspace:
| Value | Where to find it |
|---|---|
applicationId | Settings → RUM Application Management → your application → View Details |
clientToken | Settings → RUM Application Management → your application → View Details |
kubesenseRumEndpoint | Your Kubesense collector host, for example us2.kubesense.ai |
The client token is a public token meant for client-side SDKs, so it is safe to ship in the app. Never put a server API key in a mobile or web app.
Packages
kubesense_flutter_plugin is always required. Add the others only for the features your app uses:
| Feature | Package |
|---|---|
| RUM, Logs, crash and error reporting, distributed tracing, consent | kubesense_flutter_plugin (required) |
HTTP requests made with dart:io or package:http | kubesense_tracking_http_client |
| Requests made with Dio | kubesense_dio |
GraphQL requests made with gql_link | kubesense_gql_link |
| gRPC calls | kubesense_grpc_interceptor |
| Session Replay | kubesense_session_replay |
| Feature Flags | kubesense_flags_flutter |
| Feature Flags in pure Dart or a background isolate | kubesense_flags |
WebViews (webview_flutter) | kubesense_webview_tracking |
WebViews (flutter_inappwebview) | kubesense_inappwebview_tracking |
Core Setup
1. Add the packages
# pubspec.yaml
dependencies:
kubesense_flutter_plugin: ^1.0.1
# Only the integrations you use, for example:
# kubesense_tracking_http_client: ^1.0.0
# kubesense_session_replay: ^1.0.0Then run flutter pub get.
2. Set up each platform
Android
-
In
android/app/build.gradle(orbuild.gradle.kts), setminSdkto at least23. -
Make sure
android/app/src/main/AndroidManifest.xmlallows network access:<uses-permission android:name="android.permission.INTERNET" /> -
The native Kubesense Android SDK is downloaded from Maven Central. Make sure
android/settings.gradle(orsettings.gradle.kts) listsgoogle()andmavenCentral():dependencyResolutionManagement { repositories { google() mavenCentral() } }
iOS
The plugins pull in the Kubesense iOS SDK themselves: through Swift Package Manager (the default from Flutter 3.44), or from CocoaPods trunk if your app uses CocoaPods.
- Set the iOS deployment target to 15.0 or higher. With CocoaPods, set
platform :ios, '15.0'(or higher) inios/Podfileand keepuse_frameworks!. - If you use Session Replay: in Xcode, open Runner → Build Settings and set Strip Style to Non-Global Symbols. Otherwise release builds strip symbols that Session Replay needs.
Web
On Web, the plugin uses the Kubesense browser SDK, which you install from npm and load before Flutter starts.
-
From your Flutter project folder, install the browser SDK and a bundler:
npm init -y # only if you don't have a package.json yet npm install @kubesense/kubesense-browser-rum-slim@1.8.0 npm install --save-dev esbuild -
Create
web/kubesense-browser-entry.js. Importing the package registers thewindow.KUBESENSE_SDKobject that the Flutter plugin uses:import '@kubesense/kubesense-browser-rum-slim'; -
Bundle it into
web/:npx esbuild web/kubesense-browser-entry.js --bundle --format=iife --outfile=web/kubesense-browser.js -
In
web/index.html, load the bundle beforeflutter_bootstrap.js(orflutter.js):<script src="kubesense-browser.js"></script> -
If your site has a Content Security Policy, allow
connect-src https://<collector host>.
note: Don't initialize the browser SDK yourself in JavaScript: the Flutter plugin does it. Re-run the bundle command whenever you update the npm package, and commit package-lock.json so builds use the same version.
3. Initialize the SDK
Initialize once, at the start of main(). KubesenseSdk.runApp sets up error reporting, starts the SDK, and then runs your app:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:kubesense_flutter_plugin/kubesense_flutter_plugin.dart';
Future<void> main() async {
final configuration = KubesenseConfiguration(
clientToken: '<CLIENT_TOKEN>',
env: 'production',
kubesenseRumEndpoint: '<your-collector-host>', // host only: no scheme, no path
service: 'my-flutter-app',
nativeCrashReportEnabled: true,
// Logs are not available on Web yet.
loggingConfiguration: kIsWeb ? null : KubesenseLoggingConfiguration(),
rumConfiguration: KubesenseRumConfiguration(
applicationId: '<RUM_APPLICATION_ID>',
),
);
await KubesenseSdk.runApp(
configuration,
TrackingConsent.granted, // see Tracking Consent
() => runApp(const MyApp()),
);
}warning: kubesenseRumEndpoint is required, and it is the only endpoint setting: RUM, Logs, Session Replay and Flags all upload through it. The value is a host, with an optional port — intake.example.com or intake.example.com:8443. Any scheme or path you include is stripped; uploads always go over HTTPS to https://<host>/rum/api/v1/....
| Setting | Meaning |
|---|---|
clientToken | Required. The RUM application's client token. |
env | Required. The environment name you filter by in Kubesense, for example production or staging. |
kubesenseRumEndpoint | Required. The collector host. |
service, version, flavor | Identify the app build. Set version and flavor to match your release so crashes can be matched to the right build. |
nativeCrashReportEnabled | Reports native Android and iOS crashes, not only Dart errors. Default false. |
rumConfiguration | Turns on RUM. Leave it out to disable RUM. See RUM. |
loggingConfiguration | Turns on Logs. Leave it out to disable Logs. |
firstPartyHosts, firstPartyHostsWithTracingHeaders | Your own API hosts, which receive distributed tracing headers. See Distributed Tracing. |
uploadFrequency, batchSize, batchProcessingLevel | Tune how often and how much data is uploaded. |
remoteConfigurationEnabled, remoteConfigurationRefreshPeriod | See Remote Configuration. |
To see SDK messages in the console while integrating, set KubesenseSdk.instance.sdkVerbosity = CoreLoggerLevel.debug; before runApp.
Remote Configuration
Sampling rates, feature switches and privacy masking can be managed centrally in Kubesense and delivered to the SDK, so you can retune or switch off a feature without shipping an app release. Applications write no remote-configuration code.
- On Android and iOS it is enabled by default. The native SDK applies the configuration cached by the previous launch at startup, and fetches a fresh copy in the background for the next launch. Changes therefore take effect at the next launch, not mid-session.
- On Web it is disabled by default.
- Session Replay and Feature Flags follow it too:
features.sessionReplay: falseorfeatures.flags: falseleaves the feature disabled, and their remote settings (for examplesessionReplay.sampleRateand the privacy levels, orflags.trackExposures) override the in-code configuration.
KubesenseConfiguration(
clientToken: '<CLIENT_TOKEN>',
env: 'production',
kubesenseRumEndpoint: '<your-collector-host>',
remoteConfigurationEnabled: false, // stay on in-code settings only
// Android and iOS: how often long-running apps fetch it again.
remoteConfigurationRefreshPeriod: const Duration(hours: 6),
// ...
);Values below one minute are raised to one minute; Duration.zero fetches once after initialization and never again. Missing, malformed or out-of-range values never override your code — see the Android SDK's Remote Configuration section for how the document is applied and which settings it controls.
Features
Real User Monitoring (RUM)
RUM is enabled by passing rumConfiguration. The most common options:
| Option | Default | Meaning |
|---|---|---|
applicationId | — | Required. The RUM application ID. |
sessionSamplingRate | 100 | Percentage of sessions recorded (0–100). |
traceSampleRate | 100 | Percentage of traced requests whose traces are kept. |
detectLongTasks, longTaskThreshold | true, 0.1 s | Report tasks that block the UI thread longer than the threshold. |
trackFrustrations | true | Detect error clicks and other frustration signals. |
trackNonFatalAnrs | platform default | Android: report non-fatal ANRs. |
appHangThreshold | not set | iOS: report app hangs longer than this many seconds. |
trackWatchdogTerminations | false | iOS: report watchdog terminations. |
trackBackgroundEvents | false | Keep recording while the app is in the background. |
trackAnonymousUser | true | Keep a stable anonymous ID across sessions. |
reportFlutterPerformance | false | Report Flutter build and raster times. |
viewEventMapper, actionEventMapper, resourceEventMapper, errorEventMapper, longTaskEventMapper | — | Edit or drop events before they are sent. |
Views (screens)
Add the navigation observer so each route becomes a RUM view:
MaterialApp(
navigatorObservers: [
KubesenseNavigationObserver(kubesenseSdk: KubesenseSdk.instance),
],
home: const HomeScreen(),
);Give your routes names (RouteSettings(name: 'checkout')), or pass a viewInfoExtractor, so views have readable names. With GoRouter, pass the same observer to GoRouter(observers: [...]). If your navigation doesn't use a Navigator, report views yourself:
KubesenseSdk.instance.rum?.startView('checkout', 'Checkout');
// ... when the screen goes away:
KubesenseSdk.instance.rum?.stopView('checkout');Actions
Wrap your app to record taps automatically:
RumUserActionDetector(
rum: KubesenseSdk.instance.rum,
child: MaterialApp(/* ... */),
);Report other actions yourself. RumActionType is one of tap, click, scroll, swipe or custom:
KubesenseSdk.instance.rum?.addAction(RumActionType.tap, 'Place order');
// Continuous actions, such as a scroll:
KubesenseSdk.instance.rum?.startAction(RumActionType.scroll, 'Product list');
KubesenseSdk.instance.rum?.stopAction(RumActionType.scroll, 'Product list');note: If you use Session Replay, skip RumUserActionDetector: SessionReplayCapture already includes it.
Errors
KubesenseSdk.runApp reports uncaught Flutter and Dart errors automatically. Report errors you catch yourself:
try {
await placeOrder();
} catch (error, stackTrace) {
KubesenseSdk.instance.rum?.addError(
error,
RumErrorSource.source,
stackTrace: stackTrace,
);
}
// Or an error that is only a message:
KubesenseSdk.instance.rum?.addErrorInfo('Cart is empty', RumErrorSource.source);Custom timings and attributes
// Time from the start of the current view, for example when the first item is visible:
KubesenseSdk.instance.rum?.addTiming('first_item_visible');
// Attributes on all following events, or on the current view only:
KubesenseSdk.instance.rum?.addAttribute('plan', 'pro');
KubesenseSdk.instance.rum?.addViewAttribute('cart_items', 3);
KubesenseSdk.instance.rum?.removeAttribute('plan');Feature operations
Track multi-step flows, such as a checkout, and whether they succeed:
final rum = KubesenseSdk.instance.rum;
rum?.startFeatureOperation('checkout');
// ...
rum?.succeedFeatureOperation('checkout');
// or:
rum?.failFeatureOperation('checkout', RumFeatureOperationFailureReason.abandoned);Pass an operationKey when several runs of the same operation can overlap.
Session
final sessionId = await KubesenseSdk.instance.rum?.getCurrentSessionId();
KubesenseSdk.instance.rum?.stopSession(); // the next event starts a new sessionLogs
Logs are enabled by passing loggingConfiguration, then sent through loggers you create. Logs are not available on Web yet.
final logger = KubesenseSdk.instance.logs?.createLogger(
KubesenseLoggerConfiguration(
name: 'checkout',
remoteLogThreshold: LogLevel.info, // lower levels stay on the device
),
);
logger?.info('Checkout started');
logger?.warn('Slow payment provider');
logger?.error('Payment failed', attributes: {'reason': 'card_declined'});
logger?.addAttribute('cart_id', 'c-123'); // on every log of this logger
logger?.addTag('team', 'payments');KubesenseLoggerConfiguration also takes service, networkInfoEnabled, and bundleWithRumEnabled / bundleWithTraceEnabled, which link each log to the current RUM session and trace.
Crash Reporting
- Dart and Flutter errors are reported by
KubesenseSdk.runApp. - Native crashes (Android, including the NDK, and iOS) are reported when
nativeCrashReportEnabled: true. They are sent at the next launch. - On iOS, set
trackWatchdogTerminationsandappHangThresholdinKubesenseRumConfigurationto report watchdog terminations and app hangs; on Android,trackNonFatalAnrsreports non-fatal ANRs.
Distributed Tracing
For hosts you own, the SDK adds tracing headers to network requests so a request can be followed from the app into your backend. Tracing works with the network integrations: enableHttpTracking, Dio, GraphQL and gRPC.
KubesenseConfiguration(
// ...
firstPartyHosts: ['api.example.com'], // Kubesense and W3C tracecontext headers
firstPartyHostsWithTracingHeaders: {
'legacy.example.com': {TracingHeaderType.b3},
},
rumConfiguration: KubesenseRumConfiguration(
applicationId: '<RUM_APPLICATION_ID>',
traceSampleRate: 20, // % of requests whose traces are kept
),
);TracingHeaderType | Headers |
|---|---|
kubesense | x-kubesense-* |
tracecontext | W3C traceparent and tracestate (the Kubesense member is kubesense=) |
b3 | B3 single header |
b3multi | B3 multiple headers |
Hosts in firstPartyHosts (and their subdomains) get kubesense and tracecontext headers. List only your own hosts: third-party APIs may reject unknown headers.
Session Replay
Session Replay records what the user saw, as a video-like replay attached to the RUM session. It requires RUM and is not available on Web. Add kubesense_session_replay, then enable it on the configuration before runApp:
import 'package:kubesense_session_replay/kubesense_session_replay.dart';
configuration.enableSessionReplay(
KubesenseSessionReplayConfiguration(
replaySampleRate: 20, // % of RUM sessions that get a replay
textAndInputPrivacyLevel: TextAndInputPrivacyLevel.maskAll,
imagePrivacyLevel: ImagePrivacyLevel.maskAll,
touchPrivacyLevel: TouchPrivacyLevel.hide,
),
);Then wrap your app, above MaterialApp, in a capture widget that keeps a stable key:
class _MyAppState extends State<MyApp> {
final _captureKey = GlobalKey();
@override
Widget build(BuildContext context) {
return SessionReplayCapture(
key: _captureKey,
rum: KubesenseSdk.instance.rum!,
sessionReplay: KubesenseSessionReplay.instance!,
child: MaterialApp(/* ... */),
);
}
}Privacy levels
| Setting | Values (default first) |
|---|---|
textAndInputPrivacyLevel | maskAll, maskAllInputs, maskSensitiveInputs |
imagePrivacyLevel | maskAll, maskNonAssetsOnly, maskNone |
touchPrivacyLevel | hide, show |
Start with everything masked, then relax parts of the screen you've checked with the SessionReplayPrivacy widget, which overrides the levels for its subtree (or hides it entirely with hide: true):
SessionReplayPrivacy(
textAndInputPrivacyLevel: TextAndInputPrivacyLevel.maskSensitiveInputs,
child: const ProductDetails(),
);startRecordingImmediately: false lets you start recording later.
warning: On iOS, release builds need Strip Style set to Non-Global Symbols (see iOS setup), or replays are missing.
Feature Flags
Add kubesense_flags_flutter and register the plugin. It reuses the client token, environment, collector host and application ID from your configuration:
import 'package:kubesense_flags_flutter/kubesense_flags_flutter.dart';
configuration.addPlugin(const KubesenseFlagsPluginConfiguration());After the SDK has started, set who the flags are for, then read flags. The defaultValue is used whenever a flag can't be loaded, so choose one that keeps the app working:
final flags = KubesenseSdk.instance.flags?.sharedClient();
await flags?.initialize(
const FlagsEvaluationContext(
targetingKey: 'user-123',
attributes: {'plan': 'pro'},
),
);
final details = flags?.getBooleanDetails(
key: 'checkout.enabled',
defaultValue: false,
);
final enabled = details?.value ?? false;getStringDetails, getIntegerDetails, getDoubleDetails and getObjectDetails work the same way. Each result also carries the variant, the reason and any error. Call initialize again with a new context when the user changes.
Flags read from a cache filled by one request at initialize, so evaluating a flag never waits on the network. Each evaluation is added to the current RUM view (rumIntegrationEnabled, default true); exposures and evaluation counts are uploaded in the background (trackExposures and trackEvaluations on KubesenseFlagsConfiguration, both default true).
Pure Dart or a background isolate
Use the standalone kubesense_flags package, which needs no Flutter plugin:
import 'package:kubesense_flags/kubesense_flags.dart';
await KubesenseFlags.instance.enable(
configuration: KubesenseFlagsConfiguration(
kubesenseConfig: const KubesenseFlagsConfig(
clientToken: '<CLIENT_TOKEN>',
env: 'production',
kubesenseRumEndpoint: '<your-collector-host>',
applicationId: '<RUM_APPLICATION_ID>',
),
),
);
final flags = KubesenseFlags.instance.sharedClient();
await flags.initialize(const FlagsEvaluationContext(targetingKey: 'user-123'));Call flags.shutdown() when a client is no longer needed; it uploads pending exposures and evaluations first.
note: Feature Flags need flag support on your Kubesense collector. Until it is available there, every flag returns its defaultValue.
WebView Tracking
If a page shown in a WebView already runs the Kubesense browser SDK, its events can join the app's RUM session. List only hosts you trust.
webview_flutter — add kubesense_webview_tracking:
import 'package:kubesense_webview_tracking/kubesense_webview_tracking.dart';
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) // required on Android
..trackKubesenseEvents(KubesenseSdk.instance, ['shop.example.com'])
..loadRequest(Uri.parse('https://shop.example.com'));flutter_inappwebview — add kubesense_inappwebview_tracking (not supported on Web):
import 'package:kubesense_inappwebview_tracking/kubesense_inappwebview_tracking.dart';
InAppWebView(
initialUserScripts: UnmodifiableListView([
KubesenseInAppWebViewUserScript(
kubesense: KubesenseSdk.instance,
allowedHosts: {'shop.example.com'},
),
]),
onWebViewCreated: (controller) {
controller.trackKubesenseEvents(KubesenseSdk.instance);
},
);For an InAppBrowser, call webViewController?.trackKubesenseEvents(KubesenseSdk.instance) in onBrowserCreated and pass the same user script in initialUserScripts.
Integrations
Each integration records network requests as RUM resources and, for first-party hosts, adds tracing headers. Set them up before KubesenseSdk.runApp, on the same configuration.
dart:io and package:http
Add kubesense_tracking_http_client and enable tracking for every HttpClient in the app:
import 'package:kubesense_tracking_http_client/kubesense_tracking_http_client.dart';
final configuration = KubesenseConfiguration(
// ...
firstPartyHosts: ['api.example.com'],
)..enableHttpTracking(
ignoreUrlPatterns: [RegExp(r'graphql\.example\.com')], // tracked elsewhere
);enableHttpTracking replaces HttpOverrides.global; if your app sets its own HttpOverrides, set them before this call. To track only some calls, wrap a client instead:
final client = KubesenseClient(
kubesenseSdk: KubesenseSdk.instance,
innerClient: http.Client(), // optional
);Dio
Add kubesense_dio, then add the interceptor after your other Dio setup:
import 'package:kubesense_dio/kubesense_dio.dart';
final dio = Dio()..addKubesenseInterceptor(KubesenseSdk.instance);GraphQL (gql_link)
Add kubesense_gql_link and put its link before your HttpLink:
import 'package:kubesense_gql_link/kubesense_gql_link.dart';
final link = Link.from([
KubesenseGqlLink(KubesenseSdk.instance, Uri.parse(graphQlUrl)),
HttpLink(graphQlUrl),
]);gRPC
Add kubesense_grpc_interceptor and pass the interceptor to your generated client:
import 'package:kubesense_grpc_interceptor/kubesense_grpc_interceptor.dart';
final client = GreeterClient(
channel,
interceptors: [KubesenseGrpcInterceptor(KubesenseSdk.instance, channel)],
);info: If enableHttpTracking also sees Dio, GraphQL or gRPC traffic, add those URLs to its ignoreUrlPatterns, so each request is counted once.
Flutter inside an existing native app
If a native Android or iOS app already initializes the Kubesense native SDK and embeds a Flutter module, don't call KubesenseSdk.runApp in the module. Attach to the running SDK instead:
await KubesenseSdk.instance.attachToExisting(
KubesenseAttachConfiguration(
detectLongTasks: true,
firstPartyHosts: ['api.example.com'],
),
);Views, actions and errors from Flutter then join the native app's RUM session.
Identifying Users
Set the user after sign-in and clear it on sign-out. Use internal IDs rather than emails unless you're allowed to send them:
KubesenseSdk.instance.setUserInfo(id: user.id);
KubesenseSdk.instance.setAccountInfo(id: user.accountId); // optional: the user's company or account
// On sign-out:
KubesenseSdk.instance.clearUserInfo();
KubesenseSdk.instance.clearAccountInfo();Tracking Consent
The second argument to KubesenseSdk.runApp says whether the user has agreed to be tracked:
| Value | Effect |
|---|---|
TrackingConsent.granted | Data is collected and sent. |
TrackingConsent.pending | Data is collected and kept on the device until you decide. |
TrackingConsent.notGranted | Nothing is sent, and data waiting on the device is deleted. |
If you ask for consent after startup, start with pending, then call:
KubesenseSdk.instance.setTrackingConsent(TrackingConsent.granted); // or notGrantedConsent covers RUM and Logs. Feature Flags send their own requests, so decide separately whether to initialize flags for users who declined.
Verify the Integration
- Run the app, open a few screens, tap a button, make a network request, log a message and trigger a handled error.
- In Kubesense RUM, find the session under your application, environment and service, with its views, action, resource, log and error. It can take a minute to appear.
- If enabled, check for a replay with masked content, a flag evaluation on the view, and a WebView session linked to the app session.
| Problem | What to check |
|---|---|
| No data in Kubesense | kubesenseRumEndpoint is the host only (no https://, no path); the client token and application ID belong to that collector; consent isn't pending or notGranted; the device can reach the collector over HTTPS. Turn on sdkVerbosity = CoreLoggerLevel.debug. |
Android: Could not find ai.kubesense:kubesense-android-… | mavenCentral() is in settings.gradle, and your network can reach Maven Central. |
iOS: Unable to find a specification for KubesenseCore | Your local CocoaPods specs are out of date: run pod repo update, then pod install in ios/. |
| Web: nothing is recorded | kubesense-browser.js is loaded before Flutter's bootstrap script, and your Content Security Policy allows the collector. |
| Session Replay missing from iOS release builds | Strip Style is set to Non-Global Symbols. |
| Network requests counted twice | Add the duplicated URLs to enableHttpTracking(ignoreUrlPatterns: [...]). |
Best Practices
- Initialize first — create the configuration and call
KubesenseSdk.runAppat the top ofmain(), so the first screen and early errors are captured. - Enable RUM before Session Replay and Flags — both correlate with the RUM session.
- Name your routes — views named after
RouteSettings.nameare far easier to read than generated names. - List only your own hosts as first-party — tracing headers go to every host in
firstPartyHostsand its subdomains. - Keep Session Replay masked by default — relax privacy only on screens you've checked, with
SessionReplayPrivacy. - Provide safe flag defaults — the
defaultValueis what users get when flags can't be loaded. - Start with
TrackingConsent.pendingwhen you need consent first; data is uploaded only aftergranted. - Set
versionto your release version so crashes and replays can be matched to the right build.