From 83626bd22805396a6b5a6c2959fec464265b7717 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 31 Jan 2024 12:20:20 +0100 Subject: [PATCH 01/47] test --- flutter/example/lib/main.dart | 4 ++-- flutter/lib/src/sentry_flutter.dart | 8 ++++++++ flutter/lib/src/sentry_widget.dart | 23 +++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 26bbdce6d8..d43794bb90 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -724,8 +724,8 @@ void navigateToAutoCloseScreen(BuildContext context) { context, MaterialPageRoute( settings: const RouteSettings(name: 'AutoCloseScreen'), - builder: (context) => const AutoCloseScreen(), - ), + builder: (context) => const SentryDisplayWidget(child: AutoCloseScreen(), + )), ); } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 62a9043bc9..d655c6b57b 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -229,6 +229,14 @@ mixin SentryFlutter { options.sdk = sdk; } + static void reportInitialDisplay() { + print('reported accurate TTID!'); + } + + static void reportFullDisplay() { + + } + @internal static SentryNative? get native => _native; @internal diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index ed390d1ab4..c9dfb6a1c0 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -21,3 +21,26 @@ class _SentryWidgetState extends State { return content; } } + +class SentryDisplayWidget extends StatefulWidget { + final Widget child; + + const SentryDisplayWidget({super.key, required this.child}); + + @override + _SentryDisplayWidgetState createState() => _SentryDisplayWidgetState(); +} + +class _SentryDisplayWidgetState extends State { + @override + void initState() { + // TODO: implement initState + super.initState(); + SentryFlutter.reportInitialDisplay(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} From 1dbc0071d7fac8d670fbd76be2ee5ca0a4af4da3 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 2 Feb 2024 13:40:20 +0100 Subject: [PATCH 02/47] draft impl for ttid --- flutter/example/lib/auto_close_screen.dart | 1 + flutter/example/lib/main.dart | 7 +- .../navigation/sentry_navigator_observer.dart | 74 ++++++++++++++++--- flutter/lib/src/sentry_flutter.dart | 18 +++-- flutter/lib/src/sentry_widget.dart | 45 ++++++++++- 5 files changed, 125 insertions(+), 20 deletions(-) diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart index 15e8fac1fb..3a1866e564 100644 --- a/flutter/example/lib/auto_close_screen.dart +++ b/flutter/example/lib/auto_close_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sentry/sentry.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; /// This screen is only used to demonstrate how route navigation works. /// Init will create a child span and pop the screen after 3 seconds. diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index d43794bb90..2e4b2e7518 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -723,9 +723,10 @@ void navigateToAutoCloseScreen(BuildContext context) { Navigator.push( context, MaterialPageRoute( - settings: const RouteSettings(name: 'AutoCloseScreen'), - builder: (context) => const SentryDisplayWidget(child: AutoCloseScreen(), - )), + settings: const RouteSettings(name: 'AutoCloseScreen'), + builder: (context) => const SentryDisplayWidget( + child: AutoCloseScreen(), + )), ); } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 6893d47aca..37c5a75221 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -1,3 +1,4 @@ +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -86,6 +87,9 @@ class SentryNavigatorObserver extends RouteObserver> { final RouteNameExtractor? _routeNameExtractor; final AdditionalInfoExtractor? _additionalInfoProvider; final SentryNative? _native; + static ISentrySpan? _transaction2; + + static ISentrySpan? get transaction2 => _transaction2; ISentrySpan? _transaction; @@ -93,6 +97,8 @@ class SentryNavigatorObserver extends RouteObserver> { @internal static String? get currentRouteName => _currentRouteName; + static var startTime = DateTime.now(); + static ISentrySpan? ttidSpan; @override void didPush(Route route, Route? previousRoute) { @@ -108,7 +114,36 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); + + var routeName = route.settings.name ?? 'Unknown'; + _startTransaction(route); + + // Start timing + DateTime? approximationEndTimestamp; + int? approximationDurationMillis; + + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + approximationEndTimestamp = DateTime.now(); + approximationDurationMillis = + approximationEndTimestamp!.millisecond - startTime.millisecond; + }); + + SentryDisplayTracker().startTimeout(routeName, () { + _transaction2?.setMeasurement( + 'time_to_initial_display', approximationDurationMillis!, + unit: DurationSentryMeasurementUnit.milliSecond); + ttidSpan?.setTag('measurement', 'approximation'); + ttidSpan?.finish(endTimestamp: approximationEndTimestamp!); + }); + } + + void freezeUIForSeconds(int seconds) { + var sw = Stopwatch()..start(); + while (sw.elapsed.inSeconds < seconds) { + // This loop will block the UI thread. + } + sw.stop(); } @override @@ -193,16 +228,26 @@ class SentryNavigatorObserver extends RouteObserver> { if (name == '/') { name = 'root ("/")'; } - final transactionContext = SentryTransactionContext( + // final transactionContext = SentryTransactionContext( + // name, + // 'navigation', + // transactionNameSource: SentryTransactionNameSource.component, + // // ignore: invalid_use_of_internal_member + // origin: SentryTraceOrigins.autoNavigationRouteObserver, + // ); + + final transactionContext2 = SentryTransactionContext( name, - 'navigation', + 'ui.load', transactionNameSource: SentryTransactionNameSource.component, // ignore: invalid_use_of_internal_member origin: SentryTraceOrigins.autoNavigationRouteObserver, ); - _transaction = _hub.startTransactionWithContext( - transactionContext, + // IMPORTANT -> we need to wait for ttid/ttfd children to finish AND wait [autoFinishAfter] afterwards so the user can add additional spans + // right now it auto finishes when ttid/ttfd finishes but that doesn't allow the user to add spans within the idle timeout + _transaction2 = _hub.startTransactionWithContext( + transactionContext2, waitForChildren: true, autoFinishAfter: _autoFinishAfter, trimEnd: true, @@ -225,24 +270,33 @@ class SentryNavigatorObserver extends RouteObserver> { // if _enableAutoTransactions is enabled but there's no traces sample rate if (_transaction is NoOpSentrySpan) { - _transaction = null; + _transaction2 = null; return; } + startTime = DateTime.now(); + ttidSpan = _transaction2?.startChild('ui.load.initial_display'); + ttidSpan?.origin = 'auto.ui.time_to_display'; + ttidSpan?.setData('test', 'cachea'); + + // Needs to finish after 30 seconds + // If not then it will finish with status deadline exceeded + // final ttfdSpan = _transaction2?.startChild('ui.load.full_display'); + if (arguments != null) { - _transaction?.setData('route_settings_arguments', arguments); + _transaction2?.setData('route_settings_arguments', arguments); } await _hub.configureScope((scope) { - scope.span ??= _transaction; + scope.span ??= _transaction2; }); await _native?.beginNativeFramesCollection(); } - Future _finishTransaction() async { - _transaction?.status ??= SpanStatus.ok(); - await _transaction?.finish(); + Future _finishTransaction({DateTime? endTimestamp}) async { + _transaction2?.status ??= SpanStatus.ok(); + await _transaction2?.finish(endTimestamp: endTimestamp); } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index d655c6b57b..3ef591183d 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -229,13 +229,21 @@ mixin SentryFlutter { options.sdk = sdk; } - static void reportInitialDisplay() { - print('reported accurate TTID!'); + static void reportInitialDisplay(BuildContext context) { + final routeName = ModalRoute.of(context)?.settings.name ?? 'Unknown'; + final endTime = DateTime.now(); + if (!SentryDisplayTracker().reportManual(routeName)) { + SentryNavigatorObserver.transaction2?.setMeasurement( + 'time_to_initial_display', + endTime.millisecond - SentryNavigatorObserver.startTime.millisecond, + unit: DurationSentryMeasurementUnit.milliSecond); + + SentryNavigatorObserver.ttidSpan?.setTag('measurement', 'manual'); + SentryNavigatorObserver.ttidSpan?.finish(endTimestamp: endTime); + } } - static void reportFullDisplay() { - - } + static void reportFullDisplay() {} @internal static SentryNative? get native => _native; diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index c9dfb6a1c0..7db077a3e5 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import '../sentry_flutter.dart'; /// This widget serves as a wrapper to include Sentry widgets such @@ -34,9 +37,10 @@ class SentryDisplayWidget extends StatefulWidget { class _SentryDisplayWidgetState extends State { @override void initState() { - // TODO: implement initState super.initState(); - SentryFlutter.reportInitialDisplay(); + WidgetsBinding.instance.addPostFrameCallback((_) { + SentryFlutter.reportInitialDisplay(context); + }); } @override @@ -44,3 +48,40 @@ class _SentryDisplayWidgetState extends State { return widget.child; } } + +class SentryDisplayTracker { + static final SentryDisplayTracker _instance = + SentryDisplayTracker._internal(); + + factory SentryDisplayTracker() { + return _instance; + } + + SentryDisplayTracker._internal(); + + final Map _manualReportReceived = {}; + final Map _timers = {}; + + void startTimeout(String routeName, Function onTimeout) { + _timers[routeName]?.cancel(); // Cancel any existing timer + _timers[routeName] = Timer(Duration(seconds: 2), () { + // Don't send if we already received a manual report or if we're on the root route e.g App start. + if (!(_manualReportReceived[routeName] ?? false)) { + onTimeout(); + } + }); + } + + bool reportManual(String routeName) { + var wasReportedAlready = _manualReportReceived[routeName] ?? false; + _manualReportReceived[routeName] = true; + _timers[routeName]?.cancel(); + return wasReportedAlready; + } + + void clearState(String routeName) { + _manualReportReceived.remove(routeName); + _timers[routeName]?.cancel(); + _timers.remove(routeName); + } +} From f2668bb2348fba9069e6272d48e0f36cf9282e6c Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 6 Feb 2024 13:10:26 +0100 Subject: [PATCH 03/47] poc --- flutter/example/lib/auto_close_screen.dart | 4 ++- flutter/example/lib/main.dart | 1 + .../navigation/sentry_navigator_observer.dart | 30 +++++++++++-------- flutter/lib/src/sentry_flutter.dart | 13 +++++++- flutter/lib/src/sentry_flutter_options.dart | 4 +++ 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart index 3a1866e564..9fa368700a 100644 --- a/flutter/example/lib/auto_close_screen.dart +++ b/flutter/example/lib/auto_close_screen.dart @@ -26,7 +26,9 @@ class AutoCloseScreenState extends State { final childSpan = activeSpan?.startChild('complex operation', description: 'running a $delayInSeconds seconds operation'); await Future.delayed(const Duration(seconds: delayInSeconds)); - childSpan?.finish(); + await childSpan?.finish(); + await Future.delayed(const Duration(seconds: 2)); + SentryFlutter.reportFullDisplay(); // ignore: use_build_context_synchronously Navigator.of(context).pop(); } diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 2e4b2e7518..250a900b32 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -71,6 +71,7 @@ Future setupSentry(AppRunner appRunner, String dsn, options.attachScreenshot = true; options.screenshotQuality = SentryScreenshotQuality.low; options.attachViewHierarchy = true; + options.enableTimeToFullDisplayTracing = true; // We can enable Sentry debug logging during development. This is likely // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 37c5a75221..6e2f7dd99e 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -99,6 +99,9 @@ class SentryNavigatorObserver extends RouteObserver> { static String? get currentRouteName => _currentRouteName; static var startTime = DateTime.now(); static ISentrySpan? ttidSpan; + static ISentrySpan? ttfdSpan; + static var ttfdStartTime = DateTime.now(); + static Stopwatch? ttfdStopwatch; @override void didPush(Route route, Route? previousRoute) { @@ -114,14 +117,12 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); - - var routeName = route.settings.name ?? 'Unknown'; - _startTransaction(route); // Start timing DateTime? approximationEndTimestamp; int? approximationDurationMillis; + final routeName = _getRouteName(route); SchedulerBinding.instance.addPostFrameCallback((timeStamp) { approximationEndTimestamp = DateTime.now(); @@ -129,11 +130,14 @@ class SentryNavigatorObserver extends RouteObserver> { approximationEndTimestamp!.millisecond - startTime.millisecond; }); - SentryDisplayTracker().startTimeout(routeName, () { + SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { + if (routeName == '/') { + // TODO: Does TTID have to be completely in line with app start? + // If yes, how do we access the appstart metrics + } _transaction2?.setMeasurement( 'time_to_initial_display', approximationDurationMillis!, unit: DurationSentryMeasurementUnit.milliSecond); - ttidSpan?.setTag('measurement', 'approximation'); ttidSpan?.finish(endTimestamp: approximationEndTimestamp!); }); } @@ -244,8 +248,6 @@ class SentryNavigatorObserver extends RouteObserver> { origin: SentryTraceOrigins.autoNavigationRouteObserver, ); - // IMPORTANT -> we need to wait for ttid/ttfd children to finish AND wait [autoFinishAfter] afterwards so the user can add additional spans - // right now it auto finishes when ttid/ttfd finishes but that doesn't allow the user to add spans within the idle timeout _transaction2 = _hub.startTransactionWithContext( transactionContext2, waitForChildren: true, @@ -275,13 +277,17 @@ class SentryNavigatorObserver extends RouteObserver> { } startTime = DateTime.now(); - ttidSpan = _transaction2?.startChild('ui.load.initial_display'); + ttidSpan = _transaction2?.startChild('ui.load.initial_display', description: '$name initial display'); ttidSpan?.origin = 'auto.ui.time_to_display'; - ttidSpan?.setData('test', 'cachea'); - // Needs to finish after 30 seconds - // If not then it will finish with status deadline exceeded - // final ttfdSpan = _transaction2?.startChild('ui.load.full_display'); + // TODO: Needs to finish max within 30 seconds + // If timeout exceeds then it will finish with status deadline exceeded + // What to do if root also has TTFD but it's not finished yet and we start navigating to another? + // How to track the time that 30 sec have passed? + if ((_hub.options as SentryFlutterOptions).enableTimeToFullDisplayTracing && name != 'root ("/")') { + ttfdStopwatch = Stopwatch()..start(); + ttfdSpan = _transaction2?.startChild('ui.load.full_display', description: '$name full display'); + } if (arguments != null) { _transaction2?.setData('route_settings_arguments', arguments); diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 3ef591183d..efe3305efa 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -243,7 +243,18 @@ mixin SentryFlutter { } } - static void reportFullDisplay() {} + /// Reports the time it took for the screen to be fully displayed. + static void reportFullDisplay() { + if (SentryNavigatorObserver.ttfdStopwatch?.elapsedMilliseconds != null) { + SentryNavigatorObserver.ttfdStopwatch?.stop(); + SentryNavigatorObserver.ttfdSpan?.setMeasurement( + 'time_to_full_display', + SentryNavigatorObserver.ttfdStopwatch!.elapsedMilliseconds, + unit: DurationSentryMeasurementUnit.milliSecond); + SentryNavigatorObserver.ttfdStopwatch?.reset(); + } + SentryNavigatorObserver.ttfdSpan?.finish(); + } @internal static SentryNative? get native => _native; diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index ee722f8e9e..727fc7f6c0 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -223,6 +223,10 @@ class SentryFlutterOptions extends SentryOptions { /// Read timeout. This will only be synced to the Android native SDK. Duration readTimeout = Duration(seconds: 5); + /// Enable or disable the tracing of time to full display. + /// This feature requires using the [Routing Instrumentation](https://docs.sentry.io/platforms/flutter/integrations/routing-instrumentation/). + bool enableTimeToFullDisplayTracing = false; + /// By using this, you are disabling native [Breadcrumb] tracking and instead /// you are just tracking [Breadcrumb]s which result from events available /// in the current Flutter environment. From 6a707dad549393255b715e2de707196d34f2aa97 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 6 Feb 2024 14:43:45 +0100 Subject: [PATCH 04/47] use epoch --- .../native_app_start_event_processor.dart | 1 + .../navigation/sentry_navigator_observer.dart | 23 +++++++++++++++---- flutter/lib/src/sentry_flutter.dart | 19 ++++++--------- flutter/lib/src/sentry_flutter_options.dart | 2 +- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index a7abe62e05..447b60b714 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -39,6 +39,7 @@ class NativeAppStartEventProcessor implements EventProcessor { if (measurement.value >= _maxAppStartMillis) { return event; } + print('hello app'); event.measurements[measurement.name] = measurement; } return event; diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 6e2f7dd99e..d38f85a512 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -127,7 +127,7 @@ class SentryNavigatorObserver extends RouteObserver> { SchedulerBinding.instance.addPostFrameCallback((timeStamp) { approximationEndTimestamp = DateTime.now(); approximationDurationMillis = - approximationEndTimestamp!.millisecond - startTime.millisecond; + approximationEndTimestamp!.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch; }); SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { @@ -139,6 +139,7 @@ class SentryNavigatorObserver extends RouteObserver> { 'time_to_initial_display', approximationDurationMillis!, unit: DurationSentryMeasurementUnit.milliSecond); ttidSpan?.finish(endTimestamp: approximationEndTimestamp!); + print('finished already'); }); } @@ -276,17 +277,29 @@ class SentryNavigatorObserver extends RouteObserver> { return; } - startTime = DateTime.now(); - ttidSpan = _transaction2?.startChild('ui.load.initial_display', description: '$name initial display'); - ttidSpan?.origin = 'auto.ui.time_to_display'; + if (name == 'root ("/")') { + // root ttid spans have to align with app start + // so the ttid instrumentation needs to be different here. + startTime = DateTime.now(); + ttidSpan = _transaction2?.startChild('ui.load.initial_display', description: '$name initial display', startTimestamp: startTime); + ttidSpan?.origin = 'auto.ui.time_to_display'; + } else { + startTime = DateTime.now(); + ttidSpan = _transaction2?.startChild('ui.load.initial_display', description: '$name initial display', startTimestamp: startTime); + ttidSpan?.origin = 'auto.ui.time_to_display'; + } // TODO: Needs to finish max within 30 seconds // If timeout exceeds then it will finish with status deadline exceeded // What to do if root also has TTFD but it's not finished yet and we start navigating to another? // How to track the time that 30 sec have passed? + // + // temporarily disable ttfd for root since it somehow swallows other spans + // e.g the complex operation span in autoclosescreen if ((_hub.options as SentryFlutterOptions).enableTimeToFullDisplayTracing && name != 'root ("/")') { ttfdStopwatch = Stopwatch()..start(); - ttfdSpan = _transaction2?.startChild('ui.load.full_display', description: '$name full display'); + ttfdStartTime = DateTime.now(); + ttfdSpan = _transaction2?.startChild('ui.load.full_display', description: '$name full display', startTimestamp: ttfdStartTime); } if (arguments != null) { diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index efe3305efa..ea1f9b2532 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -235,25 +235,20 @@ mixin SentryFlutter { if (!SentryDisplayTracker().reportManual(routeName)) { SentryNavigatorObserver.transaction2?.setMeasurement( 'time_to_initial_display', - endTime.millisecond - SentryNavigatorObserver.startTime.millisecond, + endTime.millisecondsSinceEpoch - SentryNavigatorObserver.startTime.millisecondsSinceEpoch, unit: DurationSentryMeasurementUnit.milliSecond); - - SentryNavigatorObserver.ttidSpan?.setTag('measurement', 'manual'); SentryNavigatorObserver.ttidSpan?.finish(endTimestamp: endTime); } } /// Reports the time it took for the screen to be fully displayed. static void reportFullDisplay() { - if (SentryNavigatorObserver.ttfdStopwatch?.elapsedMilliseconds != null) { - SentryNavigatorObserver.ttfdStopwatch?.stop(); - SentryNavigatorObserver.ttfdSpan?.setMeasurement( - 'time_to_full_display', - SentryNavigatorObserver.ttfdStopwatch!.elapsedMilliseconds, - unit: DurationSentryMeasurementUnit.milliSecond); - SentryNavigatorObserver.ttfdStopwatch?.reset(); - } - SentryNavigatorObserver.ttfdSpan?.finish(); + final endTime = DateTime.now(); + SentryNavigatorObserver.ttfdSpan?.setMeasurement( + 'time_to_full_display', + endTime.millisecondsSinceEpoch - SentryNavigatorObserver.ttfdStartTime.millisecondsSinceEpoch, + unit: DurationSentryMeasurementUnit.milliSecond); + SentryNavigatorObserver.ttfdSpan?.finish(endTimestamp: endTime); } @internal diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 727fc7f6c0..a753ad2b81 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -223,7 +223,7 @@ class SentryFlutterOptions extends SentryOptions { /// Read timeout. This will only be synced to the Android native SDK. Duration readTimeout = Duration(seconds: 5); - /// Enable or disable the tracing of time to full display. + /// Enable or disable the tracing of time to full display (TTFD). /// This feature requires using the [Routing Instrumentation](https://docs.sentry.io/platforms/flutter/integrations/routing-instrumentation/). bool enableTimeToFullDisplayTracing = false; From 51d6c6dfc6e48d85024c1ab9876220ace7af8369 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 6 Feb 2024 22:35:34 +0100 Subject: [PATCH 05/47] remove duration --- flutter/example/lib/auto_close_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart index 9fa368700a..ef42a08a4f 100644 --- a/flutter/example/lib/auto_close_screen.dart +++ b/flutter/example/lib/auto_close_screen.dart @@ -27,7 +27,6 @@ class AutoCloseScreenState extends State { description: 'running a $delayInSeconds seconds operation'); await Future.delayed(const Duration(seconds: delayInSeconds)); await childSpan?.finish(); - await Future.delayed(const Duration(seconds: 2)); SentryFlutter.reportFullDisplay(); // ignore: use_build_context_synchronously Navigator.of(context).pop(); From 4c976b37953197846454659e5fedc2fad89c4a4c Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 8 Feb 2024 14:59:05 +0100 Subject: [PATCH 06/47] update --- flutter/example/lib/auto_close_screen.dart | 2 +- flutter/example/lib/main.dart | 3 + .../native_app_start_event_processor.dart | 2 +- .../native_app_start_integration.dart | 65 +++++++++++++++- .../navigation/sentry_navigator_observer.dart | 75 +++++++++++-------- flutter/lib/src/sentry_flutter.dart | 1 + 6 files changed, 112 insertions(+), 36 deletions(-) diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart index ef42a08a4f..51fe4a809f 100644 --- a/flutter/example/lib/auto_close_screen.dart +++ b/flutter/example/lib/auto_close_screen.dart @@ -29,7 +29,7 @@ class AutoCloseScreenState extends State { await childSpan?.finish(); SentryFlutter.reportFullDisplay(); // ignore: use_build_context_synchronously - Navigator.of(context).pop(); + // Navigator.of(context).pop(); } @override diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 250a900b32..0c584582a7 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -72,6 +72,7 @@ Future setupSentry(AppRunner appRunner, String dsn, options.screenshotQuality = SentryScreenshotQuality.low; options.attachViewHierarchy = true; options.enableTimeToFullDisplayTracing = true; + options.spotlight = Spotlight(enabled: true); // We can enable Sentry debug logging during development. This is likely // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. @@ -99,8 +100,10 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { + @override Widget build(BuildContext context) { + SentryFlutter.reportFullDisplay(); return feedback.BetterFeedback( child: ChangeNotifierProvider( create: (_) => ThemeProvider(), diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index 447b60b714..db4bd96285 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -39,7 +39,7 @@ class NativeAppStartEventProcessor implements EventProcessor { if (measurement.value >= _maxAppStartMillis) { return event; } - print('hello app'); + event.measurements[measurement.name] = measurement; } return event; diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 47bf79dff4..fc95000d74 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -1,6 +1,7 @@ import 'package:flutter/scheduler.dart'; import 'package:sentry/sentry.dart'; +import '../../sentry_flutter.dart'; import '../sentry_flutter_options.dart'; import '../native/sentry_native.dart'; import '../event_processor/native_app_start_event_processor.dart'; @@ -21,9 +22,71 @@ class NativeAppStartIntegration extends Integration { options.logger(SentryLevel.debug, 'Scheduler binding is null. Can\'t auto detect app start time.'); } else { - schedulerBinding.addPostFrameCallback((timeStamp) { + schedulerBinding.addPostFrameCallback((timeStamp) async { // ignore: invalid_use_of_internal_member _native.appStartEnd = options.clock(); + + final appStartEnd = _native.appStartEnd; + + if (_native.appStartEnd != null && !_native!.didFetchAppStart) { + print('fetch app start'); + final nativeAppStart = await _native!.fetchNativeAppStart(); + if (nativeAppStart == null) { + return; + } + final measurement = nativeAppStart.toMeasurement(appStartEnd!); + // We filter out app start more than 60s. + // This could be due to many different reasons. + // If you do the manual init and init the SDK too late and it does not + // compute the app start end in the very first Screen. + // If the process starts but the App isn't in the foreground. + // If the system forked the process earlier to accelerate the app start. + // And some unknown reasons that could not be reproduced. + // We've seen app starts with hours, days and even months. + if (measurement.value >= 60000) { + return; + } + + final appStartDateTime = DateTime.fromMillisecondsSinceEpoch( + nativeAppStart.appStartTime.toInt()); + + final transactionContext2 = SentryTransactionContext( + 'root ("/")', + 'ui.load', + transactionNameSource: SentryTransactionNameSource.component, + // ignore: invalid_use_of_internal_member + origin: SentryTraceOrigins.autoNavigationRouteObserver, + ); + + final transaction2 = hub.startTransactionWithContext( + transactionContext2, + waitForChildren: true, + autoFinishAfter: Duration(seconds: 3), + trimEnd: true, + startTimestamp: appStartDateTime, + onFinish: (transaction) async { + final nativeFrames = await _native + ?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } + } + }); + + final ttidSpan = transaction2.startChild('ui.load.initial_display', startTimestamp: appStartDateTime); + await ttidSpan.finish(endTimestamp: appStartEnd); + + SentryNavigatorObserver.ttfdSpan = transaction2.startChild('ui.load.full_display', startTimestamp: appStartDateTime); + + print('end of the road'); + } }); } } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index d38f85a512..b82a3b1f6e 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -4,6 +4,7 @@ import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../event_processor/flutter_enricher_event_processor.dart'; +import '../event_processor/native_app_start_event_processor.dart'; import '../native/sentry_native.dart'; /// This key must be used so that the web interface displays the events nicely @@ -127,19 +128,21 @@ class SentryNavigatorObserver extends RouteObserver> { SchedulerBinding.instance.addPostFrameCallback((timeStamp) { approximationEndTimestamp = DateTime.now(); approximationDurationMillis = - approximationEndTimestamp!.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch; + approximationEndTimestamp!.millisecondsSinceEpoch - + startTime.millisecondsSinceEpoch; }); SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { if (routeName == '/') { // TODO: Does TTID have to be completely in line with app start? // If yes, how do we access the appstart metrics + } else { + _transaction2?.setMeasurement( + 'time_to_initial_display', approximationDurationMillis!, + unit: DurationSentryMeasurementUnit.milliSecond); + ttidSpan?.finish(endTimestamp: approximationEndTimestamp!); + print('finished already'); } - _transaction2?.setMeasurement( - 'time_to_initial_display', approximationDurationMillis!, - unit: DurationSentryMeasurementUnit.milliSecond); - ttidSpan?.finish(endTimestamp: approximationEndTimestamp!); - print('finished already'); }); } @@ -249,27 +252,31 @@ class SentryNavigatorObserver extends RouteObserver> { origin: SentryTraceOrigins.autoNavigationRouteObserver, ); - _transaction2 = _hub.startTransactionWithContext( - transactionContext2, - waitForChildren: true, - autoFinishAfter: _autoFinishAfter, - trimEnd: true, - onFinish: (transaction) async { - final nativeFrames = await _native - ?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, - ); + if (name == 'root ("/")') { + + } else { + _transaction2 = _hub.startTransactionWithContext( + transactionContext2, + waitForChildren: true, + autoFinishAfter: _autoFinishAfter, + trimEnd: true, + onFinish: (transaction) async { + final nativeFrames = await _native + ?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } } - } - }, - ); + }, + ); + } // if _enableAutoTransactions is enabled but there's no traces sample rate if (_transaction is NoOpSentrySpan) { @@ -280,12 +287,13 @@ class SentryNavigatorObserver extends RouteObserver> { if (name == 'root ("/")') { // root ttid spans have to align with app start // so the ttid instrumentation needs to be different here. - startTime = DateTime.now(); - ttidSpan = _transaction2?.startChild('ui.load.initial_display', description: '$name initial display', startTimestamp: startTime); - ttidSpan?.origin = 'auto.ui.time_to_display'; + // startTime = DateTime.now(); + // ttidSpan = _transaction2?.startChild('ui.load.initial_display', description: '$name initial display', startTimestamp: startTime); + // ttidSpan?.origin = 'auto.ui.time_to_display'; } else { startTime = DateTime.now(); - ttidSpan = _transaction2?.startChild('ui.load.initial_display', description: '$name initial display', startTimestamp: startTime); + ttidSpan = _transaction2?.startChild('ui.load.initial_display', + description: '$name initial display', startTimestamp: startTime); ttidSpan?.origin = 'auto.ui.time_to_display'; } @@ -296,10 +304,11 @@ class SentryNavigatorObserver extends RouteObserver> { // // temporarily disable ttfd for root since it somehow swallows other spans // e.g the complex operation span in autoclosescreen - if ((_hub.options as SentryFlutterOptions).enableTimeToFullDisplayTracing && name != 'root ("/")') { - ttfdStopwatch = Stopwatch()..start(); + if ((_hub.options as SentryFlutterOptions).enableTimeToFullDisplayTracing && + name != 'root ("/")') { ttfdStartTime = DateTime.now(); - ttfdSpan = _transaction2?.startChild('ui.load.full_display', description: '$name full display', startTimestamp: ttfdStartTime); + ttfdSpan = _transaction2?.startChild('ui.load.full_display', + description: '$name full display', startTimestamp: ttfdStartTime); } if (arguments != null) { diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index ea1f9b2532..cb733efa06 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -244,6 +244,7 @@ mixin SentryFlutter { /// Reports the time it took for the screen to be fully displayed. static void reportFullDisplay() { final endTime = DateTime.now(); + print('end of the road2'); SentryNavigatorObserver.ttfdSpan?.setMeasurement( 'time_to_full_display', endTime.millisecondsSinceEpoch - SentryNavigatorObserver.ttfdStartTime.millisecondsSinceEpoch, From 75a76fc464d681d03503d5e0ad4dfb2c184d2408 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 9 Feb 2024 14:51:50 +0100 Subject: [PATCH 07/47] update poc --- flutter/example/lib/auto_close_screen.dart | 2 +- flutter/example/lib/main.dart | 2 +- .../native_app_start_event_processor.dart | 26 +- .../native_app_start_integration.dart | 102 ++--- .../navigation/sentry_navigator_observer.dart | 359 ++++++++++++++++-- flutter/lib/src/sentry_flutter.dart | 22 +- flutter/lib/src/sentry_widget.dart | 3 +- 7 files changed, 402 insertions(+), 114 deletions(-) diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart index 51fe4a809f..7caa09a836 100644 --- a/flutter/example/lib/auto_close_screen.dart +++ b/flutter/example/lib/auto_close_screen.dart @@ -27,7 +27,7 @@ class AutoCloseScreenState extends State { description: 'running a $delayInSeconds seconds operation'); await Future.delayed(const Duration(seconds: delayInSeconds)); await childSpan?.finish(); - SentryFlutter.reportFullDisplay(); + SentryFlutter.reportFullyDisplayed(); // ignore: use_build_context_synchronously // Navigator.of(context).pop(); } diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 0c584582a7..a347d87af9 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -103,7 +103,7 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - SentryFlutter.reportFullDisplay(); + // SentryFlutter.reportFullDisplay(); return feedback.BetterFeedback( child: ChangeNotifierProvider( create: (_) => ThemeProvider(), diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index db4bd96285..0a83ceab4b 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; +import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; /// EventProcessor that enriches [SentryTransaction] objects with app start @@ -18,28 +19,9 @@ class NativeAppStartEventProcessor implements EventProcessor { @override Future apply(SentryEvent event, {Hint? hint}) async { - final appStartEnd = _native.appStartEnd; - - if (appStartEnd != null && - event is SentryTransaction && - !_native.didFetchAppStart) { - final nativeAppStart = await _native.fetchNativeAppStart(); - if (nativeAppStart == null) { - return event; - } - final measurement = nativeAppStart.toMeasurement(appStartEnd); - // We filter out app start more than 60s. - // This could be due to many different reasons. - // If you do the manual init and init the SDK too late and it does not - // compute the app start end in the very first Screen. - // If the process starts but the App isn't in the foreground. - // If the system forked the process earlier to accelerate the app start. - // And some unknown reasons that could not be reproduced. - // We've seen app starts with hours, days and even months. - if (measurement.value >= _maxAppStartMillis) { - return event; - } - + final appStartInfo = AppStartTracker().appStartInfo; + if (appStartInfo != null && event is SentryTransaction) { + final measurement = appStartInfo.measurement; event.measurements[measurement.name] = measurement; } return event; diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index fc95000d74..d767e7d28b 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -1,4 +1,5 @@ import 'package:flutter/scheduler.dart'; +import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; import '../../sentry_flutter.dart'; @@ -23,18 +24,14 @@ class NativeAppStartIntegration extends Integration { 'Scheduler binding is null. Can\'t auto detect app start time.'); } else { schedulerBinding.addPostFrameCallback((timeStamp) async { + final appStartEnd = options.clock(); // ignore: invalid_use_of_internal_member - _native.appStartEnd = options.clock(); + _native.appStartEnd = appStartEnd; - final appStartEnd = _native.appStartEnd; + if (!_native.didFetchAppStart) { + final nativeAppStart = await _native.fetchNativeAppStart(); + final measurement = nativeAppStart?.toMeasurement(appStartEnd!); - if (_native.appStartEnd != null && !_native!.didFetchAppStart) { - print('fetch app start'); - final nativeAppStart = await _native!.fetchNativeAppStart(); - if (nativeAppStart == null) { - return; - } - final measurement = nativeAppStart.toMeasurement(appStartEnd!); // We filter out app start more than 60s. // This could be due to many different reasons. // If you do the manual init and init the SDK too late and it does not @@ -43,49 +40,23 @@ class NativeAppStartIntegration extends Integration { // If the system forked the process earlier to accelerate the app start. // And some unknown reasons that could not be reproduced. // We've seen app starts with hours, days and even months. - if (measurement.value >= 60000) { + if (nativeAppStart == null || + measurement == null || + measurement.value >= 60000) { + AppStartTracker().setAppStartInfo(null); return; } - final appStartDateTime = DateTime.fromMillisecondsSinceEpoch( - nativeAppStart.appStartTime.toInt()); - - final transactionContext2 = SentryTransactionContext( - 'root ("/")', - 'ui.load', - transactionNameSource: SentryTransactionNameSource.component, - // ignore: invalid_use_of_internal_member - origin: SentryTraceOrigins.autoNavigationRouteObserver, + final appStartInfo = AppStartInfo( + DateTime.fromMillisecondsSinceEpoch( + nativeAppStart.appStartTime.toInt()), + appStartEnd, + measurement, ); - final transaction2 = hub.startTransactionWithContext( - transactionContext2, - waitForChildren: true, - autoFinishAfter: Duration(seconds: 3), - trimEnd: true, - startTimestamp: appStartDateTime, - onFinish: (transaction) async { - final nativeFrames = await _native - ?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, - ); - } - } - }); - - final ttidSpan = transaction2.startChild('ui.load.initial_display', startTimestamp: appStartDateTime); - await ttidSpan.finish(endTimestamp: appStartEnd); - - SentryNavigatorObserver.ttfdSpan = transaction2.startChild('ui.load.full_display', startTimestamp: appStartDateTime); - - print('end of the road'); + AppStartTracker().setAppStartInfo(appStartInfo); + } else { + AppStartTracker().setAppStartInfo(null); } }); } @@ -99,3 +70,40 @@ class NativeAppStartIntegration extends Integration { /// Used to provide scheduler binding at call time. typedef SchedulerBindingProvider = SchedulerBinding? Function(); + +@internal +class AppStartInfo { + final DateTime start; + final DateTime end; + final SentryMeasurement measurement; + + AppStartInfo(this.start, this.end, this.measurement); +} + +@internal +class AppStartTracker { + static final AppStartTracker _instance = AppStartTracker._internal(); + + factory AppStartTracker() => _instance; + + AppStartInfo? _appStartInfo; + + AppStartInfo? get appStartInfo => _appStartInfo; + Function(AppStartInfo?)? _callback; + + AppStartTracker._internal(); + + void setAppStartInfo(AppStartInfo? appStartInfo) { + _appStartInfo = appStartInfo; + _notifyObserver(); + } + + void onAppStartComplete(Function(AppStartInfo?) callback) { + _callback = callback; + _callback?.call(_appStartInfo); + } + + void _notifyObserver() { + _callback?.call(_appStartInfo); + } +} diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index b82a3b1f6e..749cc5d5dd 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../event_processor/flutter_enricher_event_processor.dart'; import '../event_processor/native_app_start_event_processor.dart'; +import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; /// This key must be used so that the web interface displays the events nicely @@ -77,6 +78,11 @@ class SentryNavigatorObserver extends RouteObserver> { _native = SentryFlutter.native { if (enableAutoTransactions) { // ignore: invalid_use_of_internal_member + // _timingManager = NavigationTimingManager( + // hub: _hub, + // native: _native, + // autoFinishAfter: autoFinishAfter, + // ); _hub.options.sdk.addIntegration('UINavigationTracing'); } } @@ -88,10 +94,14 @@ class SentryNavigatorObserver extends RouteObserver> { final RouteNameExtractor? _routeNameExtractor; final AdditionalInfoExtractor? _additionalInfoProvider; final SentryNative? _native; + late final NavigationTimingManager? _timingManager; static ISentrySpan? _transaction2; static ISentrySpan? get transaction2 => _transaction2; + static final Map ttidSpanMap = {}; + static final Map ttfdSpanMap = {}; + ISentrySpan? _transaction; static String? _currentRouteName; @@ -119,6 +129,8 @@ class SentryNavigatorObserver extends RouteObserver> { _finishTransaction(); _startTransaction(route); + NavigationTimingManager2().startMeasurement(_getRouteName(route) ?? 'Unknown'); + final startTime = DateTime.now(); // Start timing DateTime? approximationEndTimestamp; @@ -132,26 +144,67 @@ class SentryNavigatorObserver extends RouteObserver> { startTime.millisecondsSinceEpoch; }); - SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { - if (routeName == '/') { - // TODO: Does TTID have to be completely in line with app start? - // If yes, how do we access the appstart metrics - } else { - _transaction2?.setMeasurement( - 'time_to_initial_display', approximationDurationMillis!, - unit: DurationSentryMeasurementUnit.milliSecond); - ttidSpan?.finish(endTimestamp: approximationEndTimestamp!); - print('finished already'); - } - }); - } - - void freezeUIForSeconds(int seconds) { - var sw = Stopwatch()..start(); - while (sw.elapsed.inSeconds < seconds) { - // This loop will block the UI thread. + // Approximation started + // SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { + // print('was ned') + // if (routeName == '/') { + // AppStartTracker().onAppStartComplete((appStartInfo) { + // final transactionContext2 = SentryTransactionContext( + // 'root ("/")', + // 'ui.load', + // transactionNameSource: SentryTransactionNameSource.component, + // // ignore: invalid_use_of_internal_member + // origin: SentryTraceOrigins.autoNavigationRouteObserver, + // ); + // final startTime = appStartInfo?.start ?? DateTime.now(); + // final endTime = appStartInfo?.end ?? approximationEndTimestamp!; + // final duration = + // appStartInfo?.end.difference(startTime).inMilliseconds ?? + // approximationDurationMillis!; + // _transaction2 = _hub.startTransactionWithContext( + // transactionContext2, + // waitForChildren: true, + // autoFinishAfter: _autoFinishAfter, + // trimEnd: true, + // startTimestamp: startTime, + // onFinish: (transaction) async { + // final nativeFrames = await _native + // ?.endNativeFramesCollection(transaction.context.traceId); + // if (nativeFrames != null) { + // final measurements = nativeFrames.toMeasurements(); + // for (final item in measurements.entries) { + // final measurement = item.value; + // transaction.setMeasurement( + // item.key, + // measurement.value, + // unit: measurement.unit, + // ); + // } + // } + // }, + // ); + // _transaction2?.setMeasurement('time_to_initial_display', duration, + // unit: DurationSentryMeasurementUnit.milliSecond); + // final ttidSpan = _transaction2?.startChild('ui.load.initial_display', + // description: 'root ("/")', startTimestamp: startTime); + // ttidSpan?.finish(endTimestamp: endTime); + // }); + // // Only finish ttidSpan if we get the appStartTime and appStartEnd + // } else { + // _transaction2?.setMeasurement( + // 'time_to_initial_display', approximationDurationMillis!, + // unit: DurationSentryMeasurementUnit.milliSecond); + // ttidSpanMap[routeName] + // ?.finish(endTimestamp: approximationEndTimestamp!); + // } + // }); + + try { + // ignore: invalid_use_of_internal_member + _hub.options.sdk.addIntegration('UINavigationTracing'); + } on Exception catch (e) { + print(e); } - sw.stop(); } @override @@ -236,13 +289,6 @@ class SentryNavigatorObserver extends RouteObserver> { if (name == '/') { name = 'root ("/")'; } - // final transactionContext = SentryTransactionContext( - // name, - // 'navigation', - // transactionNameSource: SentryTransactionNameSource.component, - // // ignore: invalid_use_of_internal_member - // origin: SentryTraceOrigins.autoNavigationRouteObserver, - // ); final transactionContext2 = SentryTransactionContext( name, @@ -252,9 +298,7 @@ class SentryNavigatorObserver extends RouteObserver> { origin: SentryTraceOrigins.autoNavigationRouteObserver, ); - if (name == 'root ("/")') { - - } else { + if (name != 'root ("/")') { _transaction2 = _hub.startTransactionWithContext( transactionContext2, waitForChildren: true, @@ -285,16 +329,13 @@ class SentryNavigatorObserver extends RouteObserver> { } if (name == 'root ("/")') { - // root ttid spans have to align with app start - // so the ttid instrumentation needs to be different here. - // startTime = DateTime.now(); - // ttidSpan = _transaction2?.startChild('ui.load.initial_display', description: '$name initial display', startTimestamp: startTime); - // ttidSpan?.origin = 'auto.ui.time_to_display'; } else { startTime = DateTime.now(); - ttidSpan = _transaction2?.startChild('ui.load.initial_display', + + final ttidSpan = _transaction2?.startChild('ui.load.initial_display', description: '$name initial display', startTimestamp: startTime); ttidSpan?.origin = 'auto.ui.time_to_display'; + ttidSpanMap[name] = ttidSpan!; } // TODO: Needs to finish max within 30 seconds @@ -306,6 +347,7 @@ class SentryNavigatorObserver extends RouteObserver> { // e.g the complex operation span in autoclosescreen if ((_hub.options as SentryFlutterOptions).enableTimeToFullDisplayTracing && name != 'root ("/")') { + print('ttfd'); ttfdStartTime = DateTime.now(); ttfdSpan = _transaction2?.startChild('ui.load.full_display', description: '$name full display', startTimestamp: ttfdStartTime); @@ -404,3 +446,250 @@ extension NativeFramesMeasurement on NativeFrames { }; } } + +class NavigationTimingManager { + late final Hub _hub; + late final SentryNative? _native; + late final Duration _autoFinishAfter; + ISentrySpan? _currentTransaction; + DateTime? _transactionStartTime; + + TTIDStrategy ttidStrategy = ApproximationTTIDStrategy(); + + static final NavigationTimingManager _instance = + NavigationTimingManager._internal(); + + factory NavigationTimingManager( + {required Hub hub, + SentryNative? native, + required Duration autoFinishAfter}) { + _instance._hub = hub; + _instance._native = native; + _instance._autoFinishAfter = autoFinishAfter; + return _instance; + } + + NavigationTimingManager._internal( + {Hub? hub, SentryNative? native, Duration? autoFinishAfter}) + : _hub = hub ?? HubAdapter(), + _native = native, + _autoFinishAfter = autoFinishAfter ?? Duration(seconds: 3); + + void startTransaction(String routeName, {bool isRootRoute = false}) { + final transactionContext = SentryTransactionContext( + routeName, + 'ui.load', + transactionNameSource: SentryTransactionNameSource.component, + origin: SentryTraceOrigins.autoNavigationRouteObserver, + ); + + _currentTransaction = _hub.startTransactionWithContext( + transactionContext, + waitForChildren: true, + autoFinishAfter: _autoFinishAfter, + trimEnd: true, + onFinish: _onTransactionFinish, + ); + + _transactionStartTime = DateTime.now(); + if (isRootRoute) { + _handleRootRouteTTID(); + } + } + + void finishTransaction() { + _currentTransaction?.status ??= SpanStatus.ok(); + _currentTransaction?.finish(endTimestamp: DateTime.now()); + } + + Future _onTransactionFinish(ISentrySpan transaction) async { + final nativeFrames = + await _native?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } + } + } + + void _handleRootRouteTTID() { + // Special handling for TTID measurement on the root route. + } +} + +@internal +class NavigationTimingManager2 { + static NavigationTimingManager2? _instance; + final Hub _hub; + final Duration _autoFinishAfter; + final SentryNative? _native; + + static final Map ttidSpanMap = {}; + static final Map ttfdSpanMap = {}; + + TTIDStrategy _strategy; + + NavigationTimingManager2._({ + Hub? hub, + Duration autoFinishAfter = const Duration(seconds: 3), + SentryNative? native, + }) : _hub = hub ?? HubAdapter(), + _autoFinishAfter = autoFinishAfter, + _native = native, + _strategy = ApproximationTTIDStrategy(); + + factory NavigationTimingManager2({ + Hub? hub, + Duration autoFinishAfter = const Duration(seconds: 3), + }) { + _instance ??= NavigationTimingManager2._( + hub: hub ?? HubAdapter(), + autoFinishAfter: autoFinishAfter, + native: SentryFlutter.native, + ); + + return _instance!; + } + + void startMeasurement(String routeName) { + SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { + // If we got inside this block, it means manual instrumentation was not done + // handle non-root approximation ttid + // non-root approximation ttid only finishes the span here and needs to get + // the duration and end of the approximation which we got from addPostFrameCallback + // e.g ttidSpan.finish() + if (_strategy is ApproximationTTIDStrategy && routeName != '/') { + print('start measureing'); + _strategy.startMeasurement(routeName); + } + }); + + } + + void endMeasurement() { + final endTime = DateTime.now(); + _strategy.endMeasurement(endTime: endTime); + } + + void startTransaction(String routeName) { + final transactionContext = SentryTransactionContext( + routeName, + 'ui.load', + transactionNameSource: SentryTransactionNameSource.component, + origin: SentryTraceOrigins.autoNavigationRouteObserver, + ); + + final approximationStartTime = DateTime.now(); + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + final approximationEndTime = DateTime.now(); + // this marks the end of the approximation + }); + + if (isRootRoute(routeName)) { + handleRootTTID(); + } else { + handleNonRootTTID(transactionContext); + } + } + + void handleNonRootTTID(SentryTransactionContext transactionContext) { + _hub.startTransactionWithContext( + transactionContext, + waitForChildren: true, + autoFinishAfter: _autoFinishAfter, + trimEnd: true, + onFinish: _onTransactionFinish, + ); + } + + void handleRootTTID() {} + + bool isRootRoute(String routeName) { + return routeName == '/' || routeName == 'root ("/")'; + } + + Future _onTransactionFinish(ISentrySpan transaction) async { + final nativeFrames = + await _native?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } + } + } + + void handleTTIDStrategy(String routeName) { + // SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { + // // If we got inside this block, it means manual instrumentation was not done + // // handle non-root approximation ttid + // // non-root approximation ttid only finishes the span here and needs to get + // // the duration and end of the approximation which we got from addPostFrameCallback + // // e.g ttidSpan.finish() + // _strategy.startMeasurement(routeName); + // }); + } + + // This is the manual approach + static void reportInitiallyDisplayed() {} + + static void reportFullyDisplayed() {} +} + +// Abstract strategy for TTID measurement +abstract class TTIDStrategy { + void startMeasurement(String routeName); + + void endMeasurement({required DateTime endTime, String? routeName}); +} + +// Implements approximation strategy for TTID +class ApproximationTTIDStrategy implements TTIDStrategy { + DateTime approximationStartTime = DateTime.now(); + DateTime approximationEndTime = DateTime.now(); + + @override + void startMeasurement(String routeName) { + approximationStartTime = DateTime.now(); + + final transaction = Sentry.getSpan(); + // SentryNavigatorObserver.ttidSpanMap[routeName] = + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + print('approximation end'); + endMeasurement(endTime: DateTime.now(), routeName: routeName); + }); + } + + @override + void endMeasurement({required DateTime endTime, String? routeName}) { + SentryNavigatorObserver.ttidSpanMap[routeName]?.finish( + endTimestamp: endTime, + ); + } +} + +// Implements manual strategy for TTID +class ManualTTIDStrategy implements TTIDStrategy { + @override + void startMeasurement(String routeName) { + print("Manual instrumentation started for $routeName"); + // Manual instrumentation logic + } + + @override + void endMeasurement({required DateTime endTime, String? routeName}) { + print("Manual instrumentation ended for $routeName at $endTime"); + // Calculate and log manual measurement + } +} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index cb733efa06..f74dea74c0 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -233,27 +233,35 @@ mixin SentryFlutter { final routeName = ModalRoute.of(context)?.settings.name ?? 'Unknown'; final endTime = DateTime.now(); if (!SentryDisplayTracker().reportManual(routeName)) { - SentryNavigatorObserver.transaction2?.setMeasurement( - 'time_to_initial_display', - endTime.millisecondsSinceEpoch - SentryNavigatorObserver.startTime.millisecondsSinceEpoch, + final transaction = Sentry.getSpan(); + final duration = endTime.millisecondsSinceEpoch - + SentryNavigatorObserver.startTime.millisecondsSinceEpoch; + transaction?.setMeasurement('time_to_initial_display', duration, unit: DurationSentryMeasurementUnit.milliSecond); - SentryNavigatorObserver.ttidSpan?.finish(endTimestamp: endTime); + if (routeName == '/') { + print('is root screen manual'); + } else { + SentryNavigatorObserver.ttidSpanMap[routeName]?.finish( + endTimestamp: endTime, + ); + } } } /// Reports the time it took for the screen to be fully displayed. - static void reportFullDisplay() { + static void reportFullyDisplayed() { final endTime = DateTime.now(); - print('end of the road2'); SentryNavigatorObserver.ttfdSpan?.setMeasurement( 'time_to_full_display', - endTime.millisecondsSinceEpoch - SentryNavigatorObserver.ttfdStartTime.millisecondsSinceEpoch, + endTime.millisecondsSinceEpoch - + SentryNavigatorObserver.ttfdStartTime.millisecondsSinceEpoch, unit: DurationSentryMeasurementUnit.milliSecond); SentryNavigatorObserver.ttfdSpan?.finish(endTimestamp: endTime); } @internal static SentryNative? get native => _native; + @internal static set native(SentryNative? value) => _native = value; static SentryNative? _native; diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 7db077a3e5..49381c9083 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; import '../sentry_flutter.dart'; /// This widget serves as a wrapper to include Sentry widgets such @@ -64,7 +65,7 @@ class SentryDisplayTracker { void startTimeout(String routeName, Function onTimeout) { _timers[routeName]?.cancel(); // Cancel any existing timer - _timers[routeName] = Timer(Duration(seconds: 2), () { + _timers[routeName] = Timer(Duration(seconds: 1), () { // Don't send if we already received a manual report or if we're on the root route e.g App start. if (!(_manualReportReceived[routeName] ?? false)) { onTimeout(); From 7f2ab85e751c355cbfde0f6a600c253fb5e1eee1 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 9 Feb 2024 15:55:01 +0100 Subject: [PATCH 08/47] update --- .../navigation/sentry_navigator_observer.dart | 294 ++++++++---------- 1 file changed, 128 insertions(+), 166 deletions(-) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 749cc5d5dd..b122566c6a 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -15,9 +15,9 @@ const _navigationKey = 'navigation'; typedef RouteNameExtractor = RouteSettings? Function(RouteSettings? settings); typedef AdditionalInfoExtractor = Map? Function( - RouteSettings? from, - RouteSettings? to, -); + RouteSettings? from, + RouteSettings? to, + ); /// This is a navigation observer to record navigational breadcrumbs. /// For now it only records navigation events and no gestures. @@ -69,7 +69,8 @@ class SentryNavigatorObserver extends RouteObserver> { bool setRouteNameAsTransaction = false, RouteNameExtractor? routeNameExtractor, AdditionalInfoExtractor? additionalInfoProvider, - }) : _hub = hub ?? HubAdapter(), + }) + : _hub = hub ?? HubAdapter(), _enableAutoTransactions = enableAutoTransactions, _autoFinishAfter = autoFinishAfter, _setRouteNameAsTransaction = setRouteNameAsTransaction, @@ -128,76 +129,10 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); - _startTransaction(route); - NavigationTimingManager2().startMeasurement(_getRouteName(route) ?? 'Unknown'); - final startTime = DateTime.now(); - - // Start timing - DateTime? approximationEndTimestamp; - int? approximationDurationMillis; - final routeName = _getRouteName(route); - - SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - approximationEndTimestamp = DateTime.now(); - approximationDurationMillis = - approximationEndTimestamp!.millisecondsSinceEpoch - - startTime.millisecondsSinceEpoch; - }); + // _startTransaction(route); - // Approximation started - // SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { - // print('was ned') - // if (routeName == '/') { - // AppStartTracker().onAppStartComplete((appStartInfo) { - // final transactionContext2 = SentryTransactionContext( - // 'root ("/")', - // 'ui.load', - // transactionNameSource: SentryTransactionNameSource.component, - // // ignore: invalid_use_of_internal_member - // origin: SentryTraceOrigins.autoNavigationRouteObserver, - // ); - // final startTime = appStartInfo?.start ?? DateTime.now(); - // final endTime = appStartInfo?.end ?? approximationEndTimestamp!; - // final duration = - // appStartInfo?.end.difference(startTime).inMilliseconds ?? - // approximationDurationMillis!; - // _transaction2 = _hub.startTransactionWithContext( - // transactionContext2, - // waitForChildren: true, - // autoFinishAfter: _autoFinishAfter, - // trimEnd: true, - // startTimestamp: startTime, - // onFinish: (transaction) async { - // final nativeFrames = await _native - // ?.endNativeFramesCollection(transaction.context.traceId); - // if (nativeFrames != null) { - // final measurements = nativeFrames.toMeasurements(); - // for (final item in measurements.entries) { - // final measurement = item.value; - // transaction.setMeasurement( - // item.key, - // measurement.value, - // unit: measurement.unit, - // ); - // } - // } - // }, - // ); - // _transaction2?.setMeasurement('time_to_initial_display', duration, - // unit: DurationSentryMeasurementUnit.milliSecond); - // final ttidSpan = _transaction2?.startChild('ui.load.initial_display', - // description: 'root ("/")', startTimestamp: startTime); - // ttidSpan?.finish(endTimestamp: endTime); - // }); - // // Only finish ttidSpan if we get the appStartTime and appStartEnd - // } else { - // _transaction2?.setMeasurement( - // 'time_to_initial_display', approximationDurationMillis!, - // unit: DurationSentryMeasurementUnit.milliSecond); - // ttidSpanMap[routeName] - // ?.finish(endTimestamp: approximationEndTimestamp!); - // } - // }); + NavigationTimingManager2().startMeasurement( + _getRouteName(route) ?? 'Unknown'); try { // ignore: invalid_use_of_internal_member @@ -328,8 +263,7 @@ class SentryNavigatorObserver extends RouteObserver> { return; } - if (name == 'root ("/")') { - } else { + if (name == 'root ("/")') {} else { startTime = DateTime.now(); final ttidSpan = _transaction2?.startChild('ui.load.initial_display', @@ -411,16 +345,16 @@ class RouteObserverBreadcrumb extends Breadcrumb { super.timestamp, Map? data, }) : super( - category: _navigationKey, - type: _navigationKey, - data: { - 'state': navigationType, - if (from != null) 'from': from, - if (fromArgs != null) 'from_arguments': fromArgs, - if (to != null) 'to': to, - if (toArgs != null) 'to_arguments': toArgs, - if (data != null) 'data': data, - }); + category: _navigationKey, + type: _navigationKey, + data: { + 'state': navigationType, + if (from != null) 'from': from, + if (fromArgs != null) 'from_arguments': fromArgs, + if (to != null) 'to': to, + if (toArgs != null) 'to_arguments': toArgs, + if (data != null) 'data': data, + }); static dynamic _formatArgs(Object? args) { if (args == null) { @@ -454,15 +388,14 @@ class NavigationTimingManager { ISentrySpan? _currentTransaction; DateTime? _transactionStartTime; - TTIDStrategy ttidStrategy = ApproximationTTIDStrategy(); + TTIDStrategy ttidStrategy; static final NavigationTimingManager _instance = - NavigationTimingManager._internal(); + NavigationTimingManager._internal(); - factory NavigationTimingManager( - {required Hub hub, - SentryNative? native, - required Duration autoFinishAfter}) { + factory NavigationTimingManager({required Hub hub, + SentryNative? native, + required Duration autoFinishAfter}) { _instance._hub = hub; _instance._native = native; _instance._autoFinishAfter = autoFinishAfter; @@ -473,7 +406,9 @@ class NavigationTimingManager { {Hub? hub, SentryNative? native, Duration? autoFinishAfter}) : _hub = hub ?? HubAdapter(), _native = native, - _autoFinishAfter = autoFinishAfter ?? Duration(seconds: 3); + _autoFinishAfter = autoFinishAfter ?? Duration(seconds: 3), + ttidStrategy = ApproximationTTIDStrategy( + hub!, native!, TransactionManager(hub, native)); void startTransaction(String routeName, {bool isRootRoute = false}) { final transactionContext = SentryTransactionContext( @@ -504,7 +439,7 @@ class NavigationTimingManager { Future _onTransactionFinish(ISentrySpan transaction) async { final nativeFrames = - await _native?.endNativeFramesCollection(transaction.context.traceId); + await _native?.endNativeFramesCollection(transaction.context.traceId); if (nativeFrames != null) { final measurements = nativeFrames.toMeasurements(); for (final item in measurements.entries) { @@ -539,10 +474,11 @@ class NavigationTimingManager2 { Hub? hub, Duration autoFinishAfter = const Duration(seconds: 3), SentryNative? native, - }) : _hub = hub ?? HubAdapter(), + }) + : _hub = hub ?? HubAdapter(), _autoFinishAfter = autoFinishAfter, _native = native, - _strategy = ApproximationTTIDStrategy(); + _strategy = ApproximationTTIDStrategy(hub!, native!, TransactionManager(hub, native)); factory NavigationTimingManager2({ Hub? hub, @@ -558,18 +494,7 @@ class NavigationTimingManager2 { } void startMeasurement(String routeName) { - SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { - // If we got inside this block, it means manual instrumentation was not done - // handle non-root approximation ttid - // non-root approximation ttid only finishes the span here and needs to get - // the duration and end of the approximation which we got from addPostFrameCallback - // e.g ttidSpan.finish() - if (_strategy is ApproximationTTIDStrategy && routeName != '/') { - print('start measureing'); - _strategy.startMeasurement(routeName); - } - }); - + _strategy.startMeasurement(routeName); } void endMeasurement() { @@ -577,7 +502,26 @@ class NavigationTimingManager2 { _strategy.endMeasurement(endTime: endTime); } - void startTransaction(String routeName) { + // This is the manual approach + static void reportInitiallyDisplayed() {} + + static void reportFullyDisplayed() {} +} + +// Abstract strategy for TTID measurement +abstract class TTIDStrategy { + void startMeasurement(String routeName); + + void endMeasurement({required DateTime endTime, String? routeName}); +} + +abstract class BaseTTIDStrategy implements TTIDStrategy { + final Hub _hub; + final SentryNative? _native; + + BaseTTIDStrategy(this._hub, this._native); + + void startTransaction(String routeName, DateTime startTime) { final transactionContext = SentryTransactionContext( routeName, 'ui.load', @@ -585,73 +529,77 @@ class NavigationTimingManager2 { origin: SentryTraceOrigins.autoNavigationRouteObserver, ); - final approximationStartTime = DateTime.now(); - SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - final approximationEndTime = DateTime.now(); - // this marks the end of the approximation - }); - - if (isRootRoute(routeName)) { - handleRootTTID(); - } else { - handleNonRootTTID(transactionContext); - } - } - - void handleNonRootTTID(SentryTransactionContext transactionContext) { _hub.startTransactionWithContext( transactionContext, waitForChildren: true, - autoFinishAfter: _autoFinishAfter, + autoFinishAfter: Duration(seconds: 3), trimEnd: true, - onFinish: _onTransactionFinish, + startTimestamp: startTime, + onFinish: (transaction) async { + final nativeFrames = await _native + ?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } + } + }, ); } - void handleRootTTID() {} - - bool isRootRoute(String routeName) { - return routeName == '/' || routeName == 'root ("/")'; - } + // Define abstract methods that subclasses need to implement. + @override + void startMeasurement(String routeName); - Future _onTransactionFinish(ISentrySpan transaction) async { - final nativeFrames = - await _native?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, - ); - } - } - } + @override + void endMeasurement({required DateTime endTime, String? routeName}); +} - void handleTTIDStrategy(String routeName) { - // SentryDisplayTracker().startTimeout(routeName ?? 'Unknown', () { - // // If we got inside this block, it means manual instrumentation was not done - // // handle non-root approximation ttid - // // non-root approximation ttid only finishes the span here and needs to get - // // the duration and end of the approximation which we got from addPostFrameCallback - // // e.g ttidSpan.finish() - // _strategy.startMeasurement(routeName); - // }); - } +class TransactionManager { + final Hub _hub; + final SentryNative? _native; - // This is the manual approach - static void reportInitiallyDisplayed() {} + TransactionManager(this._hub, this._native); - static void reportFullyDisplayed() {} -} + ISentrySpan startTransaction(String routeName, DateTime startTime) { + final transactionContext = SentryTransactionContext( + routeName, + 'ui.load', + transactionNameSource: SentryTransactionNameSource.component, + origin: SentryTraceOrigins.autoNavigationRouteObserver, + ); -// Abstract strategy for TTID measurement -abstract class TTIDStrategy { - void startMeasurement(String routeName); + print('hamma'); - void endMeasurement({required DateTime endTime, String? routeName}); + return _hub.startTransactionWithContext( + transactionContext, + waitForChildren: true, + autoFinishAfter: Duration(seconds: 3), + trimEnd: true, + startTimestamp: startTime, + onFinish: (transaction) async { + final nativeFrames = await _native + ?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } + } + }, + ); + } } // Implements approximation strategy for TTID @@ -659,15 +607,29 @@ class ApproximationTTIDStrategy implements TTIDStrategy { DateTime approximationStartTime = DateTime.now(); DateTime approximationEndTime = DateTime.now(); + final TransactionManager _transactionManager; + + final Hub _hub; + final SentryNative? _native; + + ApproximationTTIDStrategy(this._hub, this._native, this._transactionManager); + @override void startMeasurement(String routeName) { approximationStartTime = DateTime.now(); + print('hier?'); + + final transaction = _transactionManager.startTransaction(routeName, approximationStartTime); + final ttidSpan = transaction.startChild('ui.load.initial_display', + description: '$routeName initial display', startTimestamp: approximationStartTime); + SentryNavigatorObserver.ttidSpanMap[routeName] = ttidSpan; - final transaction = Sentry.getSpan(); - // SentryNavigatorObserver.ttidSpanMap[routeName] = SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - print('approximation end'); - endMeasurement(endTime: DateTime.now(), routeName: routeName); + approximationEndTime = DateTime.now(); + }); + + SentryDisplayTracker().startTimeout(routeName, () { + endMeasurement(endTime: approximationEndTime, routeName: routeName); }); } From 16c5ca0682744bd774349031e5aa29cbf108e503 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 12 Feb 2024 13:10:47 +0100 Subject: [PATCH 09/47] update --- flutter/example/lib/main.dart | 12 +- .../navigation/sentry_navigator_observer.dart | 319 ++++++------------ flutter/lib/src/sentry_flutter.dart | 24 +- flutter/lib/src/sentry_widget.dart | 24 +- 4 files changed, 133 insertions(+), 246 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index a347d87af9..b1db212b8a 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -71,12 +71,12 @@ Future setupSentry(AppRunner appRunner, String dsn, options.attachScreenshot = true; options.screenshotQuality = SentryScreenshotQuality.low; options.attachViewHierarchy = true; - options.enableTimeToFullDisplayTracing = true; + options.enableTimeToFullDisplayTracing = false; options.spotlight = Spotlight(enabled: true); // We can enable Sentry debug logging during development. This is likely // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. - options.debug = true; + options.debug = false; options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; @@ -847,7 +847,11 @@ int loop(int val) { } class SecondaryScaffold extends StatelessWidget { - const SecondaryScaffold({Key? key}) : super(key: key); + SecondaryScaffold({Key? key}) : super(key: key) { + Timer(const Duration(seconds: 1), () { + SentryFlutter.reportFullyDisplayed(); + }); + } static Future openSecondaryScaffold(BuildContext context) { return Navigator.push( @@ -856,7 +860,7 @@ class SecondaryScaffold extends StatelessWidget { settings: const RouteSettings(name: 'SecondaryScaffold', arguments: 'foobar'), builder: (context) { - return const SecondaryScaffold(); + return SecondaryScaffold(); }, ), ); diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index b122566c6a..d8eed5c1bf 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -15,9 +17,9 @@ const _navigationKey = 'navigation'; typedef RouteNameExtractor = RouteSettings? Function(RouteSettings? settings); typedef AdditionalInfoExtractor = Map? Function( - RouteSettings? from, - RouteSettings? to, - ); + RouteSettings? from, + RouteSettings? to, +); /// This is a navigation observer to record navigational breadcrumbs. /// For now it only records navigation events and no gestures. @@ -69,8 +71,7 @@ class SentryNavigatorObserver extends RouteObserver> { bool setRouteNameAsTransaction = false, RouteNameExtractor? routeNameExtractor, AdditionalInfoExtractor? additionalInfoProvider, - }) - : _hub = hub ?? HubAdapter(), + }) : _hub = hub ?? HubAdapter(), _enableAutoTransactions = enableAutoTransactions, _autoFinishAfter = autoFinishAfter, _setRouteNameAsTransaction = setRouteNameAsTransaction, @@ -95,7 +96,6 @@ class SentryNavigatorObserver extends RouteObserver> { final RouteNameExtractor? _routeNameExtractor; final AdditionalInfoExtractor? _additionalInfoProvider; final SentryNative? _native; - late final NavigationTimingManager? _timingManager; static ISentrySpan? _transaction2; static ISentrySpan? get transaction2 => _transaction2; @@ -131,8 +131,8 @@ class SentryNavigatorObserver extends RouteObserver> { _finishTransaction(); // _startTransaction(route); - NavigationTimingManager2().startMeasurement( - _getRouteName(route) ?? 'Unknown'); + NavigationTimingManager2() + .startMeasurement(_getRouteName(route) ?? 'Unknown'); try { // ignore: invalid_use_of_internal_member @@ -170,6 +170,8 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); + NavigationTimingManager2.ttidSpan = null; + NavigationTimingManager2.ttidSpan = null; _startTransaction(previousRoute); } @@ -263,7 +265,8 @@ class SentryNavigatorObserver extends RouteObserver> { return; } - if (name == 'root ("/")') {} else { + if (name == 'root ("/")') { + } else { startTime = DateTime.now(); final ttidSpan = _transaction2?.startChild('ui.load.initial_display', @@ -345,16 +348,16 @@ class RouteObserverBreadcrumb extends Breadcrumb { super.timestamp, Map? data, }) : super( - category: _navigationKey, - type: _navigationKey, - data: { - 'state': navigationType, - if (from != null) 'from': from, - if (fromArgs != null) 'from_arguments': fromArgs, - if (to != null) 'to': to, - if (toArgs != null) 'to_arguments': toArgs, - if (data != null) 'data': data, - }); + category: _navigationKey, + type: _navigationKey, + data: { + 'state': navigationType, + if (from != null) 'from': from, + if (fromArgs != null) 'from_arguments': fromArgs, + if (to != null) 'to': to, + if (toArgs != null) 'to_arguments': toArgs, + if (data != null) 'data': data, + }); static dynamic _formatArgs(Object? args) { if (args == null) { @@ -381,83 +384,6 @@ extension NativeFramesMeasurement on NativeFrames { } } -class NavigationTimingManager { - late final Hub _hub; - late final SentryNative? _native; - late final Duration _autoFinishAfter; - ISentrySpan? _currentTransaction; - DateTime? _transactionStartTime; - - TTIDStrategy ttidStrategy; - - static final NavigationTimingManager _instance = - NavigationTimingManager._internal(); - - factory NavigationTimingManager({required Hub hub, - SentryNative? native, - required Duration autoFinishAfter}) { - _instance._hub = hub; - _instance._native = native; - _instance._autoFinishAfter = autoFinishAfter; - return _instance; - } - - NavigationTimingManager._internal( - {Hub? hub, SentryNative? native, Duration? autoFinishAfter}) - : _hub = hub ?? HubAdapter(), - _native = native, - _autoFinishAfter = autoFinishAfter ?? Duration(seconds: 3), - ttidStrategy = ApproximationTTIDStrategy( - hub!, native!, TransactionManager(hub, native)); - - void startTransaction(String routeName, {bool isRootRoute = false}) { - final transactionContext = SentryTransactionContext( - routeName, - 'ui.load', - transactionNameSource: SentryTransactionNameSource.component, - origin: SentryTraceOrigins.autoNavigationRouteObserver, - ); - - _currentTransaction = _hub.startTransactionWithContext( - transactionContext, - waitForChildren: true, - autoFinishAfter: _autoFinishAfter, - trimEnd: true, - onFinish: _onTransactionFinish, - ); - - _transactionStartTime = DateTime.now(); - if (isRootRoute) { - _handleRootRouteTTID(); - } - } - - void finishTransaction() { - _currentTransaction?.status ??= SpanStatus.ok(); - _currentTransaction?.finish(endTimestamp: DateTime.now()); - } - - Future _onTransactionFinish(ISentrySpan transaction) async { - final nativeFrames = - await _native?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, - ); - } - } - } - - void _handleRootRouteTTID() { - // Special handling for TTID measurement on the root route. - } -} - @internal class NavigationTimingManager2 { static NavigationTimingManager2? _instance; @@ -465,20 +391,16 @@ class NavigationTimingManager2 { final Duration _autoFinishAfter; final SentryNative? _native; - static final Map ttidSpanMap = {}; - static final Map ttfdSpanMap = {}; - - TTIDStrategy _strategy; + static ISentrySpan? ttidSpan; + static ISentrySpan? ttfdSpan; NavigationTimingManager2._({ Hub? hub, Duration autoFinishAfter = const Duration(seconds: 3), SentryNative? native, - }) - : _hub = hub ?? HubAdapter(), + }) : _hub = hub ?? HubAdapter(), _autoFinishAfter = autoFinishAfter, - _native = native, - _strategy = ApproximationTTIDStrategy(hub!, native!, TransactionManager(hub, native)); + _native = native; factory NavigationTimingManager2({ Hub? hub, @@ -493,72 +415,86 @@ class NavigationTimingManager2 { return _instance!; } - void startMeasurement(String routeName) { - _strategy.startMeasurement(routeName); - } - - void endMeasurement() { - final endTime = DateTime.now(); - _strategy.endMeasurement(endTime: endTime); - } - - // This is the manual approach - static void reportInitiallyDisplayed() {} - - static void reportFullyDisplayed() {} -} - -// Abstract strategy for TTID measurement -abstract class TTIDStrategy { - void startMeasurement(String routeName); - - void endMeasurement({required DateTime endTime, String? routeName}); -} - -abstract class BaseTTIDStrategy implements TTIDStrategy { - final Hub _hub; - final SentryNative? _native; + void startMeasurement(String routeName) async { + final options = _hub.options is SentryFlutterOptions + // ignore: invalid_use_of_internal_member + ? _hub.options as SentryFlutterOptions + : null; + + // This has multiple branches + // - normal screen navigation -> affects all screens + // - app start navigation -> only affects root screen + bool isRootScreen = routeName == '/' || routeName == 'root ("/")'; + bool isAppStart = false; + if (isRootScreen && _native?.didFetchAppStart == false) { + // App start navigation + // Can not be manual or approx -> this is a special edge case + AppStartTracker().onAppStartComplete((appStartInfo) => { + // Create a transaction based on app start start time + // Create ttidSpan and finish immediately with the app start start & end time + // This is a small workaround to pass the correct time stamps since we cannot + // change timestamps of transactions or spans afterwards + }); + } else { + DateTime? approximationEndTime; + final startTime = DateTime.now(); + final manager = TransactionManager(_hub, _native); + final endTimeCompleter = Completer(); + final transaction = manager.startTransaction(routeName, startTime); + + if (options?.enableTimeToFullDisplayTracing == true) { + ttfdSpan = transaction.startChild('ui.load.full_display', + description: '$routeName full display', startTimestamp: startTime); + } - BaseTTIDStrategy(this._hub, this._native); + ttidSpan = transaction.startChild('ui.load.initial_display', + description: '$routeName initial display', + startTimestamp: startTime); - void startTransaction(String routeName, DateTime startTime) { - final transactionContext = SentryTransactionContext( - routeName, - 'ui.load', - transactionNameSource: SentryTransactionNameSource.component, - origin: SentryTraceOrigins.autoNavigationRouteObserver, - ); + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + approximationEndTime = DateTime.now(); + endTimeCompleter.complete(approximationEndTime); + }); - _hub.startTransactionWithContext( - transactionContext, - waitForChildren: true, - autoFinishAfter: Duration(seconds: 3), - trimEnd: true, - startTimestamp: startTime, - onFinish: (transaction) async { - final nativeFrames = await _native - ?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, - ); - } + final strategyDecision = + await SentryDisplayTracker().decideStrategyWithTimeout(routeName); + if (strategyDecision == StrategyDecision.approximation) { + if (approximationEndTime == null) { + await endTimeCompleter.future; } - }, - ); + await ttidSpan?.finish(endTimestamp: approximationEndTime); + } + } } - // Define abstract methods that subclasses need to implement. - @override - void startMeasurement(String routeName); + static void reportInitiallyDisplayed(String routeName) { + final endTime = DateTime.now(); + print('report end of display'); + if (!SentryDisplayTracker().reportManual(routeName)) { + final transaction = Sentry.getSpan(); + print(transaction); + final duration = endTime.millisecondsSinceEpoch - + SentryNavigatorObserver.startTime.millisecondsSinceEpoch; + transaction?.setMeasurement('time_to_initial_display', duration, + unit: DurationSentryMeasurementUnit.milliSecond); + if (routeName == '/') { + } else { + ttidSpan?.finish( + endTimestamp: endTime, + ); + } + } + } - @override - void endMeasurement({required DateTime endTime, String? routeName}); + static void reportFullyDisplayed() { + final endTime = DateTime.now(); + ttfdSpan?.setMeasurement( + 'time_to_full_display', + endTime.millisecondsSinceEpoch - + SentryNavigatorObserver.ttfdStartTime.millisecondsSinceEpoch, + unit: DurationSentryMeasurementUnit.milliSecond); + ttfdSpan?.finish(endTimestamp: endTime); + } } class TransactionManager { @@ -575,14 +511,13 @@ class TransactionManager { origin: SentryTraceOrigins.autoNavigationRouteObserver, ); - print('hamma'); - return _hub.startTransactionWithContext( transactionContext, waitForChildren: true, autoFinishAfter: Duration(seconds: 3), trimEnd: true, startTimestamp: startTime, + bindToScope: true, onFinish: (transaction) async { final nativeFrames = await _native ?.endNativeFramesCollection(transaction.context.traceId); @@ -601,57 +536,3 @@ class TransactionManager { ); } } - -// Implements approximation strategy for TTID -class ApproximationTTIDStrategy implements TTIDStrategy { - DateTime approximationStartTime = DateTime.now(); - DateTime approximationEndTime = DateTime.now(); - - final TransactionManager _transactionManager; - - final Hub _hub; - final SentryNative? _native; - - ApproximationTTIDStrategy(this._hub, this._native, this._transactionManager); - - @override - void startMeasurement(String routeName) { - approximationStartTime = DateTime.now(); - print('hier?'); - - final transaction = _transactionManager.startTransaction(routeName, approximationStartTime); - final ttidSpan = transaction.startChild('ui.load.initial_display', - description: '$routeName initial display', startTimestamp: approximationStartTime); - SentryNavigatorObserver.ttidSpanMap[routeName] = ttidSpan; - - SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - approximationEndTime = DateTime.now(); - }); - - SentryDisplayTracker().startTimeout(routeName, () { - endMeasurement(endTime: approximationEndTime, routeName: routeName); - }); - } - - @override - void endMeasurement({required DateTime endTime, String? routeName}) { - SentryNavigatorObserver.ttidSpanMap[routeName]?.finish( - endTimestamp: endTime, - ); - } -} - -// Implements manual strategy for TTID -class ManualTTIDStrategy implements TTIDStrategy { - @override - void startMeasurement(String routeName) { - print("Manual instrumentation started for $routeName"); - // Manual instrumentation logic - } - - @override - void endMeasurement({required DateTime endTime, String? routeName}) { - print("Manual instrumentation ended for $routeName at $endTime"); - // Calculate and log manual measurement - } -} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index f74dea74c0..b6f3c2c9e4 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -231,32 +231,12 @@ mixin SentryFlutter { static void reportInitialDisplay(BuildContext context) { final routeName = ModalRoute.of(context)?.settings.name ?? 'Unknown'; - final endTime = DateTime.now(); - if (!SentryDisplayTracker().reportManual(routeName)) { - final transaction = Sentry.getSpan(); - final duration = endTime.millisecondsSinceEpoch - - SentryNavigatorObserver.startTime.millisecondsSinceEpoch; - transaction?.setMeasurement('time_to_initial_display', duration, - unit: DurationSentryMeasurementUnit.milliSecond); - if (routeName == '/') { - print('is root screen manual'); - } else { - SentryNavigatorObserver.ttidSpanMap[routeName]?.finish( - endTimestamp: endTime, - ); - } - } + NavigationTimingManager2.reportInitiallyDisplayed(routeName); } /// Reports the time it took for the screen to be fully displayed. static void reportFullyDisplayed() { - final endTime = DateTime.now(); - SentryNavigatorObserver.ttfdSpan?.setMeasurement( - 'time_to_full_display', - endTime.millisecondsSinceEpoch - - SentryNavigatorObserver.ttfdStartTime.millisecondsSinceEpoch, - unit: DurationSentryMeasurementUnit.milliSecond); - SentryNavigatorObserver.ttfdSpan?.finish(endTimestamp: endTime); + NavigationTimingManager2.reportFullyDisplayed(); } @internal diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 49381c9083..5616435f90 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -62,6 +62,7 @@ class SentryDisplayTracker { final Map _manualReportReceived = {}; final Map _timers = {}; + final Map> _completers = {}; // Track completers void startTimeout(String routeName, Function onTimeout) { _timers[routeName]?.cancel(); // Cancel any existing timer @@ -73,10 +74,24 @@ class SentryDisplayTracker { }); } + Future decideStrategyWithTimeout(String routeName) { + var completer = Completer(); + + _timers[routeName]?.cancel(); + _timers[routeName] = Timer(Duration(seconds: 1), () { + if (_manualReportReceived[routeName] == true) { + completer.complete(StrategyDecision.manual); + } else { + completer.complete(StrategyDecision.approximation); + } + }); + + return completer.future; + } + bool reportManual(String routeName) { var wasReportedAlready = _manualReportReceived[routeName] ?? false; _manualReportReceived[routeName] = true; - _timers[routeName]?.cancel(); return wasReportedAlready; } @@ -84,5 +99,12 @@ class SentryDisplayTracker { _manualReportReceived.remove(routeName); _timers[routeName]?.cancel(); _timers.remove(routeName); + _completers.remove(routeName); } } + +enum StrategyDecision { + manual, + approximation, + undecided, +} From 69e4d3eb1791d2b6fe9f9f065b218f521ebe7c71 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 13 Feb 2024 03:08:06 +0100 Subject: [PATCH 10/47] update --- dart/lib/src/sentry_trace_origins.dart | 3 + flutter/example/lib/main.dart | 2 +- .../navigation/navigation_timing_manager.dart | 168 +++++++++++++ .../navigation_transaction_manager.dart | 43 ++++ .../navigation/sentry_navigator_observer.dart | 234 +++--------------- flutter/lib/src/sentry_flutter.dart | 11 +- flutter/lib/src/sentry_widget.dart | 39 ++- 7 files changed, 298 insertions(+), 202 deletions(-) create mode 100644 flutter/lib/src/navigation/navigation_timing_manager.dart create mode 100644 flutter/lib/src/navigation/navigation_transaction_manager.dart diff --git a/dart/lib/src/sentry_trace_origins.dart b/dart/lib/src/sentry_trace_origins.dart index 5a7c49b339..487dfebf3e 100644 --- a/dart/lib/src/sentry_trace_origins.dart +++ b/dart/lib/src/sentry_trace_origins.dart @@ -27,4 +27,7 @@ class SentryTraceOrigins { static const autoDbDriftQueryExecutor = 'auto.db.drift.query.executor'; static const autoDbDriftTransactionExecutor = 'auto.db.drift.transaction.executor'; + static const uiLoad = 'ui.load'; + static const uiTimeToInitialDisplay = 'ui.load.initial_display'; + static const uiTimeToFullDisplay = 'ui.load.full_display'; } diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index b1db212b8a..f149362cf9 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -76,7 +76,7 @@ Future setupSentry(AppRunner appRunner, String dsn, // We can enable Sentry debug logging during development. This is likely // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. - options.debug = false; + options.debug = true; options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; diff --git a/flutter/lib/src/navigation/navigation_timing_manager.dart b/flutter/lib/src/navigation/navigation_timing_manager.dart new file mode 100644 index 0000000000..9b1ef32061 --- /dev/null +++ b/flutter/lib/src/navigation/navigation_timing_manager.dart @@ -0,0 +1,168 @@ +import 'dart:async'; + +import 'package:flutter/scheduler.dart'; +import 'package:meta/meta.dart'; + +import '../../sentry_flutter.dart'; +import '../integrations/integrations.dart'; +import '../native/sentry_native.dart'; +import 'navigation_transaction_manager.dart'; + +@internal +class NavigationTimingManager { + static NavigationTimingManager? _instance; + final Hub _hub; + final Duration _autoFinishAfter; + final SentryNative? _native; + late final NavigationTransactionManager? _transactionManager; + + static ISentrySpan? _ttidSpan; + static ISentrySpan? _ttfdSpan; + static DateTime? _startTimestamp; + + NavigationTimingManager._({ + Hub? hub, + Duration autoFinishAfter = const Duration(seconds: 3), + SentryNative? native, + }) : _hub = hub ?? HubAdapter(), + _autoFinishAfter = autoFinishAfter, + _native = native { + _transactionManager = + NavigationTransactionManager(_hub, _native, _autoFinishAfter); + } + + factory NavigationTimingManager({ + Hub? hub, + Duration autoFinishAfter = const Duration(seconds: 3), + }) { + _instance ??= NavigationTimingManager._( + hub: hub ?? HubAdapter(), + autoFinishAfter: autoFinishAfter, + native: SentryFlutter.native, + ); + + return _instance!; + } + + void startMeasurement(String routeName) async { + final options = _hub.options is SentryFlutterOptions + // ignore: invalid_use_of_internal_member + ? _hub.options as SentryFlutterOptions + : null; + + // This marks the start timestamp of both TTID and TTFD spans + _startTimestamp = DateTime.now(); + + // This has multiple branches + // - normal screen navigation -> affects all screens + // - app start navigation -> only affects root screen + final isRootScreen = routeName == '/' || routeName == 'root ("/")'; + final didFetchAppStart = _native?.didFetchAppStart; + if (isRootScreen && didFetchAppStart == false) { + // App start - this is a special edge case that only happens once + AppStartTracker().onAppStartComplete((appStartInfo) { + // Create a transaction based on app start start time + // Create ttidSpan and finish immediately with the app start start & end time + // This is a small workaround to pass the correct time stamps since we mutate + // timestamps of transactions or spans in history + if (appStartInfo != null) { + final transaction = _transactionManager?.startTransaction( + routeName, appStartInfo.start); + if (transaction != null) { + final ttidSpan = _startTimeToInitialDisplaySpan( + routeName, transaction, appStartInfo.start); + ttidSpan.finish(endTimestamp: appStartInfo.end); + } + } + }); + } else { + DateTime? approximationEndTime; + final endTimeCompleter = Completer(); + final transaction = + _transactionManager?.startTransaction(routeName, _startTimestamp!); + + if (transaction != null) { + if (options?.enableTimeToFullDisplayTracing == true) { + _ttfdSpan = transaction.startChild('ui.load.full_display', + description: '$routeName full display', + startTimestamp: _startTimestamp!); + + _ttfdSpan = _startTimeToFullDisplaySpan( + routeName, transaction, _startTimestamp!); + } + _ttidSpan = _startTimeToInitialDisplaySpan( + routeName, transaction, _startTimestamp!); + } + + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + approximationEndTime = DateTime.now(); + endTimeCompleter.complete(approximationEndTime); + }); + + final strategyDecision = + await SentryDisplayTracker().decideStrategyWithTimeout2(routeName); + + switch (strategyDecision) { + case StrategyDecision.manual: + final endTimestamp = DateTime.now(); + final duration = endTimestamp.millisecondsSinceEpoch - + _startTimestamp!.millisecondsSinceEpoch; + _endTimeToInitialDisplaySpan(_ttidSpan!, transaction!, endTimestamp, duration); + break; + case StrategyDecision.approximation: + if (approximationEndTime == null) { + await endTimeCompleter.future; + } + final duration = approximationEndTime!.millisecondsSinceEpoch - + _startTimestamp!.millisecondsSinceEpoch; + _endTimeToInitialDisplaySpan( + _ttidSpan!, transaction!, approximationEndTime!, duration); + await _ttidSpan?.finish(endTimestamp: approximationEndTime); + break; + default: + print('Unknown strategy decision: $strategyDecision'); + } + } + } + + void reportInitiallyDisplayed(String routeName) { + SentryDisplayTracker().reportManual2(routeName); + } + + void reportFullyDisplayed() { + final endTime = DateTime.now(); + final transaction = Sentry.getSpan(); + final duration = endTime.millisecondsSinceEpoch - + _startTimestamp!.millisecondsSinceEpoch; + if (_ttfdSpan != null && transaction != null) { + _endTimeToFullDisplaySpan(_ttfdSpan!, transaction, endTime, duration); + } + } + + static ISentrySpan _startTimeToInitialDisplaySpan( + String routeName, ISentrySpan transaction, DateTime startTimestamp) { + return transaction.startChild(SentryTraceOrigins.uiTimeToInitialDisplay, + description: '$routeName initial display', + startTimestamp: startTimestamp); + } + + static ISentrySpan _startTimeToFullDisplaySpan( + String routeName, ISentrySpan transaction, DateTime startTimestamp) { + return transaction.startChild(SentryTraceOrigins.uiTimeToFullDisplay, + description: '$routeName full display', startTimestamp: startTimestamp); + } + + static void _endTimeToInitialDisplaySpan(ISentrySpan ttidSpan, + ISentrySpan transaction, DateTime endTimestamp, int duration) async { + transaction.setMeasurement('time_to_initial_display', duration, + unit: DurationSentryMeasurementUnit.milliSecond); + await ttidSpan.finish(endTimestamp: endTimestamp); + } + + static void _endTimeToFullDisplaySpan(ISentrySpan ttfdSpan, + ISentrySpan transaction, DateTime endTimestamp, int duration) async { + transaction.setMeasurement('time_to_full_display', duration, + unit: DurationSentryMeasurementUnit.milliSecond); + await ttfdSpan.finish(endTimestamp: endTimestamp); + } +} diff --git a/flutter/lib/src/navigation/navigation_transaction_manager.dart b/flutter/lib/src/navigation/navigation_transaction_manager.dart new file mode 100644 index 0000000000..f59821545f --- /dev/null +++ b/flutter/lib/src/navigation/navigation_transaction_manager.dart @@ -0,0 +1,43 @@ +import '../../sentry_flutter.dart'; +import '../native/sentry_native.dart'; + +class NavigationTransactionManager { + final Hub _hub; + final SentryNative? _native; + final Duration _autoFinishAfter; + + NavigationTransactionManager(this._hub, this._native, this._autoFinishAfter); + + ISentrySpan startTransaction(String routeName, DateTime startTime) { + final transactionContext = SentryTransactionContext( + routeName, + 'ui.load', + transactionNameSource: SentryTransactionNameSource.component, + origin: SentryTraceOrigins.autoNavigationRouteObserver, + ); + + return _hub.startTransactionWithContext( + transactionContext, + waitForChildren: true, + autoFinishAfter: _autoFinishAfter, + trimEnd: true, + startTimestamp: startTime, + bindToScope: true, + onFinish: (transaction) async { + final nativeFrames = await _native + ?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } + } + }, + ); + } +} diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index d8eed5c1bf..eaef69cd02 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -1,14 +1,11 @@ import 'dart:async'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; -import '../event_processor/flutter_enricher_event_processor.dart'; -import '../event_processor/native_app_start_event_processor.dart'; -import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; +import 'navigation_timing_manager.dart'; /// This key must be used so that the web interface displays the events nicely /// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/ @@ -131,7 +128,7 @@ class SentryNavigatorObserver extends RouteObserver> { _finishTransaction(); // _startTransaction(route); - NavigationTimingManager2() + NavigationTimingManager() .startMeasurement(_getRouteName(route) ?? 'Unknown'); try { @@ -170,8 +167,6 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); - NavigationTimingManager2.ttidSpan = null; - NavigationTimingManager2.ttidSpan = null; _startTransaction(previousRoute); } @@ -227,37 +222,37 @@ class SentryNavigatorObserver extends RouteObserver> { name = 'root ("/")'; } - final transactionContext2 = SentryTransactionContext( - name, - 'ui.load', - transactionNameSource: SentryTransactionNameSource.component, - // ignore: invalid_use_of_internal_member - origin: SentryTraceOrigins.autoNavigationRouteObserver, - ); - - if (name != 'root ("/")') { - _transaction2 = _hub.startTransactionWithContext( - transactionContext2, - waitForChildren: true, - autoFinishAfter: _autoFinishAfter, - trimEnd: true, - onFinish: (transaction) async { - final nativeFrames = await _native - ?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, - ); - } - } - }, - ); - } + // final transactionContext2 = SentryTransactionContext( + // name, + // 'ui.load', + // transactionNameSource: SentryTransactionNameSource.component, + // // ignore: invalid_use_of_internal_member + // origin: SentryTraceOrigins.autoNavigationRouteObserver, + // ); + // + // if (name != 'root ("/")') { + // _transaction2 = _hub.startTransactionWithContext( + // transactionContext2, + // waitForChildren: true, + // autoFinishAfter: _autoFinishAfter, + // trimEnd: true, + // onFinish: (transaction) async { + // final nativeFrames = await _native + // ?.endNativeFramesCollection(transaction.context.traceId); + // if (nativeFrames != null) { + // final measurements = nativeFrames.toMeasurements(); + // for (final item in measurements.entries) { + // final measurement = item.value; + // transaction.setMeasurement( + // item.key, + // measurement.value, + // unit: measurement.unit, + // ); + // } + // } + // }, + // ); + // } // if _enableAutoTransactions is enabled but there's no traces sample rate if (_transaction is NoOpSentrySpan) { @@ -267,12 +262,12 @@ class SentryNavigatorObserver extends RouteObserver> { if (name == 'root ("/")') { } else { - startTime = DateTime.now(); + // startTime = DateTime.now(); - final ttidSpan = _transaction2?.startChild('ui.load.initial_display', - description: '$name initial display', startTimestamp: startTime); - ttidSpan?.origin = 'auto.ui.time_to_display'; - ttidSpanMap[name] = ttidSpan!; + // final ttidSpan = _transaction2?.startChild('ui.load.initial_display', + // description: '$name initial display', startTimestamp: startTime); + // ttidSpan?.origin = 'auto.ui.time_to_display'; + // ttidSpanMap[name] = ttidSpan!; } // TODO: Needs to finish max within 30 seconds @@ -383,156 +378,3 @@ extension NativeFramesMeasurement on NativeFrames { }; } } - -@internal -class NavigationTimingManager2 { - static NavigationTimingManager2? _instance; - final Hub _hub; - final Duration _autoFinishAfter; - final SentryNative? _native; - - static ISentrySpan? ttidSpan; - static ISentrySpan? ttfdSpan; - - NavigationTimingManager2._({ - Hub? hub, - Duration autoFinishAfter = const Duration(seconds: 3), - SentryNative? native, - }) : _hub = hub ?? HubAdapter(), - _autoFinishAfter = autoFinishAfter, - _native = native; - - factory NavigationTimingManager2({ - Hub? hub, - Duration autoFinishAfter = const Duration(seconds: 3), - }) { - _instance ??= NavigationTimingManager2._( - hub: hub ?? HubAdapter(), - autoFinishAfter: autoFinishAfter, - native: SentryFlutter.native, - ); - - return _instance!; - } - - void startMeasurement(String routeName) async { - final options = _hub.options is SentryFlutterOptions - // ignore: invalid_use_of_internal_member - ? _hub.options as SentryFlutterOptions - : null; - - // This has multiple branches - // - normal screen navigation -> affects all screens - // - app start navigation -> only affects root screen - bool isRootScreen = routeName == '/' || routeName == 'root ("/")'; - bool isAppStart = false; - if (isRootScreen && _native?.didFetchAppStart == false) { - // App start navigation - // Can not be manual or approx -> this is a special edge case - AppStartTracker().onAppStartComplete((appStartInfo) => { - // Create a transaction based on app start start time - // Create ttidSpan and finish immediately with the app start start & end time - // This is a small workaround to pass the correct time stamps since we cannot - // change timestamps of transactions or spans afterwards - }); - } else { - DateTime? approximationEndTime; - final startTime = DateTime.now(); - final manager = TransactionManager(_hub, _native); - final endTimeCompleter = Completer(); - final transaction = manager.startTransaction(routeName, startTime); - - if (options?.enableTimeToFullDisplayTracing == true) { - ttfdSpan = transaction.startChild('ui.load.full_display', - description: '$routeName full display', startTimestamp: startTime); - } - - ttidSpan = transaction.startChild('ui.load.initial_display', - description: '$routeName initial display', - startTimestamp: startTime); - - SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - approximationEndTime = DateTime.now(); - endTimeCompleter.complete(approximationEndTime); - }); - - final strategyDecision = - await SentryDisplayTracker().decideStrategyWithTimeout(routeName); - if (strategyDecision == StrategyDecision.approximation) { - if (approximationEndTime == null) { - await endTimeCompleter.future; - } - await ttidSpan?.finish(endTimestamp: approximationEndTime); - } - } - } - - static void reportInitiallyDisplayed(String routeName) { - final endTime = DateTime.now(); - print('report end of display'); - if (!SentryDisplayTracker().reportManual(routeName)) { - final transaction = Sentry.getSpan(); - print(transaction); - final duration = endTime.millisecondsSinceEpoch - - SentryNavigatorObserver.startTime.millisecondsSinceEpoch; - transaction?.setMeasurement('time_to_initial_display', duration, - unit: DurationSentryMeasurementUnit.milliSecond); - if (routeName == '/') { - } else { - ttidSpan?.finish( - endTimestamp: endTime, - ); - } - } - } - - static void reportFullyDisplayed() { - final endTime = DateTime.now(); - ttfdSpan?.setMeasurement( - 'time_to_full_display', - endTime.millisecondsSinceEpoch - - SentryNavigatorObserver.ttfdStartTime.millisecondsSinceEpoch, - unit: DurationSentryMeasurementUnit.milliSecond); - ttfdSpan?.finish(endTimestamp: endTime); - } -} - -class TransactionManager { - final Hub _hub; - final SentryNative? _native; - - TransactionManager(this._hub, this._native); - - ISentrySpan startTransaction(String routeName, DateTime startTime) { - final transactionContext = SentryTransactionContext( - routeName, - 'ui.load', - transactionNameSource: SentryTransactionNameSource.component, - origin: SentryTraceOrigins.autoNavigationRouteObserver, - ); - - return _hub.startTransactionWithContext( - transactionContext, - waitForChildren: true, - autoFinishAfter: Duration(seconds: 3), - trimEnd: true, - startTimestamp: startTime, - bindToScope: true, - onFinish: (transaction) async { - final nativeFrames = await _native - ?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, - ); - } - } - }, - ); - } -} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index b6f3c2c9e4..00da5e5773 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -13,6 +13,7 @@ import 'integrations/connectivity/connectivity_integration.dart'; import 'integrations/screenshot_integration.dart'; import 'native/factory.dart'; import 'native/native_scope_observer.dart'; +import 'navigation/navigation_timing_manager.dart'; import 'profiling.dart'; import 'renderer/renderer.dart'; import 'native/sentry_native.dart'; @@ -229,14 +230,16 @@ mixin SentryFlutter { options.sdk = sdk; } - static void reportInitialDisplay(BuildContext context) { - final routeName = ModalRoute.of(context)?.settings.name ?? 'Unknown'; - NavigationTimingManager2.reportInitiallyDisplayed(routeName); + static void reportInitiallyDisplayed(BuildContext context) { + final routeName = ModalRoute.of(context)?.settings.name; + if (routeName != null) { + NavigationTimingManager().reportInitiallyDisplayed(routeName); + } } /// Reports the time it took for the screen to be fully displayed. static void reportFullyDisplayed() { - NavigationTimingManager2.reportFullyDisplayed(); + NavigationTimingManager().reportFullyDisplayed(); } @internal diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 5616435f90..1e431c1e34 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -40,7 +40,7 @@ class _SentryDisplayWidgetState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - SentryFlutter.reportInitialDisplay(context); + SentryFlutter.reportInitiallyDisplayed(context); }); } @@ -89,6 +89,43 @@ class SentryDisplayTracker { return completer.future; } + Future decideStrategyWithTimeout2(String routeName) { + // Ensure initialization of a completer for the given route name. + if (!_completers.containsKey(routeName) || _completers[routeName]!.isCompleted) { + _completers[routeName] = Completer(); + } + var completer = _completers[routeName]!; + + // Start or reset the timer only if a manual report has not been received. + if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { + _timers[routeName]?.cancel(); // Cancel any existing timer. + _timers[routeName] = Timer(Duration(seconds: 1), () { + // Double-check to prevent race conditions. + if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { + if (!completer.isCompleted) { + completer.complete(StrategyDecision.approximation); + } + } + }); + } + + return completer.future; + } + + bool reportManual2(String routeName) { + var wasReportedAlready = _manualReportReceived[routeName] ?? false; + _manualReportReceived[routeName] = true; + + // Complete the strategy decision as manual if within the timeout period. + if (_completers[routeName]?.isCompleted == false) { + _completers[routeName]?.complete(StrategyDecision.manual); + } + + // Cancel the timer as it's no longer necessary. + _timers[routeName]?.cancel(); + return wasReportedAlready; + } + bool reportManual(String routeName) { var wasReportedAlready = _manualReportReceived[routeName] ?? false; _manualReportReceived[routeName] = true; From ab8bddc0e830bbc949caa11640549d82d74305cc Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 13 Feb 2024 10:17:46 +0100 Subject: [PATCH 11/47] working ttid and ttfd for non root app start navigations --- flutter/example/lib/main.dart | 1 + .../navigation/navigation_timing_manager.dart | 139 +++++++++--------- flutter/lib/src/sentry_widget.dart | 1 - 3 files changed, 67 insertions(+), 74 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index f149362cf9..4b94ebd23e 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -77,6 +77,7 @@ Future setupSentry(AppRunner appRunner, String dsn, // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; + options.enableTimeToFullDisplayTracing = true; options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; diff --git a/flutter/lib/src/navigation/navigation_timing_manager.dart b/flutter/lib/src/navigation/navigation_timing_manager.dart index 9b1ef32061..d4798744f1 100644 --- a/flutter/lib/src/navigation/navigation_timing_manager.dart +++ b/flutter/lib/src/navigation/navigation_timing_manager.dart @@ -45,12 +45,6 @@ class NavigationTimingManager { } void startMeasurement(String routeName) async { - final options = _hub.options is SentryFlutterOptions - // ignore: invalid_use_of_internal_member - ? _hub.options as SentryFlutterOptions - : null; - - // This marks the start timestamp of both TTID and TTFD spans _startTimestamp = DateTime.now(); // This has multiple branches @@ -62,107 +56,106 @@ class NavigationTimingManager { // App start - this is a special edge case that only happens once AppStartTracker().onAppStartComplete((appStartInfo) { // Create a transaction based on app start start time - // Create ttidSpan and finish immediately with the app start start & end time - // This is a small workaround to pass the correct time stamps since we mutate + // Then create ttidSpan and finish immediately with the app start start & end time + // This is a small workaround to pass the correct time stamps since we cannot mutate // timestamps of transactions or spans in history if (appStartInfo != null) { final transaction = _transactionManager?.startTransaction( routeName, appStartInfo.start); if (transaction != null) { - final ttidSpan = _startTimeToInitialDisplaySpan( - routeName, transaction, appStartInfo.start); + final ttidSpan = _createTTIDSpan(transaction, routeName, appStartInfo.start); ttidSpan.finish(endTimestamp: appStartInfo.end); } } }); } else { - DateTime? approximationEndTime; - final endTimeCompleter = Completer(); final transaction = _transactionManager?.startTransaction(routeName, _startTimestamp!); - if (transaction != null) { - if (options?.enableTimeToFullDisplayTracing == true) { - _ttfdSpan = transaction.startChild('ui.load.full_display', - description: '$routeName full display', - startTimestamp: _startTimestamp!); - - _ttfdSpan = _startTimeToFullDisplaySpan( - routeName, transaction, _startTimestamp!); - } - _ttidSpan = _startTimeToInitialDisplaySpan( - routeName, transaction, _startTimestamp!); + if (transaction == null) { + return; } - SchedulerBinding.instance.addPostFrameCallback((timeStamp) { - approximationEndTime = DateTime.now(); - endTimeCompleter.complete(approximationEndTime); - }); + _initializeSpans(transaction, routeName, _startTimestamp!); - final strategyDecision = - await SentryDisplayTracker().decideStrategyWithTimeout2(routeName); - - switch (strategyDecision) { - case StrategyDecision.manual: - final endTimestamp = DateTime.now(); - final duration = endTimestamp.millisecondsSinceEpoch - - _startTimestamp!.millisecondsSinceEpoch; - _endTimeToInitialDisplaySpan(_ttidSpan!, transaction!, endTimestamp, duration); - break; - case StrategyDecision.approximation: - if (approximationEndTime == null) { - await endTimeCompleter.future; - } - final duration = approximationEndTime!.millisecondsSinceEpoch - - _startTimestamp!.millisecondsSinceEpoch; - _endTimeToInitialDisplaySpan( - _ttidSpan!, transaction!, approximationEndTime!, duration); - await _ttidSpan?.finish(endTimestamp: approximationEndTime); - break; - default: - print('Unknown strategy decision: $strategyDecision'); - } + final endTimestamp = await _determineEndTime(routeName); + + final duration = endTimestamp.difference(_startTimestamp!).inMilliseconds; + _finishSpan(_ttidSpan!, transaction, 'time_to_initial_display', duration, + endTimestamp); } } + Future _determineEndTime(String routeName) async { + DateTime? approximationEndTime; + final endTimeCompleter = Completer(); + + SchedulerBinding.instance.addPostFrameCallback((_) { + approximationEndTime = DateTime.now(); + endTimeCompleter.complete(approximationEndTime!); + }); + + final strategyDecision = + await SentryDisplayTracker().decideStrategyWithTimeout2(routeName); + + if (strategyDecision == StrategyDecision.manual && + !endTimeCompleter.isCompleted) { + approximationEndTime = DateTime.now(); + endTimeCompleter.complete(approximationEndTime); + } else if (!endTimeCompleter.isCompleted) { + // If the decision is not manual and the completer hasn't been completed, await it. + await endTimeCompleter.future; + } + + return approximationEndTime!; + } + void reportInitiallyDisplayed(String routeName) { SentryDisplayTracker().reportManual2(routeName); } void reportFullyDisplayed() { - final endTime = DateTime.now(); + final endTimestamp = DateTime.now(); final transaction = Sentry.getSpan(); - final duration = endTime.millisecondsSinceEpoch - - _startTimestamp!.millisecondsSinceEpoch; - if (_ttfdSpan != null && transaction != null) { - _endTimeToFullDisplaySpan(_ttfdSpan!, transaction, endTime, duration); + final duration = endTimestamp.difference(_startTimestamp!).inMilliseconds; + if (_ttidSpan == null || transaction == null) { + return; } + _finishSpan(_ttfdSpan!, transaction, 'time_to_full_display', duration, endTimestamp); } - static ISentrySpan _startTimeToInitialDisplaySpan( - String routeName, ISentrySpan transaction, DateTime startTimestamp) { - return transaction.startChild(SentryTraceOrigins.uiTimeToInitialDisplay, - description: '$routeName initial display', - startTimestamp: startTimestamp); + void _initializeSpans(ISentrySpan? transaction, String routeName, DateTime startTimestamp) { + final options = _hub.options is SentryFlutterOptions + // ignore: invalid_use_of_internal_member + ? _hub.options as SentryFlutterOptions + : null; + if (transaction == null) return; + _ttidSpan = _createTTIDSpan(transaction, routeName, startTimestamp); + if (options?.enableTimeToFullDisplayTracing == true) { + _ttfdSpan = _createTTFDSpan(transaction, routeName, startTimestamp); + } } - static ISentrySpan _startTimeToFullDisplaySpan( - String routeName, ISentrySpan transaction, DateTime startTimestamp) { - return transaction.startChild(SentryTraceOrigins.uiTimeToFullDisplay, - description: '$routeName full display', startTimestamp: startTimestamp); + ISentrySpan _createTTIDSpan(ISentrySpan transaction, String routeName, DateTime startTimestamp) { + return transaction.startChild( + SentryTraceOrigins.uiTimeToInitialDisplay, + description: '$routeName initial display', + startTimestamp: startTimestamp, + ); } - static void _endTimeToInitialDisplaySpan(ISentrySpan ttidSpan, - ISentrySpan transaction, DateTime endTimestamp, int duration) async { - transaction.setMeasurement('time_to_initial_display', duration, - unit: DurationSentryMeasurementUnit.milliSecond); - await ttidSpan.finish(endTimestamp: endTimestamp); + ISentrySpan _createTTFDSpan(ISentrySpan transaction, String routeName, DateTime startTimestamp) { + return transaction.startChild( + SentryTraceOrigins.uiTimeToFullDisplay, + description: '$routeName full display', + startTimestamp: startTimestamp, + ); } - static void _endTimeToFullDisplaySpan(ISentrySpan ttfdSpan, - ISentrySpan transaction, DateTime endTimestamp, int duration) async { - transaction.setMeasurement('time_to_full_display', duration, + void _finishSpan(ISentrySpan span, ISentrySpan transaction, + String measurementName, int duration, DateTime endTimestamp) { + transaction.setMeasurement(measurementName, duration, unit: DurationSentryMeasurementUnit.milliSecond); - await ttfdSpan.finish(endTimestamp: endTimestamp); + span.finish(endTimestamp: endTimestamp); } } diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 1e431c1e34..5425da4c01 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -143,5 +143,4 @@ class SentryDisplayTracker { enum StrategyDecision { manual, approximation, - undecided, } From 7490a837c9456a1910883dfba51df851172c82e3 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 13 Feb 2024 10:37:36 +0100 Subject: [PATCH 12/47] update --- flutter/example/lib/main.dart | 3 +- flutter/lib/sentry_flutter.dart | 1 + .../display_strategy_evaluator.dart | 69 ++++++++++ .../navigation/navigation_timing_manager.dart | 6 +- .../src/navigation/sentry_display_widget.dart | 27 ++++ flutter/lib/src/sentry_widget.dart | 119 ------------------ 6 files changed, 101 insertions(+), 124 deletions(-) create mode 100644 flutter/lib/src/navigation/display_strategy_evaluator.dart create mode 100644 flutter/lib/src/navigation/sentry_display_widget.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 4b94ebd23e..31a158328b 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -104,7 +104,6 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - // SentryFlutter.reportFullDisplay(); return feedback.BetterFeedback( child: ChangeNotifierProvider( create: (_) => ThemeProvider(), @@ -849,7 +848,7 @@ int loop(int val) { class SecondaryScaffold extends StatelessWidget { SecondaryScaffold({Key? key}) : super(key: key) { - Timer(const Duration(seconds: 1), () { + Timer(const Duration(milliseconds: 500), () { SentryFlutter.reportFullyDisplayed(); }); } diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index 0f43bf741b..d15c8b7a70 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -16,3 +16,4 @@ export 'src/screenshot/sentry_screenshot_quality.dart'; export 'src/user_interaction/sentry_user_interaction_widget.dart'; export 'src/binding_wrapper.dart'; export 'src/sentry_widget.dart'; +export 'src/navigation/sentry_display_widget.dart'; diff --git a/flutter/lib/src/navigation/display_strategy_evaluator.dart b/flutter/lib/src/navigation/display_strategy_evaluator.dart new file mode 100644 index 0000000000..92707dfb8c --- /dev/null +++ b/flutter/lib/src/navigation/display_strategy_evaluator.dart @@ -0,0 +1,69 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +@internal +class DisplayStrategyEvaluator { + static final DisplayStrategyEvaluator _instance = + DisplayStrategyEvaluator._internal(); + + factory DisplayStrategyEvaluator() { + return _instance; + } + + DisplayStrategyEvaluator._internal(); + + final Map _manualReportReceived = {}; + final Map _timers = {}; + final Map> _completers = {}; + + Future decideStrategy(String routeName) { + // Ensure initialization of a completer for the given route name. + if (!_completers.containsKey(routeName) || _completers[routeName]!.isCompleted) { + _completers[routeName] = Completer(); + } + var completer = _completers[routeName]!; + + // Start or reset the timer only if a manual report has not been received. + if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { + _timers[routeName]?.cancel(); // Cancel any existing timer. + _timers[routeName] = Timer(Duration(seconds: 1), () { + // Double-check to prevent race conditions. + if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { + if (!completer.isCompleted) { + completer.complete(StrategyDecision.approximation); + } + } + }); + } + + return completer.future; + } + + bool reportManual(String routeName) { + var wasReportedAlready = _manualReportReceived[routeName] ?? false; + _manualReportReceived[routeName] = true; + + // Complete the strategy decision as manual if within the timeout period. + if (_completers[routeName]?.isCompleted == false) { + _completers[routeName]?.complete(StrategyDecision.manual); + } + + // Cancel the timer as it's no longer necessary. + _timers[routeName]?.cancel(); + return wasReportedAlready; + } + + // TODO: when do we need to clear state? + void clearState(String routeName) { + _manualReportReceived.remove(routeName); + _timers[routeName]?.cancel(); + _timers.remove(routeName); + _completers.remove(routeName); + } +} + +enum StrategyDecision { + manual, + approximation, +} diff --git a/flutter/lib/src/navigation/navigation_timing_manager.dart b/flutter/lib/src/navigation/navigation_timing_manager.dart index d4798744f1..6e80e207bd 100644 --- a/flutter/lib/src/navigation/navigation_timing_manager.dart +++ b/flutter/lib/src/navigation/navigation_timing_manager.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; +import 'display_strategy_evaluator.dart'; import 'navigation_transaction_manager.dart'; @internal @@ -96,14 +97,13 @@ class NavigationTimingManager { }); final strategyDecision = - await SentryDisplayTracker().decideStrategyWithTimeout2(routeName); + await DisplayStrategyEvaluator().decideStrategy(routeName); if (strategyDecision == StrategyDecision.manual && !endTimeCompleter.isCompleted) { approximationEndTime = DateTime.now(); endTimeCompleter.complete(approximationEndTime); } else if (!endTimeCompleter.isCompleted) { - // If the decision is not manual and the completer hasn't been completed, await it. await endTimeCompleter.future; } @@ -111,7 +111,7 @@ class NavigationTimingManager { } void reportInitiallyDisplayed(String routeName) { - SentryDisplayTracker().reportManual2(routeName); + DisplayStrategyEvaluator().reportManual(routeName); } void reportFullyDisplayed() { diff --git a/flutter/lib/src/navigation/sentry_display_widget.dart b/flutter/lib/src/navigation/sentry_display_widget.dart new file mode 100644 index 0000000000..079df3119c --- /dev/null +++ b/flutter/lib/src/navigation/sentry_display_widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/cupertino.dart'; + +import '../../sentry_flutter.dart'; + +class SentryDisplayWidget extends StatefulWidget { + final Widget child; + + const SentryDisplayWidget({super.key, required this.child}); + + @override + _SentryDisplayWidgetState createState() => _SentryDisplayWidgetState(); +} + +class _SentryDisplayWidgetState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + SentryFlutter.reportInitiallyDisplayed(context); + }); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 5425da4c01..0b4eb779de 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -25,122 +25,3 @@ class _SentryWidgetState extends State { return content; } } - -class SentryDisplayWidget extends StatefulWidget { - final Widget child; - - const SentryDisplayWidget({super.key, required this.child}); - - @override - _SentryDisplayWidgetState createState() => _SentryDisplayWidgetState(); -} - -class _SentryDisplayWidgetState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - SentryFlutter.reportInitiallyDisplayed(context); - }); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} - -class SentryDisplayTracker { - static final SentryDisplayTracker _instance = - SentryDisplayTracker._internal(); - - factory SentryDisplayTracker() { - return _instance; - } - - SentryDisplayTracker._internal(); - - final Map _manualReportReceived = {}; - final Map _timers = {}; - final Map> _completers = {}; // Track completers - - void startTimeout(String routeName, Function onTimeout) { - _timers[routeName]?.cancel(); // Cancel any existing timer - _timers[routeName] = Timer(Duration(seconds: 1), () { - // Don't send if we already received a manual report or if we're on the root route e.g App start. - if (!(_manualReportReceived[routeName] ?? false)) { - onTimeout(); - } - }); - } - - Future decideStrategyWithTimeout(String routeName) { - var completer = Completer(); - - _timers[routeName]?.cancel(); - _timers[routeName] = Timer(Duration(seconds: 1), () { - if (_manualReportReceived[routeName] == true) { - completer.complete(StrategyDecision.manual); - } else { - completer.complete(StrategyDecision.approximation); - } - }); - - return completer.future; - } - - Future decideStrategyWithTimeout2(String routeName) { - // Ensure initialization of a completer for the given route name. - if (!_completers.containsKey(routeName) || _completers[routeName]!.isCompleted) { - _completers[routeName] = Completer(); - } - var completer = _completers[routeName]!; - - // Start or reset the timer only if a manual report has not been received. - if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { - _timers[routeName]?.cancel(); // Cancel any existing timer. - _timers[routeName] = Timer(Duration(seconds: 1), () { - // Double-check to prevent race conditions. - if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { - if (!completer.isCompleted) { - completer.complete(StrategyDecision.approximation); - } - } - }); - } - - return completer.future; - } - - bool reportManual2(String routeName) { - var wasReportedAlready = _manualReportReceived[routeName] ?? false; - _manualReportReceived[routeName] = true; - - // Complete the strategy decision as manual if within the timeout period. - if (_completers[routeName]?.isCompleted == false) { - _completers[routeName]?.complete(StrategyDecision.manual); - } - - // Cancel the timer as it's no longer necessary. - _timers[routeName]?.cancel(); - return wasReportedAlready; - } - - bool reportManual(String routeName) { - var wasReportedAlready = _manualReportReceived[routeName] ?? false; - _manualReportReceived[routeName] = true; - return wasReportedAlready; - } - - void clearState(String routeName) { - _manualReportReceived.remove(routeName); - _timers[routeName]?.cancel(); - _timers.remove(routeName); - _completers.remove(routeName); - } -} - -enum StrategyDecision { - manual, - approximation, -} From c8f553ffe1f96093ba4deacf2f12508b8f2cbf77 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 13 Feb 2024 12:58:16 +0100 Subject: [PATCH 13/47] refactor to navigator observer --- .../native_app_start_integration.dart | 2 +- .../navigation/navigation_timing_manager.dart | 161 ----------- .../navigation_transaction_manager.dart | 43 --- .../navigation/sentry_navigator_observer.dart | 251 +++++++++++------- flutter/lib/src/sentry_flutter.dart | 5 +- 5 files changed, 164 insertions(+), 298 deletions(-) delete mode 100644 flutter/lib/src/navigation/navigation_timing_manager.dart delete mode 100644 flutter/lib/src/navigation/navigation_transaction_manager.dart diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index d767e7d28b..9233e15ee7 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -24,8 +24,8 @@ class NativeAppStartIntegration extends Integration { 'Scheduler binding is null. Can\'t auto detect app start time.'); } else { schedulerBinding.addPostFrameCallback((timeStamp) async { - final appStartEnd = options.clock(); // ignore: invalid_use_of_internal_member + final appStartEnd = options.clock(); _native.appStartEnd = appStartEnd; if (!_native.didFetchAppStart) { diff --git a/flutter/lib/src/navigation/navigation_timing_manager.dart b/flutter/lib/src/navigation/navigation_timing_manager.dart deleted file mode 100644 index 6e80e207bd..0000000000 --- a/flutter/lib/src/navigation/navigation_timing_manager.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/scheduler.dart'; -import 'package:meta/meta.dart'; - -import '../../sentry_flutter.dart'; -import '../integrations/integrations.dart'; -import '../native/sentry_native.dart'; -import 'display_strategy_evaluator.dart'; -import 'navigation_transaction_manager.dart'; - -@internal -class NavigationTimingManager { - static NavigationTimingManager? _instance; - final Hub _hub; - final Duration _autoFinishAfter; - final SentryNative? _native; - late final NavigationTransactionManager? _transactionManager; - - static ISentrySpan? _ttidSpan; - static ISentrySpan? _ttfdSpan; - static DateTime? _startTimestamp; - - NavigationTimingManager._({ - Hub? hub, - Duration autoFinishAfter = const Duration(seconds: 3), - SentryNative? native, - }) : _hub = hub ?? HubAdapter(), - _autoFinishAfter = autoFinishAfter, - _native = native { - _transactionManager = - NavigationTransactionManager(_hub, _native, _autoFinishAfter); - } - - factory NavigationTimingManager({ - Hub? hub, - Duration autoFinishAfter = const Duration(seconds: 3), - }) { - _instance ??= NavigationTimingManager._( - hub: hub ?? HubAdapter(), - autoFinishAfter: autoFinishAfter, - native: SentryFlutter.native, - ); - - return _instance!; - } - - void startMeasurement(String routeName) async { - _startTimestamp = DateTime.now(); - - // This has multiple branches - // - normal screen navigation -> affects all screens - // - app start navigation -> only affects root screen - final isRootScreen = routeName == '/' || routeName == 'root ("/")'; - final didFetchAppStart = _native?.didFetchAppStart; - if (isRootScreen && didFetchAppStart == false) { - // App start - this is a special edge case that only happens once - AppStartTracker().onAppStartComplete((appStartInfo) { - // Create a transaction based on app start start time - // Then create ttidSpan and finish immediately with the app start start & end time - // This is a small workaround to pass the correct time stamps since we cannot mutate - // timestamps of transactions or spans in history - if (appStartInfo != null) { - final transaction = _transactionManager?.startTransaction( - routeName, appStartInfo.start); - if (transaction != null) { - final ttidSpan = _createTTIDSpan(transaction, routeName, appStartInfo.start); - ttidSpan.finish(endTimestamp: appStartInfo.end); - } - } - }); - } else { - final transaction = - _transactionManager?.startTransaction(routeName, _startTimestamp!); - - if (transaction == null) { - return; - } - - _initializeSpans(transaction, routeName, _startTimestamp!); - - final endTimestamp = await _determineEndTime(routeName); - - final duration = endTimestamp.difference(_startTimestamp!).inMilliseconds; - _finishSpan(_ttidSpan!, transaction, 'time_to_initial_display', duration, - endTimestamp); - } - } - - Future _determineEndTime(String routeName) async { - DateTime? approximationEndTime; - final endTimeCompleter = Completer(); - - SchedulerBinding.instance.addPostFrameCallback((_) { - approximationEndTime = DateTime.now(); - endTimeCompleter.complete(approximationEndTime!); - }); - - final strategyDecision = - await DisplayStrategyEvaluator().decideStrategy(routeName); - - if (strategyDecision == StrategyDecision.manual && - !endTimeCompleter.isCompleted) { - approximationEndTime = DateTime.now(); - endTimeCompleter.complete(approximationEndTime); - } else if (!endTimeCompleter.isCompleted) { - await endTimeCompleter.future; - } - - return approximationEndTime!; - } - - void reportInitiallyDisplayed(String routeName) { - DisplayStrategyEvaluator().reportManual(routeName); - } - - void reportFullyDisplayed() { - final endTimestamp = DateTime.now(); - final transaction = Sentry.getSpan(); - final duration = endTimestamp.difference(_startTimestamp!).inMilliseconds; - if (_ttidSpan == null || transaction == null) { - return; - } - _finishSpan(_ttfdSpan!, transaction, 'time_to_full_display', duration, endTimestamp); - } - - void _initializeSpans(ISentrySpan? transaction, String routeName, DateTime startTimestamp) { - final options = _hub.options is SentryFlutterOptions - // ignore: invalid_use_of_internal_member - ? _hub.options as SentryFlutterOptions - : null; - if (transaction == null) return; - _ttidSpan = _createTTIDSpan(transaction, routeName, startTimestamp); - if (options?.enableTimeToFullDisplayTracing == true) { - _ttfdSpan = _createTTFDSpan(transaction, routeName, startTimestamp); - } - } - - ISentrySpan _createTTIDSpan(ISentrySpan transaction, String routeName, DateTime startTimestamp) { - return transaction.startChild( - SentryTraceOrigins.uiTimeToInitialDisplay, - description: '$routeName initial display', - startTimestamp: startTimestamp, - ); - } - - ISentrySpan _createTTFDSpan(ISentrySpan transaction, String routeName, DateTime startTimestamp) { - return transaction.startChild( - SentryTraceOrigins.uiTimeToFullDisplay, - description: '$routeName full display', - startTimestamp: startTimestamp, - ); - } - - void _finishSpan(ISentrySpan span, ISentrySpan transaction, - String measurementName, int duration, DateTime endTimestamp) { - transaction.setMeasurement(measurementName, duration, - unit: DurationSentryMeasurementUnit.milliSecond); - span.finish(endTimestamp: endTimestamp); - } -} diff --git a/flutter/lib/src/navigation/navigation_transaction_manager.dart b/flutter/lib/src/navigation/navigation_transaction_manager.dart deleted file mode 100644 index f59821545f..0000000000 --- a/flutter/lib/src/navigation/navigation_transaction_manager.dart +++ /dev/null @@ -1,43 +0,0 @@ -import '../../sentry_flutter.dart'; -import '../native/sentry_native.dart'; - -class NavigationTransactionManager { - final Hub _hub; - final SentryNative? _native; - final Duration _autoFinishAfter; - - NavigationTransactionManager(this._hub, this._native, this._autoFinishAfter); - - ISentrySpan startTransaction(String routeName, DateTime startTime) { - final transactionContext = SentryTransactionContext( - routeName, - 'ui.load', - transactionNameSource: SentryTransactionNameSource.component, - origin: SentryTraceOrigins.autoNavigationRouteObserver, - ); - - return _hub.startTransactionWithContext( - transactionContext, - waitForChildren: true, - autoFinishAfter: _autoFinishAfter, - trimEnd: true, - startTimestamp: startTime, - bindToScope: true, - onFinish: (transaction) async { - final nativeFrames = await _native - ?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, - ); - } - } - }, - ); - } -} diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index eaef69cd02..36fc69e902 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -1,11 +1,14 @@ import 'dart:async'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; +import '../event_processor/flutter_enricher_event_processor.dart'; +import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; -import 'navigation_timing_manager.dart'; +import 'display_strategy_evaluator.dart'; /// This key must be used so that the web interface displays the events nicely /// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/ @@ -77,11 +80,6 @@ class SentryNavigatorObserver extends RouteObserver> { _native = SentryFlutter.native { if (enableAutoTransactions) { // ignore: invalid_use_of_internal_member - // _timingManager = NavigationTimingManager( - // hub: _hub, - // native: _native, - // autoFinishAfter: autoFinishAfter, - // ); _hub.options.sdk.addIntegration('UINavigationTracing'); } } @@ -93,24 +91,16 @@ class SentryNavigatorObserver extends RouteObserver> { final RouteNameExtractor? _routeNameExtractor; final AdditionalInfoExtractor? _additionalInfoProvider; final SentryNative? _native; - static ISentrySpan? _transaction2; - - static ISentrySpan? get transaction2 => _transaction2; - - static final Map ttidSpanMap = {}; - static final Map ttfdSpanMap = {}; ISentrySpan? _transaction; + static DateTime? _startTimestamp; + static ISentrySpan? _ttidSpan; + static ISentrySpan? _ttfdSpan; static String? _currentRouteName; @internal static String? get currentRouteName => _currentRouteName; - static var startTime = DateTime.now(); - static ISentrySpan? ttidSpan; - static ISentrySpan? ttfdSpan; - static var ttfdStartTime = DateTime.now(); - static Stopwatch? ttfdStopwatch; @override void didPush(Route route, Route? previousRoute) { @@ -126,17 +116,7 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); - // _startTransaction(route); - - NavigationTimingManager() - .startMeasurement(_getRouteName(route) ?? 'Unknown'); - - try { - // ignore: invalid_use_of_internal_member - _hub.options.sdk.addIntegration('UINavigationTracing'); - } on Exception catch (e) { - print(e); - } + _startMeasurement(route); } @override @@ -167,7 +147,7 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); - _startTransaction(previousRoute); + _startMeasurement(previousRoute); } void _addBreadcrumb({ @@ -206,7 +186,8 @@ class SentryNavigatorObserver extends RouteObserver> { } } - Future _startTransaction(Route? route) async { + Future _startTransaction(Route? route, + {DateTime? startTimestamp}) async { if (!_enableAutoTransactions) { return; } @@ -222,83 +203,173 @@ class SentryNavigatorObserver extends RouteObserver> { name = 'root ("/")'; } - // final transactionContext2 = SentryTransactionContext( - // name, - // 'ui.load', - // transactionNameSource: SentryTransactionNameSource.component, - // // ignore: invalid_use_of_internal_member - // origin: SentryTraceOrigins.autoNavigationRouteObserver, - // ); - // - // if (name != 'root ("/")') { - // _transaction2 = _hub.startTransactionWithContext( - // transactionContext2, - // waitForChildren: true, - // autoFinishAfter: _autoFinishAfter, - // trimEnd: true, - // onFinish: (transaction) async { - // final nativeFrames = await _native - // ?.endNativeFramesCollection(transaction.context.traceId); - // if (nativeFrames != null) { - // final measurements = nativeFrames.toMeasurements(); - // for (final item in measurements.entries) { - // final measurement = item.value; - // transaction.setMeasurement( - // item.key, - // measurement.value, - // unit: measurement.unit, - // ); - // } - // } - // }, - // ); - // } + final transactionContext = SentryTransactionContext( + name, + 'navigation', + transactionNameSource: SentryTransactionNameSource.component, + // ignore: invalid_use_of_internal_member + origin: SentryTraceOrigins.autoNavigationRouteObserver, + ); + + _transaction = _hub.startTransactionWithContext( + transactionContext, + waitForChildren: true, + autoFinishAfter: _autoFinishAfter, + trimEnd: true, + startTimestamp: startTimestamp, + onFinish: (transaction) async { + final nativeFrames = await _native + ?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } + } + }, + ); // if _enableAutoTransactions is enabled but there's no traces sample rate if (_transaction is NoOpSentrySpan) { - _transaction2 = null; + _transaction = null; return; } - if (name == 'root ("/")') { + if (arguments != null) { + _transaction?.setData('route_settings_arguments', arguments); + } + + await _hub.configureScope((scope) { + scope.span ??= _transaction; + }); + + await _native?.beginNativeFramesCollection(); + } + + Future _finishTransaction() async { + _transaction?.status ??= SpanStatus.ok(); + await _transaction?.finish(); + } + + void _startMeasurement(Route? route) async { + // Assigning a timestamp within this function so we don't have to force unwrap _startTimestamp + final startTimestamp = DateTime.now(); + _startTimestamp = startTimestamp; + + final routeName = _getRouteName(route); + final isRootScreen = routeName == '/'; + final didFetchAppStart = _native?.didFetchAppStart; + if (isRootScreen && didFetchAppStart == false) { + // This branch is a special edge case that only happens once + AppStartTracker().onAppStartComplete((appStartInfo) async { + // Create a transaction based on app start start time + // Then create ttidSpan and finish immediately with the app start start & end time + // This is a small workaround to pass the correct time stamps since we cannot mutate + // timestamps of transactions or spans in history + if (appStartInfo != null && routeName != null) { + await _startTransaction(route, startTimestamp: appStartInfo.start); + final ttidSpan = + _createTTIDSpan(_transaction!, routeName, appStartInfo.start); + _finishSpan(ttidSpan, _transaction!, appStartInfo.end, + measurement: appStartInfo.measurement); + } + }); } else { - // startTime = DateTime.now(); + await _startTransaction(route, startTimestamp: startTimestamp); + _initializeSpans(_transaction!, routeName!, startTimestamp); + final endTimestamp = await _determineEndTime(routeName); + final duration = endTimestamp.difference(startTimestamp).inMilliseconds; + final measurement = SentryMeasurement('time_to_initial_display', duration, + unit: DurationSentryMeasurementUnit.milliSecond); + _finishSpan(_ttidSpan!, _transaction!, endTimestamp, + measurement: measurement); + } + } + + Future _determineEndTime(String routeName) async { + DateTime? endTimestamp; + final endTimeCompleter = Completer(); + + SchedulerBinding.instance.addPostFrameCallback((_) { + endTimestamp = DateTime.now(); + endTimeCompleter.complete(endTimestamp); + }); - // final ttidSpan = _transaction2?.startChild('ui.load.initial_display', - // description: '$name initial display', startTimestamp: startTime); - // ttidSpan?.origin = 'auto.ui.time_to_display'; - // ttidSpanMap[name] = ttidSpan!; + final strategyDecision = + await DisplayStrategyEvaluator().decideStrategy(routeName); + + if (strategyDecision == StrategyDecision.manual && + !endTimeCompleter.isCompleted) { + endTimestamp = DateTime.now(); + endTimeCompleter.complete(endTimestamp); + } else if (!endTimeCompleter.isCompleted) { + await endTimeCompleter.future; } - // TODO: Needs to finish max within 30 seconds - // If timeout exceeds then it will finish with status deadline exceeded - // What to do if root also has TTFD but it's not finished yet and we start navigating to another? - // How to track the time that 30 sec have passed? - // - // temporarily disable ttfd for root since it somehow swallows other spans - // e.g the complex operation span in autoclosescreen - if ((_hub.options as SentryFlutterOptions).enableTimeToFullDisplayTracing && - name != 'root ("/")') { - print('ttfd'); - ttfdStartTime = DateTime.now(); - ttfdSpan = _transaction2?.startChild('ui.load.full_display', - description: '$name full display', startTimestamp: ttfdStartTime); + return endTimestamp!; + } + + void reportInitiallyDisplayed(String routeName) { + DisplayStrategyEvaluator().reportManual(routeName); + } + + void reportFullyDisplayed() { + final endTimestamp = DateTime.now(); + final duration = endTimestamp.difference(_startTimestamp!).inMilliseconds; + final transaction = Sentry.getSpan(); + if (_ttfdSpan == null || transaction == null) { + return; } + final measurement = SentryMeasurement('time_to_full_display', duration, + unit: DurationSentryMeasurementUnit.milliSecond); + _finishSpan(_ttfdSpan!, transaction, endTimestamp, + measurement: measurement); + } - if (arguments != null) { - _transaction2?.setData('route_settings_arguments', arguments); + void _initializeSpans( + ISentrySpan? transaction, String routeName, DateTime startTimestamp) { + final options = _hub.options is SentryFlutterOptions + // ignore: invalid_use_of_internal_member + ? _hub.options as SentryFlutterOptions + : null; + if (transaction == null) return; + _ttidSpan = _createTTIDSpan(transaction, routeName, startTimestamp); + if (options?.enableTimeToFullDisplayTracing == true) { + _ttfdSpan = _createTTFDSpan(transaction, routeName, startTimestamp); } + } - await _hub.configureScope((scope) { - scope.span ??= _transaction2; - }); + ISentrySpan _createTTIDSpan( + ISentrySpan transaction, String routeName, DateTime startTimestamp) { + return transaction.startChild( + SentryTraceOrigins.uiTimeToInitialDisplay, + description: '$routeName initial display', + startTimestamp: startTimestamp, + ); + } - await _native?.beginNativeFramesCollection(); + ISentrySpan _createTTFDSpan( + ISentrySpan transaction, String routeName, DateTime startTimestamp) { + return transaction.startChild( + SentryTraceOrigins.uiTimeToFullDisplay, + description: '$routeName full display', + startTimestamp: startTimestamp, + ); } - Future _finishTransaction({DateTime? endTimestamp}) async { - _transaction2?.status ??= SpanStatus.ok(); - await _transaction2?.finish(endTimestamp: endTimestamp); + void _finishSpan( + ISentrySpan span, ISentrySpan transaction, DateTime endTimestamp, + {SentryMeasurement? measurement}) { + if (measurement != null) { + transaction.setMeasurement(measurement.name, measurement.value, + unit: measurement.unit); + } + span.finish(endTimestamp: endTimestamp); } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 00da5e5773..50324a0962 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -13,7 +13,6 @@ import 'integrations/connectivity/connectivity_integration.dart'; import 'integrations/screenshot_integration.dart'; import 'native/factory.dart'; import 'native/native_scope_observer.dart'; -import 'navigation/navigation_timing_manager.dart'; import 'profiling.dart'; import 'renderer/renderer.dart'; import 'native/sentry_native.dart'; @@ -233,13 +232,13 @@ mixin SentryFlutter { static void reportInitiallyDisplayed(BuildContext context) { final routeName = ModalRoute.of(context)?.settings.name; if (routeName != null) { - NavigationTimingManager().reportInitiallyDisplayed(routeName); + SentryNavigatorObserver().reportInitiallyDisplayed(routeName); } } /// Reports the time it took for the screen to be fully displayed. static void reportFullyDisplayed() { - NavigationTimingManager().reportFullyDisplayed(); + SentryNavigatorObserver().reportFullyDisplayed(); } @internal From f0420ad4c23fda635fb329882df8f441bdadfedc Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 14 Feb 2024 13:09:42 +0100 Subject: [PATCH 14/47] update --- flutter/example/lib/auto_close_screen.dart | 2 +- flutter/example/lib/main.dart | 2 +- .../display_strategy_evaluator.dart | 9 -- .../navigation/sentry_navigator_observer.dart | 133 ++++++++++++------ flutter/lib/src/sentry_flutter_options.dart | 2 + 5 files changed, 94 insertions(+), 54 deletions(-) diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart index 7caa09a836..e2241cd6aa 100644 --- a/flutter/example/lib/auto_close_screen.dart +++ b/flutter/example/lib/auto_close_screen.dart @@ -27,7 +27,7 @@ class AutoCloseScreenState extends State { description: 'running a $delayInSeconds seconds operation'); await Future.delayed(const Duration(seconds: delayInSeconds)); await childSpan?.finish(); - SentryFlutter.reportFullyDisplayed(); + // SentryFlutter.reportFullyDisplayed(); // ignore: use_build_context_synchronously // Navigator.of(context).pop(); } diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 31a158328b..e1e45a7417 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -727,7 +727,7 @@ void navigateToAutoCloseScreen(BuildContext context) { Navigator.push( context, MaterialPageRoute( - settings: const RouteSettings(name: 'AutoCloseScreen'), + settings: const RouteSettings(name: 'AutoCloseScreen32'), builder: (context) => const SentryDisplayWidget( child: AutoCloseScreen(), )), diff --git a/flutter/lib/src/navigation/display_strategy_evaluator.dart b/flutter/lib/src/navigation/display_strategy_evaluator.dart index 92707dfb8c..a19061c8c4 100644 --- a/flutter/lib/src/navigation/display_strategy_evaluator.dart +++ b/flutter/lib/src/navigation/display_strategy_evaluator.dart @@ -28,7 +28,6 @@ class DisplayStrategyEvaluator { if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { _timers[routeName]?.cancel(); // Cancel any existing timer. _timers[routeName] = Timer(Duration(seconds: 1), () { - // Double-check to prevent race conditions. if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { if (!completer.isCompleted) { completer.complete(StrategyDecision.approximation); @@ -53,14 +52,6 @@ class DisplayStrategyEvaluator { _timers[routeName]?.cancel(); return wasReportedAlready; } - - // TODO: when do we need to clear state? - void clearState(String routeName) { - _manualReportReceived.remove(routeName); - _timers[routeName]?.cancel(); - _timers.remove(routeName); - _completers.remove(routeName); - } } enum StrategyDecision { diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 36fc69e902..a6ab0cca50 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -92,10 +92,17 @@ class SentryNavigatorObserver extends RouteObserver> { final AdditionalInfoExtractor? _additionalInfoProvider; final SentryNative? _native; + SentryFlutterOptions? get _options => _hub.options is SentryFlutterOptions + // ignore: invalid_use_of_internal_member + ? _hub.options as SentryFlutterOptions + : null; + ISentrySpan? _transaction; static DateTime? _startTimestamp; + static DateTime? _ttidEndTimestamp; static ISentrySpan? _ttidSpan; static ISentrySpan? _ttfdSpan; + static Timer? _ttfdTimer; static String? _currentRouteName; @@ -147,7 +154,7 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); - _startMeasurement(previousRoute); + // _startMeasurement(previousRoute); } void _addBreadcrumb({ @@ -205,7 +212,7 @@ class SentryNavigatorObserver extends RouteObserver> { final transactionContext = SentryTransactionContext( name, - 'navigation', + 'ui.load', transactionNameSource: SentryTransactionNameSource.component, // ignore: invalid_use_of_internal_member origin: SentryTraceOrigins.autoNavigationRouteObserver, @@ -216,6 +223,7 @@ class SentryNavigatorObserver extends RouteObserver> { waitForChildren: true, autoFinishAfter: _autoFinishAfter, trimEnd: true, + bindToScope: true, startTimestamp: startTimestamp, onFinish: (transaction) async { final nativeFrames = await _native @@ -244,10 +252,6 @@ class SentryNavigatorObserver extends RouteObserver> { _transaction?.setData('route_settings_arguments', arguments); } - await _hub.configureScope((scope) { - scope.span ??= _transaction; - }); - await _native?.beginNativeFramesCollection(); } @@ -257,7 +261,10 @@ class SentryNavigatorObserver extends RouteObserver> { } void _startMeasurement(Route? route) async { - // Assigning a timestamp within this function so we don't have to force unwrap _startTimestamp + _ttidSpan = null; + _ttfdSpan = null; + _transaction = null; + final startTimestamp = DateTime.now(); _startTimestamp = startTimestamp; @@ -265,33 +272,56 @@ class SentryNavigatorObserver extends RouteObserver> { final isRootScreen = routeName == '/'; final didFetchAppStart = _native?.didFetchAppStart; if (isRootScreen && didFetchAppStart == false) { - // This branch is a special edge case that only happens once - AppStartTracker().onAppStartComplete((appStartInfo) async { - // Create a transaction based on app start start time - // Then create ttidSpan and finish immediately with the app start start & end time - // This is a small workaround to pass the correct time stamps since we cannot mutate - // timestamps of transactions or spans in history - if (appStartInfo != null && routeName != null) { - await _startTransaction(route, startTimestamp: appStartInfo.start); - final ttidSpan = - _createTTIDSpan(_transaction!, routeName, appStartInfo.start); - _finishSpan(ttidSpan, _transaction!, appStartInfo.end, - measurement: appStartInfo.measurement); - } - }); + _handleAppStartMeasurement(route); } else { - await _startTransaction(route, startTimestamp: startTimestamp); - _initializeSpans(_transaction!, routeName!, startTimestamp); - final endTimestamp = await _determineEndTime(routeName); - final duration = endTimestamp.difference(startTimestamp).inMilliseconds; - final measurement = SentryMeasurement('time_to_initial_display', duration, - unit: DurationSentryMeasurementUnit.milliSecond); - _finishSpan(_ttidSpan!, _transaction!, endTimestamp, - measurement: measurement); + _handleRegularRouteMeasurement(route, startTimestamp); } } - Future _determineEndTime(String routeName) async { + /// This method listens for the completion of the app's start process via + /// [AppStartTracker], then: + /// - Starts a transaction with the app start start timestamp + /// - Starts TTID and optionally TTFD spans based on the app start start timestamp + /// - Finishes the TTID span immediately with the app start end timestamp + /// + /// We immediately finish the TTID span since we cannot . + void _handleAppStartMeasurement(Route? route) { + AppStartTracker().onAppStartComplete((appStartInfo) async { + final routeName = _currentRouteName ?? _getRouteName(route); + if (appStartInfo == null || routeName == null) return; + + await _startTransaction(route, startTimestamp: appStartInfo.start); + final transaction = _transaction; + if (transaction == null) return; + + final ttidSpan =` + _createTTIDSpan(transaction, routeName, appStartInfo.start); + if (_options?.enableTimeToFullDisplayTracing == true) { + _ttfdSpan = _createTTFDSpan(transaction, routeName, appStartInfo.start); + } + _finishSpan(ttidSpan, transaction, appStartInfo.end, + measurement: appStartInfo.measurement); + }); + } + + // Handles measuring navigation for regular routes + void _handleRegularRouteMeasurement( + Route? route, DateTime startTimestamp) async { + await _startTransaction(route, startTimestamp: startTimestamp); + + final transaction = _transaction; + final routeName = _currentRouteName ?? _getRouteName(route); + if (transaction == null || routeName == null) return; + + _initializeTimeToDisplaySpans(transaction, routeName, startTimestamp); + + final ttidSpan = _ttidSpan; + if (ttidSpan == null) return; + + await _finishInitialDisplay(ttidSpan, transaction, routeName, startTimestamp); + } + + Future _determineEndTime(String routeName) async { DateTime? endTimestamp; final endTimeCompleter = Completer(); @@ -311,39 +341,56 @@ class SentryNavigatorObserver extends RouteObserver> { await endTimeCompleter.future; } - return endTimestamp!; + return endTimestamp; } + @internal void reportInitiallyDisplayed(String routeName) { DisplayStrategyEvaluator().reportManual(routeName); } + @internal void reportFullyDisplayed() { + _ttfdTimer?.cancel(); final endTimestamp = DateTime.now(); - final duration = endTimestamp.difference(_startTimestamp!).inMilliseconds; + final startTimestamp = _startTimestamp; final transaction = Sentry.getSpan(); - if (_ttfdSpan == null || transaction == null) { + final ttfdSpan = _ttfdSpan; + if (startTimestamp == null || transaction == null || ttfdSpan == null) { return; } + final duration = endTimestamp.difference(startTimestamp).inMilliseconds; final measurement = SentryMeasurement('time_to_full_display', duration, unit: DurationSentryMeasurementUnit.milliSecond); - _finishSpan(_ttfdSpan!, transaction, endTimestamp, - measurement: measurement); + _finishSpan(ttfdSpan, transaction, endTimestamp, measurement: measurement); } - void _initializeSpans( - ISentrySpan? transaction, String routeName, DateTime startTimestamp) { - final options = _hub.options is SentryFlutterOptions - // ignore: invalid_use_of_internal_member - ? _hub.options as SentryFlutterOptions - : null; - if (transaction == null) return; + void _initializeTimeToDisplaySpans( + ISentrySpan transaction, String routeName, DateTime startTimestamp) { _ttidSpan = _createTTIDSpan(transaction, routeName, startTimestamp); - if (options?.enableTimeToFullDisplayTracing == true) { + if (_options?.enableTimeToFullDisplayTracing == true) { _ttfdSpan = _createTTFDSpan(transaction, routeName, startTimestamp); + _ttfdTimer = Timer(Duration(seconds: 6), () { + final ttidEndTimestamp = _ttidEndTimestamp; + if (_ttfdSpan?.finished == true || ttidEndTimestamp == null) { + return; + } + _ttfdSpan?.finish(status: SpanStatus.ok(), endTimestamp: ttidEndTimestamp); + }); } } + Future _finishInitialDisplay(ISentrySpan ttidSpan, ISentrySpan transaction, String routeName, DateTime startTimestamp) async { + final endTimestamp = await _determineEndTime(routeName); + if (endTimestamp == null) return; + _ttidEndTimestamp = endTimestamp; + + final duration = endTimestamp.difference(startTimestamp).inMilliseconds; + final measurement = SentryMeasurement('time_to_initial_display', duration, + unit: DurationSentryMeasurementUnit.milliSecond); + _finishSpan(ttidSpan, transaction, endTimestamp, measurement: measurement); + } + ISentrySpan _createTTIDSpan( ISentrySpan transaction, String routeName, DateTime startTimestamp) { return transaction.startChild( diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index a753ad2b81..e84a68159f 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -224,6 +224,8 @@ class SentryFlutterOptions extends SentryOptions { Duration readTimeout = Duration(seconds: 5); /// Enable or disable the tracing of time to full display (TTFD). + /// If `SentryFlutter.reportFullyDisplayed()` is not called within 30 seconds + /// after the creation of the TTFD span, it will finish with the status [SpanStatus.deadlineExceeded]. /// This feature requires using the [Routing Instrumentation](https://docs.sentry.io/platforms/flutter/integrations/routing-instrumentation/). bool enableTimeToFullDisplayTracing = false; From 4da60a115a4567a035feb526aee5b7122501f3d4 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 14 Feb 2024 13:10:04 +0100 Subject: [PATCH 15/47] update --- flutter/lib/src/navigation/sentry_navigator_observer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index a6ab0cca50..3fe3c53074 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -294,7 +294,7 @@ class SentryNavigatorObserver extends RouteObserver> { final transaction = _transaction; if (transaction == null) return; - final ttidSpan =` + final ttidSpan = _createTTIDSpan(transaction, routeName, appStartInfo.start); if (_options?.enableTimeToFullDisplayTracing == true) { _ttfdSpan = _createTTFDSpan(transaction, routeName, appStartInfo.start); From 5db69e7745ea670fecd43d0f254507dc1351b974 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 14 Feb 2024 14:23:28 +0100 Subject: [PATCH 16/47] fix timestamps --- dart/lib/src/sentry_tracer.dart | 23 +++++++++++-------- flutter/example/lib/auto_close_screen.dart | 4 ++-- .../native_app_start_event_processor.dart | 1 + .../navigation/sentry_navigator_observer.dart | 19 +++++++++------ 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index 6012a13bfb..7a0e8835ae 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -109,18 +109,23 @@ class SentryTracer extends ISentrySpan { } var _rootEndTimestamp = commonEndTimestamp; + + // Trim the end timestamp of the transaction to the very last timestamp of child spans if (_trimEnd && children.isNotEmpty) { - final childEndTimestamps = children - .where((child) => child.endTimestamp != null) - .map((child) => child.endTimestamp!); - - if (childEndTimestamps.isNotEmpty) { - final oldestChildEndTimestamp = - childEndTimestamps.reduce((a, b) => a.isAfter(b) ? a : b); - if (_rootEndTimestamp.isAfter(oldestChildEndTimestamp)) { - _rootEndTimestamp = oldestChildEndTimestamp; + DateTime? latestEndTime; + + for (var child in children) { + final childEndTimestamp = child.endTimestamp; + if (childEndTimestamp != null) { + if (latestEndTime == null || childEndTimestamp.isAfter(latestEndTime)) { + latestEndTime = child.endTimestamp; + } } } + + if (latestEndTime != null) { + _rootEndTimestamp = latestEndTime; + } } // the callback should run before because if the span is finished, diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart index e2241cd6aa..2739246b36 100644 --- a/flutter/example/lib/auto_close_screen.dart +++ b/flutter/example/lib/auto_close_screen.dart @@ -27,9 +27,9 @@ class AutoCloseScreenState extends State { description: 'running a $delayInSeconds seconds operation'); await Future.delayed(const Duration(seconds: delayInSeconds)); await childSpan?.finish(); - // SentryFlutter.reportFullyDisplayed(); + SentryFlutter.reportFullyDisplayed(); // ignore: use_build_context_synchronously - // Navigator.of(context).pop(); + Navigator.of(context).pop(); } @override diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index 0a83ceab4b..4f95c0df8c 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -20,6 +20,7 @@ class NativeAppStartEventProcessor implements EventProcessor { @override Future apply(SentryEvent event, {Hint? hint}) async { final appStartInfo = AppStartTracker().appStartInfo; + // TODO: only do this once per app start if (appStartInfo != null && event is SentryTransaction) { final measurement = appStartInfo.measurement; event.measurements[measurement.name] = measurement; diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 3fe3c53074..5842b334ec 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -318,7 +318,8 @@ class SentryNavigatorObserver extends RouteObserver> { final ttidSpan = _ttidSpan; if (ttidSpan == null) return; - await _finishInitialDisplay(ttidSpan, transaction, routeName, startTimestamp); + await _finishInitialDisplay( + ttidSpan, transaction, routeName, startTimestamp); } Future _determineEndTime(String routeName) async { @@ -371,16 +372,20 @@ class SentryNavigatorObserver extends RouteObserver> { if (_options?.enableTimeToFullDisplayTracing == true) { _ttfdSpan = _createTTFDSpan(transaction, routeName, startTimestamp); _ttfdTimer = Timer(Duration(seconds: 6), () { - final ttidEndTimestamp = _ttidEndTimestamp; - if (_ttfdSpan?.finished == true || ttidEndTimestamp == null) { + if (_ttfdSpan?.finished == true) { return; } - _ttfdSpan?.finish(status: SpanStatus.ok(), endTimestamp: ttidEndTimestamp); + _finishSpan(_ttfdSpan!, transaction, _ttidEndTimestamp!, + status: SpanStatus.deadlineExceeded()); }); } } - Future _finishInitialDisplay(ISentrySpan ttidSpan, ISentrySpan transaction, String routeName, DateTime startTimestamp) async { + Future _finishInitialDisplay( + ISentrySpan ttidSpan, + ISentrySpan transaction, + String routeName, + DateTime startTimestamp) async { final endTimestamp = await _determineEndTime(routeName); if (endTimestamp == null) return; _ttidEndTimestamp = endTimestamp; @@ -411,12 +416,12 @@ class SentryNavigatorObserver extends RouteObserver> { void _finishSpan( ISentrySpan span, ISentrySpan transaction, DateTime endTimestamp, - {SentryMeasurement? measurement}) { + {SentryMeasurement? measurement, SpanStatus? status}) { if (measurement != null) { transaction.setMeasurement(measurement.name, measurement.value, unit: measurement.unit); } - span.finish(endTimestamp: endTimestamp); + span.finish(status: status, endTimestamp: endTimestamp); } } From ab6abbf04d9f4aa613f2f87602e66c1f57265c25 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 14 Feb 2024 14:28:13 +0100 Subject: [PATCH 17/47] update --- flutter/example/lib/auto_close_screen.dart | 2 +- flutter/example/lib/main.dart | 11 ++++++++++- .../src/navigation/display_strategy_evaluator.dart | 12 ++++++------ .../src/navigation/sentry_navigator_observer.dart | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart index 2739246b36..7caa09a836 100644 --- a/flutter/example/lib/auto_close_screen.dart +++ b/flutter/example/lib/auto_close_screen.dart @@ -29,7 +29,7 @@ class AutoCloseScreenState extends State { await childSpan?.finish(); SentryFlutter.reportFullyDisplayed(); // ignore: use_build_context_synchronously - Navigator.of(context).pop(); + // Navigator.of(context).pop(); } @override diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index e1e45a7417..97028d8ab6 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -102,6 +102,15 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { + @override + void initState() { + super.initState(); + // Example of reporting TTFD + Future.delayed( + const Duration(seconds: 1), () => SentryFlutter.reportFullyDisplayed(), + ); + } + @override Widget build(BuildContext context) { return feedback.BetterFeedback( @@ -727,7 +736,7 @@ void navigateToAutoCloseScreen(BuildContext context) { Navigator.push( context, MaterialPageRoute( - settings: const RouteSettings(name: 'AutoCloseScreen32'), + settings: const RouteSettings(name: 'AutoCloseScreen'), builder: (context) => const SentryDisplayWidget( child: AutoCloseScreen(), )), diff --git a/flutter/lib/src/navigation/display_strategy_evaluator.dart b/flutter/lib/src/navigation/display_strategy_evaluator.dart index a19061c8c4..35c73af65a 100644 --- a/flutter/lib/src/navigation/display_strategy_evaluator.dart +++ b/flutter/lib/src/navigation/display_strategy_evaluator.dart @@ -15,12 +15,12 @@ class DisplayStrategyEvaluator { final Map _manualReportReceived = {}; final Map _timers = {}; - final Map> _completers = {}; + final Map> _completers = {}; - Future decideStrategy(String routeName) { + Future decideStrategy(String routeName) { // Ensure initialization of a completer for the given route name. if (!_completers.containsKey(routeName) || _completers[routeName]!.isCompleted) { - _completers[routeName] = Completer(); + _completers[routeName] = Completer(); } var completer = _completers[routeName]!; @@ -30,7 +30,7 @@ class DisplayStrategyEvaluator { _timers[routeName] = Timer(Duration(seconds: 1), () { if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { if (!completer.isCompleted) { - completer.complete(StrategyDecision.approximation); + completer.complete(TimeToDisplayStrategy.approximation); } } }); @@ -45,7 +45,7 @@ class DisplayStrategyEvaluator { // Complete the strategy decision as manual if within the timeout period. if (_completers[routeName]?.isCompleted == false) { - _completers[routeName]?.complete(StrategyDecision.manual); + _completers[routeName]?.complete(TimeToDisplayStrategy.manual); } // Cancel the timer as it's no longer necessary. @@ -54,7 +54,7 @@ class DisplayStrategyEvaluator { } } -enum StrategyDecision { +enum TimeToDisplayStrategy { manual, approximation, } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 5842b334ec..bf7628511e 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -334,7 +334,7 @@ class SentryNavigatorObserver extends RouteObserver> { final strategyDecision = await DisplayStrategyEvaluator().decideStrategy(routeName); - if (strategyDecision == StrategyDecision.manual && + if (strategyDecision == TimeToDisplayStrategy.manual && !endTimeCompleter.isCompleted) { endTimestamp = DateTime.now(); endTimeCompleter.complete(endTimestamp); From 2bfa1500423e61fb6c7fd9d7ca6aa791d0467909 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 14 Feb 2024 15:25:06 +0100 Subject: [PATCH 18/47] refactor --- .../navigation/sentry_navigator_observer.dart | 269 ++---------------- .../navigation/time_to_display_tracker.dart | 268 +++++++++++++++++ flutter/lib/src/sentry_flutter.dart | 5 +- 3 files changed, 288 insertions(+), 254 deletions(-) create mode 100644 flutter/lib/src/navigation/time_to_display_tracker.dart diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index bf7628511e..316bd9dbbf 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -1,14 +1,12 @@ import 'dart:async'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../event_processor/flutter_enricher_event_processor.dart'; -import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; -import 'display_strategy_evaluator.dart'; +import 'time_to_display_tracker.dart'; /// This key must be used so that the web interface displays the events nicely /// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/ @@ -72,37 +70,25 @@ class SentryNavigatorObserver extends RouteObserver> { RouteNameExtractor? routeNameExtractor, AdditionalInfoExtractor? additionalInfoProvider, }) : _hub = hub ?? HubAdapter(), - _enableAutoTransactions = enableAutoTransactions, - _autoFinishAfter = autoFinishAfter, _setRouteNameAsTransaction = setRouteNameAsTransaction, _routeNameExtractor = routeNameExtractor, - _additionalInfoProvider = additionalInfoProvider, - _native = SentryFlutter.native { + _additionalInfoProvider = additionalInfoProvider { if (enableAutoTransactions) { // ignore: invalid_use_of_internal_member _hub.options.sdk.addIntegration('UINavigationTracing'); } + _timeToDisplayTracker = TimeToDisplayTracker( + hub: _hub, + enableAutoTransactions: enableAutoTransactions, + autoFinishAfter: autoFinishAfter, + ); } final Hub _hub; - final bool _enableAutoTransactions; - final Duration _autoFinishAfter; final bool _setRouteNameAsTransaction; final RouteNameExtractor? _routeNameExtractor; final AdditionalInfoExtractor? _additionalInfoProvider; - final SentryNative? _native; - - SentryFlutterOptions? get _options => _hub.options is SentryFlutterOptions - // ignore: invalid_use_of_internal_member - ? _hub.options as SentryFlutterOptions - : null; - - ISentrySpan? _transaction; - static DateTime? _startTimestamp; - static DateTime? _ttidEndTimestamp; - static ISentrySpan? _ttidSpan; - static ISentrySpan? _ttfdSpan; - static Timer? _ttfdTimer; + late final TimeToDisplayTracker _timeToDisplayTracker; static String? _currentRouteName; @@ -123,7 +109,12 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); - _startMeasurement(route); + _startTimeToDisplayTracking(route); + } + + Future _finishTransaction() async { + final transaction = _hub.getSpan(); + await transaction?.finish(); } @override @@ -154,7 +145,6 @@ class SentryNavigatorObserver extends RouteObserver> { ); _finishTransaction(); - // _startMeasurement(previousRoute); } void _addBreadcrumb({ @@ -193,235 +183,10 @@ class SentryNavigatorObserver extends RouteObserver> { } } - Future _startTransaction(Route? route, - {DateTime? startTimestamp}) async { - if (!_enableAutoTransactions) { - return; - } - - String? name = _getRouteName(route); - final arguments = route?.settings.arguments; - - if (name == null) { - return; - } - - if (name == '/') { - name = 'root ("/")'; - } - - final transactionContext = SentryTransactionContext( - name, - 'ui.load', - transactionNameSource: SentryTransactionNameSource.component, - // ignore: invalid_use_of_internal_member - origin: SentryTraceOrigins.autoNavigationRouteObserver, - ); - - _transaction = _hub.startTransactionWithContext( - transactionContext, - waitForChildren: true, - autoFinishAfter: _autoFinishAfter, - trimEnd: true, - bindToScope: true, - startTimestamp: startTimestamp, - onFinish: (transaction) async { - final nativeFrames = await _native - ?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, - ); - } - } - }, - ); - - // if _enableAutoTransactions is enabled but there's no traces sample rate - if (_transaction is NoOpSentrySpan) { - _transaction = null; - return; - } - - if (arguments != null) { - _transaction?.setData('route_settings_arguments', arguments); - } - - await _native?.beginNativeFramesCollection(); - } - - Future _finishTransaction() async { - _transaction?.status ??= SpanStatus.ok(); - await _transaction?.finish(); - } - - void _startMeasurement(Route? route) async { - _ttidSpan = null; - _ttfdSpan = null; - _transaction = null; - - final startTimestamp = DateTime.now(); - _startTimestamp = startTimestamp; - + Future _startTimeToDisplayTracking(Route? route) async { final routeName = _getRouteName(route); - final isRootScreen = routeName == '/'; - final didFetchAppStart = _native?.didFetchAppStart; - if (isRootScreen && didFetchAppStart == false) { - _handleAppStartMeasurement(route); - } else { - _handleRegularRouteMeasurement(route, startTimestamp); - } - } - - /// This method listens for the completion of the app's start process via - /// [AppStartTracker], then: - /// - Starts a transaction with the app start start timestamp - /// - Starts TTID and optionally TTFD spans based on the app start start timestamp - /// - Finishes the TTID span immediately with the app start end timestamp - /// - /// We immediately finish the TTID span since we cannot . - void _handleAppStartMeasurement(Route? route) { - AppStartTracker().onAppStartComplete((appStartInfo) async { - final routeName = _currentRouteName ?? _getRouteName(route); - if (appStartInfo == null || routeName == null) return; - - await _startTransaction(route, startTimestamp: appStartInfo.start); - final transaction = _transaction; - if (transaction == null) return; - - final ttidSpan = - _createTTIDSpan(transaction, routeName, appStartInfo.start); - if (_options?.enableTimeToFullDisplayTracing == true) { - _ttfdSpan = _createTTFDSpan(transaction, routeName, appStartInfo.start); - } - _finishSpan(ttidSpan, transaction, appStartInfo.end, - measurement: appStartInfo.measurement); - }); - } - - // Handles measuring navigation for regular routes - void _handleRegularRouteMeasurement( - Route? route, DateTime startTimestamp) async { - await _startTransaction(route, startTimestamp: startTimestamp); - - final transaction = _transaction; - final routeName = _currentRouteName ?? _getRouteName(route); - if (transaction == null || routeName == null) return; - - _initializeTimeToDisplaySpans(transaction, routeName, startTimestamp); - - final ttidSpan = _ttidSpan; - if (ttidSpan == null) return; - - await _finishInitialDisplay( - ttidSpan, transaction, routeName, startTimestamp); - } - - Future _determineEndTime(String routeName) async { - DateTime? endTimestamp; - final endTimeCompleter = Completer(); - - SchedulerBinding.instance.addPostFrameCallback((_) { - endTimestamp = DateTime.now(); - endTimeCompleter.complete(endTimestamp); - }); - - final strategyDecision = - await DisplayStrategyEvaluator().decideStrategy(routeName); - - if (strategyDecision == TimeToDisplayStrategy.manual && - !endTimeCompleter.isCompleted) { - endTimestamp = DateTime.now(); - endTimeCompleter.complete(endTimestamp); - } else if (!endTimeCompleter.isCompleted) { - await endTimeCompleter.future; - } - - return endTimestamp; - } - - @internal - void reportInitiallyDisplayed(String routeName) { - DisplayStrategyEvaluator().reportManual(routeName); - } - - @internal - void reportFullyDisplayed() { - _ttfdTimer?.cancel(); - final endTimestamp = DateTime.now(); - final startTimestamp = _startTimestamp; - final transaction = Sentry.getSpan(); - final ttfdSpan = _ttfdSpan; - if (startTimestamp == null || transaction == null || ttfdSpan == null) { - return; - } - final duration = endTimestamp.difference(startTimestamp).inMilliseconds; - final measurement = SentryMeasurement('time_to_full_display', duration, - unit: DurationSentryMeasurementUnit.milliSecond); - _finishSpan(ttfdSpan, transaction, endTimestamp, measurement: measurement); - } - - void _initializeTimeToDisplaySpans( - ISentrySpan transaction, String routeName, DateTime startTimestamp) { - _ttidSpan = _createTTIDSpan(transaction, routeName, startTimestamp); - if (_options?.enableTimeToFullDisplayTracing == true) { - _ttfdSpan = _createTTFDSpan(transaction, routeName, startTimestamp); - _ttfdTimer = Timer(Duration(seconds: 6), () { - if (_ttfdSpan?.finished == true) { - return; - } - _finishSpan(_ttfdSpan!, transaction, _ttidEndTimestamp!, - status: SpanStatus.deadlineExceeded()); - }); - } - } - - Future _finishInitialDisplay( - ISentrySpan ttidSpan, - ISentrySpan transaction, - String routeName, - DateTime startTimestamp) async { - final endTimestamp = await _determineEndTime(routeName); - if (endTimestamp == null) return; - _ttidEndTimestamp = endTimestamp; - - final duration = endTimestamp.difference(startTimestamp).inMilliseconds; - final measurement = SentryMeasurement('time_to_initial_display', duration, - unit: DurationSentryMeasurementUnit.milliSecond); - _finishSpan(ttidSpan, transaction, endTimestamp, measurement: measurement); - } - - ISentrySpan _createTTIDSpan( - ISentrySpan transaction, String routeName, DateTime startTimestamp) { - return transaction.startChild( - SentryTraceOrigins.uiTimeToInitialDisplay, - description: '$routeName initial display', - startTimestamp: startTimestamp, - ); - } - - ISentrySpan _createTTFDSpan( - ISentrySpan transaction, String routeName, DateTime startTimestamp) { - return transaction.startChild( - SentryTraceOrigins.uiTimeToFullDisplay, - description: '$routeName full display', - startTimestamp: startTimestamp, - ); - } - - void _finishSpan( - ISentrySpan span, ISentrySpan transaction, DateTime endTimestamp, - {SentryMeasurement? measurement, SpanStatus? status}) { - if (measurement != null) { - transaction.setMeasurement(measurement.name, measurement.value, - unit: measurement.unit); - } - span.finish(status: status, endTimestamp: endTimestamp); + final arguments = route?.settings.arguments; + _timeToDisplayTracker.startMeasurement(routeName, arguments); } } diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart new file mode 100644 index 0000000000..0b3c487185 --- /dev/null +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -0,0 +1,268 @@ +import 'dart:async'; + +import 'package:flutter/scheduler.dart'; +import 'package:meta/meta.dart'; + +import '../../sentry_flutter.dart'; +import '../integrations/integrations.dart'; +import '../native/sentry_native.dart'; +import 'display_strategy_evaluator.dart'; + +@internal +class TimeToDisplayTracker { + final Hub _hub; + final bool _enableAutoTransactions; + final Duration _autoFinishAfter; + final SentryNative? _native; + + ISentrySpan? _transaction; + static DateTime? _startTimestamp; + static DateTime? _ttidEndTimestamp; + static ISentrySpan? _ttidSpan; + static ISentrySpan? _ttfdSpan; + static Timer? _ttfdTimer; + + SentryFlutterOptions? get _options => _hub.options is SentryFlutterOptions + // ignore: invalid_use_of_internal_member + ? _hub.options as SentryFlutterOptions + : null; + + TimeToDisplayTracker({ + required Hub? hub, + required bool enableAutoTransactions, + required Duration autoFinishAfter, + }) : _hub = hub ?? HubAdapter(), + _enableAutoTransactions = enableAutoTransactions, + _autoFinishAfter = autoFinishAfter, + _native = SentryFlutter.native; + + Future _startTransaction(String? routeName, Object? arguments, + {DateTime? startTimestamp}) async { + if (!_enableAutoTransactions) { + return null; + } + + if (routeName == null) { + return null; + } + + if (routeName == '/') { + routeName = 'root ("/")'; + } + + final transactionContext = SentryTransactionContext( + routeName, + 'ui.load', + transactionNameSource: SentryTransactionNameSource.component, + // ignore: invalid_use_of_internal_member + origin: SentryTraceOrigins.autoNavigationRouteObserver, + ); + + final transaction = _hub.startTransactionWithContext( + transactionContext, + waitForChildren: true, + autoFinishAfter: _autoFinishAfter, + trimEnd: true, + bindToScope: true, + startTimestamp: startTimestamp, + onFinish: (transaction) async { + final nativeFrames = await _native + ?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } + } + }, + ); + _transaction = transaction; + + // if _enableAutoTransactions is enabled but there's no traces sample rate + if (transaction is NoOpSentrySpan) { + return null; + } + + if (arguments != null) { + transaction.setData('route_settings_arguments', arguments); + } + + await _native?.beginNativeFramesCollection(); + + return transaction; + } + + void startMeasurement(String? routeName, Object? arguments) async { + _ttidSpan = null; + _ttfdSpan = null; + _transaction = null; + + final startTimestamp = DateTime.now(); + _startTimestamp = startTimestamp; + + final isRootScreen = routeName == '/'; + final didFetchAppStart = _native?.didFetchAppStart; + if (isRootScreen && didFetchAppStart == false) { + _handleAppStartMeasurement(routeName, arguments); + } else { + _handleRegularRouteMeasurement(routeName, arguments, startTimestamp); + } + } + + /// This method listens for the completion of the app's start process via + /// [AppStartTracker], then: + /// - Starts a transaction with the app start start timestamp + /// - Starts TTID and optionally TTFD spans based on the app start start timestamp + /// - Finishes the TTID span immediately with the app start end timestamp + /// + /// We start and immediately finish the TTID span since we cannot mutate the history of spans. + void _handleAppStartMeasurement(String? routeName, Object? arguments) { + AppStartTracker().onAppStartComplete((appStartInfo) async { + final routeName = SentryNavigatorObserver.currentRouteName; + if (appStartInfo == null || routeName == null) return; + + final transaction = await _startTransaction(routeName, arguments, + startTimestamp: appStartInfo.start); + if (transaction == null) return; + + final ttidSpan = + _createTTIDSpan(transaction, routeName, appStartInfo.start); + if (_options?.enableTimeToFullDisplayTracing == true) { + _ttfdSpan = _createTTFDSpan(transaction, routeName, appStartInfo.start); + } + _finishSpan(ttidSpan, transaction, appStartInfo.end, + measurement: appStartInfo.measurement); + }); + } + + // Handles measuring navigation for regular routes + void _handleRegularRouteMeasurement( + String? routeName, Object? arguments, DateTime startTimestamp) async { + final transaction = await _startTransaction(routeName, arguments, + startTimestamp: startTimestamp); + + if (transaction == null || routeName == null) return; + + _initializeTimeToDisplaySpans(transaction, routeName, startTimestamp); + + final ttidSpan = _ttidSpan; + if (ttidSpan == null) return; + + _finishInitialDisplay( + ttidSpan, transaction, routeName, startTimestamp); + } + + void _initializeTimeToDisplaySpans( + ISentrySpan transaction, String routeName, DateTime startTimestamp) { + _ttidSpan = _createTTIDSpan(transaction, routeName, startTimestamp); + if (_options?.enableTimeToFullDisplayTracing == true) { + _ttfdSpan = _createTTFDSpan(transaction, routeName, startTimestamp); + final ttfdAutoFinishAfter = Duration(seconds: 30); + _ttfdTimer = Timer(ttfdAutoFinishAfter, () { + if (_ttfdSpan?.finished == true) { + return; + } + _finishSpan(_ttfdSpan!, transaction, _ttidEndTimestamp!, + status: SpanStatus.deadlineExceeded()); + }); + } + } + + ISentrySpan _createTTIDSpan( + ISentrySpan transaction, String routeName, DateTime startTimestamp) { + return transaction.startChild( + SentryTraceOrigins.uiTimeToInitialDisplay, + description: '$routeName initial display', + startTimestamp: startTimestamp, + ); + } + + ISentrySpan _createTTFDSpan( + ISentrySpan transaction, String routeName, DateTime startTimestamp) { + return transaction.startChild( + SentryTraceOrigins.uiTimeToFullDisplay, + description: '$routeName full display', + startTimestamp: startTimestamp, + ); + } + + Future _determineEndTimeOfTTID(String routeName) async { + DateTime? endTimestamp; + final endTimeCompleter = Completer(); + + SchedulerBinding.instance.addPostFrameCallback((_) { + endTimestamp = DateTime.now(); + endTimeCompleter.complete(endTimestamp); + }); + + final strategyDecision = + await DisplayStrategyEvaluator().decideStrategy(routeName); + + if (strategyDecision == TimeToDisplayStrategy.manual && + !endTimeCompleter.isCompleted) { + endTimestamp = DateTime.now(); + endTimeCompleter.complete(endTimestamp); + } else if (!endTimeCompleter.isCompleted) { + // In approximation we want to wait until addPostFrameCallback has triggered + await endTimeCompleter.future; + } + + return endTimestamp; + } + + + @internal + static void reportInitiallyDisplayed(String routeName) { + DisplayStrategyEvaluator().reportManual(routeName); + } + + @internal + static void reportFullyDisplayed() { + _finishFullDisplay(); + } + + static void _finishFullDisplay() { + _ttfdTimer?.cancel(); + final endTimestamp = DateTime.now(); + final startTimestamp = _startTimestamp; + final transaction = Sentry.getSpan(); + final ttfdSpan = _ttfdSpan; + if (startTimestamp == null || transaction == null || ttfdSpan == null) { + return; + } + final duration = endTimestamp.difference(startTimestamp).inMilliseconds; + final measurement = SentryMeasurement('time_to_full_display', duration, + unit: DurationSentryMeasurementUnit.milliSecond); + _finishSpan(ttfdSpan, transaction, endTimestamp, measurement: measurement); + } + + void _finishInitialDisplay( + ISentrySpan ttidSpan, + ISentrySpan transaction, + String routeName, + DateTime startTimestamp) async { + final endTimestamp = await _determineEndTimeOfTTID(routeName); + if (endTimestamp == null) return; + _ttidEndTimestamp = endTimestamp; + + final duration = endTimestamp.difference(startTimestamp).inMilliseconds; + final measurement = SentryMeasurement('time_to_initial_display', duration, + unit: DurationSentryMeasurementUnit.milliSecond); + _finishSpan(ttidSpan, transaction, endTimestamp, measurement: measurement); + } + + static void _finishSpan( + ISentrySpan span, ISentrySpan transaction, DateTime endTimestamp, + {SentryMeasurement? measurement, SpanStatus? status}) { + if (measurement != null) { + transaction.setMeasurement(measurement.name, measurement.value, + unit: measurement.unit); + } + span.finish(status: status, endTimestamp: endTimestamp); + } +} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 50324a0962..eb0798bc31 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -13,6 +13,7 @@ import 'integrations/connectivity/connectivity_integration.dart'; import 'integrations/screenshot_integration.dart'; import 'native/factory.dart'; import 'native/native_scope_observer.dart'; +import 'navigation/time_to_display_tracker.dart'; import 'profiling.dart'; import 'renderer/renderer.dart'; import 'native/sentry_native.dart'; @@ -232,13 +233,13 @@ mixin SentryFlutter { static void reportInitiallyDisplayed(BuildContext context) { final routeName = ModalRoute.of(context)?.settings.name; if (routeName != null) { - SentryNavigatorObserver().reportInitiallyDisplayed(routeName); + TimeToDisplayTracker.reportInitiallyDisplayed(routeName); } } /// Reports the time it took for the screen to be fully displayed. static void reportFullyDisplayed() { - SentryNavigatorObserver().reportFullyDisplayed(); + TimeToDisplayTracker.reportFullyDisplayed(); } @internal From 0f1b10b83c1c71e188292215c5d2246ee5b39fac Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 14 Feb 2024 17:09:06 +0100 Subject: [PATCH 19/47] add tests --- .../navigation/sentry_navigator_observer.dart | 12 ++++---- .../test/sentry_navigator_observer_test.dart | 30 +++++++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 316bd9dbbf..f2bf74a33d 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -69,6 +69,7 @@ class SentryNavigatorObserver extends RouteObserver> { bool setRouteNameAsTransaction = false, RouteNameExtractor? routeNameExtractor, AdditionalInfoExtractor? additionalInfoProvider, + @internal TimeToDisplayTracker? timeToDisplayTracker, }) : _hub = hub ?? HubAdapter(), _setRouteNameAsTransaction = setRouteNameAsTransaction, _routeNameExtractor = routeNameExtractor, @@ -77,11 +78,12 @@ class SentryNavigatorObserver extends RouteObserver> { // ignore: invalid_use_of_internal_member _hub.options.sdk.addIntegration('UINavigationTracing'); } - _timeToDisplayTracker = TimeToDisplayTracker( - hub: _hub, - enableAutoTransactions: enableAutoTransactions, - autoFinishAfter: autoFinishAfter, - ); + _timeToDisplayTracker = timeToDisplayTracker ?? + TimeToDisplayTracker( + hub: _hub, + enableAutoTransactions: enableAutoTransactions, + autoFinishAfter: autoFinishAfter, + ); } final Hub _hub; diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index c49588ab85..f77040c52f 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -7,6 +7,7 @@ import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/native/sentry_native.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; import 'mocks.dart'; import 'mocks.mocks.dart'; @@ -50,18 +51,36 @@ void main() { }); test('transaction start begins frames collection', () async { + WidgetsFlutterBinding.ensureInitialized(); final currentRoute = route(RouteSettings(name: 'Current Route')); final mockHub = _MockHub(); final tracer = getMockSentryTracer(); _whenAnyStart(mockHub, tracer); + when(mockHub.getSpan()).thenReturn(tracer); + + when(tracer.startChild( + 'ui.load.initial_display', // Matches any string for the operation argument + description: anyNamed('description'), // Matches any description + startTimestamp: anyNamed('startTimestamp') // Matches any startTimestamp + )).thenReturn(NoOpSentrySpan()); + + when(tracer.startChild( + 'ui.load.full_display', // Matches any string for the operation argument + description: anyNamed('description'), // Matches any description + startTimestamp: anyNamed('startTimestamp') // Matches any startTimestamp + )).thenReturn(NoOpSentrySpan()); + + // when(mockTimeToDisplayTracker.startMeasurement(any, any)).thenAnswer((realInvocation) async {}); final sut = fixture.getSut(hub: mockHub); sut.didPush(currentRoute, null); + // verify(mockTimeToDisplayTracker.startMeasurement(any, any)).called(1); + // Handle internal async method calls. - await Future.delayed(const Duration(milliseconds: 10), () { + await Future.delayed(const Duration(milliseconds: 500), () { expect(mockNativeChannel.numberOfBeginNativeFramesCalls, 1); }); }); @@ -805,6 +824,12 @@ class Fixture { RouteNameExtractor? routeNameExtractor, AdditionalInfoExtractor? additionalInfoProvider, }) { + final timeToDisplayTracker = TimeToDisplayTracker( + hub: hub, + enableAutoTransactions: enableAutoTransactions, + autoFinishAfter: autoFinishAfter, + ); + return SentryNavigatorObserver( hub: hub, enableAutoTransactions: enableAutoTransactions, @@ -812,6 +837,7 @@ class Fixture { setRouteNameAsTransaction: setRouteNameAsTransaction, routeNameExtractor: routeNameExtractor, additionalInfoProvider: additionalInfoProvider, + timeToDisplayTracker: timeToDisplayTracker, ); } @@ -822,7 +848,7 @@ class Fixture { class _MockHub extends MockHub { @override - final options = defaultTestOptions(); + final options = defaultTestOptions()..enableTimeToFullDisplayTracing = true; @override late final scope = Scope(options); From bee35cfb5c332cfa7c82e4c00ff9e9b8db77e029 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 15 Feb 2024 18:38:15 +0100 Subject: [PATCH 20/47] update --- dart/lib/src/sentry_measurement.dart | 16 ++++ flutter/example/lib/main.dart | 4 +- .../native_app_start_event_processor.dart | 19 ++-- .../native_app_start_integration.dart | 26 ++++-- .../display_strategy_evaluator.dart | 11 ++- .../navigation/time_to_display_tracker.dart | 20 ++--- .../native_app_start_integration_test.dart | 72 +++++++++++++-- .../navigation/fake_app_start_tracker.dart | 24 +++++ .../time_to_display_tracker_test.dart | 88 +++++++++++++++++++ .../test/sentry_navigator_observer_test.dart | 40 +++++---- 10 files changed, 262 insertions(+), 58 deletions(-) create mode 100644 flutter/test/navigation/fake_app_start_tracker.dart create mode 100644 flutter/test/navigation/time_to_display_tracker_test.dart diff --git a/dart/lib/src/sentry_measurement.dart b/dart/lib/src/sentry_measurement.dart index 481c513e9b..3651d89b37 100644 --- a/dart/lib/src/sentry_measurement.dart +++ b/dart/lib/src/sentry_measurement.dart @@ -39,6 +39,22 @@ class SentryMeasurement { value = duration.inMilliseconds, unit = DurationSentryMeasurementUnit.milliSecond; + /// Duration of the time to initial display in milliseconds + SentryMeasurement.timeToInitialDisplay(Duration duration) + : assert(!duration.isNegative), + name = 'time_to_initial_display', + value = duration.inMilliseconds, + unit = DurationSentryMeasurementUnit.milliSecond; + + /// Duration of the time to full display in milliseconds + SentryMeasurement.timeToFullDisplay(Duration duration) + : assert(!duration.isNegative), + name = 'time_to_full_display', + value = duration.inMilliseconds, + unit = DurationSentryMeasurementUnit.milliSecond; + + // TODO: might wanna move ttid/ttfd to flutter since we don't have it on pure dart + final String name; final num value; final SentryMeasurementUnit? unit; diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 97028d8ab6..a4557f01a9 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -101,13 +101,13 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - @override void initState() { super.initState(); // Example of reporting TTFD Future.delayed( - const Duration(seconds: 1), () => SentryFlutter.reportFullyDisplayed(), + const Duration(seconds: 1), + () => SentryFlutter.reportFullyDisplayed(), ); } diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index 4f95c0df8c..60f48906d2 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -11,19 +11,24 @@ class NativeAppStartEventProcessor implements EventProcessor { /// We filter out App starts more than 60s static const _maxAppStartMillis = 60000; - NativeAppStartEventProcessor( - this._native, - ); + final IAppStartTracker? _appStartTracker; - final SentryNative _native; + NativeAppStartEventProcessor({ + IAppStartTracker? appStartTracker, + }) : _appStartTracker = appStartTracker ?? AppStartTracker(); + + bool didAddAppStartMeasurement = false; @override Future apply(SentryEvent event, {Hint? hint}) async { - final appStartInfo = AppStartTracker().appStartInfo; + final measurement = _appStartTracker?.appStartInfo?.measurement; // TODO: only do this once per app start - if (appStartInfo != null && event is SentryTransaction) { - final measurement = appStartInfo.measurement; + if (!didAddAppStartMeasurement && + measurement != null && + measurement.value.toInt() <= _maxAppStartMillis && + event is SentryTransaction) { event.measurements[measurement.name] = measurement; + didAddAppStartMeasurement = true; } return event; } diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 9233e15ee7..7331f61fb2 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -10,10 +10,13 @@ import '../event_processor/native_app_start_event_processor.dart'; /// Integration which handles communication with native frameworks in order to /// enrich [SentryTransaction] objects with app start data for mobile vitals. class NativeAppStartIntegration extends Integration { - NativeAppStartIntegration(this._native, this._schedulerBindingProvider); + NativeAppStartIntegration(this._native, this._schedulerBindingProvider, + {IAppStartTracker? appStartTracker}) + : _appStartTracker = appStartTracker ?? AppStartTracker(); final SentryNative _native; final SchedulerBindingProvider _schedulerBindingProvider; + final IAppStartTracker? _appStartTracker; @override void call(Hub hub, SentryFlutterOptions options) { @@ -43,7 +46,7 @@ class NativeAppStartIntegration extends Integration { if (nativeAppStart == null || measurement == null || measurement.value >= 60000) { - AppStartTracker().setAppStartInfo(null); + _appStartTracker?.setAppStartInfo(null); return; } @@ -54,15 +57,15 @@ class NativeAppStartIntegration extends Integration { measurement, ); - AppStartTracker().setAppStartInfo(appStartInfo); + _appStartTracker?.setAppStartInfo(appStartInfo); } else { - AppStartTracker().setAppStartInfo(null); + _appStartTracker?.setAppStartInfo(null); } }); } } - options.addEventProcessor(NativeAppStartEventProcessor(_native)); + options.addEventProcessor(NativeAppStartEventProcessor(appStartTracker: _appStartTracker)); options.sdk.addIntegration('nativeAppStartIntegration'); } @@ -80,24 +83,35 @@ class AppStartInfo { AppStartInfo(this.start, this.end, this.measurement); } +abstract class IAppStartTracker { + AppStartInfo? get appStartInfo; + + void setAppStartInfo(AppStartInfo? appStartInfo); + + void onAppStartComplete(Function(AppStartInfo?) callback); +} + @internal -class AppStartTracker { +class AppStartTracker extends IAppStartTracker { static final AppStartTracker _instance = AppStartTracker._internal(); factory AppStartTracker() => _instance; AppStartInfo? _appStartInfo; + @override AppStartInfo? get appStartInfo => _appStartInfo; Function(AppStartInfo?)? _callback; AppStartTracker._internal(); + @override void setAppStartInfo(AppStartInfo? appStartInfo) { _appStartInfo = appStartInfo; _notifyObserver(); } + @override void onAppStartComplete(Function(AppStartInfo?) callback) { _callback = callback; _callback?.call(_appStartInfo); diff --git a/flutter/lib/src/navigation/display_strategy_evaluator.dart b/flutter/lib/src/navigation/display_strategy_evaluator.dart index 35c73af65a..7bc2c829cc 100644 --- a/flutter/lib/src/navigation/display_strategy_evaluator.dart +++ b/flutter/lib/src/navigation/display_strategy_evaluator.dart @@ -5,7 +5,7 @@ import 'package:meta/meta.dart'; @internal class DisplayStrategyEvaluator { static final DisplayStrategyEvaluator _instance = - DisplayStrategyEvaluator._internal(); + DisplayStrategyEvaluator._internal(); factory DisplayStrategyEvaluator() { return _instance; @@ -19,16 +19,19 @@ class DisplayStrategyEvaluator { Future decideStrategy(String routeName) { // Ensure initialization of a completer for the given route name. - if (!_completers.containsKey(routeName) || _completers[routeName]!.isCompleted) { + if (!_completers.containsKey(routeName) || + _completers[routeName]!.isCompleted) { _completers[routeName] = Completer(); } var completer = _completers[routeName]!; // Start or reset the timer only if a manual report has not been received. - if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { + if (!_manualReportReceived.containsKey(routeName) || + !_manualReportReceived[routeName]!) { _timers[routeName]?.cancel(); // Cancel any existing timer. _timers[routeName] = Timer(Duration(seconds: 1), () { - if (!_manualReportReceived.containsKey(routeName) || !_manualReportReceived[routeName]!) { + if (!_manualReportReceived.containsKey(routeName) || + !_manualReportReceived[routeName]!) { if (!completer.isCompleted) { completer.complete(TimeToDisplayStrategy.approximation); } diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index 0b3c487185..fcc5f998e0 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -15,7 +15,6 @@ class TimeToDisplayTracker { final Duration _autoFinishAfter; final SentryNative? _native; - ISentrySpan? _transaction; static DateTime? _startTimestamp; static DateTime? _ttidEndTimestamp; static ISentrySpan? _ttidSpan; @@ -81,7 +80,6 @@ class TimeToDisplayTracker { } }, ); - _transaction = transaction; // if _enableAutoTransactions is enabled but there's no traces sample rate if (transaction is NoOpSentrySpan) { @@ -100,7 +98,6 @@ class TimeToDisplayTracker { void startMeasurement(String? routeName, Object? arguments) async { _ttidSpan = null; _ttfdSpan = null; - _transaction = null; final startTimestamp = DateTime.now(); _startTimestamp = startTimestamp; @@ -153,8 +150,7 @@ class TimeToDisplayTracker { final ttidSpan = _ttidSpan; if (ttidSpan == null) return; - _finishInitialDisplay( - ttidSpan, transaction, routeName, startTimestamp); + _finishInitialDisplay(ttidSpan, transaction, routeName, startTimestamp); } void _initializeTimeToDisplaySpans( @@ -201,12 +197,10 @@ class TimeToDisplayTracker { }); final strategyDecision = - await DisplayStrategyEvaluator().decideStrategy(routeName); + await DisplayStrategyEvaluator().decideStrategy(routeName); - if (strategyDecision == TimeToDisplayStrategy.manual && - !endTimeCompleter.isCompleted) { + if (strategyDecision == TimeToDisplayStrategy.manual) { endTimestamp = DateTime.now(); - endTimeCompleter.complete(endTimestamp); } else if (!endTimeCompleter.isCompleted) { // In approximation we want to wait until addPostFrameCallback has triggered await endTimeCompleter.future; @@ -215,7 +209,6 @@ class TimeToDisplayTracker { return endTimestamp; } - @internal static void reportInitiallyDisplayed(String routeName) { DisplayStrategyEvaluator().reportManual(routeName); @@ -241,11 +234,8 @@ class TimeToDisplayTracker { _finishSpan(ttfdSpan, transaction, endTimestamp, measurement: measurement); } - void _finishInitialDisplay( - ISentrySpan ttidSpan, - ISentrySpan transaction, - String routeName, - DateTime startTimestamp) async { + void _finishInitialDisplay(ISentrySpan ttidSpan, ISentrySpan transaction, + String routeName, DateTime startTimestamp) async { final endTimestamp = await _determineEndTimeOfTTID(routeName); if (endTimestamp == null) return; _ttidEndTimestamp = endTimestamp; diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index d4b8deaaf5..144eabd77d 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -9,6 +9,7 @@ import 'package:sentry/src/sentry_tracer.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; +import '../navigation/fake_app_start_tracker.dart'; void main() { group('$NativeAppStartIntegration', () { @@ -22,8 +23,14 @@ void main() { test('native app start measurement added to first transaction', () async { fixture.options.autoAppStart = false; - fixture.native.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - fixture.binding.nativeAppStart = NativeAppStart(0, true); + fixture.appStartTracker.setAppStartInfo( + AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0), + DateTime.fromMillisecondsSinceEpoch(10), + SentryMeasurement('app_start_cold', 10, + unit: DurationSentryMeasurementUnit.milliSecond), + ), + ); fixture.getNativeAppStartIntegration().call(fixture.hub, fixture.options); @@ -41,8 +48,14 @@ void main() { test('native app start measurement not added to following transactions', () async { fixture.options.autoAppStart = false; - fixture.native.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - fixture.binding.nativeAppStart = NativeAppStart(0, true); + fixture.appStartTracker.setAppStartInfo( + AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0), + DateTime.fromMillisecondsSinceEpoch(10), + SentryMeasurement('app_start_cold', 10, + unit: DurationSentryMeasurementUnit.milliSecond), + ), + ); fixture.getNativeAppStartIntegration().call(fixture.hub, fixture.options); @@ -59,8 +72,14 @@ void main() { test('measurements appended', () async { fixture.options.autoAppStart = false; - fixture.native.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - fixture.binding.nativeAppStart = NativeAppStart(0, true); + fixture.appStartTracker.setAppStartInfo( + AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0), + DateTime.fromMillisecondsSinceEpoch(10), + SentryMeasurement('app_start_cold', 10, + unit: DurationSentryMeasurementUnit.milliSecond), + ), + ); final measurement = SentryMeasurement.warmAppStart(Duration(seconds: 1)); fixture.getNativeAppStartIntegration().call(fixture.hub, fixture.options); @@ -80,8 +99,14 @@ void main() { test('native app start measurement not added if more than 60s', () async { fixture.options.autoAppStart = false; - fixture.native.appStartEnd = DateTime.fromMillisecondsSinceEpoch(60001); - fixture.binding.nativeAppStart = NativeAppStart(0, true); + fixture.appStartTracker.setAppStartInfo( + AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0), + DateTime.fromMillisecondsSinceEpoch(60001), + SentryMeasurement('app_start_cold', 60001, + unit: DurationSentryMeasurementUnit.milliSecond), + ), + ); fixture.getNativeAppStartIntegration().call(fixture.hub, fixture.options); @@ -93,6 +118,35 @@ void main() { expect(enriched.measurements.isEmpty, true); }); + + + test('native app start measurement only added once for multiple different transactions', () async { + fixture.options.autoAppStart = false; + await fixture.native.fetchNativeAppStart(); + fixture.appStartTracker.setAppStartInfo( + AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0), + DateTime.fromMillisecondsSinceEpoch(10), + SentryMeasurement('app_start_cold', 10, + unit: DurationSentryMeasurementUnit.milliSecond), + ), + ); + + fixture.getNativeAppStartIntegration().call(fixture.hub, fixture.options); + + final processor = fixture.options.eventProcessors.first; + final tracer = fixture.createTracer(); + + // Represents the app start transaction + final transaction = SentryTransaction(tracer); + var enriched = await processor.apply(transaction) as SentryTransaction; + expect(enriched.measurements.length, 1); + + // Represents any other transaction that happened afterwards + final transaction2 = SentryTransaction(tracer); + var secondEnriched = await processor.apply(transaction2) as SentryTransaction; + expect(secondEnriched.measurements.length, 0); + }); }); } @@ -100,6 +154,7 @@ class Fixture { final hub = MockHub(); final options = SentryFlutterOptions(dsn: fakeDsn); final binding = MockNativeChannel(); + final appStartTracker = FakeAppStartTracker(); late final native = SentryNative(options, binding); Fixture() { @@ -113,6 +168,7 @@ class Fixture { () { return TestWidgetsFlutterBinding.ensureInitialized(); }, + appStartTracker: appStartTracker, ); } diff --git a/flutter/test/navigation/fake_app_start_tracker.dart b/flutter/test/navigation/fake_app_start_tracker.dart new file mode 100644 index 0000000000..31eb5fb5cb --- /dev/null +++ b/flutter/test/navigation/fake_app_start_tracker.dart @@ -0,0 +1,24 @@ +import 'package:sentry_flutter/src/integrations/native_app_start_integration.dart'; + +class FakeAppStartTracker extends IAppStartTracker { + static final FakeAppStartTracker _instance = FakeAppStartTracker._internal(); + + factory FakeAppStartTracker() => _instance; + + AppStartInfo? _appStartInfo; + + FakeAppStartTracker._internal(); + + @override + AppStartInfo? get appStartInfo => _appStartInfo; + + @override + void onAppStartComplete(void Function(AppStartInfo?) callback) { + callback(_appStartInfo); + } + + @override + void setAppStartInfo(AppStartInfo? appStartInfo) { + _appStartInfo = appStartInfo; + } +} diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart new file mode 100644 index 0000000000..1d3c106db4 --- /dev/null +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; +import 'package:sentry/src/sentry_tracer.dart'; + +import '../mocks.dart'; + +void main() { + late Fixture fixture; + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + fixture = Fixture(); + }); + + group('time to initial display', () { + group('in root screen app start route', () {}); + + group('in regular routes', () { + test('startMeasurement creates ttid span', () async { + final sut = fixture.getSut(); + + sut.startMeasurement('Current Route', null); + + final transaction = fixture.hub.getSpan() as SentryTracer; + await Future.delayed(const Duration(milliseconds: 100)); + + final spans = transaction.children; + expect(transaction.children, hasLength(1)); + expect(spans[0].context.operation, SentryTraceOrigins.uiTimeToInitialDisplay); + }); + + group('with approximation strategy', () {}); + + group('with manual strategy', () { + }); + }); + }); + + group('time to full display', () { + setUp(() { + fixture.options.enableTimeToFullDisplayTracing = true; + }); + + test('startMeasurement creates ttfd span', () async { + final sut = fixture.getSut(enableTimeToFullDisplayTracing: true); + + sut.startMeasurement('Current Route', null); + + final transaction = fixture.hub.getSpan() as SentryTracer; + await Future.delayed(const Duration(milliseconds: 100)); + + final spans = transaction.children; + expect(transaction.children, hasLength(2)); + expect(spans[0].context.operation, SentryTraceOrigins.uiTimeToInitialDisplay); + expect(spans[1].context.operation, SentryTraceOrigins.uiTimeToFullDisplay); + }); + + group('in root screen app start route', () {}); + group('in regular routes', () {}); + }); + + test('startMeasurement creates ui.load transaction', () async { + final sut = fixture.getSut(); + + sut.startMeasurement('Current Route', null); + + final transaction = fixture.hub.getSpan(); + expect(transaction, isNotNull); + expect(transaction?.context.operation, SentryTraceOrigins.uiLoad); + }); +} + +class Fixture { + final options = SentryFlutterOptions() + ..dsn = fakeDsn + ..tracesSampleRate = 1.0; + + late final hub = Hub(options); + + TimeToDisplayTracker getSut({bool enableTimeToFullDisplayTracing = false}) { + return TimeToDisplayTracker( + hub: hub, + enableAutoTransactions: true, + autoFinishAfter: const Duration(seconds: 3), + ); + } +} diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index f77040c52f..974b1c38c2 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -7,6 +7,7 @@ import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/native/sentry_native.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry_flutter/src/navigation/display_strategy_evaluator.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; import 'mocks.dart'; @@ -41,6 +42,7 @@ void main() { late MockNativeChannel mockNativeChannel; setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); mockNativeChannel = MockNativeChannel(); SentryFlutter.native = SentryNative(SentryFlutterOptions(dsn: fakeDsn), mockNativeChannel); @@ -51,7 +53,6 @@ void main() { }); test('transaction start begins frames collection', () async { - WidgetsFlutterBinding.ensureInitialized(); final currentRoute = route(RouteSettings(name: 'Current Route')); final mockHub = _MockHub(); @@ -60,27 +61,27 @@ void main() { when(mockHub.getSpan()).thenReturn(tracer); when(tracer.startChild( - 'ui.load.initial_display', // Matches any string for the operation argument - description: anyNamed('description'), // Matches any description - startTimestamp: anyNamed('startTimestamp') // Matches any startTimestamp - )).thenReturn(NoOpSentrySpan()); + 'ui.load.initial_display', // Matches any string for the operation argument + description: anyNamed('description'), // Matches any description + startTimestamp: + anyNamed('startTimestamp') // Matches any startTimestamp + )) + .thenReturn(NoOpSentrySpan()); when(tracer.startChild( - 'ui.load.full_display', // Matches any string for the operation argument - description: anyNamed('description'), // Matches any description - startTimestamp: anyNamed('startTimestamp') // Matches any startTimestamp - )).thenReturn(NoOpSentrySpan()); - - // when(mockTimeToDisplayTracker.startMeasurement(any, any)).thenAnswer((realInvocation) async {}); + 'ui.load.full_display', // Matches any string for the operation argument + description: anyNamed('description'), // Matches any description + startTimestamp: + anyNamed('startTimestamp') // Matches any startTimestamp + )) + .thenReturn(NoOpSentrySpan()); final sut = fixture.getSut(hub: mockHub); sut.didPush(currentRoute, null); - // verify(mockTimeToDisplayTracker.startMeasurement(any, any)).called(1); - // Handle internal async method calls. - await Future.delayed(const Duration(milliseconds: 500), () { + await Future.delayed(const Duration(milliseconds: 10), () { expect(mockNativeChannel.numberOfBeginNativeFramesCalls, 1); }); }); @@ -92,6 +93,10 @@ void main() { options.tracesSampleRate = 1; final hub = Hub(options); + mockNativeChannel = MockNativeChannel(); + SentryFlutter.native = + SentryNative(SentryFlutterOptions(dsn: fakeDsn), mockNativeChannel); + final nativeFrames = NativeFrames(3, 2, 1); mockNativeChannel.nativeFrames = nativeFrames; @@ -102,6 +107,9 @@ void main() { sut.didPush(currentRoute, null); + await Future.delayed(Duration(milliseconds: 50)); + DisplayStrategyEvaluator().reportManual('Current Route'); + // Get ref to created transaction // ignore: invalid_use_of_internal_member SentryTracer? actualTransaction; @@ -116,7 +124,7 @@ void main() { final measurements = actualTransaction?.measurements ?? {}; - expect(measurements.length, 3); + expect(measurements.length, 4); final expectedTotal = SentryMeasurement.totalFrames(3); final expectedSlow = SentryMeasurement.slowFrames(2); @@ -848,7 +856,7 @@ class Fixture { class _MockHub extends MockHub { @override - final options = defaultTestOptions()..enableTimeToFullDisplayTracing = true; + final options = defaultTestOptions(); @override late final scope = Scope(options); From 18a3fdf4325d2bc6d73ca499ee891c7675164106 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 16 Feb 2024 15:17:58 +0100 Subject: [PATCH 21/47] improve --- flutter/example/lib/main.dart | 1 + .../lib/src/navigation/time_to_display_transaction_handler.dart | 0 2 files changed, 1 insertion(+) create mode 100644 flutter/lib/src/navigation/time_to_display_transaction_handler.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index a4557f01a9..2c68410354 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -13,6 +13,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_isar/sentry_isar.dart'; import 'package:sentry_sqflite/sentry_sqflite.dart'; import 'package:sqflite/sqflite.dart'; + // import 'package:sqflite_common_ffi/sqflite_ffi.dart'; // import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:universal_platform/universal_platform.dart'; diff --git a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart new file mode 100644 index 0000000000..e69de29bb2 From 07e67a3a8bf3cd6b4451f3f6382bd14d06b8aff5 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Feb 2024 10:18:14 +0100 Subject: [PATCH 22/47] update --- flutter/example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 2c68410354..80101c8c2b 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -78,7 +78,7 @@ Future setupSentry(AppRunner appRunner, String dsn, // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; - options.enableTimeToFullDisplayTracing = true; + // options.enableTimeToFullDisplayTracing = true; options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; From a3903f05f675b68794def86fc22b28f14e76a22c Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Feb 2024 10:18:36 +0100 Subject: [PATCH 23/47] update --- .../navigation/time_to_display_tracker.dart | 187 ++++++------------ .../time_to_display_transaction_handler.dart | 124 ++++++++++++ .../time_to_display_tracker_test.dart | 81 +++++++- 3 files changed, 263 insertions(+), 129 deletions(-) diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index fcc5f998e0..bfb535450d 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -7,19 +7,23 @@ import '../../sentry_flutter.dart'; import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; import 'display_strategy_evaluator.dart'; +import 'time_to_display_transaction_handler.dart'; @internal class TimeToDisplayTracker { final Hub _hub; - final bool _enableAutoTransactions; - final Duration _autoFinishAfter; final SentryNative? _native; + final TimeToDisplayTransactionHandler _transactionHandler; static DateTime? _startTimestamp; static DateTime? _ttidEndTimestamp; static ISentrySpan? _ttidSpan; static ISentrySpan? _ttfdSpan; static Timer? _ttfdTimer; + static ISentrySpan? _transaction; + + @visibleForTesting + Duration ttfdAutoFinishAfter = Duration(seconds: 30); SentryFlutterOptions? get _options => _hub.options is SentryFlutterOptions // ignore: invalid_use_of_internal_member @@ -30,75 +34,17 @@ class TimeToDisplayTracker { required Hub? hub, required bool enableAutoTransactions, required Duration autoFinishAfter, + TimeToDisplayTransactionHandler? transactionHandler, }) : _hub = hub ?? HubAdapter(), - _enableAutoTransactions = enableAutoTransactions, - _autoFinishAfter = autoFinishAfter, - _native = SentryFlutter.native; - - Future _startTransaction(String? routeName, Object? arguments, - {DateTime? startTimestamp}) async { - if (!_enableAutoTransactions) { - return null; - } - - if (routeName == null) { - return null; - } - - if (routeName == '/') { - routeName = 'root ("/")'; - } - - final transactionContext = SentryTransactionContext( - routeName, - 'ui.load', - transactionNameSource: SentryTransactionNameSource.component, - // ignore: invalid_use_of_internal_member - origin: SentryTraceOrigins.autoNavigationRouteObserver, - ); - - final transaction = _hub.startTransactionWithContext( - transactionContext, - waitForChildren: true, - autoFinishAfter: _autoFinishAfter, - trimEnd: true, - bindToScope: true, - startTimestamp: startTimestamp, - onFinish: (transaction) async { - final nativeFrames = await _native - ?.endNativeFramesCollection(transaction.context.traceId); - if (nativeFrames != null) { - final measurements = nativeFrames.toMeasurements(); - for (final item in measurements.entries) { - final measurement = item.value; - transaction.setMeasurement( - item.key, - measurement.value, - unit: measurement.unit, + _native = SentryFlutter.native, + _transactionHandler = transactionHandler ?? + TimeToDisplayTransactionHandler( + hub: hub, + enableAutoTransactions: enableAutoTransactions, + autoFinishAfter: autoFinishAfter, ); - } - } - }, - ); - - // if _enableAutoTransactions is enabled but there's no traces sample rate - if (transaction is NoOpSentrySpan) { - return null; - } - - if (arguments != null) { - transaction.setData('route_settings_arguments', arguments); - } - - await _native?.beginNativeFramesCollection(); - - return transaction; - } void startMeasurement(String? routeName, Object? arguments) async { - _ttidSpan = null; - _ttfdSpan = null; - final startTimestamp = DateTime.now(); _startTimestamp = startTimestamp; @@ -123,16 +69,25 @@ class TimeToDisplayTracker { final routeName = SentryNavigatorObserver.currentRouteName; if (appStartInfo == null || routeName == null) return; - final transaction = await _startTransaction(routeName, arguments, + final transaction = await _transactionHandler.startTransaction( + routeName, arguments, startTimestamp: appStartInfo.start); if (transaction == null) return; + _transaction = transaction; - final ttidSpan = - _createTTIDSpan(transaction, routeName, appStartInfo.start); + final ttidSpan = _transactionHandler.createSpan( + transaction, + TimeToDisplayType.timeToInitialDisplay, + routeName, + appStartInfo.start); if (_options?.enableTimeToFullDisplayTracing == true) { - _ttfdSpan = _createTTFDSpan(transaction, routeName, appStartInfo.start); + _ttfdSpan = _transactionHandler.createSpan(transaction, + TimeToDisplayType.timeToFullDisplay, routeName, appStartInfo.start); } - _finishSpan(ttidSpan, transaction, appStartInfo.end, + TimeToDisplayTransactionHandler.finishSpan( + transaction: transaction, + span: ttidSpan, + endTimestamp: appStartInfo.end, measurement: appStartInfo.measurement); }); } @@ -140,10 +95,11 @@ class TimeToDisplayTracker { // Handles measuring navigation for regular routes void _handleRegularRouteMeasurement( String? routeName, Object? arguments, DateTime startTimestamp) async { - final transaction = await _startTransaction(routeName, arguments, - startTimestamp: startTimestamp); + final transaction = await _transactionHandler + .startTransaction(routeName, arguments, startTimestamp: startTimestamp); if (transaction == null || routeName == null) return; + _transaction = transaction; _initializeTimeToDisplaySpans(transaction, routeName, startTimestamp); @@ -155,36 +111,43 @@ class TimeToDisplayTracker { void _initializeTimeToDisplaySpans( ISentrySpan transaction, String routeName, DateTime startTimestamp) { - _ttidSpan = _createTTIDSpan(transaction, routeName, startTimestamp); + _ttidSpan = _transactionHandler.createSpan(transaction, + TimeToDisplayType.timeToInitialDisplay, routeName, startTimestamp); if (_options?.enableTimeToFullDisplayTracing == true) { - _ttfdSpan = _createTTFDSpan(transaction, routeName, startTimestamp); - final ttfdAutoFinishAfter = Duration(seconds: 30); - _ttfdTimer = Timer(ttfdAutoFinishAfter, () { - if (_ttfdSpan?.finished == true) { + _ttfdSpan = _transactionHandler.createSpan(transaction, + TimeToDisplayType.timeToFullDisplay, routeName, startTimestamp); + _ttfdTimer = Timer(ttfdAutoFinishAfter, () async { + final ttfdSpan = _ttfdSpan; + final ttfdEndTimestamp = _ttidEndTimestamp; + if (ttfdSpan == null || + ttfdSpan.finished == true || + ttfdEndTimestamp == null) { return; } - _finishSpan(_ttfdSpan!, transaction, _ttidEndTimestamp!, + TimeToDisplayTransactionHandler.finishSpan( + transaction: transaction, + span: ttfdSpan, + endTimestamp: ttfdEndTimestamp, status: SpanStatus.deadlineExceeded()); }); } } - ISentrySpan _createTTIDSpan( - ISentrySpan transaction, String routeName, DateTime startTimestamp) { - return transaction.startChild( - SentryTraceOrigins.uiTimeToInitialDisplay, - description: '$routeName initial display', - startTimestamp: startTimestamp, - ); - } + void _finishInitialDisplay(ISentrySpan ttidSpan, ISentrySpan transaction, + String routeName, DateTime startTimestamp) async { + final endTimestamp = await _determineEndTimeOfTTID(routeName); + if (endTimestamp == null) return; + _ttidEndTimestamp = endTimestamp; - ISentrySpan _createTTFDSpan( - ISentrySpan transaction, String routeName, DateTime startTimestamp) { - return transaction.startChild( - SentryTraceOrigins.uiTimeToFullDisplay, - description: '$routeName full display', - startTimestamp: startTimestamp, - ); + final duration = endTimestamp.difference(startTimestamp).inMilliseconds; + final measurement = SentryMeasurement('time_to_initial_display', duration, + unit: DurationSentryMeasurementUnit.milliSecond); + + TimeToDisplayTransactionHandler.finishSpan( + transaction: transaction, + span: ttidSpan, + endTimestamp: endTimestamp, + measurement: measurement); } Future _determineEndTimeOfTTID(String routeName) async { @@ -216,14 +179,10 @@ class TimeToDisplayTracker { @internal static void reportFullyDisplayed() { - _finishFullDisplay(); - } - - static void _finishFullDisplay() { _ttfdTimer?.cancel(); final endTimestamp = DateTime.now(); final startTimestamp = _startTimestamp; - final transaction = Sentry.getSpan(); + final transaction = _transaction; final ttfdSpan = _ttfdSpan; if (startTimestamp == null || transaction == null || ttfdSpan == null) { return; @@ -231,28 +190,10 @@ class TimeToDisplayTracker { final duration = endTimestamp.difference(startTimestamp).inMilliseconds; final measurement = SentryMeasurement('time_to_full_display', duration, unit: DurationSentryMeasurementUnit.milliSecond); - _finishSpan(ttfdSpan, transaction, endTimestamp, measurement: measurement); - } - - void _finishInitialDisplay(ISentrySpan ttidSpan, ISentrySpan transaction, - String routeName, DateTime startTimestamp) async { - final endTimestamp = await _determineEndTimeOfTTID(routeName); - if (endTimestamp == null) return; - _ttidEndTimestamp = endTimestamp; - - final duration = endTimestamp.difference(startTimestamp).inMilliseconds; - final measurement = SentryMeasurement('time_to_initial_display', duration, - unit: DurationSentryMeasurementUnit.milliSecond); - _finishSpan(ttidSpan, transaction, endTimestamp, measurement: measurement); - } - - static void _finishSpan( - ISentrySpan span, ISentrySpan transaction, DateTime endTimestamp, - {SentryMeasurement? measurement, SpanStatus? status}) { - if (measurement != null) { - transaction.setMeasurement(measurement.name, measurement.value, - unit: measurement.unit); - } - span.finish(status: status, endTimestamp: endTimestamp); + TimeToDisplayTransactionHandler.finishSpan( + transaction: transaction, + span: ttfdSpan, + endTimestamp: endTimestamp, + measurement: measurement); } } diff --git a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart index e69de29bb2..6f320f3894 100644 --- a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart +++ b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart @@ -0,0 +1,124 @@ +import 'package:meta/meta.dart'; +import '../../sentry_flutter.dart'; +import '../native/sentry_native.dart'; + +enum TimeToDisplayType { timeToInitialDisplay, timeToFullDisplay } + +@internal +abstract class ITimeToDisplayTransactionHandler { + Future startTransaction(String? routeName, Object? arguments, + {DateTime? startTimestamp}); + + ISentrySpan createSpan(ISentrySpan transaction, TimeToDisplayType type, + String routeName, DateTime startTimestamp); +} + +@internal +class TimeToDisplayTransactionHandler extends ITimeToDisplayTransactionHandler { + final Hub? _hub; + final bool? _enableAutoTransactions; + final Duration? _autoFinishAfter; + final SentryNative? _native; + + TimeToDisplayTransactionHandler({ + required Hub? hub, + required bool? enableAutoTransactions, + required Duration? autoFinishAfter, + }) : _hub = hub ?? HubAdapter(), + _enableAutoTransactions = enableAutoTransactions, + _autoFinishAfter = autoFinishAfter, + _native = SentryFlutter.native; + + @override + Future startTransaction(String? routeName, Object? arguments, + {DateTime? startTimestamp}) async { + if (_enableAutoTransactions == false) { + return null; + } + + if (routeName == null) { + return null; + } + + if (routeName == '/') { + routeName = 'root ("/")'; + } + + final transactionContext = SentryTransactionContext( + routeName, + 'ui.load', + transactionNameSource: SentryTransactionNameSource.component, + // ignore: invalid_use_of_internal_member + origin: SentryTraceOrigins.autoNavigationRouteObserver, + ); + + final transaction = _hub?.startTransactionWithContext( + transactionContext, + waitForChildren: true, + autoFinishAfter: _autoFinishAfter, + trimEnd: true, + bindToScope: true, + startTimestamp: startTimestamp, + onFinish: (transaction) async { + final nativeFrames = await _native + ?.endNativeFramesCollection(transaction.context.traceId); + if (nativeFrames != null) { + final measurements = nativeFrames.toMeasurements(); + for (final item in measurements.entries) { + final measurement = item.value; + transaction.setMeasurement( + item.key, + measurement.value, + unit: measurement.unit, + ); + } + } + }, + ); + + // if _enableAutoTransactions is enabled but there's no traces sample rate + if (transaction is NoOpSentrySpan) { + return null; + } + + if (arguments != null) { + transaction?.setData('route_settings_arguments', arguments); + } + + await _native?.beginNativeFramesCollection(); + + return transaction; + } + + @override + ISentrySpan createSpan(ISentrySpan transaction, TimeToDisplayType type, + String routeName, DateTime startTimestamp) { + String operation; + String description; + switch (type) { + case TimeToDisplayType.timeToInitialDisplay: + operation = SentryTraceOrigins.uiTimeToInitialDisplay; + description = '$routeName initial display'; + break; + case TimeToDisplayType.timeToFullDisplay: + operation = SentryTraceOrigins.uiTimeToFullDisplay; + description = '$routeName full display'; + break; + } + return transaction.startChild(operation, + description: description, startTimestamp: startTimestamp); + } + + static void finishSpan( + {required ISentrySpan span, + required ISentrySpan transaction, + DateTime? endTimestamp, + SentryMeasurement? measurement, + SpanStatus? status}) { + if (measurement != null) { + transaction.setMeasurement(measurement.name, measurement.value, + unit: measurement.unit); + } + span.finish(status: status, endTimestamp: endTimestamp); + } +} diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index 1d3c106db4..ad359c70a7 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/navigation/display_strategy_evaluator.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; import 'package:sentry/src/sentry_tracer.dart'; @@ -27,12 +28,32 @@ void main() { final spans = transaction.children; expect(transaction.children, hasLength(1)); - expect(spans[0].context.operation, SentryTraceOrigins.uiTimeToInitialDisplay); + expect(spans[0].context.operation, + SentryTraceOrigins.uiTimeToInitialDisplay); }); group('with approximation strategy', () {}); group('with manual strategy', () { + test('finishes ttid span after reporting with manual api', () async { + final sut = fixture.getSut(); + + sut.startMeasurement('Current Route', null); + + final transaction = fixture.hub.getSpan() as SentryTracer; + await Future.delayed(const Duration(milliseconds: 100)); + + final ttidSpan = transaction.children + .where((element) => + element.context.operation == + SentryTraceOrigins.uiTimeToInitialDisplay) + .first; + expect(ttidSpan, isNotNull); + + await Future.delayed(const Duration(milliseconds: 100)); + + expect(ttidSpan.finished, isTrue); + }); }); }); }); @@ -42,7 +63,7 @@ void main() { fixture.options.enableTimeToFullDisplayTracing = true; }); - test('startMeasurement creates ttfd span', () async { + test('startMeasurement creates ttfd and ttid span', () async { final sut = fixture.getSut(enableTimeToFullDisplayTracing: true); sut.startMeasurement('Current Route', null); @@ -52,15 +73,63 @@ void main() { final spans = transaction.children; expect(transaction.children, hasLength(2)); - expect(spans[0].context.operation, SentryTraceOrigins.uiTimeToInitialDisplay); - expect(spans[1].context.operation, SentryTraceOrigins.uiTimeToFullDisplay); + expect(spans[0].context.operation, + SentryTraceOrigins.uiTimeToInitialDisplay); + expect( + spans[1].context.operation, SentryTraceOrigins.uiTimeToFullDisplay); }); group('in root screen app start route', () {}); - group('in regular routes', () {}); + group('in regular routes', () { + test('finishes ttfd span after calling reportFullyDisplayed', () async { + final sut = fixture.getSut(); + + sut.startMeasurement('Current Route', null); + + final transaction = fixture.hub.getSpan() as SentryTracer; + await Future.delayed(const Duration(milliseconds: 100)); + + final ttfdSpan = transaction.children + .where((element) => + element.context.operation == + SentryTraceOrigins.uiTimeToFullDisplay) + .first; + expect(ttfdSpan, isNotNull); + + SentryFlutter.reportFullyDisplayed(); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(ttfdSpan.finished, isTrue); + }); + + test( + 'not using reportFullyDisplayed finishes ttfd span after timeout with deadline exceeded', + () async { + final sut = fixture.getSut(); + sut.ttfdAutoFinishAfter = const Duration(seconds: 3); + + sut.startMeasurement('Current Route', null); + + final transaction = fixture.hub.getSpan() as SentryTracer; + await Future.delayed(const Duration(milliseconds: 100)); + DisplayStrategyEvaluator().reportManual('Current Route'); + + final ttfdSpan = transaction.children + .where((element) => + element.context.operation == + SentryTraceOrigins.uiTimeToFullDisplay) + .first; + expect(ttfdSpan, isNotNull); + + await Future.delayed(sut.ttfdAutoFinishAfter + const Duration(milliseconds: 100)); + + expect(ttfdSpan.finished, isTrue); + expect(ttfdSpan.status, SpanStatus.deadlineExceeded()); + }); + }); }); - test('startMeasurement creates ui.load transaction', () async { + test('screen load tracking creates ui.load transaction', () async { final sut = fixture.getSut(); sut.startMeasurement('Current Route', null); From 65626df2cab04b922625c3e39af84d47258f8299 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Feb 2024 13:01:03 +0100 Subject: [PATCH 24/47] update --- .../src/navigation/sentry_display_widget.dart | 3 +- .../navigation/sentry_navigator_observer.dart | 2 + .../navigation/time_to_display_tracker.dart | 46 ++++++++-- flutter/lib/src/sentry_flutter.dart | 7 +- .../time_to_display_tracker_test.dart | 91 ++++++++++++++++--- 5 files changed, 124 insertions(+), 25 deletions(-) diff --git a/flutter/lib/src/navigation/sentry_display_widget.dart b/flutter/lib/src/navigation/sentry_display_widget.dart index 079df3119c..11ace688fc 100644 --- a/flutter/lib/src/navigation/sentry_display_widget.dart +++ b/flutter/lib/src/navigation/sentry_display_widget.dart @@ -16,7 +16,8 @@ class _SentryDisplayWidgetState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - SentryFlutter.reportInitiallyDisplayed(context); + final route = ModalRoute.of(context); + SentryFlutter.reportInitiallyDisplayed(routeName: route?.settings.name); }); } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index f2bf74a33d..9ec8525169 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -187,6 +187,8 @@ class SentryNavigatorObserver extends RouteObserver> { Future _startTimeToDisplayTracking(Route? route) async { final routeName = _getRouteName(route); + _currentRouteName = routeName; + final arguments = route?.settings.arguments; _timeToDisplayTracker.startMeasurement(routeName, arguments); } diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index bfb535450d..7574621017 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -14,6 +14,7 @@ class TimeToDisplayTracker { final Hub _hub; final SentryNative? _native; final TimeToDisplayTransactionHandler _transactionHandler; + final IFrameCallbackHandler _frameCallbackHandler; static DateTime? _startTimestamp; static DateTime? _ttidEndTimestamp; @@ -34,9 +35,11 @@ class TimeToDisplayTracker { required Hub? hub, required bool enableAutoTransactions, required Duration autoFinishAfter, + IFrameCallbackHandler? frameCallbackHandler, TimeToDisplayTransactionHandler? transactionHandler, }) : _hub = hub ?? HubAdapter(), _native = SentryFlutter.native, + _frameCallbackHandler = frameCallbackHandler ?? FrameCallbackHandler(), _transactionHandler = transactionHandler ?? TimeToDisplayTransactionHandler( hub: hub, @@ -66,11 +69,11 @@ class TimeToDisplayTracker { /// We start and immediately finish the TTID span since we cannot mutate the history of spans. void _handleAppStartMeasurement(String? routeName, Object? arguments) { AppStartTracker().onAppStartComplete((appStartInfo) async { - final routeName = SentryNavigatorObserver.currentRouteName; - if (appStartInfo == null || routeName == null) return; + final name = routeName ?? SentryNavigatorObserver.currentRouteName; + if (appStartInfo == null || name == null) return; final transaction = await _transactionHandler.startTransaction( - routeName, arguments, + name, arguments, startTimestamp: appStartInfo.start); if (transaction == null) return; _transaction = transaction; @@ -78,12 +81,14 @@ class TimeToDisplayTracker { final ttidSpan = _transactionHandler.createSpan( transaction, TimeToDisplayType.timeToInitialDisplay, - routeName, + name, appStartInfo.start); + if (_options?.enableTimeToFullDisplayTracing == true) { _ttfdSpan = _transactionHandler.createSpan(transaction, - TimeToDisplayType.timeToFullDisplay, routeName, appStartInfo.start); + TimeToDisplayType.timeToFullDisplay, name, appStartInfo.start); } + TimeToDisplayTransactionHandler.finishSpan( transaction: transaction, span: ttidSpan, @@ -154,7 +159,7 @@ class TimeToDisplayTracker { DateTime? endTimestamp; final endTimeCompleter = Completer(); - SchedulerBinding.instance.addPostFrameCallback((_) { + _frameCallbackHandler.addPostFrameCallback((_) { endTimestamp = DateTime.now(); endTimeCompleter.complete(endTimestamp); }); @@ -173,7 +178,10 @@ class TimeToDisplayTracker { } @internal - static void reportInitiallyDisplayed(String routeName) { + static void reportInitiallyDisplayed({String? routeName}) { + routeName = routeName ?? SentryNavigatorObserver.currentRouteName; + + if (routeName == null) return; DisplayStrategyEvaluator().reportManual(routeName); } @@ -197,3 +205,27 @@ class TimeToDisplayTracker { measurement: measurement); } } + +abstract class IFrameCallbackHandler { + void addPostFrameCallback(FrameCallback callback, {String debugLabel}); +} + +class FrameCallbackHandler implements IFrameCallbackHandler { + @override + void addPostFrameCallback(FrameCallback callback, + {String debugLabel = 'callback'}) { + SchedulerBinding.instance.addPostFrameCallback(callback); + } +} + +class MockFrameCallbackHandler implements IFrameCallbackHandler { + FrameCallback? storedCallback; + + @override + void addPostFrameCallback(FrameCallback callback, + {String debugLabel = 'callback'}) { + Future.delayed(Duration(milliseconds: 500), () { + callback(Duration.zero); + }); + } +} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index eb0798bc31..6e6ed14aac 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -230,11 +230,8 @@ mixin SentryFlutter { options.sdk = sdk; } - static void reportInitiallyDisplayed(BuildContext context) { - final routeName = ModalRoute.of(context)?.settings.name; - if (routeName != null) { - TimeToDisplayTracker.reportInitiallyDisplayed(routeName); - } + static void reportInitiallyDisplayed({String? routeName}) { + TimeToDisplayTracker.reportInitiallyDisplayed(routeName: routeName); } /// Reports the time it took for the screen to be fully displayed. diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index ad359c70a7..54bddd2d58 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/navigation/display_strategy_evaluator.dart'; +import 'package:sentry_flutter/src/integrations/integrations.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; import 'package:sentry/src/sentry_tracer.dart'; @@ -15,24 +15,47 @@ void main() { }); group('time to initial display', () { - group('in root screen app start route', () {}); - - group('in regular routes', () { - test('startMeasurement creates ttid span', () async { + group('in root screen app start route', () { + test('startMeasurement finishes ttid span', () async { + SentryFlutter.native = TestMockSentryNative(); final sut = fixture.getSut(); - sut.startMeasurement('Current Route', null); + sut.startMeasurement('/', null); + + AppStartTracker().setAppStartInfo(AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0), + DateTime.fromMillisecondsSinceEpoch(10), + SentryMeasurement('', 0))); - final transaction = fixture.hub.getSpan() as SentryTracer; await Future.delayed(const Duration(milliseconds: 100)); + final transaction = fixture.hub.getSpan() as SentryTracer; + final spans = transaction.children; expect(transaction.children, hasLength(1)); expect(spans[0].context.operation, SentryTraceOrigins.uiTimeToInitialDisplay); + expect(spans[0].finished, isTrue); }); + }); + + group('in regular routes', () { + group('with approximation strategy', () { + test('startMeasurement finishes ttid span', () async { + final sut = fixture.getSut(); + + sut.startMeasurement('Current Route', null); + + final transaction = fixture.hub.getSpan() as SentryTracer; + await Future.delayed(const Duration(milliseconds: 2000)); - group('with approximation strategy', () {}); + final spans = transaction.children; + expect(transaction.children, hasLength(1)); + expect(spans[0].context.operation, + SentryTraceOrigins.uiTimeToInitialDisplay); + expect(spans[0].finished, isTrue); + }); + }); group('with manual strategy', () { test('finishes ttid span after reporting with manual api', () async { @@ -41,8 +64,11 @@ void main() { sut.startMeasurement('Current Route', null); final transaction = fixture.hub.getSpan() as SentryTracer; + await Future.delayed(const Duration(milliseconds: 100)); + SentryFlutter.reportInitiallyDisplayed(routeName: 'Current Route'); + final ttidSpan = transaction.children .where((element) => element.context.operation == @@ -79,7 +105,36 @@ void main() { spans[1].context.operation, SentryTraceOrigins.uiTimeToFullDisplay); }); - group('in root screen app start route', () {}); + group('in root screen app start route', () { + test('startMeasurement finishes ttfd span', () async { + SentryFlutter.native = TestMockSentryNative(); + final sut = fixture.getSut(); + + sut.startMeasurement('/', null); + + AppStartTracker().setAppStartInfo(AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0), + DateTime.fromMillisecondsSinceEpoch(10), + SentryMeasurement('', 0))); + + await Future.delayed(const Duration(milliseconds: 100)); + + final transaction = fixture.hub.getSpan() as SentryTracer; + + final ttfdSpan = transaction.children + .where((element) => + element.context.operation == + SentryTraceOrigins.uiTimeToFullDisplay) + .first; + expect(ttfdSpan, isNotNull); + + SentryFlutter.reportFullyDisplayed(); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(ttfdSpan.finished, isTrue); + }); + }); + group('in regular routes', () { test('finishes ttfd span after calling reportFullyDisplayed', () async { final sut = fixture.getSut(); @@ -103,7 +158,7 @@ void main() { }); test( - 'not using reportFullyDisplayed finishes ttfd span after timeout with deadline exceeded', + 'not using reportFullyDisplayed finishes ttfd span after timeout with deadline exceeded and ttid matching end time', () async { final sut = fixture.getSut(); sut.ttfdAutoFinishAfter = const Duration(seconds: 3); @@ -111,8 +166,8 @@ void main() { sut.startMeasurement('Current Route', null); final transaction = fixture.hub.getSpan() as SentryTracer; + await Future.delayed(const Duration(milliseconds: 100)); - DisplayStrategyEvaluator().reportManual('Current Route'); final ttfdSpan = transaction.children .where((element) => @@ -121,10 +176,19 @@ void main() { .first; expect(ttfdSpan, isNotNull); - await Future.delayed(sut.ttfdAutoFinishAfter + const Duration(milliseconds: 100)); + final ttidSpan = transaction.children + .where((element) => + element.context.operation == + SentryTraceOrigins.uiTimeToInitialDisplay) + .first; + expect(ttfdSpan, isNotNull); + + await Future.delayed( + sut.ttfdAutoFinishAfter + const Duration(milliseconds: 100)); expect(ttfdSpan.finished, isTrue); expect(ttfdSpan.status, SpanStatus.deadlineExceeded()); + expect(ttfdSpan.endTimestamp, ttidSpan.endTimestamp); }); }); }); @@ -145,6 +209,8 @@ class Fixture { ..dsn = fakeDsn ..tracesSampleRate = 1.0; + final frameCallbackHandler = MockFrameCallbackHandler(); + late final hub = Hub(options); TimeToDisplayTracker getSut({bool enableTimeToFullDisplayTracing = false}) { @@ -152,6 +218,7 @@ class Fixture { hub: hub, enableAutoTransactions: true, autoFinishAfter: const Duration(seconds: 3), + frameCallbackHandler: frameCallbackHandler, ); } } From 2928099abfd904142ada5355925f59b40c58b01a Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 20 Feb 2024 11:43:39 +0100 Subject: [PATCH 25/47] Fix tests --- .../navigation/sentry_navigator_observer.dart | 6 +- .../navigation/time_to_display_tracker.dart | 1 + .../time_to_display_transaction_handler.dart | 5 +- .../test/sentry_navigator_observer_test.dart | 120 +++++++++++------- 4 files changed, 82 insertions(+), 50 deletions(-) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 9ec8525169..3213e8a61e 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -116,7 +116,11 @@ class SentryNavigatorObserver extends RouteObserver> { Future _finishTransaction() async { final transaction = _hub.getSpan(); - await transaction?.finish(); + if (transaction == null || transaction.finished) { + return; + } + transaction.status ??= SpanStatus.ok(); + await transaction.finish(); } @override diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index 7574621017..fcb8f40d02 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -22,6 +22,7 @@ class TimeToDisplayTracker { static ISentrySpan? _ttfdSpan; static Timer? _ttfdTimer; static ISentrySpan? _transaction; + static ISentrySpan? get transaction => _transaction; @visibleForTesting Duration ttfdAutoFinishAfter = Duration(seconds: 30); diff --git a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart index 6f320f3894..7bfd599778 100644 --- a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart +++ b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart @@ -57,7 +57,6 @@ class TimeToDisplayTransactionHandler extends ITimeToDisplayTransactionHandler { waitForChildren: true, autoFinishAfter: _autoFinishAfter, trimEnd: true, - bindToScope: true, startTimestamp: startTimestamp, onFinish: (transaction) async { final nativeFrames = await _native @@ -85,6 +84,10 @@ class TimeToDisplayTransactionHandler extends ITimeToDisplayTransactionHandler { transaction?.setData('route_settings_arguments', arguments); } + _hub?.configureScope((scope) { + scope.span ??= transaction; + }); + await _native?.beginNativeFramesCollection(); return transaction; diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index 974b1c38c2..cef0e0b8ab 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -32,17 +32,18 @@ void main() { customSamplingContext: anyNamed('customSamplingContext'), startTimestamp: anyNamed('startTimestamp'), )).thenReturn(thenReturnSpan); + when(mockHub.getSpan()).thenReturn(thenReturnSpan); } setUp(() { fixture = Fixture(); + WidgetsFlutterBinding.ensureInitialized(); }); group('NativeFrames', () { late MockNativeChannel mockNativeChannel; setUp(() { - TestWidgetsFlutterBinding.ensureInitialized(); mockNativeChannel = MockNativeChannel(); SentryFlutter.native = SentryNative(SentryFlutterOptions(dsn: fakeDsn), mockNativeChannel); @@ -58,23 +59,12 @@ void main() { final tracer = getMockSentryTracer(); _whenAnyStart(mockHub, tracer); - when(mockHub.getSpan()).thenReturn(tracer); - - when(tracer.startChild( - 'ui.load.initial_display', // Matches any string for the operation argument - description: anyNamed('description'), // Matches any description - startTimestamp: - anyNamed('startTimestamp') // Matches any startTimestamp - )) - .thenReturn(NoOpSentrySpan()); - - when(tracer.startChild( - 'ui.load.full_display', // Matches any string for the operation argument - description: anyNamed('description'), // Matches any description - startTimestamp: - anyNamed('startTimestamp') // Matches any startTimestamp - )) + when(tracer.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); + when(tracer.finished).thenReturn(false); + when(tracer.status).thenReturn(SpanStatus.ok()); final sut = fixture.getSut(hub: mockHub); @@ -144,15 +134,21 @@ void main() { }); group('$SentryNavigatorObserver', () { - test('didPush starts transaction', () { + test('didPush starts transaction', () async { const name = 'Current Route'; final currentRoute = route(RouteSettings(name: name)); - const op = 'navigation'; + const op = 'ui.load'; final hub = _MockHub(); final span = getMockSentryTracer(name: name); when(span.context).thenReturn(SentrySpanContext(operation: op)); _whenAnyStart(hub, span); + when(span.finished).thenReturn(false); + when(span.status).thenReturn(SpanStatus.ok()); + when(span.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); final sut = fixture.getSut( hub: hub, @@ -163,6 +159,7 @@ void main() { final context = verify(hub.startTransactionWithContext( captureAny, + startTimestamp: anyNamed('startTimestamp'), waitForChildren: true, autoFinishAfter: anyNamed('autoFinishAfter'), trimEnd: true, @@ -184,6 +181,7 @@ void main() { final span = NoOpSentrySpan(); _whenAnyStart(hub, span); + when(hub.getSpan()).thenReturn(null); final sut = fixture.getSut( hub: hub, @@ -194,6 +192,7 @@ void main() { verify(hub.startTransactionWithContext( any, + startTimestamp: anyNamed('startTimestamp'), waitForChildren: true, autoFinishAfter: Duration(seconds: 5), trimEnd: true, @@ -212,6 +211,8 @@ void main() { final hub = _MockHub(); final span = getMockSentryTracer(); when(span.context).thenReturn(SentrySpanContext(operation: 'op')); + when(span.finished).thenReturn(false); + when(span.status).thenReturn(SpanStatus.ok()); _whenAnyStart(hub, span); final sut = fixture.getSut(hub: hub); @@ -220,6 +221,7 @@ void main() { verifyNever(hub.startTransactionWithContext( any, + startTimestamp: anyNamed('startTimestamp'), waitForChildren: true, autoFinishAfter: anyNamed('autoFinishAfter'), trimEnd: true, @@ -238,6 +240,8 @@ void main() { final span = getMockSentryTracer(); when(span.context).thenReturn(SentrySpanContext(operation: 'op')); _whenAnyStart(hub, span); + when(span.finished).thenReturn(false); + when(span.status).thenReturn(SpanStatus.ok()); final sut = fixture.getSut(hub: hub, enableAutoTransactions: false); @@ -245,6 +249,7 @@ void main() { verifyNever(hub.startTransactionWithContext( any, + startTimestamp: anyNamed('startTimestamp'), waitForChildren: true, autoFinishAfter: anyNamed('autoFinishAfter'), trimEnd: true, @@ -264,6 +269,12 @@ void main() { final span = getMockSentryTracer(); when(span.context).thenReturn(SentrySpanContext(operation: 'op')); + when(span.finished).thenReturn(false); + when(span.status).thenReturn(SpanStatus.ok()); + when(span.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); final sut = fixture.getSut(hub: hub); @@ -272,6 +283,7 @@ void main() { verify(hub.startTransactionWithContext( any, + startTimestamp: anyNamed('startTimestamp'), waitForChildren: true, autoFinishAfter: anyNamed('autoFinishAfter'), trimEnd: true, @@ -283,7 +295,7 @@ void main() { }); }); - test('didPush finishes previous transaction', () { + test('didPush finishes previous transaction', () async { final firstRoute = route(RouteSettings(name: 'First Route')); final secondRoute = route(RouteSettings(name: 'Second Route')); @@ -291,6 +303,11 @@ void main() { final span = getMockSentryTracer(); when(span.context).thenReturn(SentrySpanContext(operation: 'op')); when(span.status).thenReturn(null); + when(span.finished).thenReturn(false); + when(span.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); final sut = fixture.getSut(hub: hub); @@ -309,6 +326,11 @@ void main() { final span = getMockSentryTracer(); when(span.context).thenReturn(SentrySpanContext(operation: 'op')); when(span.status).thenReturn(null); + when(span.finished).thenReturn(false); + when(span.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); final sut = fixture.getSut(hub: hub); @@ -320,34 +342,6 @@ void main() { verify(span.finish()); }); - test('didPop re-starts previous', () { - final previousRoute = route(RouteSettings(name: 'Previous Route')); - final currentRoute = route(RouteSettings(name: 'Current Route')); - - final hub = _MockHub(); - final previousSpan = getMockSentryTracer(); - when(previousSpan.context).thenReturn(SentrySpanContext(operation: 'op')); - when(previousSpan.status).thenReturn(null); - - _whenAnyStart(hub, previousSpan); - - final sut = fixture.getSut(hub: hub); - - sut.didPop(currentRoute, previousRoute); - - verify(hub.startTransactionWithContext( - any, - waitForChildren: true, - autoFinishAfter: anyNamed('autoFinishAfter'), - trimEnd: true, - onFinish: anyNamed('onFinish'), - )); - - hub.configureScope((scope) { - expect(scope.span, previousSpan); - }); - }); - test('route arguments are set on transaction', () { final arguments = {'foo': 'bar'}; final currentRoute = route(RouteSettings( @@ -359,6 +353,11 @@ void main() { final span = getMockSentryTracer(); when(span.context).thenReturn(SentrySpanContext(operation: 'op')); when(span.status).thenReturn(null); + when(span.finished).thenReturn(false); + when(span.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); final sut = fixture.getSut(hub: hub); @@ -374,6 +373,12 @@ void main() { final hub = _MockHub(); final span = getMockSentryTracer(name: '/'); when(span.context).thenReturn(SentrySpanContext(operation: 'op')); + when(span.finished).thenReturn(false); + when(span.status).thenReturn(SpanStatus.ok()); + when(span.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); final sut = fixture.getSut(hub: hub); @@ -383,6 +388,7 @@ void main() { final context = verify(hub.startTransactionWithContext( captureAny, waitForChildren: true, + startTimestamp: anyNamed('startTimestamp'), autoFinishAfter: anyNamed('autoFinishAfter'), trimEnd: true, onFinish: anyNamed('onFinish'), @@ -403,6 +409,12 @@ void main() { final hub = _MockHub(); final span = getMockSentryTracer(name: name); when(span.context).thenReturn(SentrySpanContext(operation: op)); + when(span.finished).thenReturn(false); + when(span.status).thenReturn(SpanStatus.ok()); + when(span.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); final sut = fixture.getSut( @@ -425,6 +437,12 @@ void main() { final hub = _MockHub(); final span = getMockSentryTracer(name: oldRouteName); when(span.context).thenReturn(SentrySpanContext(operation: op)); + when(span.finished).thenReturn(false); + when(span.status).thenReturn(SpanStatus.ok()); + when(span.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); final sut = fixture.getSut( @@ -449,6 +467,11 @@ void main() { final span = getMockSentryTracer(name: oldRouteName); when(span.context).thenReturn(SentrySpanContext(operation: op)); when(span.status).thenReturn(null); + when(span.finished).thenReturn(false); + when(span.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); final sut = fixture.getSut( @@ -668,6 +691,7 @@ void main() { final hub = _MockHub(); final observer = fixture.getSut(hub: hub); + when(hub.getSpan()).thenReturn(NoOpSentrySpan()); final to = route(); final previous = route(); From 0c3388542161ce960ac880faf62b317bc27570ba Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 20 Feb 2024 16:16:10 +0100 Subject: [PATCH 26/47] Update origin and operation --- dart/lib/sentry.dart | 2 + dart/lib/src/sentry_measurement.dart | 16 ---- dart/lib/src/sentry_span_operations.dart | 8 ++ dart/lib/src/sentry_trace_origins.dart | 5 +- .../navigation/time_to_display_tracker.dart | 84 ++++++++++++------- .../time_to_display_transaction_handler.dart | 22 ++--- .../lib/src/sentry_flutter_measurement.dart | 23 +++++ .../time_to_display_tracker_test.dart | 30 ++++--- 8 files changed, 110 insertions(+), 80 deletions(-) create mode 100644 dart/lib/src/sentry_span_operations.dart create mode 100644 flutter/lib/src/sentry_flutter_measurement.dart diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 5419aa45b8..f416d0b797 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -49,6 +49,8 @@ export 'src/utils/http_header_utils.dart'; // ignore: invalid_export_of_internal_element export 'src/sentry_trace_origins.dart'; // ignore: invalid_export_of_internal_element +export 'src/sentry_span_operations.dart'; +// ignore: invalid_export_of_internal_element export 'src/utils.dart'; // spotlight debugging export 'src/spotlight.dart'; diff --git a/dart/lib/src/sentry_measurement.dart b/dart/lib/src/sentry_measurement.dart index 3651d89b37..481c513e9b 100644 --- a/dart/lib/src/sentry_measurement.dart +++ b/dart/lib/src/sentry_measurement.dart @@ -39,22 +39,6 @@ class SentryMeasurement { value = duration.inMilliseconds, unit = DurationSentryMeasurementUnit.milliSecond; - /// Duration of the time to initial display in milliseconds - SentryMeasurement.timeToInitialDisplay(Duration duration) - : assert(!duration.isNegative), - name = 'time_to_initial_display', - value = duration.inMilliseconds, - unit = DurationSentryMeasurementUnit.milliSecond; - - /// Duration of the time to full display in milliseconds - SentryMeasurement.timeToFullDisplay(Duration duration) - : assert(!duration.isNegative), - name = 'time_to_full_display', - value = duration.inMilliseconds, - unit = DurationSentryMeasurementUnit.milliSecond; - - // TODO: might wanna move ttid/ttfd to flutter since we don't have it on pure dart - final String name; final num value; final SentryMeasurementUnit? unit; diff --git a/dart/lib/src/sentry_span_operations.dart b/dart/lib/src/sentry_span_operations.dart new file mode 100644 index 0000000000..eba53aae5b --- /dev/null +++ b/dart/lib/src/sentry_span_operations.dart @@ -0,0 +1,8 @@ +import 'package:meta/meta.dart'; + +@internal +class SentrySpanOperations { + static const String uiLoad = 'ui.load'; + static const String uiTimeToInitialDisplay = 'ui.load.initial_display'; + static const String uiTimeToFullDisplay = 'ui.load.full_display'; +} \ No newline at end of file diff --git a/dart/lib/src/sentry_trace_origins.dart b/dart/lib/src/sentry_trace_origins.dart index 487dfebf3e..4377fa2c03 100644 --- a/dart/lib/src/sentry_trace_origins.dart +++ b/dart/lib/src/sentry_trace_origins.dart @@ -27,7 +27,6 @@ class SentryTraceOrigins { static const autoDbDriftQueryExecutor = 'auto.db.drift.query.executor'; static const autoDbDriftTransactionExecutor = 'auto.db.drift.transaction.executor'; - static const uiLoad = 'ui.load'; - static const uiTimeToInitialDisplay = 'ui.load.initial_display'; - static const uiTimeToFullDisplay = 'ui.load.full_display'; + static const autoUiTimeToDisplay = 'auto.ui.time_to_display'; + static const manualUiTimeToDisplay = 'manual.ui.time_to_display'; } diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index fcb8f40d02..2d514b1307 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; +import '../sentry_flutter_measurement.dart'; import 'display_strategy_evaluator.dart'; import 'time_to_display_transaction_handler.dart'; @@ -22,6 +23,7 @@ class TimeToDisplayTracker { static ISentrySpan? _ttfdSpan; static Timer? _ttfdTimer; static ISentrySpan? _transaction; + static ISentrySpan? get transaction => _transaction; @visibleForTesting @@ -79,22 +81,24 @@ class TimeToDisplayTracker { if (transaction == null) return; _transaction = transaction; - final ttidSpan = _transactionHandler.createSpan( - transaction, - TimeToDisplayType.timeToInitialDisplay, - name, - appStartInfo.start); + final ttidSpan = _transactionHandler.createSpan(transaction, + TimeToDisplayType.timeToInitialDisplay, name, appStartInfo.start); if (_options?.enableTimeToFullDisplayTracing == true) { _ttfdSpan = _transactionHandler.createSpan(transaction, TimeToDisplayType.timeToFullDisplay, name, appStartInfo.start); } - TimeToDisplayTransactionHandler.finishSpan( - transaction: transaction, - span: ttidSpan, - endTimestamp: appStartInfo.end, - measurement: appStartInfo.measurement); + transaction.setMeasurement( + appStartInfo.measurement.name, appStartInfo.measurement.value, + unit: appStartInfo.measurement.unit); + + final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( + Duration(milliseconds: appStartInfo.measurement.value.toInt())); + transaction.setMeasurement(name, ttidMeasurement.value, + unit: ttidMeasurement.unit); + + await ttidSpan.finish(endTimestamp: appStartInfo.end); }); } @@ -130,11 +134,18 @@ class TimeToDisplayTracker { ttfdEndTimestamp == null) { return; } - TimeToDisplayTransactionHandler.finishSpan( - transaction: transaction, - span: ttfdSpan, - endTimestamp: ttfdEndTimestamp, - status: SpanStatus.deadlineExceeded()); + final duration = Duration( + milliseconds: + ttfdEndTimestamp.difference(startTimestamp).inMilliseconds); + + final ttfdMeasurement = + SentryFlutterMeasurement.timeToFullDisplay(duration); + transaction.setMeasurement(ttfdMeasurement.name, ttfdMeasurement.value, + unit: ttfdMeasurement.unit); + + await ttfdSpan.finish( + status: SpanStatus.deadlineExceeded(), + endTimestamp: ttfdEndTimestamp); }); } } @@ -146,14 +157,12 @@ class TimeToDisplayTracker { _ttidEndTimestamp = endTimestamp; final duration = endTimestamp.difference(startTimestamp).inMilliseconds; - final measurement = SentryMeasurement('time_to_initial_display', duration, - unit: DurationSentryMeasurementUnit.milliSecond); - - TimeToDisplayTransactionHandler.finishSpan( - transaction: transaction, - span: ttidSpan, - endTimestamp: endTimestamp, - measurement: measurement); + final measurement = SentryFlutterMeasurement.timeToInitialDisplay( + Duration(milliseconds: duration)); + + transaction.setMeasurement(routeName, measurement.value, + unit: measurement.unit); + await ttidSpan.finish(endTimestamp: endTimestamp); } Future _determineEndTimeOfTTID(String routeName) async { @@ -170,9 +179,11 @@ class TimeToDisplayTracker { if (strategyDecision == TimeToDisplayStrategy.manual) { endTimestamp = DateTime.now(); + _ttidSpan?.origin = SentryTraceOrigins.manualUiTimeToDisplay; } else if (!endTimeCompleter.isCompleted) { // In approximation we want to wait until addPostFrameCallback has triggered await endTimeCompleter.future; + _ttidSpan?.origin = SentryTraceOrigins.autoUiTimeToDisplay; } return endTimestamp; @@ -196,17 +207,26 @@ class TimeToDisplayTracker { if (startTimestamp == null || transaction == null || ttfdSpan == null) { return; } - final duration = endTimestamp.difference(startTimestamp).inMilliseconds; - final measurement = SentryMeasurement('time_to_full_display', duration, - unit: DurationSentryMeasurementUnit.milliSecond); - TimeToDisplayTransactionHandler.finishSpan( - transaction: transaction, - span: ttfdSpan, - endTimestamp: endTimestamp, - measurement: measurement); + final duration = Duration( + milliseconds: endTimestamp.difference(startTimestamp).inMilliseconds); + final measurement = SentryFlutterMeasurement.timeToFullDisplay(duration); + transaction.setMeasurement(measurement.name, measurement.value, + unit: measurement.unit); + + ttfdSpan.finish(endTimestamp: endTimestamp); + } + + void clear() { + _startTimestamp = null; + _ttidEndTimestamp = null; + _ttidSpan = null; + _ttfdSpan = null; + _ttfdTimer = null; + _transaction = null; } } +// TODO move this class abstract class IFrameCallbackHandler { void addPostFrameCallback(FrameCallback callback, {String debugLabel}); } @@ -219,7 +239,7 @@ class FrameCallbackHandler implements IFrameCallbackHandler { } } -class MockFrameCallbackHandler implements IFrameCallbackHandler { +class FakeFrameCallbackHandler implements IFrameCallbackHandler { FrameCallback? storedCallback; @override diff --git a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart index 7bfd599778..8719f980e9 100644 --- a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart +++ b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart @@ -46,7 +46,7 @@ class TimeToDisplayTransactionHandler extends ITimeToDisplayTransactionHandler { final transactionContext = SentryTransactionContext( routeName, - 'ui.load', + SentrySpanOperations.uiLoad, transactionNameSource: SentryTransactionNameSource.component, // ignore: invalid_use_of_internal_member origin: SentryTraceOrigins.autoNavigationRouteObserver, @@ -100,28 +100,16 @@ class TimeToDisplayTransactionHandler extends ITimeToDisplayTransactionHandler { String description; switch (type) { case TimeToDisplayType.timeToInitialDisplay: - operation = SentryTraceOrigins.uiTimeToInitialDisplay; + operation = SentrySpanOperations.uiTimeToInitialDisplay; description = '$routeName initial display'; break; case TimeToDisplayType.timeToFullDisplay: - operation = SentryTraceOrigins.uiTimeToFullDisplay; + operation = SentrySpanOperations.uiTimeToFullDisplay; description = '$routeName full display'; break; } - return transaction.startChild(operation, + final span = transaction.startChild(operation, description: description, startTimestamp: startTimestamp); - } - - static void finishSpan( - {required ISentrySpan span, - required ISentrySpan transaction, - DateTime? endTimestamp, - SentryMeasurement? measurement, - SpanStatus? status}) { - if (measurement != null) { - transaction.setMeasurement(measurement.name, measurement.value, - unit: measurement.unit); - } - span.finish(status: status, endTimestamp: endTimestamp); + return span; } } diff --git a/flutter/lib/src/sentry_flutter_measurement.dart b/flutter/lib/src/sentry_flutter_measurement.dart new file mode 100644 index 0000000000..8b47a3fc56 --- /dev/null +++ b/flutter/lib/src/sentry_flutter_measurement.dart @@ -0,0 +1,23 @@ +import '../sentry_flutter.dart'; + +extension SentryFlutterMeasurement on SentryMeasurement { + /// Duration of the time to initial display in milliseconds + static SentryMeasurement timeToInitialDisplay(Duration duration) { + assert(!duration.isNegative); + return SentryMeasurement( + 'time_to_initial_display', + duration.inMilliseconds.toDouble(), + unit: DurationSentryMeasurementUnit.milliSecond, + ); + } + + /// Duration of the time to full display in milliseconds + static SentryMeasurement timeToFullDisplay(Duration duration) { + assert(!duration.isNegative); + return SentryMeasurement( + 'time_to_full_display', + duration.inMilliseconds.toDouble(), + unit: DurationSentryMeasurementUnit.milliSecond, + ); + } +} diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index 54bddd2d58..d7444a1300 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -14,6 +14,10 @@ void main() { fixture = Fixture(); }); + tearDown(() async { + await Future.delayed(const Duration(milliseconds: 500)); + }); + group('time to initial display', () { group('in root screen app start route', () { test('startMeasurement finishes ttid span', () async { @@ -34,7 +38,7 @@ void main() { final spans = transaction.children; expect(transaction.children, hasLength(1)); expect(spans[0].context.operation, - SentryTraceOrigins.uiTimeToInitialDisplay); + SentrySpanOperations.uiTimeToInitialDisplay); expect(spans[0].finished, isTrue); }); }); @@ -52,7 +56,7 @@ void main() { final spans = transaction.children; expect(transaction.children, hasLength(1)); expect(spans[0].context.operation, - SentryTraceOrigins.uiTimeToInitialDisplay); + SentrySpanOperations.uiTimeToInitialDisplay); expect(spans[0].finished, isTrue); }); }); @@ -72,7 +76,7 @@ void main() { final ttidSpan = transaction.children .where((element) => element.context.operation == - SentryTraceOrigins.uiTimeToInitialDisplay) + SentrySpanOperations.uiTimeToInitialDisplay) .first; expect(ttidSpan, isNotNull); @@ -100,9 +104,9 @@ void main() { final spans = transaction.children; expect(transaction.children, hasLength(2)); expect(spans[0].context.operation, - SentryTraceOrigins.uiTimeToInitialDisplay); + SentrySpanOperations.uiTimeToInitialDisplay); expect( - spans[1].context.operation, SentryTraceOrigins.uiTimeToFullDisplay); + spans[1].context.operation, SentrySpanOperations.uiTimeToFullDisplay); }); group('in root screen app start route', () { @@ -115,7 +119,8 @@ void main() { AppStartTracker().setAppStartInfo(AppStartInfo( DateTime.fromMillisecondsSinceEpoch(0), DateTime.fromMillisecondsSinceEpoch(10), - SentryMeasurement('', 0))); + SentryMeasurement('', 10, + unit: DurationSentryMeasurementUnit.milliSecond))); await Future.delayed(const Duration(milliseconds: 100)); @@ -124,11 +129,12 @@ void main() { final ttfdSpan = transaction.children .where((element) => element.context.operation == - SentryTraceOrigins.uiTimeToFullDisplay) + SentrySpanOperations.uiTimeToFullDisplay) .first; expect(ttfdSpan, isNotNull); SentryFlutter.reportFullyDisplayed(); + await Future.delayed(const Duration(milliseconds: 100)); expect(ttfdSpan.finished, isTrue); @@ -147,7 +153,7 @@ void main() { final ttfdSpan = transaction.children .where((element) => element.context.operation == - SentryTraceOrigins.uiTimeToFullDisplay) + SentrySpanOperations.uiTimeToFullDisplay) .first; expect(ttfdSpan, isNotNull); @@ -172,14 +178,14 @@ void main() { final ttfdSpan = transaction.children .where((element) => element.context.operation == - SentryTraceOrigins.uiTimeToFullDisplay) + SentrySpanOperations.uiTimeToFullDisplay) .first; expect(ttfdSpan, isNotNull); final ttidSpan = transaction.children .where((element) => element.context.operation == - SentryTraceOrigins.uiTimeToInitialDisplay) + SentrySpanOperations.uiTimeToInitialDisplay) .first; expect(ttfdSpan, isNotNull); @@ -200,7 +206,7 @@ void main() { final transaction = fixture.hub.getSpan(); expect(transaction, isNotNull); - expect(transaction?.context.operation, SentryTraceOrigins.uiLoad); + expect(transaction?.context.operation, SentrySpanOperations.uiLoad); }); } @@ -209,7 +215,7 @@ class Fixture { ..dsn = fakeDsn ..tracesSampleRate = 1.0; - final frameCallbackHandler = MockFrameCallbackHandler(); + final frameCallbackHandler = FakeFrameCallbackHandler(); late final hub = Hub(options); From 00e7684ea09e60f285b47054b27cbba85a6d0ce8 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 21 Feb 2024 02:27:43 +0100 Subject: [PATCH 27/47] Improve code --- flutter/lib/src/frame_callback_handler.dart | 14 ++ .../display_strategy_evaluator.dart | 63 -------- .../src/navigation/sentry_display_widget.dart | 5 +- .../navigation/sentry_navigator_observer.dart | 46 ++++++ .../navigation/time_to_display_tracker.dart | 152 +++++------------- flutter/lib/src/sentry_flutter.dart | 4 - flutter/test/fake_frame_callback_handler.dart | 20 +++ .../time_to_display_tracker_test.dart | 5 +- .../test/sentry_navigator_observer_test.dart | 2 +- 9 files changed, 126 insertions(+), 185 deletions(-) create mode 100644 flutter/lib/src/frame_callback_handler.dart delete mode 100644 flutter/lib/src/navigation/display_strategy_evaluator.dart create mode 100644 flutter/test/fake_frame_callback_handler.dart diff --git a/flutter/lib/src/frame_callback_handler.dart b/flutter/lib/src/frame_callback_handler.dart new file mode 100644 index 0000000000..40c19779db --- /dev/null +++ b/flutter/lib/src/frame_callback_handler.dart @@ -0,0 +1,14 @@ +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; + +abstract class IFrameCallbackHandler { + void addPostFrameCallback(FrameCallback callback, {String debugLabel}); +} + +class FrameCallbackHandler implements IFrameCallbackHandler { + @override + void addPostFrameCallback(FrameCallback callback, + {String debugLabel = 'callback'}) { + WidgetsBinding.instance.addPostFrameCallback(callback); + } +} diff --git a/flutter/lib/src/navigation/display_strategy_evaluator.dart b/flutter/lib/src/navigation/display_strategy_evaluator.dart deleted file mode 100644 index 7bc2c829cc..0000000000 --- a/flutter/lib/src/navigation/display_strategy_evaluator.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; - -@internal -class DisplayStrategyEvaluator { - static final DisplayStrategyEvaluator _instance = - DisplayStrategyEvaluator._internal(); - - factory DisplayStrategyEvaluator() { - return _instance; - } - - DisplayStrategyEvaluator._internal(); - - final Map _manualReportReceived = {}; - final Map _timers = {}; - final Map> _completers = {}; - - Future decideStrategy(String routeName) { - // Ensure initialization of a completer for the given route name. - if (!_completers.containsKey(routeName) || - _completers[routeName]!.isCompleted) { - _completers[routeName] = Completer(); - } - var completer = _completers[routeName]!; - - // Start or reset the timer only if a manual report has not been received. - if (!_manualReportReceived.containsKey(routeName) || - !_manualReportReceived[routeName]!) { - _timers[routeName]?.cancel(); // Cancel any existing timer. - _timers[routeName] = Timer(Duration(seconds: 1), () { - if (!_manualReportReceived.containsKey(routeName) || - !_manualReportReceived[routeName]!) { - if (!completer.isCompleted) { - completer.complete(TimeToDisplayStrategy.approximation); - } - } - }); - } - - return completer.future; - } - - bool reportManual(String routeName) { - var wasReportedAlready = _manualReportReceived[routeName] ?? false; - _manualReportReceived[routeName] = true; - - // Complete the strategy decision as manual if within the timeout period. - if (_completers[routeName]?.isCompleted == false) { - _completers[routeName]?.complete(TimeToDisplayStrategy.manual); - } - - // Cancel the timer as it's no longer necessary. - _timers[routeName]?.cancel(); - return wasReportedAlready; - } -} - -enum TimeToDisplayStrategy { - manual, - approximation, -} diff --git a/flutter/lib/src/navigation/sentry_display_widget.dart b/flutter/lib/src/navigation/sentry_display_widget.dart index 11ace688fc..1b5a7dbe69 100644 --- a/flutter/lib/src/navigation/sentry_display_widget.dart +++ b/flutter/lib/src/navigation/sentry_display_widget.dart @@ -15,9 +15,10 @@ class _SentryDisplayWidgetState extends State { @override void initState() { super.initState(); + TimeToInitialDisplayTracker().markAsManual(); + WidgetsBinding.instance.addPostFrameCallback((_) { - final route = ModalRoute.of(context); - SentryFlutter.reportInitiallyDisplayed(routeName: route?.settings.name); + TimeToInitialDisplayTracker().completeTracking(); }); } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 3213e8a61e..ae77c256ed 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -84,6 +86,7 @@ class SentryNavigatorObserver extends RouteObserver> { enableAutoTransactions: enableAutoTransactions, autoFinishAfter: autoFinishAfter, ); + } final Hub _hub; @@ -274,3 +277,46 @@ extension NativeFramesMeasurement on NativeFrames { }; } } + +class TimeToInitialDisplayTracker { + static final TimeToInitialDisplayTracker _instance = TimeToInitialDisplayTracker._internal(); + factory TimeToInitialDisplayTracker() => _instance; + TimeToInitialDisplayTracker._internal(); + + DateTime? _startTime; + bool _isManual = false; + Completer? _trackingCompleter; + + /// Starts the TTID tracking process and returns a Future that completes + /// with the tracking duration when tracking is completed. + Future? determineEndTime() { + _startTime = DateTime.now(); + _trackingCompleter = Completer(); + + // Schedules a check at the end of the frame to determine if the tracking + // should be completed immediately (approximation mode) or deferred (manual mode). + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_isManual) { + completeTracking(); + } + }); + + return _trackingCompleter?.future; + } + + void markAsManual() { + _isManual = true; + } + + void completeTracking() { + if (_startTime != null) { + final endTime = DateTime.now(); + // Reset after completion + _startTime = null; + _isManual = false; + _trackingCompleter?.complete(endTime); + } + } +} + + diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index 2d514b1307..66a0d33882 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -1,13 +1,12 @@ import 'dart:async'; -import 'package:flutter/scheduler.dart'; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; +import '../frame_callback_handler.dart'; import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; import '../sentry_flutter_measurement.dart'; -import 'display_strategy_evaluator.dart'; import 'time_to_display_transaction_handler.dart'; @internal @@ -15,11 +14,11 @@ class TimeToDisplayTracker { final Hub _hub; final SentryNative? _native; final TimeToDisplayTransactionHandler _transactionHandler; - final IFrameCallbackHandler _frameCallbackHandler; + final TimeToInitialDisplayTracker _timeToInitialDisplayTracker; + // We need to keep these static to be able to access them from reportFullyDisplayed static DateTime? _startTimestamp; static DateTime? _ttidEndTimestamp; - static ISentrySpan? _ttidSpan; static ISentrySpan? _ttfdSpan; static Timer? _ttfdTimer; static ISentrySpan? _transaction; @@ -42,13 +41,13 @@ class TimeToDisplayTracker { TimeToDisplayTransactionHandler? transactionHandler, }) : _hub = hub ?? HubAdapter(), _native = SentryFlutter.native, - _frameCallbackHandler = frameCallbackHandler ?? FrameCallbackHandler(), _transactionHandler = transactionHandler ?? TimeToDisplayTransactionHandler( hub: hub, enableAutoTransactions: enableAutoTransactions, autoFinishAfter: autoFinishAfter, - ); + ), + _timeToInitialDisplayTracker = TimeToInitialDisplayTracker(); void startMeasurement(String? routeName, Object? arguments) async { final startTimestamp = DateTime.now(); @@ -111,90 +110,51 @@ class TimeToDisplayTracker { if (transaction == null || routeName == null) return; _transaction = transaction; - _initializeTimeToDisplaySpans(transaction, routeName, startTimestamp); - - final ttidSpan = _ttidSpan; - if (ttidSpan == null) return; - - _finishInitialDisplay(ttidSpan, transaction, routeName, startTimestamp); - } - - void _initializeTimeToDisplaySpans( - ISentrySpan transaction, String routeName, DateTime startTimestamp) { - _ttidSpan = _transactionHandler.createSpan(transaction, - TimeToDisplayType.timeToInitialDisplay, routeName, startTimestamp); - if (_options?.enableTimeToFullDisplayTracing == true) { - _ttfdSpan = _transactionHandler.createSpan(transaction, - TimeToDisplayType.timeToFullDisplay, routeName, startTimestamp); - _ttfdTimer = Timer(ttfdAutoFinishAfter, () async { - final ttfdSpan = _ttfdSpan; - final ttfdEndTimestamp = _ttidEndTimestamp; - if (ttfdSpan == null || - ttfdSpan.finished == true || - ttfdEndTimestamp == null) { - return; - } - final duration = Duration( - milliseconds: - ttfdEndTimestamp.difference(startTimestamp).inMilliseconds); - - final ttfdMeasurement = - SentryFlutterMeasurement.timeToFullDisplay(duration); - transaction.setMeasurement(ttfdMeasurement.name, ttfdMeasurement.value, - unit: ttfdMeasurement.unit); - - await ttfdSpan.finish( - status: SpanStatus.deadlineExceeded(), - endTimestamp: ttfdEndTimestamp); - }); - } + await _trackTimeToInitialDisplay(transaction, startTimestamp, routeName); + _initializeTimeToFullDisplay(transaction, startTimestamp, routeName); } - void _finishInitialDisplay(ISentrySpan ttidSpan, ISentrySpan transaction, - String routeName, DateTime startTimestamp) async { - final endTimestamp = await _determineEndTimeOfTTID(routeName); - if (endTimestamp == null) return; + Future _trackTimeToInitialDisplay(ISentrySpan transaction, + DateTime startTimestamp, String routeName) async { + final endTimestamp = await _timeToInitialDisplayTracker.determineEndTime(); _ttidEndTimestamp = endTimestamp; - - final duration = endTimestamp.difference(startTimestamp).inMilliseconds; - final measurement = SentryFlutterMeasurement.timeToInitialDisplay( - Duration(milliseconds: duration)); - - transaction.setMeasurement(routeName, measurement.value, - unit: measurement.unit); - await ttidSpan.finish(endTimestamp: endTimestamp); + final ttidSpan = _transactionHandler.createSpan(transaction, + TimeToDisplayType.timeToInitialDisplay, routeName, startTimestamp); + return ttidSpan.finish(endTimestamp: endTimestamp); } - Future _determineEndTimeOfTTID(String routeName) async { - DateTime? endTimestamp; - final endTimeCompleter = Completer(); + void _initializeTimeToFullDisplay( + ISentrySpan transaction, DateTime startTimestamp, String routeName) { + if (_options?.enableTimeToFullDisplayTracing == false) { + return; + } - _frameCallbackHandler.addPostFrameCallback((_) { - endTimestamp = DateTime.now(); - endTimeCompleter.complete(endTimestamp); + final ttfdSpan = _transactionHandler.createSpan(transaction, + TimeToDisplayType.timeToFullDisplay, routeName, startTimestamp); + _ttfdSpan = ttfdSpan; + _ttfdTimer = Timer(ttfdAutoFinishAfter, () async { + handleTimeToFullDisplayTimeout(transaction, startTimestamp); }); + } - final strategyDecision = - await DisplayStrategyEvaluator().decideStrategy(routeName); - - if (strategyDecision == TimeToDisplayStrategy.manual) { - endTimestamp = DateTime.now(); - _ttidSpan?.origin = SentryTraceOrigins.manualUiTimeToDisplay; - } else if (!endTimeCompleter.isCompleted) { - // In approximation we want to wait until addPostFrameCallback has triggered - await endTimeCompleter.future; - _ttidSpan?.origin = SentryTraceOrigins.autoUiTimeToDisplay; + void handleTimeToFullDisplayTimeout( + ISentrySpan transaction, DateTime startTimestamp) async { + final ttfdSpan = _ttfdSpan; + final ttfdEndTimestamp = _ttidEndTimestamp ?? DateTime.now(); + if (ttfdSpan == null || ttfdSpan.finished == true) { + return; } + final duration = Duration( + milliseconds: + ttfdEndTimestamp.difference(startTimestamp).inMilliseconds); - return endTimestamp; - } - - @internal - static void reportInitiallyDisplayed({String? routeName}) { - routeName = routeName ?? SentryNavigatorObserver.currentRouteName; + final ttfdMeasurement = + SentryFlutterMeasurement.timeToFullDisplay(duration); + transaction.setMeasurement(ttfdMeasurement.name, ttfdMeasurement.value, + unit: ttfdMeasurement.unit); - if (routeName == null) return; - DisplayStrategyEvaluator().reportManual(routeName); + await ttfdSpan.finish( + status: SpanStatus.deadlineExceeded(), endTimestamp: ttfdEndTimestamp); } @internal @@ -215,38 +175,4 @@ class TimeToDisplayTracker { ttfdSpan.finish(endTimestamp: endTimestamp); } - - void clear() { - _startTimestamp = null; - _ttidEndTimestamp = null; - _ttidSpan = null; - _ttfdSpan = null; - _ttfdTimer = null; - _transaction = null; - } -} - -// TODO move this class -abstract class IFrameCallbackHandler { - void addPostFrameCallback(FrameCallback callback, {String debugLabel}); -} - -class FrameCallbackHandler implements IFrameCallbackHandler { - @override - void addPostFrameCallback(FrameCallback callback, - {String debugLabel = 'callback'}) { - SchedulerBinding.instance.addPostFrameCallback(callback); - } -} - -class FakeFrameCallbackHandler implements IFrameCallbackHandler { - FrameCallback? storedCallback; - - @override - void addPostFrameCallback(FrameCallback callback, - {String debugLabel = 'callback'}) { - Future.delayed(Duration(milliseconds: 500), () { - callback(Duration.zero); - }); - } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 6e6ed14aac..4a61bd06f3 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -230,10 +230,6 @@ mixin SentryFlutter { options.sdk = sdk; } - static void reportInitiallyDisplayed({String? routeName}) { - TimeToDisplayTracker.reportInitiallyDisplayed(routeName: routeName); - } - /// Reports the time it took for the screen to be fully displayed. static void reportFullyDisplayed() { TimeToDisplayTracker.reportFullyDisplayed(); diff --git a/flutter/test/fake_frame_callback_handler.dart b/flutter/test/fake_frame_callback_handler.dart new file mode 100644 index 0000000000..e03a40348a --- /dev/null +++ b/flutter/test/fake_frame_callback_handler.dart @@ -0,0 +1,20 @@ +import 'package:flutter/scheduler.dart'; +import 'package:sentry_flutter/src/frame_callback_handler.dart'; + +class FakeFrameCallbackHandler implements IFrameCallbackHandler { + FrameCallback? storedCallback; + + final Duration _finishAfterDuration; + + FakeFrameCallbackHandler( + {Duration finishAfterDuration = const Duration(milliseconds: 500)}) + : _finishAfterDuration = finishAfterDuration; + + @override + void addPostFrameCallback(FrameCallback callback, + {String debugLabel = 'callback'}) { + Future.delayed(_finishAfterDuration, () { + callback(Duration.zero); + }); + } +} diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index d7444a1300..c289fbadd7 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -4,6 +4,7 @@ import 'package:sentry_flutter/src/integrations/integrations.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import '../fake_frame_callback_handler.dart'; import '../mocks.dart'; void main() { @@ -71,7 +72,7 @@ void main() { await Future.delayed(const Duration(milliseconds: 100)); - SentryFlutter.reportInitiallyDisplayed(routeName: 'Current Route'); + // SentryFlutter.reportInitiallyDisplayed(routeName: 'Current Route'); final ttidSpan = transaction.children .where((element) => @@ -94,7 +95,7 @@ void main() { }); test('startMeasurement creates ttfd and ttid span', () async { - final sut = fixture.getSut(enableTimeToFullDisplayTracing: true); + final sut = fixture.getSut(); sut.startMeasurement('Current Route', null); diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index cef0e0b8ab..cb1c5b271e 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -98,7 +98,7 @@ void main() { sut.didPush(currentRoute, null); await Future.delayed(Duration(milliseconds: 50)); - DisplayStrategyEvaluator().reportManual('Current Route'); + TTIDModeEvaluator().reportManual('Current Route'); // Get ref to created transaction // ignore: invalid_use_of_internal_member From b3da58a7ec53e37581d0d3aa6c7465424b904f2e Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 21 Feb 2024 02:54:49 +0100 Subject: [PATCH 28/47] Improve code --- .../native_app_start_integration.dart | 4 +- .../src/navigation/sentry_display_widget.dart | 6 +- .../navigation/sentry_navigator_observer.dart | 44 ---------- .../navigation/time_to_display_tracker.dart | 87 ++++++++----------- .../time_to_display_transaction_handler.dart | 11 +-- .../time_to_initial_display_tracker.dart | 42 +++++++++ .../native_app_start_integration_test.dart | 8 +- .../test/sentry_navigator_observer_test.dart | 40 ++++----- 8 files changed, 111 insertions(+), 131 deletions(-) create mode 100644 flutter/lib/src/navigation/time_to_initial_display_tracker.dart diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 7331f61fb2..0151a602b3 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -65,7 +65,8 @@ class NativeAppStartIntegration extends Integration { } } - options.addEventProcessor(NativeAppStartEventProcessor(appStartTracker: _appStartTracker)); + options.addEventProcessor( + NativeAppStartEventProcessor(appStartTracker: _appStartTracker)); options.sdk.addIntegration('nativeAppStartIntegration'); } @@ -111,6 +112,7 @@ class AppStartTracker extends IAppStartTracker { _notifyObserver(); } + // TODO: replace this with a future @override void onAppStartComplete(Function(AppStartInfo?) callback) { _callback = callback; diff --git a/flutter/lib/src/navigation/sentry_display_widget.dart b/flutter/lib/src/navigation/sentry_display_widget.dart index 1b5a7dbe69..24a27d31be 100644 --- a/flutter/lib/src/navigation/sentry_display_widget.dart +++ b/flutter/lib/src/navigation/sentry_display_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; import '../../sentry_flutter.dart'; @@ -15,10 +16,11 @@ class _SentryDisplayWidgetState extends State { @override void initState() { super.initState(); - TimeToInitialDisplayTracker().markAsManual(); + // TODO: add via dependency injection + TTIDEndTimeTracker().markAsManual(); WidgetsBinding.instance.addPostFrameCallback((_) { - TimeToInitialDisplayTracker().completeTracking(); + TTIDEndTimeTracker().completeTracking(); }); } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index ae77c256ed..a074603521 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -86,7 +86,6 @@ class SentryNavigatorObserver extends RouteObserver> { enableAutoTransactions: enableAutoTransactions, autoFinishAfter: autoFinishAfter, ); - } final Hub _hub; @@ -277,46 +276,3 @@ extension NativeFramesMeasurement on NativeFrames { }; } } - -class TimeToInitialDisplayTracker { - static final TimeToInitialDisplayTracker _instance = TimeToInitialDisplayTracker._internal(); - factory TimeToInitialDisplayTracker() => _instance; - TimeToInitialDisplayTracker._internal(); - - DateTime? _startTime; - bool _isManual = false; - Completer? _trackingCompleter; - - /// Starts the TTID tracking process and returns a Future that completes - /// with the tracking duration when tracking is completed. - Future? determineEndTime() { - _startTime = DateTime.now(); - _trackingCompleter = Completer(); - - // Schedules a check at the end of the frame to determine if the tracking - // should be completed immediately (approximation mode) or deferred (manual mode). - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!_isManual) { - completeTracking(); - } - }); - - return _trackingCompleter?.future; - } - - void markAsManual() { - _isManual = true; - } - - void completeTracking() { - if (_startTime != null) { - final endTime = DateTime.now(); - // Reset after completion - _startTime = null; - _isManual = false; - _trackingCompleter?.complete(endTime); - } - } -} - - diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index 66a0d33882..f2dd930ce3 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -8,13 +8,14 @@ import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; import '../sentry_flutter_measurement.dart'; import 'time_to_display_transaction_handler.dart'; +import 'time_to_initial_display_tracker.dart'; @internal class TimeToDisplayTracker { final Hub _hub; final SentryNative? _native; final TimeToDisplayTransactionHandler _transactionHandler; - final TimeToInitialDisplayTracker _timeToInitialDisplayTracker; + final TTIDEndTimeTracker _timeToInitialDisplayTracker; // We need to keep these static to be able to access them from reportFullyDisplayed static DateTime? _startTimestamp; @@ -47,9 +48,9 @@ class TimeToDisplayTracker { enableAutoTransactions: enableAutoTransactions, autoFinishAfter: autoFinishAfter, ), - _timeToInitialDisplayTracker = TimeToInitialDisplayTracker(); + _timeToInitialDisplayTracker = TTIDEndTimeTracker(); - void startMeasurement(String? routeName, Object? arguments) async { + Future startMeasurement(String? routeName, Object? arguments) async { final startTimestamp = DateTime.now(); _startTimestamp = startTimestamp; @@ -84,23 +85,22 @@ class TimeToDisplayTracker { TimeToDisplayType.timeToInitialDisplay, name, appStartInfo.start); if (_options?.enableTimeToFullDisplayTracing == true) { - _ttfdSpan = _transactionHandler.createSpan(transaction, - TimeToDisplayType.timeToFullDisplay, name, appStartInfo.start); + _initializeTTFD(transaction, appStartInfo.start, name); } - transaction.setMeasurement( - appStartInfo.measurement.name, appStartInfo.measurement.value, - unit: appStartInfo.measurement.unit); - - final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( - Duration(milliseconds: appStartInfo.measurement.value.toInt())); - transaction.setMeasurement(name, ttidMeasurement.value, - unit: ttidMeasurement.unit); + _setAppStartMeasurement(ttidSpan, appStartInfo); await ttidSpan.finish(endTimestamp: appStartInfo.end); }); } + void _setAppStartMeasurement(ISentrySpan transaction, AppStartInfo appStartInfo) { + transaction.setMeasurement(appStartInfo.measurement.name, appStartInfo.measurement.value, unit: appStartInfo.measurement.unit); + + final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay(Duration(milliseconds: appStartInfo.measurement.value.toInt())); + transaction.setMeasurement(ttidMeasurement.name, ttidMeasurement.value, unit: ttidMeasurement.unit); + } + // Handles measuring navigation for regular routes void _handleRegularRouteMeasurement( String? routeName, Object? arguments, DateTime startTimestamp) async { @@ -110,11 +110,14 @@ class TimeToDisplayTracker { if (transaction == null || routeName == null) return; _transaction = transaction; - await _trackTimeToInitialDisplay(transaction, startTimestamp, routeName); - _initializeTimeToFullDisplay(transaction, startTimestamp, routeName); + await _trackTTID(transaction, startTimestamp, routeName); + + if (_options?.enableTimeToFullDisplayTracing == true) { + _initializeTTFD(transaction, startTimestamp, routeName); + } } - Future _trackTimeToInitialDisplay(ISentrySpan transaction, + Future _trackTTID(ISentrySpan transaction, DateTime startTimestamp, String routeName) async { final endTimestamp = await _timeToInitialDisplayTracker.determineEndTime(); _ttidEndTimestamp = endTimestamp; @@ -123,38 +126,21 @@ class TimeToDisplayTracker { return ttidSpan.finish(endTimestamp: endTimestamp); } - void _initializeTimeToFullDisplay( + void _initializeTTFD( ISentrySpan transaction, DateTime startTimestamp, String routeName) { - if (_options?.enableTimeToFullDisplayTracing == false) { - return; - } - - final ttfdSpan = _transactionHandler.createSpan(transaction, + _ttfdSpan = _transactionHandler.createSpan(transaction, TimeToDisplayType.timeToFullDisplay, routeName, startTimestamp); - _ttfdSpan = ttfdSpan; - _ttfdTimer = Timer(ttfdAutoFinishAfter, () async { - handleTimeToFullDisplayTimeout(transaction, startTimestamp); - }); + _ttfdTimer = Timer(ttfdAutoFinishAfter, handleTimeToFullDisplayTimeout); } - void handleTimeToFullDisplayTimeout( - ISentrySpan transaction, DateTime startTimestamp) async { + void handleTimeToFullDisplayTimeout() { final ttfdSpan = _ttfdSpan; - final ttfdEndTimestamp = _ttidEndTimestamp ?? DateTime.now(); - if (ttfdSpan == null || ttfdSpan.finished == true) { - return; - } - final duration = Duration( - milliseconds: - ttfdEndTimestamp.difference(startTimestamp).inMilliseconds); - - final ttfdMeasurement = - SentryFlutterMeasurement.timeToFullDisplay(duration); - transaction.setMeasurement(ttfdMeasurement.name, ttfdMeasurement.value, - unit: ttfdMeasurement.unit); + final endTimestamp = _ttidEndTimestamp ?? DateTime.now(); + final startTimestamp = _startTimestamp; + if (ttfdSpan == null || ttfdSpan.finished == true || startTimestamp == null) return; - await ttfdSpan.finish( - status: SpanStatus.deadlineExceeded(), endTimestamp: ttfdEndTimestamp); + _setTTFDMeasurement(startTimestamp, endTimestamp); + ttfdSpan.finish(status: SpanStatus.deadlineExceeded(), endTimestamp: endTimestamp); } @internal @@ -162,17 +148,16 @@ class TimeToDisplayTracker { _ttfdTimer?.cancel(); final endTimestamp = DateTime.now(); final startTimestamp = _startTimestamp; - final transaction = _transaction; final ttfdSpan = _ttfdSpan; - if (startTimestamp == null || transaction == null || ttfdSpan == null) { - return; - } - final duration = Duration( - milliseconds: endTimestamp.difference(startTimestamp).inMilliseconds); - final measurement = SentryFlutterMeasurement.timeToFullDisplay(duration); - transaction.setMeasurement(measurement.name, measurement.value, - unit: measurement.unit); + if (ttfdSpan == null || ttfdSpan.finished == true || startTimestamp == null) return; + _setTTFDMeasurement(startTimestamp, endTimestamp); ttfdSpan.finish(endTimestamp: endTimestamp); } + + static void _setTTFDMeasurement(DateTime startTimestamp, DateTime endTimestamp) { + final duration = endTimestamp.difference(startTimestamp); + final measurement = SentryFlutterMeasurement.timeToFullDisplay(duration); + _transaction?.setMeasurement(measurement.name, measurement.value, unit: measurement.unit); + } } diff --git a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart index 8719f980e9..a3e03d37ad 100644 --- a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart +++ b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart @@ -5,16 +5,7 @@ import '../native/sentry_native.dart'; enum TimeToDisplayType { timeToInitialDisplay, timeToFullDisplay } @internal -abstract class ITimeToDisplayTransactionHandler { - Future startTransaction(String? routeName, Object? arguments, - {DateTime? startTimestamp}); - - ISentrySpan createSpan(ISentrySpan transaction, TimeToDisplayType type, - String routeName, DateTime startTimestamp); -} - -@internal -class TimeToDisplayTransactionHandler extends ITimeToDisplayTransactionHandler { +class TimeToDisplayTransactionHandler { final Hub? _hub; final bool? _enableAutoTransactions; final Duration? _autoFinishAfter; diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart new file mode 100644 index 0000000000..fd46eb4fec --- /dev/null +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +class TTIDEndTimeTracker { + static final TTIDEndTimeTracker _instance = + TTIDEndTimeTracker._internal(); + factory TTIDEndTimeTracker() => _instance; + TTIDEndTimeTracker._internal(); + + bool _isManual = false; + Completer? _trackingCompleter; + + /// Starts the TTID end time tracking process and returns a Future that completes + /// with the tracking duration when tracking is completed. + Future? determineEndTime() { + _trackingCompleter = Completer(); + + // Schedules a check at the end of the frame to determine if the tracking + // should be completed immediately (approximation mode) or deferred (manual mode). + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_isManual) { + completeTracking(); + } + }); + + return _trackingCompleter?.future; + } + + void markAsManual() { + _isManual = true; + } + + void completeTracking() { + if (_trackingCompleter != null && !_trackingCompleter!.isCompleted) { + final endTime = DateTime.now(); + // Reset after completion + _isManual = false; + _trackingCompleter?.complete(endTime); + } + } +} diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index 144eabd77d..aa187280d7 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -119,8 +119,9 @@ void main() { expect(enriched.measurements.isEmpty, true); }); - - test('native app start measurement only added once for multiple different transactions', () async { + test( + 'native app start measurement only added once for multiple different transactions', + () async { fixture.options.autoAppStart = false; await fixture.native.fetchNativeAppStart(); fixture.appStartTracker.setAppStartInfo( @@ -144,7 +145,8 @@ void main() { // Represents any other transaction that happened afterwards final transaction2 = SentryTransaction(tracer); - var secondEnriched = await processor.apply(transaction2) as SentryTransaction; + var secondEnriched = + await processor.apply(transaction2) as SentryTransaction; expect(secondEnriched.measurements.length, 0); }); }); diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index cb1c5b271e..a79749a766 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -60,8 +60,8 @@ void main() { final tracer = getMockSentryTracer(); _whenAnyStart(mockHub, tracer); when(tracer.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); when(tracer.finished).thenReturn(false); when(tracer.status).thenReturn(SpanStatus.ok()); @@ -146,8 +146,8 @@ void main() { when(span.finished).thenReturn(false); when(span.status).thenReturn(SpanStatus.ok()); when(span.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); final sut = fixture.getSut( @@ -272,8 +272,8 @@ void main() { when(span.finished).thenReturn(false); when(span.status).thenReturn(SpanStatus.ok()); when(span.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); @@ -305,8 +305,8 @@ void main() { when(span.status).thenReturn(null); when(span.finished).thenReturn(false); when(span.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); @@ -328,8 +328,8 @@ void main() { when(span.status).thenReturn(null); when(span.finished).thenReturn(false); when(span.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); @@ -355,8 +355,8 @@ void main() { when(span.status).thenReturn(null); when(span.finished).thenReturn(false); when(span.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); @@ -376,8 +376,8 @@ void main() { when(span.finished).thenReturn(false); when(span.status).thenReturn(SpanStatus.ok()); when(span.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); @@ -412,8 +412,8 @@ void main() { when(span.finished).thenReturn(false); when(span.status).thenReturn(SpanStatus.ok()); when(span.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); @@ -440,8 +440,8 @@ void main() { when(span.finished).thenReturn(false); when(span.status).thenReturn(SpanStatus.ok()); when(span.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); @@ -469,8 +469,8 @@ void main() { when(span.status).thenReturn(null); when(span.finished).thenReturn(false); when(span.startChild('ui.load.initial_display', - description: anyNamed('description'), - startTimestamp: anyNamed('startTimestamp'))) + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, span); From f888f839ede7afb37576836b226f2fffc7fda2e5 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 21 Feb 2024 11:34:38 +0100 Subject: [PATCH 29/47] Update --- flutter/lib/src/navigation/time_to_display_tracker.dart | 6 +++--- .../lib/src/navigation/time_to_initial_display_tracker.dart | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index f2dd930ce3..f167d872e7 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -15,7 +15,7 @@ class TimeToDisplayTracker { final Hub _hub; final SentryNative? _native; final TimeToDisplayTransactionHandler _transactionHandler; - final TTIDEndTimeTracker _timeToInitialDisplayTracker; + final TTIDEndTimeTracker _ttidEndTimeTracker; // We need to keep these static to be able to access them from reportFullyDisplayed static DateTime? _startTimestamp; @@ -48,7 +48,7 @@ class TimeToDisplayTracker { enableAutoTransactions: enableAutoTransactions, autoFinishAfter: autoFinishAfter, ), - _timeToInitialDisplayTracker = TTIDEndTimeTracker(); + _ttidEndTimeTracker = TTIDEndTimeTracker(); Future startMeasurement(String? routeName, Object? arguments) async { final startTimestamp = DateTime.now(); @@ -119,7 +119,7 @@ class TimeToDisplayTracker { Future _trackTTID(ISentrySpan transaction, DateTime startTimestamp, String routeName) async { - final endTimestamp = await _timeToInitialDisplayTracker.determineEndTime(); + final endTimestamp = await _ttidEndTimeTracker.determineEndTime(); _ttidEndTimestamp = endTimestamp; final ttidSpan = _transactionHandler.createSpan(transaction, TimeToDisplayType.timeToInitialDisplay, routeName, startTimestamp); diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart index fd46eb4fec..0cbeb67c98 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; +@internal class TTIDEndTimeTracker { static final TTIDEndTimeTracker _instance = TTIDEndTimeTracker._internal(); @@ -11,8 +13,6 @@ class TTIDEndTimeTracker { bool _isManual = false; Completer? _trackingCompleter; - /// Starts the TTID end time tracking process and returns a Future that completes - /// with the tracking duration when tracking is completed. Future? determineEndTime() { _trackingCompleter = Completer(); From 1478af3d37d4cba921dc14e8710bc9c76180ab46 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 21 Feb 2024 13:08:38 +0100 Subject: [PATCH 30/47] Refactor ttfd --- flutter/example/lib/main.dart | 2 +- .../navigation/sentry_navigator_observer.dart | 2 +- .../navigation/time_to_display_tracker.dart | 79 ++++++++++++------- .../time_to_full_display_tracker.dart | 76 ++++++++++++++++++ flutter/lib/src/sentry_flutter.dart | 4 +- 5 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 flutter/lib/src/navigation/time_to_full_display_tracker.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 80101c8c2b..2c68410354 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -78,7 +78,7 @@ Future setupSentry(AppRunner appRunner, String dsn, // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; - // options.enableTimeToFullDisplayTracing = true; + options.enableTimeToFullDisplayTracing = true; options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index a074603521..9a4354a305 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -196,7 +196,7 @@ class SentryNavigatorObserver extends RouteObserver> { _currentRouteName = routeName; final arguments = route?.settings.arguments; - _timeToDisplayTracker.startMeasurement(routeName, arguments); + await _timeToDisplayTracker.startMeasurement(routeName, arguments); } } diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index f167d872e7..622acf1dae 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import 'package:sentry_flutter/src/navigation/time_to_full_display_tracker.dart'; import '../../sentry_flutter.dart'; import '../frame_callback_handler.dart'; @@ -14,7 +15,7 @@ import 'time_to_initial_display_tracker.dart'; class TimeToDisplayTracker { final Hub _hub; final SentryNative? _native; - final TimeToDisplayTransactionHandler _transactionHandler; + final TimeToDisplayTransactionHandler _ttdTransactionHandler; final TTIDEndTimeTracker _ttidEndTimeTracker; // We need to keep these static to be able to access them from reportFullyDisplayed @@ -23,6 +24,9 @@ class TimeToDisplayTracker { static ISentrySpan? _ttfdSpan; static Timer? _ttfdTimer; static ISentrySpan? _transaction; + static TTFDState ttfdState = TTFDState(); + + static TimeToFullDisplayTracker? _ttfdTracker = TimeToFullDisplayTracker(); static ISentrySpan? get transaction => _transaction; @@ -39,10 +43,11 @@ class TimeToDisplayTracker { required bool enableAutoTransactions, required Duration autoFinishAfter, IFrameCallbackHandler? frameCallbackHandler, - TimeToDisplayTransactionHandler? transactionHandler, + TimeToDisplayTransactionHandler? ttdTransactionHandler, + TimeToFullDisplayTracker? ttfdTracker, }) : _hub = hub ?? HubAdapter(), _native = SentryFlutter.native, - _transactionHandler = transactionHandler ?? + _ttdTransactionHandler = ttdTransactionHandler ?? TimeToDisplayTransactionHandler( hub: hub, enableAutoTransactions: enableAutoTransactions, @@ -75,13 +80,13 @@ class TimeToDisplayTracker { final name = routeName ?? SentryNavigatorObserver.currentRouteName; if (appStartInfo == null || name == null) return; - final transaction = await _transactionHandler.startTransaction( + final transaction = await _ttdTransactionHandler.startTransaction( name, arguments, startTimestamp: appStartInfo.start); if (transaction == null) return; _transaction = transaction; - final ttidSpan = _transactionHandler.createSpan(transaction, + final ttidSpan = _ttdTransactionHandler.createSpan(transaction, TimeToDisplayType.timeToInitialDisplay, name, appStartInfo.start); if (_options?.enableTimeToFullDisplayTracing == true) { @@ -94,17 +99,22 @@ class TimeToDisplayTracker { }); } - void _setAppStartMeasurement(ISentrySpan transaction, AppStartInfo appStartInfo) { - transaction.setMeasurement(appStartInfo.measurement.name, appStartInfo.measurement.value, unit: appStartInfo.measurement.unit); + void _setAppStartMeasurement( + ISentrySpan transaction, AppStartInfo appStartInfo) { + transaction.setMeasurement( + appStartInfo.measurement.name, appStartInfo.measurement.value, + unit: appStartInfo.measurement.unit); - final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay(Duration(milliseconds: appStartInfo.measurement.value.toInt())); - transaction.setMeasurement(ttidMeasurement.name, ttidMeasurement.value, unit: ttidMeasurement.unit); + final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( + Duration(milliseconds: appStartInfo.measurement.value.toInt())); + transaction.setMeasurement(ttidMeasurement.name, ttidMeasurement.value, + unit: ttidMeasurement.unit); } // Handles measuring navigation for regular routes void _handleRegularRouteMeasurement( String? routeName, Object? arguments, DateTime startTimestamp) async { - final transaction = await _transactionHandler + final transaction = await _ttdTransactionHandler .startTransaction(routeName, arguments, startTimestamp: startTimestamp); if (transaction == null || routeName == null) return; @@ -113,22 +123,22 @@ class TimeToDisplayTracker { await _trackTTID(transaction, startTimestamp, routeName); if (_options?.enableTimeToFullDisplayTracing == true) { - _initializeTTFD(transaction, startTimestamp, routeName); + _ttfdTracker?.initializeTTFD(transaction, startTimestamp, routeName); } } - Future _trackTTID(ISentrySpan transaction, - DateTime startTimestamp, String routeName) async { + Future _trackTTID(ISentrySpan transaction, DateTime startTimestamp, + String routeName) async { final endTimestamp = await _ttidEndTimeTracker.determineEndTime(); _ttidEndTimestamp = endTimestamp; - final ttidSpan = _transactionHandler.createSpan(transaction, + final ttidSpan = _ttdTransactionHandler.createSpan(transaction, TimeToDisplayType.timeToInitialDisplay, routeName, startTimestamp); return ttidSpan.finish(endTimestamp: endTimestamp); } void _initializeTTFD( ISentrySpan transaction, DateTime startTimestamp, String routeName) { - _ttfdSpan = _transactionHandler.createSpan(transaction, + _ttfdSpan = _ttdTransactionHandler.createSpan(transaction, TimeToDisplayType.timeToFullDisplay, routeName, startTimestamp); _ttfdTimer = Timer(ttfdAutoFinishAfter, handleTimeToFullDisplayTimeout); } @@ -137,27 +147,40 @@ class TimeToDisplayTracker { final ttfdSpan = _ttfdSpan; final endTimestamp = _ttidEndTimestamp ?? DateTime.now(); final startTimestamp = _startTimestamp; - if (ttfdSpan == null || ttfdSpan.finished == true || startTimestamp == null) return; + if (ttfdSpan == null || ttfdSpan.finished == true || startTimestamp == null) { + return; + } _setTTFDMeasurement(startTimestamp, endTimestamp); - ttfdSpan.finish(status: SpanStatus.deadlineExceeded(), endTimestamp: endTimestamp); + ttfdSpan.finish( + status: SpanStatus.deadlineExceeded(), endTimestamp: endTimestamp); } @internal - static void reportFullyDisplayed() { - _ttfdTimer?.cancel(); - final endTimestamp = DateTime.now(); - final startTimestamp = _startTimestamp; - final ttfdSpan = _ttfdSpan; - if (ttfdSpan == null || ttfdSpan.finished == true || startTimestamp == null) return; - - _setTTFDMeasurement(startTimestamp, endTimestamp); - ttfdSpan.finish(endTimestamp: endTimestamp); + static Future reportFullyDisplayed() async { + return _ttfdTracker?.reportFullyDisplayed(); } - static void _setTTFDMeasurement(DateTime startTimestamp, DateTime endTimestamp) { + static void _setTTFDMeasurement( + DateTime startTimestamp, DateTime endTimestamp) { final duration = endTimestamp.difference(startTimestamp); final measurement = SentryFlutterMeasurement.timeToFullDisplay(duration); - _transaction?.setMeasurement(measurement.name, measurement.value, unit: measurement.unit); + _transaction?.setMeasurement(measurement.name, measurement.value, + unit: measurement.unit); + } +} + +class TTFDState { + DateTime? startTimestamp; + DateTime? ttfdEndTimestamp; + ISentrySpan? ttfdSpan; + Timer? ttfdTimer; + + void reset() { + startTimestamp = null; + ttfdEndTimestamp = null; + ttfdSpan = null; + ttfdTimer?.cancel(); + ttfdTimer = null; } } diff --git a/flutter/lib/src/navigation/time_to_full_display_tracker.dart b/flutter/lib/src/navigation/time_to_full_display_tracker.dart new file mode 100644 index 0000000000..c7a5ce9d0e --- /dev/null +++ b/flutter/lib/src/navigation/time_to_full_display_tracker.dart @@ -0,0 +1,76 @@ +import 'dart:async'; + +import '../../sentry_flutter.dart'; +import '../sentry_flutter_measurement.dart'; + +class TimeToFullDisplayTracker { + static final TimeToFullDisplayTracker _singleton = + TimeToFullDisplayTracker._internal(); + + factory TimeToFullDisplayTracker() { + return _singleton; + } + + TimeToFullDisplayTracker._internal(); + + DateTime? _startTimestamp; + DateTime? _ttfdEndTimestamp; + ISentrySpan? _ttfdSpan; + Timer? _ttfdTimer; + ISentrySpan? _transaction; + Duration ttfdAutoFinishAfter = const Duration(seconds: 30); + + Future reportFullyDisplayed() async { + _ttfdTimer?.cancel(); + final endTimestamp = DateTime.now(); + final startTimestamp = _startTimestamp; + final ttfdSpan = _ttfdSpan; + if (ttfdSpan == null || + ttfdSpan.finished == true || + startTimestamp == null) { + return; + } + + _setTTFDMeasurement(startTimestamp, endTimestamp); + await ttfdSpan.finish(endTimestamp: endTimestamp); + } + + void initializeTTFD( + ISentrySpan transaction, DateTime startTimestamp, String routeName) { + _startTimestamp = startTimestamp; + _transaction = transaction; + _ttfdSpan = transaction.startChild( + SentrySpanOperations.uiTimeToFullDisplay, + description: '$routeName full display', + startTimestamp: startTimestamp); + _ttfdTimer = + Timer(ttfdAutoFinishAfter, handleTimeToFullDisplayTimeout); + } + + void setTTFDEndTimestamp(DateTime ttfdEndTimestamp) { + _ttfdEndTimestamp = ttfdEndTimestamp; + } + + void handleTimeToFullDisplayTimeout() { + final ttfdSpan = _ttfdSpan; + final endTimestamp = _ttfdEndTimestamp ?? DateTime.now(); + final startTimestamp = _startTimestamp; + if (ttfdSpan == null || + ttfdSpan.finished == true || + startTimestamp == null) { + return; + } + + _setTTFDMeasurement(startTimestamp, endTimestamp); + ttfdSpan.finish( + status: SpanStatus.deadlineExceeded(), endTimestamp: endTimestamp); + } + + void _setTTFDMeasurement(DateTime startTimestamp, DateTime endTimestamp) { + final duration = endTimestamp.difference(startTimestamp); + final measurement = SentryFlutterMeasurement.timeToFullDisplay(duration); + _transaction?.setMeasurement(measurement.name, measurement.value, + unit: measurement.unit); + } + +} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 4a61bd06f3..a3cfd0b2cc 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -231,8 +231,8 @@ mixin SentryFlutter { } /// Reports the time it took for the screen to be fully displayed. - static void reportFullyDisplayed() { - TimeToDisplayTracker.reportFullyDisplayed(); + static void reportFullyDisplayed() async { + await TimeToDisplayTracker.reportFullyDisplayed(); } @internal From b01c5d18ee6aec02ebdc12c449242924b9a4fa0b Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 21 Feb 2024 15:16:44 +0100 Subject: [PATCH 31/47] Refactor and fix tests --- .../native_app_start_event_processor.dart | 9 +- .../app_start/app_start_tracker.dart | 48 +++++ .../native_app_start_integration.dart | 54 +----- .../src/navigation/sentry_display_widget.dart | 4 +- .../navigation/sentry_navigator_observer.dart | 22 ++- .../navigation/time_to_display_tracker.dart | 167 ++++-------------- .../time_to_display_transaction_handler.dart | 22 --- .../time_to_full_display_tracker.dart | 28 ++- .../time_to_initial_display_tracker.dart | 77 +++++++- flutter/lib/src/sentry_flutter.dart | 25 ++- .../native_app_start_integration_test.dart | 4 +- .../navigation/fake_app_start_tracker.dart | 24 --- .../time_to_display_tracker_test.dart | 83 +++++---- .../test/sentry_navigator_observer_test.dart | 5 +- 14 files changed, 255 insertions(+), 317 deletions(-) create mode 100644 flutter/lib/src/integrations/app_start/app_start_tracker.dart delete mode 100644 flutter/test/navigation/fake_app_start_tracker.dart diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index 60f48906d2..bade96048b 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; +import '../integrations/app_start/app_start_tracker.dart'; import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; @@ -11,18 +12,18 @@ class NativeAppStartEventProcessor implements EventProcessor { /// We filter out App starts more than 60s static const _maxAppStartMillis = 60000; - final IAppStartTracker? _appStartTracker; + final AppStartTracker? _appStartTracker; NativeAppStartEventProcessor({ - IAppStartTracker? appStartTracker, + AppStartTracker? appStartTracker, }) : _appStartTracker = appStartTracker ?? AppStartTracker(); bool didAddAppStartMeasurement = false; @override Future apply(SentryEvent event, {Hint? hint}) async { - final measurement = _appStartTracker?.appStartInfo?.measurement; - // TODO: only do this once per app start + final appStartInfo = await _appStartTracker?.getAppStartInfo(); + final measurement = appStartInfo?.measurement; if (!didAddAppStartMeasurement && measurement != null && measurement.value.toInt() <= _maxAppStartMillis && diff --git a/flutter/lib/src/integrations/app_start/app_start_tracker.dart b/flutter/lib/src/integrations/app_start/app_start_tracker.dart new file mode 100644 index 0000000000..b2d1a4deee --- /dev/null +++ b/flutter/lib/src/integrations/app_start/app_start_tracker.dart @@ -0,0 +1,48 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../../../sentry_flutter.dart'; + +@internal +class AppStartInfo { + final DateTime start; + final DateTime end; + final SentryMeasurement measurement; + + AppStartInfo(this.start, this.end, this.measurement); +} + +@internal +class AppStartTracker { + static final AppStartTracker _instance = AppStartTracker._internal(); + Completer _appStartCompleter = Completer(); + + factory AppStartTracker() => _instance; + + AppStartInfo? _appStartInfo; + + AppStartTracker._internal(); + + void setAppStartInfo(AppStartInfo? appStartInfo) { + _appStartInfo = appStartInfo; + if (!_appStartCompleter.isCompleted) { + + // Complete the completer with the app start info when it becomes available + _appStartCompleter.complete(appStartInfo); + } else { + // If setAppStartInfo is called again, reset the completer with new app start info + _appStartCompleter = Completer(); + _appStartCompleter.complete(appStartInfo); + } + } + + Future getAppStartInfo() { + // If the app start info is already set, return it immediately + if (_appStartInfo != null) { + return Future.value(_appStartInfo); + } + // Otherwise, return the future that will complete when the app start info is set + return _appStartCompleter.future; + } +} diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 0151a602b3..49b38f6ca9 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -6,17 +6,18 @@ import '../../sentry_flutter.dart'; import '../sentry_flutter_options.dart'; import '../native/sentry_native.dart'; import '../event_processor/native_app_start_event_processor.dart'; +import 'app_start/app_start_tracker.dart'; /// Integration which handles communication with native frameworks in order to /// enrich [SentryTransaction] objects with app start data for mobile vitals. class NativeAppStartIntegration extends Integration { NativeAppStartIntegration(this._native, this._schedulerBindingProvider, - {IAppStartTracker? appStartTracker}) + {AppStartTracker? appStartTracker}) : _appStartTracker = appStartTracker ?? AppStartTracker(); final SentryNative _native; final SchedulerBindingProvider _schedulerBindingProvider; - final IAppStartTracker? _appStartTracker; + final AppStartTracker? _appStartTracker; @override void call(Hub hub, SentryFlutterOptions options) { @@ -74,52 +75,3 @@ class NativeAppStartIntegration extends Integration { /// Used to provide scheduler binding at call time. typedef SchedulerBindingProvider = SchedulerBinding? Function(); - -@internal -class AppStartInfo { - final DateTime start; - final DateTime end; - final SentryMeasurement measurement; - - AppStartInfo(this.start, this.end, this.measurement); -} - -abstract class IAppStartTracker { - AppStartInfo? get appStartInfo; - - void setAppStartInfo(AppStartInfo? appStartInfo); - - void onAppStartComplete(Function(AppStartInfo?) callback); -} - -@internal -class AppStartTracker extends IAppStartTracker { - static final AppStartTracker _instance = AppStartTracker._internal(); - - factory AppStartTracker() => _instance; - - AppStartInfo? _appStartInfo; - - @override - AppStartInfo? get appStartInfo => _appStartInfo; - Function(AppStartInfo?)? _callback; - - AppStartTracker._internal(); - - @override - void setAppStartInfo(AppStartInfo? appStartInfo) { - _appStartInfo = appStartInfo; - _notifyObserver(); - } - - // TODO: replace this with a future - @override - void onAppStartComplete(Function(AppStartInfo?) callback) { - _callback = callback; - _callback?.call(_appStartInfo); - } - - void _notifyObserver() { - _callback?.call(_appStartInfo); - } -} diff --git a/flutter/lib/src/navigation/sentry_display_widget.dart b/flutter/lib/src/navigation/sentry_display_widget.dart index 24a27d31be..bb6fd8808a 100644 --- a/flutter/lib/src/navigation/sentry_display_widget.dart +++ b/flutter/lib/src/navigation/sentry_display_widget.dart @@ -17,10 +17,10 @@ class _SentryDisplayWidgetState extends State { void initState() { super.initState(); // TODO: add via dependency injection - TTIDEndTimeTracker().markAsManual(); + TimeToInitialDisplayTracker().markAsManual(); WidgetsBinding.instance.addPostFrameCallback((_) { - TTIDEndTimeTracker().completeTracking(); + TimeToInitialDisplayTracker().completeTracking(); }); } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 9a4354a305..949f5d85b5 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; +import 'time_to_display_transaction_handler.dart'; import '../../sentry_flutter.dart'; import '../event_processor/flutter_enricher_event_processor.dart'; @@ -80,11 +80,16 @@ class SentryNavigatorObserver extends RouteObserver> { // ignore: invalid_use_of_internal_member _hub.options.sdk.addIntegration('UINavigationTracing'); } + final enableTimeToFullDisplayTracing = + (_hub.options as SentryFlutterOptions).enableTimeToFullDisplayTracing; _timeToDisplayTracker = timeToDisplayTracker ?? TimeToDisplayTracker( - hub: _hub, - enableAutoTransactions: enableAutoTransactions, - autoFinishAfter: autoFinishAfter, + enableTimeToFullDisplayTracing: enableTimeToFullDisplayTracing, + ttdTransactionHandler: TimeToDisplayTransactionHandler( + hub: _hub, + enableAutoTransactions: enableAutoTransactions, + autoFinishAfter: autoFinishAfter, + ), ); } @@ -92,7 +97,12 @@ class SentryNavigatorObserver extends RouteObserver> { final bool _setRouteNameAsTransaction; final RouteNameExtractor? _routeNameExtractor; final AdditionalInfoExtractor? _additionalInfoProvider; - late final TimeToDisplayTracker _timeToDisplayTracker; + + static TimeToDisplayTracker? _timeToDisplayTracker; + + @internal + static TimeToDisplayTracker? get timeToDisplayTracker => + _timeToDisplayTracker; static String? _currentRouteName; @@ -196,7 +206,7 @@ class SentryNavigatorObserver extends RouteObserver> { _currentRouteName = routeName; final arguments = route?.settings.arguments; - await _timeToDisplayTracker.startMeasurement(routeName, arguments); + await _timeToDisplayTracker?.startTracking(routeName, arguments); } } diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index 622acf1dae..87f81f56d5 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -5,6 +5,7 @@ import 'package:sentry_flutter/src/navigation/time_to_full_display_tracker.dart' import '../../sentry_flutter.dart'; import '../frame_callback_handler.dart'; +import '../integrations/app_start/app_start_tracker.dart'; import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; import '../sentry_flutter_measurement.dart'; @@ -13,58 +14,34 @@ import 'time_to_initial_display_tracker.dart'; @internal class TimeToDisplayTracker { - final Hub _hub; final SentryNative? _native; + final AppStartTracker _appStartTracker; final TimeToDisplayTransactionHandler _ttdTransactionHandler; - final TTIDEndTimeTracker _ttidEndTimeTracker; - - // We need to keep these static to be able to access them from reportFullyDisplayed - static DateTime? _startTimestamp; - static DateTime? _ttidEndTimestamp; - static ISentrySpan? _ttfdSpan; - static Timer? _ttfdTimer; - static ISentrySpan? _transaction; - static TTFDState ttfdState = TTFDState(); - - static TimeToFullDisplayTracker? _ttfdTracker = TimeToFullDisplayTracker(); - - static ISentrySpan? get transaction => _transaction; - - @visibleForTesting - Duration ttfdAutoFinishAfter = Duration(seconds: 30); - - SentryFlutterOptions? get _options => _hub.options is SentryFlutterOptions - // ignore: invalid_use_of_internal_member - ? _hub.options as SentryFlutterOptions - : null; + final TimeToInitialDisplayTracker _ttidTracker; + final bool _enableTimeToFullDisplayTracing; + final TimeToFullDisplayTracker _ttfdTracker; TimeToDisplayTracker({ - required Hub? hub, - required bool enableAutoTransactions, - required Duration autoFinishAfter, - IFrameCallbackHandler? frameCallbackHandler, - TimeToDisplayTransactionHandler? ttdTransactionHandler, + required bool enableTimeToFullDisplayTracing, + required TimeToDisplayTransactionHandler ttdTransactionHandler, + AppStartTracker? appStartTracker, + TimeToInitialDisplayTracker? ttidTracker, TimeToFullDisplayTracker? ttfdTracker, - }) : _hub = hub ?? HubAdapter(), - _native = SentryFlutter.native, - _ttdTransactionHandler = ttdTransactionHandler ?? - TimeToDisplayTransactionHandler( - hub: hub, - enableAutoTransactions: enableAutoTransactions, - autoFinishAfter: autoFinishAfter, - ), - _ttidEndTimeTracker = TTIDEndTimeTracker(); - - Future startMeasurement(String? routeName, Object? arguments) async { + }) : _native = SentryFlutter.native, + _enableTimeToFullDisplayTracing = enableTimeToFullDisplayTracing, + _ttdTransactionHandler = ttdTransactionHandler, + _appStartTracker = appStartTracker ?? AppStartTracker(), + _ttfdTracker = ttfdTracker ?? TimeToFullDisplayTracker(), + _ttidTracker = ttidTracker ?? TimeToInitialDisplayTracker(); + + Future startTracking(String? routeName, Object? arguments) async { final startTimestamp = DateTime.now(); - _startTimestamp = startTimestamp; - final isRootScreen = routeName == '/'; final didFetchAppStart = _native?.didFetchAppStart; if (isRootScreen && didFetchAppStart == false) { - _handleAppStartMeasurement(routeName, arguments); + return _trackAppStartTTD(routeName, arguments); } else { - _handleRegularRouteMeasurement(routeName, arguments, startTimestamp); + return _trackRegularRouteTTD(routeName, arguments, startTimestamp); } } @@ -75,112 +52,42 @@ class TimeToDisplayTracker { /// - Finishes the TTID span immediately with the app start end timestamp /// /// We start and immediately finish the TTID span since we cannot mutate the history of spans. - void _handleAppStartMeasurement(String? routeName, Object? arguments) { - AppStartTracker().onAppStartComplete((appStartInfo) async { - final name = routeName ?? SentryNavigatorObserver.currentRouteName; - if (appStartInfo == null || name == null) return; + Future _trackAppStartTTD(String? routeName, Object? arguments) async { + final appStartInfo = await _appStartTracker.getAppStartInfo(); + final name = routeName ?? SentryNavigatorObserver.currentRouteName; - final transaction = await _ttdTransactionHandler.startTransaction( - name, arguments, - startTimestamp: appStartInfo.start); - if (transaction == null) return; - _transaction = transaction; + if (appStartInfo == null || name == null) return; - final ttidSpan = _ttdTransactionHandler.createSpan(transaction, - TimeToDisplayType.timeToInitialDisplay, name, appStartInfo.start); + final transaction = await _ttdTransactionHandler.startTransaction( + name, arguments, + startTimestamp: appStartInfo.start); + if (transaction == null) return; - if (_options?.enableTimeToFullDisplayTracing == true) { - _initializeTTFD(transaction, appStartInfo.start, name); - } + await _ttidTracker.trackAppStart(transaction, appStartInfo, name); - _setAppStartMeasurement(ttidSpan, appStartInfo); - - await ttidSpan.finish(endTimestamp: appStartInfo.end); - }); - } - - void _setAppStartMeasurement( - ISentrySpan transaction, AppStartInfo appStartInfo) { - transaction.setMeasurement( - appStartInfo.measurement.name, appStartInfo.measurement.value, - unit: appStartInfo.measurement.unit); - - final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( - Duration(milliseconds: appStartInfo.measurement.value.toInt())); - transaction.setMeasurement(ttidMeasurement.name, ttidMeasurement.value, - unit: ttidMeasurement.unit); + if (_enableTimeToFullDisplayTracing) { + _ttfdTracker.startTracking(transaction, appStartInfo.start, name); + } } // Handles measuring navigation for regular routes - void _handleRegularRouteMeasurement( + Future _trackRegularRouteTTD( String? routeName, Object? arguments, DateTime startTimestamp) async { final transaction = await _ttdTransactionHandler .startTransaction(routeName, arguments, startTimestamp: startTimestamp); if (transaction == null || routeName == null) return; - _transaction = transaction; - await _trackTTID(transaction, startTimestamp, routeName); - if (_options?.enableTimeToFullDisplayTracing == true) { - _ttfdTracker?.initializeTTFD(transaction, startTimestamp, routeName); - } - } - - Future _trackTTID(ISentrySpan transaction, DateTime startTimestamp, - String routeName) async { - final endTimestamp = await _ttidEndTimeTracker.determineEndTime(); - _ttidEndTimestamp = endTimestamp; - final ttidSpan = _ttdTransactionHandler.createSpan(transaction, - TimeToDisplayType.timeToInitialDisplay, routeName, startTimestamp); - return ttidSpan.finish(endTimestamp: endTimestamp); - } + await _ttidTracker.trackRegularRoute(transaction, startTimestamp, routeName); - void _initializeTTFD( - ISentrySpan transaction, DateTime startTimestamp, String routeName) { - _ttfdSpan = _ttdTransactionHandler.createSpan(transaction, - TimeToDisplayType.timeToFullDisplay, routeName, startTimestamp); - _ttfdTimer = Timer(ttfdAutoFinishAfter, handleTimeToFullDisplayTimeout); - } - - void handleTimeToFullDisplayTimeout() { - final ttfdSpan = _ttfdSpan; - final endTimestamp = _ttidEndTimestamp ?? DateTime.now(); - final startTimestamp = _startTimestamp; - if (ttfdSpan == null || ttfdSpan.finished == true || startTimestamp == null) { - return; + if (_enableTimeToFullDisplayTracing) { + _ttfdTracker.startTracking(transaction, startTimestamp, routeName); } - - _setTTFDMeasurement(startTimestamp, endTimestamp); - ttfdSpan.finish( - status: SpanStatus.deadlineExceeded(), endTimestamp: endTimestamp); } @internal - static Future reportFullyDisplayed() async { - return _ttfdTracker?.reportFullyDisplayed(); - } - - static void _setTTFDMeasurement( - DateTime startTimestamp, DateTime endTimestamp) { - final duration = endTimestamp.difference(startTimestamp); - final measurement = SentryFlutterMeasurement.timeToFullDisplay(duration); - _transaction?.setMeasurement(measurement.name, measurement.value, - unit: measurement.unit); - } -} - -class TTFDState { - DateTime? startTimestamp; - DateTime? ttfdEndTimestamp; - ISentrySpan? ttfdSpan; - Timer? ttfdTimer; - - void reset() { - startTimestamp = null; - ttfdEndTimestamp = null; - ttfdSpan = null; - ttfdTimer?.cancel(); - ttfdTimer = null; + Future reportFullyDisplayed() async { + return _ttfdTracker.reportFullyDisplayed(); } } diff --git a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart index a3e03d37ad..12a4ef572a 100644 --- a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart +++ b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart @@ -2,8 +2,6 @@ import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../native/sentry_native.dart'; -enum TimeToDisplayType { timeToInitialDisplay, timeToFullDisplay } - @internal class TimeToDisplayTransactionHandler { final Hub? _hub; @@ -83,24 +81,4 @@ class TimeToDisplayTransactionHandler { return transaction; } - - @override - ISentrySpan createSpan(ISentrySpan transaction, TimeToDisplayType type, - String routeName, DateTime startTimestamp) { - String operation; - String description; - switch (type) { - case TimeToDisplayType.timeToInitialDisplay: - operation = SentrySpanOperations.uiTimeToInitialDisplay; - description = '$routeName initial display'; - break; - case TimeToDisplayType.timeToFullDisplay: - operation = SentrySpanOperations.uiTimeToFullDisplay; - description = '$routeName full display'; - break; - } - final span = transaction.startChild(operation, - description: description, startTimestamp: startTimestamp); - return span; - } } diff --git a/flutter/lib/src/navigation/time_to_full_display_tracker.dart b/flutter/lib/src/navigation/time_to_full_display_tracker.dart index c7a5ce9d0e..17722ba985 100644 --- a/flutter/lib/src/navigation/time_to_full_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_full_display_tracker.dart @@ -2,6 +2,7 @@ import 'dart:async'; import '../../sentry_flutter.dart'; import '../sentry_flutter_measurement.dart'; +import 'time_to_initial_display_tracker.dart'; class TimeToFullDisplayTracker { static final TimeToFullDisplayTracker _singleton = @@ -14,7 +15,6 @@ class TimeToFullDisplayTracker { TimeToFullDisplayTracker._internal(); DateTime? _startTimestamp; - DateTime? _ttfdEndTimestamp; ISentrySpan? _ttfdSpan; Timer? _ttfdTimer; ISentrySpan? _transaction; @@ -25,6 +25,7 @@ class TimeToFullDisplayTracker { final endTimestamp = DateTime.now(); final startTimestamp = _startTimestamp; final ttfdSpan = _ttfdSpan; + if (ttfdSpan == null || ttfdSpan.finished == true || startTimestamp == null) { @@ -32,28 +33,21 @@ class TimeToFullDisplayTracker { } _setTTFDMeasurement(startTimestamp, endTimestamp); - await ttfdSpan.finish(endTimestamp: endTimestamp); + return ttfdSpan.finish(endTimestamp: endTimestamp); } - void initializeTTFD( + void startTracking( ISentrySpan transaction, DateTime startTimestamp, String routeName) { _startTimestamp = startTimestamp; _transaction = transaction; - _ttfdSpan = transaction.startChild( - SentrySpanOperations.uiTimeToFullDisplay, - description: '$routeName full display', - startTimestamp: startTimestamp); - _ttfdTimer = - Timer(ttfdAutoFinishAfter, handleTimeToFullDisplayTimeout); - } - - void setTTFDEndTimestamp(DateTime ttfdEndTimestamp) { - _ttfdEndTimestamp = ttfdEndTimestamp; + _ttfdSpan = transaction.startChild(SentrySpanOperations.uiTimeToFullDisplay, + description: '$routeName full display', startTimestamp: startTimestamp); + _ttfdSpan?.origin = SentryTraceOrigins.manualUiTimeToDisplay; + _ttfdTimer = Timer(ttfdAutoFinishAfter, handleTimeToFullDisplayTimeout); } void handleTimeToFullDisplayTimeout() { final ttfdSpan = _ttfdSpan; - final endTimestamp = _ttfdEndTimestamp ?? DateTime.now(); final startTimestamp = _startTimestamp; if (ttfdSpan == null || ttfdSpan.finished == true || @@ -61,6 +55,11 @@ class TimeToFullDisplayTracker { return; } + // If for some reason we can't get the ttid end timestamp + // we'll use the start timestamp + autoFinishTime as a fallback + final endTimestamp = TimeToInitialDisplayTracker().endTimestamp ?? + startTimestamp.add(ttfdAutoFinishAfter); + _setTTFDMeasurement(startTimestamp, endTimestamp); ttfdSpan.finish( status: SpanStatus.deadlineExceeded(), endTimestamp: endTimestamp); @@ -72,5 +71,4 @@ class TimeToFullDisplayTracker { _transaction?.setMeasurement(measurement.name, measurement.value, unit: measurement.unit); } - } diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart index 0cbeb67c98..01c50824ea 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -3,22 +3,82 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; +import '../../sentry_flutter.dart'; +import '../frame_callback_handler.dart'; +import '../integrations/app_start/app_start_tracker.dart'; +import '../integrations/integrations.dart'; +import '../sentry_flutter_measurement.dart'; + @internal -class TTIDEndTimeTracker { - static final TTIDEndTimeTracker _instance = - TTIDEndTimeTracker._internal(); - factory TTIDEndTimeTracker() => _instance; - TTIDEndTimeTracker._internal(); +class TimeToInitialDisplayTracker { + static final TimeToInitialDisplayTracker _instance = + TimeToInitialDisplayTracker._internal(); + factory TimeToInitialDisplayTracker() => _instance; + TimeToInitialDisplayTracker._internal(); + IFrameCallbackHandler frameCallbackHandler = FrameCallbackHandler(); bool _isManual = false; Completer? _trackingCompleter; + DateTime? _endTimestamp; + + /// This endTimestamp is needed in the [TimeToFullDisplayTracker] class + @internal + DateTime? get endTimestamp => _endTimestamp; + + Future trackRegularRoute(ISentrySpan transaction, DateTime startTimestamp, String routeName) async { + final endTimestamp = await determineEndTime(); + if (endTimestamp == null) return; + + final ttidSpan = transaction.startChild( + SentrySpanOperations.uiTimeToInitialDisplay, + description: '$routeName initial display', + startTimestamp: startTimestamp); + + if (_isManual) { + ttidSpan.origin = SentryTraceOrigins.manualUiTimeToDisplay; + } else { + ttidSpan.origin = SentryTraceOrigins.autoUiTimeToDisplay; + } + + final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( + Duration( + milliseconds: + endTimestamp.difference(startTimestamp).inMilliseconds)); + transaction.setMeasurement(ttidMeasurement.name, ttidMeasurement.value, + unit: ttidMeasurement.unit); + return ttidSpan.finish(endTimestamp: endTimestamp); + } + + Future trackAppStart(ISentrySpan transaction, AppStartInfo appStartInfo, String routeName) async { + final ttidSpan = transaction.startChild( + SentrySpanOperations.uiTimeToInitialDisplay, + description: '$routeName initial display', + startTimestamp: appStartInfo.start, + ); + ttidSpan.origin = SentryTraceOrigins.autoUiTimeToDisplay; + + transaction.setMeasurement( + appStartInfo.measurement.name, appStartInfo.measurement.value, + unit: appStartInfo.measurement.unit); + + final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( + Duration(milliseconds: appStartInfo.measurement.value.toInt()), + ); + transaction.setMeasurement(ttidMeasurement.name, ttidMeasurement.value, unit: ttidMeasurement.unit); + + // Since app start measurement is immediate, finish the TTID span with the app start's end timestamp + await ttidSpan.finish(endTimestamp: appStartInfo.end); + + // Store the end timestamp for potential use by TTFD tracking + _endTimestamp = appStartInfo.end; + } Future? determineEndTime() { _trackingCompleter = Completer(); // Schedules a check at the end of the frame to determine if the tracking // should be completed immediately (approximation mode) or deferred (manual mode). - WidgetsBinding.instance.addPostFrameCallback((_) { + frameCallbackHandler.addPostFrameCallback((_) { if (!_isManual) { completeTracking(); } @@ -33,10 +93,11 @@ class TTIDEndTimeTracker { void completeTracking() { if (_trackingCompleter != null && !_trackingCompleter!.isCompleted) { - final endTime = DateTime.now(); + final endTimestamp = DateTime.now(); + _endTimestamp = endTimestamp; // Reset after completion _isManual = false; - _trackingCompleter?.complete(endTime); + _trackingCompleter?.complete(endTimestamp); } } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index a3cfd0b2cc..2c05efbaea 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -34,8 +34,7 @@ typedef FlutterOptionsConfiguration = FutureOr Function( mixin SentryFlutter { static const _channel = MethodChannel('sentry_flutter'); - static Future init( - FlutterOptionsConfiguration optionsConfiguration, { + static Future init(FlutterOptionsConfiguration optionsConfiguration, { AppRunner? appRunner, @internal MethodChannel channel = _channel, @internal PlatformChecker? platformChecker, @@ -83,7 +82,7 @@ mixin SentryFlutter { await _initDefaultValues(flutterOptions, channel); await Sentry.init( - (options) => optionsConfiguration(options as SentryFlutterOptions), + (options) => optionsConfiguration(options as SentryFlutterOptions), appRunner: appRunner, // ignore: invalid_use_of_internal_member options: flutterOptions, @@ -99,10 +98,8 @@ mixin SentryFlutter { } } - static Future _initDefaultValues( - SentryFlutterOptions options, - MethodChannel channel, - ) async { + static Future _initDefaultValues(SentryFlutterOptions options, + MethodChannel channel,) async { options.addEventProcessor(FlutterExceptionEventProcessor()); // Not all platforms have a native integration. @@ -126,11 +123,9 @@ mixin SentryFlutter { /// Install default integrations /// https://medium.com/flutter-community/error-handling-in-flutter-98fce88a34f0 - static List _createDefaultIntegrations( - MethodChannel channel, - SentryFlutterOptions options, - bool isOnErrorSupported, - ) { + static List _createDefaultIntegrations(MethodChannel channel, + SentryFlutterOptions options, + bool isOnErrorSupported,) { final integrations = []; final platformChecker = options.platformChecker; final platform = platformChecker.platform; @@ -190,7 +185,7 @@ mixin SentryFlutter { if (_native != null) { integrations.add(NativeAppStartIntegration( _native!, - () { + () { try { /// Flutter >= 2.12 throws if SchedulerBinding.instance isn't initialized. return SchedulerBinding.instance; @@ -231,8 +226,8 @@ mixin SentryFlutter { } /// Reports the time it took for the screen to be fully displayed. - static void reportFullyDisplayed() async { - await TimeToDisplayTracker.reportFullyDisplayed(); + static Future reportFullyDisplayed() async { + return SentryNavigatorObserver.timeToDisplayTracker?.reportFullyDisplayed(); } @internal diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index aa187280d7..54efcd419c 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -3,13 +3,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/integrations/app_start/app_start_tracker.dart'; import 'package:sentry_flutter/src/integrations/native_app_start_integration.dart'; import 'package:sentry_flutter/src/native/sentry_native.dart'; import 'package:sentry/src/sentry_tracer.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; -import '../navigation/fake_app_start_tracker.dart'; void main() { group('$NativeAppStartIntegration', () { @@ -156,7 +156,7 @@ class Fixture { final hub = MockHub(); final options = SentryFlutterOptions(dsn: fakeDsn); final binding = MockNativeChannel(); - final appStartTracker = FakeAppStartTracker(); + final appStartTracker = AppStartTracker(); late final native = SentryNative(options, binding); Fixture() { diff --git a/flutter/test/navigation/fake_app_start_tracker.dart b/flutter/test/navigation/fake_app_start_tracker.dart deleted file mode 100644 index 31eb5fb5cb..0000000000 --- a/flutter/test/navigation/fake_app_start_tracker.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:sentry_flutter/src/integrations/native_app_start_integration.dart'; - -class FakeAppStartTracker extends IAppStartTracker { - static final FakeAppStartTracker _instance = FakeAppStartTracker._internal(); - - factory FakeAppStartTracker() => _instance; - - AppStartInfo? _appStartInfo; - - FakeAppStartTracker._internal(); - - @override - AppStartInfo? get appStartInfo => _appStartInfo; - - @override - void onAppStartComplete(void Function(AppStartInfo?) callback) { - callback(_appStartInfo); - } - - @override - void setAppStartInfo(AppStartInfo? appStartInfo) { - _appStartInfo = appStartInfo; - } -} diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index c289fbadd7..bb465b4e0c 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -1,8 +1,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/integrations/app_start/app_start_tracker.dart'; import 'package:sentry_flutter/src/integrations/integrations.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry_flutter/src/navigation/time_to_display_transaction_handler.dart'; +import 'package:sentry_flutter/src/navigation/time_to_full_display_tracker.dart'; +import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; import '../fake_frame_callback_handler.dart'; import '../mocks.dart'; @@ -25,7 +29,7 @@ void main() { SentryFlutter.native = TestMockSentryNative(); final sut = fixture.getSut(); - sut.startMeasurement('/', null); + sut.startTracking('/', null); AppStartTracker().setAppStartInfo(AppStartInfo( DateTime.fromMillisecondsSinceEpoch(0), @@ -49,7 +53,7 @@ void main() { test('startMeasurement finishes ttid span', () async { final sut = fixture.getSut(); - sut.startMeasurement('Current Route', null); + sut.startTracking('Current Route', null); final transaction = fixture.hub.getSpan() as SentryTracer; await Future.delayed(const Duration(milliseconds: 2000)); @@ -66,7 +70,7 @@ void main() { test('finishes ttid span after reporting with manual api', () async { final sut = fixture.getSut(); - sut.startMeasurement('Current Route', null); + sut.startTracking('Current Route', null); final transaction = fixture.hub.getSpan() as SentryTracer; @@ -97,7 +101,7 @@ void main() { test('startMeasurement creates ttfd and ttid span', () async { final sut = fixture.getSut(); - sut.startMeasurement('Current Route', null); + await sut.startTracking('Current Route', null); final transaction = fixture.hub.getSpan() as SentryTracer; await Future.delayed(const Duration(milliseconds: 100)); @@ -115,15 +119,16 @@ void main() { SentryFlutter.native = TestMockSentryNative(); final sut = fixture.getSut(); - sut.startMeasurement('/', null); - - AppStartTracker().setAppStartInfo(AppStartInfo( - DateTime.fromMillisecondsSinceEpoch(0), - DateTime.fromMillisecondsSinceEpoch(10), - SentryMeasurement('', 10, - unit: DurationSentryMeasurementUnit.milliSecond))); + // Simulate app start info being fetched async + Future.delayed(const Duration(milliseconds: 500), () async { + AppStartTracker().setAppStartInfo(AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0), + DateTime.fromMillisecondsSinceEpoch(10), + SentryMeasurement('', 10, + unit: DurationSentryMeasurementUnit.milliSecond))); + }); - await Future.delayed(const Duration(milliseconds: 100)); + await sut.startTracking('/', null); final transaction = fixture.hub.getSpan() as SentryTracer; @@ -134,9 +139,7 @@ void main() { .first; expect(ttfdSpan, isNotNull); - SentryFlutter.reportFullyDisplayed(); - - await Future.delayed(const Duration(milliseconds: 100)); + await fixture.getSut().reportFullyDisplayed(); expect(ttfdSpan.finished, isTrue); }); @@ -146,10 +149,9 @@ void main() { test('finishes ttfd span after calling reportFullyDisplayed', () async { final sut = fixture.getSut(); - sut.startMeasurement('Current Route', null); + await sut.startTracking('Current Route', null); final transaction = fixture.hub.getSpan() as SentryTracer; - await Future.delayed(const Duration(milliseconds: 100)); final ttfdSpan = transaction.children .where((element) => @@ -158,8 +160,7 @@ void main() { .first; expect(ttfdSpan, isNotNull); - SentryFlutter.reportFullyDisplayed(); - await Future.delayed(const Duration(milliseconds: 100)); + await fixture.getSut().reportFullyDisplayed(); expect(ttfdSpan.finished, isTrue); }); @@ -168,30 +169,28 @@ void main() { 'not using reportFullyDisplayed finishes ttfd span after timeout with deadline exceeded and ttid matching end time', () async { final sut = fixture.getSut(); - sut.ttfdAutoFinishAfter = const Duration(seconds: 3); + fixture.ttfdTracker.ttfdAutoFinishAfter = const Duration(seconds: 1); - sut.startMeasurement('Current Route', null); + await sut.startTracking('Current Route', null); final transaction = fixture.hub.getSpan() as SentryTracer; - await Future.delayed(const Duration(milliseconds: 100)); - - final ttfdSpan = transaction.children + final ttidSpan = transaction.children .where((element) => element.context.operation == - SentrySpanOperations.uiTimeToFullDisplay) + SentrySpanOperations.uiTimeToInitialDisplay) .first; - expect(ttfdSpan, isNotNull); + expect(ttidSpan, isNotNull); - final ttidSpan = transaction.children + final ttfdSpan = transaction.children .where((element) => element.context.operation == - SentrySpanOperations.uiTimeToInitialDisplay) + SentrySpanOperations.uiTimeToFullDisplay) .first; expect(ttfdSpan, isNotNull); - await Future.delayed( - sut.ttfdAutoFinishAfter + const Duration(milliseconds: 100)); + await Future.delayed(fixture.ttfdTracker.ttfdAutoFinishAfter + + const Duration(milliseconds: 100)); expect(ttfdSpan.finished, isTrue); expect(ttfdSpan.status, SpanStatus.deadlineExceeded()); @@ -203,7 +202,7 @@ void main() { test('screen load tracking creates ui.load transaction', () async { final sut = fixture.getSut(); - sut.startMeasurement('Current Route', null); + await sut.startTracking('Current Route', null); final transaction = fixture.hub.getSpan(); expect(transaction, isNotNull); @@ -220,12 +219,24 @@ class Fixture { late final hub = Hub(options); - TimeToDisplayTracker getSut({bool enableTimeToFullDisplayTracing = false}) { + final ttidTracker = TimeToInitialDisplayTracker() + ..frameCallbackHandler = FakeFrameCallbackHandler(); + + final ttfdTracker = TimeToFullDisplayTracker(); + + TimeToDisplayTracker getSut() { + final enableTimeToFullDisplayTracing = + options.enableTimeToFullDisplayTracing; + return TimeToDisplayTracker( - hub: hub, - enableAutoTransactions: true, - autoFinishAfter: const Duration(seconds: 3), - frameCallbackHandler: frameCallbackHandler, + enableTimeToFullDisplayTracing: enableTimeToFullDisplayTracing, + ttdTransactionHandler: TimeToDisplayTransactionHandler( + hub: hub, + enableAutoTransactions: true, + autoFinishAfter: const Duration(seconds: 30), + ), + ttidTracker: ttidTracker, + ttfdTracker: ttfdTracker, ); } } diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index a79749a766..85f1ce06c6 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -7,8 +7,8 @@ import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/native/sentry_native.dart'; import 'package:sentry/src/sentry_tracer.dart'; -import 'package:sentry_flutter/src/navigation/display_strategy_evaluator.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; +import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; import 'mocks.dart'; import 'mocks.mocks.dart'; @@ -98,7 +98,8 @@ void main() { sut.didPush(currentRoute, null); await Future.delayed(Duration(milliseconds: 50)); - TTIDModeEvaluator().reportManual('Current Route'); + TimeToInitialDisplayTracker().markAsManual(); + TimeToInitialDisplayTracker().completeTracking(); // Get ref to created transaction // ignore: invalid_use_of_internal_member From 96b9f83af69d5f8b53a7d2105c22681923a3696b Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 22 Feb 2024 13:54:42 +0100 Subject: [PATCH 32/47] Add tests --- flutter/example/lib/main.dart | 6 +- flutter/lib/src/frame_callback_handler.dart | 2 +- .../src/navigation/sentry_display_widget.dart | 11 +- .../navigation/sentry_navigator_observer.dart | 9 +- .../navigation/time_to_display_tracker.dart | 6 +- .../time_to_display_transaction_handler.dart | 4 - .../time_to_initial_display_tracker.dart | 6 +- flutter/test/fake_frame_callback_handler.dart | 7 +- .../sentry_display_widget_test.dart | 115 ++++++++++++++++++ .../time_to_display_tracker_test.dart | 21 ++-- .../test/sentry_navigator_observer_test.dart | 11 +- 11 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 flutter/test/navigation/sentry_display_widget_test.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 2c68410354..d5053bc411 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -78,7 +78,7 @@ Future setupSentry(AppRunner appRunner, String dsn, // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; - options.enableTimeToFullDisplayTracing = true; + // options.enableTimeToFullDisplayTracing = true; options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; @@ -738,8 +738,8 @@ void navigateToAutoCloseScreen(BuildContext context) { context, MaterialPageRoute( settings: const RouteSettings(name: 'AutoCloseScreen'), - builder: (context) => const SentryDisplayWidget( - child: AutoCloseScreen(), + builder: (context) => SentryDisplayWidget( + child: const AutoCloseScreen(), )), ); } diff --git a/flutter/lib/src/frame_callback_handler.dart b/flutter/lib/src/frame_callback_handler.dart index 40c19779db..3a22d3fb1f 100644 --- a/flutter/lib/src/frame_callback_handler.dart +++ b/flutter/lib/src/frame_callback_handler.dart @@ -5,7 +5,7 @@ abstract class IFrameCallbackHandler { void addPostFrameCallback(FrameCallback callback, {String debugLabel}); } -class FrameCallbackHandler implements IFrameCallbackHandler { +class DefaultFrameCallbackHandler implements IFrameCallbackHandler { @override void addPostFrameCallback(FrameCallback callback, {String debugLabel = 'callback'}) { diff --git a/flutter/lib/src/navigation/sentry_display_widget.dart b/flutter/lib/src/navigation/sentry_display_widget.dart index bb6fd8808a..63338fa82f 100644 --- a/flutter/lib/src/navigation/sentry_display_widget.dart +++ b/flutter/lib/src/navigation/sentry_display_widget.dart @@ -2,11 +2,17 @@ import 'package:flutter/cupertino.dart'; import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; import '../../sentry_flutter.dart'; +import '../frame_callback_handler.dart'; class SentryDisplayWidget extends StatefulWidget { final Widget child; + final IFrameCallbackHandler _frameCallbackHandler; - const SentryDisplayWidget({super.key, required this.child}); + SentryDisplayWidget({ + super.key, + required this.child, + IFrameCallbackHandler? frameCallbackHandler, + }) : _frameCallbackHandler = frameCallbackHandler ?? DefaultFrameCallbackHandler(); @override _SentryDisplayWidgetState createState() => _SentryDisplayWidgetState(); @@ -16,10 +22,9 @@ class _SentryDisplayWidgetState extends State { @override void initState() { super.initState(); - // TODO: add via dependency injection TimeToInitialDisplayTracker().markAsManual(); - WidgetsBinding.instance.addPostFrameCallback((_) { + widget._frameCallbackHandler.addPostFrameCallback((_) { TimeToInitialDisplayTracker().completeTracking(); }); } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 949f5d85b5..2638db0b61 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -86,7 +86,7 @@ class SentryNavigatorObserver extends RouteObserver> { TimeToDisplayTracker( enableTimeToFullDisplayTracing: enableTimeToFullDisplayTracing, ttdTransactionHandler: TimeToDisplayTransactionHandler( - hub: _hub, + hub: hub, enableAutoTransactions: enableAutoTransactions, autoFinishAfter: autoFinishAfter, ), @@ -109,6 +109,11 @@ class SentryNavigatorObserver extends RouteObserver> { @internal static String? get currentRouteName => _currentRouteName; + Completer? _completedDisplayTracking; + + @internal + Completer? get completedDisplayTracking => _completedDisplayTracking; + @override void didPush(Route route, Route? previousRoute) { super.didPush(route, previousRoute); @@ -202,11 +207,13 @@ class SentryNavigatorObserver extends RouteObserver> { } Future _startTimeToDisplayTracking(Route? route) async { + _completedDisplayTracking = Completer(); final routeName = _getRouteName(route); _currentRouteName = routeName; final arguments = route?.settings.arguments; await _timeToDisplayTracker?.startTracking(routeName, arguments); + completedDisplayTracking?.complete(); } } diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index 87f81f56d5..88bbeb916b 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -36,7 +36,10 @@ class TimeToDisplayTracker { Future startTracking(String? routeName, Object? arguments) async { final startTimestamp = DateTime.now(); - final isRootScreen = routeName == '/'; + if (routeName == '/') { + routeName = 'root ("/")'; + } + final isRootScreen = routeName == 'root ("/")'; final didFetchAppStart = _native?.didFetchAppStart; if (isRootScreen && didFetchAppStart == false) { return _trackAppStartTTD(routeName, arguments); @@ -78,7 +81,6 @@ class TimeToDisplayTracker { if (transaction == null || routeName == null) return; - await _ttidTracker.trackRegularRoute(transaction, startTimestamp, routeName); if (_enableTimeToFullDisplayTracing) { diff --git a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart index 12a4ef572a..c6538b91d0 100644 --- a/flutter/lib/src/navigation/time_to_display_transaction_handler.dart +++ b/flutter/lib/src/navigation/time_to_display_transaction_handler.dart @@ -29,10 +29,6 @@ class TimeToDisplayTransactionHandler { return null; } - if (routeName == '/') { - routeName = 'root ("/")'; - } - final transactionContext = SentryTransactionContext( routeName, SentrySpanOperations.uiLoad, diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart index 01c50824ea..9b089150b8 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ffi'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -16,7 +17,7 @@ class TimeToInitialDisplayTracker { factory TimeToInitialDisplayTracker() => _instance; TimeToInitialDisplayTracker._internal(); - IFrameCallbackHandler frameCallbackHandler = FrameCallbackHandler(); + IFrameCallbackHandler frameCallbackHandler = DefaultFrameCallbackHandler(); bool _isManual = false; Completer? _trackingCompleter; DateTime? _endTimestamp; @@ -40,6 +41,8 @@ class TimeToInitialDisplayTracker { ttidSpan.origin = SentryTraceOrigins.autoUiTimeToDisplay; } + _isManual = false; + final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( Duration( milliseconds: @@ -96,7 +99,6 @@ class TimeToInitialDisplayTracker { final endTimestamp = DateTime.now(); _endTimestamp = endTimestamp; // Reset after completion - _isManual = false; _trackingCompleter?.complete(endTimestamp); } } diff --git a/flutter/test/fake_frame_callback_handler.dart b/flutter/test/fake_frame_callback_handler.dart index e03a40348a..6216f6bea8 100644 --- a/flutter/test/fake_frame_callback_handler.dart +++ b/flutter/test/fake_frame_callback_handler.dart @@ -12,9 +12,8 @@ class FakeFrameCallbackHandler implements IFrameCallbackHandler { @override void addPostFrameCallback(FrameCallback callback, - {String debugLabel = 'callback'}) { - Future.delayed(_finishAfterDuration, () { - callback(Duration.zero); - }); + {String debugLabel = 'callback'}) async { + await Future.delayed(_finishAfterDuration); + callback(Duration.zero); } } diff --git a/flutter/test/navigation/sentry_display_widget_test.dart b/flutter/test/navigation/sentry_display_widget_test.dart new file mode 100644 index 0000000000..3c66ffc96e --- /dev/null +++ b/flutter/test/navigation/sentry_display_widget_test.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry_flutter/src/integrations/app_start/app_start_tracker.dart'; +import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; +import 'package:sentry_flutter/src/sentry_flutter_measurement.dart'; + +import '../fake_frame_callback_handler.dart'; +import '../mocks.dart'; + +void main() { + PageRoute route(RouteSettings? settings) => PageRouteBuilder( + pageBuilder: (_, __, ___) => Container(), + settings: settings, + ); + + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + testWidgets('SentryDisplayWidget reports manual ttid span after didPush', (WidgetTester tester) async { + final currentRoute = route(RouteSettings(name: 'Current Route')); + + await tester.runAsync(() async { + fixture.navigatorObserver.didPush(currentRoute, null); + await tester.pumpWidget(fixture.getSut()); + await fixture.navigatorObserver.completedDisplayTracking?.future; + }); + + final tracer = fixture.hub.getSpan() as SentryTracer; + final spans = tracer.children.where((element) => + element.context.operation == + SentrySpanOperations.uiTimeToInitialDisplay); + + expect(spans, hasLength(1)); + final ttidSpan = spans.first; + expect(ttidSpan.context.operation, + SentrySpanOperations.uiTimeToInitialDisplay); + expect(ttidSpan.finished, isTrue); + expect(ttidSpan.context.description, 'Current Route initial display'); + expect(ttidSpan.origin, SentryTraceOrigins.manualUiTimeToDisplay); + expect(tracer.measurements, hasLength(1)); + final measurement = tracer.measurements['time_to_initial_display']; + expect(measurement, isNotNull); + expect(measurement?.unit, DurationSentryMeasurementUnit.milliSecond); + }); + + testWidgets('SentryDisplayWidget is ignored for app starts', (WidgetTester tester) async { + final currentRoute = route(RouteSettings(name: '/')); + + await tester.runAsync(() async { + fixture.navigatorObserver.didPush(currentRoute, null); + await tester.pumpWidget(fixture.getSut()); + AppStartTracker().setAppStartInfo(AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0).toUtc(), + DateTime.fromMillisecondsSinceEpoch(10).toUtc(), + SentryFlutterMeasurement.timeToInitialDisplay( + Duration(milliseconds: 10), + ), + )); + await fixture.navigatorObserver.completedDisplayTracking?.future; + }); + + final tracer = fixture.hub.getSpan() as SentryTracer; + final spans = tracer.children.where((element) => + element.context.operation == + SentrySpanOperations.uiTimeToInitialDisplay); + + expect(spans, hasLength(1)); + + final ttidSpan = spans.first; + expect(ttidSpan.context.operation, + SentrySpanOperations.uiTimeToInitialDisplay); + expect(ttidSpan.finished, isTrue); + expect(ttidSpan.context.description, 'root ("/") initial display'); + expect(ttidSpan.origin, SentryTraceOrigins.autoUiTimeToDisplay); + + expect(ttidSpan.startTimestamp, DateTime.fromMillisecondsSinceEpoch(0).toUtc()); + expect(ttidSpan.endTimestamp, DateTime.fromMillisecondsSinceEpoch(10).toUtc()); + + expect(tracer.measurements, hasLength(1)); + final measurement = tracer.measurements['time_to_initial_display']; + expect(measurement, isNotNull); + expect(measurement?.value, 10); + expect(measurement?.unit, DurationSentryMeasurementUnit.milliSecond); + }); +} + +class Fixture { + final Hub hub = Hub(SentryFlutterOptions(dsn: fakeDsn)..tracesSampleRate = 1.0); + late final SentryNavigatorObserver navigatorObserver; + late final TimeToInitialDisplayTracker timeToInitialDisplayTracker; + + Fixture() { + SentryFlutter.native = TestMockSentryNative(); + navigatorObserver = SentryNavigatorObserver(hub: hub); + timeToInitialDisplayTracker = TimeToInitialDisplayTracker(); + TimeToInitialDisplayTracker().frameCallbackHandler = + FakeFrameCallbackHandler(); + } + + MaterialApp getSut() { + return MaterialApp( + home: SentryDisplayWidget( + frameCallbackHandler: FakeFrameCallbackHandler( + finishAfterDuration: Duration(milliseconds: 50), + ), + child: Text('my text'), + ), + ); + } +} diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index bb465b4e0c..9e1e48bdbf 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -29,12 +29,15 @@ void main() { SentryFlutter.native = TestMockSentryNative(); final sut = fixture.getSut(); - sut.startTracking('/', null); + Future.delayed(const Duration(milliseconds: 500), () async { + AppStartTracker().setAppStartInfo(AppStartInfo( + DateTime.fromMillisecondsSinceEpoch(0), + DateTime.fromMillisecondsSinceEpoch(10), + SentryMeasurement('', 10, + unit: DurationSentryMeasurementUnit.milliSecond))); + }); - AppStartTracker().setAppStartInfo(AppStartInfo( - DateTime.fromMillisecondsSinceEpoch(0), - DateTime.fromMillisecondsSinceEpoch(10), - SentryMeasurement('', 0))); + await sut.startTracking('/', null); await Future.delayed(const Duration(milliseconds: 100)); @@ -70,14 +73,16 @@ void main() { test('finishes ttid span after reporting with manual api', () async { final sut = fixture.getSut(); - sut.startTracking('Current Route', null); + Future.delayed(const Duration(milliseconds: 100), () { + fixture.ttidTracker.markAsManual(); + fixture.ttidTracker.completeTracking(); + }); + await sut.startTracking('Current Route', null); final transaction = fixture.hub.getSpan() as SentryTracer; await Future.delayed(const Duration(milliseconds: 100)); - // SentryFlutter.reportInitiallyDisplayed(routeName: 'Current Route'); - final ttidSpan = transaction.children .where((element) => element.context.operation == diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index 85f1ce06c6..f484081d48 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -8,6 +8,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/native/sentry_native.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; +import 'package:sentry_flutter/src/navigation/time_to_display_transaction_handler.dart'; import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; import 'mocks.dart'; @@ -858,11 +859,13 @@ class Fixture { AdditionalInfoExtractor? additionalInfoProvider, }) { final timeToDisplayTracker = TimeToDisplayTracker( - hub: hub, - enableAutoTransactions: enableAutoTransactions, - autoFinishAfter: autoFinishAfter, + enableTimeToFullDisplayTracing: true, + ttdTransactionHandler: TimeToDisplayTransactionHandler( + hub: hub, + enableAutoTransactions: true, + autoFinishAfter: const Duration(seconds: 30), + ), ); - return SentryNavigatorObserver( hub: hub, enableAutoTransactions: enableAutoTransactions, From f5353863484da96c5d9a9d97d844bd109e0c25e9 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 22 Feb 2024 14:53:30 +0100 Subject: [PATCH 33/47] add ttid tracker tests --- .../time_to_initial_display_tracker.dart | 1 + flutter/test/mocks.dart | 2 + flutter/test/mocks.mocks.dart | 57 ++++-- .../time_to_initial_display_test.dart | 175 ++++++++++++++++++ 4 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 flutter/test/navigation/time_to_initial_display_test.dart diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart index 9b089150b8..3c84f6f14c 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -41,6 +41,7 @@ class TimeToInitialDisplayTracker { ttidSpan.origin = SentryTraceOrigins.autoUiTimeToDisplay; } + // Reset after completion _isManual = false; final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( diff --git a/flutter/test/mocks.dart b/flutter/test/mocks.dart index b2e01788c1..bebd2903e2 100644 --- a/flutter/test/mocks.dart +++ b/flutter/test/mocks.dart @@ -9,6 +9,7 @@ import 'package:sentry/src/sentry_tracer.dart'; import 'package:meta/meta.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/frame_callback_handler.dart'; import 'package:sentry_flutter/src/renderer/renderer.dart'; import 'package:sentry_flutter/src/native/sentry_native.dart'; import 'package:sentry_flutter/src/native/sentry_native_binding.dart'; @@ -47,6 +48,7 @@ ISentrySpan startTransactionShim( SentryTracer, SentryTransaction, MethodChannel, + IFrameCallbackHandler, ], customMocks: [ MockSpec(fallbackGenerators: {#startTransaction: startTransactionShim}) ]) diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index e2807ef405..768e3a32b3 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -1,26 +1,31 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in sentry_flutter/test/mocks.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i7; +import 'package:flutter/scheduler.dart' as _i13; import 'package:flutter/src/services/binary_messenger.dart' as _i6; import 'package:flutter/src/services/message_codec.dart' as _i5; -import 'package:flutter/src/services/platform_channel.dart' as _i10; +import 'package:flutter/src/services/platform_channel.dart' as _i11; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i9; import 'package:sentry/sentry.dart' as _i2; -import 'package:sentry/src/profiling.dart' as _i9; +import 'package:sentry/src/profiling.dart' as _i10; import 'package:sentry/src/protocol.dart' as _i3; import 'package:sentry/src/sentry_envelope.dart' as _i8; import 'package:sentry/src/sentry_tracer.dart' as _i4; +import 'package:sentry_flutter/src/frame_callback_handler.dart' as _i12; -import 'mocks.dart' as _i11; +import 'mocks.dart' as _i14; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -192,7 +197,10 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: '', + returnValue: _i9.dummyValue( + this, + Invocation.getter(#name), + ), ) as String); @override @@ -223,7 +231,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - set profiler(_i9.SentryProfiler? _profiler) => super.noSuchMethod( + set profiler(_i10.SentryProfiler? _profiler) => super.noSuchMethod( Invocation.setter( #profiler, _profiler, @@ -232,7 +240,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - set profileInfo(_i9.SentryProfileInfo? _profileInfo) => super.noSuchMethod( + set profileInfo(_i10.SentryProfileInfo? _profileInfo) => super.noSuchMethod( Invocation.setter( #profileInfo, _profileInfo, @@ -714,7 +722,7 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { /// A class which mocks [MethodChannel]. /// /// See the documentation for Mockito's code generation for more information. -class MockMethodChannel extends _i1.Mock implements _i10.MethodChannel { +class MockMethodChannel extends _i1.Mock implements _i11.MethodChannel { MockMethodChannel() { _i1.throwOnMissingStub(this); } @@ -722,7 +730,10 @@ class MockMethodChannel extends _i1.Mock implements _i10.MethodChannel { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: '', + returnValue: _i9.dummyValue( + this, + Invocation.getter(#name), + ), ) as String); @override @@ -803,6 +814,30 @@ class MockMethodChannel extends _i1.Mock implements _i10.MethodChannel { ); } +/// A class which mocks [IFrameCallbackHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockIFrameCallbackHandler extends _i1.Mock + implements _i12.IFrameCallbackHandler { + MockIFrameCallbackHandler() { + _i1.throwOnMissingStub(this); + } + + @override + void addPostFrameCallback( + _i13.FrameCallback? callback, { + String? debugLabel, + }) => + super.noSuchMethod( + Invocation.method( + #addPostFrameCallback, + [callback], + {#debugLabel: debugLabel}, + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [Hub]. /// /// See the documentation for Mockito's code generation for more information. @@ -845,7 +880,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.Scope); @override - set profilerFactory(_i9.SentryProfilerFactory? value) => super.noSuchMethod( + set profilerFactory(_i10.SentryProfilerFactory? value) => super.noSuchMethod( Invocation.setter( #profilerFactory, value, @@ -1050,7 +1085,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #customSamplingContext: customSamplingContext, }, ), - returnValue: _i11.startTransactionShim( + returnValue: _i14.startTransactionShim( name, operation, description: description, diff --git a/flutter/test/navigation/time_to_initial_display_test.dart b/flutter/test/navigation/time_to_initial_display_test.dart new file mode 100644 index 0000000000..6d5530c5b5 --- /dev/null +++ b/flutter/test/navigation/time_to_initial_display_test.dart @@ -0,0 +1,175 @@ +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/integrations/app_start/app_start_tracker.dart'; +import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; + +import '../fake_frame_callback_handler.dart'; +import '../mocks.dart'; +import 'package:sentry/src/sentry_tracer.dart'; + +void main() { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('app start', () { + test('tracking creates and finishes ttid span with correct measurements', () async { + final sut = fixture.getSut(); + final transaction = fixture.hub.startTransaction('fake', 'fake') + as SentryTracer; + final startTimestamp = DateTime.now(); + final appStartInfo = AppStartInfo( + startTimestamp, + startTimestamp.add(Duration(milliseconds: 10)), + SentryMeasurement.coldAppStart(Duration(milliseconds: 10))); + + await sut.trackAppStart(transaction, appStartInfo, 'route ("/")'); + + final children = transaction.children; + expect(children, hasLength(1)); + + final ttidSpan = children.first; + expect(ttidSpan.context.operation, SentrySpanOperations.uiTimeToInitialDisplay); + expect(ttidSpan.finished, isTrue); + expect(ttidSpan.context.description, 'route ("/") initial display'); + expect(ttidSpan.origin, SentryTraceOrigins.autoUiTimeToDisplay); + + final ttidMeasurement = transaction.measurements['time_to_initial_display']; + expect(ttidMeasurement, isNotNull); + expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); + expect(ttidMeasurement?.value, 10); + + final appStartMeasurement = transaction.measurements['app_start_cold']; + expect(appStartMeasurement, isNotNull); + expect(appStartMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); + expect(appStartMeasurement?.value, 10); + }); + }); + + group('regular route', () { + test('approximation tracking creates and finishes ttid span with correct measurements', () async { + final sut = fixture.getSut(); + final transaction = fixture.hub.startTransaction('fake', 'fake') + as SentryTracer; + final startTimestamp = DateTime.now(); + + await sut.trackRegularRoute(transaction, startTimestamp, 'regular route'); + + final children = transaction.children; + expect(children, hasLength(1)); + + final ttidSpan = children.first; + expect(ttidSpan.context.operation, SentrySpanOperations.uiTimeToInitialDisplay); + expect(ttidSpan.finished, isTrue); + expect(ttidSpan.context.description, 'regular route initial display'); + expect(ttidSpan.origin, SentryTraceOrigins.autoUiTimeToDisplay); + + final ttidMeasurement = transaction.measurements['time_to_initial_display']; + expect(ttidMeasurement, isNotNull); + expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); + expect(ttidMeasurement?.value, greaterThan(fixture.finishAfterDuration.inMilliseconds)); + expect(ttidMeasurement?.value, lessThan(fixture.finishAfterDuration.inMilliseconds + 10)); + }); + + test('manual tracking creates and finishes ttid span with correct measurements', () async { + final sut = fixture.getSut(); + final transaction = fixture.hub.startTransaction('fake', 'fake') + as SentryTracer; + final startTimestamp = DateTime.now(); + + sut.markAsManual(); + Future.delayed(fixture.finishAfterDuration, () { + sut.completeTracking(); + }); + await sut.trackRegularRoute(transaction, startTimestamp, 'regular route'); + + final children = transaction.children; + expect(children, hasLength(1)); + + final ttidSpan = children.first; + expect(ttidSpan.context.operation, SentrySpanOperations.uiTimeToInitialDisplay); + expect(ttidSpan.finished, isTrue); + expect(ttidSpan.context.description, 'regular route initial display'); + expect(ttidSpan.origin, SentryTraceOrigins.manualUiTimeToDisplay); + + final ttidMeasurement = transaction.measurements['time_to_initial_display']; + expect(ttidMeasurement, isNotNull); + expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); + expect(ttidMeasurement?.value, greaterThan(fixture.finishAfterDuration.inMilliseconds)); + expect(ttidMeasurement?.value, lessThan(fixture.finishAfterDuration.inMilliseconds + 10)); + }); + }); + + group('determineEndtime', () { + test('can complete automatically in approximation mode', () async { + final sut = fixture.getSut(); + + final futureEndTime = sut.determineEndTime(); + + expect(futureEndTime, completes); + }); + + test('prevents automatic completion in manual mode', () async { + final sut = fixture.getSut(); + + sut.markAsManual(); + final futureEndTime = sut.determineEndTime(); + + expect(futureEndTime, doesNotComplete); + }); + + test('can complete manually in manual mode', () async { + final sut = fixture.getSut(); + + sut.markAsManual(); + final futureEndTime = sut.determineEndTime(); + + sut.completeTracking(); + expect(futureEndTime, completes); + }); + + test('returns the correct approximation end time', () async { + final startTime = DateTime.now(); + final sut = fixture.getSut(); + + final futureEndTime = sut.determineEndTime(); + + final endTime = await futureEndTime; + expect(endTime?.difference(startTime).inSeconds, + fixture.finishAfterDuration.inSeconds); + }); + + test('returns the correct manual end time', () async { + final startTime = DateTime.now(); + final sut = fixture.getSut(); + + sut.markAsManual(); + final futureEndTime = sut.determineEndTime(); + + Future.delayed(fixture.finishAfterDuration, () { + sut.completeTracking(); + }); + + final endTime = await futureEndTime; + expect(endTime?.difference(startTime).inSeconds, + fixture.finishAfterDuration.inSeconds); + }); + }); +} + +class Fixture { + final hub = Hub(SentryFlutterOptions(dsn: fakeDsn)..tracesSampleRate = 1.0); + final finishAfterDuration = Duration(milliseconds: 100); + late final fakeFrameCallbackHandler = + FakeFrameCallbackHandler(finishAfterDuration: finishAfterDuration); + + TimeToInitialDisplayTracker getSut() { + final sut = TimeToInitialDisplayTracker(); + sut.frameCallbackHandler = fakeFrameCallbackHandler; + return sut; + } +} From 7a748b34d9d427c958cbdf6fd5c4be7ff2764c22 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 23 Feb 2024 13:27:15 +0100 Subject: [PATCH 34/47] Update tests --- dart/lib/src/sentry_span_operations.dart | 2 +- dart/lib/src/sentry_tracer.dart | 3 +- flutter/lib/src/frame_callback_handler.dart | 4 +- .../app_start/app_start_tracker.dart | 1 - .../src/navigation/sentry_display_widget.dart | 7 +- .../navigation/time_to_display_tracker.dart | 8 +- .../time_to_full_display_tracker.dart | 65 ++++++++----- .../time_to_initial_display_tracker.dart | 45 ++++++--- flutter/lib/src/sentry_flutter.dart | 21 +++-- flutter/test/fake_frame_callback_handler.dart | 2 +- flutter/test/mocks.dart | 2 - flutter/test/mocks.mocks.dart | 57 +++--------- .../sentry_display_widget_test.dart | 21 +++-- .../time_to_display_tracker_test.dart | 22 ++--- .../time_to_full_display_tracker_test.dart | 82 +++++++++++++++++ ...time_to_initial_display_tracker_test.dart} | 92 ++++++++++--------- 16 files changed, 267 insertions(+), 167 deletions(-) create mode 100644 flutter/test/navigation/time_to_full_display_tracker_test.dart rename flutter/test/navigation/{time_to_initial_display_test.dart => time_to_initial_display_tracker_test.dart} (64%) diff --git a/dart/lib/src/sentry_span_operations.dart b/dart/lib/src/sentry_span_operations.dart index eba53aae5b..27b1d22496 100644 --- a/dart/lib/src/sentry_span_operations.dart +++ b/dart/lib/src/sentry_span_operations.dart @@ -5,4 +5,4 @@ class SentrySpanOperations { static const String uiLoad = 'ui.load'; static const String uiTimeToInitialDisplay = 'ui.load.initial_display'; static const String uiTimeToFullDisplay = 'ui.load.full_display'; -} \ No newline at end of file +} diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index 7a0e8835ae..b6bc4e3d7a 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -117,7 +117,8 @@ class SentryTracer extends ISentrySpan { for (var child in children) { final childEndTimestamp = child.endTimestamp; if (childEndTimestamp != null) { - if (latestEndTime == null || childEndTimestamp.isAfter(latestEndTime)) { + if (latestEndTime == null || + childEndTimestamp.isAfter(latestEndTime)) { latestEndTime = child.endTimestamp; } } diff --git a/flutter/lib/src/frame_callback_handler.dart b/flutter/lib/src/frame_callback_handler.dart index 3a22d3fb1f..8ec5e4c68b 100644 --- a/flutter/lib/src/frame_callback_handler.dart +++ b/flutter/lib/src/frame_callback_handler.dart @@ -1,11 +1,11 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; -abstract class IFrameCallbackHandler { +abstract class FrameCallbackHandler { void addPostFrameCallback(FrameCallback callback, {String debugLabel}); } -class DefaultFrameCallbackHandler implements IFrameCallbackHandler { +class DefaultFrameCallbackHandler implements FrameCallbackHandler { @override void addPostFrameCallback(FrameCallback callback, {String debugLabel = 'callback'}) { diff --git a/flutter/lib/src/integrations/app_start/app_start_tracker.dart b/flutter/lib/src/integrations/app_start/app_start_tracker.dart index b2d1a4deee..87807b382a 100644 --- a/flutter/lib/src/integrations/app_start/app_start_tracker.dart +++ b/flutter/lib/src/integrations/app_start/app_start_tracker.dart @@ -27,7 +27,6 @@ class AppStartTracker { void setAppStartInfo(AppStartInfo? appStartInfo) { _appStartInfo = appStartInfo; if (!_appStartCompleter.isCompleted) { - // Complete the completer with the app start info when it becomes available _appStartCompleter.complete(appStartInfo); } else { diff --git a/flutter/lib/src/navigation/sentry_display_widget.dart b/flutter/lib/src/navigation/sentry_display_widget.dart index 63338fa82f..5793f52e63 100644 --- a/flutter/lib/src/navigation/sentry_display_widget.dart +++ b/flutter/lib/src/navigation/sentry_display_widget.dart @@ -6,13 +6,14 @@ import '../frame_callback_handler.dart'; class SentryDisplayWidget extends StatefulWidget { final Widget child; - final IFrameCallbackHandler _frameCallbackHandler; + final FrameCallbackHandler _frameCallbackHandler; SentryDisplayWidget({ super.key, required this.child, - IFrameCallbackHandler? frameCallbackHandler, - }) : _frameCallbackHandler = frameCallbackHandler ?? DefaultFrameCallbackHandler(); + FrameCallbackHandler? frameCallbackHandler, + }) : _frameCallbackHandler = + frameCallbackHandler ?? DefaultFrameCallbackHandler(); @override _SentryDisplayWidgetState createState() => _SentryDisplayWidgetState(); diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index 88bbeb916b..bb03c3c143 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -61,9 +61,8 @@ class TimeToDisplayTracker { if (appStartInfo == null || name == null) return; - final transaction = await _ttdTransactionHandler.startTransaction( - name, arguments, - startTimestamp: appStartInfo.start); + final transaction = await _ttdTransactionHandler + .startTransaction(name, arguments, startTimestamp: appStartInfo.start); if (transaction == null) return; await _ttidTracker.trackAppStart(transaction, appStartInfo, name); @@ -81,7 +80,8 @@ class TimeToDisplayTracker { if (transaction == null || routeName == null) return; - await _ttidTracker.trackRegularRoute(transaction, startTimestamp, routeName); + await _ttidTracker.trackRegularRoute( + transaction, startTimestamp, routeName); if (_enableTimeToFullDisplayTracing) { _ttfdTracker.startTracking(transaction, startTimestamp, routeName); diff --git a/flutter/lib/src/navigation/time_to_full_display_tracker.dart b/flutter/lib/src/navigation/time_to_full_display_tracker.dart index 17722ba985..b595839cce 100644 --- a/flutter/lib/src/navigation/time_to_full_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_full_display_tracker.dart @@ -4,12 +4,28 @@ import '../../sentry_flutter.dart'; import '../sentry_flutter_measurement.dart'; import 'time_to_initial_display_tracker.dart'; +abstract class EndTimestampProvider { + DateTime? get endTimestamp; +} + +class TTIDEndTimestampProvider implements EndTimestampProvider { + @override + DateTime? get endTimestamp => TimeToInitialDisplayTracker().endTimestamp; +} + class TimeToFullDisplayTracker { - static final TimeToFullDisplayTracker _singleton = + static final TimeToFullDisplayTracker _instance = TimeToFullDisplayTracker._internal(); - factory TimeToFullDisplayTracker() { - return _singleton; + factory TimeToFullDisplayTracker( + {EndTimestampProvider? endTimestampProvider, Duration? autoFinishAfter}) { + if (autoFinishAfter != null) { + _instance._autoFinishAfter = autoFinishAfter; + } + if (endTimestampProvider != null) { + _instance._endTimestampProvider = endTimestampProvider; + } + return _instance; } TimeToFullDisplayTracker._internal(); @@ -18,23 +34,8 @@ class TimeToFullDisplayTracker { ISentrySpan? _ttfdSpan; Timer? _ttfdTimer; ISentrySpan? _transaction; - Duration ttfdAutoFinishAfter = const Duration(seconds: 30); - - Future reportFullyDisplayed() async { - _ttfdTimer?.cancel(); - final endTimestamp = DateTime.now(); - final startTimestamp = _startTimestamp; - final ttfdSpan = _ttfdSpan; - - if (ttfdSpan == null || - ttfdSpan.finished == true || - startTimestamp == null) { - return; - } - - _setTTFDMeasurement(startTimestamp, endTimestamp); - return ttfdSpan.finish(endTimestamp: endTimestamp); - } + Duration _autoFinishAfter = const Duration(seconds: 30); + EndTimestampProvider _endTimestampProvider = TTIDEndTimestampProvider(); void startTracking( ISentrySpan transaction, DateTime startTimestamp, String routeName) { @@ -43,7 +44,7 @@ class TimeToFullDisplayTracker { _ttfdSpan = transaction.startChild(SentrySpanOperations.uiTimeToFullDisplay, description: '$routeName full display', startTimestamp: startTimestamp); _ttfdSpan?.origin = SentryTraceOrigins.manualUiTimeToDisplay; - _ttfdTimer = Timer(ttfdAutoFinishAfter, handleTimeToFullDisplayTimeout); + _ttfdTimer = Timer(_autoFinishAfter, handleTimeToFullDisplayTimeout); } void handleTimeToFullDisplayTimeout() { @@ -56,15 +57,31 @@ class TimeToFullDisplayTracker { } // If for some reason we can't get the ttid end timestamp - // we'll use the start timestamp + autoFinishTime as a fallback - final endTimestamp = TimeToInitialDisplayTracker().endTimestamp ?? - startTimestamp.add(ttfdAutoFinishAfter); + // we'll use the start timestamp + autoFinishAfter as a fallback + final endTimestamp = _endTimestampProvider.endTimestamp ?? + startTimestamp.add(_autoFinishAfter); _setTTFDMeasurement(startTimestamp, endTimestamp); ttfdSpan.finish( status: SpanStatus.deadlineExceeded(), endTimestamp: endTimestamp); } + Future reportFullyDisplayed() async { + _ttfdTimer?.cancel(); + final endTimestamp = DateTime.now(); + final startTimestamp = _startTimestamp; + final ttfdSpan = _ttfdSpan; + + if (ttfdSpan == null || + ttfdSpan.finished == true || + startTimestamp == null) { + return; + } + + _setTTFDMeasurement(startTimestamp, endTimestamp); + return ttfdSpan.finish(endTimestamp: endTimestamp); + } + void _setTTFDMeasurement(DateTime startTimestamp, DateTime endTimestamp) { final duration = endTimestamp.difference(startTimestamp); final measurement = SentryFlutterMeasurement.timeToFullDisplay(duration); diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart index 3c84f6f14c..0a6b40ca23 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -14,10 +14,18 @@ import '../sentry_flutter_measurement.dart'; class TimeToInitialDisplayTracker { static final TimeToInitialDisplayTracker _instance = TimeToInitialDisplayTracker._internal(); - factory TimeToInitialDisplayTracker() => _instance; + + factory TimeToInitialDisplayTracker( + {FrameCallbackHandler? frameCallbackHandler}) { + if (frameCallbackHandler != null) { + _instance._frameCallbackHandler = frameCallbackHandler; + } + return _instance; + } + TimeToInitialDisplayTracker._internal(); - IFrameCallbackHandler frameCallbackHandler = DefaultFrameCallbackHandler(); + FrameCallbackHandler _frameCallbackHandler = DefaultFrameCallbackHandler(); bool _isManual = false; Completer? _trackingCompleter; DateTime? _endTimestamp; @@ -26,7 +34,8 @@ class TimeToInitialDisplayTracker { @internal DateTime? get endTimestamp => _endTimestamp; - Future trackRegularRoute(ISentrySpan transaction, DateTime startTimestamp, String routeName) async { + Future trackRegularRoute(ISentrySpan transaction, + DateTime startTimestamp, String routeName) async { final endTimestamp = await determineEndTime(); if (endTimestamp == null) return; @@ -41,19 +50,20 @@ class TimeToInitialDisplayTracker { ttidSpan.origin = SentryTraceOrigins.autoUiTimeToDisplay; } - // Reset after completion - _isManual = false; - final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( Duration( milliseconds: - endTimestamp.difference(startTimestamp).inMilliseconds)); + endTimestamp.difference(startTimestamp).inMilliseconds)); transaction.setMeasurement(ttidMeasurement.name, ttidMeasurement.value, unit: ttidMeasurement.unit); - return ttidSpan.finish(endTimestamp: endTimestamp); + await ttidSpan.finish(endTimestamp: endTimestamp); + + // We can clear the state after creating and finishing the ttid span has finished + clear(); } - Future trackAppStart(ISentrySpan transaction, AppStartInfo appStartInfo, String routeName) async { + Future trackAppStart(ISentrySpan transaction, AppStartInfo appStartInfo, + String routeName) async { final ttidSpan = transaction.startChild( SentrySpanOperations.uiTimeToInitialDisplay, description: '$routeName initial display', @@ -68,7 +78,8 @@ class TimeToInitialDisplayTracker { final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( Duration(milliseconds: appStartInfo.measurement.value.toInt()), ); - transaction.setMeasurement(ttidMeasurement.name, ttidMeasurement.value, unit: ttidMeasurement.unit); + transaction.setMeasurement(ttidMeasurement.name, ttidMeasurement.value, + unit: ttidMeasurement.unit); // Since app start measurement is immediate, finish the TTID span with the app start's end timestamp await ttidSpan.finish(endTimestamp: appStartInfo.end); @@ -80,9 +91,14 @@ class TimeToInitialDisplayTracker { Future? determineEndTime() { _trackingCompleter = Completer(); + // If we already know it's manual we can return the future immediately + if (_isManual) { + return _trackingCompleter?.future; + } + // Schedules a check at the end of the frame to determine if the tracking // should be completed immediately (approximation mode) or deferred (manual mode). - frameCallbackHandler.addPostFrameCallback((_) { + _frameCallbackHandler.addPostFrameCallback((_) { if (!_isManual) { completeTracking(); } @@ -103,4 +119,11 @@ class TimeToInitialDisplayTracker { _trackingCompleter?.complete(endTimestamp); } } + + void clear() { + _isManual = false; + _trackingCompleter = null; + // We can't clear the ttid end time stamp here, because it might be needed + // in the [TimeToFullDisplayTracker] class + } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 2c05efbaea..9c4f777f4a 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -34,7 +34,8 @@ typedef FlutterOptionsConfiguration = FutureOr Function( mixin SentryFlutter { static const _channel = MethodChannel('sentry_flutter'); - static Future init(FlutterOptionsConfiguration optionsConfiguration, { + static Future init( + FlutterOptionsConfiguration optionsConfiguration, { AppRunner? appRunner, @internal MethodChannel channel = _channel, @internal PlatformChecker? platformChecker, @@ -82,7 +83,7 @@ mixin SentryFlutter { await _initDefaultValues(flutterOptions, channel); await Sentry.init( - (options) => optionsConfiguration(options as SentryFlutterOptions), + (options) => optionsConfiguration(options as SentryFlutterOptions), appRunner: appRunner, // ignore: invalid_use_of_internal_member options: flutterOptions, @@ -98,8 +99,10 @@ mixin SentryFlutter { } } - static Future _initDefaultValues(SentryFlutterOptions options, - MethodChannel channel,) async { + static Future _initDefaultValues( + SentryFlutterOptions options, + MethodChannel channel, + ) async { options.addEventProcessor(FlutterExceptionEventProcessor()); // Not all platforms have a native integration. @@ -123,9 +126,11 @@ mixin SentryFlutter { /// Install default integrations /// https://medium.com/flutter-community/error-handling-in-flutter-98fce88a34f0 - static List _createDefaultIntegrations(MethodChannel channel, - SentryFlutterOptions options, - bool isOnErrorSupported,) { + static List _createDefaultIntegrations( + MethodChannel channel, + SentryFlutterOptions options, + bool isOnErrorSupported, + ) { final integrations = []; final platformChecker = options.platformChecker; final platform = platformChecker.platform; @@ -185,7 +190,7 @@ mixin SentryFlutter { if (_native != null) { integrations.add(NativeAppStartIntegration( _native!, - () { + () { try { /// Flutter >= 2.12 throws if SchedulerBinding.instance isn't initialized. return SchedulerBinding.instance; diff --git a/flutter/test/fake_frame_callback_handler.dart b/flutter/test/fake_frame_callback_handler.dart index 6216f6bea8..eac89466f4 100644 --- a/flutter/test/fake_frame_callback_handler.dart +++ b/flutter/test/fake_frame_callback_handler.dart @@ -1,7 +1,7 @@ import 'package:flutter/scheduler.dart'; import 'package:sentry_flutter/src/frame_callback_handler.dart'; -class FakeFrameCallbackHandler implements IFrameCallbackHandler { +class FakeFrameCallbackHandler implements FrameCallbackHandler { FrameCallback? storedCallback; final Duration _finishAfterDuration; diff --git a/flutter/test/mocks.dart b/flutter/test/mocks.dart index bebd2903e2..b2e01788c1 100644 --- a/flutter/test/mocks.dart +++ b/flutter/test/mocks.dart @@ -9,7 +9,6 @@ import 'package:sentry/src/sentry_tracer.dart'; import 'package:meta/meta.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/frame_callback_handler.dart'; import 'package:sentry_flutter/src/renderer/renderer.dart'; import 'package:sentry_flutter/src/native/sentry_native.dart'; import 'package:sentry_flutter/src/native/sentry_native_binding.dart'; @@ -48,7 +47,6 @@ ISentrySpan startTransactionShim( SentryTracer, SentryTransaction, MethodChannel, - IFrameCallbackHandler, ], customMocks: [ MockSpec(fallbackGenerators: {#startTransaction: startTransactionShim}) ]) diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 768e3a32b3..e2807ef405 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -1,31 +1,26 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.2 from annotations // in sentry_flutter/test/mocks.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i7; -import 'package:flutter/scheduler.dart' as _i13; import 'package:flutter/src/services/binary_messenger.dart' as _i6; import 'package:flutter/src/services/message_codec.dart' as _i5; -import 'package:flutter/src/services/platform_channel.dart' as _i11; +import 'package:flutter/src/services/platform_channel.dart' as _i10; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i9; import 'package:sentry/sentry.dart' as _i2; -import 'package:sentry/src/profiling.dart' as _i10; +import 'package:sentry/src/profiling.dart' as _i9; import 'package:sentry/src/protocol.dart' as _i3; import 'package:sentry/src/sentry_envelope.dart' as _i8; import 'package:sentry/src/sentry_tracer.dart' as _i4; -import 'package:sentry_flutter/src/frame_callback_handler.dart' as _i12; -import 'mocks.dart' as _i14; +import 'mocks.dart' as _i11; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -197,10 +192,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: _i9.dummyValue( - this, - Invocation.getter(#name), - ), + returnValue: '', ) as String); @override @@ -231,7 +223,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - set profiler(_i10.SentryProfiler? _profiler) => super.noSuchMethod( + set profiler(_i9.SentryProfiler? _profiler) => super.noSuchMethod( Invocation.setter( #profiler, _profiler, @@ -240,7 +232,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - set profileInfo(_i10.SentryProfileInfo? _profileInfo) => super.noSuchMethod( + set profileInfo(_i9.SentryProfileInfo? _profileInfo) => super.noSuchMethod( Invocation.setter( #profileInfo, _profileInfo, @@ -722,7 +714,7 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { /// A class which mocks [MethodChannel]. /// /// See the documentation for Mockito's code generation for more information. -class MockMethodChannel extends _i1.Mock implements _i11.MethodChannel { +class MockMethodChannel extends _i1.Mock implements _i10.MethodChannel { MockMethodChannel() { _i1.throwOnMissingStub(this); } @@ -730,10 +722,7 @@ class MockMethodChannel extends _i1.Mock implements _i11.MethodChannel { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: _i9.dummyValue( - this, - Invocation.getter(#name), - ), + returnValue: '', ) as String); @override @@ -814,30 +803,6 @@ class MockMethodChannel extends _i1.Mock implements _i11.MethodChannel { ); } -/// A class which mocks [IFrameCallbackHandler]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockIFrameCallbackHandler extends _i1.Mock - implements _i12.IFrameCallbackHandler { - MockIFrameCallbackHandler() { - _i1.throwOnMissingStub(this); - } - - @override - void addPostFrameCallback( - _i13.FrameCallback? callback, { - String? debugLabel, - }) => - super.noSuchMethod( - Invocation.method( - #addPostFrameCallback, - [callback], - {#debugLabel: debugLabel}, - ), - returnValueForMissingStub: null, - ); -} - /// A class which mocks [Hub]. /// /// See the documentation for Mockito's code generation for more information. @@ -880,7 +845,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.Scope); @override - set profilerFactory(_i10.SentryProfilerFactory? value) => super.noSuchMethod( + set profilerFactory(_i9.SentryProfilerFactory? value) => super.noSuchMethod( Invocation.setter( #profilerFactory, value, @@ -1085,7 +1050,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #customSamplingContext: customSamplingContext, }, ), - returnValue: _i14.startTransactionShim( + returnValue: _i11.startTransactionShim( name, operation, description: description, diff --git a/flutter/test/navigation/sentry_display_widget_test.dart b/flutter/test/navigation/sentry_display_widget_test.dart index 3c66ffc96e..62884d4d65 100644 --- a/flutter/test/navigation/sentry_display_widget_test.dart +++ b/flutter/test/navigation/sentry_display_widget_test.dart @@ -21,7 +21,8 @@ void main() { fixture = Fixture(); }); - testWidgets('SentryDisplayWidget reports manual ttid span after didPush', (WidgetTester tester) async { + testWidgets('SentryDisplayWidget reports manual ttid span after didPush', + (WidgetTester tester) async { final currentRoute = route(RouteSettings(name: 'Current Route')); await tester.runAsync(() async { @@ -48,7 +49,8 @@ void main() { expect(measurement?.unit, DurationSentryMeasurementUnit.milliSecond); }); - testWidgets('SentryDisplayWidget is ignored for app starts', (WidgetTester tester) async { + testWidgets('SentryDisplayWidget is ignored for app starts', + (WidgetTester tester) async { final currentRoute = route(RouteSettings(name: '/')); await tester.runAsync(() async { @@ -78,8 +80,10 @@ void main() { expect(ttidSpan.context.description, 'root ("/") initial display'); expect(ttidSpan.origin, SentryTraceOrigins.autoUiTimeToDisplay); - expect(ttidSpan.startTimestamp, DateTime.fromMillisecondsSinceEpoch(0).toUtc()); - expect(ttidSpan.endTimestamp, DateTime.fromMillisecondsSinceEpoch(10).toUtc()); + expect(ttidSpan.startTimestamp, + DateTime.fromMillisecondsSinceEpoch(0).toUtc()); + expect( + ttidSpan.endTimestamp, DateTime.fromMillisecondsSinceEpoch(10).toUtc()); expect(tracer.measurements, hasLength(1)); final measurement = tracer.measurements['time_to_initial_display']; @@ -90,16 +94,17 @@ void main() { } class Fixture { - final Hub hub = Hub(SentryFlutterOptions(dsn: fakeDsn)..tracesSampleRate = 1.0); + final Hub hub = + Hub(SentryFlutterOptions(dsn: fakeDsn)..tracesSampleRate = 1.0); late final SentryNavigatorObserver navigatorObserver; late final TimeToInitialDisplayTracker timeToInitialDisplayTracker; + final fakeFrameCallbackHandler = FakeFrameCallbackHandler(); Fixture() { SentryFlutter.native = TestMockSentryNative(); navigatorObserver = SentryNavigatorObserver(hub: hub); - timeToInitialDisplayTracker = TimeToInitialDisplayTracker(); - TimeToInitialDisplayTracker().frameCallbackHandler = - FakeFrameCallbackHandler(); + timeToInitialDisplayTracker = TimeToInitialDisplayTracker( + frameCallbackHandler: fakeFrameCallbackHandler); } MaterialApp getSut() { diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index 9e1e48bdbf..2ebb00a215 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -19,10 +19,6 @@ void main() { fixture = Fixture(); }); - tearDown(() async { - await Future.delayed(const Duration(milliseconds: 500)); - }); - group('time to initial display', () { group('in root screen app start route', () { test('startMeasurement finishes ttid span', () async { @@ -56,7 +52,7 @@ void main() { test('startMeasurement finishes ttid span', () async { final sut = fixture.getSut(); - sut.startTracking('Current Route', null); + await sut.startTracking('Current Route', null); final transaction = fixture.hub.getSpan() as SentryTracer; await Future.delayed(const Duration(milliseconds: 2000)); @@ -174,7 +170,6 @@ void main() { 'not using reportFullyDisplayed finishes ttfd span after timeout with deadline exceeded and ttid matching end time', () async { final sut = fixture.getSut(); - fixture.ttfdTracker.ttfdAutoFinishAfter = const Duration(seconds: 1); await sut.startTracking('Current Route', null); @@ -194,8 +189,8 @@ void main() { .first; expect(ttfdSpan, isNotNull); - await Future.delayed(fixture.ttfdTracker.ttfdAutoFinishAfter + - const Duration(milliseconds: 100)); + await Future.delayed( + fixture.ttfdAutoFinishAfter + const Duration(milliseconds: 100)); expect(ttfdSpan.finished, isTrue); expect(ttfdSpan.status, SpanStatus.deadlineExceeded()); @@ -220,14 +215,15 @@ class Fixture { ..dsn = fakeDsn ..tracesSampleRate = 1.0; - final frameCallbackHandler = FakeFrameCallbackHandler(); - late final hub = Hub(options); - final ttidTracker = TimeToInitialDisplayTracker() - ..frameCallbackHandler = FakeFrameCallbackHandler(); + final frameCallbackHandler = FakeFrameCallbackHandler(); + late final ttidTracker = + TimeToInitialDisplayTracker(frameCallbackHandler: frameCallbackHandler); - final ttfdTracker = TimeToFullDisplayTracker(); + final ttfdAutoFinishAfter = Duration(milliseconds: 500); + late final ttfdTracker = + TimeToFullDisplayTracker(autoFinishAfter: ttfdAutoFinishAfter); TimeToDisplayTracker getSut() { final enableTimeToFullDisplayTracing = diff --git a/flutter/test/navigation/time_to_full_display_tracker_test.dart b/flutter/test/navigation/time_to_full_display_tracker_test.dart new file mode 100644 index 0000000000..19520de804 --- /dev/null +++ b/flutter/test/navigation/time_to_full_display_tracker_test.dart @@ -0,0 +1,82 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/navigation/time_to_full_display_tracker.dart'; +import 'package:sentry/src/sentry_tracer.dart'; + +import '../mocks.dart'; + +void main() { + late Fixture fixture; + late SentryTracer transaction; + late DateTime startTimestamp; + late EndTimestampProvider endTimestampProvider; + const routeName = 'regular route'; + + setUp(() { + fixture = Fixture(); + transaction = fixture.hub.startTransaction('test_transaction', 'test') + as SentryTracer; + + // start timestamp needs to be after the transaction has started + startTimestamp = DateTime.now().toUtc(); + endTimestampProvider = FakeTTIDEndTimeStampProvider(startTimestamp); + }); + + test('reportFullyDisplayed() marks the TTFD span as finished', () async { + final sut = fixture.getSut(endTimestampProvider); + + sut.startTracking(transaction, startTimestamp, routeName); + await sut.reportFullyDisplayed(); + + final ttfdSpan = transaction.children.first; + expect(transaction.children, hasLength(1)); + expect(ttfdSpan.context.operation, + equals(SentrySpanOperations.uiTimeToFullDisplay)); + expect(ttfdSpan.finished, isTrue); + expect(ttfdSpan.context.description, equals('$routeName full display')); + expect(ttfdSpan.origin, equals(SentryTraceOrigins.manualUiTimeToDisplay)); + }); + + test( + 'TTFD span finishes automatically after timeout with correct status and end time', + () async { + final sut = fixture.getSut(endTimestampProvider); + + sut.startTracking(transaction, startTimestamp, routeName); + + // Simulate delay to trigger automatic finish + await Future.delayed( + fixture.autoFinishAfter + const Duration(milliseconds: 100)); + + final ttfdSpan = transaction.children.first; + expect(transaction.children, hasLength(1)); + expect(ttfdSpan.endTimestamp, equals(endTimestampProvider.endTimestamp)); + expect(ttfdSpan.context.operation, + equals(SentrySpanOperations.uiTimeToFullDisplay)); + expect(ttfdSpan.finished, isTrue); + expect(ttfdSpan.status, equals(SpanStatus.deadlineExceeded())); + expect(ttfdSpan.context.description, equals('$routeName full display')); + expect(ttfdSpan.origin, equals(SentryTraceOrigins.manualUiTimeToDisplay)); + }); +} + +class Fixture { + final hub = Hub(SentryFlutterOptions(dsn: fakeDsn)..tracesSampleRate = 1.0); + final autoFinishAfter = const Duration(milliseconds: 100); + + TimeToFullDisplayTracker getSut(EndTimestampProvider endTimestampProvider) { + return TimeToFullDisplayTracker( + endTimestampProvider: endTimestampProvider, + autoFinishAfter: autoFinishAfter); + } +} + +class FakeTTIDEndTimeStampProvider implements EndTimestampProvider { + final DateTime _endTimestamp; + + FakeTTIDEndTimeStampProvider(DateTime startTimestamp) + : _endTimestamp = startTimestamp.add(const Duration(seconds: 1)).toUtc(); + + @override + DateTime? get endTimestamp => _endTimestamp; +} diff --git a/flutter/test/navigation/time_to_initial_display_test.dart b/flutter/test/navigation/time_to_initial_display_tracker_test.dart similarity index 64% rename from flutter/test/navigation/time_to_initial_display_test.dart rename to flutter/test/navigation/time_to_initial_display_tracker_test.dart index 6d5530c5b5..474faf7254 100644 --- a/flutter/test/navigation/time_to_initial_display_test.dart +++ b/flutter/test/navigation/time_to_initial_display_tracker_test.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/integrations/app_start/app_start_tracker.dart'; @@ -11,16 +9,22 @@ import 'package:sentry/src/sentry_tracer.dart'; void main() { late Fixture fixture; + late TimeToInitialDisplayTracker sut; setUp(() { fixture = Fixture(); + sut = fixture.getSut(); + }); + + tearDown(() { + sut.clear(); }); group('app start', () { - test('tracking creates and finishes ttid span with correct measurements', () async { - final sut = fixture.getSut(); - final transaction = fixture.hub.startTransaction('fake', 'fake') - as SentryTracer; + test('tracking creates and finishes ttid span with correct measurements', + () async { + final transaction = + fixture.hub.startTransaction('fake', 'fake') as SentryTracer; final startTimestamp = DateTime.now(); final appStartInfo = AppStartInfo( startTimestamp, @@ -33,28 +37,32 @@ void main() { expect(children, hasLength(1)); final ttidSpan = children.first; - expect(ttidSpan.context.operation, SentrySpanOperations.uiTimeToInitialDisplay); + expect(ttidSpan.context.operation, + SentrySpanOperations.uiTimeToInitialDisplay); expect(ttidSpan.finished, isTrue); expect(ttidSpan.context.description, 'route ("/") initial display'); expect(ttidSpan.origin, SentryTraceOrigins.autoUiTimeToDisplay); - final ttidMeasurement = transaction.measurements['time_to_initial_display']; + final ttidMeasurement = + transaction.measurements['time_to_initial_display']; expect(ttidMeasurement, isNotNull); expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); expect(ttidMeasurement?.value, 10); final appStartMeasurement = transaction.measurements['app_start_cold']; expect(appStartMeasurement, isNotNull); - expect(appStartMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); + expect( + appStartMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); expect(appStartMeasurement?.value, 10); }); }); group('regular route', () { - test('approximation tracking creates and finishes ttid span with correct measurements', () async { - final sut = fixture.getSut(); - final transaction = fixture.hub.startTransaction('fake', 'fake') - as SentryTracer; + test( + 'approximation tracking creates and finishes ttid span with correct measurements', + () async { + final transaction = + fixture.hub.startTransaction('fake', 'fake') as SentryTracer; final startTimestamp = DateTime.now(); await sut.trackRegularRoute(transaction, startTimestamp, 'regular route'); @@ -63,26 +71,31 @@ void main() { expect(children, hasLength(1)); final ttidSpan = children.first; - expect(ttidSpan.context.operation, SentrySpanOperations.uiTimeToInitialDisplay); + expect(ttidSpan.context.operation, + SentrySpanOperations.uiTimeToInitialDisplay); expect(ttidSpan.finished, isTrue); expect(ttidSpan.context.description, 'regular route initial display'); expect(ttidSpan.origin, SentryTraceOrigins.autoUiTimeToDisplay); - final ttidMeasurement = transaction.measurements['time_to_initial_display']; + final ttidMeasurement = + transaction.measurements['time_to_initial_display']; expect(ttidMeasurement, isNotNull); expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); - expect(ttidMeasurement?.value, greaterThan(fixture.finishAfterDuration.inMilliseconds)); - expect(ttidMeasurement?.value, lessThan(fixture.finishAfterDuration.inMilliseconds + 10)); + expect(ttidMeasurement?.value, + greaterThan(fixture.finishFrameAfterDuration.inMilliseconds)); + expect(ttidMeasurement?.value, + lessThan(fixture.finishFrameAfterDuration.inMilliseconds + 10)); }); - test('manual tracking creates and finishes ttid span with correct measurements', () async { - final sut = fixture.getSut(); - final transaction = fixture.hub.startTransaction('fake', 'fake') - as SentryTracer; + test( + 'manual tracking creates and finishes ttid span with correct measurements', + () async { + final transaction = + fixture.hub.startTransaction('fake', 'fake') as SentryTracer; final startTimestamp = DateTime.now(); sut.markAsManual(); - Future.delayed(fixture.finishAfterDuration, () { + Future.delayed(fixture.finishFrameAfterDuration, () { sut.completeTracking(); }); await sut.trackRegularRoute(transaction, startTimestamp, 'regular route'); @@ -91,31 +104,31 @@ void main() { expect(children, hasLength(1)); final ttidSpan = children.first; - expect(ttidSpan.context.operation, SentrySpanOperations.uiTimeToInitialDisplay); + expect(ttidSpan.context.operation, + SentrySpanOperations.uiTimeToInitialDisplay); expect(ttidSpan.finished, isTrue); expect(ttidSpan.context.description, 'regular route initial display'); expect(ttidSpan.origin, SentryTraceOrigins.manualUiTimeToDisplay); - final ttidMeasurement = transaction.measurements['time_to_initial_display']; + final ttidMeasurement = + transaction.measurements['time_to_initial_display']; expect(ttidMeasurement, isNotNull); expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); - expect(ttidMeasurement?.value, greaterThan(fixture.finishAfterDuration.inMilliseconds)); - expect(ttidMeasurement?.value, lessThan(fixture.finishAfterDuration.inMilliseconds + 10)); + expect(ttidMeasurement?.value, + greaterThan(fixture.finishFrameAfterDuration.inMilliseconds)); + expect(ttidMeasurement?.value, + lessThan(fixture.finishFrameAfterDuration.inMilliseconds + 10)); }); }); group('determineEndtime', () { test('can complete automatically in approximation mode', () async { - final sut = fixture.getSut(); - final futureEndTime = sut.determineEndTime(); expect(futureEndTime, completes); }); test('prevents automatic completion in manual mode', () async { - final sut = fixture.getSut(); - sut.markAsManual(); final futureEndTime = sut.determineEndTime(); @@ -123,8 +136,6 @@ void main() { }); test('can complete manually in manual mode', () async { - final sut = fixture.getSut(); - sut.markAsManual(); final futureEndTime = sut.determineEndTime(); @@ -134,42 +145,39 @@ void main() { test('returns the correct approximation end time', () async { final startTime = DateTime.now(); - final sut = fixture.getSut(); final futureEndTime = sut.determineEndTime(); final endTime = await futureEndTime; expect(endTime?.difference(startTime).inSeconds, - fixture.finishAfterDuration.inSeconds); + fixture.finishFrameAfterDuration.inSeconds); }); test('returns the correct manual end time', () async { final startTime = DateTime.now(); - final sut = fixture.getSut(); sut.markAsManual(); final futureEndTime = sut.determineEndTime(); - Future.delayed(fixture.finishAfterDuration, () { + Future.delayed(fixture.finishFrameAfterDuration, () { sut.completeTracking(); }); final endTime = await futureEndTime; expect(endTime?.difference(startTime).inSeconds, - fixture.finishAfterDuration.inSeconds); + fixture.finishFrameAfterDuration.inSeconds); }); }); } class Fixture { final hub = Hub(SentryFlutterOptions(dsn: fakeDsn)..tracesSampleRate = 1.0); - final finishAfterDuration = Duration(milliseconds: 100); + final finishFrameAfterDuration = Duration(milliseconds: 100); late final fakeFrameCallbackHandler = - FakeFrameCallbackHandler(finishAfterDuration: finishAfterDuration); + FakeFrameCallbackHandler(finishAfterDuration: finishFrameAfterDuration); TimeToInitialDisplayTracker getSut() { - final sut = TimeToInitialDisplayTracker(); - sut.frameCallbackHandler = fakeFrameCallbackHandler; - return sut; + return TimeToInitialDisplayTracker( + frameCallbackHandler: fakeFrameCallbackHandler); } } From 852684df154bf51f833010ef8ead050ac700594c Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 23 Feb 2024 13:55:59 +0100 Subject: [PATCH 35/47] fix --- dart/lib/src/sentry_tracer.dart | 3 +- .../native_app_start_integration.dart | 53 ++++++++++--------- .../src/navigation/sentry_display_widget.dart | 3 +- .../navigation/time_to_display_tracker.dart | 2 +- .../time_to_initial_display_tracker.dart | 3 -- 5 files changed, 31 insertions(+), 33 deletions(-) diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index b6bc4e3d7a..56be03a690 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -368,7 +368,8 @@ class SentryTracer extends ISentrySpan { Dsn.parse(_hub.options.dsn!).publicKey, release: _hub.options.release, environment: _hub.options.environment, - userId: null, // because of PII not sending it for now + userId: null, + // because of PII not sending it for now userSegment: user?.segment, transaction: _isHighQualityTransactionName(transactionNameSource) ? name : null, diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 49b38f6ca9..2a51da1c81 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -32,36 +32,37 @@ class NativeAppStartIntegration extends Integration { final appStartEnd = options.clock(); _native.appStartEnd = appStartEnd; - if (!_native.didFetchAppStart) { - final nativeAppStart = await _native.fetchNativeAppStart(); - final measurement = nativeAppStart?.toMeasurement(appStartEnd!); - - // We filter out app start more than 60s. - // This could be due to many different reasons. - // If you do the manual init and init the SDK too late and it does not - // compute the app start end in the very first Screen. - // If the process starts but the App isn't in the foreground. - // If the system forked the process earlier to accelerate the app start. - // And some unknown reasons that could not be reproduced. - // We've seen app starts with hours, days and even months. - if (nativeAppStart == null || - measurement == null || - measurement.value >= 60000) { - _appStartTracker?.setAppStartInfo(null); - return; - } + if (_native.didFetchAppStart) { + _appStartTracker?.setAppStartInfo(null); + return; + } - final appStartInfo = AppStartInfo( - DateTime.fromMillisecondsSinceEpoch( - nativeAppStart.appStartTime.toInt()), - appStartEnd, - measurement, - ); + final nativeAppStart = await _native.fetchNativeAppStart(); + final measurement = nativeAppStart?.toMeasurement(appStartEnd); - _appStartTracker?.setAppStartInfo(appStartInfo); - } else { + // We filter out app start more than 60s. + // This could be due to many different reasons. + // If you do the manual init and init the SDK too late and it does not + // compute the app start end in the very first Screen. + // If the process starts but the App isn't in the foreground. + // If the system forked the process earlier to accelerate the app start. + // And some unknown reasons that could not be reproduced. + // We've seen app starts with hours, days and even months. + if (nativeAppStart == null || + measurement == null || + measurement.value >= 60000) { _appStartTracker?.setAppStartInfo(null); + return; } + + final appStartInfo = AppStartInfo( + DateTime.fromMillisecondsSinceEpoch( + nativeAppStart.appStartTime.toInt()), + appStartEnd, + measurement, + ); + + _appStartTracker?.setAppStartInfo(appStartInfo); }); } } diff --git a/flutter/lib/src/navigation/sentry_display_widget.dart b/flutter/lib/src/navigation/sentry_display_widget.dart index 5793f52e63..df087f3e91 100644 --- a/flutter/lib/src/navigation/sentry_display_widget.dart +++ b/flutter/lib/src/navigation/sentry_display_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/cupertino.dart'; -import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; +import 'time_to_initial_display_tracker.dart'; -import '../../sentry_flutter.dart'; import '../frame_callback_handler.dart'; class SentryDisplayWidget extends StatefulWidget { diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index bb03c3c143..8cbd17f924 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'package:sentry_flutter/src/navigation/time_to_full_display_tracker.dart'; +import 'time_to_full_display_tracker.dart'; import '../../sentry_flutter.dart'; import '../frame_callback_handler.dart'; diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart index 0a6b40ca23..394c7a947f 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -1,13 +1,10 @@ import 'dart:async'; -import 'dart:ffi'; -import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../frame_callback_handler.dart'; import '../integrations/app_start/app_start_tracker.dart'; -import '../integrations/integrations.dart'; import '../sentry_flutter_measurement.dart'; @internal From ae62328253a07fd936e92ee1509e558d04fc93ed Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 23 Feb 2024 13:58:41 +0100 Subject: [PATCH 36/47] remove comments app start tracker --- flutter/lib/src/integrations/app_start/app_start_tracker.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/flutter/lib/src/integrations/app_start/app_start_tracker.dart b/flutter/lib/src/integrations/app_start/app_start_tracker.dart index 87807b382a..a139bb7a03 100644 --- a/flutter/lib/src/integrations/app_start/app_start_tracker.dart +++ b/flutter/lib/src/integrations/app_start/app_start_tracker.dart @@ -27,21 +27,17 @@ class AppStartTracker { void setAppStartInfo(AppStartInfo? appStartInfo) { _appStartInfo = appStartInfo; if (!_appStartCompleter.isCompleted) { - // Complete the completer with the app start info when it becomes available _appStartCompleter.complete(appStartInfo); } else { - // If setAppStartInfo is called again, reset the completer with new app start info _appStartCompleter = Completer(); _appStartCompleter.complete(appStartInfo); } } Future getAppStartInfo() { - // If the app start info is already set, return it immediately if (_appStartInfo != null) { return Future.value(_appStartInfo); } - // Otherwise, return the future that will complete when the app start info is set return _appStartCompleter.future; } } From d9129227ec76d007472fd0f675263c4fe516ec5b Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 28 Feb 2024 11:22:41 +0100 Subject: [PATCH 37/47] fix tests --- .../app_start/app_start_tracker.dart | 6 +-- .../navigation/sentry_navigator_observer.dart | 26 ++++----- .../navigation/time_to_display_tracker.dart | 5 +- .../time_to_full_display_tracker.dart | 8 +++ .../time_to_initial_display_tracker_test.dart | 3 +- .../test/sentry_navigator_observer_test.dart | 54 ++++++------------- 6 files changed, 41 insertions(+), 61 deletions(-) diff --git a/flutter/lib/src/integrations/app_start/app_start_tracker.dart b/flutter/lib/src/integrations/app_start/app_start_tracker.dart index a139bb7a03..74a84646cb 100644 --- a/flutter/lib/src/integrations/app_start/app_start_tracker.dart +++ b/flutter/lib/src/integrations/app_start/app_start_tracker.dart @@ -16,14 +16,12 @@ class AppStartInfo { @internal class AppStartTracker { static final AppStartTracker _instance = AppStartTracker._internal(); - Completer _appStartCompleter = Completer(); - factory AppStartTracker() => _instance; + AppStartTracker._internal(); + Completer _appStartCompleter = Completer(); AppStartInfo? _appStartInfo; - AppStartTracker._internal(); - void setAppStartInfo(AppStartInfo? appStartInfo) { _appStartInfo = appStartInfo; if (!_appStartCompleter.isCompleted) { diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 2638db0b61..100a8286b1 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -80,17 +80,19 @@ class SentryNavigatorObserver extends RouteObserver> { // ignore: invalid_use_of_internal_member _hub.options.sdk.addIntegration('UINavigationTracing'); } - final enableTimeToFullDisplayTracing = - (_hub.options as SentryFlutterOptions).enableTimeToFullDisplayTracing; - _timeToDisplayTracker = timeToDisplayTracker ?? - TimeToDisplayTracker( - enableTimeToFullDisplayTracing: enableTimeToFullDisplayTracing, - ttdTransactionHandler: TimeToDisplayTransactionHandler( - hub: hub, - enableAutoTransactions: enableAutoTransactions, - autoFinishAfter: autoFinishAfter, - ), - ); + final options = _hub.options; + if (options is SentryFlutterOptions) { + final enableTimeToFullDisplayTracing = options.enableTimeToFullDisplayTracing; + _timeToDisplayTracker = timeToDisplayTracker ?? + TimeToDisplayTracker( + enableTimeToFullDisplayTracing: enableTimeToFullDisplayTracing, + ttdTransactionHandler: TimeToDisplayTransactionHandler( + hub: hub, + enableAutoTransactions: enableAutoTransactions, + autoFinishAfter: autoFinishAfter, + ), + ); + } } final Hub _hub; @@ -111,7 +113,7 @@ class SentryNavigatorObserver extends RouteObserver> { Completer? _completedDisplayTracking; - @internal + @visibleForTesting Completer? get completedDisplayTracking => _completedDisplayTracking; @override diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index 8cbd17f924..ed0d9495b5 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -4,11 +4,8 @@ import 'package:meta/meta.dart'; import 'time_to_full_display_tracker.dart'; import '../../sentry_flutter.dart'; -import '../frame_callback_handler.dart'; import '../integrations/app_start/app_start_tracker.dart'; -import '../integrations/integrations.dart'; import '../native/sentry_native.dart'; -import '../sentry_flutter_measurement.dart'; import 'time_to_display_transaction_handler.dart'; import 'time_to_initial_display_tracker.dart'; @@ -72,7 +69,7 @@ class TimeToDisplayTracker { } } - // Handles measuring navigation for regular routes + /// Starts and finishes Time To Display spans for regular routes meaning routes that are not root. Future _trackRegularRouteTTD( String? routeName, Object? arguments, DateTime startTimestamp) async { final transaction = await _ttdTransactionHandler diff --git a/flutter/lib/src/navigation/time_to_full_display_tracker.dart b/flutter/lib/src/navigation/time_to_full_display_tracker.dart index b595839cce..5c23fef8a2 100644 --- a/flutter/lib/src/navigation/time_to_full_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_full_display_tracker.dart @@ -1,18 +1,26 @@ import 'dart:async'; +import 'package:meta/meta.dart'; + import '../../sentry_flutter.dart'; import '../sentry_flutter_measurement.dart'; import 'time_to_initial_display_tracker.dart'; +/// We need to retrieve the end time stamp in case TTFD timeout is triggered. +/// In those cases TTFD end time should match TTID end time. +/// This provider allows us to inject endTimestamps for testing as well. +@internal abstract class EndTimestampProvider { DateTime? get endTimestamp; } +@internal class TTIDEndTimestampProvider implements EndTimestampProvider { @override DateTime? get endTimestamp => TimeToInitialDisplayTracker().endTimestamp; } +@internal class TimeToFullDisplayTracker { static final TimeToFullDisplayTracker _instance = TimeToFullDisplayTracker._internal(); diff --git a/flutter/test/navigation/time_to_initial_display_tracker_test.dart b/flutter/test/navigation/time_to_initial_display_tracker_test.dart index 474faf7254..b32181b16f 100644 --- a/flutter/test/navigation/time_to_initial_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_initial_display_tracker_test.dart @@ -109,13 +109,12 @@ void main() { expect(ttidSpan.finished, isTrue); expect(ttidSpan.context.description, 'regular route initial display'); expect(ttidSpan.origin, SentryTraceOrigins.manualUiTimeToDisplay); - final ttidMeasurement = transaction.measurements['time_to_initial_display']; expect(ttidMeasurement, isNotNull); expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); expect(ttidMeasurement?.value, - greaterThan(fixture.finishFrameAfterDuration.inMilliseconds)); + greaterThanOrEqualTo(fixture.finishFrameAfterDuration.inMilliseconds)); expect(ttidMeasurement?.value, lessThan(fixture.finishFrameAfterDuration.inMilliseconds + 10)); }); diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index 1dda9ccc75..cf7941e921 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -11,6 +11,7 @@ import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_transaction_handler.dart'; import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; +import 'fake_frame_callback_handler.dart'; import 'mocks.dart'; import 'mocks.mocks.dart'; @@ -92,16 +93,11 @@ void main() { mockNativeChannel.nativeFrames = nativeFrames; final sut = fixture.getSut( - hub: hub, - autoFinishAfter: Duration(milliseconds: 50), + hub: hub ); sut.didPush(currentRoute, null); - await Future.delayed(Duration(milliseconds: 50)); - TimeToInitialDisplayTracker().markAsManual(); - TimeToInitialDisplayTracker().completeTracking(); - // Get ref to created transaction // ignore: invalid_use_of_internal_member SentryTracer? actualTransaction; @@ -110,7 +106,9 @@ void main() { actualTransaction = scope.span as SentryTracer; }); - await Future.delayed(Duration(milliseconds: 500)); + await sut.completedDisplayTracking?.future; + + await Future.delayed(Duration(milliseconds: 1500)); expect(mockNativeChannel.numberOfEndNativeFramesCalls, 1); @@ -356,40 +354,14 @@ void main() { final sut = fixture.getSut(hub: hub); sut.didPush(currentRoute, null); + when(span.finished).thenReturn(true); + sut.didPop(currentRoute, null); sut.didPop(currentRoute, null); verify(span.finish()).called(1); }); - test('didPop re-starts previous', () { - final previousRoute = route(RouteSettings(name: 'Previous Route')); - final currentRoute = route(RouteSettings(name: 'Current Route')); - - final hub = _MockHub(); - final previousSpan = getMockSentryTracer(); - when(previousSpan.context).thenReturn(SentrySpanContext(operation: 'op')); - when(previousSpan.status).thenReturn(null); - - _whenAnyStart(hub, previousSpan); - - final sut = fixture.getSut(hub: hub); - - sut.didPop(currentRoute, previousRoute); - - verify(hub.startTransactionWithContext( - any, - waitForChildren: true, - autoFinishAfter: anyNamed('autoFinishAfter'), - trimEnd: true, - onFinish: anyNamed('onFinish'), - )); - - hub.configureScope((scope) { - expect(scope.span, previousSpan); - }); - }); - test('route arguments are set on transaction', () { final arguments = {'foo': 'bar'}; final currentRoute = route(RouteSettings( @@ -899,18 +871,22 @@ class Fixture { SentryNavigatorObserver getSut({ required Hub hub, bool enableAutoTransactions = true, - Duration autoFinishAfter = const Duration(seconds: 3), + Duration autoFinishAfter = const Duration(seconds: 1), bool setRouteNameAsTransaction = false, RouteNameExtractor? routeNameExtractor, AdditionalInfoExtractor? additionalInfoProvider, }) { + final frameCallbackHandler = FakeFrameCallbackHandler(); + final timeToInitialDisplayTracker = + TimeToInitialDisplayTracker(frameCallbackHandler: frameCallbackHandler); final timeToDisplayTracker = TimeToDisplayTracker( - enableTimeToFullDisplayTracing: true, + enableTimeToFullDisplayTracing: false, ttdTransactionHandler: TimeToDisplayTransactionHandler( hub: hub, - enableAutoTransactions: true, - autoFinishAfter: const Duration(seconds: 30), + enableAutoTransactions: enableAutoTransactions, + autoFinishAfter: autoFinishAfter, ), + ttidTracker: timeToInitialDisplayTracker, ); return SentryNavigatorObserver( hub: hub, From 8f9c5da38d99eb3e9a372bd95f0f7f7bd206b027 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 28 Feb 2024 11:30:53 +0100 Subject: [PATCH 38/47] add comments --- .../src/navigation/sentry_display_widget.dart | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/flutter/lib/src/navigation/sentry_display_widget.dart b/flutter/lib/src/navigation/sentry_display_widget.dart index df087f3e91..ffed11cce7 100644 --- a/flutter/lib/src/navigation/sentry_display_widget.dart +++ b/flutter/lib/src/navigation/sentry_display_widget.dart @@ -3,6 +3,30 @@ import 'time_to_initial_display_tracker.dart'; import '../frame_callback_handler.dart'; +/// A widget that reports the Time To Initially Displayed (TTID) of its child widget. +/// +/// This widget wraps around another widget to measure and report the time it takes +/// for the child widget to be initially displayed on the screen. This method +/// allows a more accurate measurement than what the default TTID implementation +/// provides. The TTID measurement begins when the route to the widget is pushed and ends +/// when [WidgetsBinding.instance.addPostFrameCallback] is triggered. +/// +/// Wrap the widget you want to measure with [SentryDisplayWidget], and ensure that you +/// have set up Sentry's routing instrumentation according to the Sentry documentation. +/// +/// ```dart +/// SentryDisplayWidget( +/// child: MyWidget(), +/// ) +/// ``` +/// +/// Make sure to configure Sentry's routing instrumentation in your app by following +/// the guidelines provided in Sentry's documentation for Flutter integrations: +/// https://docs.sentry.io/platforms/flutter/integrations/routing-instrumentation/ +/// +/// See also: +/// - [Sentry's documentation on Flutter integrations](https://docs.sentry.io/platforms/flutter/) +/// for more information on how to integrate Sentry into your Flutter application. class SentryDisplayWidget extends StatefulWidget { final Widget child; final FrameCallbackHandler _frameCallbackHandler; From 67f622af72b33e7f2127e975d07d0171c2b27c8f Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 28 Feb 2024 11:32:06 +0100 Subject: [PATCH 39/47] adjust test --- .../test/navigation/time_to_initial_display_tracker_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/test/navigation/time_to_initial_display_tracker_test.dart b/flutter/test/navigation/time_to_initial_display_tracker_test.dart index b32181b16f..098ef10e2e 100644 --- a/flutter/test/navigation/time_to_initial_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_initial_display_tracker_test.dart @@ -82,7 +82,7 @@ void main() { expect(ttidMeasurement, isNotNull); expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); expect(ttidMeasurement?.value, - greaterThan(fixture.finishFrameAfterDuration.inMilliseconds)); + greaterThanOrEqualTo(fixture.finishFrameAfterDuration.inMilliseconds)); expect(ttidMeasurement?.value, lessThan(fixture.finishFrameAfterDuration.inMilliseconds + 10)); }); From 011533d10246520e6a2d7d24d79793c296b97ffe Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 28 Feb 2024 11:49:29 +0100 Subject: [PATCH 40/47] try other test --- .../time_to_display_tracker_test.dart | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index 2ebb00a215..ad6d8e0352 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -99,20 +99,19 @@ void main() { fixture.options.enableTimeToFullDisplayTracing = true; }); - test('startMeasurement creates ttfd and ttid span', () async { + test('startMeasurement creates ttfd and ttid span', () { final sut = fixture.getSut(); - await sut.startTracking('Current Route', null); - - final transaction = fixture.hub.getSpan() as SentryTracer; - await Future.delayed(const Duration(milliseconds: 100)); + return sut.startTracking('Current Route', null).then((value){ + final transaction = fixture.hub.getSpan() as SentryTracer; - final spans = transaction.children; - expect(transaction.children, hasLength(2)); - expect(spans[0].context.operation, - SentrySpanOperations.uiTimeToInitialDisplay); - expect( - spans[1].context.operation, SentrySpanOperations.uiTimeToFullDisplay); + final spans = transaction.children; + expect(transaction.children, hasLength(2)); + expect(spans[0].context.operation, + SentrySpanOperations.uiTimeToInitialDisplay); + expect( + spans[1].context.operation, SentrySpanOperations.uiTimeToFullDisplay); + }); }); group('in root screen app start route', () { From 9e7c1f063e50bda0d41e547aef81fed55a814aaf Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 28 Feb 2024 12:44:44 +0100 Subject: [PATCH 41/47] format and fix test --- .../navigation/sentry_navigator_observer.dart | 3 ++- .../navigation/time_to_display_tracker_test.dart | 6 +++--- .../time_to_initial_display_tracker_test.dart | 16 ++++++++-------- flutter/test/sentry_navigator_observer_test.dart | 4 +--- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 100a8286b1..c1e61cd207 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -82,7 +82,8 @@ class SentryNavigatorObserver extends RouteObserver> { } final options = _hub.options; if (options is SentryFlutterOptions) { - final enableTimeToFullDisplayTracing = options.enableTimeToFullDisplayTracing; + final enableTimeToFullDisplayTracing = + options.enableTimeToFullDisplayTracing; _timeToDisplayTracker = timeToDisplayTracker ?? TimeToDisplayTracker( enableTimeToFullDisplayTracing: enableTimeToFullDisplayTracing, diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index ad6d8e0352..fb592343cd 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -102,15 +102,15 @@ void main() { test('startMeasurement creates ttfd and ttid span', () { final sut = fixture.getSut(); - return sut.startTracking('Current Route', null).then((value){ + return sut.startTracking('Current Route', null).then((value) { final transaction = fixture.hub.getSpan() as SentryTracer; final spans = transaction.children; expect(transaction.children, hasLength(2)); expect(spans[0].context.operation, SentrySpanOperations.uiTimeToInitialDisplay); - expect( - spans[1].context.operation, SentrySpanOperations.uiTimeToFullDisplay); + expect(spans[1].context.operation, + SentrySpanOperations.uiTimeToFullDisplay); }); }); diff --git a/flutter/test/navigation/time_to_initial_display_tracker_test.dart b/flutter/test/navigation/time_to_initial_display_tracker_test.dart index 098ef10e2e..de9855389a 100644 --- a/flutter/test/navigation/time_to_initial_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_initial_display_tracker_test.dart @@ -81,10 +81,10 @@ void main() { transaction.measurements['time_to_initial_display']; expect(ttidMeasurement, isNotNull); expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); - expect(ttidMeasurement?.value, - greaterThanOrEqualTo(fixture.finishFrameAfterDuration.inMilliseconds)); - expect(ttidMeasurement?.value, - lessThan(fixture.finishFrameAfterDuration.inMilliseconds + 10)); + expect( + ttidMeasurement?.value, + greaterThanOrEqualTo( + fixture.finishFrameAfterDuration.inMilliseconds)); }); test( @@ -113,10 +113,10 @@ void main() { transaction.measurements['time_to_initial_display']; expect(ttidMeasurement, isNotNull); expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); - expect(ttidMeasurement?.value, - greaterThanOrEqualTo(fixture.finishFrameAfterDuration.inMilliseconds)); - expect(ttidMeasurement?.value, - lessThan(fixture.finishFrameAfterDuration.inMilliseconds + 10)); + expect( + ttidMeasurement?.value, + greaterThanOrEqualTo( + fixture.finishFrameAfterDuration.inMilliseconds)); }); }); diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index cf7941e921..6b35342e1c 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -92,9 +92,7 @@ void main() { final nativeFrames = NativeFrames(3, 2, 1); mockNativeChannel.nativeFrames = nativeFrames; - final sut = fixture.getSut( - hub: hub - ); + final sut = fixture.getSut(hub: hub); sut.didPush(currentRoute, null); From 82b86fbb73c74d9894367f99fcbd0045cd70e27a Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 28 Feb 2024 14:19:21 +0100 Subject: [PATCH 42/47] add comment --- flutter/lib/src/navigation/sentry_navigator_observer.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index c1e61cd207..9c27210de3 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -24,6 +24,8 @@ typedef AdditionalInfoExtractor = Map? Function( /// This is a navigation observer to record navigational breadcrumbs. /// For now it only records navigation events and no gestures. /// +/// It also records Time to Initial Display (TTID) and Time to Full Display (TTFD). +/// /// [Route]s can always be null and their [Route.settings] can also always be null. /// For example, if the application starts, there is no previous route. /// The [RouteSettings] is null if a developer has not specified any From aa42e952a54ad6acbc8db3315ba003af97562ea6 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 29 Feb 2024 12:50:55 +0100 Subject: [PATCH 43/47] exit early in app start event processor --- .../native_app_start_event_processor.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index bade96048b..44d01013cf 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -22,12 +22,15 @@ class NativeAppStartEventProcessor implements EventProcessor { @override Future apply(SentryEvent event, {Hint? hint}) async { + if (didAddAppStartMeasurement || event is! SentryTransaction) { + return event; + } + final appStartInfo = await _appStartTracker?.getAppStartInfo(); final measurement = appStartInfo?.measurement; - if (!didAddAppStartMeasurement && - measurement != null && - measurement.value.toInt() <= _maxAppStartMillis && - event is SentryTransaction) { + + if (measurement != null && + measurement.value.toInt() <= _maxAppStartMillis) { event.measurements[measurement.name] = measurement; didAddAppStartMeasurement = true; } From 25291cb704535d61339140b31a10faee954ea4be Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 29 Feb 2024 13:26:12 +0100 Subject: [PATCH 44/47] Apply review --- .../native_app_start_event_processor.dart | 8 ++------ .../app_start/app_start_tracker.dart | 4 ++-- .../native_app_start_integration.dart | 5 ++++- .../navigation/time_to_display_tracker.dart | 18 ++++++++++-------- .../time_to_full_display_tracker.dart | 13 ++++++++++++- .../time_to_initial_display_tracker.dart | 4 ---- 6 files changed, 30 insertions(+), 22 deletions(-) diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index 44d01013cf..448702f857 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -9,14 +9,11 @@ import '../native/sentry_native.dart'; /// EventProcessor that enriches [SentryTransaction] objects with app start /// measurement. class NativeAppStartEventProcessor implements EventProcessor { - /// We filter out App starts more than 60s - static const _maxAppStartMillis = 60000; - final AppStartTracker? _appStartTracker; NativeAppStartEventProcessor({ AppStartTracker? appStartTracker, - }) : _appStartTracker = appStartTracker ?? AppStartTracker(); + }) : _appStartTracker = appStartTracker ?? AppStartTracker(); bool didAddAppStartMeasurement = false; @@ -29,8 +26,7 @@ class NativeAppStartEventProcessor implements EventProcessor { final appStartInfo = await _appStartTracker?.getAppStartInfo(); final measurement = appStartInfo?.measurement; - if (measurement != null && - measurement.value.toInt() <= _maxAppStartMillis) { + if (measurement != null) { event.measurements[measurement.name] = measurement; didAddAppStartMeasurement = true; } diff --git a/flutter/lib/src/integrations/app_start/app_start_tracker.dart b/flutter/lib/src/integrations/app_start/app_start_tracker.dart index 74a84646cb..006433d959 100644 --- a/flutter/lib/src/integrations/app_start/app_start_tracker.dart +++ b/flutter/lib/src/integrations/app_start/app_start_tracker.dart @@ -8,9 +8,8 @@ import '../../../sentry_flutter.dart'; class AppStartInfo { final DateTime start; final DateTime end; - final SentryMeasurement measurement; - AppStartInfo(this.start, this.end, this.measurement); + AppStartInfo(this.start, this.end); } @internal @@ -39,3 +38,4 @@ class AppStartTracker { return _appStartCompleter.future; } } + diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 2a51da1c81..1f40bff654 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -19,6 +19,9 @@ class NativeAppStartIntegration extends Integration { final SchedulerBindingProvider _schedulerBindingProvider; final AppStartTracker? _appStartTracker; + /// We filter out App starts more than 60s + static const _maxAppStartMillis = 60000; + @override void call(Hub hub, SentryFlutterOptions options) { if (options.autoAppStart) { @@ -50,7 +53,7 @@ class NativeAppStartIntegration extends Integration { // We've seen app starts with hours, days and even months. if (nativeAppStart == null || measurement == null || - measurement.value >= 60000) { + measurement.value > _maxAppStartMillis) { _appStartTracker?.setAppStartInfo(null); return; } diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index ed0d9495b5..518084e4ac 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -39,6 +39,8 @@ class TimeToDisplayTracker { final isRootScreen = routeName == 'root ("/")'; final didFetchAppStart = _native?.didFetchAppStart; if (isRootScreen && didFetchAppStart == false) { + // Dart cannot infer here that routeName is not nullable + if (routeName == null) return; return _trackAppStartTTD(routeName, arguments); } else { return _trackRegularRouteTTD(routeName, arguments, startTimestamp); @@ -52,21 +54,21 @@ class TimeToDisplayTracker { /// - Finishes the TTID span immediately with the app start end timestamp /// /// We start and immediately finish the TTID span since we cannot mutate the history of spans. - Future _trackAppStartTTD(String? routeName, Object? arguments) async { + Future _trackAppStartTTD(String routeName, Object? arguments) async { final appStartInfo = await _appStartTracker.getAppStartInfo(); - final name = routeName ?? SentryNavigatorObserver.currentRouteName; + final name = routeName; - if (appStartInfo == null || name == null) return; + if (appStartInfo == null) return; final transaction = await _ttdTransactionHandler .startTransaction(name, arguments, startTimestamp: appStartInfo.start); if (transaction == null) return; - await _ttidTracker.trackAppStart(transaction, appStartInfo, name); - if (_enableTimeToFullDisplayTracing) { _ttfdTracker.startTracking(transaction, appStartInfo.start, name); } + + await _ttidTracker.trackAppStart(transaction, appStartInfo, name); } /// Starts and finishes Time To Display spans for regular routes meaning routes that are not root. @@ -77,12 +79,12 @@ class TimeToDisplayTracker { if (transaction == null || routeName == null) return; - await _ttidTracker.trackRegularRoute( - transaction, startTimestamp, routeName); - if (_enableTimeToFullDisplayTracing) { _ttfdTracker.startTracking(transaction, startTimestamp, routeName); } + + await _ttidTracker.trackRegularRoute( + transaction, startTimestamp, routeName); } @internal diff --git a/flutter/lib/src/navigation/time_to_full_display_tracker.dart b/flutter/lib/src/navigation/time_to_full_display_tracker.dart index 5c23fef8a2..454efe9cc4 100644 --- a/flutter/lib/src/navigation/time_to_full_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_full_display_tracker.dart @@ -72,6 +72,8 @@ class TimeToFullDisplayTracker { _setTTFDMeasurement(startTimestamp, endTimestamp); ttfdSpan.finish( status: SpanStatus.deadlineExceeded(), endTimestamp: endTimestamp); + + clearState(); } Future reportFullyDisplayed() async { @@ -87,7 +89,9 @@ class TimeToFullDisplayTracker { } _setTTFDMeasurement(startTimestamp, endTimestamp); - return ttfdSpan.finish(endTimestamp: endTimestamp); + await ttfdSpan.finish(endTimestamp: endTimestamp); + + clearState(); } void _setTTFDMeasurement(DateTime startTimestamp, DateTime endTimestamp) { @@ -96,4 +100,11 @@ class TimeToFullDisplayTracker { _transaction?.setMeasurement(measurement.name, measurement.value, unit: measurement.unit); } + + void clearState() { + _startTimestamp = null; + _ttfdSpan = null; + _ttfdTimer = null; + _transaction = null; + } } diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart index 394c7a947f..c49b1bc5f6 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -68,10 +68,6 @@ class TimeToInitialDisplayTracker { ); ttidSpan.origin = SentryTraceOrigins.autoUiTimeToDisplay; - transaction.setMeasurement( - appStartInfo.measurement.name, appStartInfo.measurement.value, - unit: appStartInfo.measurement.unit); - final ttidMeasurement = SentryFlutterMeasurement.timeToInitialDisplay( Duration(milliseconds: appStartInfo.measurement.value.toInt()), ); From 32d3bc017e399b2134a709ee84453e4320191772 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 29 Feb 2024 16:22:10 +0100 Subject: [PATCH 45/47] Update review --- .../native_app_start_event_processor.dart | 18 +++--------------- .../app_start/app_start_tracker.dart | 4 ++-- .../native_app_start_integration.dart | 12 ++++++++++++ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index 448702f857..407d86e47c 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -2,9 +2,8 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; +import '../../sentry_flutter.dart'; import '../integrations/app_start/app_start_tracker.dart'; -import '../integrations/integrations.dart'; -import '../native/sentry_native.dart'; /// EventProcessor that enriches [SentryTransaction] objects with app start /// measurement. @@ -13,13 +12,13 @@ class NativeAppStartEventProcessor implements EventProcessor { NativeAppStartEventProcessor({ AppStartTracker? appStartTracker, - }) : _appStartTracker = appStartTracker ?? AppStartTracker(); + }) : _appStartTracker = appStartTracker ?? AppStartTracker(); bool didAddAppStartMeasurement = false; @override Future apply(SentryEvent event, {Hint? hint}) async { - if (didAddAppStartMeasurement || event is! SentryTransaction) { + if (!didAddAppStartMeasurement || event is! SentryTransaction) { return event; } @@ -34,14 +33,3 @@ class NativeAppStartEventProcessor implements EventProcessor { } } -extension NativeAppStartMeasurement on NativeAppStart { - SentryMeasurement toMeasurement(DateTime appStartEnd) { - final appStartDateTime = - DateTime.fromMillisecondsSinceEpoch(appStartTime.toInt()); - final duration = appStartEnd.difference(appStartDateTime); - - return isColdStart - ? SentryMeasurement.coldAppStart(duration) - : SentryMeasurement.warmAppStart(duration); - } -} diff --git a/flutter/lib/src/integrations/app_start/app_start_tracker.dart b/flutter/lib/src/integrations/app_start/app_start_tracker.dart index 006433d959..74a84646cb 100644 --- a/flutter/lib/src/integrations/app_start/app_start_tracker.dart +++ b/flutter/lib/src/integrations/app_start/app_start_tracker.dart @@ -8,8 +8,9 @@ import '../../../sentry_flutter.dart'; class AppStartInfo { final DateTime start; final DateTime end; + final SentryMeasurement measurement; - AppStartInfo(this.start, this.end); + AppStartInfo(this.start, this.end, this.measurement); } @internal @@ -38,4 +39,3 @@ class AppStartTracker { return _appStartCompleter.future; } } - diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 1f40bff654..c7472ce2d2 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -79,3 +79,15 @@ class NativeAppStartIntegration extends Integration { /// Used to provide scheduler binding at call time. typedef SchedulerBindingProvider = SchedulerBinding? Function(); + +extension NativeAppStartMeasurement on NativeAppStart { + SentryMeasurement toMeasurement(DateTime appStartEnd) { + final appStartDateTime = + DateTime.fromMillisecondsSinceEpoch(appStartTime.toInt()); + final duration = appStartEnd.difference(appStartDateTime); + + return isColdStart + ? SentryMeasurement.coldAppStart(duration) + : SentryMeasurement.warmAppStart(duration); + } +} From 34033f9af54e6bc646bbb5e233a11e752e1c0351 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 1 Mar 2024 10:49:35 +0100 Subject: [PATCH 46/47] remove debug label from framecallbackhandler --- flutter/lib/src/frame_callback_handler.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/src/frame_callback_handler.dart b/flutter/lib/src/frame_callback_handler.dart index 8ec5e4c68b..4c02a0a7a4 100644 --- a/flutter/lib/src/frame_callback_handler.dart +++ b/flutter/lib/src/frame_callback_handler.dart @@ -2,13 +2,12 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; abstract class FrameCallbackHandler { - void addPostFrameCallback(FrameCallback callback, {String debugLabel}); + void addPostFrameCallback(FrameCallback callback); } class DefaultFrameCallbackHandler implements FrameCallbackHandler { @override - void addPostFrameCallback(FrameCallback callback, - {String debugLabel = 'callback'}) { + void addPostFrameCallback(FrameCallback callback) { WidgetsBinding.instance.addPostFrameCallback(callback); } } From 32faabfe7c29270ce1bfd858c35fc858114962d4 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 1 Mar 2024 12:16:06 +0100 Subject: [PATCH 47/47] pr improvements --- .../event_processor/native_app_start_event_processor.dart | 6 +++--- .../lib/src/integrations/app_start/app_start_tracker.dart | 8 +++----- .../lib/src/navigation/time_to_full_display_tracker.dart | 4 ++-- .../src/navigation/time_to_initial_display_tracker.dart | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/flutter/lib/src/event_processor/native_app_start_event_processor.dart b/flutter/lib/src/event_processor/native_app_start_event_processor.dart index 407d86e47c..d438d098e3 100644 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ b/flutter/lib/src/event_processor/native_app_start_event_processor.dart @@ -14,11 +14,11 @@ class NativeAppStartEventProcessor implements EventProcessor { AppStartTracker? appStartTracker, }) : _appStartTracker = appStartTracker ?? AppStartTracker(); - bool didAddAppStartMeasurement = false; + bool _didAddAppStartMeasurement = false; @override Future apply(SentryEvent event, {Hint? hint}) async { - if (!didAddAppStartMeasurement || event is! SentryTransaction) { + if (!_didAddAppStartMeasurement || event is! SentryTransaction) { return event; } @@ -27,7 +27,7 @@ class NativeAppStartEventProcessor implements EventProcessor { if (measurement != null) { event.measurements[measurement.name] = measurement; - didAddAppStartMeasurement = true; + _didAddAppStartMeasurement = true; } return event; } diff --git a/flutter/lib/src/integrations/app_start/app_start_tracker.dart b/flutter/lib/src/integrations/app_start/app_start_tracker.dart index 74a84646cb..9bbead7945 100644 --- a/flutter/lib/src/integrations/app_start/app_start_tracker.dart +++ b/flutter/lib/src/integrations/app_start/app_start_tracker.dart @@ -15,9 +15,9 @@ class AppStartInfo { @internal class AppStartTracker { - static final AppStartTracker _instance = AppStartTracker._internal(); + static final AppStartTracker _instance = AppStartTracker._(); factory AppStartTracker() => _instance; - AppStartTracker._internal(); + AppStartTracker._(); Completer _appStartCompleter = Completer(); AppStartInfo? _appStartInfo; @@ -25,11 +25,9 @@ class AppStartTracker { void setAppStartInfo(AppStartInfo? appStartInfo) { _appStartInfo = appStartInfo; if (!_appStartCompleter.isCompleted) { - _appStartCompleter.complete(appStartInfo); - } else { _appStartCompleter = Completer(); - _appStartCompleter.complete(appStartInfo); } + _appStartCompleter.complete(appStartInfo); } Future getAppStartInfo() { diff --git a/flutter/lib/src/navigation/time_to_full_display_tracker.dart b/flutter/lib/src/navigation/time_to_full_display_tracker.dart index 454efe9cc4..0bbe70d6f7 100644 --- a/flutter/lib/src/navigation/time_to_full_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_full_display_tracker.dart @@ -23,7 +23,7 @@ class TTIDEndTimestampProvider implements EndTimestampProvider { @internal class TimeToFullDisplayTracker { static final TimeToFullDisplayTracker _instance = - TimeToFullDisplayTracker._internal(); + TimeToFullDisplayTracker._(); factory TimeToFullDisplayTracker( {EndTimestampProvider? endTimestampProvider, Duration? autoFinishAfter}) { @@ -36,7 +36,7 @@ class TimeToFullDisplayTracker { return _instance; } - TimeToFullDisplayTracker._internal(); + TimeToFullDisplayTracker._(); DateTime? _startTimestamp; ISentrySpan? _ttfdSpan; diff --git a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart index c49b1bc5f6..6a8f31d406 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -10,7 +10,7 @@ import '../sentry_flutter_measurement.dart'; @internal class TimeToInitialDisplayTracker { static final TimeToInitialDisplayTracker _instance = - TimeToInitialDisplayTracker._internal(); + TimeToInitialDisplayTracker._(); factory TimeToInitialDisplayTracker( {FrameCallbackHandler? frameCallbackHandler}) { @@ -20,7 +20,7 @@ class TimeToInitialDisplayTracker { return _instance; } - TimeToInitialDisplayTracker._internal(); + TimeToInitialDisplayTracker._(); FrameCallbackHandler _frameCallbackHandler = DefaultFrameCallbackHandler(); bool _isManual = false;