Skip to content

Commit 1dbc007

Browse files
committed
draft impl for ttid
1 parent 83626bd commit 1dbc007

File tree

5 files changed

+125
-20
lines changed

5 files changed

+125
-20
lines changed

flutter/example/lib/auto_close_screen.dart

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:sentry/sentry.dart';
3+
import 'package:sentry_flutter/sentry_flutter.dart';
34

45
/// This screen is only used to demonstrate how route navigation works.
56
/// Init will create a child span and pop the screen after 3 seconds.

flutter/example/lib/main.dart

+4-3
Original file line numberDiff line numberDiff line change
@@ -723,9 +723,10 @@ void navigateToAutoCloseScreen(BuildContext context) {
723723
Navigator.push(
724724
context,
725725
MaterialPageRoute(
726-
settings: const RouteSettings(name: 'AutoCloseScreen'),
727-
builder: (context) => const SentryDisplayWidget(child: AutoCloseScreen(),
728-
)),
726+
settings: const RouteSettings(name: 'AutoCloseScreen'),
727+
builder: (context) => const SentryDisplayWidget(
728+
child: AutoCloseScreen(),
729+
)),
729730
);
730731
}
731732

flutter/lib/src/navigation/sentry_navigator_observer.dart

+64-10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:flutter/scheduler.dart';
12
import 'package:flutter/widgets.dart';
23
import 'package:meta/meta.dart';
34

@@ -86,13 +87,18 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
8687
final RouteNameExtractor? _routeNameExtractor;
8788
final AdditionalInfoExtractor? _additionalInfoProvider;
8889
final SentryNative? _native;
90+
static ISentrySpan? _transaction2;
91+
92+
static ISentrySpan? get transaction2 => _transaction2;
8993

9094
ISentrySpan? _transaction;
9195

9296
static String? _currentRouteName;
9397

9498
@internal
9599
static String? get currentRouteName => _currentRouteName;
100+
static var startTime = DateTime.now();
101+
static ISentrySpan? ttidSpan;
96102

97103
@override
98104
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
@@ -108,7 +114,36 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
108114
);
109115

110116
_finishTransaction();
117+
118+
var routeName = route.settings.name ?? 'Unknown';
119+
111120
_startTransaction(route);
121+
122+
// Start timing
123+
DateTime? approximationEndTimestamp;
124+
int? approximationDurationMillis;
125+
126+
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
127+
approximationEndTimestamp = DateTime.now();
128+
approximationDurationMillis =
129+
approximationEndTimestamp!.millisecond - startTime.millisecond;
130+
});
131+
132+
SentryDisplayTracker().startTimeout(routeName, () {
133+
_transaction2?.setMeasurement(
134+
'time_to_initial_display', approximationDurationMillis!,
135+
unit: DurationSentryMeasurementUnit.milliSecond);
136+
ttidSpan?.setTag('measurement', 'approximation');
137+
ttidSpan?.finish(endTimestamp: approximationEndTimestamp!);
138+
});
139+
}
140+
141+
void freezeUIForSeconds(int seconds) {
142+
var sw = Stopwatch()..start();
143+
while (sw.elapsed.inSeconds < seconds) {
144+
// This loop will block the UI thread.
145+
}
146+
sw.stop();
112147
}
113148

114149
@override
@@ -193,16 +228,26 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
193228
if (name == '/') {
194229
name = 'root ("/")';
195230
}
196-
final transactionContext = SentryTransactionContext(
231+
// final transactionContext = SentryTransactionContext(
232+
// name,
233+
// 'navigation',
234+
// transactionNameSource: SentryTransactionNameSource.component,
235+
// // ignore: invalid_use_of_internal_member
236+
// origin: SentryTraceOrigins.autoNavigationRouteObserver,
237+
// );
238+
239+
final transactionContext2 = SentryTransactionContext(
197240
name,
198-
'navigation',
241+
'ui.load',
199242
transactionNameSource: SentryTransactionNameSource.component,
200243
// ignore: invalid_use_of_internal_member
201244
origin: SentryTraceOrigins.autoNavigationRouteObserver,
202245
);
203246

204-
_transaction = _hub.startTransactionWithContext(
205-
transactionContext,
247+
// IMPORTANT -> we need to wait for ttid/ttfd children to finish AND wait [autoFinishAfter] afterwards so the user can add additional spans
248+
// right now it auto finishes when ttid/ttfd finishes but that doesn't allow the user to add spans within the idle timeout
249+
_transaction2 = _hub.startTransactionWithContext(
250+
transactionContext2,
206251
waitForChildren: true,
207252
autoFinishAfter: _autoFinishAfter,
208253
trimEnd: true,
@@ -225,24 +270,33 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
225270

226271
// if _enableAutoTransactions is enabled but there's no traces sample rate
227272
if (_transaction is NoOpSentrySpan) {
228-
_transaction = null;
273+
_transaction2 = null;
229274
return;
230275
}
231276

277+
startTime = DateTime.now();
278+
ttidSpan = _transaction2?.startChild('ui.load.initial_display');
279+
ttidSpan?.origin = 'auto.ui.time_to_display';
280+
ttidSpan?.setData('test', 'cachea');
281+
282+
// Needs to finish after 30 seconds
283+
// If not then it will finish with status deadline exceeded
284+
// final ttfdSpan = _transaction2?.startChild('ui.load.full_display');
285+
232286
if (arguments != null) {
233-
_transaction?.setData('route_settings_arguments', arguments);
287+
_transaction2?.setData('route_settings_arguments', arguments);
234288
}
235289

236290
await _hub.configureScope((scope) {
237-
scope.span ??= _transaction;
291+
scope.span ??= _transaction2;
238292
});
239293

240294
await _native?.beginNativeFramesCollection();
241295
}
242296

243-
Future<void> _finishTransaction() async {
244-
_transaction?.status ??= SpanStatus.ok();
245-
await _transaction?.finish();
297+
Future<void> _finishTransaction({DateTime? endTimestamp}) async {
298+
_transaction2?.status ??= SpanStatus.ok();
299+
await _transaction2?.finish(endTimestamp: endTimestamp);
246300
}
247301
}
248302

flutter/lib/src/sentry_flutter.dart

+13-5
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,21 @@ mixin SentryFlutter {
229229
options.sdk = sdk;
230230
}
231231

232-
static void reportInitialDisplay() {
233-
print('reported accurate TTID!');
232+
static void reportInitialDisplay(BuildContext context) {
233+
final routeName = ModalRoute.of(context)?.settings.name ?? 'Unknown';
234+
final endTime = DateTime.now();
235+
if (!SentryDisplayTracker().reportManual(routeName)) {
236+
SentryNavigatorObserver.transaction2?.setMeasurement(
237+
'time_to_initial_display',
238+
endTime.millisecond - SentryNavigatorObserver.startTime.millisecond,
239+
unit: DurationSentryMeasurementUnit.milliSecond);
240+
241+
SentryNavigatorObserver.ttidSpan?.setTag('measurement', 'manual');
242+
SentryNavigatorObserver.ttidSpan?.finish(endTimestamp: endTime);
243+
}
234244
}
235245

236-
static void reportFullDisplay() {
237-
238-
}
246+
static void reportFullDisplay() {}
239247

240248
@internal
241249
static SentryNative? get native => _native;

flutter/lib/src/sentry_widget.dart

+43-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/cupertino.dart';
4+
import 'package:flutter/material.dart';
25
import '../sentry_flutter.dart';
36

47
/// This widget serves as a wrapper to include Sentry widgets such
@@ -34,13 +37,51 @@ class SentryDisplayWidget extends StatefulWidget {
3437
class _SentryDisplayWidgetState extends State<SentryDisplayWidget> {
3538
@override
3639
void initState() {
37-
// TODO: implement initState
3840
super.initState();
39-
SentryFlutter.reportInitialDisplay();
41+
WidgetsBinding.instance.addPostFrameCallback((_) {
42+
SentryFlutter.reportInitialDisplay(context);
43+
});
4044
}
4145

4246
@override
4347
Widget build(BuildContext context) {
4448
return widget.child;
4549
}
4650
}
51+
52+
class SentryDisplayTracker {
53+
static final SentryDisplayTracker _instance =
54+
SentryDisplayTracker._internal();
55+
56+
factory SentryDisplayTracker() {
57+
return _instance;
58+
}
59+
60+
SentryDisplayTracker._internal();
61+
62+
final Map<String, bool> _manualReportReceived = {};
63+
final Map<String, Timer> _timers = {};
64+
65+
void startTimeout(String routeName, Function onTimeout) {
66+
_timers[routeName]?.cancel(); // Cancel any existing timer
67+
_timers[routeName] = Timer(Duration(seconds: 2), () {
68+
// Don't send if we already received a manual report or if we're on the root route e.g App start.
69+
if (!(_manualReportReceived[routeName] ?? false)) {
70+
onTimeout();
71+
}
72+
});
73+
}
74+
75+
bool reportManual(String routeName) {
76+
var wasReportedAlready = _manualReportReceived[routeName] ?? false;
77+
_manualReportReceived[routeName] = true;
78+
_timers[routeName]?.cancel();
79+
return wasReportedAlready;
80+
}
81+
82+
void clearState(String routeName) {
83+
_manualReportReceived.remove(routeName);
84+
_timers[routeName]?.cancel();
85+
_timers.remove(routeName);
86+
}
87+
}

0 commit comments

Comments
 (0)