Skip to content

Commit a3903f0

Browse files
committed
update
1 parent 07e67a3 commit a3903f0

File tree

3 files changed

+263
-129
lines changed

3 files changed

+263
-129
lines changed

flutter/lib/src/navigation/time_to_display_tracker.dart

Lines changed: 64 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,23 @@ import '../../sentry_flutter.dart';
77
import '../integrations/integrations.dart';
88
import '../native/sentry_native.dart';
99
import 'display_strategy_evaluator.dart';
10+
import 'time_to_display_transaction_handler.dart';
1011

1112
@internal
1213
class TimeToDisplayTracker {
1314
final Hub _hub;
14-
final bool _enableAutoTransactions;
15-
final Duration _autoFinishAfter;
1615
final SentryNative? _native;
16+
final TimeToDisplayTransactionHandler _transactionHandler;
1717

1818
static DateTime? _startTimestamp;
1919
static DateTime? _ttidEndTimestamp;
2020
static ISentrySpan? _ttidSpan;
2121
static ISentrySpan? _ttfdSpan;
2222
static Timer? _ttfdTimer;
23+
static ISentrySpan? _transaction;
24+
25+
@visibleForTesting
26+
Duration ttfdAutoFinishAfter = Duration(seconds: 30);
2327

2428
SentryFlutterOptions? get _options => _hub.options is SentryFlutterOptions
2529
// ignore: invalid_use_of_internal_member
@@ -30,75 +34,17 @@ class TimeToDisplayTracker {
3034
required Hub? hub,
3135
required bool enableAutoTransactions,
3236
required Duration autoFinishAfter,
37+
TimeToDisplayTransactionHandler? transactionHandler,
3338
}) : _hub = hub ?? HubAdapter(),
34-
_enableAutoTransactions = enableAutoTransactions,
35-
_autoFinishAfter = autoFinishAfter,
36-
_native = SentryFlutter.native;
37-
38-
Future<ISentrySpan?> _startTransaction(String? routeName, Object? arguments,
39-
{DateTime? startTimestamp}) async {
40-
if (!_enableAutoTransactions) {
41-
return null;
42-
}
43-
44-
if (routeName == null) {
45-
return null;
46-
}
47-
48-
if (routeName == '/') {
49-
routeName = 'root ("/")';
50-
}
51-
52-
final transactionContext = SentryTransactionContext(
53-
routeName,
54-
'ui.load',
55-
transactionNameSource: SentryTransactionNameSource.component,
56-
// ignore: invalid_use_of_internal_member
57-
origin: SentryTraceOrigins.autoNavigationRouteObserver,
58-
);
59-
60-
final transaction = _hub.startTransactionWithContext(
61-
transactionContext,
62-
waitForChildren: true,
63-
autoFinishAfter: _autoFinishAfter,
64-
trimEnd: true,
65-
bindToScope: true,
66-
startTimestamp: startTimestamp,
67-
onFinish: (transaction) async {
68-
final nativeFrames = await _native
69-
?.endNativeFramesCollection(transaction.context.traceId);
70-
if (nativeFrames != null) {
71-
final measurements = nativeFrames.toMeasurements();
72-
for (final item in measurements.entries) {
73-
final measurement = item.value;
74-
transaction.setMeasurement(
75-
item.key,
76-
measurement.value,
77-
unit: measurement.unit,
39+
_native = SentryFlutter.native,
40+
_transactionHandler = transactionHandler ??
41+
TimeToDisplayTransactionHandler(
42+
hub: hub,
43+
enableAutoTransactions: enableAutoTransactions,
44+
autoFinishAfter: autoFinishAfter,
7845
);
79-
}
80-
}
81-
},
82-
);
83-
84-
// if _enableAutoTransactions is enabled but there's no traces sample rate
85-
if (transaction is NoOpSentrySpan) {
86-
return null;
87-
}
88-
89-
if (arguments != null) {
90-
transaction.setData('route_settings_arguments', arguments);
91-
}
92-
93-
await _native?.beginNativeFramesCollection();
94-
95-
return transaction;
96-
}
9746

9847
void startMeasurement(String? routeName, Object? arguments) async {
99-
_ttidSpan = null;
100-
_ttfdSpan = null;
101-
10248
final startTimestamp = DateTime.now();
10349
_startTimestamp = startTimestamp;
10450

@@ -123,27 +69,37 @@ class TimeToDisplayTracker {
12369
final routeName = SentryNavigatorObserver.currentRouteName;
12470
if (appStartInfo == null || routeName == null) return;
12571

126-
final transaction = await _startTransaction(routeName, arguments,
72+
final transaction = await _transactionHandler.startTransaction(
73+
routeName, arguments,
12774
startTimestamp: appStartInfo.start);
12875
if (transaction == null) return;
76+
_transaction = transaction;
12977

130-
final ttidSpan =
131-
_createTTIDSpan(transaction, routeName, appStartInfo.start);
78+
final ttidSpan = _transactionHandler.createSpan(
79+
transaction,
80+
TimeToDisplayType.timeToInitialDisplay,
81+
routeName,
82+
appStartInfo.start);
13283
if (_options?.enableTimeToFullDisplayTracing == true) {
133-
_ttfdSpan = _createTTFDSpan(transaction, routeName, appStartInfo.start);
84+
_ttfdSpan = _transactionHandler.createSpan(transaction,
85+
TimeToDisplayType.timeToFullDisplay, routeName, appStartInfo.start);
13486
}
135-
_finishSpan(ttidSpan, transaction, appStartInfo.end,
87+
TimeToDisplayTransactionHandler.finishSpan(
88+
transaction: transaction,
89+
span: ttidSpan,
90+
endTimestamp: appStartInfo.end,
13691
measurement: appStartInfo.measurement);
13792
});
13893
}
13994

14095
// Handles measuring navigation for regular routes
14196
void _handleRegularRouteMeasurement(
14297
String? routeName, Object? arguments, DateTime startTimestamp) async {
143-
final transaction = await _startTransaction(routeName, arguments,
144-
startTimestamp: startTimestamp);
98+
final transaction = await _transactionHandler
99+
.startTransaction(routeName, arguments, startTimestamp: startTimestamp);
145100

146101
if (transaction == null || routeName == null) return;
102+
_transaction = transaction;
147103

148104
_initializeTimeToDisplaySpans(transaction, routeName, startTimestamp);
149105

@@ -155,36 +111,43 @@ class TimeToDisplayTracker {
155111

156112
void _initializeTimeToDisplaySpans(
157113
ISentrySpan transaction, String routeName, DateTime startTimestamp) {
158-
_ttidSpan = _createTTIDSpan(transaction, routeName, startTimestamp);
114+
_ttidSpan = _transactionHandler.createSpan(transaction,
115+
TimeToDisplayType.timeToInitialDisplay, routeName, startTimestamp);
159116
if (_options?.enableTimeToFullDisplayTracing == true) {
160-
_ttfdSpan = _createTTFDSpan(transaction, routeName, startTimestamp);
161-
final ttfdAutoFinishAfter = Duration(seconds: 30);
162-
_ttfdTimer = Timer(ttfdAutoFinishAfter, () {
163-
if (_ttfdSpan?.finished == true) {
117+
_ttfdSpan = _transactionHandler.createSpan(transaction,
118+
TimeToDisplayType.timeToFullDisplay, routeName, startTimestamp);
119+
_ttfdTimer = Timer(ttfdAutoFinishAfter, () async {
120+
final ttfdSpan = _ttfdSpan;
121+
final ttfdEndTimestamp = _ttidEndTimestamp;
122+
if (ttfdSpan == null ||
123+
ttfdSpan.finished == true ||
124+
ttfdEndTimestamp == null) {
164125
return;
165126
}
166-
_finishSpan(_ttfdSpan!, transaction, _ttidEndTimestamp!,
127+
TimeToDisplayTransactionHandler.finishSpan(
128+
transaction: transaction,
129+
span: ttfdSpan,
130+
endTimestamp: ttfdEndTimestamp,
167131
status: SpanStatus.deadlineExceeded());
168132
});
169133
}
170134
}
171135

172-
ISentrySpan _createTTIDSpan(
173-
ISentrySpan transaction, String routeName, DateTime startTimestamp) {
174-
return transaction.startChild(
175-
SentryTraceOrigins.uiTimeToInitialDisplay,
176-
description: '$routeName initial display',
177-
startTimestamp: startTimestamp,
178-
);
179-
}
136+
void _finishInitialDisplay(ISentrySpan ttidSpan, ISentrySpan transaction,
137+
String routeName, DateTime startTimestamp) async {
138+
final endTimestamp = await _determineEndTimeOfTTID(routeName);
139+
if (endTimestamp == null) return;
140+
_ttidEndTimestamp = endTimestamp;
180141

181-
ISentrySpan _createTTFDSpan(
182-
ISentrySpan transaction, String routeName, DateTime startTimestamp) {
183-
return transaction.startChild(
184-
SentryTraceOrigins.uiTimeToFullDisplay,
185-
description: '$routeName full display',
186-
startTimestamp: startTimestamp,
187-
);
142+
final duration = endTimestamp.difference(startTimestamp).inMilliseconds;
143+
final measurement = SentryMeasurement('time_to_initial_display', duration,
144+
unit: DurationSentryMeasurementUnit.milliSecond);
145+
146+
TimeToDisplayTransactionHandler.finishSpan(
147+
transaction: transaction,
148+
span: ttidSpan,
149+
endTimestamp: endTimestamp,
150+
measurement: measurement);
188151
}
189152

190153
Future<DateTime?> _determineEndTimeOfTTID(String routeName) async {
@@ -216,43 +179,21 @@ class TimeToDisplayTracker {
216179

217180
@internal
218181
static void reportFullyDisplayed() {
219-
_finishFullDisplay();
220-
}
221-
222-
static void _finishFullDisplay() {
223182
_ttfdTimer?.cancel();
224183
final endTimestamp = DateTime.now();
225184
final startTimestamp = _startTimestamp;
226-
final transaction = Sentry.getSpan();
185+
final transaction = _transaction;
227186
final ttfdSpan = _ttfdSpan;
228187
if (startTimestamp == null || transaction == null || ttfdSpan == null) {
229188
return;
230189
}
231190
final duration = endTimestamp.difference(startTimestamp).inMilliseconds;
232191
final measurement = SentryMeasurement('time_to_full_display', duration,
233192
unit: DurationSentryMeasurementUnit.milliSecond);
234-
_finishSpan(ttfdSpan, transaction, endTimestamp, measurement: measurement);
235-
}
236-
237-
void _finishInitialDisplay(ISentrySpan ttidSpan, ISentrySpan transaction,
238-
String routeName, DateTime startTimestamp) async {
239-
final endTimestamp = await _determineEndTimeOfTTID(routeName);
240-
if (endTimestamp == null) return;
241-
_ttidEndTimestamp = endTimestamp;
242-
243-
final duration = endTimestamp.difference(startTimestamp).inMilliseconds;
244-
final measurement = SentryMeasurement('time_to_initial_display', duration,
245-
unit: DurationSentryMeasurementUnit.milliSecond);
246-
_finishSpan(ttidSpan, transaction, endTimestamp, measurement: measurement);
247-
}
248-
249-
static void _finishSpan(
250-
ISentrySpan span, ISentrySpan transaction, DateTime endTimestamp,
251-
{SentryMeasurement? measurement, SpanStatus? status}) {
252-
if (measurement != null) {
253-
transaction.setMeasurement(measurement.name, measurement.value,
254-
unit: measurement.unit);
255-
}
256-
span.finish(status: status, endTimestamp: endTimestamp);
193+
TimeToDisplayTransactionHandler.finishSpan(
194+
transaction: transaction,
195+
span: ttfdSpan,
196+
endTimestamp: endTimestamp,
197+
measurement: measurement);
257198
}
258199
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import 'package:meta/meta.dart';
2+
import '../../sentry_flutter.dart';
3+
import '../native/sentry_native.dart';
4+
5+
enum TimeToDisplayType { timeToInitialDisplay, timeToFullDisplay }
6+
7+
@internal
8+
abstract class ITimeToDisplayTransactionHandler {
9+
Future<ISentrySpan?> startTransaction(String? routeName, Object? arguments,
10+
{DateTime? startTimestamp});
11+
12+
ISentrySpan createSpan(ISentrySpan transaction, TimeToDisplayType type,
13+
String routeName, DateTime startTimestamp);
14+
}
15+
16+
@internal
17+
class TimeToDisplayTransactionHandler extends ITimeToDisplayTransactionHandler {
18+
final Hub? _hub;
19+
final bool? _enableAutoTransactions;
20+
final Duration? _autoFinishAfter;
21+
final SentryNative? _native;
22+
23+
TimeToDisplayTransactionHandler({
24+
required Hub? hub,
25+
required bool? enableAutoTransactions,
26+
required Duration? autoFinishAfter,
27+
}) : _hub = hub ?? HubAdapter(),
28+
_enableAutoTransactions = enableAutoTransactions,
29+
_autoFinishAfter = autoFinishAfter,
30+
_native = SentryFlutter.native;
31+
32+
@override
33+
Future<ISentrySpan?> startTransaction(String? routeName, Object? arguments,
34+
{DateTime? startTimestamp}) async {
35+
if (_enableAutoTransactions == false) {
36+
return null;
37+
}
38+
39+
if (routeName == null) {
40+
return null;
41+
}
42+
43+
if (routeName == '/') {
44+
routeName = 'root ("/")';
45+
}
46+
47+
final transactionContext = SentryTransactionContext(
48+
routeName,
49+
'ui.load',
50+
transactionNameSource: SentryTransactionNameSource.component,
51+
// ignore: invalid_use_of_internal_member
52+
origin: SentryTraceOrigins.autoNavigationRouteObserver,
53+
);
54+
55+
final transaction = _hub?.startTransactionWithContext(
56+
transactionContext,
57+
waitForChildren: true,
58+
autoFinishAfter: _autoFinishAfter,
59+
trimEnd: true,
60+
bindToScope: true,
61+
startTimestamp: startTimestamp,
62+
onFinish: (transaction) async {
63+
final nativeFrames = await _native
64+
?.endNativeFramesCollection(transaction.context.traceId);
65+
if (nativeFrames != null) {
66+
final measurements = nativeFrames.toMeasurements();
67+
for (final item in measurements.entries) {
68+
final measurement = item.value;
69+
transaction.setMeasurement(
70+
item.key,
71+
measurement.value,
72+
unit: measurement.unit,
73+
);
74+
}
75+
}
76+
},
77+
);
78+
79+
// if _enableAutoTransactions is enabled but there's no traces sample rate
80+
if (transaction is NoOpSentrySpan) {
81+
return null;
82+
}
83+
84+
if (arguments != null) {
85+
transaction?.setData('route_settings_arguments', arguments);
86+
}
87+
88+
await _native?.beginNativeFramesCollection();
89+
90+
return transaction;
91+
}
92+
93+
@override
94+
ISentrySpan createSpan(ISentrySpan transaction, TimeToDisplayType type,
95+
String routeName, DateTime startTimestamp) {
96+
String operation;
97+
String description;
98+
switch (type) {
99+
case TimeToDisplayType.timeToInitialDisplay:
100+
operation = SentryTraceOrigins.uiTimeToInitialDisplay;
101+
description = '$routeName initial display';
102+
break;
103+
case TimeToDisplayType.timeToFullDisplay:
104+
operation = SentryTraceOrigins.uiTimeToFullDisplay;
105+
description = '$routeName full display';
106+
break;
107+
}
108+
return transaction.startChild(operation,
109+
description: description, startTimestamp: startTimestamp);
110+
}
111+
112+
static void finishSpan(
113+
{required ISentrySpan span,
114+
required ISentrySpan transaction,
115+
DateTime? endTimestamp,
116+
SentryMeasurement? measurement,
117+
SpanStatus? status}) {
118+
if (measurement != null) {
119+
transaction.setMeasurement(measurement.name, measurement.value,
120+
unit: measurement.unit);
121+
}
122+
span.finish(status: status, endTimestamp: endTimestamp);
123+
}
124+
}

0 commit comments

Comments
 (0)