observeTabController static method

TabObservation observeTabController(
  1. TabController controller, {
  2. List<String>? tabNames,
  3. String? group,
  4. bool foldIntoScreenName = true,
})

Reports every selection change on controller as a ui.navigation tab_switch span, and — unless foldIntoScreenName is false — folds the selected tab into screen.name.

Listening to the controller — rather than intercepting onTap — is what catches swipe-driven TabBarView changes as well as TabBar taps.

late final _tabs = TabController(length: 3, vsync: this);
TabObservation? _obs;

@override
void initState() {
  super.initState();
  _obs = VuTelemetry.observeTabController(
    _tabs,
    tabNames: const ['Taps', 'Values', 'Gestures'],
  );
}

@override
void dispose() {
  _obs?.dispose();   // the SDK owns the listener, you own the controller
  _tabs.dispose();
  super.dispose();
}

Prefer VuTabScope when you can wrap the widget instead — it handles the lifetime for you. Pass group when a screen has more than one tab surface.

tabNames supplies the label per index; without it tabs report as tab_0, tab_1, … A controller built with a non-zero initialIndex does not emit a span merely by being observed.

Set foldIntoScreenName to false when the destinations are already distinct routes (a shell over per-branch nested Navigators): the switch still emits tab_switch with navigation.tab.*, but screen.name keeps the branch's own name rather than gaining a redundant segment.

Implementation

static TabObservation observeTabController(
  TabController controller, {
  List<String>? tabNames,
  String? group,
  bool foldIntoScreenName = true,
}) {
  return tab_tracking.observeTabController(
    controller,
    tabNames: tabNames,
    group: group,
    foldIntoScreenName: foldIntoScreenName,
  );
}