Flutter SDK observability reference

This LaunchDarkly observability plugin is available for early access

This LaunchDarkly observability plugin is currently available in Early Access, and APIs are subject to change until a 1.x version is released.

This topic documents how to get started with the LaunchDarkly observability plugin for the Flutter SDK.

The launchdarkly_flutter_observability package provides error monitoring, logging, tracing, and session replay through a single public facade named LDObserve.

SDK quick links

LaunchDarkly SDKs are open source. In addition to this reference guide, we provide source, API reference documentation, and a sample application:

ResourceLocation
GitHub repository@launchdarkly/flutter observability
Sample applicationFlutter example app

Prerequisites and dependencies

This reference guide assumes you are familiar with the LaunchDarkly Flutter SDK.

The observability plugin requires the LaunchDarkly Flutter SDK version 4.18.0 or later, and Flutter 3.27 or later.

The Flutter observability plugin is compatible with iOS, Android, and web platforms. Session replay is supported only on iOS and Android. Web session replay is not yet available.

Get started

Follow these steps to get started:

Install the package

Add both the LaunchDarkly Flutter SDK and the observability package to your pubspec.yaml:

pubspec.yaml
dependencies:
launchdarkly_flutter_client_sdk: ^4.18.0
launchdarkly_flutter_observability: ^0.18.0

Then run:

Install dependencies
flutter pub get

For iOS, install the native pod dependencies from your app’s ios/ directory:

iOS pod install
cd ios && pod install && cd ..

No extra setup is required for Android. Gradle resolves the plugin automatically.

After you install the dependencies, import the packages into your code:

Import
import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart';
import 'package:launchdarkly_flutter_observability/launchdarkly_flutter_observability.dart';

Initialize observability

To initialize, you need your LaunchDarkly environment’s mobile key. This authorizes your application to connect to a particular environment within LaunchDarkly. To learn more, read Initialize the client in the Flutter SDK reference guide.

Flutter observability mobile and web builds use different credential types

The Flutter observability SDK uses a mobile key for iOS and Android builds. For web builds, use a client-side ID instead. Keys are specific to each project and environment. They are available on the SDK keys page under Settings. To learn more about key types, read Keys.

Mobile keys are not secret and you can expose them in your client-side code without risk. However, never embed a server-side SDK key into a client-side application.

There are two initialization variants. Use LDObserve.init if you are using the LaunchDarkly client, or LDObserve.initStandalone to initialize without a client.

Initialize with a LaunchDarkly client

Pass your LDClient to LDObserve.init. This registers the observability plugin on the client so feature flag evaluations are correlated with your telemetry:

Initialize with LDClient
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart';
import 'package:launchdarkly_flutter_observability/launchdarkly_flutter_observability.dart';
void main() {
runZonedGuarded(
() {
WidgetsFlutterBinding.ensureInitialized();
final client = LDClient(
LDConfig(
'example-mobile-key',
AutoEnvAttributes.enabled,
),
LDContextBuilder().kind('user', 'example-context-key').build(),
);
client.start();
LDObserve.init(
client,
observability: const ObservabilityOptions(
serviceName: 'my-flutter-app',
),
);
// Report errors caught by the Flutter framework
FlutterError.onError = (FlutterErrorDetails details) {
LDObserve.recordException(details.exception, stackTrace: details.stack);
};
runApp(const MyApp());
},
(err, stack) {
// Report uncaught errors from the zone
LDObserve.recordException(err, stackTrace: stack);
},
);
}

Initialize standalone

To initialize without creating a LaunchDarkly client, pass your mobile key directly to LDObserve.initStandalone:

Initialize standalone
await LDObserve.initStandalone(
'example-mobile-key',
observability: const ObservabilityOptions(
serviceName: 'my-flutter-app',
),
);

Configure plugin options

Pass an ObservabilityOptions object to LDObserve.init or LDObserve.initStandalone to configure observability behavior:

ObservabilityOptions
LDObserve.init(
client,
observability: ObservabilityOptions(
serviceName: 'my-flutter-app',
// Recommended: set to the latest deployed git SHA or semantic version
serviceVersion: 'example-sha',
instrumentation: InstrumentationOptions(
networkRequests: true,
launchTimes: true,
debugPrint: DebugPrintSetting.releaseOnly(),
),
),
);

These ObservabilityOptions configuration options are available on all platforms:

  • isEnabled: Enables or disables observability. Defaults to true.
  • serviceName: The service name for telemetry. Defaults to "observability-flutter".
  • serviceVersion: The service version, commonly a Git SHA or semantic version string. Defaults to "0.1.0".
  • instrumentation: An InstrumentationOptions object that controls automatic instrumentation. To learn more, read Instrumentation options.
  • otlpEndpoint: The OTLP endpoint for reporting OpenTelemetry data. Defaults to https://otel.observability.app.launchdarkly.com:4318. You do not need to change this for most configurations.
  • backendUrl: The LaunchDarkly back-end URL. Defaults to https://pub.observability.app.launchdarkly.com. You do not need to change this for most configurations.
  • contextFriendlyName: A display name to identify the user’s session in the observability UI.
  • attributes: A map of additional resource attributes to include in telemetry.
  • analytics: An AnalyticsOptions object that controls telemetry for product analytics events. Use AnalyticsOptions.enabled to enable all types or AnalyticsOptions.disabled to disable all:
    • taps: Emits a click span for each user tap. Defaults to true. Setting this to false also stops tap detection, rather than detecting taps without publishing them. To learn more, read Record taps.
    • views: Emits a screen_view span for each screen view. Defaults to true. To learn more, read Record screen views.
    • trackEvents: Emits a track span when a custom event is tracked with track(). Defaults to true.
    • appLifecycle (Android and iOS): Emits app_foreground and app_background spans as the app moves between states. Defaults to true.
    • appLaunch (Android and iOS): Emits an app_launch span once per process launch, including an app.start span event with the cold or warm startup dimension. Defaults to true.
    • customClickTargetResolver: A function that names your own widget types as tap targets. To learn more, read Name your own widget types. To learn more, read Product analytics events.

The following ObservabilityOptions configuration options are available only on Android and iOS:

  • customHeaders: Extra HTTP headers added to OTLP exports, for example for proxies or authentication. Defaults to {}.
  • sessionBackgroundTimeout: How long the app can stay in the background before the session ends. Defaults to 15 minutes.
  • logsApiLevel: The minimum severity of logs forwarded to the logs pipeline. Use ObservabilityLogLevel.none to disable logs. Defaults to ObservabilityLogLevel.info.
  • traces: A TracesOptions object that controls automatic trace generation. Toggles includeErrors and includeSpans. Both default to true.
  • metricsEnabled: Whether metrics are exported. Defaults to true.

For more information on plugin options, read Configuration for client-side observability.

Instrumentation options

The InstrumentationOptions class controls which automatic instrumentation features are active. Pass it to the instrumentation parameter of ObservabilityOptions.

InstrumentationOptions
ObservabilityOptions(
instrumentation: InstrumentationOptions(
networkRequests: true,
launchTimes: true,
debugPrint: DebugPrintSetting.always(),
),
)

These instrumentation options are available on all platforms:

  • networkRequests: When true, automatically instruments HTTP network requests. Defaults to true.
  • launchTimes: When true, measures and reports application launch time. Defaults to true.
  • debugPrint: Controls whether debugPrint calls are automatically captured as log events:
    • DebugPrintSetting.releaseOnly() (the default) captures debugPrint calls only in release builds.
    • DebugPrintSetting.always() captures debugPrint calls in all build configurations. When enabled, debugPrint output does not appear in the Flutter console.
    • DebugPrintSetting.disabled() does not instrument debugPrint.

The following InstrumentationOptions configuration option is available only on Android and iOS:

  • crashReporting: When true, reports uncaught exceptions as errors. Defaults to true.

Intercept print statements

To capture the output from print statements, pass LDObserve.zoneSpecification() to runZonedGuarded:

Intercept print
void main() {
runZonedGuarded(
() {
// Initialize and run your app
},
(err, stack) {
LDObserve.recordException(err, stackTrace: stack);
},
zoneSpecification: LDObserve.zoneSpecification(),
);
}

Map contexts to friendly names

Use contextFriendlyName to set a human-readable display name for the user’s session when displayed in the observability User Interface (UI):

contextFriendlyName
ObservabilityOptions(
contextFriendlyName: 'Bob Smith',
)

Configure product analytics event collection

The observability SDK for Flutter can record the following product analytics events as OpenTelemetry spans:

  • Track events (manual, all platforms): A track span recorded when your code calls LDObserve.track(). The trackEvents flag in AnalyticsOptions controls whether the span is emitted. To learn more, read Recording product analytics events.
  • Taps (automatic, all platforms): A click span for each user tap, with the widget type, identifier, visible label, widget ancestry path, and coordinates. Enabled by default. Tap capture requires the SessionReplayCapture widget. To learn more, read Record taps.
  • Screen views (automatic or manual, all platforms): A screen_view span when your app shows a screen, with the screen name and optional details such as the screen class, screen identifier, and category. Enabled by default. Screen views are reported from your Dart code, either by the LDNavigatorObserver or by a call to LDObserve.trackScreenView. To learn more, read Record screen views.
  • App lifecycle (automatic, Android and iOS): An app_foreground or app_background span as the app moves between the foreground and background states. Enabled by default.
  • App launches (automatic, Android and iOS): An app_launch span once per process launch, with the launch type and version information, plus an app.start span event that records the cold or warm startup dimension. Enabled by default. To learn more, read App launch events.

All product analytics span events also include information about the LaunchDarkly context that generated the event.

Use the generated span events to create custom product analytics charts, such as time series and funnels. To learn more, read Product analytics events.

To enable all compatible product analytics events, set analytics to AnalyticsOptions.enabled in ObservabilityOptions:

Enable all product analytics events
ObservabilityOptions(
analytics: AnalyticsOptions.enabled,
)

To enable or disable individual event types, use AnalyticsOptions instead:

Enable individual event types
ObservabilityOptions(
analytics: AnalyticsOptions(
trackEvents: true, // all platforms
taps: true, // all platforms
views: true, // all platforms
appLifecycle: true, // Android and iOS
appLaunch: true, // Android and iOS
),
)
Default values for product analytics

Track event, tap, screen view, app lifecycle, and app launch collection are all enabled by default. Use AnalyticsOptions.disabled to disable all product analytics spans at once.

The taps and views options behave differently on mobile than they do on web:

  • taps: Gates the Dart-side tap detection as well as the click span. Setting it to false stops the plugin from resolving tapped widgets at all. On Android and iOS, the session replay Click timeline event is recorded regardless of this setting.
  • views: Gates the screen_view span only. On Android and iOS, the session replay Navigate timeline event is recorded regardless of this setting.

AnalyticsOptions also accepts a customClickTargetResolver, which names your own widget types as tap targets. To learn more, read Name your own widget types.

Configure session replay

Session replay is in Early Access

Session replay for Flutter is available in Early Access. APIs are subject to change until a 1.x version is released.

Session replay captures screen recordings of user actions to help you understand how users interact with your application. It is included in the launchdarkly_flutter_observability package and uses native iOS and Android libraries to capture and upload recordings.

Session replay for Flutter is supported only on iOS and Android. Web session replay is not yet available.

Initialize session replay

To enable session replay, pass a SessionReplayOptions object to the replay parameter of LDObserve.init or LDObserve.initStandalone, and wrap your app in SessionReplayCapture:

Initialize with session replay
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart';
import 'package:launchdarkly_flutter_observability/launchdarkly_flutter_observability.dart';
void main() {
runZonedGuarded(
() {
WidgetsFlutterBinding.ensureInitialized();
final client = LDClient(
LDConfig(
'example-mobile-key',
AutoEnvAttributes.enabled,
),
LDContextBuilder().kind('user', 'example-context-key').build(),
);
client.start();
LDObserve.init(
client,
observability: const ObservabilityOptions(
serviceName: 'my-flutter-app',
),
replay: const SessionReplayOptions(
isEnabled: true,
privacy: PrivacyOptions(maskTextInputs: true),
),
);
FlutterError.onError = (FlutterErrorDetails details) {
LDObserve.recordException(details.exception, stackTrace: details.stack);
};
// Wrap your app in SessionReplayCapture to enable screen recording
runApp(const SessionReplayCapture(child: MyApp()));
},
(err, stack) {
LDObserve.recordException(err, stackTrace: stack);
},
);
}

SessionReplayCapture widget

Wrap your top-level widget in SessionReplayCapture to allow the native session replay library to capture screen content. The widget also hosts tap capture on every platform, including web, where session replay is not yet available. Wrapping your app with it is safe on every platform:

SessionReplayCapture
runApp(const SessionReplayCapture(child: MyApp()));

Wrap your app as high in the widget tree as you can, ideally around MaterialApp. Dialogs, bottom sheets, and other routes are children of the app’s Navigator, so a wrapper further down the tree excludes taps on anything your app pushes above it. To learn more, read Record taps.

Session replay configuration options

Pass a SessionReplayOptions object to LDObserve.init or LDObserve.initStandalone to control session replay behavior:

SessionReplayOptions
const SessionReplayOptions(
isEnabled: true,
privacy: PrivacyOptions(
maskTextInputs: true,
maskWebViews: false,
maskLabels: false,
maskImages: false,
),
)

If you omit the replay parameter, session replay does not start. The isEnabled default applies only to the SessionReplayOptions object you pass in.

These SessionReplayOptions configuration options are available on all platforms:

  • isEnabled: Controls whether session recording starts. Defaults to true.
  • privacy: A PrivacyOptions object that controls which UI elements are masked. To learn more, read Privacy options.
  • serviceName: The service name reported for session replay telemetry. Defaults to "sessionreplay-flutter".

The following SessionReplayOptions configuration options are available only on Android and iOS:

  • sampleRate: Probability from 0.0 to 1.0 that session replay starts when enabled. Defaults to 1.0.
  • frameRate: Target capture rate in frames per second. Defaults to 1.0.
  • scale: The resolution multiplier for captured frames, where 1.0 is 1x (160 DPI) and 2.0 is 2x. Higher values capture more detail but produce larger frames. A null value is treated as 1.0. Defaults to 1.0.
  • imageQuality: The JPEG encoding quality of exported frames, from 0.0 (lowest quality, smallest payload) to 1.0 (highest quality, largest payload). Values outside that range are clamped. Defaults to 0.3.

Privacy options

Use PrivacyOptions to control which elements are masked in session replay recordings. By default, text inputs are masked to protect user data.

PrivacyOptions
const PrivacyOptions(
maskTextInputs: true, // default — masks all text inputs
maskWebViews: false, // when true, masks WebView content
maskLabels: false, // when true, masks all text labels
maskImages: false, // when true, masks all images
minimumAlpha: 0.02, // views below this alpha are not captured (iOS only)
maskClickText: false, // when true, drops the label from click events
)

The available privacy options are:

  • maskTextInputs: Masks all text input fields. Defaults to true.
  • maskWebViews: Masks the contents of web views. When enabled, web views appear as blank rectangles in recordings. Defaults to false.
  • maskLabels: Masks all text labels. Defaults to false.
  • maskImages: Masks all images. Defaults to false.
  • minimumAlpha: (iOS only) Minimum alpha value for a view to be captured. Views with a lower alpha are not recorded. Defaults to 0.02.
  • maskClickText: Drops the visible label from click events, so taps report the widget type and identifier but no text. Defaults to false. This option is independent of maskLabels, which controls whether the plugin paints over text in captured frames. To learn more, read Record taps.

LaunchDarkly applies masks to every captured frame. Masks follow their widgets through scrolling, transforms, and animations. If a mask cannot be placed reliably in a frame, LaunchDarkly drops that frame rather than risk exposing unmasked content.

Per-widget masking

In addition to screen-wide PrivacyOptions, you can redact individual widgets using LDMask, LDIgnore, and LDUnmask.

Use LDMask to redact a widget’s subtree in all captured frames:

LDMask
LDMask(
child: Text(creditCardNumber),
)

Use LDIgnore to exclude a subtree from session replay entirely. In Flutter, LDIgnore behaves like LDMask and paints over the region in every captured frame, so its contents never appear in a recording:

LDIgnore
LDIgnore(
child: VideoPlayer(controller),
)

Use LDUnmask to exempt a subtree from global masking rules such as maskTextInputs. For example, to reveal one non-sensitive field on a page where every input is masked:

LDUnmask
// maskTextInputs masks every field; reveal just this one
LDUnmask(
child: TextField(controller: searchController),
)

LDUnmask only overrides global masking. It does not override an explicit LDMask or LDIgnore. An LDUnmask nested inside either one stays masked, because an explicit per-widget mask always takes precedence.

LDMask, LDIgnore, and LDUnmask are active on iOS and Android. On web they render their child unchanged.

Mask by widget type or key

When wrapping widgets is not convenient, name them once in PrivacyOptions by their Type or Key. LaunchDarkly resolves these rules on the Flutter side and does not send them to the native SDKs:

Mask by widget type or key
PrivacyOptions(
maskWidgetTypes: {CreditCardField},
maskWidgetKeys: {const ValueKey('ssn-field')},
unmaskWidgetTypes: {SearchBox},
ignoreWidgetTypes: {LiveCameraPreview},
)

The available widget matching options are:

  • maskWidgetTypes: A set of widget Types to mask wherever they appear. Defaults to an empty set.
  • maskWidgetKeys: A set of widget Keys to mask wherever they appear. Defaults to an empty set.
  • unmaskWidgetTypes: A set of widget Types to reveal from global masking. Defaults to an empty set.
  • unmaskWidgetKeys: A set of widget Keys to reveal from global masking. Defaults to an empty set.
  • ignoreWidgetTypes: A set of widget Types to ignore. Defaults to an empty set.
  • ignoreWidgetKeys: A set of widget Keys to ignore. Defaults to an empty set.

These rules follow the same precedence as the wrapper widgets. A mask or ignore match takes precedence over an unmask match.

To learn more about session replay configuration, read Configuration for session replay.

Record screen views

Flutter renders your entire app into a single native view, so the native observability SDKs never detect your Flutter route changes. To record screen views, report route changes from your Dart code.

Each screen view emits a screen_view span. On Android and iOS, it also adds a Navigate event to the session replay timeline.

Record screen views automatically

Add an LDNavigatorObserver to the navigatorObservers list on your top-level navigator. Every route change then becomes a screen view:

LDNavigatorObserver
MaterialApp(
navigatorObservers: [LDNavigatorObserver()],
// ...
);

The observer reads the screen name from route.settings.name and skips routes that do not have one. An app that pushes a bare MaterialPageRoute records nothing and reports no error. To avoid this, name your routes where you push them:

Name a route
Navigator.of(context).push(
MaterialPageRoute<void>(
settings: const RouteSettings(name: '/checkout'),
builder: (_) => const CheckoutPage(),
),
);

Skipping unnamed routes is deliberate. It keeps the dialogs and bottom sheets that showDialog and showModalBottomSheet push without settings from appearing as screens.

To record those routes anyway, or to derive screen names another way, pass a screenNameExtractor. Returning null from the extractor skips the route, so the extractor also works as a filter for routes you do not want to record:

screenNameExtractor
MaterialApp(
navigatorObservers: [
LDNavigatorObserver(
screenNameExtractor: (route) =>
route.settings.name ?? route.settings.arguments?.toString(),
category: 'navigation',
),
],
);

Report route patterns instead of URLs

Routers that navigate by URL, including go_router, GetX, Beamer, and auto_route, put the concrete path in route.settings.name. Without route patterns, '/orders/42' becomes its own screen, one screen fragments into one screen per order, and an order identifier ends up in a name that appears in the LaunchDarkly UI.

Route patterns require launchdarkly_flutter_observability version 0.19.0 or later.

In version 0.19.0 and later, the observer also drops the query string and fragment from every screen name, so '/reset?token=abc123' and '/search?q=running+shoes' are reported as /reset and /search. A query string holds values rather than structure, and a screen name identifies a screen. Because names that are not paths keep their punctuation, a confirmation route named 'Delete this?' is reported as written.

Path parameters are the one part of a route name that the plugin cannot resolve on its own. Only your app knows which segments are identifiers. List the patterns your app registers, and LDRoutePatterns.extractor collapses concrete paths back onto them:

LDRoutePatterns.extractor
LDNavigatorObserver(
screenNameExtractor: LDRoutePatterns.extractor(const [
'/orders',
'/orders/new', // A literal listed first wins over '/orders/:id'.
'/orders/:id',
'/orders/:id/receipt',
]),
);

That flow then reports /orders, /orders/:id, and /orders/:id/receipt, no matter which order identifiers are involved.

Patterns use the same syntax those routers use:

  • A :name segment matches exactly one path segment.
  • A trailing * matches the rest of the path.

The extractor compares patterns in the order you list them, and the first match wins. Place a literal before any pattern that also matches it, such as '/orders/new' before '/orders/:id'.

Because a route that matches no pattern is reported by its path, any screen you forget to list still appears. To treat your pattern list as an allow list and record only those patterns you name, pass skipUnmatched: true.

Each LDNavigatorObserver instance observes a single navigator, and Flutter asserts if you share one instance between navigators. The following setups need additional configuration:

  • Nested navigators: A tab shell, or a Navigator inside a page, reports only its own routes. Create a separate observer instance for each navigator.
  • MaterialApp.router: Routers such as go_router, auto_route, and Beamer do not accept navigatorObservers. Pass the observer to the router’s own observer list instead, such as GoRouter(observers: [LDNavigatorObserver()]).
  • GetMaterialApp: GetX merges the observers you pass with its own, so navigatorObservers: [LDNavigatorObserver()] works the same way it does on MaterialApp. GetMaterialApp.router is the exception. It accepts navigatorObservers and then builds its delegate without them, so pass them as routerDelegate: GetDelegate(navigatorObservers: [LDNavigatorObserver()]) instead.
  • Navigation that leaves the route stack unchanged: Switching tabs in an IndexedStack, or paging a PageView, is invisible to any observer. Record these screen views manually.

Record screen views manually

To record a screen view yourself, or to add detail that an observer cannot supply, call LDObserve.trackScreenView:

trackScreenView
LDObserve.trackScreenView(
'Checkout',
screenClass: 'CheckoutPage',
category: 'commerce',
properties: <String, Object?>{'cart_size': 3},
);

Only the screen name is required. The plugin records screenClass, screenId, and category as the event.screen_class, event.screen_id, and event.category span attributes, and attaches any properties you pass as additional attributes.

Record taps

The plugin captures taps automatically and records each one as a click span. Every tap that resolves to a recognized widget describes that widget by type, identifier, visible label, ancestry path, and coordinates. On Android and iOS, the same tap also adds a Click event to the session replay timeline. This enables you to jump to the moment a user pressed a widget.

Tap capture requires you to wrap your widget tree in SessionReplayCapture, which hosts the tap detector on every platform, including web. To learn more, read SessionReplayCapture widget.

The plugin resolves the tapped widget in Dart, because Flutter renders its entire interface into one native view. A native hit test names that view, FlutterSurfaceView, for every tap in your app regardless of which widget the user pressed.

Tap event attributes

Each click span carries the following attributes:

AttributeDescription
event.tagThe widget type, such as ElevatedButton.
event.idA stable identifier for the widget. The plugin uses the first value it finds, in this order: the id of an enclosing LDClick, an id from your customClickTargetResolver, a Semantics.identifier, or a ValueKey. This attribute is optional. A widget with none of these is still reported, grouped by type and ancestry path.
event.textThe visible label: a button’s own text, or otherwise a semantic label, icon label, or tooltip. The plugin does not harvest a container’s inner text, so a tapped row reports ListTile rather than whichever word sat under the user’s finger. A Radio with no label falls back to its value.
event.xpathThe widget ancestry, such as Scaffold/Column/ProductRow/IconButton#cart.add. The plugin leaves out framework plumbing, including theme and media query providers, builders, focus and semantics wrappers, and single-child layout and painting boxes, and keeps only the innermost ten segments.
event.x, event.yThe tap coordinates. Automatic capture reports the units native taps use on that platform, which are physical pixels on Android and logical pixels on iOS, so a Flutter tap lands on the session replay timeline next to a native one.

The plugin recognizes the following widgets out of the box:

  • Material and Cupertino buttons, including IconButton and FloatingActionButton
  • Selection controls, including Switch, Checkbox, Radio, and Slider
  • Chips, tabs, and menus, including PopupMenuButton and DropdownButton
  • Navigation bars
  • ListTile, InkWell, and GestureDetector

A tap on unrecognized empty space, or on a disabled control, records nothing.

event.xpath segments for widget types the plugin does not recognize come from the Dart runtime type, which release builds compiled with --obfuscate mangle. The event.tag and event.id attributes stay readable in obfuscated builds, so you can group your product analytics charts on those attributes.

Name a widget with LDClick

To give a specific widget a stable identifier, wrap it in LDClick. Because the widget renders its child unchanged and emits nothing itself, wrapping a button cannot double count a tap:

LDClick
LDClick(
id: 'checkout.pay',
properties: <String, Object?>{'cart_size': 3},
child: ElevatedButton(onPressed: _pay, child: const Text('Pay')),
);

A tap on any descendant resolves to the id of the nearest enclosing LDClick, so you can wrap a composite control to name the entire control. Use LDClick when a widget has no Key or Semantics.identifier, or when the automatic name is too generic to group on.

Name your own widget types

A design system button often looks like an anonymous composition of Material widgets. To name every instance of one of your own widget types at once, register a customClickTargetResolver in AnalyticsOptions instead of tagging each call site:

customClickTargetResolver
ObservabilityOptions(
analytics: AnalyticsOptions(
customClickTargetResolver: (widget) => switch (widget) {
PrimaryButton(:final label) =>
LDClickTargetInfo(tag: 'PrimaryButton', text: label),
_ => null,
},
),
);

Return an LDClickTargetInfo for the types you want to name, and null for everything else so the built-in rules apply. LDClickTargetInfo accepts the following parameters:

  • tag: The widget type name reported as event.tag. Required.
  • id: A stable identifier for the instance, reported as event.id.
  • text: The visible label, reported as event.text. If you omit it, the plugin falls back to its usual text extraction.
  • preferInnerTarget: When true, a more specific target nested inside this widget is reported instead. Set this for containers that only make a region tappable, such as a card or a row, to ensure that the plugin reports a button inside the containers. Defaults to false.

Use a string literal for tag rather than runtimeType.toString(). Release builds compiled with --obfuscate mangle runtime type names, which would group the same widget differently in every build.

The resolver runs for every widget above the tap, so keep it inexpensive and free of side effects. The plugin catches and logs an exception from your resolver and then continues with the built-in rules.

Record taps manually

For an interaction that automatic capture cannot observe, such as a shake, a hardware button, or a custom gesture recognizer, call LDObserve.trackClick yourself. Pass x and y in logical pixels, the same units a Flutter Offset uses:

trackClick
LDObserve.trackClick(
id: 'onboarding.shake_to_skip',
tag: 'ShakeGesture',
x: 24,
y: 80,
properties: <String, Object?>{'step': 2},
);

Avoid calling LDObserve.trackClick from an onPressed handler that automatic capture already observes, because that counts the tap twice. To give an automatically captured widget a stable name, wrap it in LDClick instead.

Tap capture limitations

Tap capture has the following limitations:

  • Only taps count as clicks. The plugin does not report a press that moves further than kTouchSlop, such as a scroll or a drag, a press that outlasts the long press timeout, or any multi-touch gesture. The native SDKs apply the same rule, which keeps Flutter and native clicks comparable.
  • Embedded platform views resolve to their host widget. A tap in a WebView or a native map reports the Flutter widget hosting it, such as WebViewWidget. The plugin does not describe what the user pressed inside the embedded view.
  • Pointer-blocking overlays are honored. An IgnorePointer is transparent to the plugin, because the tap passed through it. Because an AbsorbPointer, such as a loading overlay laid over a Stack, consumes the tap, the plugin reports neither its children nor the controls painted behind it. This matches what Flutter delivered to your app.
  • Tap labels follow your masking rules. The plugin never reports text inside an LDMask or LDIgnore subtree, and never reads the contents of an editable field. Setting maskClickText in PrivacyOptions turns off tap labels entirely while still recording the taps. To learn more, read Privacy options.

Manual instrumentation

After initializing the observability plugin, use LDObserve to manually instrument your Flutter application with custom logs, traces, and product analytics events.

Recording custom logs

Use LDObserve.recordLog to emit a structured log. severity is a plain string. Common levels are trace, debug, info, warn, error, and fatal. properties is a plain Dart map with no LaunchDarkly or OpenTelemetry types required:

Record logs
// Record a basic log message
LDObserve.recordLog(
'User login successful',
severity: 'info',
);
// Record a log with custom properties
LDObserve.recordLog(
'Authentication completed',
severity: 'info',
properties: <String, Object?>{
'user_id': '12345',
'action': 'login',
},
);

Recording custom traces

Use LDObserve.startSpan to create a span for tracing an operation. Spans nest automatically under the currently active span. Always end spans when the operation completes:

Record traces
// Start a span with custom properties
final span = LDObserve.startSpan(
'database_query',
properties: <String, Object?>{
'table': 'users',
'operation': 'select',
},
);
// Perform your operation
await performDatabaseQuery();
// Always end the span
span.end();

To set the span’s kind, pass the kind parameter to startSpan. SpanKind supports internal, which is the default, along with client, server, producer, and consumer.

Each span supports these methods:

  • setAttribute(name, value): Sets a single attribute on the span.
  • setAttributes(map): Sets multiple attributes on the span.
  • addEvent(name, {attributes}): Records a named event on the span.
  • setStatus(SpanStatusCode): Sets the span status to ok, error, or unset.
  • recordException(exception, {stackTrace, attributes}): Records an exception on the span.
  • end(): Ends the span.

To record spans independently instead of nesting them, end each span before you start the next:

Sequential spans
final span1 = LDObserve.startSpan('SequentialOperation1');
span1.setAttribute('sequence', '1');
span1.end();
final span2 = LDObserve.startSpan('SequentialOperation2');
span2.setAttribute('sequence', '2');
span2.end();

Attribute and property values

Attributes and properties are plain Dart values, so no LaunchDarkly or OpenTelemetry types are required. A value can be a String, int, double, bool, or a homogeneous list of any of those types. The SDK ignores values it cannot represent as an attribute, such as nested maps or mixed-type lists.

The recordLog, recordException, startSpan, and track methods all accept a properties map of these same plain values.

Recording product analytics events

Use LDObserve.track to record a custom event as a product analytics span:

Track a custom event
// Track an event with properties and an optional metric value
LDObserve.track(
'purchase_completed',
properties: <String, Object?>{
'product_id': 'SKU-123',
'price': 29.99,
},
metricValue: 29.99,
);
// Track an event with no properties
LDObserve.track('button_tapped');

LDObserve also records screen views and taps manually. To learn more, read Record screen views manually and Record taps manually.

Shut down observability

Call LDObserve.shutdown() to shut down observability. You cannot restart observability after you shut it down:

Shut down observability
LDObserve.shutdown();

Identify contexts

To tie observability data to the correct context, use the LaunchDarkly client to identify or switch contexts:

Identify a context
final userContext = LDContextBuilder()
.kind('user', 'user-key')
.name('Bob Smith')
.build();
await client.identify(userContext);

You do not need to call any LDObserve method. The observability plugin hooks into the LaunchDarkly client and, on Android and iOS, forwards each completed identify to the native observability SDK and to session replay. This attributes subsequent LDObserve.track events to the active context and records the context on the active session replay recording.

Explore supported features

The observability plugin supports the following features. After the SDK and plugins are initialized, you can access these from within your application:

Review observability data in LaunchDarkly

After you initialize the SDK and observability plugin, your application automatically starts sending observability data back to LaunchDarkly, including errors and logs. You can review this information in the LaunchDarkly user interface. To learn how, read Observability.