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