Routing Instrumentation
Learn more about the Sentry Routing Instrumentation for the Flutter SDK.
Sentry's routing instrumentation for Flutter automatically tracks and reports page navigation events in your app. It supports imperative navigation (Navigator.push()), the declarative Router API (MaterialApp.router), and popular routing packages like GoRouter and auto_route.
The routing instrumentation feature is shipped with Sentry's Flutter SDK automatically.
Before starting, ensure:
- The Sentry Flutter SDK
9.1.0or later is installed. - The Sentry Flutter SDK is initialized. Learn more here.
- Tracing is set up. Learn more here.
How you add SentryNavigatorObserver depends on which navigation approach you use.
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async {
await SentryFlutter.init((options) {
options.dsn = '___DSN___';
}, appRunner: () => runApp(SentryWidget(child: MyApp())));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
navigatorObservers: [
SentryNavigatorObserver(),
],
...
);
}
}
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async {
await SentryFlutter.init((options) {
options.dsn = '___DSN___';
}, appRunner: () => runApp(SentryWidget(child: MyApp())));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
navigatorObservers: [
SentryNavigatorObserver(),
],
...
);
}
}
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
// When using a custom RouterDelegate, pass the observer
// to the Navigator you build inside the delegate's build() method.
class MyRouterDelegate extends RouterDelegate<MyRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRoutePath> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
observers: [SentryNavigatorObserver()],
pages: [
// Your pages based on app state
],
onDidRemovePage: (page) {
// Update your app state based on the removed page
notifyListeners();
},
);
}
// ... implement setNewRoutePath and other required methods
}
Future<void> main() async {
await SentryFlutter.init((options) {
options.dsn = '___DSN___';
}, appRunner: () => runApp(SentryWidget(child: MyApp())));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: MyRouterDelegate(),
routeInformationParser: MyRouteInformationParser(),
);
}
}
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:go_router/go_router.dart';
final _router = GoRouter(
routes: [
...
],
observers: [SentryNavigatorObserver()],
);
Future<void> main() async {
await SentryFlutter.init((options) {
options.dsn = '___DSN___';
}, appRunner: () => runApp(SentryWidget(child: MyApp())));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:auto_route/auto_route.dart';
// Your generated router
final _appRouter = AppRouter();
Future<void> main() async {
await SentryFlutter.init((options) {
options.dsn = '___DSN___';
}, appRunner: () => runApp(SentryWidget(child: MyApp())));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _appRouter.config(
navigatorObservers: () => [
SentryNavigatorObserver(),
],
),
);
}
}
The instrumentation sets the span operation to ui.load and the span name to the provided route name.
How you define route names depends on your navigation approach:
- Navigator (imperative): Pass a
namein theRouteSettingswhen pushing routes. - Router: Set the
nameon thePageobjects you return in yourpageslist (for example,MaterialPage(name: 'My Widget', child: ...)). - GoRouter: GoRouter automatically uses the route
pathas the name. You can optionally set thenameparameter on yourGoRouteto override it. - auto_route: Route names are generated automatically from your
@RoutePage()annotations — no extra configuration needed.
MaterialPageRoute(
builder: (BuildContext context) => MyWidget(),
settings: RouteSettings(name: 'My Widget'),
)
MaterialPageRoute(
builder: (BuildContext context) => MyWidget(),
settings: RouteSettings(name: 'My Widget'),
)
// Set the name on the Page objects in your RouterDelegate's pages list.
MaterialPage(
name: 'My Widget',
child: MyWidget(),
)
// The path is used as the route name by default.
// You can optionally set name to override it.
GoRoute(
path: '/mywidget',
name: 'My Widget', // optional, falls back to path
builder: (BuildContext context, GoRouterState state) {
return const MyWidget();
}
)
// Route names are auto-generated from your page annotations.
// No additional configuration is needed.
()
class MyWidgetPage extends StatelessWidget {
// ...
}
Time to initial display (TTID) provides insight into how long it takes your Widget to launch and draw their first frame. This is measured by adding a span for navigation to a Widget. The SDK then sets the span operation to ui.load.initial-display and the span description to the Widget's route name, followed by initial display (for example, MyWidget initial display).
TTID is enabled by default.
Time to full display (TTFD) provides insight into how long it would take your Widget to launch and load all of its content. This is measured by adding a span for each navigation to a Widget. The SDK then sets the span operation to ui.load.full-display and the span description to the Widget's route name, followed by full display (for example, MyWidget full display).
TTFD is disabled by default. To enable TTFD measurements, follow these steps:
await SentryFlutter.init(
(options) {
options.dsn = '___DSN___';
options.enableTimeToFullDisplayTracing = true;
}, appRunner: () => runApp(SentryWidget(child: MyApp())),
);
await SentryFlutter.init(
(options) {
options.dsn = '___DSN___';
options.enableTimeToFullDisplayTracing = true;
}, appRunner: () => runApp(SentryWidget(child: MyApp())),
);
There are two ways to report when your widget is fully displayed:
Embed your target widget in SentryDisplayWidget and call SentryDisplayWidget.of(context).reportFullyDisplayed().
Retrieve the current display span via SentryFlutter.currentDisplay() in initState() and call currentDisplay.reportFullyDisplayed() — no wrapper needed.
Important for StatelessWidget:
If you're navigating to a StatelessWidget, you must use the SentryDisplayWidget wrapper. SentryDisplayWidget automatically reports TTFD as soon as the build completes. You do not need to call reportFullyDisplayed() yourself.
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: 'MyWidget'),
builder: (context) => SentryDisplayWidget(child: MyWidget()),
),
);
// Inside MyWidget’s State:
void initState() {
super.initState();
// Do some long running work...
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
SentryDisplayWidget.of(context).reportFullyDisplayed();
}
});
}
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: 'MyWidget'),
builder: (context) => SentryDisplayWidget(child: MyWidget()),
),
);
// Inside MyWidget’s State:
void initState() {
super.initState();
// Do some long running work...
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
SentryDisplayWidget.of(context).reportFullyDisplayed();
}
});
}
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: 'MyWidget'),
builder: (context) => MyWidget(),
),
);
// Inside MyWidget’s State:
void initState() {
super.initState();
// Get a reference to the current display before doing work.
final currentDisplay = SentryFlutter.currentDisplay();
// Do some long running work...
Future.delayed(const Duration(seconds: 3), () {
currentDisplay?.reportFullyDisplayed();
});
}
If the span finishes through the API, its status will be set to SpanStatus.OK.
If the span doesn't finish after 30 seconds, it will be finished by the SDK automatically, and its status will be set to SpanStatus.DEADLINE_EXCEEDED. In this case, its duration will match the TTID span.
Set up a new widget that executes an expensive operation.
import 'package:flutter/material.dart';
import 'package:sentry/sentry.dart';
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
SentryDisplayWidget.of(context).reportFullyDisplayed();
}
});
}
Widget build(BuildContext context) {
return ...
}
}
import 'package:flutter/material.dart';
import 'package:sentry/sentry.dart';
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
MyWidgetState createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
SentryDisplayWidget.of(context).reportFullyDisplayed();
}
});
}
Widget build(BuildContext context) {
return ...
}
}
Use the navigator to transition to your widget. This should create and send a transaction named after the widget's route.
import 'package:flutter/material.dart';
import 'my_widget.dart';
/// Push to a new screen
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: 'MyWidget'),
builder: (context) => SentryDisplayWidget(child: MyWidget()),
),
);
import 'package:flutter/material.dart';
import 'my_widget.dart';
/// Push to a new screen
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: 'MyWidget'),
builder: (context) => SentryDisplayWidget(child: MyWidget()),
),
);
// Update your delegate's state to trigger a page change.
final delegate = Router.of(context).routerDelegate as MyRouterDelegate;
delegate.showDetail();
import 'package:go_router/go_router.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
context.push('/mywidget')
import 'package:auto_route/auto_route.dart';
context.pushRoute(const MyWidgetRoute());
Log into sentry.io and open your project's performance page to see the transaction MyWidget.
Adjust the duration before a routing transaction automatically finishes. The default is 3 seconds.
SentryNavigatorObserver(
autoFinishAfter: Duration(seconds: 5)
)
SentryNavigatorObserver(
autoFinishAfter: Duration(seconds: 5)
)
When configuring the autoFinishAfter parameter, consider the following behaviours:
- Started child spans will be attached to the navigation transaction - for example the
MyWidgettransaction. - If child spans finish after the
autoFinishAftertime, the transaction extends and finishes when all child spans finished. - If child spans finish before the
autoFinishAftertime, the transaction's end time will be set to the last child end time.
Set enableAutoTransactions to false if you only want to track navigation breadcrumbs. Enabled by default.
SentryNavigatorObserver(
enableAutoTransactions: false,
)
SentryNavigatorObserver(
enableAutoTransactions: false,
)
Set ignoreRoutes if you want routes to be ignored and not processed by the navigation observer. Empty by default.
SentryNavigatorObserver(
ignoreRoutes: ["/ignoreThisRoute", "/my/ignored/route"],
)
SentryNavigatorObserver(
ignoreRoutes: ["/ignoreThisRoute", "/my/ignored/route"],
)
Set setRouteNameAsTransaction to true to override the transaction name with the route name. An existing transaction in the scope 'CustomTransaction' will be renamed to 'MyWidget' for example. Disabled by default.
SentryNavigatorObserver(
setRouteNameAsTransaction: true,
)
SentryNavigatorObserver(
setRouteNameAsTransaction: true,
)
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").