Skip to content

Commit 5e7abc5

Browse files
authored
refactor: fetch app start in integration instead of event processor (#1905)
* Change app start integration in a way that works with ttid as well * Formatting * Update * add visibleForTesting * Update * update * Add app start info test * Remove set app start info null * Review improvements * run on arm mac * Fix integration test
1 parent 014c3ea commit 5e7abc5

11 files changed

+181
-73
lines changed

.github/workflows/flutter_test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ jobs:
111111

112112
cocoa:
113113
name: "${{ matrix.target }} | ${{ matrix.sdk }}"
114-
runs-on: macos-13
114+
runs-on: macos-latest-xlarge
115115
timeout-minutes: 30
116116
defaults:
117117
run:

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
- Use `recordHttpBreadcrumbs` to set iOS `enableNetworkBreadcrumbs` ([#1884](https://github.com/getsentry/sentry-dart/pull/1884))
88

9+
### Improvements
10+
11+
- App start is now fetched within integration instead of event processor ([#1905](https://github.com/getsentry/sentry-dart/pull/1905))
12+
913
## 7.16.1
1014

1115
### Fixes

flutter/example/integration_test/integration_test.dart

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// ignore_for_file: avoid_print
2+
// ignore_for_file: invalid_use_of_internal_member
23

34
import 'dart:async';
45
import 'dart:convert';
@@ -8,6 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
89
import 'package:sentry_flutter/sentry_flutter.dart';
910
import 'package:sentry_flutter_example/main.dart';
1011
import 'package:http/http.dart';
12+
import 'package:sentry_flutter/src/integrations/native_app_start_integration.dart';
1113

1214
void main() {
1315
// const org = 'sentry-sdks';
@@ -24,6 +26,8 @@ void main() {
2426
// Using fake DSN for testing purposes.
2527
Future<void> setupSentryAndApp(WidgetTester tester,
2628
{String? dsn, BeforeSendCallback? beforeSendCallback}) async {
29+
NativeAppStartIntegration.isIntegrationTest = true;
30+
2731
await setupSentry(
2832
() async {
2933
await tester.pumpWidget(SentryScreenshotWidget(

flutter/lib/src/event_processor/native_app_start_event_processor.dart

+11-39
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,29 @@ import 'dart:async';
22

33
import 'package:sentry/sentry.dart';
44

5+
import '../integrations/integrations.dart';
56
import '../native/sentry_native.dart';
67

78
/// EventProcessor that enriches [SentryTransaction] objects with app start
89
/// measurement.
910
class NativeAppStartEventProcessor implements EventProcessor {
10-
/// We filter out App starts more than 60s
11-
static const _maxAppStartMillis = 60000;
12-
13-
NativeAppStartEventProcessor(
14-
this._native,
15-
);
16-
1711
final SentryNative _native;
1812

13+
NativeAppStartEventProcessor(this._native);
14+
1915
@override
2016
Future<SentryEvent?> apply(SentryEvent event, {Hint? hint}) async {
21-
final appStartEnd = _native.appStartEnd;
17+
if (_native.didAddAppStartMeasurement || event is! SentryTransaction) {
18+
return event;
19+
}
20+
21+
final appStartInfo = await NativeAppStartIntegration.getAppStartInfo();
22+
final measurement = appStartInfo?.toMeasurement();
2223

23-
if (appStartEnd != null &&
24-
event is SentryTransaction &&
25-
!_native.didFetchAppStart) {
26-
final nativeAppStart = await _native.fetchNativeAppStart();
27-
if (nativeAppStart == null) {
28-
return event;
29-
}
30-
final measurement = nativeAppStart.toMeasurement(appStartEnd);
31-
// We filter out app start more than 60s.
32-
// This could be due to many different reasons.
33-
// If you do the manual init and init the SDK too late and it does not
34-
// compute the app start end in the very first Screen.
35-
// If the process starts but the App isn't in the foreground.
36-
// If the system forked the process earlier to accelerate the app start.
37-
// And some unknown reasons that could not be reproduced.
38-
// We've seen app starts with hours, days and even months.
39-
if (measurement.value >= _maxAppStartMillis) {
40-
return event;
41-
}
24+
if (measurement != null) {
4225
event.measurements[measurement.name] = measurement;
26+
_native.didAddAppStartMeasurement = true;
4327
}
4428
return event;
4529
}
4630
}
47-
48-
extension NativeAppStartMeasurement on NativeAppStart {
49-
SentryMeasurement toMeasurement(DateTime appStartEnd) {
50-
final appStartDateTime =
51-
DateTime.fromMillisecondsSinceEpoch(appStartTime.toInt());
52-
final duration = appStartEnd.difference(appStartDateTime);
53-
54-
return isColdStart
55-
? SentryMeasurement.coldAppStart(duration)
56-
: SentryMeasurement.warmAppStart(duration);
57-
}
58-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import 'package:flutter/scheduler.dart';
2+
3+
abstract class FrameCallbackHandler {
4+
void addPostFrameCallback(FrameCallback callback);
5+
}
6+
7+
class DefaultFrameCallbackHandler implements FrameCallbackHandler {
8+
@override
9+
void addPostFrameCallback(FrameCallback callback) {
10+
try {
11+
/// Flutter >= 2.12 throws if SchedulerBinding.instance isn't initialized.
12+
SchedulerBinding.instance.addPostFrameCallback(callback);
13+
} catch (_) {}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,103 @@
1-
import 'package:flutter/scheduler.dart';
2-
import 'package:sentry/sentry.dart';
1+
import 'dart:async';
32

4-
import '../sentry_flutter_options.dart';
3+
import 'package:meta/meta.dart';
4+
5+
import '../../sentry_flutter.dart';
6+
import '../frame_callback_handler.dart';
57
import '../native/sentry_native.dart';
68
import '../event_processor/native_app_start_event_processor.dart';
79

810
/// Integration which handles communication with native frameworks in order to
911
/// enrich [SentryTransaction] objects with app start data for mobile vitals.
1012
class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
11-
NativeAppStartIntegration(this._native, this._schedulerBindingProvider);
13+
NativeAppStartIntegration(this._native, this._frameCallbackHandler);
1214

1315
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+
}
1550

1651
@override
1752
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+
1861
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+
});
29101
}
30102

31103
options.addEventProcessor(NativeAppStartEventProcessor(_native));
@@ -34,5 +106,19 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
34106
}
35107
}
36108

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+
}

flutter/lib/src/native/sentry_native.dart

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class SentryNative {
2727
/// Flag indicating if app start was already fetched.
2828
bool get didFetchAppStart => _didFetchAppStart;
2929

30+
/// Flag indicating if app start measurement was added to the first transaction.
31+
bool didAddAppStartMeasurement = false;
32+
3033
/// Fetch [NativeAppStart] from native channels. Can only be called once.
3134
Future<NativeAppStart?> fetchNativeAppStart() async {
3235
_didFetchAppStart = true;

flutter/lib/src/sentry_flutter.dart

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import 'dart:async';
22
import 'dart:ui';
33

4-
import 'package:flutter/scheduler.dart';
54
import 'package:flutter/services.dart';
65
import 'package:flutter/widgets.dart';
76
import 'package:meta/meta.dart';
87
import '../sentry_flutter.dart';
98
import 'event_processor/android_platform_exception_event_processor.dart';
109
import 'event_processor/flutter_exception_event_processor.dart';
1110
import 'event_processor/platform_exception_event_processor.dart';
11+
import 'frame_callback_handler.dart';
1212
import 'integrations/connectivity/connectivity_integration.dart';
1313
import 'integrations/screenshot_integration.dart';
1414
import 'native/factory.dart';
@@ -189,13 +189,7 @@ mixin SentryFlutter {
189189
if (_native != null) {
190190
integrations.add(NativeAppStartIntegration(
191191
_native!,
192-
() {
193-
try {
194-
/// Flutter >= 2.12 throws if SchedulerBinding.instance isn't initialized.
195-
return SchedulerBinding.instance;
196-
} catch (_) {}
197-
return null;
198-
},
192+
DefaultFrameCallbackHandler(),
199193
));
200194
}
201195
return integrations;
@@ -231,6 +225,7 @@ mixin SentryFlutter {
231225

232226
@internal
233227
static SentryNative? get native => _native;
228+
234229
@internal
235230
static set native(SentryNative? value) => _native = value;
236231
static SentryNative? _native;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'package:flutter/scheduler.dart';
2+
import 'package:sentry_flutter/src/frame_callback_handler.dart';
3+
4+
class FakeFrameCallbackHandler implements FrameCallbackHandler {
5+
FrameCallback? storedCallback;
6+
7+
final Duration _finishAfterDuration;
8+
9+
FakeFrameCallbackHandler(
10+
{Duration finishAfterDuration = const Duration(milliseconds: 500)})
11+
: _finishAfterDuration = finishAfterDuration;
12+
13+
@override
14+
void addPostFrameCallback(FrameCallback callback) async {
15+
// ignore: inference_failure_on_instance_creation
16+
await Future.delayed(_finishAfterDuration);
17+
callback(Duration.zero);
18+
}
19+
}

0 commit comments

Comments
 (0)