Kubesense

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.

FieldValue
Flutter3.27 or newer (Dart 3.6 or newer)
AndroidminSdk 23
iOS15.0 or newer
WebSupported, except Logs and Session Replay, which are not available on Web yet.
Distributionpub.dev, packages kubesense_*
Native SDKsAndroid 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:

ValueWhere to find it
applicationIdSettings → RUM Application Management → your application → View Details
clientTokenSettings → RUM Application Management → your application → View Details
kubesenseRumEndpointYour 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:

FeaturePackage
RUM, Logs, crash and error reporting, distributed tracing, consentkubesense_flutter_plugin (required)
HTTP requests made with dart:io or package:httpkubesense_tracking_http_client
Requests made with Diokubesense_dio
GraphQL requests made with gql_linkkubesense_gql_link
gRPC callskubesense_grpc_interceptor
Session Replaykubesense_session_replay
Feature Flagskubesense_flags_flutter
Feature Flags in pure Dart or a background isolatekubesense_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.0

Then run flutter pub get.

2. Set up each platform

Android

  1. In android/app/build.gradle (or build.gradle.kts), set minSdk to at least 23.

  2. Make sure android/app/src/main/AndroidManifest.xml allows network access:

    <uses-permission android:name="android.permission.INTERNET" />
  3. The native Kubesense Android SDK is downloaded from Maven Central. Make sure android/settings.gradle (or settings.gradle.kts) lists google() and mavenCentral():

    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.

  1. Set the iOS deployment target to 15.0 or higher. With CocoaPods, set platform :ios, '15.0' (or higher) in ios/Podfile and keep use_frameworks!.
  2. 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.

  1. 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
  2. Create web/kubesense-browser-entry.js. Importing the package registers the window.KUBESENSE_SDK object that the Flutter plugin uses:

    import '@kubesense/kubesense-browser-rum-slim';
  3. Bundle it into web/:

    npx esbuild web/kubesense-browser-entry.js --bundle --format=iife --outfile=web/kubesense-browser.js
  4. In web/index.html, load the bundle before flutter_bootstrap.js (or flutter.js):

    <script src="kubesense-browser.js"></script>
  5. 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/....

SettingMeaning
clientTokenRequired. The RUM application's client token.
envRequired. The environment name you filter by in Kubesense, for example production or staging.
kubesenseRumEndpointRequired. The collector host.
service, version, flavorIdentify the app build. Set version and flavor to match your release so crashes can be matched to the right build.
nativeCrashReportEnabledReports native Android and iOS crashes, not only Dart errors. Default false.
rumConfigurationTurns on RUM. Leave it out to disable RUM. See RUM.
loggingConfigurationTurns on Logs. Leave it out to disable Logs.
firstPartyHosts, firstPartyHostsWithTracingHeadersYour own API hosts, which receive distributed tracing headers. See Distributed Tracing.
uploadFrequency, batchSize, batchProcessingLevelTune how often and how much data is uploaded.
remoteConfigurationEnabled, remoteConfigurationRefreshPeriodSee 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: false or features.flags: false leaves the feature disabled, and their remote settings (for example sessionReplay.sampleRate and the privacy levels, or flags.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:

OptionDefaultMeaning
applicationId—Required. The RUM application ID.
sessionSamplingRate100Percentage of sessions recorded (0–100).
traceSampleRate100Percentage of traced requests whose traces are kept.
detectLongTasks, longTaskThresholdtrue, 0.1 sReport tasks that block the UI thread longer than the threshold.
trackFrustrationstrueDetect error clicks and other frustration signals.
trackNonFatalAnrsplatform defaultAndroid: report non-fatal ANRs.
appHangThresholdnot setiOS: report app hangs longer than this many seconds.
trackWatchdogTerminationsfalseiOS: report watchdog terminations.
trackBackgroundEventsfalseKeep recording while the app is in the background.
trackAnonymousUsertrueKeep a stable anonymous ID across sessions.
reportFlutterPerformancefalseReport 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 session

Logs

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 trackWatchdogTerminations and appHangThreshold in KubesenseRumConfiguration to report watchdog terminations and app hangs; on Android, trackNonFatalAnrs reports 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
  ),
);
TracingHeaderTypeHeaders
kubesensex-kubesense-*
tracecontextW3C traceparent and tracestate (the Kubesense member is kubesense=)
b3B3 single header
b3multiB3 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

SettingValues (default first)
textAndInputPrivacyLevelmaskAll, maskAllInputs, maskSensitiveInputs
imagePrivacyLevelmaskAll, maskNonAssetsOnly, maskNone
touchPrivacyLevelhide, 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);

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();

The second argument to KubesenseSdk.runApp says whether the user has agreed to be tracked:

ValueEffect
TrackingConsent.grantedData is collected and sent.
TrackingConsent.pendingData is collected and kept on the device until you decide.
TrackingConsent.notGrantedNothing 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 notGranted

Consent covers RUM and Logs. Feature Flags send their own requests, so decide separately whether to initialize flags for users who declined.


Verify the Integration

  1. Run the app, open a few screens, tap a button, make a network request, log a message and trigger a handled error.
  2. 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.
  3. If enabled, check for a replay with masked content, a flag evaluation on the view, and a WebView session linked to the app session.
ProblemWhat to check
No data in KubesensekubesenseRumEndpoint 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 KubesenseCoreYour local CocoaPods specs are out of date: run pod repo update, then pod install in ios/.
Web: nothing is recordedkubesense-browser.js is loaded before Flutter's bootstrap script, and your Content Security Policy allows the collector.
Session Replay missing from iOS release buildsStrip Style is set to Non-Global Symbols.
Network requests counted twiceAdd the duplicated URLs to enableHttpTracking(ignoreUrlPatterns: [...]).

Best Practices

  • Initialize first — create the configuration and call KubesenseSdk.runApp at the top of main(), 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.name are far easier to read than generated names.
  • List only your own hosts as first-party — tracing headers go to every host in firstPartyHosts and its subdomains.
  • Keep Session Replay masked by default — relax privacy only on screens you've checked, with SessionReplayPrivacy.
  • Provide safe flag defaults — the defaultValue is what users get when flags can't be loaded.
  • Start with TrackingConsent.pending when you need consent first; data is uploaded only after granted.
  • Set version to your release version so crashes and replays can be matched to the right build.