1
- import 'package:flutter/scheduler.dart' ;
2
- import 'package:sentry/sentry.dart' ;
1
+ import 'dart:async' ;
3
2
4
- import '../sentry_flutter_options.dart' ;
3
+ import 'package:meta/meta.dart' ;
4
+
5
+ import '../../sentry_flutter.dart' ;
6
+ import '../frame_callback_handler.dart' ;
5
7
import '../native/sentry_native.dart' ;
6
8
import '../event_processor/native_app_start_event_processor.dart' ;
7
9
8
10
/// Integration which handles communication with native frameworks in order to
9
11
/// enrich [SentryTransaction] objects with app start data for mobile vitals.
10
12
class NativeAppStartIntegration extends Integration <SentryFlutterOptions > {
11
- NativeAppStartIntegration (this ._native, this ._schedulerBindingProvider );
13
+ NativeAppStartIntegration (this ._native, this ._frameCallbackHandler );
12
14
13
15
final SentryNative _native;
14
- final SchedulerBindingProvider _schedulerBindingProvider;
16
+ final FrameCallbackHandler _frameCallbackHandler;
17
+
18
+ /// We filter out App starts more than 60s
19
+ static const _maxAppStartMillis = 60000 ;
20
+
21
+ static Completer <AppStartInfo ?> _appStartCompleter =
22
+ Completer <AppStartInfo ?>();
23
+ static AppStartInfo ? _appStartInfo;
24
+
25
+ @internal
26
+ static bool isIntegrationTest = false ;
27
+
28
+ @internal
29
+ static void setAppStartInfo (AppStartInfo ? appStartInfo) {
30
+ _appStartInfo = appStartInfo;
31
+ if (_appStartCompleter.isCompleted) {
32
+ _appStartCompleter = Completer <AppStartInfo ?>();
33
+ }
34
+ _appStartCompleter.complete (appStartInfo);
35
+ }
36
+
37
+ @internal
38
+ static Future <AppStartInfo ?> getAppStartInfo () {
39
+ if (_appStartInfo != null ) {
40
+ return Future .value (_appStartInfo);
41
+ }
42
+ return _appStartCompleter.future;
43
+ }
44
+
45
+ @visibleForTesting
46
+ static void clearAppStartInfo () {
47
+ _appStartInfo = null ;
48
+ _appStartCompleter = Completer <AppStartInfo ?>();
49
+ }
15
50
16
51
@override
17
52
void call (Hub hub, SentryFlutterOptions options) {
53
+ if (isIntegrationTest) {
54
+ final appStartInfo = AppStartInfo (AppStartType .cold,
55
+ start: DateTime .now (),
56
+ end: DateTime .now ().add (const Duration (milliseconds: 100 )));
57
+ setAppStartInfo (appStartInfo);
58
+ return ;
59
+ }
60
+
18
61
if (options.autoAppStart) {
19
- final schedulerBinding = _schedulerBindingProvider ();
20
- if (schedulerBinding == null ) {
21
- options.logger (SentryLevel .debug,
22
- 'Scheduler binding is null. Can\' t auto detect app start time.' );
23
- } else {
24
- schedulerBinding.addPostFrameCallback ((timeStamp) {
25
- // ignore: invalid_use_of_internal_member
26
- _native.appStartEnd = options.clock ();
27
- });
28
- }
62
+ _frameCallbackHandler.addPostFrameCallback ((timeStamp) async {
63
+ if (_native.didFetchAppStart) {
64
+ return ;
65
+ }
66
+
67
+ // We only assign the current time if it's not already set - this is useful in tests
68
+ // ignore: invalid_use_of_internal_member
69
+ _native.appStartEnd ?? = options.clock ();
70
+ final appStartEnd = _native.appStartEnd;
71
+ final nativeAppStart = await _native.fetchNativeAppStart ();
72
+
73
+ if (nativeAppStart == null || appStartEnd == null ) {
74
+ return ;
75
+ }
76
+
77
+ final appStartDateTime = DateTime .fromMillisecondsSinceEpoch (
78
+ nativeAppStart.appStartTime.toInt ());
79
+ final duration = appStartEnd.difference (appStartDateTime);
80
+
81
+ // We filter out app start more than 60s.
82
+ // This could be due to many different reasons.
83
+ // If you do the manual init and init the SDK too late and it does not
84
+ // compute the app start end in the very first Screen.
85
+ // If the process starts but the App isn't in the foreground.
86
+ // If the system forked the process earlier to accelerate the app start.
87
+ // And some unknown reasons that could not be reproduced.
88
+ // We've seen app starts with hours, days and even months.
89
+ if (duration.inMilliseconds > _maxAppStartMillis) {
90
+ setAppStartInfo (null );
91
+ return ;
92
+ }
93
+
94
+ final appStartInfo = AppStartInfo (
95
+ nativeAppStart.isColdStart ? AppStartType .cold : AppStartType .warm,
96
+ start: DateTime .fromMillisecondsSinceEpoch (
97
+ nativeAppStart.appStartTime.toInt ()),
98
+ end: appStartEnd);
99
+ setAppStartInfo (appStartInfo);
100
+ });
29
101
}
30
102
31
103
options.addEventProcessor (NativeAppStartEventProcessor (_native));
@@ -34,5 +106,19 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
34
106
}
35
107
}
36
108
37
- /// Used to provide scheduler binding at call time.
38
- typedef SchedulerBindingProvider = SchedulerBinding ? Function ();
109
+ enum AppStartType { cold, warm }
110
+
111
+ class AppStartInfo {
112
+ AppStartInfo (this .type, {required this .start, required this .end});
113
+
114
+ final AppStartType type;
115
+ final DateTime start;
116
+ final DateTime end;
117
+
118
+ SentryMeasurement toMeasurement () {
119
+ final duration = end.difference (start);
120
+ return type == AppStartType .cold
121
+ ? SentryMeasurement .coldAppStart (duration)
122
+ : SentryMeasurement .warmAppStart (duration);
123
+ }
124
+ }
0 commit comments