Skip to content

Commit c8cb108

Browse files
authored
flutter: update ttfd (#14131)
Flutter 9.1.0 brings lots of improvements to TTFD
1 parent 5e05ea2 commit c8cb108

File tree

1 file changed

+143
-62
lines changed

1 file changed

+143
-62
lines changed

includes/dart-integrations/routing-instrumentation.mdx

Lines changed: 143 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,103 +23,193 @@ The routing instrumentation feature is shipped with Sentry's Flutter SDK automat
2323

2424
Before starting, ensure:
2525

26-
1. The Sentry Flutter SDK is initialized. Learn more [here](/platforms/dart/guides/flutter/#configure).
27-
2. Tracing is set up. Learn more [here](/platforms/dart/guides/flutter/tracing/).
26+
1. The Sentry Flutter SDK `9.1.0` or later is installed.
27+
2. The Sentry Flutter SDK is initialized. Learn more [here](/platforms/dart/guides/flutter/#configure).
28+
3. Tracing is set up. Learn more [here](/platforms/dart/guides/flutter/tracing/).
2829

2930
## Configure
3031

3132
### 1. Add `SentryNavigationObserver`
3233

3334
Add an instance of `SentryNavigationObserver` to your application's `navigatorObservers`.
3435

35-
```dart {8} {tabTitle: Flutter Routing}
36+
```dart {14-16} {tabTitle: Flutter Routing}
3637
import 'package:flutter/material.dart';
3738
import 'package:sentry_flutter/sentry_flutter.dart';
3839
39-
return MaterialApp(
40-
title: 'My Widget',
41-
home: MyWidget(),
42-
navigatorObservers: [
43-
SentryNavigatorObserver()
44-
],
45-
);
40+
Future<void> main() async {
41+
await SentryFlutter.init((options) {
42+
options.dsn = '___DSN___';
43+
}, appRunner: () => runApp(SentryWidget(child: MyApp())));
44+
}
45+
46+
class MyApp extends StatelessWidget {
47+
@override
48+
Widget build(BuildContext context) {
49+
return MaterialApp(
50+
navigatorObservers: [
51+
SentryNavigatorObserver(),
52+
],
53+
...
54+
);
55+
}
56+
}
4657
```
4758

48-
```dart {6} {tabTitle: GoRouter}
49-
import 'package:go_router/go_router.dart';
59+
```dart {9} {tabTitle: GoRouter}
60+
import 'package:flutter/material.dart';
5061
import 'package:sentry_flutter/sentry_flutter.dart';
62+
import 'package:go_router/go_router.dart';
5163
52-
final router = GoRouter(
53-
routes: ...,
64+
final _router = GoRouter(
65+
routes: [
66+
...
67+
],
5468
observers: [SentryNavigatorObserver()],
5569
);
70+
71+
Future<void> main() async {
72+
await SentryFlutter.init((options) {
73+
options.dsn = '___DSN___';
74+
}, appRunner: () => runApp(SentryWidget(child: MyApp())));
75+
}
76+
77+
class MyApp extends StatelessWidget {
78+
@override
79+
Widget build(BuildContext context) {
80+
return MaterialApp.router(
81+
routerConfig: _router,
82+
);
83+
}
84+
}
85+
5686
```
5787

5888
### 2. Define Route Names
5989

6090
By default the application's main route name is `"/"`.
6191
The instrumentation sets the span `operation` to `navigation` and the span `name` to the provided route name.
62-
For transactions to be created when navigation changes, you need to provide route names:
92+
For transactions to be created and breadcrumbs to be added when navigation changes, you need to provide route names:
6393

6494
- Flutter routing: use `RouteSettings` and set the name in the constructor.
6595
- GoRouter: use the `name` parameter to set the route name.
6696

67-
```dart {4} {tabTitle: Flutter Routing}
68-
/// Setting the route name is required
97+
<Alert>
98+
99+
Make sure that you set the route name for all routes.
100+
If you do not set the route name, the SDK will not instrument performance insights such as [TTID](/platforms/dart/guides/flutter/integrations/routing-instrumentation/#time-to-initial-display) or [TTFD](/platforms/dart/guides/flutter/integrations/routing-instrumentation/#time-to-full-display) or create breadcrumbs for the route.
101+
102+
</Alert>
103+
104+
```dart {tabTitle: Flutter Routing}
69105
MaterialPageRoute(
70106
builder: (BuildContext context) => MyWidget(),
71107
settings: RouteSettings(name: 'My Widget'),
72108
)
73109
```
74110

75-
```dart {13} {tabTitle: GoRouter}
76-
final GoRouter _router = GoRouter(
77-
observers: [SentryNavigatorObserver()],
78-
routes: <RouteBase>[
79-
GoRoute(
80-
path: '/',
81-
name: 'Home',
82-
builder: (BuildContext context, GoRouterState state) {
83-
return const HomeScreen();
84-
},
85-
routes: <RouteBase>[
86-
GoRoute(
87-
path: 'mywidget',
88-
name: 'MyWidget',
89-
builder: (BuildContext context, GoRouterState state) {
90-
return const MyWidget();
91-
},
92-
),
93-
],
94-
),
95-
],
96-
);
111+
```dart {tabTitle: GoRouter}
112+
GoRoute(
113+
path: 'mywidget',
114+
name: 'My Widget',
115+
builder: (BuildContext context, GoRouterState state) {
116+
return const MyWidget();
117+
}
118+
)
97119
```
98120

99121
## Time to Initial Display
100122

101-
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`).
123+
Time to initial display (TTID) provides insight into how long it takes your Widget to launch and draw their first frame.
124+
This is measured by adding a span for navigation to a Widget.
125+
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`).
126+
127+
TTID is enabled by default.
102128

103129
## Time to Full Display
104130

105-
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`).
131+
Time to full display (TTFD) provides insight into how long it would take your Widget to launch and load all of its content.
132+
This is measured by adding a span for each navigation to a Widget.
133+
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`).
106134

107135
TTFD is disabled by default. To enable TTFD measurements, follow these steps:
108136

109-
1. Enable the `enableTimeToFullDisplayTracing` option in the SDK configuration:
137+
### 1. Enable the `enableTimeToFullDisplayTracing` option in the SDK configuration
110138

111-
```dart {3}
139+
```dart {4}
112140
await SentryFlutter.init(
113141
(options) {
142+
options.dsn = '___DSN___';
114143
options.enableTimeToFullDisplayTracing = true;
115-
}, appRunner: () => runApp(MyApp()),
144+
}, appRunner: () => runApp(SentryWidget(child: MyApp())),
116145
);
117146
```
118147

119-
2. Report the span manually:
148+
### 2. Report the TTFD span
120149

121-
```dart
122-
await SentryFlutter.reportFullyDisplayed();
150+
There are two ways to report when your widget is fully displayed:
151+
152+
#### 1. Wrap with `SentryDisplayWidget`
153+
154+
Embed your target widget in `SentryDisplayWidget` and call `SentryDisplayWidget.of(context).reportFullyDisplayed()`.
155+
156+
#### 2. Call the API directly
157+
158+
Retrieve the current display span via `SentryFlutter.currentDisplay()` in `initState()` and call `currentDisplay.reportFullyDisplayed()` — no wrapper needed.
159+
160+
161+
<Alert>
162+
163+
**Important for `StatelessWidget`:**
164+
165+
If you're navigating to a `StatelessWidget`, you must use the `SentryDisplayWidget` wrapper.
166+
`SentryDisplayWidget` automatically reports TTFD as soon as the build completes.
167+
You do not need to call `reportFullyDisplayed()` yourself.
168+
169+
</Alert>
170+
171+
```dart {5, 13-18} {tabTitle: Wrap with SentryDisplayWidget}
172+
Navigator.push(
173+
context,
174+
MaterialPageRoute(
175+
settings: const RouteSettings(name: 'MyWidget'),
176+
builder: (context) => SentryDisplayWidget(child: MyWidget()),
177+
),
178+
);
179+
180+
// Inside MyWidget’s State:
181+
@override
182+
void initState() {
183+
super.initState();
184+
// Do some long running work...
185+
Future.delayed(const Duration(seconds: 3), () {
186+
if (mounted) {
187+
SentryDisplayWidget.of(context).reportFullyDisplayed();
188+
}
189+
});
190+
}
191+
```
192+
193+
```dart {14, 13-18} {tabTitle: Use the API Directly}
194+
Navigator.push(
195+
context,
196+
MaterialPageRoute(
197+
settings: const RouteSettings(name: 'MyWidget'),
198+
builder: (context) => MyWidget(),
199+
),
200+
);
201+
202+
// Inside MyWidget’s State:
203+
@override
204+
void initState() {
205+
super.initState();
206+
// Get a reference to the current display before doing work.
207+
final currentDisplay = SentryFlutter.currentDisplay();
208+
// Do some long running work...
209+
Future.delayed(const Duration(seconds: 3), () {
210+
currentDisplay?.reportFullyDisplayed();
211+
});
212+
}
123213
```
124214

125215
If the span finishes through the API, its status will be set to `SpanStatus.OK`.
@@ -136,7 +226,6 @@ Set up a new widget that executes an expensive operation.
136226
import 'package:flutter/material.dart';
137227
import 'package:sentry/sentry.dart';
138228
139-
/// Widget that executes an expensive operation
140229
class MyWidget extends StatefulWidget {
141230
const MyWidget({super.key});
142231
@@ -145,22 +234,14 @@ class MyWidget extends StatefulWidget {
145234
}
146235
147236
class MyWidgetState extends State<MyWidget> {
148-
static const delayInSeconds = 5;
149-
150237
@override
151238
void initState() {
152239
super.initState();
153-
_doComplexOperation();
154-
}
155-
156-
/// Attach child spans to the routing transaction
157-
/// or the transaction will not be sent to Sentry.
158-
Future<void> _doComplexOperation() async {
159-
final activeTransaction = Sentry.getSpan();
160-
final childSpan = activeTransaction?.startChild('complex operation',
161-
description: 'running a $delayInSeconds seconds operation');
162-
await Future.delayed(const Duration(seconds: delayInSeconds));
163-
childSpan?.finish();
240+
Future.delayed(const Duration(seconds: 3), () {
241+
if (mounted) {
242+
SentryDisplayWidget.of(context).reportFullyDisplayed();
243+
}
244+
});
164245
}
165246
166247
@override
@@ -183,7 +264,7 @@ Navigator.push(
183264
context,
184265
MaterialPageRoute(
185266
settings: const RouteSettings(name: 'MyWidget'),
186-
builder: (context) => MyWidget(),
267+
builder: (context) => SentryDisplayWidget(child: MyWidget()),
187268
),
188269
);
189270
```

0 commit comments

Comments
 (0)