Customize autocaptured screens

Automatic screen view tracking can be done using the CSQNavigatorObserver. This will observe the navigation stack of the application and log screen events accordingly.

This observer can be attached to any WidgetApp (like MaterialApp, CupertinoApp) or to a RouterConfig

import 'package:contentsquare/csq.dart';
MaterialApp(
navigatorObservers: [
CSQNavigatorObserver(),
],
);

The CSQNavigatorObserver can be configured with the following arguments:

An optional callback function that determines whether a specific route should be excluded from tracking. The function takes a Route as input and returns a bool.

Use this function to selectively exclude certain routes from automatic tracking by returning true for those routes.

import 'package:contentsquare/csq.dart';
CSQNavigatorObserver(
excludeRouteFromTracking: (route) {
// Exclude a specific route name
if (route.settings.name == '/myRoute') return true;
// Exclude popup routes (menus, popups, dialogs)
if (route is PopupRoute) return true;
// Exclude dialog routes (AlertDialog, showDialog)
// Many dialogs inherit from PopupRoute, but this is extra safety:
if (route.runtimeType.toString().contains('DialogRoute')) return true;
// Exclude full-screen dialogs
if (route is PageRoute && route.fullscreenDialog == true) return true;
return false;
},
)

By default, all routes are tracked.

An optional callback function used to customize the screen name for a given route. The function takes a Route as input and returns a String representing the desired screen name.

Use this function to define custom screen names for specific routes.

import 'package:contentsquare/csq.dart';
CSQNavigatorObserver(
screenNameProvider: (route) {
final screenName = _formatScreenName(route.settings.name);
return screenName;
},
)

If you’re using the AutoRoute package, refer to the CSQNavigatorAutoRouteObserver documentation for automatic screen tracking support, including tab navigation.

PageView widgets sometimes are used to build swipeable screens such as onboarding flows, multi-step forms, or carousels. However, because PageView navigation does not push new routes to the navigation stack, navigator observers are not triggered when pages change.

To ensure correct tracking, each page transition must be tracked manually using the onPageChanged callback.

For example, when using PageView.builder, you can track each page as an individual screen:

import 'package:contentsquare/csq.dart';
class PageViewTrackingExample extends StatelessWidget {
const PageViewTrackingExample({super.key});
final _controller = PageController();
// Define meaningful screen names for each page
final _pageNames = const <String>[
'onboarding_step_1',
'onboarding_step_2',
'onboarding_step_3',
];
@override
Widget build(BuildContext context) {
return PageView.builder(
controller: _controller,
itemCount: _pageNames.length,
onPageChanged: (index) {
// Track screen view manually for each page change
CSQ().trackScreenview(screenName: _pageNames[index]);
},
itemBuilder: (_, index) {
return OnboardingPage(step: index);
},
);
}
}

If you use a pageBuilder in your routes, make sure to set the page .name.

Example using GoRoute:

import 'package:contentsquare/csq.dart';
GoRoute(
path: 'checkout',
name: 'Checkout Screen',
pageBuilder: (context, state) => MaterialPage(
name: 'Checkout Screen',
key: ValueKey(state.pathParameters),
fullscreenDialog: true,
child: const CheckoutScreen(),
),
),

TabBar views are commonly tracked as individual screens. However, most TabBar implementations push the route to the navigation stack only during initialization, which can make it challenging to properly track navigation events for each tab change.

To track tab changes correctly, you need to manually call trackScreenview when the active tab changes. Here’s an example using TabController:

import 'package:contentsquare/csq.dart';
class TabBarExample extends StatefulWidget {
const TabBarExample({super.key});
@override
State<TabBarExample> createState() => _TabBarExampleState();
}
class _TabBarExampleState extends State<TabBarExample>
with SingleTickerProviderStateMixin {
late TabController _tabController;
// Define meaningful screen names for each tab
final _tabNames = const <String>[
'home_tab',
'search_tab',
'profile_tab',
];
@override
void initState() {
super.initState();
_tabController = TabController(length: _tabNames.length, vsync: this);
// Track initial tab
CSQ().trackScreenview(screenName: _tabNames[_tabController.index]);
// Listen to tab changes
_tabController.addListener(() {
if (!_tabController.indexIsChanging) {
CSQ().trackScreenview(screenName: _tabNames[_tabController.index]);
}
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(icon: Icon(Icons.home), text: 'Home'),
Tab(icon: Icon(Icons.search), text: 'Search'),
Tab(icon: Icon(Icons.person), text: 'Profile'),
],
),
),
body: TabBarView(
controller: _tabController,
children: const [
HomeTab(),
SearchTab(),
ProfileTab(),
],
),
);
}
}