Skip to main content
Configure automatic screen and interaction capture for Flutter applications using the Userpilot Flutter SDK. The Userpilot Flutter SDK can automatically capture screen views and user interactions without any explicit instrumentation in your app. Auto Capture runs entirely in Dart — no build-time plugin is required, and the same Dart integration is used on Android and iOS.

SDK Configuration

Auto capture needs three integration points:
  1. Userpilot.initialize(..., UserpilotOptions(...)) — starts the native Userpilot SDK and, when autocapture options are provided, starts the Dart autocapture pipeline.
  2. UserpilotAutocaptureWidget(child: ...) — wraps your app so the SDK can attach a global pointer route and listen for scroll settle notifications (PageView, TabBarView, wheel pickers).
  3. UserpilotAutocaptureNavigatorObserver() — added to each Navigator you want tracked for $screen and dialog/bottom-sheet view_presented events.
Call Userpilot.initialize(...) when your app boots (often from an initState / splash path after WidgetsFlutterBinding.ensureInitialized() if you await it before runApp).
OptionTypeDefaultDescription
enableScreenAutoCaptureboolfalseEnables automatic screen tracking; captures $screen on push/pop/replace.
enableInteractionAutoCaptureboolfalseEnables automatic interaction tracking: tap, value-change, selection-change, text-change, and view_presented.
enableInteractionAccessibilityLabelCapturebooltrueCaptures accessibility/semantic metadata (label, identifier, hint, value, tag).
enableInteractionTextCapturebooltrueCaptures visible text labels (button titles, list-tile titles, text-field labels/placeholders, dialog titles, trigger-source titles, etc.). Set false to strip those text fields from interaction payloads. Truncated to a fixed cap when enabled.
enableInteractionValueCaptureboolfalseCaptures user-input values (slider value, toggle state, picked date/time, dropdown choice). Off by default for privacy.
maxHierarchyDepthint25Maximum number of ancestor widgets included in event metadata.
NoteAuto Capture automatically tracks screen views and ignores manually sent screen events while enabled.If you are migrating from manual screen tracking, please review the migration guide here.

Screen Capture

UserpilotAutocaptureNavigatorObserver() returns a plain NavigatorObserver. Whenever Flutter pushes, pops, or replaces a named PageRoute, the observer emits a $screen event whose title is the route’s RouteSettings.name. Routes without an explicit name are ignored so framework-generated route classes do not leak into screen analytics. The observer is stateless and safe to instantiate multiple times. Add it to every Navigator you want tracked (root navigator, shell navigators, tab branch navigators).
Manual screensUserpilot.screen('<title>') is a no-op while screen autocapture is enabled. To send screens manually, leave enableScreenAutoCapture: false in UserpilotOptions.

GoRouter

Add the observer to GoRouter’s observers list. For nested navigators (ShellRoute, StatefulShellBranch), add a fresh observer instance per navigator:
GoRouter limitations:
  • StatefulShellRoute.indexedStack keeps every branch alive in an IndexedStack. Switching tabs does not push or pop, so $screen only fires on the first visit to each branch. Use a non-stateful ShellRoute if you need a screen event on every tab swap.
  • CustomTransitionPage must be constructed with key: state.pageKey and name: state.matchedLocation. Without name, the generated page route is unnamed and no $screen event is emitted.

AutoRoute

Pass a fresh observer instance through AutoRoute’s navigatorObservers builder. The builder is invoked once per nested router (root, every AutoTabsRouter branch, etc.), so every navigator in the tree gets its own observer with no extra setup:

Interaction Capture

When enableInteractionAutoCapture: true, the SDK emits an event for the following user interactions:
Interaction typeEmitted for
touchGeneric taps on buttons and gesture surfaces such as ElevatedButton, FilledButton, TextButton, OutlinedButton, IconButton, FloatingActionButton, BackButton, CloseButton, CupertinoButton, InkWell, InkResponse, and GestureDetector.
switch_changedSwitch, SwitchListTile, CupertinoSwitch
checkbox_selectedCheckbox, CheckboxListTile, CupertinoCheckbox
toggle_button_selectedToggleButtons, SegmentedButton
radio_button_selectedRadio, RadioListTile, CupertinoRadio
slider_changedSlider, RangeSlider, CupertinoSlider
chip_selectedChoiceChip, FilterChip, InputChip, ActionChip
date_picker_changedDatePicker, CalendarDatePicker, CupertinoDatePicker
time_picker_changedTimePicker, CupertinoTimerPicker
text_field_changedTextField, TextFormField, CupertinoTextField (debounced; typed text is not captured)
list_item_selectedListView / ListTile rows
spinner_selectedDropdownButton, DropdownMenu items
wheel_item_selectedCupertinoPicker, wheel scroll views
page_selectedPageView swipes
tab_host_selectedTabBar tabs (the in-screen tab strip)
tab_selectedBottomNavigationBar, NavigationBar, NavigationRail items
menu_item_selectedPopupMenuButton items, MenuAnchor items
view_presentedDialog and bottom-sheet presentation
Each interaction maps to a high-level analytics category (tap, text_change, selection_change, value_change, view_presented) — that is the event name that reaches your analytics backend.

Component Value Gating

By default, user-input values are stripped from event payloads — slider values, toggle states, picked dates and times, and dropdown selections do not leave the device. To include them, pass enableInteractionValueCapture: true in UserpilotOptions. Position and configuration fields (selected_index, min, max, direction) are always captured because they don’t reveal user input.

Privacy Controls

Wire Metadata Keys

Interaction events use the keys in payload_schema.dart (_allowedKeys). Common fields:
KeyMeaning
target_classClassified widget type (e.g. ElevatedButton, AlertDialog)
target_textPrimary visible label / title for the control
deepest_target_typeWidget type at the hit point
hierarchySemicolon-separated ancestor path (see below)
accessibility_label, accessibility_identifier, accessibility_hint, accessibility_value, view_tagFlattened semantics metadata
selected_indexList/tab/page index
selected_value, valuesCaptured value for single-value and multi-value controls when value capture is enabled
placeholderText-field hint / decoration hint
presentation_surfacePresented UI component type (e.g. AlertDialog, BottomSheet) — not system_dialog / bottom_sheet
Screen events ($screen) still use a single title field on the native bridge.

hierarchy Format

Built from the hit element up to maxHierarchyDepth, then the current screen name. Each segment looks like: WidgetType:attr__desc="…",attr__id="…",attr__index="n"
  • attr__desc — only the semantics accessibility label for that ancestor. Omitted when enableInteractionAccessibilityLabelCapture: false, under UserpilotRedactText, or when no label exists.
  • attr__idSemantics.identifier or ValueKey value when present. Semantics.identifier is omitted when enableInteractionAccessibilityLabelCapture: false or under UserpilotRedactText; ValueKey remains structural metadata.
  • attr__index — sibling index under the parent.
Segments are joined with ;. Under UserpilotRedactText, hierarchy is preserved but text/accessibility attributes are omitted so redacted text does not appear in the path.

UserpilotRedactText

Wraps a subtree so autocapture replaces every text label it would otherwise capture (target_text, placeholder, dialog titles, trigger source names, accessibility strings, etc.) with ****. The interaction event still fires — the widget is still identified by type, position, and structure — only the human-readable text is masked. Capture configuration is applied before redacted values leave the SDK. If enableInteractionTextCapture: false, text fields such as target_text are removed from the payload instead of being sent as ****. If enableInteractionAccessibilityLabelCapture: false, accessibility fields are removed from the payload and from hierarchy attributes.
hierarchy remains on redacted events, but text/accessibility attributes such as attr__desc and Semantics.identifier are omitted. Remove the wrapper to disable masking for that subtree.

UserpilotIgnoreInteractions

Wraps a subtree so autocapture skips interaction events whose hit-tested widget lies inside it (taps, value changes, scroll selections, debounced text-field updates tied to that field, etc.). Route-driven $screen events are not suppressed by UserpilotIgnoreInteractions — they come from the navigator observer, not from widget hit testing. The host app’s own gesture handlers (onPressed, onTap, …) still run — only autocapture ignores those interactions.
Use UserpilotIgnoreInteractions for interactions whose mere occurrence would leak intent (e.g. tapping a row in a contacts list, where row position correlates 1:1 with contact identity). Both widgets are passive markers — their build returns child unchanged. Wrap the outermost widget you want affected; descendants of the wrapper are covered, ancestors are not.

Runtime Controls

To stop or resume collection without changing options — for example a “private mode” — use:
Both calls are idempotent. They preserve the options supplied to Userpilot.initialize(...).

Limitations

  • Routes pushed without RouteSettings.name are ignored for screen autocapture. Always pass a name (or use named routes via onGenerateRoute) when a route should be tracked as a screen.
  • Multiple Navigator instances — every nested Navigator has its own observers list. Add UserpilotAutocaptureNavigatorObserver() to each navigator you want tracked.
  • Non-PageRoute transitions — the observer only treats PageRoute subclasses as screens. Custom transitions that extend TransitionRoute directly are not captured. Both go_router and auto_route produce PageRoute subclasses for their default page builders, so this only affects bespoke routing implementations.
  • Text-field attachment — editable fields are discovered when autocapture is registered and when focus moves (and during internal rescans). A field that appears without a focus change may not be watched until the next scan trigger.
  • Duplicate taps — identical tap payloads within a short window (~500 ms) may be dropped to avoid double events from rebuilds or pointer routing.